Skip to content

Instantly share code, notes, and snippets.

@Shazwazza
Created September 26, 2019 13:43
Show Gist options
  • Star 3 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Shazwazza/3d32f4f37d9adadfe56400d0c24db6bd to your computer and use it in GitHub Desktop.
Save Shazwazza/3d32f4f37d9adadfe56400d0c24db6bd to your computer and use it in GitHub Desktop.
Example of creating a custom lucene index in Umbraco 8
/// <summary>
/// Custom service to work with products
/// </summary>
public interface IProductService
{
IEnumerable<Product> GetAll();
}
/// <summary>
/// Product model
/// </summary>
public class Product
{
public System.Guid Id { get; set; }
public string Name { get; set; }
public double Price { get; set; }
}
/// <summary>
/// Custom implementation of LuceneIndex in order to implement <see cref="IIndexDiagnostics"/>
/// </summary>
public class ProductIndex : LuceneIndex, IIndexDiagnostics
{
//NOTE: None of this will be necessary in 8.2 if you just want to use the underlying LuceneIndex object
// see: https://github.com/umbraco/Umbraco-CMS/pull/6447
public ProductIndex(string name, Directory luceneDirectory, FieldDefinitionCollection fieldDefinitions, Analyzer analyzer, IProfilingLogger profilingLogger)
: base(name, luceneDirectory, fieldDefinitions, analyzer, null, null)
{
_luceneIndexDiagnostics = new LuceneIndexDiagnostics(this, profilingLogger);
}
// create a reference to a pre-built index diagnostics for lucene indexes
private readonly LuceneIndexDiagnostics _luceneIndexDiagnostics;
//wrap results
public int DocumentCount => _luceneIndexDiagnostics.DocumentCount;
public int FieldCount => _luceneIndexDiagnostics.FieldCount;
public IReadOnlyDictionary<string, object> Metadata => _luceneIndexDiagnostics.Metadata;
public Attempt<string> IsHealthy() => _luceneIndexDiagnostics.IsHealthy();
}
/// <summary>
/// Creates and registers custom indexes on startup
/// </summary>
public class ProductIndexComponent : IComponent
{
private readonly IExamineManager _examineManager;
private readonly ProductIndexCreator _productIndexCreator;
public ProductIndexComponent(IExamineManager examineManager, ProductIndexCreator productIndexCreator)
{
_examineManager = examineManager;
_productIndexCreator = productIndexCreator;
}
public void Initialize()
{
foreach (var index in _productIndexCreator.Create())
_examineManager.AddIndex(index);
}
public void Terminate() { }
}
/// <summary>
/// Registers all components and services for custom indexes
/// </summary>
[RuntimeLevel(MinLevel = RuntimeLevel.Run)]
public class ProductIndexComposer : IUserComposer
{
public void Compose(Composition composition)
{
composition.Components().Append<ProductIndexComponent>();
composition.RegisterUnique<ProductIndexValueSetBuilder>();
composition.Register<ProductIndexPopulator>(Lifetime.Singleton);
composition.RegisterUnique<ProductIndexCreator>();
composition.RegisterUnique<IProductService, ProductService>();
}
}
/// <summary>
/// Factory to create the custom index
/// </summary>
public class ProductIndexCreator : LuceneIndexCreator
{
private readonly IProfilingLogger _logger;
public ProductIndexCreator(IProfilingLogger logger)
{
_logger = logger;
}
public override IEnumerable<IIndex> Create()
{
var index = new ProductIndex("ProductIndex",
CreateFileSystemLuceneDirectory("ProductIndex"),
new FieldDefinitionCollection(
new FieldDefinition("name", FieldDefinitionTypes.FullTextSortable),
new FieldDefinition("price", FieldDefinitionTypes.FullText)
),
new StandardAnalyzer(Version.LUCENE_30),
_logger);
return new[] { index };
}
}
/// <summary>
/// Populates the custom index with data when it needs to be rebuilt
/// </summary>
public class ProductIndexPopulator : IndexPopulator
{
private readonly ProductIndexValueSetBuilder _productValueSetBuilder;
private readonly IProductService _productService;
public ProductIndexPopulator(ProductIndexValueSetBuilder productValueSetBuilder, IProductService productService)
{
_productValueSetBuilder = productValueSetBuilder;
_productService = productService;
RegisterIndex("ProductIndex");
}
protected override void PopulateIndexes(IReadOnlyList<IIndex> indexes)
{
var products = _productService.GetAll().ToArray();
foreach (var index in indexes)
{
index.IndexItems(_productValueSetBuilder.GetValueSets(products));
}
}
}
/// <summary>
/// Converts <see cref="Product"/> to Examine ValueSet's
/// </summary>
public class ProductIndexValueSetBuilder : IValueSetBuilder<Product>
{
public IEnumerable<ValueSet> GetValueSets(params Product[] products)
{
foreach (var product in products)
{
var indexValues = new Dictionary<string, object>
{
["name"] = product.Name,
["price"] = product.Price
};
var valueSet = new ValueSet(product.Id.ToString(), "product", indexValues);
yield return valueSet;
}
}
}
/// <summary>
/// Implementation of IProductService
/// </summary>
public class ProductService : IProductService
{
public IEnumerable<Product> GetAll()
{
//mock data
yield return new Product { Id = System.Guid.NewGuid(), Name = "Hello", Price = 13.50 };
yield return new Product { Id = System.Guid.NewGuid(), Name = "World", Price = 10.12 };
yield return new Product { Id = System.Guid.NewGuid(), Name = "Hi", Price = 100.99 };
yield return new Product { Id = System.Guid.NewGuid(), Name = "There", Price = 1000.20 };
}
}
@snerpton
Copy link

