Getting Started with Elasticsearch, Part 2 - Searching with a HttpClient

Full source code available here.

In the previous blog post I showed how to setup Elasticsearch, create and index and seed the index with some sample documents. That is not a lot of use without the ability to search it.

In this post I will show how to use a typed HttpClient to perform searches. I’ve chosen not to use the two libraries provided by the Elasticsearch company because I want to stick with JSON requests that I can write and test with any tool like Fiddler, Postman or Rest Client for Visual Studio Code.

If you haven’t worked with HttpClientFactory you can check out my posts on it or the MicroSoft docs page.

The Typed HttpClient

A typed HttpClient lets you, in a sense, hide away that you are using a HttpClient at all. The methods the typed client exposes are more business related than technical - the the type of request, the body of the request, how the response is handled are all hidden away from the consumer. Using a typed client feels like using any other class with exposed method.

This typed client will expose three methods, one to search by company name, one to search my customer name and state, and one to return all results in a paged manner.

Start with an interface that specifies the methods to expose -

1public interface ISearchService
2{
3    Task<string> CompanyName(string companyName);
4    Task<string> NameAndState(string name, string state);
5    Task<string> All(int skip, int take, string orderBy, string direction);
6}

Create the class that implements that interface and takes a HttpClient as a constructor parameter -

1public class SearchService : ISearchService
2{
3    private readonly HttpClient _httpClient;
4    public SearchService(HttpClient httpClient)
5    {
6        _httpClient = httpClient;
7    }
8    //snip...

Implement the search functionality (and yes, I don’t like the amount of escaping I need to send a simple request with a JSON body) -

 1public async Task<string> CompanyName(string companyName)
 2{
 3    string query = @"{{
 4                        ""query"": {{
 5                            ""match_phrase_prefix"": {{ ""companyName"" : ""{0}"" }} 
 6                        }}
 7                    }}";
 8    string queryWithParams = string.Format(query, companyName);
 9    return await SendRequest(queryWithParams);
10}
11
12public async Task<string> NameAndState(string name, string state)
13{
14    string query = @"{{
15                        ""query"": {{
16                            ""bool"": {{
17                                ""must"": [
18                                    {{""match_phrase_prefix"": {{ ""fullName"" : ""{0}"" }} }}
19                                    ,{{""match"": {{ ""address.state"" : ""{1}""}}}} 
20                                ]
21                            }}
22                        }}
23                    }}";
24    string queryWithParams = string.Format(query, name, state);
25    return await SendRequest(queryWithParams);
26}
27
28public async Task<string> All(int skip, int take, string orderBy, string direction)
29{
30    string query = @"{{
31                    ""sort"":{{""{2}"": {{""order"":""{3}""}}}},
32                    ""from"": {0},
33                    ""size"": {1}
34                    }}";
35
36    string queryWithParams = string.Format(query, skip, take, orderBy, direction);
37    return await SendRequest(queryWithParams);
38}

And finally send the requests to the Elasticsearch server -

 1private async Task<string> SendRequest(string queryWithParams)
 2{
 3    var request = new HttpRequestMessage()
 4    {
 5        Method = HttpMethod.Get,
 6        Content = new StringContent(queryWithParams, Encoding.UTF8, "application/json")
 7    };
 8    var response = await _httpClient.SendAsync(request);
 9    var content = await response.Content.ReadAsStringAsync();
10    return content;
11}

The Setup

That’s the typed client taken care of, but it has be added to the HttpClientFactory, that is done in Startup.cs.

In the ConfigureServices(..) method add this -

1services.AddHttpClient<ISearchService, SearchService>(client =>
2{
3    client.BaseAddress = new Uri("http://localhost:9200/customers/_search");
4});

I am running Elasticsearch on localhost:9200.

That’s all there is to registering the HttpClient with the factory. Now all that is left is to use the typed client in the controller.

Searching The typed client is passed to the controller via constructor injection -

 1[ApiController]
 2[Route("[controller]")]
 3public class SearchController : ControllerBase
 4{
 5    private readonly ISearchService _searchService;
 6    public SearchController(ISearchService searchService)
 7    {
 8        _searchService = searchService;
 9    }
10    //snip...

Add some action methods to respond to API requests and call the methods on the typed client.

 1[HttpGet("company/{companyName}")]
 2public async Task<ActionResult> GetCompany(string companyName)
 3{
 4    var result = await _searchService.CompanyName(companyName);
 5    return Ok(result);
 6}
 7
 8[HttpGet("nameAndState/")]
 9public async Task<ActionResult> GetNameAndState(string name, string state)
10{
11    var result = await _searchService.NameAndState(name, state);
12    return Ok(result);
13}
14
15[HttpGet("all/")]
16public async Task<ActionResult> GetAll(int skip = 0, int take = 10, string orderBy = "dateOfBirth", string direction = "asc")
17{
18    var result = await _searchService.All(skip, take, orderBy, direction);
19    return Ok(result);
20}

That’s it. You are up and running with Elasticsearch, a seeded index, and a API to perform searches.

In the next post I’ll show you how to deploy Elasticsearch to AWS with some nice infrastructure as code.

Full source code available here.

comments powered by Disqus

Related