Include templates dynamically based on Sitecore field values in your SOLR index

In one of our projects, we had the need to create a separate index only for items of a certain set of templates – and the content admin needed to have the flexibility to edit this list of templates.

If this was only a question of including / excluding templates in the search, it would be a much better solution to just allow the content author to select a set of templates on a multilist field on a settings item some place.
However, from a performance perspective – this was the solution we definitely needed considering our content structure, and content editing process.

Here’s how we did it –

We added a field on /sitecore/templates/System/Templates/Template called ‘Exclude From Search Index’
We created a custom crawler then, to check this field on the item template, before adding it to the index. Do ensure you use this crawler only on non-sitecore system indexes to avoid Sitecore responding in an unexpected way.

using Foundation.Search.Models.sitecore.templates.Foundation.Search;
using Sitecore.ContentSearch;
using Sitecore.Data.Items;

namespace Foundation.Search.Solr.Crawlers
{
    public class SearchIndexCrawler : SitecoreItemCrawler
    {
        protected override bool IsExcludedFromIndex(SitecoreIndexableItem indexable, bool checkLocation = false)
        {
            bool isExcluded = base.IsExcludedFromIndex(indexable, checkLocation);

            if (isExcluded)
                return true;

            Item item = indexable;
            return item.Template.InnerItem[I_Indexing_SettingsConstants.Exclude_From_Search_IndexFieldName] == "1";
        }

        protected override bool IndexUpdateNeedDelete(SitecoreIndexableItem indexable)
        {
            Item item = indexable;
            return item.Template.InnerItem[I_Indexing_SettingsConstants.Exclude_From_Search_IndexFieldName] == "1";
        }
    }
}

You can then use this crawler in your site index:



web
/sitecore


Advertisements

Adding search facets on computed fields in Sitecore 9 with SOLR

Adding facets to help content authors in searching content is always a great idea, especially when you have rather large buckets with loads of items.

One thing I noticed while trying to set this up, was that I needed untokenized fields in the index to make this work as expected, while text fields by default are tokenized in solr.

To understand this better, lets take an example of a bucket of products – say jewelry. I have a field called Jewelry Type on my products. If I wanted to give the content author an option to find jewelry by this field, I could add a facet on the existing field.

I would expect to see facet values like these:

1

But instead, I would see separate facets for each word, like ‘Engagement’, ‘Rings’, ‘Right’, ‘Hand’ etc. That’s certainly not what we need.

So to do that – instead of changing your solr settings to treat all fields of the certain type as untokenized (which would open a different can of worms!), we created a computed field to populate the Jewelry Type values into an untokenized field. This would treat the whole field value as a complete string and not break it down.

This is a relatively straightforward computed field:

using Foundation.Search.Solr.Helpers;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;

namespace Foundation.Search.Solr.ComputedFields
{
	public class JewelryType : IComputedIndexField
	{
		public object ComputeFieldValue(IIndexable indexable)
		{
			SitecoreIndexableItem sitecoreIndexable = indexable as SitecoreIndexableItem;

			if (sitecoreIndexable == null) return null;

			if (sitecoreIndexable.Item.TemplateID.ToString() == Constants.ProductTemplateId)
			{
				return sitecoreIndexable.Item["JewelryType"];
			}

			return null;
		}

		public string FieldName { get; set; }
		public string ReturnType { get; set; }
	}
}

Now we set up the facet with this new computed field created: /sitecore/system/Settings/Buckets/Facets

Using the template: /sitecore/templates/System/Item Buckets/Facet

Where the field name – matches the name of the computed field you created.

2

You could choose to mark this facet as global or local.
If your facet is very specific to a particular bucketed item template, you could as well mark it as local, and then add it on the particular bucket item only.

3

 

Searching on module content on pages in Sitecore

When developing search implementation for a Sitecore site, we usually do an index search on the internal _content field which is an accumulation of all the string fields on the current item. Often the pages have a lot of important data on module datasources as opposed to be on the context item itself. We also have reference fields on the context item which might warrant updating the search results based on the business importance of such data.

To ensure that your search functionality also takes into account this additional data while retrieving results, we added a couple of computed fields to account for this data.

  • ModuleContent – This computed field accesses the presentation of an item and finds the renderings with datasources on them. It then adds all the string content on these datasource items to the computed field.
  • ReferenceContent – This computed field accesses all the fields on the context item which are reference fields and iterates through the referenced items to add into the index, the string fields of all these referenced items.
  • Now, while we can create new computed fields, we could also append to _content, which will let the search code unchanged. Writing to the _content field from the computed index field code simply appends the data into the existing field, thus extending it.

    public class ModuleContent : IComputedIndexField
        {
            public string FieldName { get; set; }
            public string ReturnType { get; set; }
    
            public object ComputeFieldValue(IIndexable indexable)
            {
                Item obj = indexable as SitecoreIndexableItem;
                if (obj?.Template == null)
                    return null;
    
                if (obj.Paths.IsContentItem)
                {
                    try
                    {
                        LayoutDefinition layout = LayoutDefinition.Parse(LayoutField.GetFieldValue(obj.Fields["__Renderings"]));
    
                        if (layout.Devices != null && layout.Devices.Count > 0)
                        {
                            DeviceDefinition dev = (DeviceDefinition)layout.Devices[0];
                            ArrayList renderings = dev?.Renderings;
                            if (renderings != null)
                            {
                                IEnumerable<RenderingDefinition> renderingDefinitions = renderings.Cast<RenderingDefinition>();
    
                                foreach (RenderingDefinition renderingDefinition in renderingDefinitions)
                                {
                                    if (renderingDefinition.ItemID != null)
                                    {
                                        if (!string.IsNullOrWhiteSpace(renderingDefinition.Datasource))
                                        {
                                            Item datasource = SitecoreHelper.GetItemByID(renderingDefinition.Datasource);
    
                                            if (datasource != null)
                                            {
                                                return datasource.Fields.Where(f => !f.Name.StartsWith("__")
                                                    && f.Name != IBase_Rendering_AssetsConstants.Rendering_ScriptsFieldName
                                                    && f.Name != IBase_Rendering_AssetsConstants.Rendering_StylesheetsFieldName
                                                    && (f.Type == "Single-Line Text" || f.Type == "Multi-Line Text" || f.Type == "Rich Text"))
                                                    .Select(f => datasource.Fields[f.ID]?.Value).Where(v => !string.IsNullOrWhiteSpace(v));
                                            }
                                        }
                                    }
                                }
                            }
                        }
    
                        return new List<string>();
                    }
                    catch (Exception ex)
                    {
                        Log.Error("Site.Project._classes.Computed_Fields.ModuleContent", ex, this);
                    }
                }
    
                return string.Empty;
            }
        }
    

    In the configuration, we include the computed field:

    <configuration xmlns:patch="http://www.sitecore.net/xmlconfig/">
      <sitecore>
        <contentSearch>
          <indexConfigurations>
            <defaultLuceneIndexConfiguration type="Sitecore.ContentSearch.LuceneProvider.LuceneIndexConfiguration, Sitecore.ContentSearch.LuceneProvider">
              <fields hint="raw:AddComputedIndexField">
                <field fieldName="_content" returnType="stringCollection" storageType="YES">
                  Site.Domain._classes.Computed_Fields.ModuleContent,Site.Domain
                </field>
              </fields>
            </defaultLuceneIndexConfiguration>
          </indexConfigurations>
        </contentSearch>
      </sitecore>
    </configuration>
    

    In a similar manner, we could include the reference item string fields, except we would filter by link / list field tyes like Multilist / Treelist / Droptree / Droplink etc in the code, to get the guids, then items and then string fields on them, to include in the index for the current item.

    When time comes for the search, we would simply do a free text index search on the _content field to get results.

    Hope this helps!

Faceting on product specifications with SOLR computed fields in Sitecore

In most product repositories, we come across product specification sets for each product, which usually comprise of a key value pair.
2015-09-20_015800
We had one such scenario in our project, and we came across the requirement to facet on various specification keys.

We have currently added a list of comma separated (key, value) pairs in the specifications computed field. The computed field needed to be declared as a string collection:

<field fieldName="productspecificationstext" returnType="stringCollection" storageType="YES" indexType="string">
    mysite.ComputedFields.ProductSpecificationsText,mysite
</field>

And in the code for the computed field (implementing IComputedIndexField), you will want to split this list and return it as a List.
So this

public object ComputeFieldValue(IIndexable indexable)

would return List<string>

Here’s a sample from the index:

2015-09-20_020439

So then we facet on this field, and filter out the facets that start with (specification key + ‘,’) for the specification key we want the facets for and remove the preceding key and ‘,’.
So if I were to facet on size specification for the below data (provided we had a separator between the specification key and value), we would get:

2015-09-20_021329

Large (1)
Small (3)
Medium (1)

This way, we use the same computed field to facet on any kind of specification.

The method used would be the Sitecore.ContentSearch method:

2015-09-20_021622