Skip to content

jonasbergqvist/DemoAlloyWithGraph

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Creating search page in Alloy using Graph

alt text

Create an Alloy site with Optimizely Graph

Create a new Alloy site

Follow the following instructions to create a new Alloy example site: https://support.optimizely.com/hc/en-us/articles/14970775439757-Setting-Up-an-Alloy-Site-for-CMS-12

Install nuget package Optimizely.ContentGraph.Cms

dotnet package add Optimizely.ContentGraph.Cms --project DemoAlloyWithGraph.csproj

Configure Graph account

Add the following inside appsettings.json

  "Optimizely": {
    "ContentGraph": {
      "GatewayAddress": "https://latest.cg.optimizely.com",
      "AppKey": "",
      "Secret": "",
      "SingleKey": "",

      "BufferedIndexingGracePeriod": 10000,
      "PreventFieldCollision": true,
      "UseDateTimeGQLType": true
    }
  },

Set appKey, Secret, and SingleKey to the values you have in your Graph account

Register Graph with settings

Create a new folder named "Graph" under the project root, and add a new file in the folder named 'GraphStartup.cs' with the following content

using EPiServer.DependencyInjection;
using Optimizely.ContentGraph.Cms.Configuration;
using Optimizely.ContentGraph.Cms.Configuration.Internal;

namespace DemoAlloyWithGraph.Graph
{
    public class GraphStartup
    {
        public static void RegisterGraph(IServiceCollection services)
        {
            services.AddContentDeliveryApi();
            services.AddContentGraph(options =>
            {
                options.ExtractMedia = false;
            });

            services.ConfigureScheduleJob(
                checkStatusInterval: TimeSpan.FromSeconds(60),
                checkStatusSleepOnFailure: TimeSpan.FromSeconds(15),
                waitUntilFinalResult: false
            );

            services.Configure<EventIndexingOptions>(options =>
            {
                options.IndexReferencingContent = ReferencingPropertyTypes.Inlined;
                options.SyncContentsInParallelTaskAndForget = false; // Synchronous processing for better control
            });
        }
    }
}

Make sure this code is being run at startup, by adding the following code in the 'ConfigureServices' method inside 'Startup.cs':

GraphStartup.RegisterGraph(services);

Include base type

Create a new file under the "Graph" folder named 'GraphInitializableModule.cs' with the following content

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using Optimizely.ContentGraph.Cms.NetCore.ConventionsApi;
using EPiServer.ServiceLocation;
using DemoAlloyWithGraph.Models.Pages;

[ModuleDependency]
public class GraphInitializableModule : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var conventionRepository = context.Locate.Advanced.GetInstance<ConventionRepository>();
        RegisterConventions(conventionRepository);
    }

    public void Uninitialize(InitializationEngine context)
    {
    }

    private static void RegisterConventions(ConventionRepository conventionRepository)
    {
        conventionRepository.IncludeAbstract<ContentData>();
    }
}

Add extension methods for search functionality

Create a new file under the "Graph" folder named 'ContentExtensions.cs' with the following content

public static class ContentExtensions
{
    public static string SearchTitle(this ContentData contentData)
    {
        if (contentData is SitePageData sitePageData && !string.IsNullOrWhiteSpace(sitePageData.MetaTitle))
        {
            return sitePageData.MetaTitle;
        }

        if (contentData is IContent content)
        {
            return content.Name;
        }

        return null;
    }

    public static string SearchDescription(this ContentData contentData)
    {
        if (contentData is StandardPage standardPage && !string.IsNullOrWhiteSpace(standardPage.MainBody?.ToString()))
        {
            return standardPage.MainBody.ToString();
        }

        if (contentData is SitePageData sitePageData)
        {
            if (!string.IsNullOrWhiteSpace(sitePageData.TeaserText))
            {
                return sitePageData.TeaserText;
            }
            
            if (!string.IsNullOrWhiteSpace(sitePageData.MetaDescription))
            {
                return sitePageData.MetaDescription;
            }
        }

        return null;
    }

    public static List<string> SearchKeywords(this ContentData contentData)
    {
        if (contentData is SitePageData sitePageData && sitePageData.MetaKeywords != null)
        {
            return [.. sitePageData.MetaKeywords];
        }

        return [];
    }

