diff --git a/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala index 76d12eaf9..04b57c93d 100644 --- a/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala +++ b/modules/library/src/it/scala/zio/elasticsearch/HttpExecutorSpec.scala @@ -31,9 +31,9 @@ import zio.elasticsearch.result.{Item, UpdateByQueryResult} import zio.elasticsearch.script.Script import zio.json.ast.Json.{Arr, Str} import zio.stream.{Sink, ZSink} -import zio.test.Assertion._ -import zio.test.TestAspect._ import zio.test._ +import zio.test.TestAspect._ +import zio.test.Assertion._ import java.time.LocalDate import scala.util.Random @@ -634,7 +634,7 @@ object HttpExecutorSpec extends IntegrationSpec { nested(path = TestDocument.subDocumentList, query = matchAll) res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument] - } yield assert(res)(Assertion.hasSameElements(List(firstDocument, secondDocument))) + } yield assert(res)(hasSameElements(List(firstDocument, secondDocument))) } } @@ around( Executor.execute( @@ -644,6 +644,58 @@ object HttpExecutorSpec extends IntegrationSpec { ) ), Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie + ), + test("search for a document using should with satisfying minimumShouldMatch condition") { + checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) { + (firstDocumentId, firstDocument, secondDocumentId, secondDocument) => + for { + _ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll)) + _ <- + Executor.execute( + ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocument) + ) + _ <- Executor.execute( + ElasticRequest + .upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocument) + .refreshTrue + ) + query = should( + matches(TestDocument.stringField, firstDocument.stringField), + matches(TestDocument.intField, firstDocument.intField), + matches(TestDocument.doubleField, firstDocument.doubleField + 1) + ).minimumShouldMatch(2) + res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument] + } yield assert(res)(Assertion.contains(firstDocument)) + } + } @@ around( + Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), + Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie + ), + test("search for a document using should without satisfying minimumShouldMatch condition") { + checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) { + (firstDocumentId, firstDocument, secondDocumentId, secondDocument) => + for { + _ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll)) + _ <- + Executor.execute( + ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, firstDocument) + ) + _ <- Executor.execute( + ElasticRequest + .upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocument) + .refreshTrue + ) + query = should( + matches(TestDocument.stringField, firstDocument.stringField), + matches(TestDocument.intField, firstDocument.intField + 1), + matches(TestDocument.doubleField, firstDocument.doubleField + 1) + ).minimumShouldMatch(2) + res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument] + } yield assert(res)(isEmpty) + } + } @@ around( + Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), + Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie ) ) @@ shrinks(0), suite("searching for documents with inner hits")( @@ -668,7 +720,7 @@ object HttpExecutorSpec extends IntegrationSpec { res = items.map(_.innerHitAs[TestSubDocument]("subDocumentList")).collect { case Right(value) => value } } yield assert(res)( - Assertion.hasSameElements(List(firstDocument.subDocumentList, secondDocument.subDocumentList)) + hasSameElements(List(firstDocument.subDocumentList, secondDocument.subDocumentList)) ) } } @@ around( diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala index d4672c695..e4ee7169f 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala @@ -38,12 +38,7 @@ object ElasticQuery { * an instance of [[zio.elasticsearch.query.WildcardQuery]] that represents the wildcard query to be performed. */ final def contains[S](field: Field[S, _], value: String): WildcardQuery[S] = - Wildcard( - field = field.toString, - value = s"*$value*", - boost = None, - caseInsensitive = None - ) + Wildcard(field = field.toString, value = s"*$value*", boost = None, caseInsensitive = None) /** * Constructs an instance of [[zio.elasticsearch.query.WildcardQuery]] using the specified parameters. @@ -99,7 +94,7 @@ object ElasticQuery { * satisfy the criteria. */ final def filter[S: Schema](queries: ElasticQuery[S]*): BoolQuery[S] = - Bool[S](filter = queries.toList, must = Nil, mustNot = Nil, should = Nil, boost = None) + Bool[S](filter = queries.toList, must = Nil, mustNot = Nil, should = Nil, boost = None, minimumShouldMatch = None) /** * Constructs an instance of [[zio.elasticsearch.query.BoolQuery]] with queries that must satisfy the criteria using @@ -112,7 +107,7 @@ object ElasticQuery { * satisfy the criteria. */ final def filter(queries: ElasticQuery[Any]*): BoolQuery[Any] = - Bool[Any](filter = queries.toList, must = Nil, mustNot = Nil, should = Nil, boost = None) + Bool[Any](filter = queries.toList, must = Nil, mustNot = Nil, should = Nil, boost = None, minimumShouldMatch = None) /** * Constructs an instance of [[zio.elasticsearch.query.MatchAllQuery]] used for matching all documents. @@ -202,7 +197,7 @@ object ElasticQuery { * satisfy the criteria. */ final def must[S: Schema](queries: ElasticQuery[S]*): BoolQuery[S] = - Bool[S](filter = Nil, must = queries.toList, mustNot = Nil, should = Nil, boost = None) + Bool[S](filter = Nil, must = queries.toList, mustNot = Nil, should = Nil, boost = None, minimumShouldMatch = None) /** * Constructs an instance of [[zio.elasticsearch.query.BoolQuery]] with queries that must satisfy the criteria using @@ -215,7 +210,7 @@ object ElasticQuery { * satisfy the criteria. */ final def must(queries: ElasticQuery[Any]*): BoolQuery[Any] = - Bool[Any](filter = Nil, must = queries.toList, mustNot = Nil, should = Nil, boost = None) + Bool[Any](filter = Nil, must = queries.toList, mustNot = Nil, should = Nil, boost = None, minimumShouldMatch = None) /** * Constructs an instance of [[zio.elasticsearch.query.BoolQuery]] with queries that must not satisfy the criteria @@ -230,7 +225,7 @@ object ElasticQuery { * satisfy the criteria. */ final def mustNot[S: Schema](queries: ElasticQuery[S]*): BoolQuery[S] = - Bool[S](filter = Nil, must = Nil, mustNot = queries.toList, should = Nil, boost = None) + Bool[S](filter = Nil, must = Nil, mustNot = queries.toList, should = Nil, boost = None, minimumShouldMatch = None) /** * Constructs an instance of [[zio.elasticsearch.query.BoolQuery]] with queries that must not satisfy the criteria @@ -243,7 +238,7 @@ object ElasticQuery { * satisfy the criteria. */ final def mustNot(queries: ElasticQuery[Any]*): BoolQuery[Any] = - Bool[Any](filter = Nil, must = Nil, mustNot = queries.toList, should = Nil, boost = None) + Bool[Any](filter = Nil, must = Nil, mustNot = queries.toList, should = Nil, boost = None, minimumShouldMatch = None) /** * Constructs a type-safe instance of [[zio.elasticsearch.query.NestedQuery]] using the specified parameters. @@ -316,7 +311,7 @@ object ElasticQuery { * satisfy the criteria. */ final def should[S: Schema](queries: ElasticQuery[S]*): BoolQuery[S] = - Bool[S](filter = Nil, must = Nil, mustNot = Nil, should = queries.toList, boost = None) + Bool[S](filter = Nil, must = Nil, mustNot = Nil, should = queries.toList, boost = None, minimumShouldMatch = None) /** * Constructs an instance of [[zio.elasticsearch.query.BoolQuery]] with queries that should satisfy the criteria using @@ -329,7 +324,7 @@ object ElasticQuery { * satisfy the criteria. */ final def should(queries: ElasticQuery[Any]*): BoolQuery[Any] = - Bool[Any](filter = Nil, must = Nil, mustNot = Nil, should = queries.toList, boost = None) + Bool[Any](filter = Nil, must = Nil, mustNot = Nil, should = queries.toList, boost = None, minimumShouldMatch = None) /** * Constructs a type-safe instance of [[zio.elasticsearch.query.WildcardQuery]] using the specified parameters. @@ -346,12 +341,7 @@ object ElasticQuery { * an instance of [[zio.elasticsearch.query.WildcardQuery]] that represents the wildcard query to be performed. */ final def startsWith[S](field: Field[S, _], value: String): WildcardQuery[S] = - Wildcard( - field = field.toString, - value = s"$value*", - boost = None, - caseInsensitive = None - ) + Wildcard(field = field.toString, value = s"$value*", boost = None, caseInsensitive = None) /** * Constructs an instance of [[zio.elasticsearch.query.WildcardQuery]] using the specified parameters. @@ -385,12 +375,7 @@ object ElasticQuery { * an instance of [[zio.elasticsearch.query.TermQuery]] that represents the term query to be performed. */ final def term[S, A: ElasticPrimitive](field: Field[S, A], value: A): TermQuery[S] = - Term( - field = field.toString, - value = value, - boost = None, - caseInsensitive = None - ) + Term(field = field.toString, value = value, boost = None, caseInsensitive = None) /** * Constructs a type-safe instance of [[zio.elasticsearch.query.TermQuery]] using the specified parameters. @@ -424,12 +409,7 @@ object ElasticQuery { * an instance of [[zio.elasticsearch.query.WildcardQuery]] that represents the wildcard query to be performed. */ final def wildcard[S](field: Field[S, _], value: String): Wildcard[S] = - Wildcard( - field = field.toString, - value = value, - boost = None, - caseInsensitive = None - ) + Wildcard(field = field.toString, value = value, boost = None, caseInsensitive = None) /** * Constructs an instance of [[zio.elasticsearch.query.WildcardQuery]] using the specified parameters. diff --git a/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala b/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala index 21ee3dbf6..6548ab4ee 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala @@ -24,13 +24,13 @@ import zio.schema.Schema import scala.annotation.unused sealed trait ElasticQuery[-S] { self => - def paramsToJson(fieldPath: Option[String]): Json + private[elasticsearch] def paramsToJson(fieldPath: Option[String]): Json - final def toJson: Obj = + private[elasticsearch] final def toJson: Obj = Obj("query" -> self.paramsToJson(None)) } -sealed trait BoolQuery[S] extends ElasticQuery[S] with HasBoost[BoolQuery[S]] { +sealed trait BoolQuery[S] extends ElasticQuery[S] with HasBoost[BoolQuery[S]] with HasMinimumShouldMatch[BoolQuery[S]] { def filter[S1 <: S: Schema](queries: ElasticQuery[S1]*): BoolQuery[S1] def filter(queries: ElasticQuery[Any]*): BoolQuery[S] @@ -53,7 +53,8 @@ private[elasticsearch] final case class Bool[S]( must: List[ElasticQuery[S]], mustNot: List[ElasticQuery[S]], should: List[ElasticQuery[S]], - boost: Option[Double] + boost: Option[Double], + minimumShouldMatch: Option[Int] ) extends BoolQuery[S] { self => def boost(value: Double): BoolQuery[S] = self.copy(boost = Some(value)) @@ -64,6 +65,9 @@ private[elasticsearch] final case class Bool[S]( def filter(queries: ElasticQuery[Any]*): BoolQuery[S] = self.copy(filter = filter ++ queries) + def minimumShouldMatch(value: Int): BoolQuery[S] = + self.copy(minimumShouldMatch = Some(value)) + def must[S1 <: S: Schema](queries: ElasticQuery[S1]*): BoolQuery[S1] = self.copy(must = must ++ queries) @@ -83,7 +87,8 @@ private[elasticsearch] final case class Bool[S]( if (must.nonEmpty) Some("must" -> Arr(must.map(_.paramsToJson(fieldPath)): _*)) else None, if (mustNot.nonEmpty) Some("must_not" -> Arr(mustNot.map(_.paramsToJson(fieldPath)): _*)) else None, if (should.nonEmpty) Some("should" -> Arr(should.map(_.paramsToJson(fieldPath)): _*)) else None, - boost.map("boost" -> Num(_)) + boost.map("boost" -> Num(_)), + minimumShouldMatch.map("minimum_should_match" -> Num(_)) ).collect { case Some(obj) => obj } Obj("bool" -> Obj(boolFields: _*)) diff --git a/modules/library/src/main/scala/zio/elasticsearch/query/package.scala b/modules/library/src/main/scala/zio/elasticsearch/query/package.scala index 0bc086490..60ea150c3 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/query/package.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/query/package.scala @@ -120,6 +120,21 @@ package object query { def innerHits(innerHits: InnerHits): Q } + private[elasticsearch] trait HasMinimumShouldMatch[Q <: HasMinimumShouldMatch[Q]] { + + /** + * Sets the `minimumShouldMatch` parameter for this [[ElasticQuery]]. The `minimumShouldMatch` value is the number + * of should clauses returned documents must match. If the [[zio.elasticsearch.query.BoolQuery]] includes at least + * one `should` clause and no `must`/`filter` clauses, the default value is 1. Otherwise, the default value is 0. + * + * @param value + * a number to set `minimumShouldMatch` parameter to + * @return + * a new instance of the [[ElasticQuery]] with the `minimumShouldMatch` value set. + */ + def minimumShouldMatch(value: Int): Q + } + private[elasticsearch] trait HasScoreMode[Q <: HasScoreMode[Q]] { /** diff --git a/modules/library/src/test/scala/zio/elasticsearch/QueryDSLSpec.scala b/modules/library/src/test/scala/zio/elasticsearch/QueryDSLSpec.scala index 68b4a1dda..bcd57852f 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/QueryDSLSpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/QueryDSLSpec.scala @@ -71,7 +71,8 @@ object QueryDSLSpec extends ZIOSpecDefault { must = Nil, mustNot = Nil, should = Nil, - boost = None + boost = None, + minimumShouldMatch = None ) ) ) @@ -92,7 +93,8 @@ object QueryDSLSpec extends ZIOSpecDefault { must = Nil, mustNot = Nil, should = Nil, - boost = Some(1.0) + boost = Some(1.0), + minimumShouldMatch = None ) ) ) @@ -114,7 +116,8 @@ object QueryDSLSpec extends ZIOSpecDefault { ), mustNot = Nil, should = Nil, - boost = None + boost = None, + minimumShouldMatch = None ) ) ) @@ -136,7 +139,8 @@ object QueryDSLSpec extends ZIOSpecDefault { Match(field = "customer_gender", value = "MALE", boost = None) ), should = Nil, - boost = None + boost = None, + minimumShouldMatch = None ) ) ) @@ -157,7 +161,8 @@ object QueryDSLSpec extends ZIOSpecDefault { Match(field = "stringField", value = "StringField", boost = None), Match(field = "customer_gender", value = "MALE", boost = None) ), - boost = None + boost = None, + minimumShouldMatch = None ) ) ) @@ -181,7 +186,8 @@ object QueryDSLSpec extends ZIOSpecDefault { must = List(Match(field = "customer_age", value = 23, boost = None)), mustNot = List(Match(field = "intField", value = 17, boost = None)), should = List(Match(field = "customer_id", value = 1, boost = None)), - boost = None + boost = None, + minimumShouldMatch = None ) ) ) @@ -205,7 +211,8 @@ object QueryDSLSpec extends ZIOSpecDefault { ), mustNot = List(Match(field = "intField", value = 17, boost = None)), should = List(Match(field = "doubleField", value = 23.0, boost = None)), - boost = None + boost = None, + minimumShouldMatch = None ) ) ) @@ -229,12 +236,15 @@ object QueryDSLSpec extends ZIOSpecDefault { Match(field = "customer_gender", value = "MALE", boost = None) ), should = List(Match(field = "intField", value = 23, boost = None)), - boost = None + boost = None, + minimumShouldMatch = None ) ) ) }, - test("successfully create `Filter/Must/MustNot/Should` mixed query with Should containing two Match queries") { + test( + "successfully create `Filter/Must/MustNot/Should` mixed query with Should containing two Match queries and `minimumShouldMatch`" + ) { val query = filter(matches(field = TestDocument.stringField, value = "StringField")) .must(matches(field = TestDocument.intField, value = 23)) .mustNot(matches(field = "day_of_month", value = 17)) @@ -242,6 +252,7 @@ object QueryDSLSpec extends ZIOSpecDefault { matches(field = "day_of_week", value = "Monday"), matches(field = "customer_gender", value = "MALE") ) + .minimumShouldMatch(2) assert(query)( equalTo( @@ -253,7 +264,8 @@ object QueryDSLSpec extends ZIOSpecDefault { Match(field = "day_of_week", value = "Monday", boost = None), Match(field = "customer_gender", value = "MALE", boost = None) ), - boost = None + boost = None, + minimumShouldMatch = Some(2) ) ) ) @@ -272,7 +284,8 @@ object QueryDSLSpec extends ZIOSpecDefault { must = List(Match(field = "doubleField", value = 23.0, boost = None)), mustNot = List(Match(field = "intField", value = 17, boost = None)), should = List(Match(field = "stringField", value = "StringField", boost = None)), - boost = Some(1.0) + boost = Some(1.0), + minimumShouldMatch = None ) ) ) @@ -985,6 +998,71 @@ object QueryDSLSpec extends ZIOSpecDefault { assert(query.toJson)(equalTo(expected.toJson)) }, + test( + "properly encode Bool Query with Filter, Must, MustNot and Should containing `Match` leaf query and with both boost and minimumShouldMatch" + ) { + val query = filter(matches(field = "customer_age", value = 23)) + .must(matches(field = "customer_id", value = 1)) + .mustNot(matches(field = "day_of_month", value = 17)) + .should( + matches(field = "day_of_week", value = "Monday"), + matches(field = "day_of_week", value = "Tuesday"), + matches(field = "day_of_week", value = "Wednesday") + ) + .boost(1.0) + .minimumShouldMatch(2) + val expected = + """ + |{ + | "query": { + | "bool": { + | "filter": [ + | { + | "match": { + | "customer_age": 23 + | } + | } + | ], + | "must": [ + | { + | "match": { + | "customer_id": 1 + | } + | } + | ], + | "must_not": [ + | { + | "match": { + | "day_of_month": 17 + | } + | } + | ], + | "should": [ + | { + | "match": { + | "day_of_week": "Monday" + | } + | }, + | { + | "match": { + | "day_of_week": "Tuesday" + | } + | }, + | { + | "match": { + | "day_of_week": "Wednesday" + | } + | } + | ], + | "boost": 1.0, + | "minimum_should_match": 2 + | } + | } + |} + |""".stripMargin + + assert(query.toJson)(equalTo(expected.toJson)) + }, test("properly encode Exists Query") { val query = exists(field = "day_of_week") val expected =