Indexing data from another node and keeping it up to date with changes in Umbraco

jemayn

Jesper Mayntzhusen

Posted on September 20, 2021

Indexing data from another node and keeping it up to date with changes in Umbraco

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:
image
image

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>();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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()
        {
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Finally we can check the index and see that the new author fields are added to the documents:

image

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:

image

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:

image

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;
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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>();
        }
    }
}

Enter fullscreen mode Exit fullscreen mode

And that'll do it! At this point we can change the author info and the related books will automatically have their fields updated:

indexing

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

💖 💪 🙅 🚩
jemayn
Jesper Mayntzhusen

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