---
title: Personalized Search & Navigation
slug: cms-12/docs/integrate-with-personalized-search-navigation
docTags: 
createdAt: 2026-08-10T15:55:53.000Z
---

Personalized Search & Navigation (formerly Personalized Find) is a personalization service you can add to Optimizely Search & Navigation as part of the Optimizely Personalization product suite.

:::hint{type="info"}
**Note**
Personalized Search & Navigation currently only works with Optimizely Commerce Connect.
:::

## Packages

In addition to `Episerver.Find`, these packages are needed for the extension:

- `EPiServer.Personalization.Commerce` – Integration for product recommendations.
- `EPiServer.Find.Personalization`  – Search & Navigation integration components.

## Extension components

Personalized Search & Navigation fetches attributes from a personalization service containing a list including property name, property value, and boosting. The list is included as “boosting” with the Search & Navigation service when searching or filtering content.

The boosting changes the generated “score” on content, to make the result personalized. Depending on the query and what attributes return from the personalization service, this functionality will have a different impact on the result.

## Configure

To enable retrieval of search attributes, configure the Attribute API. The required attributes are `episerver:personalization.Site` and `episerver:personalization.ClientToken`. See [Optimizely Product Recommendations](docId\:Ag3UEqh7rK98ddBWjIJJO) for information about configuring the Recommendations API.

## Retrieve attributes

The Recommendations API must fetch attributes before executing a personalized Search & Navigation query and store or cache them for fast retrieval *before* the user needs them so that latency is not adversely affected when a site visitor executes a query or filter. The following sections describe how to retrieve attributes.

### Call the Refresh method

Call the `Refresh` method on an instance of the `EPiServer.Find.Personalization.IPersonalizationClient`. To retrieve the active `EPiServer.Find.Personalization.IPersonalizationClient` instance, call the `Personalization()` extension on `EPiServer.Find.Personalization.IClient`.

```csharp
using EPiServer.Find.Personalization;

namespace OptimizelySite.Controllers {
  public class MyController: PageController < MyPage > {
    private readonly IClient _client;
    public MyController(IClient client) {
      _client = client;
    }
    public ActionResult Index(MyPage currentPage) {
      System.Threading.Tasks.Task.Factory.StartNew(currentContext => {
          System.Web.HttpContext.Current = (HttpContext) currentContext;
          _client.Personalization().Refresh();
        },
        HttpContext.Current.ApplicationInstance.Context);
      return View(currentPage);
    }
  }
}
```

### Decorate a controller with RefreshPreferences

Decorate a controller with an `EPiServer.Find.Personalization.RefreshPreferences` attribute to get attributes from the API when it executes any action method on the controller. To avoid unnecessary refreshing, check the `IPreferenceRepository` for attributes first, or use the controller to track if a refresh was already done for the visitor.

```csharp
namespace OptimizelySite2.Controllers {
  [EPiServer.Find.Personalization.RefreshPreferences]
  public class MyController: PageController < MyPage > {
    public ActionResult Index(MyPage currentPage) {
      return View(currentPage);
    }
  }
}
```

The Recommendations API call is asynchronous, so it has little effect on the time it takes to execute the controller.

After retrieval, CMS stores the attributes in an `EPiServer.Find.Personalization.IPreferenceRepository`. The default implementation stores the attributes in the session and a cookie for fast retrieval during or between sessions no longer than two days apart.

If you want to override the default, implement `EPiServer.Find.Personalization.IPreferenceRepository` and change the implementation using the conventions during initialization: `IClient.Personalization().Conventions.PreferenceRepository`. The interface has two methods to implement, `Save` and `Load`, which are self-explanatory.

## Execute personalized search queries

To enable personalization in Optimizely Search & Navigation queries, add `.UsingPersonalization()` to the query on the search client. CMS scores the results according to attribute data.

**Example**

```csharp
var result = _client.Search<ProductContent>()
  .For("ferrari")
  .UsingPersonalization()
  .GetContentResult();
```

## Sample code

This section provides sample code for comparing search results that use `.UsingPersonalization()` against search results that do not. The examples use the Quicksilver Optimizely Commerce Connect sample site and the Personalized Search & Navigation APIs.

:::hint{type="info"}
**Note**
Recommendations and personalized search results currently only supports Optimizely Commerce Connect content.  Also, the `.UsingPersonalization()` syntax only boosts Optimizely Commerce Connect content properties.
:::

### Configure the example

1. Install the [Quicksilver](https://github.com/episerver/Quicksilver) sample site.
2. Install the NuGet packages `EPiServer.Personalization.Commerce` and `EPiServer.Find.Personalization`.
3. Add the following configuration from the `Optimizely.Personalization.Commerce` package to the `web.config` file (CMS 11) or `appsettings.json` (CMS 12) of the site.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/Sswkwpmoe3YD9oHKADRhR-20260722-164947.png "1-personalization-configuration.png")

4. Verify that the data tracking and recommendations work as expected.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/js9Lo0qFGY2uuq91uEnbo_0e0071b-2-recommendation.png)

