Extensions

How to Extend Handlers

In order to create a new handler, you need to create a class and inherit from IndexingHandlerBase abstract class. Then, you will have to override 2 methods:

public abstract string Name { get; }
public abstract void Execute(IIndexingContext context);

The first one is a property representing the name of the handler.

The second one is a method that represents the inner workings of the handler.

To include the handler in the indexing process, you have to add it to the indexing chain using the

AddHandler() where TType : IIndexingHandler method, part of the IndexingChain class.

Choose a position in either the Full Indexing or Incremental Indexing chains, and call the method with the generic type TType equal to the one of the handler you created. The position will determine the moment in the indexing chain where the handler will run its Execute method’s code.

Example of how to add a custom handler:

public class CustomHandler : IndexingHandlerBase
{
    public override string Name => "Custom Handler";
    
    public override void Execute(IIndexingContext context)
    {
        //Your code here
    }
}
private IndexingChain FullIndexingChain(IContainer container)
{
    return new IndexingChain(container)
        .AddHandler<DeletePreviousIndexHandler>()
        .AddHandler<CreateIndexHandler>()
        .AddHandler<CustomHandler>() // add your handler here 
        .AddHandler<RebuildIndexHandler>()
        .AddHandler<SetCurrentIndexHandler>();
}

In the example, CreateIndexHandler will run before CustomHandler and RebuildIndexHandler immediately after.

How to Extend Pipes

In order to create a new Pipe, you have to create a new class that implements:

IPipe<ICategoryIndexingCommand<T>>, where T : NodeContent

if you want to add a pipe for further processing of catalog categories.

IPipe<IEntryIndexingCommand<T>>, where T : EntryContentBase

if you want to add a pipe for further processing of catalog entries.

IPipe<IPageIndexingCommand<T>> where T : PageData

if you want to add a pipe for further processing of site pages.

You will have to implement 2 properties and a method

  int Order { get; }
  string Name { get; }
  void Execute(T command);
  • Order is a property which determines the priority of a pipe execution inside a pipeline.
  • Name is a property depicting the name of the pipe.
  • Execute is a method which holds the inner workings of the pipe. Generic parameter T is of type IPipeCommand. This is a base interface which the mentioned ICategoryIndexingCommand, IEntryIndexingCommand and IPageIndexingCommand inherit from.

Afterwards, you have to register your pipe in order for it to be picked up and executed. Use one of the built-in extension methods:

  • ExtendCategoryIndexing<>()
container.AddHawksearchCategoryIndexing<NodeContent>()
             .ExtendCategoryIndexing<NodeContent, CustomCategoryIndexingPipe>();
  • ExtendProductIndexing<>()
 container.AddHawksearchProductIndexing<GenericProduct, GenericVariant>()
            .ExtendProductIndexing<GenericProduct, EntriesImagePipe<GenericProduct>>()
            .ExtendProductIndexing<GenericProduct, GenericProductAttributesPipe>();
  • ExtendVariantIndexing<>()
container.AddHawksearchVariantIndexing<GenericVariant>()
            .ExtendVariantIndexing<GenericVariant, EntriesImagePipe<GenericVariant>>()
            .ExtendVariantIndexing<GenericVariant, GenericVariantAttributesPipe>()
            .ExtendVariantIndexing<GenericVariant, GenericVariantShippingPipe>();
  • ExtendBundleIndexing<>()
container.AddHawksearchBundleIndexing<GenericBundle, GenericVariant>()
            .ExtendBundleIndexing<GenericBundle, CustomBundleIndexingPipe>();
  • ExtendPackageIndexing<>()
container.AddHawksearchPackageIndexing<GenericPackage, GenericVariant>()
            .ExtendPackageIndexing<GenericPackage, CustomPackageIndexingPipe>();
  • ExtendPageIndexing<>()
container.AddHawksearchPageIndexing<LocationItemPage>()
            .ExtendPageIndexing<LocationItemPage, FoundationPageAttributesPipe<LocationItemPage>>()
            .ExtendPageIndexing<LocationItemPage, LocationItemPageAttributesPipe>();
