Getting started

edit

NEST is a high level Elasticsearch .NET client that still maps very closely to the original Elasticsearch API. All requests and responses are exposed through types, making it ideal for getting up and running quickly.

Under the covers, NEST uses the Elasticsearch.Net low level client to dispatch requests and responses, using and extending many of the types within Elasticsearch.Net. The low level client itself is still exposed on the high level client through the .LowLevel property.

Connecting

edit

To connect to Elasticsearch running locally at http://localhost:9200 is as simple as instantiating a new instance of the client

var client = new ElasticClient();

Often you may need to pass additional configuration options to the client such as the address of Elasticsearch if it’s running on a remote machine. This is where ConnectionSettings come in; an instance can be instantiated to provide the client with different configurations.

var settings = new ConnectionSettings(new Uri("http://example.com:9200"))
    .DefaultIndex("people");

var client = new ElasticClient(settings);

In this example, a default index was also specified to use if no other index is supplied for the request or can be inferred for the POCO generic type parameter in the request. There are many other Configuration options on ConnectionSettings, which it inherits from ConnectionConfiguration, the type used to pass additional configuration options to the low level client in Elasticsearch.Net.

Specifying a default index is optional but NEST may throw an exception if no index can be inferred for a given request. To understand more around how an index can be specified for a request, see Index name inference.

ConnectionSettings is not restricted to being passed a single address for Elasticsearch. There are several different types of Connection pool available in NEST, each with different characteristics, that can be used to configure the client. The following example uses a SniffingConnectionPool seeded with the addresses of three Elasticsearch nodes in the cluster, and the client will use this type of pool to maintain a list of available nodes within the cluster to which it can send requests in a round-robin fashion.

var uris = new[]
{
    new Uri("http://localhost:9200"),
    new Uri("http://localhost:9201"),
    new Uri("http://localhost:9202"),
};

var connectionPool = new SniffingConnectionPool(uris);
var settings = new ConnectionSettings(connectionPool)
    .DefaultIndex("people");

var client = new ElasticClient(settings);

Enabling the Compatibility Mode

edit

The Elasticsearch server version 8.0 is introducing a new compatibility mode that allows you a smoother upgrade experience from 7 to 8. In a nutshell, you can use the latest 7.x Elasticsearch client with an 8.x Elasticsearch server, giving more room to coordinate the upgrade of your codebase to the next major version.

If you want to leverage this functionality, please make sure that you are using the latest 7.x client and set the environment variable ELASTIC_CLIENT_APIVERSIONING to true. The client is handling the rest internally. For every 8.0 and beyond client, you’re all set! The compatibility mode is enabled by default.

Using the Client in a Function-as-a-Service Environment

edit

When using the client in FaaS environments, we recommend you follow the platform recommended approach to store the client in a global variable such that it is reused for requests where possible. See using the Client in a Function-as-a-Service Environment for more information.

Indexing

edit

Once a client had been configured to connect to Elasticsearch, we need to get some data into the cluster to work with.

Imagine we have the following Plain Old CLR Object (POCO)

public class Person
{
    public int Id { get; set; }
    public string FirstName { get; set; }
    public string LastName { get; set; }
}

Indexing a single instance of the POCO either synchronously or asynchronously, is as simple as

var person = new Person
{
    Id = 1,
    FirstName = "Martijn",
    LastName = "Laarman"
};

var indexResponse = client.IndexDocument(person); 

var asyncIndexResponse = await client.IndexDocumentAsync(person); 

synchronous method that returns an IndexResponse

asynchronous method that returns a Task<IndexResponse> that can be awaited

All methods available within NEST are exposed as both synchronous and asynchronous versions, with the latter using the idiomatic *Async suffix on the method name.

This will index the document to the endpoint /people/_doc/1. NEST is smart enough to infer the the id for the document by looking for an Id property on the POCO. Take a look at Ids inference to see other ways in which NEST can be configured to infer an id for a document. The default index configured on ConnectionSettings has been used as the index name for the request.

By default, NEST camel cases the property names on the POCO when serializing the POCO into a JSON document to send to Elasticsearch. You can change this behaviour by using the .DefaultFieldNameInferrer(Func<string,string>) method on ConnectionSettings.

Searching

edit

Now that we have indexed some documents we can begin to search for them.

var searchResponse = client.Search<Person>(s => s
    .From(0)
    .Size(10)
    .Query(q => q
         .Match(m => m
            .Field(f => f.FirstName)
            .Query("Martijn")
         )
    )
);

var people = searchResponse.Documents;

people now holds the first ten people whose first name matches Martijn. The search endpoint for this query is /people/_search and the index ("people") has been determined from

  1. the default index on ConnectionSettings
  2. the Person generic type parameter on the search.

which generates a request to the search endpoint /people/_search, using the default index specified on ConnectionSettings as the index in the search request.

Similarly, a search can be performed in all indices with .AllIndices()

var searchResponse = client.Search<Person>(s => s
    .AllIndices()
    .From(0)
    .Size(10)
    .Query(q => q
         .Match(m => m
            .Field(f => f.FirstName)
            .Query("Martijn")
         )
    )
);

which generates a request to the search endpoint /_search.

Single or multiple index names can be provided in the request; see the documentation on Indices paths and Document paths, respectively.

All of the search examples so far have used NEST’s Fluent API which uses lambda expressions to construct a query with a structure that mimics the structure of a query expressed in the Elasticsearch’s JSON based Query DSL.

NEST also exposes an Object Initializer syntax that can also be used to construct queries, for those not keen on deeply nested lambda expressions (layout is key!).

Here’s the same query as the previous example, this time constructed using the Object Initializer syntax

var searchRequest = new SearchRequest<Person>(Nest.Indices.All) 
{
    From = 0,
    Size = 10,
    Query = new MatchQuery
    {
        Field = Infer.Field<Person>(f => f.FirstName),
        Query = "Martijn"
    }
};

var searchResponse = await client.SearchAsync<Person>(searchRequest);

All indices and types are specified in the constructor

As indicated at the start of this section, the high level client still exposes the low level client from Elasticsearch.Net through the .LowLevel property on the client. The low level client can be useful in scenarios where you may already have the JSON that represents the request that you wish to send and don’t wish to translate it over to the Fluent API or Object Initializer syntax at this point in time, or perhaps there is a bug in the client that can be worked around by sending a request as a string or anonymous type.

Using the low level client via the .LowLevel property means you can get with the best of both worlds:

  1. Use the high level client
  2. Use the low level client where it makes sense, taking advantage of all the strong types within NEST, and its serializer for deserialization.

Here’s an example

var searchResponse = client.LowLevel.Search<SearchResponse<Person>>("people", PostData.Serializable(new
{
    from = 0,
    size = 10,
    query = new
    {
        match = new
        {
            field = "firstName",
            query = "Martijn"
        }
    }
}));

var responseJson = searchResponse;

Here, the query is represented as an anonymous type, but the body of the response is a concrete implementation of the same response type returned from the high level client, NEST.

Aggregations

edit

In addition to structured and unstructured search, Elasticsearch is also able to aggregate data based on a search query

var searchResponse = await client.SearchAsync<Person>(s => s
    .Size(0)
    .Query(q => q
         .Match(m => m
            .Field(f => f.FirstName)
            .Query("Martijn")
         )
    )
    .Aggregations(a => a
        .Terms("last_names", ta => ta
            .Field(f => f.LastName)
        )
    )
);

var termsAggregation = searchResponse.Aggregations.Terms("last_names");

In this example, a match query to search for people with the first name of "Martijn" is issued as before; this time however,

  1. a size of 0 is set because we don’t want the first 10 documents that match this query to be returned, we’re only interested in the aggregation results
  2. a terms aggregation is specified to group matching documents into buckets based on last name.

termsAggregation can be used to get the count of documents for each bucket, where each bucket will be keyed by last name.

See Writing aggregations for more details.