5. Go to some product pages on the site so the personalization system can track and collect behavioral data.
6. Create a `PersonalizedFindPage` page type.

```csharp
namespace EPiServer.Reference.Commerce.Site.Features.Personalization.Models {
  [ContentType(DisplayName = "PersonalizedFindPage", GUID = "e7e5cea1-10cf-4e1a-ab36-1c922d997e28", Description = "")]
  public class PersonalizedFindPage: PageData {
    [CultureSpecific]
    [Display(
      Name = "Main body",
      Description = "The main body will be shown in the main content area of the page, using the XHTML-editor you can insert for example text, images and tables.",
      GroupName = SystemTabNames.Content,
      Order = 1)]
    public virtual XhtmlString MainBody {
      get;
      set;
    }
  }
}
```

7. Create a `PersonalizedFindPage` page view model.

```csharp
public class PersonalizedFindPageViewModel {
  public PersonalizedFindPage CurrentPage {
    get;
    set;
  }
  public bool UsePersonalization {
    get;
    set;
  }
  public IEnumerable < PreferenceAttributeData > CurrentPersonalizationAttributes {
    get;
    set;
  }
  public string SearchTerm {
    get;
    set;
  }
}
```

8. Create a `PersonalizedFindPage` controller.

```csharp
private readonly IClient _client;
private readonly UrlResolver _urlResolver;
public PersonalizedFindPageController(IClient client, UrlResolver urlResolver) {
  _client = client;
}
public ActionResult Index(PersonalizedFindPage currentPage, string q, bool ? usePersonalization = null) {
  var viewModel = new PersonalizedFindPageViewModel {
    CurrentPage = currentPage, SearchTerm = q, UsePersonalization = usePersonalization.HasValue && usePersonalization.Value
  };

  System.Threading.Tasks.Task.Factory.StartNew(currentContext => {
      System.Web.HttpContext.Current = (HttpContext) currentContext;
      _client.Personalization().Refresh();
    },
    System.Web.HttpContext.Current.ApplicationInstance.Context);

  var prefData = _client.Personalization().Conventions.PreferenceRepository.Load();

  if (prefData != null) {
    var attributes = prefData.Attributes;
    viewModel.CurrentPersonalizationAttributes = attributes;
  }
  return View(viewModel);
}
```

9. Create a view for displaying the attributes.

```csharp
@using EPiServer.Core
@using EPiServer.Web.Mvc.Html
@model EPiServer.Reference.Commerce.Site.Features.Personalization.ViewModels.PersonalizedFindPageViewModel</pre>
<div>Current attributes from Personalization:</div>
  <table class="table">
    <tr>
      <th>Attribute name</th>
      <th>Attribute value</th>
      <th>Boost factor</th>
    </tr>
    @if (Model.CurrentPersonalizationAttributes != null)
      {
        foreach (var attribute in Model.CurrentPersonalizationAttributes)
          {
            <tr>
              <td>@attribute.Name</td>
              <td>@attribute.Value</td>
              <td>@attribute.Score</td>
            </tr>
          }
      }
  </table>
```

The view result should look like this:

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/acmLALheXbaH6qujzwsp0_4cd583f-4-personalization-attributes.PNG)

Make sure the returned attributes are not empty.

### Compare the search results

This example visualizes the difference between search results with and without personalization.

:::hint{type="info"}
**Note**
Personalized Search & Navigation works only with free-text search queries, as described in [Search](docId\:ubMxc8DlIS7wjfw3n04xI).
:::

First, add `.UsingPersonalization()` to the search query.

```csharp
var resultsPersonalized = _client.Search()
  .For(q)
  .FilterForVisitor()
  .UsingPersonalization()
  .GetContentResult();
```

Then, add another search query without` .UsingPersonalization()`, for comparison.

```csharp
var resultsWithoutPersonalized  = _client.Search()
  .For(q)
  .FilterForVisitor()
  .GetContentResult();
```

Add more properties to the model to preview the result on the front-end.

```csharp
namespace EPiServer.Reference.Commerce.Site.Features.Personalization.ViewModels {
  public class PersonalizedFindPageViewModel {
    public PersonalizedFindPage CurrentPage {
      get;
      set;
    }
    public bool UsePersonalization {
      get;
      set;
    }
    public IEnumerable < PreferenceAttributeData > CurrentPersonalizationAttributes {
      get;
      set;
    }
    public IEnumerable < SearchResultModel > SearchResults {
      get;
      set;
    }
    public IEnumerable < SearchResultModel > PersonalizedSearchResults {
      get;
      set;
    }
    public string SearchTerm {
      get;
      set;
    }
  }

  public class SearchResultModel {
    public string Url {
      get;
      set;
    }
    public Uri Image {
      get;
      set;
    }
    public double Score {
      get;
      set;
    }
    public string Name {
      get;
      set;
    }
    public string Brand {
      get;
      set;
    }
    public IEnumerable < string > AvailableColors {
      get;
      set;
    }
  }
}
```

The controller:

