A few days back I got a question of how to do fuzzy filtering on a field using the Fluent API in EPiServer.Find. Some had seen that Elasticsearch had the possibility to do FuzzyQueries and that the core classes in EPiServer.Find had an implementation of this query but that there wasn’t a corresponding filter in either Elasticsearch or EPiServer.Find for doing this. However this is quite easily done anyway and it will enable you to do queries like:

var result = SearchClient.Instance.Search<MyIndexedObject>()
               .Filter(x => x.Author.Fuzzy("henric"))
               .GetResult();

Writing a QueryFilter

In Elasticsearch there is the possibility of wrapping a query in a filter using the Query Filter. The latest official release for EPiServer.Find does however not have an implementation of it so we have to start by creating a new Filter class mapping to the QueryFilter implementation in Elasticsearch. Filter classes in EPiServer.Find are simply classes implementing Filter and when passed to the Json serializer are serialized to the corresponding Elasticsearch filter. So we create a QueryFilter class implementing Filter and a Json converter for serializing it:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Newtonsoft.Json;
using EPiServer.Find.Api.Querying;

namespace FuzzyFiltering
{
    [JsonConverter(typeof(QueryFilterConverter))]
    public class QueryFilter : Filter
    {
        public QueryFilter(IQuery query)
        {
            Query = query;
        }

        public IQuery Query { get; set; }

        public bool Cache { get; set; }
    }

    internal class QueryFilterConverter : JsonConverter
    {
        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        {
            var filter = (QueryFilter)value;
            if (filter.Cache)
            {
                writer.WriteStartObject();
                writer.WritePropertyName("fquery");
                writer.WriteStartObject();
                writer.WritePropertyName("query");
                serializer.Serialize(writer, filter.Query);
                writer.WritePropertyName("_cache");
                serializer.Serialize(writer, filter.Cache);
                writer.WriteEndObject();
                writer.WriteEndObject();
            }
            else
            {
                writer.WriteStartObject();
                writer.WritePropertyName("query");
                serializer.Serialize(writer, filter.Query);
                writer.WriteEndObject();
            }
        }

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            return null;
        }

        public override bool CanConvert(Type objectType)
        {
            return true;
        }
    }
}

Now we have the possibility to do Fuzzy filtering, or any other query filtering, by simply passing the query class to the query filter. Now it is time to extend the filter extensions to get that fluent querying that we love.

Writing a Filter extension for strings to do fuzzy matching

A filter extension is simply an extension of the core type that we want to extend, in our case string, and that returns a DelegateFilterBuilder. The DelegateFilterBuilder in turn is created by passing a mapping from the ‘field name’ to a filter. EPiServer.Find will automatically resolve the internal Elasticsearch field name for filters for you so all you have to do is the mapping (the filter field mappings are non tokenized so your query has to match the complete value of the field):

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EPiServer.Find;
using EPiServer.Find.Api.Querying.Queries;

namespace FuzzyFiltering
{
    public static class FilterExtensions
    {
        public static DelegateFilterBuilder Fuzzy(this string value, string almost)
        {
            return new DelegateFilterBuilder(field => new QueryFilter(new FuzzyQuery(field, almost)));
        }

        public static DelegateFilterBuilder Fuzzy(this string value, string almost, double minSimilarity)
        {
            return new DelegateFilterBuilder(field => new QueryFilter(new FuzzyQuery(field, almost) { MinSimilarity = minSimilarity }));
        }
    }
}

Voila, now we have a Fuzzy extension that we can use in our queries.

Wrapping it up

Importing the QueryFilter and FilterExtensions into your workspace you are now able to do that fluent search request you always wanted:

var result = SearchClient.Instance.Search<MyIndexedObject>()
               .Filter(x => x.Author.Fuzzy("henric"))
               .GetResult();