How to Simplify Azure Search Implementations

If you have ever struggled with implementing Azure Search on your projects then you’re not alone. Find out how we solved this with a simple package, the Cogworks.AzureSearch.

We're dedicated and fully committed to building amazing solutions on top of the Microsoft Azure cloud ecosystem (we proved this recently when we turned into a Microsoft Silver Partner which we’re rather proud of). Our latest mission has been to work on the Azure Search library and simplify our processes on and off our client projects.  

In this blog post, we will explain what an Azure Search is and how it is helping to streamline project workflow. The development team has created an open source package to help us get the most out of our projects and I'd like to show you how it can help you too with a quick tutorial on an example project. The Cogworks.AzureSearch is now available for download.

 

What is Azure Search?

Azure Search (also known as the Azure Cognitive Search) gives developers the tools to create richer, better experiences without the need to be a search expert. Microsoft describes the Azure Cognitive Search as the only cloud search service with built-in AI capabilities that enrich all types of information to easily identify and explore relevant content at scale. 

Image source: Microsoft

 

Features of Azure Cognitive Search:

- Indexing features.
- Query and user experience, f.e. free-form text search, geo-search, filters and facets.
- Security features.
- Programmability.
- Portal features.
- AI enrichment and knowledge mining.
- You can find a more in-depth description of each feature here.

 

Azure Search in action.


So you can really understand how it works, this is how the architecture for a site with the Azure Search function might look:

Image source: Microsoft

You might be wondering but why use Azure Search over built-in search engines like Examine? 

There are many reasons why you might want to incorporate Azure Search into your projects such as: 

- It’s features, simplicity, performance and stability.
- Scalability and e.g. working with slots (independence from web app).
- Separation of concerns.
- Independence from platform. It can be queried (and indexed) from various apps and devices.

Check out the many other reasons here.

 

How to simplify Azure Search implementations.


It’s easy to get started with documentation and SDK so we can start indexing and searching! 

But there are several questions:

- How do I configure indexes?

- How do I use indexes and operations on those?

- How do I inject dedicated indexes service to IoC?

We have created a package that answers all of these questions in order to create consistency and standardisation in Azure Search implementations. The package includes high-level integration based on abstraction, with a single, simple way to work on dedicated index and performing generic and domain searching and is currently available on GitHub or as a Nuget Package.

 

Start working with Azure Search wrapper.

To start working with the Cogworks.AzureSearch we have created some IoC extensions that allow you to quickly start with configuring and communicating with the Azure Search:

Autofac 

LightInject 

Umbraco 

 

Example project structure.


For the purpose of an example, we will be working on Blog domain. Let’s say our solution has the following structure:

 

- AzureSearchExample.Core (containing models and services).
- AzureSearchExample.App (containing application configuration, view, reference to AzureSearchExample.Core). This can include one application types:  web, console app, function, and WebJob for example.

 

Index definition.


Let's install the wrapper package Cogworks.AzureSearch into AzureSearchExample.Core. The entire principle on which Azure Search is based on is indexes so we need to create an index describing the domain model:

 

public class PostModel : IAzureModel
{
    [Key]
    [IsSearchable]
    [IsFilterable]
     public string Id { get; set; }




    [IsSearchable]
    [IsRetrievable(true)]
    [IndexAnalyzer(AnalyzerName.AsString.Whitespace)]
    [SearchAnalyzer(AnalyzerName.AsString.Whitespace)]
     public string Title { get; set; }




    [IsSearchable]
    [IndexAnalyzer(AnalyzerName.AsString.Whitespace)]
    [SearchAnalyzer(AnalyzerName.AsString.Whitespace)]
     public string Content { get; set; }




    [IsSortable]
    [IsFilterable]
     public DateTime PublishDate { get; set; }




    [IsRetrievable(true)]
     public string UrlSegment { get; set; }




    [IsSearchable]
    [IsFilterable]
    [IsRetrievable(true)]
    [IndexAnalyzer(AnalyzerName.AsString.Whitespace)]
    [SearchAnalyzer(AnalyzerName.AsString.Whitespace)]
     public string Author { get; set; }
}