    public static List<string> SearchCategories(this ContentData contentData)
    {
        var categoryRepository = ServiceLocator.Current.GetInstance<CategoryRepository>();

        if (contentData is SitePageData sitePageData && sitePageData.Category != null)
        {
            return sitePageData.Category.Select(x => categoryRepository?.Get(x)?.Name)?.ToList();
        }

        return [];
    }
}

Add the extensions to Graph, by uppdating the RegisterConventions method in GraphInitializableModule.cs

using EPiServer.Framework;
using EPiServer.Framework.Initialization;
using Optimizely.ContentGraph.Cms.NetCore.ConventionsApi;
using EPiServer.ServiceLocation;
using DemoAlloyWithGraph.Models.Pages;

[ModuleDependency]
public class GraphInitializableModule : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var conventionRepository = context.Locate.Advanced.GetInstance<ConventionRepository>();
        RegisterConventions(conventionRepository);
    }

    public void Uninitialize(InitializationEngine context)
    {
    }

    private static void RegisterConventions(ConventionRepository conventionRepository)
    {
        conventionRepository.IncludeAbstract<ContentData>();

        conventionRepository.ForInstancesOf<ContentData>()
            .IncludeField(x => x.SearchTitle()).Set(x => x.SearchTitle(), IndexingType.Searchable)
            .IncludeField(x => x.SearchDescription()).Set(x => x.SearchDescription(), IndexingType.Searchable)
            .IncludeField(x => x.SearchKeywords()).Set(x => x.SearchKeywords(), IndexingType.Searchable)
            .IncludeField(x => x.SearchCategories()).Set(x => x.SearchCategories(), IndexingType.Searchable);
    }
}

Create GraphQL query with normal relevance ranking

Create GraphQL query with searchPhrase parameter

query SearchQuery(
  $searchPhrase:String,
) {
  ContentData(
    where: {
      _fulltext: { match: $searchPhrase }
    }
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
  }
}

Add facets with filters

query SearchQuery(
  $searchPhrase:String,
  $searchKeywords: [String!],
  $searchCategories: [String!]
) {
  ContentData(
    where: {
      _fulltext: { match: $searchPhrase }
    }
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
    facets {
      SearchKeywords(filters:$searchKeywords) {
        name
        count
      }
      SearchCategories(filters:$searchCategories) {
        name
        count
      }
    }
  }
}

Add locales

query SearchQuery(
  $locales: [Locales], 
  $searchPhrase:String,
  $searchKeywords: [String!],
  $searchCategories: [String!]
) {
  ContentData(
    locale: $locales
    where: {
      _fulltext: { match: $searchPhrase }
    }
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
    facets {
      SearchKeywords(filters:$searchKeywords) {
        name
        count
      }
      SearchCategories(filters:$searchCategories) {
        name
        count
      }
    }
  }
}

Order by relevance

query SearchQuery(
  $locales: [Locales], 
  $searchPhrase:String,
  $searchKeywords: [String!],
  $searchCategories: [String!]
) {
  ContentData(
    locale: $locales
    where: {
      _fulltext: { match: $searchPhrase }
    }
    orderBy: { _ranking: RELEVANCE }
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
    facets {
      SearchKeywords(filters:$searchKeywords) {
        name
        count
      }
      SearchCategories(filters:$searchCategories) {
        name
        count
      }
    }
  }
}

Creating search page using Claude, Github Copilot, or similar

Update search page to use Graph with dummy query

Give the LLM the following instructions:

I would like to use GraphQL Codegen with the GraphQL endpoint 'https://latest.cg.optimizely.com/content/v2?auth=Jjn2D2rGWZsu20EFvQ2ftf1x3GXcIBpnsLtxxDbyqPrKFYu3' inside the vew Index.cshtml

I would like to use ReactJs, to do client side rendering inside the view.

I would like to use GraphQL Codegen with preset: 'client'

I would like you to create a simple GraphQL query for verification using the following query: 'query TotalItems { Data { total } }'

I would like you to test that the page is working after implementation, by starting the site using dotnet run, and then checking the response on url localhost:5000/en/search

Change the search page to be a real search page

