Skip to content

Commit 264dd05

Browse files
(dsl): Support simpleQueryString query (#643)
1 parent 574d724 commit 264dd05

File tree

7 files changed

+387
-37
lines changed

7 files changed

+387
-37
lines changed
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
The `SimpleQueryString` query provides a simple query syntax for performing searches across multiple fields.
2+
3+
To use the `SimpleQueryString` query, import the following:
4+
```scala
5+
import zio.elasticsearch.query.SimpleQueryStringQuery
6+
import zio.elasticsearch.ElasticQuery._
7+
```
8+
9+
You can create a `SimpleQueryString` query without specifying `fields` using the `simpleQueryString` method:
10+
```scala
11+
val query: SimpleQueryStringQuery[Any] = simpleQueryString(query = "name")
12+
```
13+
14+
If you want to specify which fields should be searched, you can use the `fields` method:
15+
```scala
16+
val query: SimpleQueryStringQuery[Document] =
17+
simpleQueryString(query = "name").fields("stringField1", "stringField2")
18+
```
19+
20+
To define `fields` in a type-safe manner, use the overloaded `fields` method with `field` definitions from your document:
21+
```scala
22+
val query: SimpleQueryStringQuery[Document] =
23+
simpleQueryString(query = "name").fields(Document.stringField1, Document.stringField2)
24+
```
25+
26+
Alternatively, you can pass a Chunk of `fields`:
27+
```scala
28+
val query: SimpleQueryStringQuery[Document] =
29+
simpleQueryString(query = "name").fields(Chunk(Document.stringField1, Document.stringField2))
30+
```
31+
32+
If you want to define the `minimum_should_match` parameter, use the `minimumShouldMatch` method:
33+
```scala
34+
val query: SimpleQueryStringQuery[Any] =
35+
simpleQueryString(query = "name").minimumShouldMatch(2)
36+
```
37+
38+
You can also construct the query manually with all parameters:
39+
```scala
40+
val query: SimpleQueryStringQuery[Document] =
41+
SimpleQueryString(
42+
query = "name",
43+
fields = Chunk("stringField"),
44+
minimumShouldMatch = Some(2)
45+
)
46+
```

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

Lines changed: 62 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1064,7 +1064,7 @@ object HttpExecutorSpec extends IntegrationSpec {
10641064
_ <- Executor.execute(ElasticRequest.bulk(req1, req2, req3).refreshTrue)
10651065
query = ElasticQuery.kNN(TestDocument.vectorField, 2, 3, Chunk(-5.0, 9.0, -12.0))
10661066
res <- Executor.execute(ElasticRequest.knnSearch(firstSearchIndex, query)).documentAs[TestDocument]
1067-
} yield (assert(res)(equalTo(Chunk(firstDocumentUpdated, thirdDocumentUpdated))))
1067+
} yield assert(res)(equalTo(Chunk(firstDocumentUpdated, thirdDocumentUpdated)))
10681068
}
10691069
} @@ around(
10701070
Executor.execute(
@@ -1092,7 +1092,7 @@ object HttpExecutorSpec extends IntegrationSpec {
10921092
res <- Executor
10931093
.execute(ElasticRequest.knnSearch(firstSearchIndex, query).filter(filter))
10941094
.documentAs[TestDocument]
1095-
} yield (assert(res)(equalTo(Chunk(firstDocumentUpdated, secondDocumentUpdated))))
1095+
} yield assert(res)(equalTo(Chunk(firstDocumentUpdated, secondDocumentUpdated)))
10961096
}
10971097
} @@ around(
10981098
Executor.execute(
@@ -1136,7 +1136,7 @@ object HttpExecutorSpec extends IntegrationSpec {
11361136
)
11371137
)
11381138
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1139-
} yield (assert(res)(equalTo(Chunk(secondDocumentUpdated, firstDocumentUpdated))))
1139+
} yield assert(res)(equalTo(Chunk(secondDocumentUpdated, firstDocumentUpdated)))
11401140
}
11411141
} @@ around(
11421142
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
@@ -1162,7 +1162,7 @@ object HttpExecutorSpec extends IntegrationSpec {
11621162
)
11631163
).boost(2.1)
11641164
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1165-
} yield (assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument)))
1165+
} yield assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument))
11661166
}
11671167
} @@ around(
11681168
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
@@ -1407,11 +1407,63 @@ object HttpExecutorSpec extends IntegrationSpec {
14071407
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
14081408
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
14091409
),
1410+
test("search for a document using a simple query string query") {
1411+
checkOnce(genDocumentId, genTestDocument, genMultiWordString(), genDocumentId, genTestDocument) {
1412+
(firstDocumentId, firstDocument, multiWordString, secondDocumentId, secondDocument) =>
1413+
val firstDoc = firstDocument.copy(stringField = multiWordString)
1414+
1415+
for {
1416+
_ <- Executor.execute(ElasticRequest.upsert(firstSearchIndex, firstDocumentId, firstDoc))
1417+
_ <- Executor.execute(
1418+
ElasticRequest.upsert(firstSearchIndex, secondDocumentId, secondDocument).refreshTrue
1419+
)
1420+
searchTerm = multiWordString.split("\\s+").head
1421+
query = simpleQueryString(searchTerm).fields(Chunk(TestDocument.stringField))
1422+
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1423+
} yield assert(res)(Assertion.contains(firstDoc))
1424+
}
1425+
} @@ around(
1426+
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
1427+
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
1428+
),
1429+
test("search for a document using a simple query string query with empty fields") {
1430+
checkOnce(genDocumentId, genTestDocument, genMultiWordString(), genDocumentId, genTestDocument) {
1431+
(firstDocumentId, firstDocument, multiWordString, secondDocumentId, secondDocument) =>
1432+
val firstDoc = firstDocument.copy(stringField = multiWordString)
1433+
1434+
for {
1435+
_ <- Executor.execute(ElasticRequest.upsert(firstSearchIndex, firstDocumentId, firstDoc))
1436+
_ <- Executor.execute(
1437+
ElasticRequest.upsert(firstSearchIndex, secondDocumentId, secondDocument).refreshTrue
1438+
)
1439+
searchTerm = multiWordString.split("\\s+").head
1440+
query = simpleQueryString(searchTerm)
1441+
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1442+
} yield assert(res)(Assertion.contains(firstDoc))
1443+
}
1444+
} @@ around(
1445+
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
1446+
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
1447+
),
1448+
test("search for a document using a simple query string query with non-existent field") {
1449+
checkOnce(genDocumentId, genTestDocument, genMultiWordString()) { (docId, doc, multiWordString) =>
1450+
val docWithMultiWord = doc.copy(stringField = multiWordString)
1451+
1452+
for {
1453+
_ <- Executor.execute(ElasticRequest.upsert(firstSearchIndex, docId, docWithMultiWord).refreshTrue)
1454+
searchTerm = multiWordString.split("\\s+").head
1455+
query = simpleQueryString(searchTerm).fields("nonExistentField")
1456+
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1457+
} yield assert(res)(Assertion.isEmpty)
1458+
}
1459+
} @@ around(
1460+
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
1461+
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
1462+
),
14101463
test("search for a document which contains a term using a wildcard query") {
14111464
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
14121465
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
14131466
for {
1414-
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
14151467
_ <- Executor.execute(
14161468
ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocument)
14171469
)
@@ -1437,7 +1489,6 @@ object HttpExecutorSpec extends IntegrationSpec {
14371489
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
14381490
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
14391491
for {
1440-
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
14411492
_ <- Executor.execute(
14421493
ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocument)
14431494
)
@@ -1533,7 +1584,7 @@ object HttpExecutorSpec extends IntegrationSpec {
15331584
)
15341585
query = matchBooleanPrefix(TestDocument.stringField, "this is test bo")
15351586
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1536-
} yield (assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument)))
1587+
} yield assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument))
15371588
}
15381589
} @@ around(
15391590
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
@@ -1619,7 +1670,7 @@ object HttpExecutorSpec extends IntegrationSpec {
16191670
value = s"${firstDocument.stringField} te"
16201671
)
16211672
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1622-
} yield (assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument)))
1673+
} yield assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument))
16231674
}
16241675
} @@ around(
16251676
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
@@ -1642,7 +1693,7 @@ object HttpExecutorSpec extends IntegrationSpec {
16421693
query =
16431694
multiMatch(value = "test").fields(TestDocument.stringField).matchingType(BestFields)
16441695
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
1645-
} yield (assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument)))
1696+
} yield assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument))
16461697
}
16471698
} @@ around(
16481699
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
@@ -1878,9 +1929,9 @@ object HttpExecutorSpec extends IntegrationSpec {
18781929
res <- Executor
18791930
.execute(ElasticRequest.search(firstSearchIndex, query))
18801931
.documentAs[TestDocument]
1881-
} yield (assert(res)(Assertion.contains(firstDocument)) && assert(res)(
1932+
} yield assert(res)(Assertion.contains(firstDocument)) && assert(res)(
18821933
!Assertion.contains(secondDocument)
1883-
))
1934+
)
18841935
}
18851936
} @@ around(
18861937
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,12 @@ trait IntegrationSpec extends ZIOSpecDefault {
7575
longitude <- Gen.bigDecimal(10, 90).map(_.setScale(2, BigDecimal.RoundingMode.HALF_UP).toDouble)
7676
} yield GeoPoint(latitude, longitude)
7777

78+
def genMultiWordString(minWords: Int = 2, maxWords: Int = 5): Gen[Any, String] =
79+
for {
80+
wordCount <- Gen.int(minWords, maxWords)
81+
words <- Gen.listOfN(wordCount)(Gen.stringBounded(5, 10)(Gen.alphaChar))
82+
} yield words.mkString(" ")
83+
7884
def genTestDocument: Gen[Any, TestDocument] = for {
7985
stringField <- Gen.stringBounded(5, 10)(Gen.alphaChar)
8086
dateField <- Gen.localDate(LocalDate.parse("2010-12-02"), LocalDate.parse("2022-12-05"))

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1007,6 +1007,44 @@ object ElasticQuery {
10071007
minimumShouldMatch = None
10081008
)
10091009

1010+
/**
1011+
* Constructs a type-safe instance of [[zio.elasticsearch.query.SimpleQueryStringQuery]] using the specified
1012+
* parameters. [[zio.elasticsearch.query.SimpleQueryStringQuery]] supports query strings with simple syntax for
1013+
* searching multiple fields.
1014+
*
1015+
* @param fields
1016+
* the type-safe fields to be searched
1017+
* @param query
1018+
* the query string to search for
1019+
* @tparam S
1020+
* the document type on which the query is executed
1021+
* @return
1022+
* an instance of [[zio.elasticsearch.query.SimpleQueryStringQuery]] that represents the query to be performed.
1023+
*/
1024+
final def simpleQueryString[S: Schema](query: String, fields: Field[S, _]*): SimpleQueryStringQuery[S] =
1025+
SimpleQueryString[S](
1026+
query = query,
1027+
fields = Chunk.fromIterable(fields.map(_.toString)),
1028+
minimumShouldMatch = None
1029+
)
1030+
1031+
/**
1032+
* Constructs an instance of [[zio.elasticsearch.query.SimpleQueryStringQuery]] using the specified parameters.
1033+
* [[zio.elasticsearch.query.SimpleQueryStringQuery]] supports query strings with simple syntax for searching multiple
1034+
* fields.
1035+
*
1036+
* @param query
1037+
* the query string to search for
1038+
* @return
1039+
* an instance of [[zio.elasticsearch.query.SimpleQueryStringQuery]] that represents the query to be performed.
1040+
*/
1041+
final def simpleQueryString(query: String): SimpleQueryStringQuery[Any] =
1042+
SimpleQueryString(
1043+
query = query,
1044+
fields = Chunk.empty,
1045+
minimumShouldMatch = None
1046+
)
1047+
10101048
/**
10111049
* Constructs a type-safe instance of [[zio.elasticsearch.query.WildcardQuery]] using the specified parameters.
10121050
* [[zio.elasticsearch.query.WildcardQuery]] is used for matching documents containing a value that starts with the

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

Lines changed: 44 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -852,30 +852,9 @@ private[elasticsearch] final case class MatchPhrasePrefix[S](field: String, valu
852852
sealed trait MultiMatchQuery[S]
853853
extends ElasticQuery[S]
854854
with HasBoost[MultiMatchQuery[S]]
855+
with HasFields[MultiMatchQuery, S]
855856
with HasMinimumShouldMatch[MultiMatchQuery[S]] {
856857

857-
/**
858-
* Sets the type-safe `fields` parameter for this [[zio.elasticsearch.query.ElasticQuery]]. The `fields` parameter is
859-
* array of type-safe fields that will be searched.
860-
*
861-
* @param fields
862-
* a array of type-safe fields to set `fields` parameter to
863-
* @return
864-
* an instance of the [[zio.elasticsearch.query.ElasticQuery]] enriched with the type-safe `fields` parameter.
865-
*/
866-
def fields[S1 <: S: Schema](field: Field[S1, String], fields: Field[S1, String]*): MultiMatchQuery[S1]
867-
868-
/**
869-
* Sets the `fields` parameter for this [[zio.elasticsearch.query.ElasticQuery]]. The `fields` parameter is array of
870-
* fields that will be searched.
871-
*
872-
* @param fields
873-
* a array of fields to set `fields` parameter to
874-
* @return
875-
* an instance of the [[zio.elasticsearch.query.ElasticQuery]] enriched with the `fields` parameter.
876-
*/
877-
def fields(field: String, fields: String*): MultiMatchQuery[S]
878-
879858
/**
880859
* Sets the `type` parameter for this [[zio.elasticsearch.query.ElasticQuery]]. The `type` parameter decides the way
881860
* [[zio.elasticsearch.query.ElasticQuery]] is executed internally.
@@ -911,11 +890,14 @@ private[elasticsearch] final case class MultiMatch[S](
911890
def boost(boost: Double): MultiMatchQuery[S] =
912891
self.copy(boost = Some(boost))
913892

914-
def fields[S1 <: S: Schema](field: Field[S1, String], fields: Field[S1, String]*): MultiMatchQuery[S1] =
915-
self.copy(fields = Chunk.fromIterable((field +: fields).map(_.toString)))
916-
917893
def fields(field: String, fields: String*): MultiMatchQuery[S] =
918-
self.copy(fields = Chunk.fromIterable(field +: fields))
894+
copy(fields = Chunk.fromIterable(field +: fields))
895+
896+
def fields[S1 <: S: Schema](fields: Chunk[Field[S1, _]]): MultiMatchQuery[S1] =
897+
copy(fields = fields.map(_.toString))
898+
899+
def fields[S1 <: S: Schema](field: Field[S1, _], fields: Field[S1, _]*): MultiMatchQuery[S1] =
900+
self.copy(fields = Chunk.fromIterable((field +: fields).map(_.toString)))
919901

920902
def matchingType(matchingType: MultiMatchType): MultiMatchQuery[S] =
921903
self.copy(matchingType = Some(matchingType))
@@ -1186,6 +1168,42 @@ private[elasticsearch] final case class Script(script: zio.elasticsearch.script.
11861168
Obj("script" -> Obj(("script" -> script.toJson) +: Chunk.fromIterable(boost.map("boost" -> _.toJson))))
11871169
}
11881170

1171+
sealed trait SimpleQueryStringQuery[S]
1172+
extends ElasticQuery[S]
1173+
with HasFields[SimpleQueryStringQuery, S]
1174+
with HasMinimumShouldMatch[SimpleQueryStringQuery[S]]
1175+
1176+
private[elasticsearch] final case class SimpleQueryString[S](
1177+
query: String,
1178+
fields: Chunk[String],
1179+
minimumShouldMatch: Option[Int]
1180+
) extends SimpleQueryStringQuery[S] { self =>
1181+
1182+
def fields(field: String, fields: String*): SimpleQueryStringQuery[S] =
1183+
copy(fields = Chunk.fromIterable(field +: fields))
1184+
1185+
def fields[S1 <: S: Schema](fields: Chunk[Field[S1, _]]): SimpleQueryStringQuery[S1] =
1186+
copy(fields = fields.map(_.toString))
1187+
1188+
def fields[S1 <: S: Schema](field: Field[S1, _], fields: Field[S1, _]*): SimpleQueryStringQuery[S1] =
1189+
self.copy(fields = Chunk.fromIterable((field +: fields).map(_.toString)))
1190+
1191+
def minimumShouldMatch(value: Int): SimpleQueryString[S] =
1192+
copy(minimumShouldMatch = Some(value))
1193+
1194+
private[elasticsearch] def toJson(fieldPath: Option[String]): Json = {
1195+
val fieldsJson = if (fields.nonEmpty) Some("fields" -> Arr(fields.map(_.toJson))) else None
1196+
1197+
val params = Chunk(
1198+
Some("query" -> query.toJson),
1199+
fieldsJson,
1200+
minimumShouldMatch.map("minimum_should_match" -> _.toJson)
1201+
).flatten
1202+
1203+
Obj("simple_query_string" -> Obj(params))
1204+
}
1205+
}
1206+
11891207
sealed trait TermQuery[S] extends ElasticQuery[S] with HasBoost[TermQuery[S]] with HasCaseInsensitive[TermQuery[S]]
11901208

11911209
private[elasticsearch] final case class Term[S, A: ElasticPrimitive](

0 commit comments

Comments
 (0)