Skip to content

Instantly share code, notes, and snippets.

@JimBobSquarePants
Created May 26, 2020 23:03
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save JimBobSquarePants/f07c60bd281614bf3f018de8c595c7ee to your computer and use it in GitHub Desktop.
Save JimBobSquarePants/f07c60bd281614bf3f018de8c595c7ee to your computer and use it in GitHub Desktop.
// Create a query where the result must match or contain the given query
// and the subset of properties and filters.
descriptor.Query(
q => q.Bool(
b => b.Must(
mu =>
mu.MultiMatch(
m => m.Fields(propertyExpressions) // Search within these properties or all.
.Query(searchTerm) // For this query
.Operator(Operator.Or))) // In any of the properties.
.Filter(filters))); // Further filter results.
@russcam
Copy link

russcam commented May 27, 2020

I'm not sure I 100% understand, but I think you want to know how to augment with filters? One way I've done this in the past with the fluent syntax is to split out the building of query parts into separate methods. For example, in Linqpad with 7.7.0,

private static void Main()
{
    var defaultIndex = "products";
    var pool = new SingleNodeConnectionPool(new Uri("http://localhost:9200"));

    var settings = new ConnectionSettings(pool, JsonNetSerializer.Default)
        .DefaultIndex(defaultIndex)
        // following method calls added only for development, probably don't want these on in production 
        .DisableDirectStreaming()
        .PrettyJson()
        .OnRequestCompleted(callDetails =>
        {
            if (callDetails.RequestBodyInBytes != null)
            {
                Console.WriteLine(
                    $"{callDetails.HttpMethod} {callDetails.Uri} \n" +
                    $"{Encoding.UTF8.GetString(callDetails.RequestBodyInBytes)}");
            }
            else
            {
                Console.WriteLine($"{callDetails.HttpMethod} {callDetails.Uri}");
            }

            Console.WriteLine();

            if (callDetails.ResponseBodyInBytes != null)
            {
                Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
                         $"{Encoding.UTF8.GetString(callDetails.ResponseBodyInBytes)}\n" +
                         $"{new string('-', 30)}\n");
            }
            else
            {
                Console.WriteLine($"Status: {callDetails.HttpStatusCode}\n" +
                         $"{new string('-', 30)}\n");
            }
        });

    var client = new ElasticClient(settings);
    
    if (client.Indices.Exists(defaultIndex).Exists)
    {
        client.Indices.Delete(defaultIndex);
    }
    
    client.Indices.Create(defaultIndex, c => c
        .Map<Product>(m => m
            .AutoMap()
            .Properties(p => p
                .Object<Category>(o => o
                    .Name(n => n.Categories)
                    .Properties(pp => pp
                        .Keyword(k => k
                            .Name(n => n.Name)
                        )
                    )
                )
            )
        )
    );
    
    
    client.Bulk(b => b
        .IndexMany(new [] {
            new Product
            {
                Name = "product1",
                Categories = new List<Category>
                {
                    new Category { Name = "category1" },
                    new Category { Name = "category2" }
                }
            },
            new Product
            {
                Name = "product2",
                Categories = new List<Category>
                {
                    new Category { Name = "category2" },
                    new Category { Name = "category3" }
                }
            },
            new Product
            {
                Name = "another product1",
                Categories = new List<Category>
                {
                    new Category { Name = "category1" },
                    new Category { Name = "category2" }
                }
            },
        })
        .Refresh(Refresh.WaitFor)
    );
    
    var categories = new [] 
    {
        "category1", "category2"
    };
    
    var propertyExpressions = Nest.Infer.Fields<Product>(p => p.Name);    
    var searchTerm = "another";
    
    client.Search<Product>(s => s
        .Query(q => q
            .Bool(b => b
                .Must(mu => mu
                    .MultiMatch(m => m
                        .Fields(propertyExpressions) // Search within these properties or all.
                        .Query(searchTerm) // For this query
                        .Operator(Operator.Or)
                    )
                ) // In any of the properties.
                .Filter(Filters(categories))
            )
        )
    );
}


public static Func<QueryContainerDescriptor<Product>, QueryContainer>[] Filters(string[] categories)
{
    var filters = new List<Func<QueryContainerDescriptor<Product>, QueryContainer>>(categories.Length);
    foreach (var c in categories)
    {
        filters.Add(q => q.Term(p => p.Categories.First().Name, c));
    }

    return filters.ToArray();
}


public class Product
{
    // Id and other properties removed for brevity..
    public string Name { get; set; }
    public ICollection<Category> Categories { get; set; } = new HashSet<Category>();
}

public class Category
{
    // Id and other properties removed for brevity..
    public string Name { get; set; }
    
    [Ignore]
    public Product Product { get; set; }
}

Another way would be to start with a QueryContainer and use the overloaded operators on QueryContainer to combine queries. Using the previous example, the following would build the same query

client.Search<Product>(s => s
        .Query(q => BuildQuery(q, searchTerm, propertyExpressions, categories))
);

public static QueryContainer BuildQuery(QueryContainerDescriptor<Product> q, string searchTerm, Fields propertyExpressions, string[] categories)
{
	QueryContainer qc = q;

	qc &= Query<Product>
		.MultiMatch(m => m
			.Fields(propertyExpressions) // Search within these properties or all.
			.Query(searchTerm) // For this query
			.Operator(Operator.Or)
		);
		
	foreach (var c in categories)
	{
		qc &= +Query<Product>
			.Term(p => p.Categories.First().Name, c);
	}

	return qc;
}

which is

{
  "query": {
    "bool": {
      "filter": [{
        "term": {
          "categories.name": {
            "value": "category1"
          }
        }
      }, {
        "term": {
          "categories.name": {
            "value": "category2"
          }
        }
      }],
      "must": [{
        "multi_match": {
          "fields": ["name"],
          "operator": "or",
          "query": "another"
        }
      }]
    }
  }
}

Note that the term query on categories.name doesn't care if the field is a single value or multiple value. In the case of multiple values, only one value has to match.

The search result in both cases is

{
  "took" : 2,
  "timed_out" : false,
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : {
      "value" : 1,
      "relation" : "eq"
    },
    "max_score" : 0.81427324,
    "hits" : [
      {
        "_index" : "products",
        "_type" : "_doc",
        "_id" : "guZpU3IBhN_zGyAEwf-g",
        "_score" : 0.81427324,
        "_source" : {
          "name" : "another product1",
          "categories" : [
            {
              "name" : "category1"
            },
            {
              "name" : "category2"
            }
          ]
        }
      }
    ]
  }
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment