Indexes: Analyzers

RavenDB uses indexes to facilitate fast queries powered by Lucene, the full-text search engine.

The indexing of a single document starts from creating Lucene's Document according an index definition. Lucene processes it by breaking it into fields and splitting all the text from each Field into tokens (Terms) in a process called Tokenization. Those tokens will be stored in the index, and later will be searched upon. The Tokenization process uses an object called an Analyzer underneath.

The indexing process and its results can be controlled by various field options and Analyzers.

Understanding Analyzers

Lucene offers several out of the box Analyzers, and the new ones can be created easily. Various analyzers differ in the way they split the text stream ("tokenize"), and in the way they process those tokens in post-tokenization.

For example, given this sample text:

The quick brown fox jumped over the lazy dogs, Bob@hotmail.com 123432.

  • StandardAnalyzer, which is Lucene's default, will produce the following tokens:

    [quick] [brown] [fox] [jumped] [over] [lazy] [dog] [bob@hotmail.com] [123432]

  • StopAnalyzer will work similarly, but will not perform light stemming and will only tokenize on white space:

    [quick] [brown] [fox] [jumped] [over] [lazy] [dogs] [bob] [hotmail] [com]

  • SimpleAnalyzer will tokenize on all non-alpha characters and will make all the tokens lowercase:

    [the] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dogs] [bob] [hotmail] [com]

  • WhitespaceAnalyzer will just tokenize on white spaces:

    [The] [quick] [brown] [fox] [jumped] [over] [the] [lazy] [dogs,] [bob@hotmail.com] [123432.]

  • KeywordAnalyzer will perform no tokenization, and will consider the whole text a stream as one token:

    [The quick brown fox jumped over the lazy dogs, bob@hotmail.com 123432.]

RavenDB Default Analyzer

By default, RavenDB uses the custom analyzer called LowerCaseKeywordAnalyzer for all indexed content. Its implementation behaves like Lucene's KeywordAnalyzer, but it also performs case normalization by converting all characters to lower case.

RavenDB stores the entire term as a single token, in a lower cased form. Given the same sample above text, LowerCaseKeywordAnalyzer will produce a single token:

[the quick brown fox jumped over the lazy dogs, bob@hotmail.com 123432.]

This default analyzer allows you to perform exact searches which is exactly what you would expect. However, it doesn't allow you to perform full-text searches. For that purposes, a different analyzer should be used.

To allow full-text search on the text fields, you can use the analyzers provided out of the box with Lucene. These are available as part of the Lucene library which ships with RavenDB.

For most cases, Lucene's StandardAnalyzer would be your analyzer of choice. As shown above, this analyzer is aware of e-mail and network addresses when tokenizing. It normalizes cases, filters out common English words, and does some basic English stemming as well.

For languages other than English, or if you need a custom analysis process, you can roll your own Analyzer. It is quite simple and may be already available as a contrib package for Lucene. There are also Collation analyzers available (you can read more about them here).

Using Non-Default Analyzer

To make a document property indexed using a specific Analyzer, all you need to do is to match it with the name of the property:

public class BlogPosts_ByTagsAndContent : AbstractIndexCreationTask<BlogPost>
{
    public BlogPosts_ByTagsAndContent()
    {
        Map = posts => from post in posts
                       select new
                       {
                           post.Tags,
                           post.Content
                       };

        Analyzers.Add(x => x.Tags, "SimpleAnalyzer");
        Analyzers.Add(x => x.Content, typeof(SnowballAnalyzer).AssemblyQualifiedName);
    }
}
store.Maintenance.Send(new PutIndexesOperation(new IndexDefinitionBuilder<BlogPost>("BlogPosts/ByTagsAndContent")
{
    Map = posts => from post in posts
                   select new
                   {
                       post.Tags,
                       post.Content
                   },
    Analyzers =
    {
        {x => x.Tags, "SimpleAnalyzer"},
        {x => x.Content, typeof(SnowballAnalyzer).AssemblyQualifiedName}
    }
}.ToIndexDefinition(store.Conventions)));

Information

The analyzer you are referencing to has to be available to the RavenDB server instance. When using analyzers that do not come with the default Lucene.NET distribution, you need to drop all the necessary DLLs into the RavenDB working directory (where Raven.Server executable is located), and use their fully qualified type name (including the assembly name).

Creating Own Analyzer

You can create a custom analyzer on your own and deploy it to RavenDB server. To do that pefrom the following steps:

  • create a class that inherits from abstract Lucene.Net.Analysis.Analyzer (you need to reference Lucene.Net.dll supplied with RavenDB Server package),
  • your DLL needs to be placed next to RavenDB binaries (note it needs to be compatible with .NET Core 2.0 e.g. .NET Standard 2.0 assembly)
  • the fully qualified name needs to be specified for an indexing field that is going to be tokenized by the analyzer

public class MyAnalyzer : Lucene.Net.Analysis.Analyzer
{
    public override TokenStream TokenStream(string fieldName, TextReader reader)
    {
        throw new CodeOmitted();
    }
}

Manipulating Field Indexing Behavior

By default, each indexed field is analyzed using the LowerCaseKeywordAnalyzer which indexes a field as a single, lower cased term.

This behavior can be changed by turning off the field analysis (setting the FieldIndexing option for this field to Exact). This causes all the properties to be treated as a single token and the matches must be exact (case sensitive), using the KeywordAnalyzer behind the scenes.

public class Employees_ByFirstAndLastName : AbstractIndexCreationTask<Employee>
{
    public Employees_ByFirstAndLastName()
    {
        Map = employees => from employee in employees
                           select new
                           {
                               LastName = employee.LastName,
                               FirstName = employee.FirstName
                           };

        Indexes.Add(x => x.FirstName, FieldIndexing.Exact);
    }
}

FieldIndexing.Search allows performing full text search operations against the field:

public class BlogPosts_ByContent : AbstractIndexCreationTask<BlogPost>
{
    public BlogPosts_ByContent()
    {
        Map = posts => from post in posts
                       select new
                       {
                           Title = post.Title,
                           Content = post.Content
                       };

        Indexes.Add(x => x.Content, FieldIndexing.Search);
    }
}

If you want to disable indexing on a particular field, use the FieldIndexing.No option. This can be useful when you want to store field data in the index, but don't want to make it available for querying, however it will available for extraction by projections:

public class BlogPosts_ByTitle : AbstractIndexCreationTask<BlogPost>
{
    public BlogPosts_ByTitle()
    {
        Map = posts => from post in posts
                       select new
                       {
                           Title = post.Title,
                           Content = post.Content
                       };

        Indexes.Add(x => x.Content, FieldIndexing.No);
        Stores.Add(x => x.Content, FieldStorage.Yes);
    }
}

Ordering When Field is Searchable

When field is marked as Search sorting must be done using additional field. More here.