We should not use habitat examples in production but we do it anyway. Especially when its something as complex as indexing. Implementing custom indexing can take even 3 days of work but if you just want to makes things work and save some time you probably just copied over habitat Foundation.Indexing project.

It's good example of how to use computed fields, facets and search but still it's not tailored to everyone's needs. Projects I worked so far were using Azure Search. It took me a while to understand why it works on my local machine with Solr and it doesn't work on hosted server with Azure Serach. Even when you deploy Azure version of Sitecore 9 with all required Azure config files, Habitat example doesn't seam to work at all.

The problem lies in Foundation.Indexing.config file in <indexConfigurations> section. Everything under this node is configured to work with Solr only. If you work with Solr locally and Azure Search on hosted server your solution would be creating transform config file to replace those values. Below I present result of transformed config file you have to use to enable indexing feature in Azure:

<?xml version="1.0" encoding="utf-8"?>
<configuration xmlns:patch="http://www.sitecore.net/xmlconfig/" xmlns:set="http://www.sitecore.net/xmlconfig/set/">
    <sitecore>
        <settings>
            <setting name="ContentSearch.ParallelIndexing.Enabled" value="true" />
        </settings>
        <solutionFramework>
            <indexing>
                <patch:attribute name="defaultProvider">fallback</patch:attribute>
                <providers>
                    <add name="fallback" type="Sitecorebond.Core.Indexing.Infrastructure.Providers.FallbackSearchResultFormatter, Sitecorebond.Core" />
                </providers>
            </indexing>
        </solutionFramework>
        <contentSearch>
            <indexConfigurations>
                <defaultCloudIndexConfiguration type="Sitecore.ContentSearch.Azure.CloudIndexConfiguration, Sitecore.ContentSearch.Azure">
                  <fieldMap type="Sitecore.ContentSearch.Azure.FieldMaps.CloudFieldMap, Sitecore.ContentSearch.Azure" >
                    <fieldNames hint="raw:AddFieldByFieldName">
                      <field fieldName="all_templates" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Collections.Generic.List`1[[System.String, mscorlib]]" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" >
                        <Analyzer type="Sitecore.ContentSearch.LuceneProvider.Analyzers.LowerCaseKeywordAnalyzer, Sitecore.ContentSearch.LuceneProvider" />
                      </field>
                      <field fieldName="has_presentation" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" />
                      <field fieldName="has_search_result_formatter" storageType="YES" indexType="TOKENIZED" vectorType="NO" boost="1f" type="System.Boolean" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" />
                      <field fieldName="search_result_formatter" storageType="YES" indexType="UNTOKENIZED" vectorType="NO" type="System.String" settingType="Sitecore.ContentSearch.Azure.CloudSearchFieldConfiguration, Sitecore.ContentSearch.Azure" />
                    </fieldNames>
                  </fieldMap>
                  <virtualFields type="Sitecore.ContentSearch.VirtualFieldProcessorMap, Sitecore.ContentSearch">
                    <processors hint="raw:AddFromConfiguration">
                      <add fieldName="content_type" type="Sitecorebond.Core.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Sitecorebond.Core"/>
                    </processors>
                  </virtualFields>
                  <documentOptions type="Sitecore.ContentSearch.Azure.CloudSearchDocumentBuilderOptions,Sitecore.ContentSearch.Azure" >
                    <fields hint="raw:AddComputedIndexField">
                      <field fieldName="has_presentation" storageType="no" indexType="untokenized">Sitecorebond.Core.Indexing.Infrastructure.Fields.HasPresentationComputedField, Sitecorebond.Core</field>
                      <field fieldName="all_templates" storageType="no" indexType="untokenized">Sitecorebond.Core.Indexing.Infrastructure.Fields.AllTemplatesComputedField, Sitecorebond.Core</field>
                      <field fieldName="has_search_result_formatter" storageType="no" indexType="untokenized">Sitecorebond.Core.Indexing.Infrastructure.Fields.HasSearchResultFormatterComputedField, Sitecorebond.Core</field>
                      <field fieldName="search_result_formatter" storageType="no" indexType="untokenized">Sitecorebond.Core.Indexing.Infrastructure.Fields.SearchResultFormatterComputedField, Sitecorebond.Core</field>
                    </fields>
                  </documentOptions>
                </defaultCloudIndexConfiguration>
            </indexConfigurations>
        </contentSearch>
    </sitecore>
</configuration>

Now all you need to do is rebuilding your master and web indexes.

Few things I've found with this solution really bothered me. When using FindAll() method we have to pass our root node which will set context of search. In my case it didn't work and I've spend whole day trying to understand whether I made a mistake copying the example or maybe it's problem with index itself. My sitecore content node had white space in the name so my path looked like this:

/sitecore/content/Sitecore Bond/Test

Every time I used FindAll method I was receiving results from whole website regardless the root node. I am not sure if that error occurs in Solr or just Azure but apparently having whitespace in the name results in having slightly different logic than I expected. Azure search parse linq query to this pseudo code:

select where '/sitecore/content/Sitecore' or 'Bond/Test'.

Not cool, that means every node which starts with '/sitecore/content/Sitecore' will be included into result ignoring specified root node. I fixed it by using this ugly trick:

Go to Sitecore.Foundation.Indexing.Services.SearchService find this method SetQueryRoots and this line

rootPredicates = rootPredicates.Or(item => item.Path.StartsWith(provider.Root.Paths.FullPath));

And change it to:

var path = provider.Root.Paths.FullPath.Replace(" ", "+");
rootPredicates = rootPredicates.Or(item => item.Path.StartsWith(path));

Replacing spaces with '+' sign results in better search results but I didn't check what azure query is generated from that linq method so I'm not sure if that solution works in every scenario.

Another problem which was quite annoying was the way how Guid is stored in an index. I tried to search a field which was collection of Guids. Sitecore stores it like this
{2AD65227-19ED-4275-B1CA-57D998360408}|{5A16B75D-4708-4888-9371-3AE02BBE5C39}

I've altered GetFreeTextPredicateService method in SearchIndexingProvider to split that string by pipe and search by single Guid. I realised my mistake when I read Azure Search documentation Supported Data Types. Guid is not on the list and it's stored in index as string like this 2ad6522719ed4275b1ca57d998360408|5a16b75d4708488893713ae02bbe5c39

Lower case, no hyphens, no brackets. Hence my changes include cleaning Guids like in the example below:

public static class GetFreeTextPredicateService
    {
        public static Expression<Func<SearchResultItem, bool>> GetFreeTextPredicate(string[] fieldNames, IQuery query, string[] splitFieldNames)
        {
            var predicate = PredicateBuilder.False<SearchResultItem>();
            if (string.IsNullOrWhiteSpace(query.QueryText))
            {
                return predicate;
            }
            foreach (var name in fieldNames)
            {
                if (splitFieldNames.Contains(name))
                {
                    var guidQuery = query.QueryText.CleanGuidForAzureQuery();
                    if (guidQuery.IndexOf('|') > 0)
                    {
                        foreach (var subQuery in guidQuery.Split('|'))
                        {
                            if (!string.IsNullOrWhiteSpace(subQuery))
                            {
                                predicate = predicate.Or(i => i[name].Contains(subQuery));
                            }
                        }
                    }
                    else
                    {
                        predicate = predicate.Or(i => i[name].Contains(guidQuery));
                    }
                }
                else
                {
                    predicate = predicate.Or(i => i[name].Contains(query.QueryText));
                }
            }
            return predicate;
        }
    }
public static class StringExtensions
{
	public static string CleanGuidForAzureQuery(this string query)
	{
		return Sitecore.ContentSearch.Utilities.IdHelper.NormalizeGuid(query);
	}
}

Azure has issues with hyphens in general. Using Contains in a predicate builder may surprise you. I had language field which I had to compare to my string so I used logic like this

predicate.Or(i => "en-GB".Contains(i.Language))

it is totally valid query from Linq point of view but Azure says noooo.