Azure Search comes with many more features than the default Lucene engine in Examine and you can leverage these features with ExamineX.

Analyzers

There are many additional analyzers in Azure Search than there are available in Lucene, some of which tend to work better than Lucene’s depending on your requirements. For example, Microsoft’s documentation states:

The default analyzer is Standard Lucene, which works well for English, but perhaps not as well as Lucene’s English analyzer or Microsoft’s English analyzer.

Default analyzers

The default analyzers used in ExamineX are configured to be synonymous with the ones shipped by default in Umbraco:

  • InternalIndex: keyword_lowercase_asciifolding, this is a custom analyzer that ExamineX adds to the index which is the keyword (i.e. whitespace) analyzer with both the lowercase and asciifolding filters applied.
  • ExternalIndex: standard.lucene, the lucene standard analyzer
  • MembersIndex: keyword_lowercase_asciifolding (as above)

It is possible to change the default analyzer used for each index:

Examples:

Default field types & analyzers

Any customized fields types and analyzers that you have applied to your indexes in Examine using LuceneDirectoryIndexOptions will be automatically converted over to your ExamineX indexes when ExamineX is enabled. However, if you require more granular control over the Azure AI Search fields used in ExamineX, the below definitions can be used when configuring the ExamineX specific AzureSearchIndexOptions.

Important: If you are configuring LuceneDirectoryIndexOptions than you must configure these options before the call to .AddExamineXAzureSearch(). If you are configuring AzureSearchIndexOptions, you should use the IOptions PostConfigure method.

Important: If you are using an Umbraco IComposer to configure LuceneDirectoryIndexOptions, you must attribute your composer with [ComposeBefore(typeof(ExamineXComposer))] to ensure that it executes before the ExamineXComposer. If you are configuring AzureSearchIndexOptions, you should use the IOptions PostConfigure method.

  • AzureSearchFieldDefinitionTypes.FullTextMultiValue - Default. Will be indexed with FullText to allow multiple values per field, sorting cannot be allowed with multiple values. This is the default to be inline with how Umbraco typically interacts with Lucene which allows multiple values to be indexed per field. For example, the Grid column fields always submit multiple values per field.
  • AzureSearchFieldDefinitionTypes.FullText. The field will be indexed with the index’s default Analyzer without sortability. Generally this is fine for normal text searching.
  • AzureSearchFieldDefinitionTypes.FullTextSortable - Will be indexed with FullText but also enable sorting on this field for search results.
  • AzureSearchFieldDefinitionTypes.Integer - Stored as a numerical structure.
  • AzureSearchFieldDefinitionTypes.Double - Stored as a numerical structure.
  • AzureSearchFieldDefinitionTypes.Long - Stored as a numerical structure.
  • AzureSearchFieldDefinitionTypes.DateTime - Stored as a DateTime.
  • AzureSearchFieldDefinitionTypes.Raw - Will be indexed with the keyword analyzer so searching will only match with an exact value.

Examples:

Custom field types & analyzers

You can define custom field types in ExamineX (similar to how you would create custom field types in Examine). This is useful when you want need a custom analyzer for your fields.

Examples:

Events

All of the underlying Examine events are available in ExamineX such as TransformingIndexValues, IndexingError and OperationComplete.

These additional events are available in ExamineX:

AzureSearchIndex.CreatingOrUpdatingIndex - Allows you to modify the definition of the index before it is created in Azure Search.

It is advised to not remove any indexes, indexers, fields, field mappings or custom analyzers created with ExamineX otherwise unexpected errors may result

Customizing the Azure Search index

Using the AzureSearchIndex.CreatingOrUpdatingIndex can be quite powerful if you want to leverage more out of Azure Cognitive Search than what is provided by default. For example, with this event you could create custom scoring profiles and custom analyzers.

An example of adding an event handler for CreatingOrUpdatingIndex:

if (examineManager.TryGetIndex("ExternalIndex", out var index) 
    && index is AzureSearchIndex azureIndex)
{
    azureIndex.CreatingOrUpdatingIndex += AzureIndex_CreatingIndex;
}

An example of creating a custom scoring profiles:

private void AzureIndex_CreatingOrUpdatingIndex(object sender, CreatingOrUpdatingIndexEventArgs e)
{
    // NOTE: You cannot add a scoring rule for a field unless that field exists in the index definition!
    //       When ExamineX first creates the index it will only contain the fields defined in the
    //       initial field definitions. When new items are indexed and new fields are detected then
    //       the Azure Cognitive Search index is updated.

    // get the azure cognitive search definition
    var index = e.AzureSearchIndexDefinition;

    // get or create scoring profiles list (will be null for new indexes)
    index.ScoringProfiles = index.ScoringProfiles ?? new List<ScoringProfile>();

    // this example will create a scoring profile called "pages"
    const string scoringProfileName = "pages";

    // get or create a scoring profile
    var scoringProfile = index.ScoringProfiles.FirstOrDefault(x => x.Name == scoringProfileName);
    if (scoringProfile == null)
        index.ScoringProfiles.Add(scoringProfile = new ScoringProfile
        {
            Name = scoringProfileName,
            FunctionAggregation = ScoringFunctionAggregation.Sum
        });

    // add a 'boost' of 3 for the "pageTitle" field if the field exists
    if (index.Fields.Any(x => x.Name == "pageTitle"))
    {
        // ensure the object exists
        scoringProfile.TextWeights = scoringProfile.TextWeights ?? new TextWeights(new Dictionary<string, double>());
        scoringProfile.TextWeights.Weights.Add("pageTitle", 3);
    }

    // add a 'boost' for pages that have been updated within the last two days
    if (index.Fields.Any(x => x.Name == "updateDate"))
    {
        // ensure the object exists
        scoringProfile.Functions = scoringProfile.Functions ?? new List<ScoringFunction>();

        // check existing or add
        var updateDateFreshness = scoringProfile.Functions.FirstOrDefault(x => x.FieldName == "updateDate");
        if (updateDateFreshness == null)
            scoringProfile.Functions.Add(updateDateFreshness = new FreshnessScoringFunction
            {
                FieldName = "updateDate",
                Boost = 3,
                Parameters = new FreshnessScoringParameters(new TimeSpan(2, 0, 0, 0)),
                Interpolation = ScoringFunctionInterpolation.Logarithmic
            });
    }

    // Set the default scoring profile
    index.DefaultScoringProfile = scoringProfileName;
}