Search code examples
c#nest

Compound query in Elasticsearch.NET


Imagine I have an index with a bunch of Order objects in it

class Order
{
    int CustomerId { get; set; }
    DateTime OrderDate { get; set; }
    // Other fields
}

For a particular customer ID, I am able to find out the last order that the customer made with the following query:

IElasticClient client;

async Task<Order> GetLastOrder(int customerId)
{
    var searchResponse = await client.SearchAsync<Order>(s => s
        .Query(q => q
            .Term(f => f
                .Field(e => e.CustomerId)
                .Term(customerId)) && q
            .DateRange(r => r
                .Field(e => e.OrderDate)
                .LessThan(DateMath.Now)))
        .Sort(o => o
            .Descending(e => e.OrderDate))
        .Size(1));

    return searchResponse.ApiCall.Success
        ? searchResponse.Documents.First()
        : null;
}

However, in order to support the data loader pattern, I want to query the last order made by multiple customers, given by a collection of customer IDs. I started off like this:

async Task<IDictionary<int, Order>> GetLastOrders(IEnumerable<int> customerIds)
{
    var searchResponse = await client.SearchAsync<Order>(s => s
        .Query(q => q
            .Terms(f => f
                .Field(e => e.CustomerId)
                .Terms(customerIds)) && q
            .DateRange(r => r
                .Field(e => e.OrderDate)
                .LessThan(DateMath.Now)))
        .Sort(o => o
            .Descending(e => e.OrderDate))
        .Size(1));

    return searchResponse.ApiCall.Success
        ? searchResponse.Documents.ToDictionary(i => i.CustomerId)
        : new Dictionary<string, Order>();
}

Unfortunately this does not work, as it only returns the first record of entire query, which will only return a single Order. How can I modify this query to return one Order per customer ID?


Solution

  • Collapse can be used to return top most record for a field

    await _client.SearchAsync<Order>(s => s
                                            .Query(q => q
                                            .Terms(f => f
                                            .Field(e => e.CustomerId)
                                            .Terms(cuIds)) && q
                                            .DateRange(r => r
                                                             .Field(e => e.OrderDate)
                                                             .LessThan(DateMath.Now)))
                                            .Sort(o => o
                                                  .Descending(e => e.OrderDate))
                                            .Collapse(c => c.Field(e => e.CustomerId))
                                            .Size(10)
                                            );
    

    corresponding query in DSL

    {
      "collapse": {
        "field": "customerId"
      },
      "query": {
        "bool": {
          "must": [
            {
              "terms": {
                "customerId": [
                  1,
                  2
                ]
              }
            },
            {
              "range": {
                "orderDate": {
                  "lt": "now"
                }
              }
            }
          ]
        }
      },
      "size": 10,
      "sort": [
        {
          "orderDate": {
            "order": "desc"
          }
        }
      ]
    }