Perfect, thanks for sharing this @Shazwazza – answered all my questions :-)

@bjarnef
Copy link

bjarnef commented Jan 8, 2021

@Shazwazza is is possible to pass in some additional parameters/data to GetValueSets. In a multi-site solution we sometimes need fetch some site specific data e.g. with an id stored on root nodes.

Currenly it seems to require the extend the model/item for an additional property?
However the model might also be used elsewhere, so it might be a specific model specific for indexing.

For example:

protected override void PopulateIndexes(IReadOnlyList<IIndex> indexes)
{
    var rootNodes = _umbracoContextAccessor.UmbracoContext.Content.GetAtRoot();

    foreach (var node in rootNodes)
    {
        var settings = node.FirstChild<Settings>();
        var siteKey = settings?.SiteKey;

        var items = _customService.GetData(siteKey).ToArray();

        if (data.Length > 0) 
        {
            foreach (var index in indexes)
            {
                // Add node.Id (site id) on each item, so we can search/filter on this
                index.IndexItems(_customValueSetBuilder.GetValueSets(items));
            }
        }
    }
    
}

It would be great if IndexPopulator had an overload of PopulateIndexes or with an optional method to pass in e.g. Dictionary<string, object> or similar with some additional data.

For now we can extend with a property something like this:

protected override void PopulateIndexes(IReadOnlyList<IIndex> indexes)
{
    var rootNodes = _umbracoContextAccessor.UmbracoContext.Content.GetAtRoot();

    foreach (var node in rootNodes)
    {
        var settings = node.FirstChild<Settings>();
        var siteKey = settings?.SiteKey;

        var items = _customService.GetData(siteKey).Select(c => { c.SiteId = node.Id; return c; }).ToArray();

        if (data.Length > 0) 
        {
            foreach (var index in indexes)
            {
                index.IndexItems(_customValueSetBuilder.GetValueSets(items));
            }
        }
    }
    
}

@gdiazderadaa
Copy link

Hi @Shazwazza I've followed this example to create a custom index but can't get it to work properly.

  • If I create the index using UmbracoContentIndex then the index is created but using all the fields rather than the ones I specify in the field definition collection on creation
  • If I create the index using the custom index class (ProductIndex in your example) then the index is created with 0 fields.

Any ideas what I may be missing here?

NB: I'm using Umbraco 8.9.1

@Shazwazza
Copy link
Author

Hi all - these examples were made a long time ago and I'm not really maintaining them - they should all be ported to real documentation. Feel free to help out with that if you can. For Umbraco based stuff it should be in https://github.com/umbraco/UmbracoDocs for Examine specific (non-umbraco related), docs are here https://shazwazza.github.io/Examine/

@gdiazderadaa

If I create the index using UmbracoContentIndex then the index is created but using all the fields rather than the ones I specify in the field definition collection on creation

The definition collection is not a filter. It just defines the field types. Examine 1.x will index all data that is passed to it in value sets. This is different from 0.x where it would only index fields that you 'define'. You can filter fields with the validator or on events.

@bjarnef

is is possible to pass in some additional parameters/data to GetValueSets. In a multi-site solution we sometimes need fetch some site specific data e.g. with an id stored on root nodes.

The code above is just examples, you are in charge of this code. If you want to add any functionality you're free to do so. ProductIndexValueSetBuilder is just a custom service, you can use whatever you want to create value sets.

@bjarnef
Copy link

bjarnef commented Jan 21, 2021

@Shazwazza yes, unfortunately we can't pass in e.g. Dictionary<string, object> or similar with some additional data to GetValueSets() since the signature of the method doesn't allow this, so it has to be part of the entity/item itself.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment