X

Writing simple REST-client for Azure Search

In my last blog post about REST-clients Why Azure REST API-s and how to prepare for using them? I introduced how to write simple base class for Azure REST API-s. In this post we will implement simple REST-client for Azure Search. This is the same code that I’m using in my beer store sample application. I also make some notes about how to use search clients in domain models.

Planning search indexes is important part of successful search in every system. To get started with planning of search indexes on Azure Search feel free to read my blog post Planning and creating Azure Search indexes that uses same product catalogue stuff that I use here.

Getting started with Azure Search client

First let’s focus on some Azure Search specifics. To make request to Azure Search REST API end-point we need the following information to be available for our client:

  • API version – the version of API we are consuming (if we don’t specify correct version then we cannot be sure if API interface is the one we expect),
  • service name – name of our search service, part of API URL,
  • admin key – this is the key to validate that request is coming from valid source.

The code that uses our Azure Search REST client must provide these values to us because our REST client doesn’t deal with configuration settings.

We use the REST client base class from my blog post Why Azure REST API-s and how to prepare for using them?. In its most primitive form our REST client for Azure Search looks like this:

public class RestClient : RestClientBase
{
    private const string _apiVersion = "2015-02-28";
    private readonly string _adminKey;
    private readonly string _serviceName;
 
    public RestClient(string serviceName, string adminKey)
    {
        _serviceName = serviceName;
        _adminKey = adminKey;
    }
 
    public override void AddHeaders(HttpRequestHeaders headers)
    {
        headers.Add("api-key", _adminKey);
    }
}

AddHeaders() method is defined in base class. In this method we add search specific headers to headers collection. This method is called by base class when it builds HTTP requests for search service calls. API version and service name are used in search methods later.

Updating product index

Search is only useful when there is data in search index and therefore we start with product indexing. We create data transfer object (DTO) for search index update. By structure this DTO is simple  – it has product properties and some additional properties to tell Azure Search what product it is and what we want to do with it.

[DataContract]
public class ProductUpdateDto
{
    [DataMember(Name = "@search.action")]
    public string SearchAction { get; set; }
 
    [DataMember(Name = "id")]
    public string Id { get; set; }
 
    [DataMember]
    public string Name { get; set; }
 
    [DataMember]
    public string ShortDescription { get; set; }
 
    [DataMember]
    public string Description { get; set; }
 
    [DataMember]
    public decimal Price { get; set; }
 
    [DataMember]
    public int ProductCategoryId { get; set; }
 
    [DataMember]
    public double? AlcVol { get; set; }
 
    [DataMember]
    public string ImageUrl { get; set; }
 
    [DataMember]
    public string[] AlcVolTags { get; set; }
 
    [DataMember]
    public string ManufacturerName { get; set; }
 
    public ProductUpdateDto()
    {
        AlcVolTags = new string[] { };
    }
}

And here is the REST client method that sends product to search index.

public async Task<IndexItemsResponse> IndexProduct(ProductUpdateDto dto)
{
    var url = string.Format("https://{0}.search.windows.net/indexes/products/docs/index", _serviceName);
    url += "?api-version=" + _apiVersion;
 
    var productToSave = new { value = new ProductUpdateDto[] { dto } };
    var productJson = JsonConvert.SerializeObject(productToSave);
    var result = await Download<IndexItemsResponse>(url, HttpMethod.Post, productJson);
    return result;
}

One thing to notice. Search API methods that update or maintain index will also return response with data when called. When updating index we can update multiple items with same call and this is why response has multiple response values. To represent it in object oriented code we need to classes – one for response object and one for each products indexing result.

[DataContract]
public class IndexItemsResponse
{
    [DataMember(Name = "value")]
    public IndexItemResponse[] Items { get; set; }
}
 
[DataContract]
public class IndexItemResponse
{
    [DataMember(Name = "key")]
    public string Key { get; set; }
 
    [DataMember(Name = "status")]
    public bool Status { get; set; }
 
    [DataMember(Name = "errorMessage")]
    public string ErrorMessage { get; set; }
}

Now we have Azure Search client with one method that indexes products. I skip some architectural stuff here to keep the post smaller. In real scenario you should have some domain level service interface for search and at least one implementation class. This class knows the specifics of given search client and mapping data from domain classes to search DTO-s happens also there.

Also you can have more than one implementation if you introduce search service interface. If you have to switch from one search service to another then you just have to write new implementation and make minor modifications to your dependency injection code.

Searching from product index

Now let’s implement searching. We are using same request and response interface classes thing as before but now we create classes for search. We start with query class. The only purpose of this class is to move query parameters from client layers to domain services and data layers. I also define abstract query base class because some things are common for almost all queries in system.

public abstract class BaseQuery
{
    public int Page { get; set; }
    public int PageSize { get; set; }
    public string OrderBy { get; set; }
    public bool Ascending { get; set; }
 
    public BaseQuery()
    {
        Page = 1;
        PageSize = 10;
        Ascending = true;
    }
}
 
public class ProductQuery : BaseQuery
{
    public int? ProductCategoryId { get; set; }
    public int? ProductTypeId { get; set; }
 
    public string Term { get; set; }
    public int? AlcVolMin { get; set; }
    public int? AlcVolMax { get; set; }
    public decimal? MinPrice { get; set; }
    public decimal? MaxPrice { get; set; }
}

As you can see then all queries inheriting from BaseQuery class have some paging and ordering readiness.

Usually we have some model classes or DTO-s that are easier to use in client layers that domain classes and often we want client layers to communicate with service layer where use cases are implemented. Here is the example product DTO that code here uses.

public class ProductDto
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string ShortDescription { get; set; }
    public string Description { get; set; }
    public int ManufacturerId { get; set; }
    public string ManufacturerName { get; set; }
    public decimal Price { get; set; }
    public int ProductCategoryId { get; set; }
    public string ProductCategoryName { get; set; }
    public string ImageUrl { get; set; }
    public int ProductTypeId { get; set; }
    public string ProductTypeName { get; set; }
    public bool HasImage { get; set; }
}

But search response contains some general data and not just array of items found. By example, if we want to implement paging correctly then we need to ask also the total number of results. It’s common attribute that is independent from results returned. For this we will define ProductSearchResult class.

[DataContract]
public class ProductSearchResponse
{
    [DataMember(Name = "@odata.count")]
    public int Count;
 
    [DataMember(Name = "value")]
    public List<ProductDto> Values;
}

Now we have everything we need to make call to search service. We add new public method for this to our Azure Search REST-client.

public async Task<ProductSearchResponse> SearchProducts(ProductQuery query)
{
    var url = string.Format("https://{0}.search.windows.net/indexes/products/docs/", _serviceName);
    url += "?api-version=" + _apiVersion + "&search=" + WebUtility.UrlEncode(query.Term);
 
    var skip = (query.Page - 1) * query.PageSize;
    var take = query.PageSize;
    string filter = BuildFilter(query);
 
    if (!string.IsNullOrEmpty(filter))
        url += "&$filter=" + WebUtility.UrlEncode(filter);
 
    if (skip > 0)
        url += "&$skip=" + skip;
 
    url += "&$take=" + take;
    url += "&$count=true";
 
    var result = await Download<ProductSearchResponse>(url);
    return result;
}
 
private static string BuildFilter(ProductQuery query)
{
    var filter = "";
 
    if (query.AlcVolMax.HasValue)
        filter += "AlcVol le " + query.AlcVolMax;
 
    if (query.AlcVolMin.HasValue)
    {
        if (!string.IsNullOrEmpty(filter))
            filter += " and ";
 
        filter += "AlcVol ge " + query.AlcVolMin;
    }
 
    // Apply more filters here
 
    return filter;
}

Now our REST-client supports also searching and paging.

Although Azure search service can return many properties for given entity it is still possible that you have to query the entity later from database to get another set of properties that are not available in search index. This kind of “materialization” happens in domain search service classes that hide all those dirty details from calling code.

Wrapping up

Using Azure Search without ready-made client classes is not very complex. We wrote simple REST-client that builds calls to Azure Search service and returns DTO-s known to our domain. I admit that perhaps we got too much classes for this simple thing but we still have valid object model we can use. Of course, all search related classes can be extended by adding more search properties. I also admit that domain part of this story may be a little too general and it’s not very easy to catch for everybody but those who know domain model world better should understand how to interface the code with their systems. Other guys can just take search client code and go with it. As a conclusion we can say that Azure Search is easy also without ready-made API packages.

Liked this post? Empower your friends by sharing it!
Categories: .NET Azure Search
Related Post