One approach that we have used is to index the content of all data sources in a single computed field. You can search that field when searching the index. This can probably be improved, but it's a start.
I believe this is also the approach the are using in the Habitat project.
Copied/inspired from: http://kamsar.net/index.php/2014/05/indexing-subcontent/
public class DataSourceContentField : IComputedIndexField
{
public virtual string FieldName { get; set; }
public virtual string ReturnType { get; set; }
// TODO: Change to dictionary with supported template fields as the value
internal static HashSet<ID> SupportedTemplates { get; set; } = new HashSet<ID>();
public object ComputeFieldValue(IIndexable indexable)
{
var sitecoreIndexable = indexable as SitecoreIndexableItem;
if (sitecoreIndexable == null) return null;
// find renderings with datasources set
var customDataSources = ExtractRenderingDataSourceItems(sitecoreIndexable.Item);
// extract text from data sources
var contentToAdd = customDataSources.SelectMany(GetItemContent).ToList();
if (contentToAdd.Count == 0) return null;
Log.Debug($"[{nameof(DataSourceContentField)}] Item: {sitecoreIndexable.AbsolutePath}, Content: {string.Join(" ", contentToAdd)}", this);
return string.Join(" ", contentToAdd);
}
/// <summary>
/// Finds all renderings on an item's layout details with valid custom data sources set and returns the data source items.
/// </summary>
protected virtual IEnumerable<Item> ExtractRenderingDataSourceItems(Item baseItem)
{
var currentLayoutXml = LayoutField.GetFieldValue(baseItem.Fields[FieldIDs.LayoutField]);
if (string.IsNullOrEmpty(currentLayoutXml)) yield break;
var layout = LayoutDefinition.Parse(currentLayoutXml);
// loop over devices in the rendering
for (var deviceIndex = layout.Devices.Count - 1; deviceIndex >= 0; deviceIndex--)
{
var device = layout.Devices[deviceIndex] as DeviceDefinition;
if (device == null) continue;
// loop over renderings within the device
for (var renderingIndex = device.Renderings.Count - 1; renderingIndex >= 0; renderingIndex--)
{
var rendering = device.Renderings[renderingIndex] as RenderingDefinition;
if (string.IsNullOrWhiteSpace(rendering?.Datasource)) continue;
// if the rendering has a custom data source, we resolve the data source item and place its text fields into the content to add
var dataSource = DataSourceHelper.ResolveDataSource(rendering.Datasource, baseItem);
if (dataSource != baseItem && dataSource.IsDerivedFrom(SupportedTemplates))
{
yield return dataSource;
}
}
}
}
/// <summary>
/// Extracts textual content from an item's fields
/// </summary>
protected virtual IEnumerable<string> GetItemContent(Item dataSource)
{
foreach (Field field in dataSource.Fields)
{
// this check is what Sitecore uses to determine if a field belongs in _content (see LuceneDocumentBuilder.AddField())
if (!IndexOperationsHelper.IsTextField(new SitecoreItemDataField(field))) continue;
var fieldValue = StringUtil.RemoveTags(field.Value ?? string.Empty);
if (!string.IsNullOrWhiteSpace(fieldValue)) yield return fieldValue;
}
}
}