```csharp
namespace EPiServer.Reference.Commerce.Site.Features.Personalization.Controllers {
  public class PersonalizedFindPageController: PageController < PersonalizedFindPage > {
    private readonly IClient _client;
    private readonly UrlResolver _urlResolver;
    public PersonalizedFindPageController(IClient client, UrlResolver urlResolver) {
      _client = client;
      _urlResolver = urlResolver;
    }
    public ActionResult Index(PersonalizedFindPage currentPage, string q, bool ? usePersonalization = null) {
      var viewModel = new PersonalizedFindPageViewModel {
        CurrentPage = currentPage, SearchTerm = q, UsePersonalization = usePersonalization.HasValue && usePersonalization.Value
      };

      System.Threading.Tasks.Task.Factory.StartNew(currentContext => {
          System.Web.HttpContext.Current = (HttpContext) currentContext;
          _client.Personalization().Refresh();
        },
        System.Web.HttpContext.Current.ApplicationInstance.Context);

      var prefData = _client.Personalization().Conventions.PreferenceRepository.Load();

      if (prefData != null) {
        var attributes = prefData.Attributes;
        viewModel.CurrentPersonalizationAttributes = attributes;
      }

      if (!string.IsNullOrEmpty(q)) {
        var resultsPersonalized = _client.Search < FashionProduct > ()
          .For(q)
          .FilterForVisitor()
          .Track()
          .UsingPersonalization()
          .GetContentResult();

        var resultsWithoutPersonalized = _client.Search < FashionProduct > ()
          .For(q)
          .FilterForVisitor()
          .Track()
          .GetContentResult();

        if (resultsWithoutPersonalized != null) {
          viewModel.SearchResults = GetSearchResultModels(resultsWithoutPersonalized);
        }
        if (resultsPersonalized != null) {
          viewModel.PersonalizedSearchResults = GetSearchResultModels(resultsPersonalized);
        }
      }
      return View(viewModel);
    }

    private IEnumerable < SearchResultModel > GetSearchResultModels(IContentResult < FashionProduct > contentResult) {
      return contentResult.Select(fashionProduct => new SearchResultModel {
        Name = fashionProduct.Name,
          Brand = fashionProduct.Brand,
          AvailableColors = fashionProduct.AvailableColors,
          Score = contentResult.SearchResult.Hits.First(x => x.Document.ContentLink == fashionProduct.ContentLink).Score.GetValueOrDefault(),
          Url = _urlResolver.GetUrl(fashionProduct.ContentLink),
      });
    }
  }
}
```

The view for displaying the attributes:

```csharp
@using EPiServer.Web.Mvc.Html
@model EPiServer.Reference.Commerce.Site.Features.Personalization.ViewModels.PersonalizedFindPageViewModel
<div>
  @Html.PropertyFor(m => m.CurrentPage.MainBody)
</div>

@using (Html.BeginForm("", "", FormMethod.Get, new { @class = "form-inline" }))
  {
    <div class="form-group">
      <input type="text" name="q" id="q" value="@Model.SearchTerm" class="form-control"  />
      <button type="submit" class="btn btn-primary">
        <span class="glyphicon glyphicon-search" aria-hidden="true"></span> Find!
      </button>
    </div>
  }

@if (!string.IsNullOrEmpty(Model.SearchTerm))
  {
    <h2>Your search for <b>@Model.SearchTerm</b> resulted in the following hits:</h2>
    <h3>With personalization</h3>
    <div class="row">
      <div class="col-lg-12">
        <div class="row">
          <div class="col-lg-5">Product</div>
          <div class="col-lg-2">Score</div>
        </div>    
        @foreach (var hit in Model.PersonalizedSearchResults)
          {
            <div class="row">
              <div class="col-lg-5"><a href="@hit.Url">@hit.Name</a></div>
              <div class="col-lg-2">@hit.Score</div>
            </div>
          }
      </div>
    </div>
              
    <h3>Without personalization</h3>
    <div class="row">
      <div class="col-lg-12">
        <div class="row">
          <div class="col-lg-5">Product</div>
          <div class="col-lg-2">Score</div>
        </div>  
        @foreach (var hit in Model.SearchResults)
          {
            <div class="row">
              <div class="col-lg-5">@hit.Name</div>
              <div class="col-lg-2">@hit.Score</div>
            </div>
          }
      </div>
    </div>
  }

<h3>Current attributes from Personalization:</h3>
<table class="table">
  <tr>
    <th>Attribute name</th>
    <th>Attribute value</th>
    <th>Boost factor</th>
  </tr>
  @if (Model.CurrentPersonalizationAttributes != null)
    {
      foreach (var attribute in Model.CurrentPersonalizationAttributes)
        {
          <tr>
            <td>@attribute.Name</td>
            <td>@attribute.Value</td>
            <td>@attribute.Score</td>
          </tr>
        }
    }
</table>
```

**Result**

The search query that uses `.UsingPersonalization` gives boosted results using visitor attributes and orders the search results differently.

![](https://api.archbee.com/api/optimize/BrbvVORKJ5iIeSMZye5kI/g4tBampotQHwCuI9llKxs_aa60d2b-6-results-comparison.png)
