Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support minimumShouldMatch on BoolQuery #184

Merged
merged 6 commits into from
Apr 25, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand All @@ -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")(
Expand All @@ -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(
Expand Down
44 changes: 12 additions & 32 deletions modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand All @@ -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))
Expand All @@ -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))

Comment on lines +68 to +70
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Move this after mustNot?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorted alphabetically.

def must[S1 <: S: Schema](queries: ElasticQuery[S1]*): BoolQuery[S1] =
self.copy(must = must ++ queries)

Expand All @@ -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: _*))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this applies only on BoolQuery, maybe write it here instead of ElasticQuery.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if there are any other places to be supported.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least match bool prefix query supports this. So, let's have it in a separate trait.

*/
def minimumShouldMatch(value: Int): Q
}

private[elasticsearch] trait HasScoreMode[Q <: HasScoreMode[Q]] {

/**
Expand Down
Loading