diff --git a/docs/overview/queries/elastic_query_match_boolean_prefix.md b/docs/overview/queries/elastic_query_match_boolean_prefix.md new file mode 100644 index 000000000..b5a1290eb --- /dev/null +++ b/docs/overview/queries/elastic_query_match_boolean_prefix.md @@ -0,0 +1,31 @@ +--- +id: elastic_query_match_boolean_prefix +title: "Match Boolean Prefix Query" +--- + +The `MatchBooleanPrefix` query analyzes its input and constructs a `bool` query from the terms. Each term except the last is used in a `term` query. +The last term is used in a `prefix` query. + +In order to use the `MatchBooleanPrefix` query import the following: +```scala +import zio.elasticsearch.query.MatchBooleanPrefixQuery +import zio.elasticsearch.ElasticQuery._ +``` + +You can create a `MatchBooleanPrefix` query using the `matchBooleanPrefix` method this way: +```scala +val query: MatchBooleanPrefixQuery = matchBooleanPrefix(field = "stringField", value = "test") +``` + +You can create a [type-safe](https://lambdaworks.github.io/zio-elasticsearch/overview/overview_zio_prelude_schema) `MatchBooleanPrefix` query using the `matchBooleanPrefix` method this way: +```scala +val query: MatchBooleanPrefixQuery = matchBooleanPrefix(field = Document.stringField, value = "test") +``` + +If you want to change the `minimum_should_match` parameter, you can use the `minimumShouldMatch` method: +```scala +val queryWithMinimumShouldMatch: MatchBooleanPrefixQuery = matchBooleanPrefix(field = Document.stringField, value = "test").minimumShouldMatch(2) +``` + +You can find more information about `MatchBooleanPrefix` query [here](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/query-dsl-match-bool-prefix-query.html). + diff --git a/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala b/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala index fb581911f..acb1fbdf7 100644 --- a/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala +++ b/modules/integration/src/test/scala/zio/elasticsearch/HttpExecutorSpec.scala @@ -929,6 +929,27 @@ object HttpExecutorSpec extends IntegrationSpec { Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie ), + test("search for a document using a match boolean prefix query") { + checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) { + (firstDocumentId, firstDocument, secondDocumentId, secondDocument) => + for { + _ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll)) + document = firstDocument.copy(stringField = "test this is boolean") + _ <- + Executor.execute(ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, document)) + _ <- Executor.execute( + ElasticRequest + .upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocument) + .refreshTrue + ) + query = matchBooleanPrefix(TestDocument.stringField, "this is test bo") + res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument] + } yield (assert(res)(Assertion.contains(document)) && assert(res)(!Assertion.contains(secondDocument))) + } + } @@ around( + Executor.execute(ElasticRequest.createIndex(firstSearchIndex)), + Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie + ), test("search for a document using a match phrase query") { checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) { (firstDocumentId, firstDocument, secondDocumentId, secondDocument) => diff --git a/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala b/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala index f12cde6c6..a5376a691 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala @@ -392,7 +392,7 @@ object ElasticQuery { * @tparam S * document for which field query is executed * @tparam A - * the type of value to be matched. A JSON decoder must be in scope for this type + * the type of value to be matched. A JSON decoder must be provided in the scope for this type * @return * an instance of [[zio.elasticsearch.query.MatchQuery]] that represents the match query to be performed. */ @@ -408,13 +408,53 @@ object ElasticQuery { * @param value * the value to be matched, represented by an instance of type `A` * @tparam A - * the type of value to be matched. A JSON decoder must be in scope for this type + * the type of value to be matched. A JSON decoder must be provided in the scope for this type * @return * an instance of [[zio.elasticsearch.query.MatchQuery]] that represents the match query to be performed. */ final def matches[A: ElasticPrimitive](field: String, value: A): MatchQuery[Any] = Match(field = field, value = value) + /** + * Constructs a type-safe instance of [[zio.elasticsearch.query.MatchBooleanPrefixQuery]] using the specified + * parameters. [[zio.elasticsearch.query.MatchBooleanPrefixQuery]] analyzes its input and constructs a + * [[zio.elasticsearch.query.BoolQuery]] from the terms. Each term except the last is used in a + * [[zio.elasticsearch.query.TermQuery]]. The last term is used in a [[zio.elasticsearch.query.PrefixQuery]] query. + * + * @param field + * the type-safe field for which query is specified for + * @param value + * the value to be matched, represented by an instance of type `A` + * @tparam S + * document for which field query is executed + * @tparam A + * the type of value to be matched. A JSON decoder must be provided in the scope for this type + * @return + * an instance of [[zio.elasticsearch.query.MatchBooleanPrefixQuery]] that represents the match boolean prefix query + * to be performed. + */ + final def matchBooleanPrefix[S, A: ElasticPrimitive](field: Field[S, A], value: A): MatchBooleanPrefixQuery[S] = + MatchBooleanPrefix(field = field.toString, value, minimumShouldMatch = None) + + /** + * Constructs an instance of [[zio.elasticsearch.query.MatchBooleanPrefixQuery]] using the specified parameters. + * [[zio.elasticsearch.query.MatchBooleanPrefixQuery]] analyzes its input and constructs a + * [[zio.elasticsearch.query.BoolQuery]] from the terms. Each term except the last is used in a + * [[zio.elasticsearch.query.TermQuery]]. The last term is used in a [[zio.elasticsearch.query.PrefixQuery]]. + * + * @param field + * the field for which query is specified for + * @param value + * the value to be matched, represented by an instance of type `A` + * @tparam A + * the type of value to be matched. A JSON decoder must be provided in the scope for this type + * @return + * an instance of [[zio.elasticsearch.query.MatchBooleanPrefixQuery]] that represents the match boolean prefix query + * to be performed. + */ + final def matchBooleanPrefix[A: ElasticPrimitive](field: String, value: A): MatchBooleanPrefixQuery[Any] = + MatchBooleanPrefix(field = field, value = value, minimumShouldMatch = None) + /** * Constructs a type-safe instance of [[zio.elasticsearch.query.MatchPhraseQuery]] using the specified parameters. * [[zio.elasticsearch.query.MatchPhraseQuery]] analyzes the text and creates a phrase query out of the analyzed text. @@ -781,7 +821,7 @@ object ElasticQuery { * @param value * text value that will be used for the query * @tparam A - * the type of value to be matched. A JSON decoder must be in scope for this type + * the type of value to be matched. A JSON decoder must be provided in the scope for this type * @tparam S * document for which field query is executed * @return @@ -800,7 +840,7 @@ object ElasticQuery { * @param value * text value that will be used for the query * @tparam A - * the type of value to be matched. A JSON decoder must be in scope for this type + * the type of value to be matched. A JSON decoder must be provided in the scope for this type * @return * an instance of [[zio.elasticsearch.query.TermQuery]] that represents the term query to be performed. */ @@ -820,7 +860,7 @@ object ElasticQuery { * @tparam S * document for which field query is executed * @tparam A - * the type of value to be matched. A JSON decoder must be in scope for this type + * the type of value to be matched. A JSON decoder must be provided in the scope for this type * @return * an instance of [[zio.elasticsearch.query.TermsQuery]] that represents the term query to be performed. */ @@ -838,7 +878,7 @@ object ElasticQuery { * @param values * a list of terms that should be find in the provided field * @tparam A - * the type of value to be matched. A JSON decoder must be in scope for this type + * the type of value to be matched. A JSON decoder must be provided in the scope for this type * @return * an instance of [[zio.elasticsearch.query.TermsQuery]] that represents the term query to be performed. */ 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 76a3e89a4..a9cc34387 100644 --- a/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala +++ b/modules/library/src/main/scala/zio/elasticsearch/query/Queries.scala @@ -567,6 +567,27 @@ private[elasticsearch] final case class MatchAll(boost: Option[Double]) extends Obj("match_all" -> Obj(Chunk.fromIterable(boost.map("boost" -> _.toJson)))) } +sealed trait MatchBooleanPrefixQuery[S] extends ElasticQuery[S] with HasMinimumShouldMatch[MatchBooleanPrefixQuery[S]] + +private[elasticsearch] final case class MatchBooleanPrefix[S, A: ElasticPrimitive]( + field: String, + value: A, + minimumShouldMatch: Option[Int] +) extends MatchBooleanPrefixQuery[S] { self => + + def minimumShouldMatch(value: Int): MatchBooleanPrefixQuery[S] = + self.copy(minimumShouldMatch = Some(value)) + + private[elasticsearch] def toJson(fieldPath: Option[String]): Json = + Obj( + "match_bool_prefix" -> Obj( + fieldPath.foldRight(field)(_ + "." + _) -> minimumShouldMatch.fold(value.toJson)(m => + Obj("query" -> value.toJson) merge Obj("minimum_should_match" -> m.toJson) + ) + ) + ) +} + sealed trait MatchPhraseQuery[S] extends ElasticQuery[S] with HasBoost[MatchPhraseQuery[S]] private[elasticsearch] final case class MatchPhrase[S](field: String, value: String, boost: Option[Double]) diff --git a/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala b/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala index 422571f7f..5b92b2a2e 100644 --- a/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala +++ b/modules/library/src/test/scala/zio/elasticsearch/ElasticQuerySpec.scala @@ -846,6 +846,57 @@ object ElasticQuerySpec extends ZIOSpecDefault { equalTo(MatchAll(boost = Some(3.14))) ) }, + test("matchBooleanPrefix") { + val queryString = matchBooleanPrefix("stringField", "test") + val queryBool = matchBooleanPrefix("booleanField", true) + val queryInt = matchBooleanPrefix("intField", 1) + val queryStringTs = matchBooleanPrefix(TestDocument.stringField, "test") + val queryBoolTs = matchBooleanPrefix(TestDocument.booleanField, true) + val queryIntTs = matchBooleanPrefix(TestDocument.intField, 1) + val queryWithSuffix = matchBooleanPrefix(TestDocument.stringField.raw, "test") + val queryWithMinimumShouldMatch = matchBooleanPrefix(TestDocument.stringField, "test").minimumShouldMatch(3) + + assert(queryString)( + equalTo(MatchBooleanPrefix[Any, String](field = "stringField", value = "test", minimumShouldMatch = None)) + ) && + assert(queryBool)( + equalTo(MatchBooleanPrefix[Any, Boolean](field = "booleanField", value = true, minimumShouldMatch = None)) + ) && + assert(queryInt)( + equalTo(MatchBooleanPrefix[Any, Int](field = "intField", value = 1, minimumShouldMatch = None)) + ) && + assert(queryStringTs)( + equalTo( + MatchBooleanPrefix[TestDocument, String](field = "stringField", value = "test", minimumShouldMatch = None) + ) + ) && + assert(queryBoolTs)( + equalTo( + MatchBooleanPrefix[TestDocument, Boolean](field = "booleanField", value = true, minimumShouldMatch = None) + ) + ) && + assert(queryIntTs)( + equalTo(MatchBooleanPrefix[TestDocument, Int](field = "intField", value = 1, minimumShouldMatch = None)) + ) && + assert(queryWithSuffix)( + equalTo( + MatchBooleanPrefix[TestDocument, String]( + field = "stringField.raw", + value = "test", + minimumShouldMatch = None + ) + ) + ) && + assert(queryWithMinimumShouldMatch)( + equalTo( + MatchBooleanPrefix[TestDocument, String]( + field = "stringField", + value = "test", + minimumShouldMatch = Some(3) + ) + ) + ) + }, test("matches") { val queryString = matches("stringField", "test") val queryBool = matches("booleanField", true) @@ -2415,6 +2466,76 @@ object ElasticQuerySpec extends ZIOSpecDefault { assert(query.toJson(fieldPath = None))(equalTo(expected.toJson)) && assert(queryWithBoost.toJson(fieldPath = None))(equalTo(expectedWithBoost.toJson)) }, + test("matchBooleanPrefix") { + val queryString = matchBooleanPrefix("stringField", "test") + val queryBool = matchBooleanPrefix("booleanField", true) + val queryInt = matchBooleanPrefix("intField", 1) + val queryStringTs = matchBooleanPrefix(TestDocument.stringField, "test") + val queryBoolTs = matchBooleanPrefix(TestDocument.booleanField, true) + val queryIntTs = matchBooleanPrefix(TestDocument.intField, 1) + val queryWithSuffix = matchBooleanPrefix(TestDocument.stringField.raw, "test") + val queryWithMinimumShouldMatch = matchBooleanPrefix(TestDocument.stringField, "test").minimumShouldMatch(3) + + val expectedString = + """ + |{ + | "match_bool_prefix": { + | "stringField": "test" + | } + |} + |""".stripMargin + + val expectedBool = + """ + |{ + | "match_bool_prefix": { + | "booleanField": true + | } + |} + |""".stripMargin + + val expectedInt = + """ + |{ + | "match_bool_prefix": { + | "intField": 1 + | } + |} + |""".stripMargin + + val expectedWithSuffix = + """ + |{ + | "match_bool_prefix": { + | "stringField.raw": "test" + | } + |} + |""".stripMargin + + val expectedWithMinimumShouldMatch = + """ + |{ + | "match_bool_prefix": { + | "stringField": { + | "query": "test", + | "minimum_should_match": 3 + | } + | } + |} + |""".stripMargin + + assert(queryString.toJson(fieldPath = None))(equalTo(expectedString.toJson)) && + assert(queryBool.toJson(fieldPath = None))(equalTo(expectedBool.toJson)) && + assert(queryInt.toJson(fieldPath = None))(equalTo(expectedInt.toJson)) && + assert(queryWithMinimumShouldMatch.toJson(fieldPath = None))( + equalTo(expectedWithMinimumShouldMatch.toJson) + ) && + assert(queryStringTs.toJson(fieldPath = None))(equalTo(expectedString.toJson)) && + assert(queryBoolTs.toJson(fieldPath = None))(equalTo(expectedBool.toJson)) && + assert(queryIntTs.toJson(fieldPath = None))(equalTo(expectedInt.toJson)) && + assert(queryWithSuffix.toJson(fieldPath = None))(equalTo(expectedWithSuffix.toJson)) && + assert(queryWithMinimumShouldMatch.toJson(fieldPath = None))(equalTo(expectedWithMinimumShouldMatch.toJson)) + }, test("matches") { val query = matches("testField", true) val queryTsInt = matches(TestDocument.intField, 39) diff --git a/website/sidebars.js b/website/sidebars.js index 6f2f58d93..cabc61ad0 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -22,6 +22,7 @@ module.exports = { 'overview/queries/elastic_query_has_parent', 'overview/queries/elastic_query_match', 'overview/queries/elastic_query_match_all', + 'overview/queries/elastic_query_match_boolean_prefix', 'overview/queries/elastic_query_match_phrase', 'overview/queries/elastic_query_match_phrase_prefix', 'overview/queries/elastic_query_nested',