{"id":55815,"date":"2020-10-29T08:00:31","date_gmt":"2020-10-29T07:00:31","guid":{"rendered":"https:\/\/code-maze.com\/?p=55815"},"modified":"2024-04-28T14:44:03","modified_gmt":"2024-04-28T12:44:03","slug":"elasticsearch-aspnet-core","status":"publish","type":"post","link":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/","title":{"rendered":"Elasticsearch in ASP.NET Core"},"content":{"rendered":"<p><span style=\"font-weight: 400;\">In this article, we are going to use the popular open-source search and analytics engine &#8211; Elasticsearch to power a simple search system in ASP.NET Core.<\/span><\/p>\n<p><span style=\"color: #ff0000;\"><span style=\"color: #000000;\"><div style=\"padding: 20px; border-left: 5px #dc2323 solid; display: block; margin-bottom: 20px; box-shadow: 1px 1px 5px 0px lightgrey;\">To download the source code for this article, visit <a href=\"https:\/\/github.com\/CodeMazeBlog\/elasticsearch-aspnet-core.git\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">Elasticsearch in ASP.NET Core<\/a> repositroy<\/div><\/span><\/span><\/p>\n<p><span style=\"font-weight: 400;\">Before we jump into code, in the first section let\u2019s spend a moment explaining Elasticsearch and some common use cases.<\/span><\/p>\n<h2><a id=\"theory\"><\/a>What is Elasticsearch?<\/h2>\n<p><span style=\"font-weight: 400;\">Elasticsearch is a free, open-source search database based on the Lucene search library.\u00a0<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Some key features include:<\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Distributed and scalable, including the ability for sharding and replicas<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Documents stored as JSON<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">All interactions over a RESTful HTTP API<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Handy companion software called Kibana which allows interrogation and analysis of data<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">A wealth of client-side libraries for all popular languages<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">In a <\/span><a href=\"https:\/\/code-maze.com\/cqrs-mediatr-in-aspnet-core\/\" target=\"_blank\" rel=\"noopener noreferrer\">previous article<\/a><span style=\"font-weight: 400;\">, we discussed CQRS and how sometimes we\u2019d like to split out the read system into a separate database. Elasticsearch can fit this situation perfectly, as it\u2019s optimized for the read scenarios and provides near real-time search functionality because of the way the engine is designed.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">In the next section, let\u2019s look at how to connect to our local Elasticsearch cluster in an ASP.NET Core application.<\/span><\/p>\n<h2><a id=\"data\"><\/a>Adding Data to Elasticsearch<\/h2>\n<p><span style=\"font-weight: 400;\">There are a number of ways to add data to Elasticsearch, but a simple way for our purposes is to make use of the Bulk REST API, which allows us to send simple curl requests to Elasticsearch. <\/span><span style=\"font-weight: 400;\">Documents in Elasticsearch are stored in \u201cindexes\u201d, which can be thought of as \u201ctables\u201d in a relational database.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">In this article, we are going to use \u201cBooks\u201d as sample data. You can download the sample data file <\/span><a href=\"https:\/\/github.com\/CodeMazeBlog\/elasticsearch-aspnet-core\/tree\/main\/sample-data\" target=\"_blank\" rel=\"nofollow noopener noreferrer\">here<\/a><span style=\"font-weight: 400;\">.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s open up a command prompt and run the command (change the port as necessary):<\/span><\/p>\n<p><code><span style=\"font-weight: 400;\">curl -XPOST localhost:9200\/books\/book\/_bulk --data-binary @sample-data.json -H \"Content-Type: application\/json\"<\/span><\/code><\/p>\n<p><span style=\"font-weight: 400;\">The command simply reads the sample JSON file, and adds the data to a new index called \u201cbooks\u201d. It\u2019s worth mentioning here that instead of allowing Elasticsearch to create the document mappings for us (what we are doing here), we can have more control over this behavior. However, that\u2019s a more advanced topic so here we are just accepting the defaults.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">We should see lots of output in the command window signifying all the successful index operations. To confirm everything worked, let\u2019s open up a browser and go to <\/span><code>http:\/\/localhost:9200\/_search<\/code>:<\/p>\n<p><a href=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Data.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-55816\" src=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Data.jpg\" alt=\"Elasticsearch Data\" width=\"603\" height=\"715\" srcset=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Data.jpg 603w, https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Data-253x300.jpg 253w\" sizes=\"auto, (max-width: 603px) 100vw, 603px\" \/><\/a><\/p>\n<p><span style=\"font-weight: 400;\">We can see that our sample data was inserted successfully into Elasticsearch, which is fantastic!<\/span><\/p>\n<p><span style=\"font-weight: 400;\">In the next section, we\u2019ll see how to connect to Elasticsearch in ASP.NET.<\/span><\/p>\n<h2><a id=\"connecting\"><\/a>Connecting to Elasticsearch in ASP.NET Core<\/h2>\n<p><span style=\"font-weight: 400;\">To demonstrate how to use Elasticsearch in ASP.NET Core, we\u2019re going to create a simple web application with a text field input. When the user enters some text and clicks a button, we\u2019ll send that search query to Elasticsearch.<\/span><\/p>\n<h3>Creating the ASP.NET Core MVC application<\/h3>\n<p><span style=\"font-weight: 400;\">Let\u2019s get started by creating a standard ASP.NET Core application in Visual Studio, choosing Web Application (MVC) as the type:<\/span><\/p>\n<p><a href=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Creating-a-new-ASP.NET-Core-MVC-application.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-55818\" src=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Creating-a-new-ASP.NET-Core-MVC-application.jpg\" alt=\"Creating a new ASP.NET Core MVC application\" width=\"948\" height=\"618\" srcset=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Creating-a-new-ASP.NET-Core-MVC-application.jpg 948w, https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Creating-a-new-ASP.NET-Core-MVC-application-300x196.jpg 300w, https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Creating-a-new-ASP.NET-Core-MVC-application-768x501.jpg 768w\" sizes=\"auto, (max-width: 948px) 100vw, 948px\" \/><\/a><\/p>\n<h3>Creating the model<\/h3>\n<p><span style=\"font-weight: 400;\">We will need a class to deserialize the JSON results from Elasticsearch, so let\u2019s add the following class called \u201cBook\u201d to the Models folder:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">public class Book\r\n{\r\n    public string Title { get; set; }\r\n    public string Isbn { get; set; }\r\n    public int PageCount { get; set; }\r\n    public string ThumbnailUrl { get; set; }\r\n    public string ShortDescription { get; set; }\r\n    public string LongDescription { get; set; }\r\n    public string Status { get; set; }\r\n    public string Authors { get; set; }\r\n    public string Categories { get; set; }\r\n}<\/pre>\n<h3>Creating an Elasticsearch connection<\/h3>\n<p><span style=\"font-weight: 400;\">To connect to Elasticsearch, we\u2019ll use the official NEST library developed by the team at Elastic.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s open up the Package Manager Console and install NEST:<\/span><\/p>\n<p><code><span style=\"font-weight: 400;\">PM&gt; install-package NEST<\/span><\/code><\/p>\n<p><span style=\"font-weight: 400;\">Next, let\u2019s open up Startup.cs and create the connection in the ConfigureServices method:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">var pool = new SingleNodeConnectionPool(new Uri(\"http:\/\/localhost:9200\"));\r\nvar settings = new ConnectionSettings(pool)\r\n    .DefaultIndex(\u201cbooks\u201d);\r\nvar client = new ElasticClient(settings);\r\nservices.AddSingleton(client);<\/pre>\n<p><span style=\"font-weight: 400;\">There are a lot more configuration options available when creating the connection, but let\u2019s keep things very simple here and keep all the defaults.<\/span><\/p>\n<h3>Creating the Search interface<\/h3>\n<p><span style=\"font-weight: 400;\">Next, let\u2019s open up Index.cshtml and add the following HTML:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">@model Nest.ISearchResponse&lt;Book&gt;\r\n\r\n&lt;form asp-controller=\"Home\" asp-action=\"Index\" method=\"post\"&gt;\r\n    &lt;input type=\"text\" name=\"query\" \/&gt;\r\n    &lt;input type=\"submit\" value=\"Search\" \/&gt;\r\n&lt;\/form&gt;\r\n\r\n@if (Model.Documents.Any())\r\n{\r\n    &lt;h3&gt;Search Results&lt;\/h3&gt;\r\n    &lt;ul&gt;\r\n    @foreach (var book in Model.Documents)\r\n    {\r\n        &lt;li&gt;Title: @book.Title (ISBN: @book.Isbn)&lt;\/li&gt;\r\n    }\r\n    &lt;\/ul&gt;\r\n}<\/pre>\n<p><span style=\"font-weight: 400;\">Let\u2019s discuss our HTML:<\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">First, we strongly type the view to ISearchResponse&lt;Book&gt;. This is the type of response from Elasticsearch calls<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Then we add an HTML form which will allow us to post the search query back to the server, which we will then pass along to Elasticsearch and render the results.<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Finally, we add a simple loop that iterates through the matching documents and renders the title and ISBN.<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">Now that we\u2019ve got the foundation set up, in the next section we\u2019ll explore the foundation of searches in Elasticsearch, \u201cQueries\u201d.<\/span><\/p>\n<h2><a id=\"queries\"><\/a>Elasticsearch Queries<\/h2>\n<p><span style=\"font-weight: 400;\">Now that we have our data in Elasticsearch, we want to be able to query it. Elasticsearch offers a very powerful DSL to perform numerous types of search operations, and the NEST library offers two approaches to use that DSL: object initializer syntax, and a Fluent API. In this article, we are going to use the Fluent API.<\/span><\/p>\n<h3>MatchAll query<\/h3>\n<p><span style=\"font-weight: 400;\">The simplest query of all is the MatchAll query, which as the name suggests returns all the documents in an index. It can be likened to a \u201cSELECT *\u201d query in a relational database.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s open up HomeController and firstly assign a local instance of ElasticClient:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">private readonly ElasticClient _client;\r\n\r\npublic HomeController(ILogger&lt;HomeController&gt; logger, ElasticClient client)\r\n{\r\n    _logger = logger;\r\n    _client = client;\r\n}<\/pre>\n<p><span style=\"font-weight: 400;\">Next, let\u2019s modify the Index() method:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">public IActionResult Index()\r\n{\r\n    var results = _client.Search&lt;Book&gt;(s =&gt; s\r\n        .Query(q =&gt; q\r\n            .MatchAll()\r\n        )\r\n    );\r\n\r\n    return View(results);\r\n}<\/pre>\n<p><span style=\"font-weight: 400;\">Here we\u2019re using the Search method on the client, specifying our Book model as the type parameter. This tells NEST how to deserialize the results coming back from Elasticsearch.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">We\u2019re then calling the MatchAll() method on the Query() method to do the search and return the View with the results.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">If we run our app, we should see the following:<\/span><\/p>\n<p><a href=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-MatchAll.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-55819\" src=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-MatchAll.jpg\" alt=\"Elasticsearch MatchAll\" width=\"481\" height=\"334\" srcset=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-MatchAll.jpg 481w, https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-MatchAll-300x208.jpg 300w\" sizes=\"auto, (max-width: 481px) 100vw, 481px\" \/><\/a><\/p>\n<p><span style=\"font-weight: 400;\">These results are the first ten books from the index.\u00a0<\/span><\/p>\n<h3>Term query<\/h3>\n<p><span style=\"font-weight: 400;\">Probably the most common query in Elasticsearch is the Term query. This allows us to find documents matching an exact query, which is great for scenarios like searching by ID or a simple value.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s modify our Index() method again:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">public IActionResult Index(string query)\r\n{\r\n    ISearchResponse&lt;Book&gt; results;\r\n\r\n    if (!string.IsNullOrWhiteSpace(query))\r\n    {\r\n        results = _client.Search&lt;Book&gt;(s =&gt; s\r\n            .Query(q =&gt; q\r\n                .Term(t =&gt; t\r\n                    .Field(f =&gt; f.Isbn)\r\n                    .Value(query)\r\n                )\r\n            )\r\n        );\r\n    }\r\n    else\r\n    {\r\n        results = _client.Search&lt;Book&gt;(s =&gt; s\r\n            .Query(q =&gt; q\r\n                .MatchAll()\r\n            )\r\n        );\r\n    }\r\n\r\n    return View(results);\r\n}<\/pre>\n<p><span style=\"font-weight: 400;\">We\u2019ve amended our code to accept a parameter called \u201cquery\u201d into the method. This corresponds to the text field submitted in the form post, which if supplied uses the Term method to search upon the ISBN field.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s run our application again, and if we enter one of the ISBNs and hit search, the page should be refreshed with the expected result:<\/span><\/p>\n<p><a href=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Term.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-55820\" src=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Term.jpg\" alt=\"Elasticsearch Term\" width=\"402\" height=\"104\" srcset=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Term.jpg 402w, https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Term-300x78.jpg 300w, https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Term-400x104.jpg 400w\" sizes=\"auto, (max-width: 402px) 100vw, 402px\" \/><\/a><\/p>\n<h3>Match query<\/h3>\n<p><span style=\"font-weight: 400;\">Often with search systems, we want the user to enter in some text, and we want that text to match any part of the content in the document. That\u2019s where the Match query comes in.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s modify the first if statement in the Index method again:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">results = _client.Search&lt;Book&gt;(s =&gt; s\r\n    .Query(q =&gt; q\r\n        .Match(t =&gt; t\r\n            .Field(f =&gt; f.Title)\r\n            .Query(query)\r\n        )\r\n    )\r\n);<\/pre>\n<p><span style=\"font-weight: 400;\">The syntax is very similar to the Term query. This time we\u2019re searching upon the Title field, which has multiple words, which Elasticsearch analyses in order for us to query upon.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s run our application again, and this time enter the text <\/span><i><span style=\"font-weight: 400;\">android <\/span><\/i><span style=\"font-weight: 400;\">and hit Search:<\/span><\/p>\n<p><a href=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Match.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-55821\" src=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Match.jpg\" alt=\"Elasticsearch Match\" width=\"472\" height=\"232\" srcset=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Match.jpg 472w, https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Match-300x147.jpg 300w\" sizes=\"auto, (max-width: 472px) 100vw, 472px\" \/><\/a><\/p>\n<p><span style=\"font-weight: 400;\">There are a few key points here worth mentioning:<\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">We match multiple documents<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">All matching documents contain the text \u201candroid\u201d in the title<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Even though we specified android in lowercase, it still matched documents with Android (title case). This behavior can be overridden with custom analyzers, but that\u2019s an advanced topic for another time.<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">There are a lot of other queries supported by Elasticsearch, but the Term and Match queries form the basic use cases.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">In the next section, we\u2019ll go over another Elasticsearch feature called Aggregations.<\/span><\/p>\n<h2><a id=\"aggregations\"><\/a>Elasticsearch Aggregations<\/h2>\n<p><span style=\"font-weight: 400;\">When we have a large data set, often we want to summarise or \u2018aggregate\u2019 that data, to serve functionality like:<\/span><\/p>\n<ul>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Summary page<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Paging or counts<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Faceted navigation<\/span><\/li>\n<li style=\"font-weight: 400;\"><span style=\"font-weight: 400;\">Tag bubbles<\/span><\/li>\n<\/ul>\n<p><span style=\"font-weight: 400;\">This is where we can use aggregations to quickly compute results.<\/span><\/p>\n<h3>Range Aggregation for PageCount<\/h3>\n<p><span style=\"font-weight: 400;\">Since we are dealing with books, it might be interesting to see some statistics on the number of pages. For this, we can make use of the Range aggregation.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s modify our MatchAll query to include this aggregation:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">results = _client.Search&lt;Book&gt;(s =&gt; s\r\n    .Query(q =&gt; q\r\n        .MatchAll()\r\n    )\r\n    .Aggregations(a =&gt; a\r\n        .Range(\"pageCounts\", r =&gt; r\r\n            .Field(f =&gt; f.PageCount)\r\n            .Ranges(r =&gt; r.From(0),\r\n                    r =&gt; r.From(200).To(400),\r\n                    r =&gt; r.From(400).To(600),\r\n                    r =&gt; r.From(600)\r\n            )\r\n        )\r\n    )\r\n);<\/pre>\n<p><span style=\"font-weight: 400;\">Here in addition to our MatchAll query, we are using the Aggregations method to specify some aggregations, in our case the \u201cRange\u201d aggregation.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s open our Index.cshtml and add some HTML to render our new aggregation:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">@if (Model.Aggregations != null)\r\n{\r\n    &lt;h3&gt;Aggregations&lt;\/h3&gt;\r\n\r\n    @if (Model.Aggregations.ContainsKey(\"pageCounts\"))\r\n    {\r\n        &lt;h4&gt;PageCounts (Range)&lt;\/h4&gt;\r\n        &lt;ul&gt;\r\n        @foreach (var bucket in Model.Aggregations.Range(\"pageCounts\").Buckets)\r\n        {\r\n            &lt;li&gt;@bucket.Key: @bucket.DocCount&lt;\/li&gt;\r\n        }\r\n        &lt;\/ul&gt;\r\n    }\r\n}<\/pre>\n<p><span style=\"font-weight: 400;\">Similar to the previous HTML, we\u2019re simply looping through each \u201cbucket\u201d (or group) in the <code>pageCounts<\/code> aggregations, and rendering the information about it.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s run our app again, and we should see the results:<\/span><\/p>\n<p><a href=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Range-Aggregation.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-55822\" src=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Range-Aggregation.jpg\" alt=\"Elasticsearch Range Aggregation\" width=\"239\" height=\"184\" \/><\/a><\/p>\n<h3>Terms Aggregation for Categories<\/h3>\n<p><span style=\"font-weight: 400;\">We mentioned previously the use case of tag bubbles or faceted UI systems. This is where the Terms aggregation comes in. It\u2019s similar to a \u201cGROUP BY\u201d clause in a relational database, where we can get statistics on various words (or \u2018terms\u2019) across documents.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s modify our Aggregations code again:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">results = _client.Search&lt;Book&gt;(s =&gt; s\r\n    .Query(q =&gt; q\r\n        .MatchAll()\r\n    )\r\n    .Aggregations(a =&gt; a\r\n        .Range(\"pageCounts\", r =&gt; r\r\n            .Field(f =&gt; f.PageCount)\r\n            .Ranges(r =&gt; r.From(0),\r\n                    r =&gt; r.From(200).To(400),\r\n                    r =&gt; r.From(400).To(600),\r\n                    r =&gt; r.From(600)\r\n            )\r\n        )\r\n        .Terms(\"categories\", t =&gt; t\r\n            .Field(\"categories.keyword\")\r\n        )\r\n    )\r\n);<\/pre>\n<p><span style=\"font-weight: 400;\">Notice we\u2019ve added the \u201cTerms\u201d aggregation in addition to the existing Range aggregation. An interesting part here is the field value set to the string \u201ccategories.keyword\u201d. The reason for this is that Terms aggregation is an expensive operation, and isn\u2019t usually done on \u201ctext\u201d fields. However, all text fields by default have backing \u201ckeyword\u201d sub-fields, which are optimized for this use case.\u00a0<\/span><\/p>\n<p><span style=\"font-weight: 400;\">This approach shows how we can do the terms aggregation on text fields. Another approach would be to explicitly map the field as \u201ckeyword\u201d, but then we lose the full-text search functionality like \u201cMatch\u201d. So this approach gives us the best of both worlds.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">Let\u2019s modify our Index.cshtml for our new aggregation:<\/span><\/p>\n<pre class=\"EnlighterJSRAW\" data-enlighter-language=\"csharp\">@if (Model.Aggregations != null)\r\n{\r\n    &lt;h3&gt;Aggregations&lt;\/h3&gt;\r\n\r\n    @if (Model.Aggregations.ContainsKey(\"pageCounts\"))\r\n    {\r\n        &lt;h4&gt;PageCounts (Range)&lt;\/h4&gt;\r\n        &lt;ul&gt;\r\n        @foreach (var bucket in Model.Aggregations.Range(\"pageCounts\").Buckets)\r\n        {\r\n            &lt;li&gt;@bucket.Key: @bucket.DocCount&lt;\/li&gt;\r\n        }\r\n        &lt;\/ul&gt;\r\n    }\r\n\r\n    @if (Model.Aggregations.ContainsKey(\"categories\"))\r\n    {\r\n        &lt;h4&gt;Categories (Terms)&lt;\/h4&gt;\r\n        &lt;ul&gt;\r\n        @foreach (var bucket in Model.Aggregations.Terms(\"categories\").Buckets)\r\n        {\r\n            &lt;li&gt;@bucket.Key: @bucket.DocCount&lt;\/li&gt;\r\n        }\r\n        &lt;\/ul&gt;\r\n    }\r\n}<\/pre>\n<p><span style=\"font-weight: 400;\">Let\u2019s run our app again:<\/span><\/p>\n<p><a href=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Terms-Aggregation.jpg\"><img loading=\"lazy\" decoding=\"async\" class=\"alignnone size-full wp-image-55823\" src=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/Elasticsearch-Terms-Aggregation.jpg\" alt=\"Elasticsearch Terms Aggregation\" width=\"229\" height=\"286\" \/><\/a><\/p>\n<p><span style=\"font-weight: 400;\">As expected we now have the number of documents matching each of the categories.<\/span><\/p>\n<p><span style=\"font-weight: 400;\">This approach shows how we can do the terms aggregation on text fields. Another approach would be to explicitly map the field as \u201ckeyword\u201d, but then we lose the full-text search functionality like \u201cMatch\u201d. So this approach gives us the best of both worlds.<\/span><\/p>\n<h2>Conclusion<\/h2>\n<p><span style=\"font-weight: 400;\">In this article, we provided a very basic introduction to Elasticsearch in ASP.NET Core. We touched on some basic queries and aggregations that are the starting point for most developers. Elasticsearch is a very mature database with lots more features that have been introduced here, but hopefully, this puts you in the right direction.<\/span><\/p><\/p>\n","protected":false},"excerpt":{"rendered":"<p>In this article, we are going to use the popular open-source search and analytics engine &#8211; Elasticsearch to power a simple search system in ASP.NET Core. Before we jump into code, in the first section let\u2019s spend a moment explaining Elasticsearch and some common use cases. What is Elasticsearch? Elasticsearch is a free, open-source search [&hellip;]<\/p>\n","protected":false},"author":155,"featured_media":56193,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"_et_pb_use_builder":"","_et_pb_old_content":"","_et_gb_content_width":"","footnotes":""},"categories":[12],"tags":[803,79,801,802],"class_list":["post-55815","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-csharp","tag-aggregations","tag-asp-net-core","tag-elasticsearch","tag-queries","et-has-post-format-content","et_post_format-et-post-format-standard"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v24.7 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Elasticsearch in ASP.NET Core - Code Maze<\/title>\n<meta name=\"description\" content=\"How to use Elasticsearch in ASP.NET Core, including using queries and aggregations to build a search system.\" \/>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Elasticsearch in ASP.NET Core - Code Maze\" \/>\n<meta property=\"og:description\" content=\"How to use Elasticsearch in ASP.NET Core, including using queries and aggregations to build a search system.\" \/>\n<meta property=\"og:url\" content=\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/\" \/>\n<meta property=\"og:site_name\" content=\"Code Maze\" \/>\n<meta property=\"article:published_time\" content=\"2020-10-29T07:00:31+00:00\" \/>\n<meta property=\"article:modified_time\" content=\"2024-04-28T12:44:03+00:00\" \/>\n<meta property=\"og:image\" content=\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png\" \/>\n\t<meta property=\"og:image:width\" content=\"1100\" \/>\n\t<meta property=\"og:image:height\" content=\"620\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/png\" \/>\n<meta name=\"author\" content=\"Ryan Miranda\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:creator\" content=\"@CodeMazeBlog\" \/>\n<meta name=\"twitter:site\" content=\"@CodeMazeBlog\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Ryan Miranda\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"10 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":[\"Article\",\"BlogPosting\"],\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#article\",\"isPartOf\":{\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/\"},\"author\":{\"name\":\"Ryan Miranda\",\"@id\":\"https:\/\/code-maze.com\/#\/schema\/person\/2a6a28a959468b5fa087c623a938fd8c\"},\"headline\":\"Elasticsearch in ASP.NET Core\",\"datePublished\":\"2020-10-29T07:00:31+00:00\",\"dateModified\":\"2024-04-28T12:44:03+00:00\",\"mainEntityOfPage\":{\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/\"},\"wordCount\":1649,\"commentCount\":8,\"publisher\":{\"@id\":\"https:\/\/code-maze.com\/#organization\"},\"image\":{\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png\",\"keywords\":[\"Aggregations\",\"asp.net core\",\"Elasticsearch\",\"Queries\"],\"articleSection\":[\"C#\"],\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"CommentAction\",\"name\":\"Comment\",\"target\":[\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#respond\"]}]},{\"@type\":\"WebPage\",\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/\",\"url\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/\",\"name\":\"Elasticsearch in ASP.NET Core - Code Maze\",\"isPartOf\":{\"@id\":\"https:\/\/code-maze.com\/#website\"},\"primaryImageOfPage\":{\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage\"},\"image\":{\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage\"},\"thumbnailUrl\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png\",\"datePublished\":\"2020-10-29T07:00:31+00:00\",\"dateModified\":\"2024-04-28T12:44:03+00:00\",\"description\":\"How to use Elasticsearch in ASP.NET Core, including using queries and aggregations to build a search system.\",\"breadcrumb\":{\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/\"]}]},{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage\",\"url\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png\",\"contentUrl\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png\",\"width\":1100,\"height\":620,\"caption\":\"elasticsearch in asp.net core\"},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/code-maze.com\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Elasticsearch in ASP.NET Core\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/code-maze.com\/#website\",\"url\":\"https:\/\/code-maze.com\/\",\"name\":\"Code Maze\",\"description\":\"Learn. Code. Succeed.\",\"publisher\":{\"@id\":\"https:\/\/code-maze.com\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/code-maze.com\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Organization\",\"@id\":\"https:\/\/code-maze.com\/#organization\",\"name\":\"Code Maze\",\"url\":\"https:\/\/code-maze.com\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/code-maze.com\/#\/schema\/logo\/image\/\",\"url\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/01\/Code-Maze-Only-Logo-Transparent-HRez.png\",\"contentUrl\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/01\/Code-Maze-Only-Logo-Transparent-HRez.png\",\"width\":3511,\"height\":3510,\"caption\":\"Code Maze\"},\"image\":{\"@id\":\"https:\/\/code-maze.com\/#\/schema\/logo\/image\/\"},\"sameAs\":[\"https:\/\/x.com\/CodeMazeBlog\"]},{\"@type\":\"Person\",\"@id\":\"https:\/\/code-maze.com\/#\/schema\/person\/2a6a28a959468b5fa087c623a938fd8c\",\"name\":\"Ryan Miranda\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/code-maze.com\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2024\/01\/Ryan-Miranda-400-150x150.png\",\"contentUrl\":\"https:\/\/code-maze.com\/wp-content\/uploads\/2024\/01\/Ryan-Miranda-400-150x150.png\",\"caption\":\"Ryan Miranda\"},\"description\":\"Ryan is a tech enthusiast with over 15 years of diverse experience spanning various industries, team sizes, and project approaches. His focus is on big picture thinking, leading high performing engineering teams, and being an ambassador for technology to other business executives, effectively bridging the realms of technology and business.\",\"sameAs\":[\"https:\/\/www.linkedin.com\/in\/rpmir1\/\"],\"url\":\"https:\/\/code-maze.com\/author\/ryanmiranda\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Elasticsearch in ASP.NET Core - Code Maze","description":"How to use Elasticsearch in ASP.NET Core, including using queries and aggregations to build a search system.","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/","og_locale":"en_US","og_type":"article","og_title":"Elasticsearch in ASP.NET Core - Code Maze","og_description":"How to use Elasticsearch in ASP.NET Core, including using queries and aggregations to build a search system.","og_url":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/","og_site_name":"Code Maze","article_published_time":"2020-10-29T07:00:31+00:00","article_modified_time":"2024-04-28T12:44:03+00:00","og_image":[{"width":1100,"height":620,"url":"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png","type":"image\/png"}],"author":"Ryan Miranda","twitter_card":"summary_large_image","twitter_creator":"@CodeMazeBlog","twitter_site":"@CodeMazeBlog","twitter_misc":{"Written by":"Ryan Miranda","Est. reading time":"10 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":["Article","BlogPosting"],"@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#article","isPartOf":{"@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/"},"author":{"name":"Ryan Miranda","@id":"https:\/\/code-maze.com\/#\/schema\/person\/2a6a28a959468b5fa087c623a938fd8c"},"headline":"Elasticsearch in ASP.NET Core","datePublished":"2020-10-29T07:00:31+00:00","dateModified":"2024-04-28T12:44:03+00:00","mainEntityOfPage":{"@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/"},"wordCount":1649,"commentCount":8,"publisher":{"@id":"https:\/\/code-maze.com\/#organization"},"image":{"@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage"},"thumbnailUrl":"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png","keywords":["Aggregations","asp.net core","Elasticsearch","Queries"],"articleSection":["C#"],"inLanguage":"en-US","potentialAction":[{"@type":"CommentAction","name":"Comment","target":["https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#respond"]}]},{"@type":"WebPage","@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/","url":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/","name":"Elasticsearch in ASP.NET Core - Code Maze","isPartOf":{"@id":"https:\/\/code-maze.com\/#website"},"primaryImageOfPage":{"@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage"},"image":{"@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage"},"thumbnailUrl":"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png","datePublished":"2020-10-29T07:00:31+00:00","dateModified":"2024-04-28T12:44:03+00:00","description":"How to use Elasticsearch in ASP.NET Core, including using queries and aggregations to build a search system.","breadcrumb":{"@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/code-maze.com\/elasticsearch-aspnet-core\/"]}]},{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#primaryimage","url":"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png","contentUrl":"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/10\/elasticsearch-in-asp.net-core.png","width":1100,"height":620,"caption":"elasticsearch in asp.net core"},{"@type":"BreadcrumbList","@id":"https:\/\/code-maze.com\/elasticsearch-aspnet-core\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/code-maze.com\/"},{"@type":"ListItem","position":2,"name":"Elasticsearch in ASP.NET Core"}]},{"@type":"WebSite","@id":"https:\/\/code-maze.com\/#website","url":"https:\/\/code-maze.com\/","name":"Code Maze","description":"Learn. Code. Succeed.","publisher":{"@id":"https:\/\/code-maze.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/code-maze.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Organization","@id":"https:\/\/code-maze.com\/#organization","name":"Code Maze","url":"https:\/\/code-maze.com\/","logo":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/code-maze.com\/#\/schema\/logo\/image\/","url":"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/01\/Code-Maze-Only-Logo-Transparent-HRez.png","contentUrl":"https:\/\/code-maze.com\/wp-content\/uploads\/2020\/01\/Code-Maze-Only-Logo-Transparent-HRez.png","width":3511,"height":3510,"caption":"Code Maze"},"image":{"@id":"https:\/\/code-maze.com\/#\/schema\/logo\/image\/"},"sameAs":["https:\/\/x.com\/CodeMazeBlog"]},{"@type":"Person","@id":"https:\/\/code-maze.com\/#\/schema\/person\/2a6a28a959468b5fa087c623a938fd8c","name":"Ryan Miranda","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/code-maze.com\/#\/schema\/person\/image\/","url":"https:\/\/code-maze.com\/wp-content\/uploads\/2024\/01\/Ryan-Miranda-400-150x150.png","contentUrl":"https:\/\/code-maze.com\/wp-content\/uploads\/2024\/01\/Ryan-Miranda-400-150x150.png","caption":"Ryan Miranda"},"description":"Ryan is a tech enthusiast with over 15 years of diverse experience spanning various industries, team sizes, and project approaches. His focus is on big picture thinking, leading high performing engineering teams, and being an ambassador for technology to other business executives, effectively bridging the realms of technology and business.","sameAs":["https:\/\/www.linkedin.com\/in\/rpmir1\/"],"url":"https:\/\/code-maze.com\/author\/ryanmiranda\/"}]}},"_links":{"self":[{"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/posts\/55815","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/users\/155"}],"replies":[{"embeddable":true,"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/comments?post=55815"}],"version-history":[{"count":2,"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/posts\/55815\/revisions"}],"predecessor-version":[{"id":62199,"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/posts\/55815\/revisions\/62199"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/media\/56193"}],"wp:attachment":[{"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/media?parent=55815"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/categories?post=55815"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/code-maze.com\/wp-json\/wp\/v2\/tags?post=55815"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}