Search code examples
elasticsearchnest

in NEST, how do I dynamically build a query from a list of terms?


Say my user provides a list of search terms which I've collected into an array/list, and now I want to combine those OR-wise into a NEST query using MatchPhrase. How would I do that? The code for a (single) search term would look something like this:

var search = client.Search<ElasticRequirement>(s => s
.Query(q =>
    q.MatchPhrase(m => m.OnField(f => f.Title).Query(term.ToLower()).Slop(slop))
    || q.MatchPhrase(m => m.OnField(f => f.Description).Query(text).Slop(slop))
    )
   .LowercaseExpandedTerms()
   .Explain()
   .Query(q => q.Fuzzy(f => f.PrefixLength(1).OnField(c => c.Title).OnField(c => c.Description)))
);

This is fine, but I need to apply that same MatchPhrase filter once for each provided search term. Any help much appreciated.


Solution

  • You can use bool should expressions to build your query dynamically. I'll provide the complete solution below. Call BuildQuery() method with appropriate parameters.

    ISearchResponse<ElasticRequirement> BuildQuery(IElasticClient client, IEnumerable<string> terms, int slop)
    {
        return client.Search<ElasticRequirement>(s => s
            .Query(q => q
                .Bool(b => b
                    .Should(terms.Select(t => BuildPhraseQueryContainer(q, t, slop)).ToArray())))
            .LowercaseExpandedTerms()
            .Explain()
            .Query(q => q.Fuzzy(f => f.PrefixLength(1).OnField(c => c.Title).OnField(c => c.Description))));
    }
    
    QueryContainer BuildPhraseQueryContainer(QueryDescriptor<ElasticRequirement> qd, string term, int slop)
    {
        return qd.MatchPhrase(m => m.OnField(f => f.Title).Query(term.ToLower()).Slop(slop)) ||
            qd.MatchPhrase(m => m.OnField(f => f.Description).Query(term.ToLower()).Slop(slop));
    }
    

    For terms = {"term1", "term2", "term3"} and slop = 0, the Elasticsearch search JSON command that will get built by my code is as under:

    {
      "explain": true,
      "query": {
        "bool": {
          "should": [
            {
              "bool": {
                "should": [
                  {
                    "match": {
                      "title": {
                        "type": "phrase",
                        "query": "term1",
                        "slop": 0
                      }
                    }
                  },
                  {
                    "match": {
                      "description": {
                        "type": "phrase",
                        "query": "term1",
                        "slop": 0
                      }
                    }
                  }
                ]
              }
            },
            {
              "bool": {
                "should": [
                  {
                    "match": {
                      "title": {
                        "type": "phrase",
                        "query": "term2",
                        "slop": 0
                      }
                    }
                  },
                  {
                    "match": {
                      "description": {
                        "type": "phrase",
                        "query": "term2",
                        "slop": 0
                      }
                    }
                  }
                ]
              }
            },
            {
              "bool": {
                "should": [
                  {
                    "match": {
                      "title": {
                        "type": "phrase",
                        "query": "term3",
                        "slop": 0
                      }
                    }
                  },
                  {
                    "match": {
                      "description": {
                        "type": "phrase",
                        "query": "term3",
                        "slop": 0
                      }
                    }
                  }
                ]
              }
            }
          ]
        }
      }
    }
    

    You can tweak this code such that all the match commands are under the same should node. I'll leave that up to you to figure out :)