Similar to the answer provided by @vandsh, but with less dependencies I created this gist, which is outlined below.
Note: This method assumes all datasources are referenced by ID
. It relies on the links database to find linked items, which would fail if relying on a query.
Define Which Renderings are "indexable"
This avoids indexing renderings such as Navigation or Related Articles, which really aren't pertinent to the actual page content.
You'll want to update this list as you build your module library with relevant text-heavy renderings.
Config Patch
<configuration>
<sitecore>
<indexedRenderings>
<indexedRendering name="RichText" id="{4F8942FF-C4E4-408A-B3D5-4657EE5050CA}" />
<indexedRendering name="EmployeeBio" id="{53F8166B-6627-4C1B-BED5-FBD0B36EC527}" />
<indexedRendering name="HighlightStatic" id="{75100A87-B7B0-4D1F-BE85-1A8FC6342514}" />
<indexedRendering name="IntroText" id="{89D7EE42-A5E1-4F24-B247-BFD382E501D4}" />
</indexedRenderings>
</sitecore>
</configuration>
Class To Read Config
using System.Collections.Generic;
using System.Linq;
using System.Xml;
using Sitecore.Configuration;
using Sitecore.Data;
using Sitecore.Xml;
namespace Common.Sitecore.ConfigElements
{
public class IndexedRenderings
{
public string Name { get; set; }
public ID Id { get; set; }
public static List<IndexedRenderings> IndexableRenderings()
{
return (from XmlNode node in Factory.GetConfigNodes("indexedRenderings/indexedRendering")
select new IndexedRenderings
{
Name = XmlUtil.GetAttribute("name", node),
Id = new ID(XmlUtil.GetAttribute("id", node))
}).ToList();
}
}
}
Define Custom RenderedContent Field
This field will contain the filtered text of all datasources (allowed by config section) added to the page item. The contents is stored in the index as renderedContent
. It checks both the Layout
and FinalLayout
of the item for renderings that are marked as allowed, then iterates on the fields of the datasource, ultimately piping everything to the same renderedContent
string.
Note: You probably don't want to use this field for display on a search results page, for example, but can still be leveraged to provide relevant results.
Config Patch
<configuration>
<sitecore>
<contentSearch>
<indexConfigurations>
<defaultLuceneIndexConfiguration> <!-- swap for your configuration, e.g. SOLR!!! -->
<documentOptions>
<fields hint="raw:AddComputedIndexField">
<field fieldName="renderedContent" storageType="no" indexType="tokenized">Common.Sitecore.Search.ComputedFields.RenderedContent,Common</field>
</fields>
</documentOptions>
</defaultLuceneIndexConfiguration>
</indexConfigurations>
</contentSearch>
</sitecore>
</configuration>
Custom Field Class
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using Sitecore;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.ComputedFields;
using Sitecore.Data;
using Sitecore.Data.Fields;
using Sitecore.Data.Items;
using Sitecore.WordOCX.HtmlDocument;
using Common.Sitecore.ConfigElements;
namespace Common.Sitecore.Search.ComputedFields
{
public class RenderedContent : AbstractComputedIndexField
{
private IEnumerable<ID> _indexableRenderings;
public override object ComputeFieldValue(IIndexable indexable)
{
var item = (Item)(indexable as SitecoreIndexableItem);
if (item == null)
return null;
_indexableRenderings = IndexedRenderings.IndexableRenderings().Select(i => i.Id);
if (!_indexableRenderings.Any())
return null;
var deviceItem = item.Database.Resources.Devices.GetAll().FirstOrDefault(d => d.IsDefault);
if (deviceItem == null)
return null;
var itemDatasources = ExtractRenderingDataSourceItems(item, _indexableRenderings, item.Fields[FieldIDs.FinalLayoutField], deviceItem).ToList(); // for later `Distinct()`
itemDatasources.AddRange(ExtractRenderingDataSourceItems(item, _indexableRenderings, item.Fields[FieldIDs.LayoutField], deviceItem));
itemDatasources = itemDatasources.Distinct().ToList();
if (!itemDatasources.Any())
return null;
var content = new List<string>();
foreach (var itemDatasource in itemDatasources)
GetItemContent(itemDatasource, content);
if (!content.Any())
return null;
var aggregateContent = content.Aggregate((a, b) => $"{a} {b}");
aggregateContent = StripLineBreaksAndWhiteSpace(aggregateContent);
return aggregateContent;
}
private static IEnumerable<Item> ExtractRenderingDataSourceItems(Item item, IEnumerable<ID> indexableRenderings, Field layoutField, DeviceItem deviceItem)
{
if (layoutField == null)
return Enumerable.Empty<Item>();
var layoutXml = LayoutField.GetFieldValue(layoutField);
if (string.IsNullOrEmpty(layoutXml))
return Enumerable.Empty<Item>();
var renderings = item.Visualization.GetRenderings(deviceItem, false);
if (renderings == null || renderings.Length == 0)
return Enumerable.Empty<Item>();
var datasourceItems = new List<Item>();
for (var renderingIndex = renderings.Length - 1; renderingIndex >= 0; renderingIndex--)
{
var rendering = renderings[renderingIndex];
if (rendering == null || rendering.Database != item.Database)
continue;
if (!indexableRenderings.Contains(rendering.RenderingID))
continue;
var datasourceId = rendering.Settings.DataSource;
if (!ID.IsID(datasourceId))
continue;
var datasourceItem = item.Database.GetItem(new ID(datasourceId));
if(datasourceItem == null)
continue;
datasourceItems.Add(datasourceItem);
}
return datasourceItems;
}
/// <summary>
/// Extracts textual content from an item's fields
/// </summary>
private static void GetItemContent(Item dataSource, ICollection<string> content)
{
//.Where(field => !field.Name.StartsWith("__")
foreach (Field field in dataSource.Fields)
{
if (!IndexOperationsHelper.IsTextField(new SitecoreItemDataField(field)))
continue;
var fieldValue = StripHtmlTags(field.Value ?? string.Empty);
if (!string.IsNullOrWhiteSpace(fieldValue))
content.Add(fieldValue);
}
foreach (Item child in dataSource.Children)
{
GetItemContent(child, content);
}
}
private static string StripHtmlTags(string source)
{
if (source == null)
return null;
var doc = new HtmlDocument();
doc.LoadHtml(source);
return doc.DocumentNode.InnerText;
}
private static string StripLineBreaksAndWhiteSpace(string source)
{
if(string.IsNullOrWhiteSpace(source))
return null;
source = source.Replace(System.Environment.NewLine, "");
source = Regex.Replace(source, @"\s+", " ");
return source.Trim(' ');
}
}
}
Tell Sitecore to Update Indexed Page Content if a Datasource is Changed
Since a content author will (not might) update a datasource and not update the associated page, the below changes instruct Sitecore to find any pages that may be relying on the datasource and to reindex them. If relying on a shared datasource, all pages relying on that datasource will update.
Config Patch
<configuration>
<sitecore>
<pipelines>
<indexing.getDependencies>
<processor type="Common.Sitecore.Search.GetDependencies.GetDatasourceDependencies, Common"/>
</indexing.getDependencies>
</pipelines>
</sitecore>
</configuration>
GetDependencies Class
using System.Linq;
using Sitecore;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.Pipelines.GetDependencies;
using Sitecore.Data;
using Sitecore.Data.Items;
using Sitecore.Diagnostics;
namespace Common.Sitecore.Search.GetDependencies
{
public class GetDatasourceDependencies : BaseProcessor
{
public override void Process(GetDependenciesArgs context)
{
Assert.IsNotNull(context.IndexedItem, "indexed item");
Assert.IsNotNull(context.Dependencies, "dependencies");
var item = (Item)(context.IndexedItem as SitecoreIndexableItem);
if (item == null)
return;
bool Func(ItemUri uri) => (uri != null) && uri != item.Uri;
var source = (
from l
in Globals.LinkDatabase.GetReferrers(item, FieldIDs.LayoutField)
select l.GetSourceItem().Uri)
.Where(Func)
.ToList();
source.AddRange(
(
from l
in Globals.LinkDatabase.GetReferrers(item, FieldIDs.FinalLayoutField)
select l.GetSourceItem().Uri)
.Where(Func)
);
source = source.Distinct().ToList();
context.Dependencies.AddRange(source.Select(x => (SitecoreItemUniqueId)x));
}
}
}
At this point you can write queries against this field as you typically would. Example of a custom search object with the field implemented:
using System.Runtime.Serialization;
using Sitecore.ContentSearch;
using Sitecore.ContentSearch.SearchTypes;
namespace Common.Sitecore.Seach
{
public class CustomSearchResultItem : SearchResultItem
{
[DataMember]
[IndexField("renderedContent")]
public string RenderedContent { get; set; }
}
}