Kotlin Query DSL
Elasticsearch has a Query DSL and the Java Rest High Level Client comes with a very expansive set of builders that you can use to programmatically construct queries. Of course builders are something that you should avoid in Kotlin.
On this page we outline a few ways in which you can build queries both programmatically using the builders that come with the Java client, using json strings, and using our Kotlin DSL.
We will use the same example as before in Search.
Java Builders
The Java client comes with org.elasticsearch.index.query.QueryBuilders
which provides static methods to create builders for the various queries. This covers most but probably not all of the query DSL but should cover most commonly used things.
val results = repo.search {
(
source()
searchSource.size(20)
.query(
()
boolQuery.must(matchQuery("name", "quick").boost(2.0f))
.must(matchQuery("name", "brown"))
)
)
}
("We found ${results.totalHits} results.") println
Captured Output:
We found 3 hits results.
This is unfortunately quite ugly from a Kotlin point of view. Lets see if we can clean that up a little.
// more idomatic Kotlin using apply { ... }
val results = repo.search {
(
source().apply {
searchSource(
query().apply {
boolQuery().apply {
must(matchQuery("name", "quick").boost(2.0f))
add(matchQuery("name", "brown"))
add}
}
)
}
)
}
("We found ${results.totalHits} results.") println
Captured Output:
We found 3 hits results.
Using apply
gets rid of the need to chain all the calls and it is a little better but still a little verbose.
Kotlin Search DSL
To address this, this library provides a Kotlin DSL that allows you to mix both type safe DSL constructs and simple schema-less manipulation of maps. We’ll show several versions of the same query above to show how this works.
The example below uses the type safe way to set up the same query as before.
// more idomatic Kotlin using apply { ... }
val results = repo.search {
// SearchRequest.dsl is the extension function that allows us to use the dsl.
{
configure // SearchDSL is passed to the block as this
// It extends our MapBackedProperties class
// This allows us to delegate properties to a MutableMap
// from is a property that is stored in the map
= 0
from
// MapBackedProperties actually implements MutableMap
// and delegates to a simple MutableMap.
// so this works too: this["from"] = 0
// Unfortunately Maps have their own size property so we can't
// use that as a property name for the query size :-(
= 20
resultSize // this actually puts a key "size" in the map
// query is a function that takes an ESQuery instance
=
query // bool is a function that create a BoolQuery,
// which extends ESQuery, that is injected into the block
{
bool // BoolQuery has a function called must
// it also has filter, should, and mustNot
(
must// must has a vararg list of ESQuery
(Thing::name, "qiuck") {
match// match always needs a field and query
// but boost is optional
= 2.0
boost // so we find something despite the misspelled quick
= "auto"
fuzziness },
// the block param for match is nullable and
// defaults to null
// you can refer fields by string name as well
("name", "quick brown") {
matchPhrase= 1
slop }
)
}
}
}
("We found ${results.totalHits} results.") println
Captured Output:
We found 3 hits results.
Extending the DSL
The Elasticsearch DSL is huge and only a part is covered in our Kotlin DSL so far. Currently, most compound, text, and term level queries are supported.
Using the DSL in schema-less mode allows you to work around this and you can of course mix both approaches.
However, if you need something added to the DSL it is really easy to do this yourself. For example this is the implementation of the match we use above:
enum class MatchOperator { AND, OR }
@Suppress("EnumEntryName")
enum class ZeroTermsQuery { all, none }
@SearchDSLMarker
class MatchQueryConfig : MapBackedProperties() {
var query by property<String>()
var boost by property<Double>()
var analyzer by property<String>()
var autoGenerateSynonymsPhraseQuery by property<Boolean>()
var fuzziness by property<String>()
var maxExpansions by property<Int>()
var prefixLength by property<Int>()
var transpositions by property<Boolean>()
var fuzzyRewrite by property<String>()
var lenient by property<Boolean>()
var operator by property<MatchOperator>()
var minimumShouldMatch by property<String>()
var zeroTermsQuery by property<ZeroTermsQuery>()
}
@SearchDSLMarker
class MatchQuery(
field: String,
query: String,
matchQueryConfig: MatchQueryConfig = MatchQueryConfig(),
block: (MatchQueryConfig.() -> Unit)? = null
: ESQuery(name = "match") {
) // The map is empty until we assign something
{
init (field, matchQueryConfig)
putNoSnakeCase.query = query
matchQueryConfig?.invoke(matchQueryConfig)
block}
}
fun SearchDSL.match(
field: KProperty<*>,
query: String, block: (MatchQueryConfig.() -> Unit)? = null
) = MatchQuery(field.name, query, block = block)
fun SearchDSL.match(
field: String,
query: String, block: (MatchQueryConfig.() -> Unit)? = null
) = MatchQuery(field, query, block = block)
For more information on how to extend the DSL read Extending and Customizing the Kotlin DSLs