As you see we need to implement a markup interface IAzureModel. This is needed for working on a dedicated index with dedicated operations that we’ll show you later in the blog. 

To work properly with the index, our models need to have defined Azure Search attributes that can tell us more about data search possibilities (configurations and sometimes complex definitions) - more details can be found here.

Now we need to communicate with the Azure Search service and our indexes.

Configuration.


First thing is to add into AzureSearchExample.App one of Azure Search IoC extension methods (the extension method depends on the container you are using, please see above links for dedicated IoC). In your container configuration make the following:

container.RegisterAzureSearch()
    .RegisterClientOptions("[AzureSearchServiceName]", "[AzureSearchCredentials]")
    .RegisterIndexOptions(false, false) // for now required
    .RegisterIndexDefinitions<PostModel>("blog-posts");

Auto index creation or updating.

If you don't want to manually create index definition in the Azure Portal then you can use an initializer to create or update index definition automatically. In your startup initializer perform the following:

 

public class SomeStartupService
{
   private readonly IAzureInitializer<PostModel> _initializer;
   public SomeStartupService(IAzureInitializer<PostModel> initializer)
       => _initializer = initializer;
   public async Task Initialize()
       => await _initializer.InitializeAsync();
}



Note: In the future, there will be a functionality to get all initializers for all indexes and perform it at once.

 

Adding to index elements.

public class SomeIndexService
{
   private readonly IAzureDocumentOperation<PostModel> _postDocumentOperation;
   public SomeStartupService(IAzureDocumentOperation<PostModel> postDocumentOperation)
       => _postDocumentOperation = postDocumentOperation;
   public async Task SingleEntryAddOrUpdateExample()
   {
       var result = await _postDocumentOperation.AddOrUpdateDocumentAsync(new PostModel
       {
           Id = "single-id",
           Title = "First post",
           PublishDate = DateTime.UtcNow,
           Content = "First post",
           Author = "Author"
        });




       if (!result.Succeeded)
       {
           // handle it
        }




       // updating existing entry that match Id key
       await _postDocumentOperation.AddOrUpdateDocumentAsync(new PostModel
       {
          Id = "single-id",
           Title = "First post",
           PublishDate = DateTime.UtcNow,
           Content = "First post...",
           Author = "Author"
       });
    }




   
   public async Task MultipleEntriesAddOrUpdateExample()
   {
      var items = new List<PostModel>
       {
          new PostModel
          {
              Id = "0",
              Title = "Some 0 Title",
              PublishDate = DateTime.UtcNow,
              Content = "Some 0 Content",
              Author = "Author 0"
          },
          new PostModel
          {
              Id = "1",
             Title = "Some 1 Title",
              PublishDate = DateTime.UtcNow,
              Content = "Some 1 Content",
              Author = "Author 1"
          },
        };




       _ = await _postDocumentOperation.AddOrUpdateDocumentsAsync(items);
  }

 

Removing from index elements.

public class SomeIndexService
{
    private readonly IAzureDocumentOperation<PostModel> _postDocumentOperation;




   public SomeStartupService(IAzureDocumentOperation<PostModel> postDocumentOperation)
        => _postDocumentOperation = postDocumentOperation;




   //...
   // previous adding logic
    //...




   public async Task SingleEntryTryRemovingExample()
   {
       var result = await _postDocumentOperation.TryRemoveDocumentAsync(new PostModel
       {
           Id = "single-id",
        });




       if (!result.Succeeded)
       {
           // handle it
       }
    }




   public async Task MultipleEntriesTryRemovingExample()
   {
       var items = new List<PostModel>
       {
          new PostModel
          {
             Id = "0"
          },
          new PostModel
          {
             Id = "1"
          },
        };




       _ = await _postDocumentOperation.TryRemoveDocumentsAsync(items);
   }
}
Searching
public class BlogPostSurfaceController : SurfaceController
{
    private readonly IAzureSearch<PostModel> _blogPostSearch;


   public BlogPostSurfaceController(IAzureSearch<PostModel> blogSearch)
        => _blogPostSearch = blogPostSearch;


