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
}