Skip to content

Commit

Permalink
(dsl): Support Disjunction max query (#360)
Browse files Browse the repository at this point in the history
  • Loading branch information
vanjaftn authored Nov 17, 2023
1 parent 8c2e8c8 commit fa1f343
Show file tree
Hide file tree
Showing 7 changed files with 214 additions and 2 deletions.
29 changes: 29 additions & 0 deletions docs/overview/queries/elastic_query_disjunction_max.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
---
id: elastic_query_disjunction_max
title: "Disjunction max Query"
---

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.

In order to use the `Disjunction max` query import the following:
```scala
import zio.elasticsearch.query.DisjunctionMax
import zio.elasticsearch.ElasticQuery.disjunctionMax
```

You can create a `Disjunction max` query using the `disjunctionMax` method this way:
```scala
val query: DisjunctionMaxQuery = disjunctionMax(query = term(field = "stringField", value = "test"), queries = exists(field = "intField"), exists(field = "existsField"))
```

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:
```scala
val query: DisjunctionMaxQuery = disjunctionMax(query = term(field = Document.stringField, value = "test"), queries = exists(field = Document.intField), term(field = Document.termField, value = "test"))
```

If you want to change the `tieBreaker`, you can use `tieBreaker` method:
```scala
val queryWithTieBreaker: DisjunctionMaxQuery = disjunctionMax(query = exists(field = "existsField"), queries = ids(values = "1", "2", "3"), term(field = "termField", value = "test")).tieBreaker(0.5f)
```

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).
Original file line number Diff line number Diff line change
Expand Up @@ -1161,6 +1161,44 @@ object HttpExecutorSpec extends IntegrationSpec {
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
),
test("search for a document using a disjunction max query") {
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
for {
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
firstDocumentUpdated =
firstDocument.copy(stringField = s"This is a ${firstDocument.stringField} test.")
secondDocumentUpdated =
secondDocument.copy(stringField =
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}"
)
_ <- Executor.execute(
ElasticRequest
.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocumentUpdated)
)
_ <-
Executor.execute(
ElasticRequest
.upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocumentUpdated)
.refreshTrue
)
query = disjunctionMax(
term(
field = TestDocument.stringField,
value = firstDocument.stringField.toLowerCase
),
matchPhrase(
field = TestDocument.stringField,
value = secondDocument.stringField
)
)
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
} yield assert(res)(equalTo(Chunk(secondDocumentUpdated, firstDocumentUpdated)))
}
} @@ around(
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
),
test("search for a document using a fuzzy query") {
checkOnce(genDocumentId, genTestDocument) { (firstDocumentId, firstDocument) =>
for {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ object ElasticPrimitive {
def toJson(value: Double): Json = Num(value)
}

implicit object ElasticFloat extends ElasticPrimitive[Float] {
def toJson(value: Float): Json = Num(value)
}

implicit object ElasticInt extends ElasticPrimitive[Int] {
def toJson(value: Int): Json = Num(value)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,32 @@ object ElasticQuery {
final def contains(field: String, value: String): WildcardQuery[Any] =
Wildcard(field = field, value = s"*$value*", boost = None, caseInsensitive = None)

/**
* Constructs a type-safe instance of [[zio.elasticsearch.query.DisjunctionMax]] using the specified parameters.
*
* @param queries
* the rest of the queries to be wrapped inside of disjunction max query
* @tparam S
* document for which field query is executed. An implicit `Schema` instance must be in scope
* @return
* an instance of [[zio.elasticsearch.query.DisjunctionMax]] that represents the `disjunction max` query to be
* performed.
*/
final def disjunctionMax[S: Schema](query: ElasticQuery[S], queries: ElasticQuery[S]*): DisjunctionMaxQuery[S] =
DisjunctionMax[S](queries = query +: Chunk.fromIterable(queries), tieBreaker = None)

/**
* Constructs an instance of [[zio.elasticsearch.query.DisjunctionMax]] using the specified parameters.
*
* @param queries
* the rest of the queries to be wrapped inside of disjunction max query
* @return
* an instance of [[zio.elasticsearch.query.DisjunctionMax]] that represents the `disjunction max` query to be
* performed.
*/
final def disjunctionMax(query: ElasticQuery[Any], queries: ElasticQuery[Any]*): DisjunctionMaxQuery[Any] =
DisjunctionMax[Any](queries = query +: Chunk.fromIterable(queries), tieBreaker = None)

/**
* Constructs a type-safe instance of [[zio.elasticsearch.query.ExistsQuery]], that checks existence of the field,
* using the specified parameters.
Expand Down Expand Up @@ -215,7 +241,7 @@ object ElasticQuery {
* @tparam S
* document for which field query is executed
* @return
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the fuzzy query to be performed.
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the `fuzzy` query to be performed.
*/
final def fuzzy[S](field: Field[S, String], value: String): FuzzyQuery[S] =
Fuzzy(field = field.toString, value = value, fuzziness = None, maxExpansions = None, prefixLength = None)
Expand All @@ -230,7 +256,7 @@ object ElasticQuery {
* @param value
* text value that will be used for the query
* @return
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the fuzzy query to be performed.
* an instance of [[zio.elasticsearch.query.FuzzyQuery]] that represents the `fuzzy` query to be performed.
*/
final def fuzzy(field: String, value: String): FuzzyQuery[Any] =
Fuzzy(field = field, value = value, fuzziness = None, maxExpansions = None, prefixLength = None)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,39 @@ private[elasticsearch] final case class ConstantScore[S](query: ElasticQuery[S],
)
}

sealed trait DisjunctionMaxQuery[S] extends ElasticQuery[S] {

/**
* Sets the `tieBreaker` parameter for the [[zio.elasticsearch.query.DisjunctionMaxQuery]]. The `tieBreaker` value is
* a floating-point factor between 0 and 1.0 that is used to give more weight to documents that match multiple query
* clauses. Default is 0 (which means only the highest score counts).
*
* @param value
* a number to set `tieBreaker` parameter to
* @return
* an instance of the [[zio.elasticsearch.query.DisjunctionMaxQuery]] enriched with the `tieBreaker` parameter.
*/
def tieBreaker(value: Float): DisjunctionMaxQuery[S]
}

private[elasticsearch] final case class DisjunctionMax[S](
queries: Chunk[ElasticQuery[S]],
tieBreaker: Option[Float]
) extends DisjunctionMaxQuery[S] { self =>

def tieBreaker(value: Float): DisjunctionMaxQuery[S] =
self.copy(tieBreaker = Some(value))

private[elasticsearch] def toJson(fieldPath: Option[String]): Json = {
val disMaxFields =
Chunk(
Some("queries" -> Arr(queries.map(_.toJson(fieldPath)))),
tieBreaker.map("tie_breaker" -> _.toJson)
).collect { case Some(obj) => obj }
Obj("dis_max" -> Obj(disMaxFields))
}
}

sealed trait ExistsQuery[S] extends ElasticQuery[S] with HasBoost[ExistsQuery[S]]

private[elasticsearch] final case class Exists[S](field: String, boost: Option[Double]) extends ExistsQuery[S] { self =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,39 @@ object ElasticQuerySpec extends ZIOSpecDefault {
)
)
},
test("disjunctionMax") {
val query = disjunctionMax(exists("existsField"), ids("1", "2", "3"))
val queryTs = disjunctionMax(exists(TestDocument.stringField), ids("1", "2", "3"))
val queryWithTieBreaker = disjunctionMax(exists("existsField"), ids("1", "2", "3")).tieBreaker(0.5f)

assert(query)(
equalTo(
DisjunctionMax[Any](
queries =
Chunk(Exists[Any](field = "existsField", boost = None), Ids[Any](values = Chunk("1", "2", "3"))),
tieBreaker = None
)
)
) &&
assert(queryTs)(
equalTo(
DisjunctionMax[TestDocument](
queries =
Chunk(Exists[Any](field = "stringField", boost = None), Ids[Any](values = Chunk("1", "2", "3"))),
tieBreaker = None
)
)
) &&
assert(queryWithTieBreaker)(
equalTo(
DisjunctionMax[Any](
queries =
Chunk(Exists[Any](field = "existsField", boost = None), Ids[Any](values = Chunk("1", "2", "3"))),
tieBreaker = Some(0.5f)
)
)
)
},
test("exists") {
val query = exists("testField")
val queryTs = exists(TestDocument.intField)
Expand Down Expand Up @@ -2507,6 +2540,53 @@ object ElasticQuerySpec extends ZIOSpecDefault {
assert(queryWithCaseInsensitive.toJson(fieldPath = None))(equalTo(expectedWithCaseInsensitive.toJson)) &&
assert(queryWithAllParams.toJson(fieldPath = None))(equalTo(expectedWithAllParams.toJson))
},
test("disjunctionMax") {
val query = disjunctionMax(exists("existsField"), ids("1", "2", "3"))
val queryTs = disjunctionMax(exists(TestDocument.stringField), ids("1", "2", "3"))
val queryWithTieBreaker =
disjunctionMax(exists("existsField"), ids("1", "2", "3")).tieBreaker(0.5f)

val expected =
"""
|{
| "dis_max": {
| "queries": [
| { "exists": { "field": "existsField" } },
| { "ids": { "values": ["1", "2", "3"] } }
| ]
| }
|}
|""".stripMargin

val expectedTs =
"""
|{
| "dis_max": {
| "queries": [
| { "exists": { "field": "stringField" } },
| { "ids": { "values": ["1", "2", "3"] } }
| ]
| }
|}
|""".stripMargin

val expectedWithTieBreaker =
"""
|{
| "dis_max": {
| "queries": [
| { "exists": { "field": "existsField" } },
| { "ids": { "values": ["1", "2", "3"] } }
| ],
| "tie_breaker": 0.5
| }
|}
|""".stripMargin

assert(query.toJson(fieldPath = None))(equalTo(expected.toJson)) &&
assert(queryTs.toJson(fieldPath = None))(equalTo(expectedTs.toJson)) &&
assert(queryWithTieBreaker.toJson(fieldPath = None))(equalTo(expectedWithTieBreaker.toJson))
},
test("exists") {
val query = exists("testField")
val queryTs = exists(TestDocument.dateField)
Expand Down
2 changes: 2 additions & 0 deletions website/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ module.exports = {
'overview/elastic_query',
'overview/queries/elastic_query_bool',
'overview/queries/elastic_query_constant_score',
'overview/queries/elastic_query_disjunction_max',
'overview/queries/elastic_query_exists',
'overview/queries/elastic_query_function_score',
'overview/queries/elastic_query_fuzzy',
Expand Down Expand Up @@ -57,6 +58,7 @@ module.exports = {
'overview/aggregations/elastic_aggregation_sum',
'overview/aggregations/elastic_aggregation_terms',
'overview/aggregations/elastic_aggregation_value_count',
'overview/aggregations/elastic_aggregation_weighted_avg',
],
},
{
Expand Down

0 comments on commit fa1f343

Please sign in to comment.