   public void GetTopThreeLatestBlogPosts()
   {
       var results = _blogPostSearch.Search("*", new AzureSearchParameters
       {
           Take = 3,
           OrderBy = new List<string>
           {
               nameof(PostModel.PublishDate) + " desc"
           }
       });
    }

 

Domain searching.

public interface IAuthorBlogSearchService
{
   IEnumerable<PostModel> GetAuthorBlogPosts(string author);
}


public class AuthorBlogSearchService : AzureSearch<PostModel>, IAuthorBlogSearchService
{
   public AuthorBlogSearchService(IAzureDocumentSearch<PostModel> azureSearchRepository) : base(azureSearchRepository)
   {
    }


   public IEnumerable<PostModel> GetAuthorBlogPosts(string author)
   {
        var authorBaseFilter = nameof(PostModel.Author).EqualsValue(author);




       var results = Search("*", new AzureSearchParameters
       {
           Filter = authorBaseFilter,
           OrderBy = new List<string> { nameof(PostModel.PublishDate) + " desc" },
           Skip = 0,
           Take = 10
        });




       return results.Results
           .Select(r => r.Document)
           .ToArray();
   }
}

And then register our custom domain search in the IoC using dedicated extension method:

 

container.RegisterAzureSearch()
  .RegisterClientOptions("[AzureSearchServiceName]", "[AzureSearchCredentials]")
   .RegisterIndexOptions(false, false) // for now required
   .RegisterIndexDefinitions<PostModel>("blog-posts")
    .RegisterDomainSearcher<AuthorBlogSearchService, IAuthorBlogSearchService, PostModel>();

Usage example

 

public class BlogPostSurfaceController : SurfaceController
{
   private readonly IAzureSearch<PostModel> _blogPostSearch;
    private readonly IAuthorBlogSearchService _authorBlogSearch;



   public BlogPostSurfaceController(
       IAzureSearch<PostModel> blogSearch,
       IAuthorBlogSearchService authorBlogSearch)
       => (_blogPostSearch, _authorBlogSearch) = (blogPostSearch, authorBlogSearch);
   
   //...
  // standard search
    //...



   public void GetAuthorBlogPosts()
   {
       var results = _authorBlogSearchService.GetAuthorBlogPosts("Author");
   }
}

Advanced usage.

Sometimes you need something more advanced! For these times, use all available API methods for all index and search operations. All you need to do is to inject IAzureSearchRepository<> of your index model (in our case IAzureSearchRepository<PostModel>).

 

Index operations.

public class SomeIndexService
{
   private readonly IAzureIndexOperation<PostModel> _indexOperation;
   public SomeIndexService(IAzureIndexOperation<PostModel> indexOperation)
        => _indexOperation = indexOperation;




   public async Task Work()
   {
       _ = await _indexOperation.IndexExistsAsync();
       _ = await _indexOperation.IndexCreateOrUpdateAsync();
       _ = await _indexOperation.IndexClearAsync();
       await _indexOperation.IndexDeleteAsync()
    }

All methods - repository.

 

public class SomeIndexService
{
   private readonly IAzureSearchRepository<PostModel> _postRepository;
   public SomeIndexService(IAzureSearchRepository<PostModel> postRepository)
        => _postRepository = postRepository;




   public async Task Work()
   {
       // all API available for all presented above methods, including document, index, search operations
   }
}

That's it!

Hopefully, we have been able to show you how easy it is to work with our wrapper on Azure Search so you can create a single standard and convention to work with Azure Search and connect it with IoC! Our open source creation is available for download on Nuget.org and GitHub Packages. As always, we value the expertise of the open source community so we welcome any feedback you have...

 

Feel free to reach out to our dev team on socials! :)

 

Cogworks.

  • Image for Generate Free SSL Certificates and Bind It to Azure Webapp With Devops Build

    Generate Free SSL Certificates and Bind It to Azure Webapp With Devops

  • Image for Do one thing. Managing your mind in the workplace. Strategy

    Do one thing. Managing your mind in the workplace.

  • Image for Umbraco v8: How to Pick a Block Style Editor Build

    Umbraco v8: How to Pick a Block Style Editor

  • Image for How to Simplify Azure Search Implementations Build

    How to Simplify Azure Search Implementations

Ready to collaborate ?

Get in touch to see how we can transform your digital presence.

Send us a message