I would like you to update the SearchPage.tsx to use the following query instead:

query SearchQuery(
  $locales: [Locales], 
  $searchPhrase:String,
  $searchKeywords: [String!],
  $searchCategories: [String!]
) {
  ContentData(
    locale: $locales
    where: {
      _fulltext: { match: $searchPhrase }
    }
    orderBy: { _ranking: RELEVANCE }
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
    facets {
      SearchKeywords(filters:$searchKeywords) {
        name
        count
      }
      SearchCategories(filters:$searchCategories) {
        name
        count
      }
    }
  }
}

Make sure to add the query inside the queries folder, and auto generate the result using graphql codegen.

I would like you to test that the page is working after implementation by checking the response on url localhost:5000/en/search

Improve the page a bit

The whole search-app-root is now being updated every time a change is being made by the user in SearchPage.tsx, for example searching for a phrase or selecting a facet value.

I would like you to update the logic, to only re-render the search result, and facets when searching on a phrase or selecting/deselecting a facet value. This will make the page feel much more responsive.

The main div in SearchPage/Index.cshtml is being collapsed when the user interacts, which makes the page blip when searching with a search phrase, selecting facet values, or deselecting facet values.

Update query to use semantic ranking and boosting

Change ranking to semantic

query SearchQuery(
  $locales: [Locales], 
  $searchPhrase:String,
  $searchKeywords: [String!],
  $searchCategories: [String!]
) {
  ContentData(
    locale: $locales
    where: {
      _fulltext: { match: $searchPhrase }
    }
    orderBy: { _ranking: SEMANTIC, _semanticWeight: 0.5}
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
    facets {
      SearchKeywords(filters:$searchKeywords) {
        name
        count
      }
      SearchCategories(filters:$searchCategories) {
        name
        count
      }
    }
  }
}

Add boosting

query SearchQuery(
  $locales: [Locales], 
  $searchPhrase:String,
  $searchKeywords: [String!],
  $searchCategories: [String!]
) {
  ContentData(
    locale: $locales
    where: {
      _or: [
        { _fulltext: { match: $searchPhrase } },
        { SearchTitle: {match: $searchPhrase, boost: 5} }
        { SearchDescription: {match: $searchPhrase, boost: 3} }
      ]
    }
    orderBy: { _ranking: SEMANTIC }
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
    facets {
      SearchKeywords(filters:$searchKeywords) {
        name
        count
      }
      SearchCategories(filters:$searchCategories) {
        name
        count
      }
    }
  }
}

Update the search page with the new query

I would like you to update the SearchQuery to be:

query SearchQuery(
  $locales: [Locales], 
  $searchPhrase:String,
  $searchKeywords: [String!],
  $searchCategories: [String!]
) {
  ContentData(
    locale: $locales
    where: {
      _or: [
        { _fulltext: { match: $searchPhrase } },
        { SearchTitle: {match: $searchPhrase, boost: 5} }
        { SearchDescription: {match: $searchPhrase, boost: 3} }
      ]
    }
    orderBy: { _ranking: SEMANTIC }
  ) {
    items {
      SearchTitle
      SearchDescription(highlight: {enabled:true})
      RelativePath
      _score
    }
    facets {
      SearchKeywords(filters:$searchKeywords) {
        name
        count
      }
      SearchCategories(filters:$searchCategories) {
        name
        count
      }
    }
  }
}

Run the GraphQL codegen tool to update any query response. I would also like you to test that the page is working after implementation by checking the response on url localhost:5000/en/search

Update search page to proxy Graph request through MVC Controller

I would like the SearchPage.tsx to make a request to the SearchController with the query, instead of making the request directly to https://latest.cg.optimizely.com/content/v2?auth=Jjn2D2rGWZsu20EFvQ2ftf1x3GXcIBpnsLtxxDbyqPrKFYu3. I would then like the Controller to make the request to https://latest.cg.optimizely.com/content/v2?auth=Jjn2D2rGWZsu20EFvQ2ftf1x3GXcIBpnsLtxxDbyqPrKFYu3 and return the result to the SearchPage.tsx.

Make sure the page is returning a 200 response, and result when searching on phrase Alloy.

About

No description, website, or topics provided.

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors