Search code examples
c#.netlinqelasticsearchnest

Equivalent linq query in elasticsearch.net(NEST)


I have the following document type in elastic :

public class ProductDto
{
   public Int64 Id { get; set; }

   public String Title{ get; set; }

   public bool Visible { get; set; }

   public IList<ProductSupplierDto> ProductSuppliers { get; set; }
}

public class ProductSupplierDto
{
   public Int64 Id { get; set; }

   public String Title{ get; set; }

   public bool Enabled { get; set; }
}

how to write below linq query with Nest libray :

var products = db.products.where(p=>  p.Visible 
                                   && p.ProductSuppliers.Any(ps=>ps.Enabled)
                                 ).ToList();

in nest library i have the following query:

var baseQuery = Query<ProductDto>.Term(qt => qt.Field(f => 
                                      f.Visible).Value(true));

how to add productsuppliers filter to baseQuery ?

I use this method for create index :

    private async Task CreateIndexIfItDoesntExist<T>(string index) where T: class
    {
        if (!this.client.IndexExists(index).Exists)
        {
            var indexDescriptor = new CreateIndexDescriptor(index)
                            .Settings(x => x.NumberOfReplicas(0))
                            .Mappings(mappings => mappings
                                .Map<T>(m => m.AutoMap()));

            await this.client.CreateIndexAsync(index, i => indexDescriptor);

       // Max out the result window so you can have pagination for >100 pages
           await this.client.UpdateIndexSettingsAsync(index, ixs => ixs
             .IndexSettings(s => s
                 .Setting("max_result_window", int.MaxValue)));


        }
    }

and call like this :

await CreateIndexIfItDoesntExist<ProductDto>("products");

methods for index data:

    private async Task<IndexResult> IndexDocuments<T>(T[] datas, string index) where T:class
    {
        int batchSize = 1000; // magic
        int totalBatches = (int)Math.Ceiling((double)datas.Length / batchSize);

        for (int i = 0; i < totalBatches; i++)
        {
            var response = await this.client.IndexManyAsync(datas.Skip(i * batchSize).Take(batchSize), index);

            if (!response.IsValid)
            {
                return new IndexResult
                {
                    IsValid = false,
                    ErrorReason = response.ServerError?.Error?.Reason,
                    Exception = response.OriginalException
                };
            }
            else
            {
                Debug.WriteLine($"Successfully indexed batch {i + 1}");
            }
        }

        return new IndexResult
        {
            IsValid = true
        };
    }

Solution

  • ProductSupplierDto in ProductSuppliers will be mapped as an object type with automapping, so the following query will achieve what you're after

    var client = new ElasticClient();
    
    var searchResponse = client.Search<ProductDto>(s => s
        .Query(q => +q
            .Term(f => f.Visible, true) && +q
            .Term(f => f.ProductSuppliers[0].Enabled, true)
        )    
    );  
    

    This generates the following query

    {
      "query": {
        "bool": {
          "filter": [
            {
              "term": {
                "visible": {
                  "value": true
                }
              }
            },
            {
              "term": {
                "productSuppliers.enabled": {
                  "value": true
                }
              }
            }
          ]
        }
      }
    }
    

    A couple of points

    1. The query uses operator overloading on queries to combine them together and generate queries that execute in a filter context (in this case, a bool query filter clause). Since a document either matches or it doesn't, a relevancy score for matching does not need to be calculated.
    2. f => f.ProductSuppliers[0].Enabled is an expression to get the path to a field. It does not mean "get the value of Enabled from the first item in ProductSuppliers", but means "get the path to the Enabled field of all items in the ProductSuppliers property". the indexer into the collection here is only to be able to access the properties of the ProductSupplierDto type.

    You may want to consider mapping ProductSuppliers as a nested type so that you would be able to query across properties of individual items in a ProductSuppliers collection. With ProductSuppliers mapped as a nested type, the query would then be

    var searchResponse = client.Search<ProductDto>(s => s
        .Query(q => +q
            .Term(f => f.Visible, true) && +q
            .Nested(n => n
                .Path(p => p.ProductSuppliers)
                .Query(nq => nq
                    .Term(f => f.ProductSuppliers[0].Enabled, true)
                )
            )           
        )    
    );