Skip to content

Commit

Permalink
Merge pull request #162 from jillesvangurp/constructQueryClause-helper
Browse files Browse the repository at this point in the history
constructQueryClause helper
  • Loading branch information
jillesvangurp authored Jan 9, 2025
2 parents aac097f + cce51f3 commit 2a3c5cc
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 23 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,8 @@ There are of course a lot more features that this library supports. The

- [Highlighting](https://jillesvangurp.github.io/kt-search/manual/Highlighting.html)

- [Reusing your Query logic](https://jillesvangurp.github.io/kt-search/manual/ReusableSearchQueries.html)

### Indices and Documents

- [Deleting by query](https://jillesvangurp.github.io/kt-search/manual/DeleteByQuery.html)
Expand Down
2 changes: 2 additions & 0 deletions docs/src/test/kotlin/documentation/manual/manual-index.kt
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ enum class ManualPages(title: String = "") {
GettingStarted("Getting Started"),
ClientConfiguration("Client Configuration"),
Search("Search and Queries"),
ReusableSearchQueries("Reusing your Query logic"),
TextQueries("Text Queries"),
TermLevelQueries("Term Level Queries"),
CompoundQueries("Compound Queries"),
Expand Down Expand Up @@ -70,6 +71,7 @@ val sections = listOf(
ManualPages.DeepPaging to deepPagingMd,
ManualPages.JoinQueries to joinQueriesMd,
ManualPages.Highlighting to highlightingMd,
ManualPages.ReusableSearchQueries to reusingQueryLogicMd
)
),
Section("Indices and Documents", listOf(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
package documentation.manual.search

import com.jillesvangurp.ktsearch.search
import com.jillesvangurp.searchdsls.querydsl.QueryClauses
import com.jillesvangurp.searchdsls.querydsl.bool
import com.jillesvangurp.searchdsls.querydsl.constructQueryClause
import com.jillesvangurp.searchdsls.querydsl.matchPhrase
import com.jillesvangurp.searchdsls.querydsl.term
import documentation.sourceGitRepository
import kotlinx.datetime.Clock
import kotlinx.datetime.TimeZone
import kotlinx.datetime.toLocalDateTime

val reusingQueryLogicMd = sourceGitRepository.md {
val indexName = "docs-search-demo"
client.indexTestFixture(indexName)

+"""
The Search DSL allows you to construct queries via helper extension functions on
`SearchDSL` and the `QueryClauses` interface. This makes it easy to use with the search
functions in the client or `IndexRepository`.
Here's a simple example of a query that looks for the "best thing ever" suitable for work.
We'll build out this example to show you how to
""".trimIndent()

example {
client.search(indexName) {
query = bool {
filter(term(TestDoc::tags, "work"))
must(matchPhrase(TestDoc::name, "best thing ever"))
}
}
}

+"""
In this example, bool, and match are all extension functions on `QueryClauses` and
the receiver block has a `this` of type `SearchDSL`, which implements `QueryClauses`.
"""

section("Adding some logic") {
+"""
Because it is all kotlin, you can programmatically construct complex queries,
use conditional logic, maybe look up a few things and generate clauses for that, etc.
All while keeping things type safe.
Let's make our example a bit more interesting by taking time into account.
""".trimIndent()

example {
val now = Clock.System.now()

client.search(indexName) {
query = bool {
val hour = now.toLocalDateTime(TimeZone.UTC).hour
if (hour in 9..17) {
filter(
term(TestDoc::tags, "work")
)
} else {
filter(
term(TestDoc::tags, "leisure")
)
}
must(matchPhrase(TestDoc::name, "best thing ever"))
}
}
}

+"""
This simple example creates different queries depending on the time.
After all, what's the best thing ever probably depends on some context.
Which would include the time of day.
""".trimIndent()
}


section("Writing your own extension function") {
+"""
Conditional logic like above can get complicated pretty quickly. To keep that under control,
you will want to use all the usual strategies such as extracting functions,
using object oriented designs, or doing things with extension functions, delegation, etc.
Note, that the example above uses UTC. Obviously we'd want to take things like
Timezones into account as well. And maybe the user has a calendar and a
busy schedule. There are all sorts of things to consider!
The point is that queries can get complicated quite quickly and you need
good mechanisms to keep things clean and structured. Kt-search provides you all the tools
you need to stay on top of this.
""".trimIndent()

example {
fun QueryClauses.timeTagClause() = term(TestDoc::tags,
if(Clock.System.now().toLocalDateTime(TimeZone.UTC).hour in 9..17) {
"work"
} else {
"leisure"
})

client.search(indexName) {
bool {
filter(timeTagClause())
must(matchPhrase(TestDoc::name, "best thing ever"))
}
}

client.search(indexName) {
bool {
filter(timeTagClause())
must(matchPhrase(TestDoc::name, "something I need"))
}
}
}
+"""
Here's an improved version of the query. We extracted the logic that creates the term clause
and we can now easily add it to different queries.
""".trimIndent()
}
section("Helper functions") {
+"""
The above example is nice but it is not always practical to rely on having a receiver object.
""".trimIndent()

example {
class MyFancyQueryBuilder {
fun timeTagClause() = constructQueryClause {
term(
TestDoc::tags,
if (Clock.System.now().toLocalDateTime(TimeZone.UTC).hour in 9..17) {
"work"
} else {
"leisure"
}
)
}
}

val qb = MyFancyQueryBuilder()

client.search(indexName) {
bool {
filter(qb.timeTagClause())
must(matchPhrase(TestDoc::name, "best thing ever"))
}
}
}
+"""
Here we introduce a helper class. You could imagine adding all sorts of logic and helper classes.
Of course using a class creates the problem that using that it complicates using extension functions.
The `constructQueryClause` helper function provides a solution to that problem. It allows you to create
functions that take a block that expects a `QueryClauses` object and returns an `ESQuery`.
And it then creates an anonymous QueryClauses object for you uses that to call your block.
""".trimIndent()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,9 @@ class SearchTest : SearchTestBase() {
client.indexDocument(index, TestDocument("bar").json(false), refresh = Refresh.WaitFor, id = "3")

val result = client.search("$index,$index") {
query = matchAll(boost = 3.5)
query = constructQueryClause {
matchAll(boost = 3.5)
}
}
result.hits!!.hits shouldHaveSize 3
result.hits!!.hits.map(SearchResponse.Hit::score) shouldBe listOf(3.5, 3.5, 3.5)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import io.ktor.client.HttpClient
import io.ktor.client.HttpClientConfig
import io.ktor.client.engine.curl.Curl

// FIXME intellij weirdness with this class on mac; none of the ktor stuff is found somehow
// of course it builds fine with gradle; no idea if this is actually usable
actual fun defaultKtorHttpClient(
logging: Boolean,
user: String?,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,3 +1,17 @@
package com.jillesvangurp.searchdsls.querydsl

interface QueryClauses
interface QueryClauses

/**
* Allows you to create a query without a search dsl context. Returns the [ESQuery] created by your [block].
*
* This makes it possible to create utility functions that aren't extension functions
* on QueryClauses that construct queries. The core use case is reusable functionality for
* building queries or parts of queries.
*
* It works by creating an anonymous object implementing QueryClauses and then passing that to [block].
*/
fun constructQueryClause(block: QueryClauses.() -> ESQuery): ESQuery {
val obj = object : QueryClauses {}
return block(obj)
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,19 +23,19 @@ class BoolQuery : ESQuery(name = "bool") {
var boost by queryDetails.property<Double>()
}

fun SearchDSL.bool(block: BoolQuery.() -> Unit): BoolQuery {
fun QueryClauses.bool(block: BoolQuery.() -> Unit): BoolQuery {
val q = BoolQuery()
block.invoke(q)
return q
}

fun SearchDSL.or(queries: List<ESQuery>) = bool { should(queries) }
fun SearchDSL.and(queries: List<ESQuery>) = bool { filter(queries) }
fun SearchDSL.not(queries: List<ESQuery>) = bool { mustNot(queries) }
fun QueryClauses.or(queries: List<ESQuery>) = bool { should(queries) }
fun QueryClauses.and(queries: List<ESQuery>) = bool { filter(queries) }
fun QueryClauses.not(queries: List<ESQuery>) = bool { mustNot(queries) }

fun SearchDSL.or(vararg queries: ESQuery) = bool { should(*queries) }
fun SearchDSL.and(vararg queries: ESQuery) = bool { filter(*queries) }
fun SearchDSL.not(vararg queries: ESQuery) = bool { mustNot(*queries) }
fun QueryClauses.or(vararg queries: ESQuery) = bool { should(*queries) }
fun QueryClauses.and(vararg queries: ESQuery) = bool { filter(*queries) }
fun QueryClauses.not(vararg queries: ESQuery) = bool { mustNot(*queries) }

@SearchDSLMarker
class BoostingQuery : ESQuery(name = "boosting") {
Expand All @@ -51,7 +51,7 @@ class BoostingQuery : ESQuery(name = "boosting") {
}
}

fun SearchDSL.boosting(block: BoostingQuery.() -> Unit): BoostingQuery {
fun QueryClauses.boosting(block: BoostingQuery.() -> Unit): BoostingQuery {
val q = BoostingQuery()
block.invoke(q)
return q
Expand All @@ -63,7 +63,7 @@ class ConstantScoreQuery : ESQuery(name = "constant_score") {
var boost: Double by queryDetails.property()
}

fun SearchDSL.constantScore(block: ConstantScoreQuery.() -> Unit): ConstantScoreQuery {
fun QueryClauses.constantScore(block: ConstantScoreQuery.() -> Unit): ConstantScoreQuery {
val q = ConstantScoreQuery()
block.invoke(q)
return q
Expand All @@ -77,7 +77,7 @@ class DisMaxQuery : ESQuery(name = "dis_max") {
var boost: Double by queryDetails.property()
}

fun SearchDSL.disMax(block: DisMaxQuery.() -> Unit): DisMaxQuery {
fun QueryClauses.disMax(block: DisMaxQuery.() -> Unit): DisMaxQuery {
val q = DisMaxQuery()
block.invoke(q)
return q
Expand Down Expand Up @@ -212,6 +212,6 @@ class FunctionScoreQuery(
}
}

fun SearchDSL.functionScore(block: FunctionScoreQuery.() -> Unit): FunctionScoreQuery {
fun QueryClauses.functionScore(block: FunctionScoreQuery.() -> Unit): FunctionScoreQuery {
return FunctionScoreQuery(block)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ class DistanceFeature() : ESQuery("distance_feature") {
var boost by property<Double>()
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: KProperty<*>,
pivot: String,
origin: String,
Expand All @@ -23,7 +23,7 @@ fun SearchDSL.distanceFeature(
it
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: String,
pivot: String,
origin: String,
Expand All @@ -36,7 +36,7 @@ fun SearchDSL.distanceFeature(
it
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: KProperty<*>,
pivot: String,
origin: List<Double>,
Expand All @@ -49,7 +49,7 @@ fun SearchDSL.distanceFeature(
it
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: String,
pivot: String,
origin: List<Double>,
Expand All @@ -62,7 +62,7 @@ fun SearchDSL.distanceFeature(
it
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: KProperty<*>,
pivot: String,
origin: Array<Double>,
Expand All @@ -75,7 +75,7 @@ fun SearchDSL.distanceFeature(
it
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: String,
pivot: String,
origin: Array<Double>,
Expand All @@ -88,7 +88,7 @@ fun SearchDSL.distanceFeature(
it
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: KProperty<*>,
pivot: String,
origin: DoubleArray,
Expand All @@ -101,7 +101,7 @@ fun SearchDSL.distanceFeature(
it
}

fun SearchDSL.distanceFeature(
fun QueryClauses.distanceFeature(
field: String,
pivot: String,
origin: DoubleArray,
Expand Down Expand Up @@ -166,5 +166,5 @@ class RankFeature(val field: String, block: (RankFeature.() -> Unit)? = null) :
}
}

fun SearchDSL.rankFeature(field: String, block: (RankFeature.() -> Unit)? = null) = RankFeature(field, block)
fun SearchDSL.rankFeature(field: KProperty<*>, block: (RankFeature.() -> Unit)? = null) = RankFeature(field, block)
fun QueryClauses.rankFeature(field: String, block: (RankFeature.() -> Unit)? = null) = RankFeature(field, block)
fun QueryClauses.rankFeature(field: KProperty<*>, block: (RankFeature.() -> Unit)? = null) = RankFeature(field, block)

0 comments on commit 2a3c5cc

Please sign in to comment.