Skip to content

Commit fa1f343

Browse files
authored
(dsl): Support Disjunction max query (#360)
1 parent 8c2e8c8 commit fa1f343

File tree

7 files changed

+214
-2
lines changed

7 files changed

+214
-2
lines changed
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
---
2+
id: elastic_query_disjunction_max
3+
title: "Disjunction max Query"
4+
---
5+
6+
The `Disjunction max` query returns documents that match one or more query clauses. For documents that match multiple query clauses, the relevance score is set to the highest relevance score from all matching query clauses. When the relevance scores of the returned documents are identical, tie breaker parameter can be used for giving more weight to documents that match multiple query clauses.
7+
8+
In order to use the `Disjunction max` query import the following:
9+
```scala
10+
import zio.elasticsearch.query.DisjunctionMax
11+
import zio.elasticsearch.ElasticQuery.disjunctionMax
12+
```
13+
14+
You can create a `Disjunction max` query using the `disjunctionMax` method this way:
15+
```scala
16+
val query: DisjunctionMaxQuery = disjunctionMax(query = term(field = "stringField", value = "test"), queries = exists(field = "intField"), exists(field = "existsField"))
17+
```
18+
19+
You can create a [type-safe](https://lambdaworks.github.io/zio-elasticsearch/overview/overview_zio_prelude_schema) `Disjunction max` query using the `disjunctionMax` method this way:
20+
```scala
21+
val query: DisjunctionMaxQuery = disjunctionMax(query = term(field = Document.stringField, value = "test"), queries = exists(field = Document.intField), term(field = Document.termField, value = "test"))
22+
```
23+
24+
If you want to change the `tieBreaker`, you can use `tieBreaker` method:
25+
```scala
26+
val queryWithTieBreaker: DisjunctionMaxQuery = disjunctionMax(query = exists(field = "existsField"), queries = ids(values = "1", "2", "3"), term(field = "termField", value = "test")).tieBreaker(0.5f)
27+
```
28+
29+
You can find more information about `Disjunction max` query [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-dis-max-query.html).

modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1161,6 +1161,44 @@ object HttpExecutorSpec extends IntegrationSpec {
11611161
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
11621162
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
11631163
),
1164+
test("search for a document using a disjunction max query") {
1165+
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
1166+
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
1167+
for {
1168+
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
1169+
firstDocumentUpdated =
1170+
firstDocument.copy(stringField = s"This is a ${firstDocument.stringField} test.")
1171+
secondDocumentUpdated =
1172+
secondDocument.copy(stringField =
1173+
s"This is a ${secondDocument.stringField} test. It should be in the list before ${firstDocument.stringField}, because it has higher relevance score than ${firstDocument.stringField}"
1174+
)
1175+
_ <- Executor.execute(
1176+
ElasticRequest
1177+
.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocumentUpdated)
1178+
)
1179+
_ <-
1180+
Executor.execute(
1181+
ElasticRequest
1182+
.upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocumentUpdated)
1183+
.refreshTrue
1184+
)
1185+
query = disjunctionMax(
1186+
term(
1187+
field = TestDocument.stringField,
1188+
value = firstDocument.stringField.toLowerCase
1189+
),
1190+
matchPhrase(
1191+
field = TestDocument.stringField,
1192+
value = secondDocument.stringField
1193+
)
1194+
)
1195+
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1196+
} yield assert(res)(equalTo(Chunk(secondDocumentUpdated, firstDocumentUpdated)))
1197+
}
1198+
} @@ around(
1199+
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
1200+
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
1201+
),
11641202
test("search for a document using a fuzzy query") {
11651203
checkOnce(genDocumentId, genTestDocument) { (firstDocumentId, firstDocument) =>
11661204
for {

modules/library/src/main/scala/zio/elasticsearch/ElasticPrimitive.scala

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,10 @@ object ElasticPrimitive {
3939
def toJson(value: Double): Json = Num(value)
4040
}
4141

42+
implicit object ElasticFloat extends ElasticPrimitive[Float] {
43+
def toJson(value: Float): Json = Num(value)
44+
}
45+
4246
implicit object ElasticInt extends ElasticPrimitive[Int] {
4347
def toJson(value: Int): Json = Num(value)
4448
}

modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,32 @@ object ElasticQuery {
8787
final def contains(field: String, value: String): WildcardQuery[Any] =
8888
Wildcard(field = field, value = s"*$value*", boost = None, caseInsensitive = None)
8989

90+
/**
91+
* Constructs a type-safe instance of [[zio.elasticsearch.query.DisjunctionMax]] using the specified parameters.
92+
*
93+
* @param queries
94+
* the rest of the queries to be wrapped inside of disjunction max query
95+
* @tparam S
96+
* document for which field query is executed. An implicit `Schema` instance must be in scope
97+
* @return
98+
* an instance of [[zio.elasticsearch.query.DisjunctionMax]] that represents the `disjunction max` query to be
99+
* performed.
100+
*/
101+
final def disjunctionMax[S: Schema](query: ElasticQuery[S], queries: ElasticQuery[S]*): DisjunctionMaxQuery[S] =
102+
DisjunctionMax[S](queries = query +: Chunk.fromIterable(queries), tieBreaker = None)
103+
104+
/**
105+
* Constructs an instance of [[zio.elasticsearch.query.DisjunctionMax]] using the specified parameters.
106+
*
107+
* @param queries
108+
* the rest of the queries to be wrapped inside of disjunction max query
109+
* @return
110+
* an instance of [[zio.elasticsearch.query.DisjunctionMax]] that represents the `disjunction max` query to be
111+
* performed.
112+
*/
113+
final def disjunctionMax(query: ElasticQuery[Any], queries: ElasticQuery[Any]*): DisjunctionMaxQuery[Any] =
114+
DisjunctionMax[Any](queries = query +: Chunk.fromIterable(queries), tieBreaker = None)
115+
90116
/**
91117
* Constructs a type-safe instance of [[zio.elasticsearch.query.ExistsQuery]], that checks existence of the field,
92118
* using the specified parameters.
@@ -215,7 +241,7 @@ object ElasticQuery {
215241
* @tparam S
216242
* document for which field query is executed
217243
* @return
218-
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the fuzzy query to be performed.
244+
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the `fuzzy` query to be performed.
219245
*/
220246
final def fuzzy[S](field: Field[S, String], value: String): FuzzyQuery[S] =
221247
Fuzzy(field = field.toString, value = value, fuzziness = None, maxExpansions = None, prefixLength = None)
@@ -230,7 +256,7 @@ object ElasticQuery {
230256
* @param value
231257
* text value that will be used for the query
232258
* @return
233-
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the fuzzy query to be performed.
259+
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the `fuzzy` query to be performed.
234260
*/
235261
final def fuzzy(field: String, value: String): FuzzyQuery[Any] =
236262
Fuzzy(field = field, value = value, fuzziness = None, maxExpansions = None, prefixLength = None)

modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,39 @@ private[elasticsearch] final case class ConstantScore[S](query: ElasticQuery[S],
200200
)
201201
}
202202

203+
sealed trait DisjunctionMaxQuery[S] extends ElasticQuery[S] {
204+
205+
/**
206+
* Sets the `tieBreaker` parameter for the [[zio.elasticsearch.query.DisjunctionMaxQuery]]. The `tieBreaker` value is
207+
* a floating-point factor between 0 and 1.0 that is used to give more weight to documents that match multiple query
208+
* clauses. Default is 0 (which means only the highest score counts).
209+
*
210+
* @param value
211+
* a number to set `tieBreaker` parameter to
212+
* @return
213+
* an instance of the [[zio.elasticsearch.query.DisjunctionMaxQuery]] enriched with the `tieBreaker` parameter.
214+
*/
215+
def tieBreaker(value: Float): DisjunctionMaxQuery[S]
216+
}
217+
218+
private[elasticsearch] final case class DisjunctionMax[S](
219+
queries: Chunk[ElasticQuery[S]],
220+
tieBreaker: Option[Float]
221+
) extends DisjunctionMaxQuery[S] { self =>
222+
223+
def tieBreaker(value: Float): DisjunctionMaxQuery[S] =
224+
self.copy(tieBreaker = Some(value))
225+
226+
private[elasticsearch] def toJson(fieldPath: Option[String]): Json = {
227+
val disMaxFields =
228+
Chunk(
229+
Some("queries" -> Arr(queries.map(_.toJson(fieldPath)))),
230+
tieBreaker.map("tie_breaker" -> _.toJson)
231+
).collect { case Some(obj) => obj }
232+
Obj("dis_max" -> Obj(disMaxFields))
233+
}
234+
}
235+
203236
sealed trait ExistsQuery[S] extends ElasticQuery[S] with HasBoost[ExistsQuery[S]]
204237

205238
private[elasticsearch] final case class Exists[S](field: String, boost: Option[Double]) extends ExistsQuery[S] { self =>

modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -416,6 +416,39 @@ object ElasticQuerySpec extends ZIOSpecDefault {
416416
)
417417
)
418418
},
419+
test("disjunctionMax") {
420+
val query = disjunctionMax(exists("existsField"), ids("1", "2", "3"))
421+
val queryTs = disjunctionMax(exists(TestDocument.stringField), ids("1", "2", "3"))
422+
val queryWithTieBreaker = disjunctionMax(exists("existsField"), ids("1", "2", "3")).tieBreaker(0.5f)
423+
424+
assert(query)(
425+
equalTo(
426+
DisjunctionMax[Any](
427+
queries =
428+
Chunk(Exists[Any](field = "existsField", boost = None), Ids[Any](values = Chunk("1", "2", "3"))),
429+
tieBreaker = None
430+
)
431+
)
432+
) &&
433+
assert(queryTs)(
434+
equalTo(
435+
DisjunctionMax[TestDocument](
436+
queries =
437+
Chunk(Exists[Any](field = "stringField", boost = None), Ids[Any](values = Chunk("1", "2", "3"))),
438+
tieBreaker = None
439+
)
440+
)
441+
) &&
442+
assert(queryWithTieBreaker)(
443+
equalTo(
444+
DisjunctionMax[Any](
445+
queries =
446+
Chunk(Exists[Any](field = "existsField", boost = None), Ids[Any](values = Chunk("1", "2", "3"))),
447+
tieBreaker = Some(0.5f)
448+
)
449+
)
450+
)
451+
},
419452
test("exists") {
420453
val query = exists("testField")
421454
val queryTs = exists(TestDocument.intField)
@@ -2507,6 +2540,53 @@ object ElasticQuerySpec extends ZIOSpecDefault {
25072540
assert(queryWithCaseInsensitive.toJson(fieldPath = None))(equalTo(expectedWithCaseInsensitive.toJson)) &&
25082541
assert(queryWithAllParams.toJson(fieldPath = None))(equalTo(expectedWithAllParams.toJson))
25092542
},
2543+
test("disjunctionMax") {
2544+
val query = disjunctionMax(exists("existsField"), ids("1", "2", "3"))
2545+
val queryTs = disjunctionMax(exists(TestDocument.stringField), ids("1", "2", "3"))
2546+
val queryWithTieBreaker =
2547+
disjunctionMax(exists("existsField"), ids("1", "2", "3")).tieBreaker(0.5f)
2548+
2549+
val expected =
2550+
"""
2551+
|{
2552+
| "dis_max": {
2553+
| "queries": [
2554+
| { "exists": { "field": "existsField" } },
2555+
| { "ids": { "values": ["1", "2", "3"] } }
2556+
| ]
2557+
| }
2558+
|}
2559+
|""".stripMargin
2560+
2561+
val expectedTs =
2562+
"""
2563+
|{
2564+
| "dis_max": {
2565+
| "queries": [
2566+
| { "exists": { "field": "stringField" } },
2567+
| { "ids": { "values": ["1", "2", "3"] } }
2568+
| ]
2569+
| }
2570+
|}
2571+
|""".stripMargin
2572+
2573+
val expectedWithTieBreaker =
2574+
"""
2575+
|{
2576+
| "dis_max": {
2577+
| "queries": [
2578+
| { "exists": { "field": "existsField" } },
2579+
| { "ids": { "values": ["1", "2", "3"] } }
2580+
| ],
2581+
| "tie_breaker": 0.5
2582+
| }
2583+
|}
2584+
|""".stripMargin
2585+
2586+
assert(query.toJson(fieldPath = None))(equalTo(expected.toJson)) &&
2587+
assert(queryTs.toJson(fieldPath = None))(equalTo(expectedTs.toJson)) &&
2588+
assert(queryWithTieBreaker.toJson(fieldPath = None))(equalTo(expectedWithTieBreaker.toJson))
2589+
},
25102590
test("exists") {
25112591
val query = exists("testField")
25122592
val queryTs = exists(TestDocument.dateField)

website/sidebars.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ module.exports = {
1616
'overview/elastic_query',
1717
'overview/queries/elastic_query_bool',
1818
'overview/queries/elastic_query_constant_score',
19+
'overview/queries/elastic_query_disjunction_max',
1920
'overview/queries/elastic_query_exists',
2021
'overview/queries/elastic_query_function_score',
2122
'overview/queries/elastic_query_fuzzy',
@@ -57,6 +58,7 @@ module.exports = {
5758
'overview/aggregations/elastic_aggregation_sum',
5859
'overview/aggregations/elastic_aggregation_terms',
5960
'overview/aggregations/elastic_aggregation_value_count',
61+
'overview/aggregations/elastic_aggregation_weighted_avg',
6062
],
6163
},
6264
{

0 commit comments

Comments
 (0)