Last updated by gorshing 3 years ago

Query Builder

Searchable Plugin comes with a builder, which makes programmatic queries Groovy, baby.

When you pass a closure to one of the search methods, you are using the query builder.

TODO the query builder needs a reference section

It builds on Compass

The query builder syntax mirrors Compass's own CompassQueryBuilder: to get the most out of the Searchable Plugin's query builder, you should take a moment to familiarise yourself with it.

And makes it better

The plugin's Groovy query builder improves on the raw Compass experience in a few ways. First it relieves you of the burden to call toQuery() when using the various specific builders obtained from calling some CompassQueryBuilder methods, and it allows you to easily nest these specific builders using closures.

It also simplifies the job of constructing boolean queries by not requiring you to explicitly create a boolean builder and add "should" clauses. You still need to explicitly add must and must not clauses, but other clauses in a boolean context are assumed to be should clauses.

The rules

The methods you can invoke depend on the current context. In the outer-most context you can invoke CompassQueryBuilder methods.

Within nested contexts (which are created by nested closures) you can call both CompassQueryBuilder methods and whatever methods the current nested builder supports.

The builder also allows you to call Compass's various query builders' options "setters" using a literal options Map as the last argument, instead of requiring method invocations.

The builder shortens a few method names too, so the CompassBooleanQueryBuilder addMust, addShould, and addMustNot methods can be shortened to must, should, and mustNot (and as already mentioned you typically don't need to use should because any clause that is not a must or mustNot is assumed to be should) and the CompassQuery addSort method can be shortedned to sort.

And finally because it's Groovy, you have the language at your disposal so you can use control flow, loops, variables etc.

Enough theory, let's explore some examples.

By Example

Say you want to search for items in the index where "pages" is less than 50 and "type" is "poetry". A String query for this might look like "pages:[* TO 50] type:poetry".

Using the builder you would do

search {                    // <-- create an implicit boolean query
    lt("pages", 50)         // <-- uses CompassQueryBuilder#lt, and adds a boolean "should" clause
    term("type", "poetry")  // <-- uses CompassQueryBuilder#term, and adds a boolean "should" clause
}

We just built a boolean query! It has two clauses: the search must match EITHER "pages" < 50 OR "type" == "poetry". Not bad but this query will match when either condition is true, and not necessarily both.

So let's improve the search results and make sure that matches DO have "type" == "poetry".

search {                          // <-- creates an implicit boolean query
    lt("pages", 50)               // <-- uses CompassQueryBuilder#lt, and adds a boolean "should"
    must(term("type", "poetry"))  // <-- uses CompassQueryBuilder#term, adds a boolean "must"
}

Ok let's assume we're getting matches we don't want, and so we add another "mustNot" clause:

search {                           // <-- creates an implicit boolean query
    lt("pages", 50)                // <-- uses CompassQueryBuilder#lt, and adds a boolean "should"
    must(term("type", "poetry"))   // <-- uses CompassQueryBuilder#term, adds a boolean "must"
    mustNot(term("theme", "war"))  // <-- uses CompassQueryBuilder#term, adds a boolean "mustNot"
}

Great, so now we're matching any non-war poetry under 50 pages!

Now we want to search for a specific phrase "all hands on deck", let's add it to the query. First we try it as a nested String query:

search {                                   // <-- creates an implicit boolean query
    must(queryString("all hands on deck")) // <-- uses CompassQueryBuilder#queryString, and adds a boolean must
    lt("pages", 50)                        // <-- uses CompassQueryBuilder#lt, and adds a boolean "should"
    must(term("type", "poetry"))           // <-- uses CompassQueryBuilder#term, adds a boolean "must"
    mustNot(term("theme", "war"))          // <-- uses CompassQueryBuilder#term, adds a boolean "mustNot"
}

The nested String query has introduced the possibility for the query to matching anything containing any of the words "all", "hands", "on", or "deck" and maybe in any searchable property. But we wanted to search for this exact text, hmmm.

Check the CompassQueryBuilder API and you'll notice that CompassQueryBuilder#queryString returns a CompassQueryStringBuilder, so in fact we can create a context for that builder with a closure and in that closure call any methods the CompassQueryStringBuilder exposes to tighten up the query:

search {                                      // <-- creates an implicit boolean query
    must(queryString("all hands on deck") {   // <-- creates a nested CompassQueryStringBuilder context
        useAndDefaultOperator()               // <-- calls CompassQueryStringBuilder#useAndDefaultOperator
        setDefaultSearchProperty("body")      // <-- calls CompassQueryStringBuilder#setDefaultSearchProperty
    })                                        // <-- added as boolean must to surrounding boolean
    lt("pages", 50)                           // <-- uses CompassQueryBuilder#lt, and adds a boolean "should"
    must(term("type", "poetry"))              // <-- uses CompassQueryBuilder#term, adds a boolean "must"
    mustNot(term("theme", "war"))             // <-- uses CompassQueryBuilder#term, adds a boolean "mustNot"
}

Ok, now the query requires that ALL of the words "all hands on deck" are present in matches. And we have set the defaultSearchProperty to "body", so the string query will now match terms in the searchable "body" property.

But we can do a little better. First let's use an options Map instead of calling those setters:

search {                                    // <-- creates an implicit boolean query
    must(queryString("all hands on deck", [useAndDefaultOperator: true, defaultSearchProperty: "body"]))
        // ^^ add a "must" nested query string, calling useAndDefaultOperator() and setDefaultSearchProperty("body")
        //    on the CompassQueryStringBuilder
    lt("pages", 50)                         // <-- uses CompassQueryBuilder#lt, and adds a boolean "should"
    must(term("type", "poetry"))            // <-- uses CompassQueryBuilder#term, adds a boolean "must"
    mustNot(term("theme", "war"))           // <-- uses CompassQueryBuilder#term, adds a boolean "mustNot"
}

This is the same query as before, only fewer lines of code.

So now let's use CompassQueryBuilder#multiPhrase instead of queryString, since a multi-phrase query can require that the words appear in order, whereas a query string generally just requires the words appear _somewhere_.

search {                                    // <-- creates an implicit boolean query
    must(multiPhrase("body", [slop: 2]) {   // <-- creates a nested CompassMultiPhraseQueryBuilder context, calling setSlop(2)
        add("all")                          // <-- calls CompassMultiPhraseQueryBuilder#add
        add("hands")                        // <-- calls CompassMultiPhraseQueryBuilder#add
        add("on")
        add("deck")
    })                                      // <-- adds multiPhrase as boolean "must"
    lt("pages", 50)                         // <-- uses CompassQueryBuilder#lt, and adds a boolean "should"
    must(term("type", "poetry"))            // <-- uses CompassQueryBuilder#term, adds a boolean "must"
    mustNot(term("theme", "war"))           // <-- uses CompassQueryBuilder#term, adds a boolean "mustNot"
}

Let's go all out for the final example and add three new features: (i) move the number pages clause into a nested boolean with a new clause for items with 50 or more pages, and a "boost" (meaning higher score/relevance) for the smaller number, (ii) a clause for "publishedDate" being within the last 4 weeks (and note the use of a Date object) and (iii) a sort first by relevance then author surname.

search {                                    // <-- creates an implicit boolean query
    must(multiPhrase("body", [slop: 2]) {   // <-- creates a nested CompassMultiPhraseQueryBuilder context, and calls setSlop(2)
        add("all")                          // <-- calls CompassMultiPhraseQueryBuilder#add
        add("hands")                        // <-- calls CompassMultiPhraseQueryBuilder#add
        add("on")
        add("deck")
    })                                      // <-- adds multiPhrase as boolean "must"
    must {                                  // <-- creates an nested boolean query, implicitly
        ge("pages", 50)                     // <-- uses CompassQueryBuilder#ge, adds a boolean "should"
        lt("pages", 50, [boost: 1.5])       // <-- uses CompassQueryBuilder#lt, calls setBoost(1.5f), adds a boolean "should"
    }                                       // <-- adds nested boolean as "must" clause to outer boolean
    must(term("type", "poetry"))            // <-- uses CompassQueryBuilder#term, adds a boolean "must"
    mustNot(term("theme", "war"))           // <-- uses CompassQueryBuilder#term, adds a boolean "mustNot"
    ge("publishedDate", new Date() - 28)
    sort(CompassQuery.SortImplicitType.SCORE)                  // <-- uses CompassQuery#addSort
    sort("authorSurname", CompassQuery.SortPropertyType.STRING) // <-- uses CompassQuery#addSort
}