container.AddHawksearchPageIndexing<StandardPage.StandardPage>()
            .ExtendPageIndexing<StandardPage.StandardPage, FoundationPageAttributesPipe<StandardPage.StandardPage>>()
            .ExtendPageIndexing<StandardPage.StandardPage, StandardPageAttributesPipe>();

Examples

New pipe for adding shipping information to a variant

Creation

public class GenericVariantShippingPipe : IPipe<IEntryIndexingCommand<GenericVariant>>
{
    public int Order => 410;
    public string Name => "Variant Shipping Attributes";

    public void Execute(IEntryIndexingCommand<GenericVariant> command)
    {
        foreach (var entry in command.Entries)
        {
            var productItem = entry.OutputEntry;
            var variant = entry.InputEntry;

            productItem.Attributes[nameof(variant.MinQuantity)] = new List<decimal> { variant.MinQuantity ?? decimal.Zero };
            productItem.Attributes[nameof(variant.MaxQuantity)] = new List<decimal> { variant.MaxQuantity ?? decimal.Zero };
            productItem.Attributes[nameof(variant.Weight)] = new List<double> { variant.Weight };
        }
    }
}

Registration

container.AddHawksearchVariantIndexing<GenericVariant>()
    .ExtendVariantIndexing<GenericVariant, GenericVariantShippingPipe>();

New pipe for adding custom attributes to a certain page type (LocationItemPage from Foundation)

Creation

public class LocationItemPageAttributesPipe : IPipe<IPageIndexingCommand<LocationItemPage>>
{
    public int Order => 360;
    public string Name => "Location Page Attributes";

    private readonly IUrlResolver _urlResolver;

    public LocationItemPageAttributesPipe(IUrlResolver urlResolver)
    {
        _urlResolver = urlResolver;
    }

    public void Execute(IPageIndexingCommand<LocationItemPage> command)
    {
        foreach (var entry in command.Entries)
        {
            var contentItem = entry.OutputEntry;
            var page = entry.InputEntry;

            contentItem.Attributes["ImageUrl"] = new List<string> { _urlResolver.GetUrl(page.Image) };
            contentItem.Attributes["Description"] = new List<string> { page.MainIntro };
        }
    }
}

Registration

 container.AddHawksearchPageIndexing<LocationItemPage>()
                .ExtendPageIndexing<LocationItemPage, LocationItemPageAttributesPipe>();

New pipe for indexing an entry’s images - a general example for all entries

Creation

public class EntriesImagePipe<T> : IPipe<IEntryIndexingCommand<T>> where T : EntryContentBase
{
    public int Order => 350;
    public string Name => "Images lookup";

    private readonly IUrlResolver _urlResolver;
    private readonly IContentLoader _contentLoader;
    private readonly ILogger _logger = LogManager.GetLogger();

    public EntriesImagePipe(IUrlResolver urlResolver, IContentLoader contentLoader)
    {
        _urlResolver = urlResolver;
        _contentLoader = contentLoader;
    }

    public void Execute(IEntryIndexingCommand<T> command)
    {
        var entriesWithError = new List<ContentToIndex<T, HawksearchProductItem>>();
        foreach (var entry in command.Entries)
        {
            if (!TryProcessOne(command.Context, entry))
            {
                entriesWithError.Add(entry);
            }
        }

        if (entriesWithError.Any())
        {
            command.Entries = command.Entries
                .Except(entriesWithError)
                .ToList();
        }
    }

    private bool TryProcessOne(IIndexingContext context, ContentToIndex<T, HawksearchProductItem> entry)
    {
        var inputEntry = entry.InputEntry;
        try
        {
            foreach (var commerceMedia in inputEntry.CommerceMediaCollection)
            {
                if (_contentLoader.TryGet<ImageMediaData>(commerceMedia.AssetLink, out var imageMediaData))
                {
                    entry.OutputEntry.ImageUrl.Add(_urlResolver.GetUrl(imageMediaData));

                    if (!string.IsNullOrWhiteSpace(imageMediaData.AltText))
                    {
                        entry.OutputEntry.ImageAlt.Add(imageMediaData.AltText);
                    }
                    
                    break;
                }
            }

            return true;
        }
        catch (Exception ex)
        {
            string message = $"Error occurred while processing: `{inputEntry.DisplayName}`, {inputEntry.Code}.";
            context.AddDetailsToReport(message);
            _logger.Error(message, ex);
            return false;
        }
    }
}

