🌐 Website Implementation
Search Navigation
📖 Docs

Search & Navigation - Optimizely CMS 12

Exam Area: Content Area 3 – Website Implementation & Delivery (25%)
Source: https://academy.optimizely.com/student/path/3128969/activity/4970325
Published: Feb 24, 2026


Overview (Academy Key Points)

  • Architectural Choice: Search & Navigation for .NET-heavy sites, or Optimizely Graph for decoupled, SaaS-first delivery.
  • Sync Strategy: Combine real-time IContentEvents for immediate updates with scheduled jobs for full-tree integrity.
  • Precision Control: Use the [Searchable] attribute and Conventions API to eliminate index bloat.
  • Security Default: Enforce FilterForVisitor() and IExcludeFromSearch to ensure data privacy.

1. Indexing Engine Architecture (Academy – 4970325)

Optimizely Search & Navigation (EPiServer.Find):

Optimizely Graph:


2. Search & Navigation – Setup (EPiServer.Find)

Installation:

dotnet add package EPiServer.Find
dotnet add package EPiServer.Find.Cms

Configuration:

{
  "EPiServer": {
    "Find": {
      "ServiceUrl": "https://search-api.episerver.net/path/",
      "DefaultIndex": "mysite_dev"
    }
  }
}

3. Indexing Content

// Automatic indexing via IContentEvents
[InitializableModule]
public class SearchIndexingModule : IInitializableModule
{
    private IContentEvents _contentEvents;
    private IClient _searchClient;

    public void Initialize(InitializationEngine context)
    {
        _contentEvents = context.Locate.Advanced.GetInstance<IContentEvents>();
        _searchClient = context.Locate.Advanced.GetInstance<IClient>();

        _contentEvents.PublishedContent += OnPublished;
        _contentEvents.DeletedContent += OnDeleted;
    }

    private void OnPublished(object sender, ContentEventArgs e)
    {
        _searchClient.Index(e.Content);
    }

    private void OnDeleted(object sender, DeleteContentEventArgs e)
    {
        _searchClient.Delete<IContent>(x => x.ContentLink.ID.Match(e.ContentLink.ID));
    }
}

4. Searching Content

using EPiServer.Find;
using EPiServer.Find.Cms;

public class SearchService
{
    private readonly IClient _searchClient;

    public SearchService(IClient searchClient)
    {
        _searchClient = searchClient;
    }

    public SearchResults<IContent> Search(string query, int page = 1, int pageSize = 10)
    {
        return _searchClient.Search<IContent>()
            .For(query)
            .FilterForVisitor()          // Security: only return content the user can access
            .ExcludeDeleted()
            .Skip((page - 1) * pageSize)
            .Take(pageSize)
            .GetResult();
    }
}

5. [Searchable] Attribute – Index Control

public class ArticlePage : PageData
{
    // Included in full-text index (default)
    [Searchable]
    public virtual string Heading { get; set; }

    // Excluded from full-text index
    [Searchable(false)]
    public virtual string InternalNotes { get; set; }

    // Searchable but with boosted weight
    [Searchable]
    [Display(Order = 10)]
    public virtual XhtmlString MainBody { get; set; }
}

6. Conventions API – Exclude Content Types

// Exclude entire content types or properties via Conventions API
[InitializableModule]
public class SearchConventionsModule : IInitializableModule
{
    public void Initialize(InitializationEngine context)
    {
        var client = context.Locate.Advanced.GetInstance<IClient>();

        // Exclude entire content type
        client.Conventions.ForInstancesOf<PrivatePage>()
            .ExcludeFromIndex();

        // Include block content inside page index
        client.Conventions.ForInstancesOf<ArticlePage>()
            .IncludeField(x => x.MainBody);
    }
}

7. FilterForVisitor() – Security

// ALWAYS call FilterForVisitor() in search queries
// Ensures only content the current user has Read access to is returned
var results = _searchClient.Search<PageData>()
    .For(query)
    .FilterForVisitor()   // ← Required for access control
    .GetResult();

8. Best Bets

// Best Bets: promote specific results for certain search terms
// Configured in CMS Admin → Search & Navigation → Best Bets
// No code required — managed through the UI

// To retrieve best bets programmatically:
var bestBets = _searchClient.Search<PageData>()
    .For(query)
    .ApplyBestBets()
    .FilterForVisitor()
    .GetResult();

Review Questions

  1. How do Search & Navigation and Optimizely Graph differ? (Find: .NET-heavy, Elasticsearch-based; Graph: SaaS GraphQL, decoupled, modern)
  2. Which NuGet packages are needed to install Search & Navigation? (EPiServer.Find and EPiServer.Find.Cms)
  3. What does [Searchable(false)] do? (Excludes a specific property from the full-text index)
  4. What is the Conventions API used for? (Exclude entire content types from the index)
  5. Why must FilterForVisitor() be used? (Security – ensures only content the user has permission to view is returned)
  6. How does event-driven indexing work? (Subscribe to IContentEvents – when content is published or deleted, a delta index update is automatically triggered)
  7. When is the IndexInContentAreas attribute used? (When you want to include block content in the parent page's search index)