Skip to content

Latest commit

 

History

2 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

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

Packages

Contributors

Languages