Registration

container.AddHawksearchProductIndexing<GenericProduct, GenericVariant>()
     .ExtendProductIndexing<GenericProduct, EntriesImagePipe<GenericProduct>>();
container.AddHawksearchVariantIndexing<GenericVariant>()
    .ExtendVariantIndexing<GenericVariant, EntriesImagePipe<GenericVariant>>();

How to Extend Field Mappings

To extend the types of the supported properties decorated with [IncludeInHawksearch], you should create a “mapper” class and implement IOutputFieldValueMapper interface:

List<object> Map(object source, PropertyInfo property);
bool CanMap(PropertyInfo property);
  • Map is a method that receives the source object containing the property and that property’s general info, then tries to return a mapped value.
  • CanMap is a method that receives the property’s general info, then decides whether or not this “mapper” is suitable to handle the requested type.

Afterwards, register the mapper as a viable implementation for the IOutputFieldValueMapper service in your DI container. Using this approach, you could index more complex types such as Optimizely blocks.

Example (of the mapper responsible for handling Optimizely’s complex data)

Creation

public class OptiOutputFieldValueMapper : IOutputFieldValueMapper
{
    private static readonly Type[] ValidTypes =
    { 
        typeof(ContentReference), typeof(XhtmlString)
    };

    public virtual List<object> Map(object source, PropertyInfo property)
    {
        var value = property.GetValue(source);

        switch (value)
        {
            case null:
                return new List<object> { string.Empty };

            case ContentReference contentLink:
                return new List<object> { contentLink.ToString() };

            case XhtmlString xhtmlString:
                return new List<object> { Regex.Replace(xhtmlString.ToHtmlString(), "<.*?>", string.Empty) };

            default:
                throw new NotSupportedException($"The decorated property {property.Name} of type " +
                                                $"{property.PropertyType.Name} is not supported.");
        }
    }

    public virtual bool CanMap(PropertyInfo property)
    {
        return ValidTypes.Contains(property.PropertyType);
    }
}

Registration

 services.AddSingleton<IOutputFieldValueMapper, OptiOutputFieldValueMapper>();

Using GeoPoint Fields

The GeoPoint is a Hawksearch data type that holds a pair of latitude/longitude values.
A special class was created to represent this data type named GeoPoint. This class has 2 constructors - one for passing the latitude/longitude as double values, one for passing the latitude/longitude as strings

public GeoPoint(double latitude, double longitude)
public GeoPoint(string latitude, string longitude)

Example of introducing the GeoPoint for indexing in a custom pipe

public class LocationItemPageAttributesPipe : IPipe<IPageIndexingCommand<LocationItemPage>>
{
    public int Order => 360;
    public string Name => "Location Page Attributes";

    private readonly IUrlResolver _urlResolver;

    public LocationItemPageAttributesPipe(IUrlResolver urlResolver)
    {
        _urlResolver = urlResolver;
    }

    public void Execute(IPageIndexingCommand<LocationItemPage> command)
    {
        foreach (var entry in command.Entries)
        {
            var contentItem = entry.OutputEntry;
            var page = entry.InputEntry;

            contentItem.Attributes["ImageUrl"] = new List<string> { _urlResolver.GetUrl(page.Image) };
            contentItem.Attributes["Description"] = new List<string> { page.MainIntro };
            contentItem.Attributes["coordinates"] = new List<GeoPoint> { new GeoPoint(page.Latitude, page.Longitude) };
        }
    }
}

Troubleshooting

If you happen to receive from Hawksearch the message (also displayed in the Indexing Jobs' logs)

Incorrect input format. It's not allowed to use nested objects.

you might want to lowercase the name of the sent attribute. For instance, if you have

contentItem.Attributes["Coordinates"] = new List<GeoPoint> { new GeoPoint(page.Latitude, page.Longitude) };

change it to:

contentItem.Attributes["coordinates"] = new List<GeoPoint> { new GeoPoint(page.Latitude, page.Longitude) };

even though in the Hawksearch engine your field might be defined as “Coordinates” with capital “C”.