Skip to content

Commit dbeb4b0

Browse files
authored
(dsl): Support Terms set query (#355)
1 parent 8f9990c commit dbeb4b0

File tree

6 files changed

+814
-9
lines changed

6 files changed

+814
-9
lines changed
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
---
2+
id: elastic_query_terms_set
3+
title: "Terms Set Query"
4+
---
5+
6+
The `TermsSet` query returns documents that contain the minimum amount of exact terms in a provided field. The Terms set query is the same as [[zio.elasticsearch.query.TermsQuery]], except you can define the number of matching terms required to return a document.
7+
8+
In order to use the `TermsSet` query import the following:
9+
```scala
10+
import zio.elasticsearch.query.TermsSetQuery
11+
import zio.elasticsearch.ElasticQuery.termsSetQuery
12+
```
13+
14+
You can create a `TermsSet` query with defined `minimumShouldMatchField` using the `termsSet` method this way:
15+
```scala
16+
val query: TermsSetQuery = termsSet(field = "stringField", minimumShouldMatchField = "intField", terms = "a", "b", "c")
17+
```
18+
19+
You can create a [type-safe](https://lambdaworks.github.io/zio-elasticsearch/overview/overview_zio_prelude_schema) `TermsSet` query with defined `minimumShouldMatchField` using the `termsSet` method this way:
20+
```scala
21+
val query: TermsSetQuery = termsSet(field = Document.name, minimumShouldMatchField = Document.intField, terms = "a", "b", "c")
22+
```
23+
24+
You can create a `TermsSet` query with defined `minimumShouldMatchScript` using the `termsSetScript` method this way:
25+
```scala
26+
import zio.elasticsearch.script.Script
27+
28+
val query: TermsSetQuery = termsSetScript(field = "stringField", minimumShouldMatchScript = Script("doc['intField'].value"), terms = "a", "b", "c")
29+
```
30+
31+
You can create a [type-safe](https://lambdaworks.github.io/zio-elasticsearch/overview/overview_zio_prelude_schema) `TermsSet` query with defined `minimumShouldMatchScript` using the `termsSetScript` method this way:
32+
```scala
33+
import zio.elasticsearch.script.Script
34+
35+
val query: TermsSetQuery = termsSetScript(field = Document.name, minimumShouldMatchScript = Script("doc['intField'].value"), terms = "a", "b", "c")
36+
```
37+
38+
If you want to change the `boost`, you can use `boost` method:
39+
```scala
40+
val queryWithBoostAndMinimumShouldMatchField: TermsSetQuery = termsSet(field = "booleanField", minimumShouldMatchField = "intField", terms = true, false).boost(2.0)
41+
val queryWithBoostAndMinimumShouldMatchScript: TermsSetQuery = termsSetScript(field = "booleanField", minimumShouldMatchScript = Script("doc['intField'].value"), terms = true, false).boost(2.0)
42+
```
43+
44+
You can find more information about `TermsSet` query [here](https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl-terms-set-query.html).

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

Lines changed: 73 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1415,6 +1415,77 @@ object HttpExecutorSpec extends IntegrationSpec {
14151415
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
14161416
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
14171417
),
1418+
test("search for a document using a terms set query with minimumShouldMatchField") {
1419+
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
1420+
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
1421+
for {
1422+
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
1423+
firstDocumentUpdated =
1424+
firstDocument.copy(stringField = s"this is ${firstDocument.stringField} test", intField = 2)
1425+
secondDocumentUpdated =
1426+
secondDocument.copy(
1427+
stringField =
1428+
s"this is ${secondDocument.stringField} another test, not ${firstDocument.stringField}",
1429+
intField = 2
1430+
)
1431+
_ <-
1432+
Executor.execute(
1433+
ElasticRequest
1434+
.bulk(
1435+
ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocumentUpdated),
1436+
ElasticRequest
1437+
.upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocumentUpdated)
1438+
)
1439+
.refreshTrue
1440+
)
1441+
query = termsSet(
1442+
field = "stringField",
1443+
minimumShouldMatchField = "intField",
1444+
terms = secondDocument.stringField.toLowerCase,
1445+
firstDocument.stringField.toLowerCase
1446+
)
1447+
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1448+
} yield assert(res)(hasSameElements(Chunk(secondDocumentUpdated)))
1449+
}
1450+
} @@ around(
1451+
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
1452+
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
1453+
),
1454+
test("search for a document using a terms set query with minimumShouldMatchScript") {
1455+
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
1456+
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
1457+
for {
1458+
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
1459+
firstDocumentUpdated =
1460+
firstDocument.copy(stringField = s"this is ${firstDocument.stringField} test", intField = 2)
1461+
secondDocumentUpdated =
1462+
secondDocument.copy(
1463+
stringField = s"this is ${secondDocument.stringField} test, not ${firstDocument.stringField}",
1464+
intField = 2
1465+
)
1466+
_ <-
1467+
Executor.execute(
1468+
ElasticRequest
1469+
.bulk(
1470+
ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocumentUpdated),
1471+
ElasticRequest
1472+
.upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocumentUpdated)
1473+
)
1474+
.refreshTrue
1475+
)
1476+
query = termsSetScript(
1477+
field = TestDocument.stringField,
1478+
minimumShouldMatchScript = Script("doc['intField'].value"),
1479+
terms = firstDocument.stringField.toLowerCase,
1480+
secondDocument.stringField.toLowerCase
1481+
)
1482+
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1483+
} yield assert(res)(hasSameElements(Chunk(secondDocumentUpdated)))
1484+
}
1485+
} @@ around(
1486+
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
1487+
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
1488+
),
14181489
test("search for a document using nested query") {
14191490
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
14201491
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
@@ -1470,7 +1541,7 @@ object HttpExecutorSpec extends IntegrationSpec {
14701541
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
14711542
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
14721543
),
1473-
test("search for a document using should without satisfying minimumShouldMatch condition") {
1544+
test("search for a document using script query") {
14741545
checkN(4)(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
14751546
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
14761547
for {
@@ -1550,7 +1621,7 @@ object HttpExecutorSpec extends IntegrationSpec {
15501621
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
15511622
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
15521623
),
1553-
test("search for a document using script query") {
1624+
test("search for a document using should with unsatisfying minimumShouldMatch condition") {
15541625
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
15551626
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
15561627
for {

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

Lines changed: 128 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -931,12 +931,12 @@ object ElasticQuery {
931931
* the type-safe field for which query is specified for
932932
* @param values
933933
* a list of terms that should be find in the provided field
934-
* @tparam S
935-
* document for which field query is executed
936934
* @tparam A
937935
* the type of value to be matched. A JSON decoder must be provided in the scope for this type
936+
* @tparam S
937+
* document for which field query is executed
938938
* @return
939-
* an instance of [[zio.elasticsearch.query.TermsQuery]] that represents the term query to be performed.
939+
* an instance of [[zio.elasticsearch.query.TermsQuery]] that represents the terms query to be performed.
940940
*/
941941
final def terms[S, A: ElasticPrimitive](field: Field[S, A], values: A*): TermsQuery[S] =
942942
Terms(field = field.toString, values = Chunk.fromIterable(values), boost = None)
@@ -954,11 +954,135 @@ object ElasticQuery {
954954
* @tparam A
955955
* the type of value to be matched. A JSON decoder must be provided in the scope for this type
956956
* @return
957-
* an instance of [[zio.elasticsearch.query.TermsQuery]] that represents the term query to be performed.
957+
* an instance of [[zio.elasticsearch.query.TermsQuery]] that represents the terms query to be performed.
958958
*/
959959
final def terms[A: ElasticPrimitive](field: String, values: A*): TermsQuery[Any] =
960960
Terms(field = field, values = Chunk.fromIterable(values), boost = None)
961961

962+
/**
963+
* Constructs a type-safe instance of [[zio.elasticsearch.query.TermsSetQuery]] using the specified parameters.
964+
* [[zio.elasticsearch.query.TermsSetQuery]] is used for matching documents that contain the minimum amount of exact
965+
* terms in a provided field. The terms set query is the same as [[zio.elasticsearch.query.TermsQuery]], except you
966+
* can define the number of matching terms required to return a document.
967+
*
968+
* @param field
969+
* the type-safe field for which query is specified for
970+
* @param terms
971+
* a list of terms that should be find in the provided field
972+
* @param minimumShouldMatchField
973+
* the type-safe field representing the number of matching terms required to return a document
974+
* @tparam A
975+
* the type of value to be matched. A JSON decoder must be provided in the scope for this type
976+
* @tparam S
977+
* document for which field query is executed
978+
* @return
979+
* an instance of [[zio.elasticsearch.query.TermsSetQuery]] that represents the terms set query to be performed.
980+
*/
981+
final def termsSet[S, A: ElasticPrimitive](
982+
field: Field[S, A],
983+
minimumShouldMatchField: Field[S, _],
984+
terms: A*
985+
): TermsSetQuery[S] =
986+
TermsSet(
987+
field = field.toString,
988+
terms = Chunk.fromIterable(terms),
989+
boost = None,
990+
minimumShouldMatchField = Some(minimumShouldMatchField.toString),
991+
minimumShouldMatchScript = None
992+
)
993+
994+
/**
995+
* Constructs an instance of [[zio.elasticsearch.query.TermsSetQuery]] using the specified parameters.
996+
* [[zio.elasticsearch.query.TermsSetQuery]] is used for matching documents that contain the minimum amount of exact
997+
* terms in a provided field. The terms set query is the same as [[zio.elasticsearch.query.TermsQuery]], except you
998+
* can define the number of matching terms required to return a document.
999+
*
1000+
* @param field
1001+
* the field for which query is specified for
1002+
* @param terms
1003+
* a list of terms that should be find in the provided field
1004+
* @param minimumShouldMatchField
1005+
* the number of matching terms required to return a document
1006+
* @tparam A
1007+
* the type of value to be matched. A JSON decoder must be provided in the scope for this type
1008+
* @return
1009+
* an instance of [[zio.elasticsearch.query.TermsSetQuery]] that represents the terms set query to be performed.
1010+
*/
1011+
final def termsSet[A: ElasticPrimitive](
1012+
field: String,
1013+
minimumShouldMatchField: String,
1014+
terms: A*
1015+
): TermsSetQuery[Any] =
1016+
TermsSet(
1017+
field = field,
1018+
terms = Chunk.fromIterable(terms),
1019+
boost = None,
1020+
minimumShouldMatchField = Some(minimumShouldMatchField),
1021+
minimumShouldMatchScript = None
1022+
)
1023+
1024+
/**
1025+
* Constructs a type-safe instance of [[zio.elasticsearch.query.TermsSetQuery]] using the specified parameters.
1026+
* [[zio.elasticsearch.query.TermsSetQuery]] is used for matching documents that contain the minimum amount of exact
1027+
* terms in a provided field. The terms set query is the same as [[zio.elasticsearch.query.TermsQuery]], except you
1028+
* can define the number of matching terms required to return a document.
1029+
*
1030+
* @param field
1031+
* the type-safe field for which query is specified for
1032+
* @param terms
1033+
* a list of terms that should be find in the provided field
1034+
* @param minimumShouldMatchScript
1035+
* custom script containing the number of matching terms required to return a document
1036+
* @tparam A
1037+
* the type of value to be matched. A JSON decoder must be provided in the scope for this type
1038+
* @tparam S
1039+
* document for which field query is executed
1040+
* @return
1041+
* an instance of [[zio.elasticsearch.query.TermsSetQuery]] that represents the terms set query to be performed.
1042+
*/
1043+
final def termsSetScript[S, A: ElasticPrimitive](
1044+
field: Field[S, A],
1045+
minimumShouldMatchScript: Script,
1046+
terms: A*
1047+
): TermsSetQuery[S] =
1048+
TermsSet(
1049+
field = field.toString,
1050+
terms = Chunk.fromIterable(terms),
1051+
boost = None,
1052+
minimumShouldMatchField = None,
1053+
minimumShouldMatchScript = Some(minimumShouldMatchScript)
1054+
)
1055+
1056+
/**
1057+
* Constructs an instance of [[zio.elasticsearch.query.TermsSetQuery]] using the specified parameters.
1058+
* [[zio.elasticsearch.query.TermsSetQuery]] is used for matching documents that contain the minimum amount of exact
1059+
* terms in a provided field. The terms set query is the same as [[zio.elasticsearch.query.TermsQuery]], except you
1060+
* can define the number of matching terms required to return a document.
1061+
*
1062+
* @param field
1063+
* the field for which query is specified for
1064+
* @param terms
1065+
* a list of terms that should be find in the provided field
1066+
* @param minimumShouldMatchScript
1067+
* custom script containing the number of matching terms required to return a document
1068+
* @tparam A
1069+
* the type of value to be matched. A JSON decoder must be provided in the scope for this type
1070+
* @return
1071+
* an instance of [[zio.elasticsearch.query.TermsSetQuery]] that represents the terms set query to be performed.
1072+
*/
1073+
final def termsSetScript[A: ElasticPrimitive](
1074+
field: String,
1075+
minimumShouldMatchScript: Script,
1076+
terms: A*
1077+
): TermsSetQuery[Any] =
1078+
TermsSet(
1079+
field = field,
1080+
terms = Chunk.fromIterable(terms),
1081+
boost = None,
1082+
minimumShouldMatchField = None,
1083+
minimumShouldMatchScript = Some(minimumShouldMatchScript)
1084+
)
1085+
9621086
/**
9631087
* Constructs a type-safe instance of [[zio.elasticsearch.query.WildcardQuery]] using the specified parameters.
9641088
* [[zio.elasticsearch.query.WildcardQuery]] is used for matching documents containing a value that matches a provided

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

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -353,13 +353,13 @@ private[elasticsearch] final case class FunctionScore[S](
353353
sealed trait FuzzyQuery[S] extends ElasticQuery[S] {
354354

355355
/**
356-
* Sets the `fuzziness` parameter for this [[zio.elasticsearch.query.ElasticQuery]]. The `fuzziness` value refers to
357-
* the ability to find results that are similar to, but not exactly the same as, the search term or query.
356+
* Sets the `fuzziness` parameter for the [[zio.elasticsearch.query.FuzzyQuery]]. The `fuzziness` value refers to the
357+
* ability to find results that are similar to, but not exactly the same as, the search term or query.
358358
*
359359
* @param value
360360
* the text value to represent the 'fuzziness' field
361361
* @return
362-
* an instance of the [[zio.elasticsearch.query.ElasticQuery]] enriched with the `fuzziness` parameter.
362+
* an instance of the [[zio.elasticsearch.query.FuzzyQuery]] enriched with the `fuzziness` parameter.
363363
*/
364364
def fuzziness(value: String): FuzzyQuery[S]
365365

@@ -1073,6 +1073,28 @@ private[elasticsearch] final case class Terms[S, A: ElasticPrimitive](
10731073
}
10741074
}
10751075

1076+
sealed trait TermsSetQuery[S] extends ElasticQuery[S] with HasBoost[TermsSetQuery[S]]
1077+
1078+
private[elasticsearch] final case class TermsSet[S, A: ElasticPrimitive](
1079+
field: String,
1080+
terms: Chunk[A],
1081+
boost: Option[Double],
1082+
minimumShouldMatchField: Option[String],
1083+
minimumShouldMatchScript: Option[zio.elasticsearch.script.Script]
1084+
) extends TermsSetQuery[S] { self =>
1085+
1086+
def boost(value: Double): TermsSetQuery[S] =
1087+
self.copy(boost = Some(value))
1088+
1089+
private[elasticsearch] def toJson(fieldPath: Option[String]): Json = {
1090+
val termsSetFields =
1091+
Some("terms" -> Arr(terms.map(_.toJson))) ++ minimumShouldMatchField.map(
1092+
"minimum_should_match_field" -> _.toJson
1093+
) ++ minimumShouldMatchScript.map("minimum_should_match_script" -> _.toJson) ++ boost.map("boost" -> _.toJson)
1094+
Obj("terms_set" -> Obj(fieldPath.foldRight(field)(_ + "." + _) -> Obj(Chunk.fromIterable(termsSetFields))))
1095+
}
1096+
}
1097+
10761098
sealed trait WildcardQuery[S]
10771099
extends ElasticQuery[S]
10781100
with HasBoost[WildcardQuery[S]]

0 commit comments

Comments
 (0)