Indexing data from another node and keeping it up to date with changes in Umbraco
Jesper Mayntzhusen
Posted on September 20, 2021
Usecase and setup
This will be a short tutorial that shows you how to index data from another node on specific content nodes in the external index.
Imagine for example you are creating a library site with books nested under author nodes. You may want to index some of the author information on the books so if you search for fx authorname, author time period, etc the books show up.
So we start with a structure like this:
We have 2 parts - getting the author data indexed on the book documents and ensuring the index data is up to date when changes occur.
Indexing the author data on the book documents
To index the data we will create a Component and register it in a Composer:
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace AutoImages.Composers
{
public class SiteComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<BookIndexerComponent>();
}
}
}
In the Component we will use TransformingIndexValues
event which runs when the index is being built and allows you to extend or edit the fields in the index.
In our case we will check on the currently indexed document to see if it is of the type book - if it is we continue and find it's parent and the values from it. Finally we add those values to the books index:
using Examine;
using Examine.Providers;
using System;
using System.Collections.Generic;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Logging;
using Umbraco.Web;
namespace AutoImages.Composers
{
public class BookIndexerComponent : IComponent
{
// The doc type id of the "Book" document type
const string BookDoctypeId = "1154";
private readonly IExamineManager _examineManager;
private readonly IUmbracoContextFactory _umbracoContextFactory;
private readonly IProfilingLogger _logger;
public BookIndexerComponent(IExamineManager examineManager, IUmbracoContextFactory umbracoContextFactory, IProfilingLogger logger)
{
_examineManager = examineManager;
_umbracoContextFactory = umbracoContextFactory;
_logger = logger;
}
public void Initialize()
{
if (!_examineManager.TryGetIndex(Umbraco.Core.Constants.UmbracoIndexes.ExternalIndexName, out IIndex index))
throw new InvalidOperationException($"No index found by name {Umbraco.Core.Constants.UmbracoIndexes.ExternalIndexName}");
if (!(index is BaseIndexProvider indexProvider))
throw new InvalidOperationException("Could not cast");
indexProvider.TransformingIndexValues += IndexProviderTransformingIndexValues;
}
private void IndexProviderTransformingIndexValues(object sender, IndexingItemEventArgs e)
{
// Get the nodetypealias from the index
if (e.ValueSet.Values.TryGetValue("nodeType", out List<object> nodeType) == false) return;
// if it is not type book we exit
if (nodeType.FirstOrDefault()?.ToString() != BookDoctypeId) return;
if (e.ValueSet.Values.TryGetValue("id", out List<object> id) == false) return;
if (id.FirstOrDefault()?.ToString() == "") return;
using (var context = _umbracoContextFactory.EnsureUmbracoContext())
{
_logger.Debug<BookIndexerComponent>($"Indexing Book node with id {id}");
try
{
var node = context.UmbracoContext.Content.GetById(int.Parse(id.FirstOrDefault().ToString()));
if (node == null) return;
var author = node.Parent;
// get values from the parent node and index them on this document
e.ValueSet.Add("authorName", author.Name);
e.ValueSet.Add("authorStyle", author.Value<string>("style")?.ToLowerInvariant());
e.ValueSet.Add("authorCentury", author.Value<string>("century")?.ToLowerInvariant());
}
catch (Exception ex)
{
_logger.Error<BookIndexerComponent>(ex, $"Error occured while indexing fabric node with id: {id.FirstOrDefault()}");
}
}
}
public void Terminate()
{
}
}
}
Finally we can check the index and see that the new author fields are added to the documents:
So if we search for some data that is only saved on the author - like their style, we will get the books as results as well since they now index that value:
Keeping the index up to date
The problem with this approach though is that we only update the book index fields whenever the index is being rebuilt for the book pages - so if we were to change some data on the author the books wouldn't automatically stay up to date.
I've gone and changed J.R.R. Tolkien to have the style high-fantasy
instead, and now the books don't show up:
So let's write some code that ensures the books are re-indexed whenever the author is updated:
using Examine;
using System;
using System.Linq;
using Umbraco.Core.Composing;
using Umbraco.Core.Models;
using Umbraco.Core.Services;
using Umbraco.Core.Services.Changes;
using Umbraco.Examine;
using Umbraco.Web.Cache;
namespace AutoImages.Events
{
public class ContentCacheRefresherEvent : IComponent
{
private readonly IContentService _contentService;
private readonly IPublishedContentValueSetBuilder _valueSetBuilder;
private readonly IExamineManager _examineManager;
private IIndex _index;
public ContentCacheRefresherEvent(IContentService contentService, IPublishedContentValueSetBuilder valueSetBuilder, IExamineManager examineManager)
{
_contentService = contentService;
_valueSetBuilder = valueSetBuilder;
_examineManager = examineManager;
}
public void Initialize()
{
if (!_examineManager.TryGetIndex(Umbraco.Core.Constants.UmbracoIndexes.ExternalIndexName, out IIndex index))
throw new InvalidOperationException($"No index found by name {Umbraco.Core.Constants.UmbracoIndexes.ExternalIndexName}");
_index = index;
ContentCacheRefresher.CacheUpdated += ContentCacheRefresher_CacheUpdated;
}
private void ContentCacheRefresher_CacheUpdated(ContentCacheRefresher sender, Umbraco.Core.Cache.CacheRefresherEventArgs e)
{
foreach (var payload in (ContentCacheRefresher.JsonPayload[])e.MessageObject)
{
// only continue if the cache change is to the node content, not it being deleted, etc.
if (payload.ChangeTypes.HasType(TreeChangeTypes.RefreshNode))
{
var content = _contentService.GetById(payload.Id);
if (content.ContentType.Alias == "author")
{
// Get all children of the author
var children = _contentService.GetPagedChildren(payload.Id, 0, 999, out long totalRecords);
if (children.Count() == 0) return;
// Cast to array to fit valuesetbuild signature
IContent[] childArray = children.Cast<IContent>().ToArray();
// Rebuild the index for the authors children
_index.IndexItems(_valueSetBuilder.GetValueSets(childArray));
}
}
}
}
public void Terminate()
{
ContentCacheRefresher.CacheUpdated -= ContentCacheRefresher_CacheUpdated;
}
}
}
And as always - make sure to register the component in the composer:
using AutoImages.Events;
using Umbraco.Core;
using Umbraco.Core.Composing;
namespace AutoImages.Composers
{
public class SiteComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<BookIndexerComponent>();
composition.Components().Append<ContentCacheRefresherEvent>();
}
}
}
And that'll do it! At this point we can change the author info and the related books will automatically have their fields updated:
Thanks for reading! If you like this post, have any feedback, suggestions, improvements to my hacky code or anything else please let me know on Twitter - @jespermayn
Posted on September 20, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
September 20, 2021