Navigation and service panel


Content

ContentSearch instead of Sitecore queries - preserving the Content Tree order

By Marta Imos-Merska on 7. July 2016, No comments

Lately I've been working on a Sitecore 8.1 project, where we decided to use the Sitecore ContentSearch API instead of Sitecore queries to retrieve items. When starting with this approach, something you notice at the very beginning is that the order of the retrieved items needs some customization.

When we use ContentSearch in place of Sitecore queries, most of the time we'd like to retrieve the items in the exact same order as we would have using the query. That means we want to preserve the order of the items that we can see in the Content Tree. If we don't apply any sorting to the ContentSearch result, the order of the retrieved items will be the order in which documents were added to the index.

Sortorder

The order in the Content Tree is calculated based on the value of the Sortorder field and then the item name. Unfortunately, both Lucene and Solr default configurations exclude the Sortorder field from indexing. Since we don't want to modify the default configs, we need to add a new computed field to the index to mimic the Sitecore query sorting.

The example below uses Solr, however you'll encounter the same behavior with Lucene, and the same solution can be applied there as well. The example retrieves items for, let's say, a navigation. It takes the result of the ContentSearch query, gets the items, and outputs them directly to the front-end without any further processing.

To add the Sortorder field to index, we need to add the following:

Computed index field class:

using Sitecore.Configuration;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data.Items;

public class SortOrderComputedField : IComputedIndexField
{
    /// <summary>
    /// The value that Content Tree assumes for non-empty, non-integer string values of Sortorder.
    /// </summary>
    private const int DefaultSortOrderForStringValues = 0;

    /// <summary>
    /// The name of the setting with default value to replace empty Sortorder value.
    /// </summary>
    private const string DefaultSortOrderSettingName = "DefaultSortOrderValue";

    public object ComputeFieldValue(IIndexable indexable)
    {
        var item = (Item)(indexable as SitecoreIndexableItem);
        if (item == null) return null;

        if (string.IsNullOrEmpty(item[Sitecore.FieldIDs.Sortorder]))
        {
            return Settings.GetIntSetting(DefaultSortOrderSettingName, 0);
        }

        int sortOrder;
        return int.TryParse(item[Sitecore.FieldIDs.Sortorder], out sortOrder)
            ? sortOrder
            : DefaultSortOrderForStringValues;
    }

    public string FieldName { get; set; }

    public string ReturnType { get; set; }
}

Entry in config:

<contentSearch>

    <indexConfigurations>
        <defaultSolrIndexConfiguration>          
            <fields hint="raw:AddComputedIndexField">
                <field fieldName="sortorder" returnType="int">MyTest.Models.Fields.SortOrderComputedField, MyTest</field>
            </fields>
        </defaultSolrIndexConfiguration>
    </indexConfigurations>

</contentSearch>

Extension to SearchResultItem:

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;

public class SearchResultItemBase : SearchResultItem
{
    [IndexField("sortorder")]
    public int SortOrder { get; set; }
}

Now we can perform our sorting, with the last two lines below:

using (var context = ContentSearchManager.CreateSearchContext((SitecoreIndexableItem) homeItem.InnerItem))
{
    var navigationSearchResultItems = context.GetQueryable<SearchResultItemBase>()
        .Filter(resultItem => resultItem.Language == homeItem.Language)
        .Filter(resultItem => resultItem.Path.StartsWith(fullParentPath))
        .Filter(resultItem => resultItem.TemplateId == navigationItemTemplateId)
        .OrderBy(x => x.SortOrder)
        .ThenBy(x => x.Name);
}

Item Name

Now, it turns out that's not all. In the example above we sort by Sortorder and then by the Name property provided by Sitecore.ContentSearch.SearchTypes.SearchResultItem. But the latter maps to an index field that is tokenized. Let's take a look at the following items:

Image Text


In this example, the Egypt item is set to be first. The Algeria and South Africa items don't have Sortorder set, so they are sorted by item name, which puts Algeria first. But if we take a look on what our ContentSearch query provides, we'll see this:

Image Text


Since the item name is tokenized in the index, South Africa gets divided into two tokens South and Africa, which puts Africa before Algeria. So what we need is an untokenized field for the item name in the index:

Config:

<field fieldName="itemnamelowercase" stored="true" indexed="true" indexType="untokenized" returnType="string">
    MyTest.Models.Fields.ItemNameLowerCaseComputedField , MyTest
</field>

Computed field class:

using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data.Items;

public class ItemNameLowerCaseComputedField : IComputedIndexField
{
    public object ComputeFieldValue(IIndexable indexable)
    {
        var item = (Item)(indexable as SitecoreIndexableItem);
        return item?.Name.ToLowerInvariant();
    }

    public string FieldName { get; set; }

    public string ReturnType { get; set; }
}

Corresponding field in SearchResultItemBase:

public class SearchResultItemBase : SearchResultItem
{
    [IndexField("itemnamelowercase")]
    public virtual string ItemNameLowerCase { get; set; }

    [IndexField("sortorder")]
    public int SortOrder { get; set; }
}

After the modification of our search query:

using (var context = ContentSearchManager.CreateSearchContext((SitecoreIndexableItem) homeItem.InnerItem))
{
    var navigationSearchResultItems = context.GetQueryable<SearchResultItemBase>()
        .Filter(resultItem => resultItem.Language == homeItem.Language)
        .Filter(resultItem => resultItem.Path.StartsWith(fullParentPath))
        .Filter(resultItem => resultItem.TemplateId == navigationItemTemplateId)
        .OrderBy(x => x.SortOrder)
        .ThenBy(x => x.ItemNameLowerCase);
}

we'll receive the expected result:

Image Text


Lowercase Strings Sorting

Another important thing here is to use lowercase values, because the ContentSearch API's OrderBy() sorts all uppercase strings first. So without applying the transformation to lowercase in the following situation:

Image Text


we'd receive:

Image Text


Note: Bug in Sitecore Solr Search Provider

There's a bug in Sitecore search provider for Solr, which reverses the order in which sorting is applied, if you use multiple fields sorting (it executes ThenBy() before OrderBy()). You can find more info about this bug in this blog post. So, in order to make the above solution work with Solr, you'll still need to apply a Sitecore patch.

There's no such bug in Lucene provider.


For more information about computed index fields or Solr config, you can refer to following blog posts:

No comments

Add your comment

Your email address will not be published. Required fields are marked *

*