Search code examples
elasticsearchnest

Elasticsearch must with subquery intersection


I want to perform an Elasticsearch query which combinates two subqueries (AND operator), each one of those subqueries searching in different fields (OR operator).

For example, if I pass the "name" parameter it searches only in name fields (firstname + lastname), if I pass the "contact" parameter it searches in contact fields (ContactEmail + ContactTelephone).

The code below return :

  • All results if name is null but contact provided (should only return right part)
  • All results if contact is null but name provided (should only return left part)
  • union results (OR operator) if name and contact values are provided (should return left intersect right)
searchQuery = searchQuery
    .MinScore(minScore)
    .Query(qu => qu
        .Bool(b => b
            .Must(m => m
                .MultiMatch(mm=> mm
                    .Fields(fs=> fs
                        .Field(f => f.Firstname)
                        .Field(f => f.Lastname)
                    )
                    .Query(name)
                    .Operator(Operator.Or)
                )
            )
            .Must(m => m
                .MultiMatch(mm => mm
                    .Fields(fs => fs
                        .Field(f => f.ContactEmail)
                        .Field(f => f.ContactTelephone)
                    )
                    .Query(contact)
                    .Operator(Operator.Or)
                )
            )
        )
    );

I am using Must because I want the associated score.

I think there are 2 issues: applying AND instead of OR and ignoring subquery if criteria is empty. Any idea?


Solution

  • The two queries in must clauses must be part of the same .Must() call.

    Given the following POCO

    public class Person
    {
        public string Firstname { get; set; }
        public string Lastname { get; set; }
        public string ContactEmail {get;set;}
        public string ContactTelephone {get;set;}
    
    }
    

    The query should look like the following

    var client = new ElasticClient(settings);
    
    var minScore = 2;
    string name = "name";
    string contact = "contact";
    
    var response = client.Search<Person>(s => s
        .MinScore(minScore)
        .Query(qu => qu
            .Bool(b => b
                .Must(m => m
                    .MultiMatch(mm => mm
                        .Fields(fs => fs
                            .Field(f => f.Firstname)
                            .Field(f => f.Lastname)
                        )
                        .Query(name)
                        .Operator(Operator.Or)
                    ), m => m
                    .MultiMatch(mm => mm
                        .Fields(fs => fs
                            .Field(f => f.ContactEmail)
                            .Field(f => f.ContactTelephone)
                        )
                        .Query(contact)
                        .Operator(Operator.Or)
                    )
                )
            )
        )
    );
    

    which produces the following

    {
      "min_score": 2.0,
      "query": {
        "bool": {
          "must": [
            {
              "multi_match": {
                "fields": [
                  "contactEmail",
                  "contactTelephone"
                ],
                "operator": "or",
                "query": "contact"
              }
            }
          ]
        }
      }
    }
    

    If either name or contact are null, that query will be omitted. For example, setting name to null

    string name = null;
    
    var response = client.Search<Person>(s => s
        .MinScore(minScore)
        .Query(qu => qu
            .Bool(b => b
                .Must(m => m
                    .MultiMatch(mm => mm
                        .Fields(fs => fs
                            .Field(f => f.Firstname)
                            .Field(f => f.Lastname)
                        )
                        .Query(name)
                        .Operator(Operator.Or)
                    ), m => m
                    .MultiMatch(mm => mm
                        .Fields(fs => fs
                            .Field(f => f.ContactEmail)
                            .Field(f => f.ContactTelephone)
                        )
                        .Query(contact)
                        .Operator(Operator.Or)
                    )
                )
            )
        )
    );
    

    yields

    {
      "min_score": 2.0,
      "query": {
        "bool": {
          "must": [
            {
              "multi_match": {
                "fields": [
                  "contactEmail",
                  "contactTelephone"
                ],
                "operator": "or",
                "query": "contact"
              }
            }
          ]
        }
      }
    }
    

    This takes advantage of a client feature called conditionless queries - a specific query is considered to be conditionless if a component of the query is empty when it should not be, in order to form a complete query. For example, for a multi_match query, it is considered to be conditionless if the Query is null

    MultiMatch query conditionless

    The intention with conditionless queries is that it makes writing more complex queries easier, and the behaviour can be bypassed by specifying Verbatim() on the query. Since they go against the principle of least surprise however, there is an intention to remove them in the future. To perform the same query without relying on conditionless behaviour

    var response = client.Search<Person>(s => s
        .MinScore(minScore)
        .Query(qu => qu
            .Bool(b => b
                .Must(m =>
                    {
                        if (name == null)
                            return m;
                            
                        return m
                            .MultiMatch(mm => mm
                                .Fields(fs => fs
                                    .Field(f => f.Firstname)
                                    .Field(f => f.Lastname)
                                )
                                .Query(name)
                                .Operator(Operator.Or)
                            );
                    }, m => 
                    {
                        if (contact == null)
                            return m;
    
                        return m
                            .MultiMatch(mm => mm
                                .Fields(fs => fs
                                    .Field(f => f.ContactEmail)
                                    .Field(f => f.ContactTelephone)
                                )
                                .Query(contact)
                                .Operator(Operator.Or)
                            );
                    }
                )
            )
        )
    );