Skip to content

Commit

Permalink
(dsl): Support 'matchPhrase' query (#172)
Browse files Browse the repository at this point in the history
  • Loading branch information
drmarjanovic authored Apr 23, 2023
1 parent a113dc6 commit 93966cb
Show file tree
Hide file tree
Showing 9 changed files with 275 additions and 71 deletions.
6 changes: 6 additions & 0 deletions docs/overview/queries/elastic_query_match_phrase.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
id: elastic_query_match_phrase
title: "Match Phrase Query"
---

TBD
29 changes: 29 additions & 0 deletions modules/example/src/main/scala/example/api/ErrorResponse.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
/*
* Copyright 2022 LambdaWorks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package example.api

import zio.Chunk
import zio.json.{DeriveJsonEncoder, JsonEncoder}

final case class ErrorResponse(errors: ErrorResponseData)

object ErrorResponse {
implicit val encoder: JsonEncoder[ErrorResponse] = DeriveJsonEncoder.gen[ErrorResponse]

def fromReasons(reasons: String*): ErrorResponse =
new ErrorResponse(ErrorResponseData(Chunk.fromIterable(reasons)))
}
26 changes: 26 additions & 0 deletions modules/example/src/main/scala/example/api/ErrorResponseData.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
* Copyright 2022 LambdaWorks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package example.api

import zio.Chunk
import zio.json.{DeriveJsonEncoder, JsonEncoder}

final case class ErrorResponseData(body: Chunk[String])

object ErrorResponseData {
implicit val encoder: JsonEncoder[ErrorResponseData] = DeriveJsonEncoder.gen[ErrorResponseData]
}
19 changes: 0 additions & 19 deletions modules/example/src/main/scala/example/api/package.scala
Original file line number Diff line number Diff line change
Expand Up @@ -16,31 +16,12 @@

package example

import zio.Chunk
import zio.http.Request
import zio.json._

package object api {

final case class ErrorResponseData(body: Chunk[String])

object ErrorResponseData {
implicit val encoder: JsonEncoder[ErrorResponseData] = DeriveJsonEncoder.gen[ErrorResponseData]
}

final case class ErrorResponse(errors: ErrorResponseData)

object ErrorResponse {
implicit val encoder: JsonEncoder[ErrorResponse] = DeriveJsonEncoder.gen[ErrorResponse]

def fromReasons(reasons: String*): ErrorResponse =
new ErrorResponse(ErrorResponseData(Chunk.fromIterable(reasons)))
}

implicit final class RequestOps(private val req: Request) extends AnyVal {
def limit: Int = req.url.queryParams.get("limit").flatMap(_.headOption).flatMap(_.toIntOption).getOrElse(10)

def offset: Int = req.url.queryParams.get("offset").flatMap(_.headOption).flatMap(_.toIntOption).getOrElse(0)
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -592,6 +592,30 @@ object HttpExecutorSpec extends IntegrationSpec {
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) =>
for {
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
document = firstDocument.copy(stringField = s"this is ${firstDocument.stringField} test")
_ <-
Executor.execute(ElasticRequest.upsert[TestDocument](firstSearchIndex, firstDocumentId, document))
_ <- Executor.execute(
ElasticRequest
.upsert[TestDocument](firstSearchIndex, secondDocumentId, secondDocument)
.refreshTrue
)
query = matchPhrase(
field = TestDocument.stringField,
value = firstDocument.stringField
)
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
} yield assert(res)(Assertion.contains(document))
}
} @@ around(
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
),
test("search for a document using nested query") {
checkOnce(genDocumentId, genTestDocument, genDocumentId, genTestDocument) {
(firstDocumentId, firstDocument, secondDocumentId, secondDocument) =>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ object ElasticQuery {

/**
* Constructs a type-safe instance of [[MatchQuery]] using the specified parameters. [[MatchQuery]] is used for
* matching a provided text, number, date or boolean value
* matching a provided text, number, date or boolean value.
*
* @param field
* the [[Field]] object representing the type-safe field for which query is specified for
Expand All @@ -132,11 +132,11 @@ object ElasticQuery {
* an instance of [[MatchQuery]] that represents the match query to be performed.
*/
final def matches[S, A: ElasticPrimitive](field: Field[S, A], value: A): MatchQuery[S] =
Match(field = field.toString, value = value)
Match(field = field.toString, value = value, boost = None)

/**
* Constructs an instance of [[MatchQuery]] using the specified parameters. [[MatchQuery]] is used for matching a
* provided text, number, date or boolean value
* provided text, number, date or boolean value.
*
* @param field
* the field for which query is specified for
Expand All @@ -148,7 +148,37 @@ object ElasticQuery {
* an instance of [[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)
Match(field = field, value = value, boost = None)

/**
* Constructs a type-safe instance of [[MatchPhraseQuery]] using the specified parameters. [[MatchPhraseQuery]]
* analyzes the text and creates a phrase query out of the analyzed text.
*
* @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 S
* document for which field query is executed
* @return
* an instance of [[MatchPhraseQuery]] that represents the match phrase query to be performed.
*/
final def matchPhrase[S](field: Field[S, String], value: String): MatchPhraseQuery[S] =
MatchPhrase(field = field.toString, value = value, boost = None)

/**
* Constructs an instance of [[MatchPhraseQuery]] using the specified parameters. [[MatchPhraseQuery]] analyzes the
* text and creates a phrase query out of the analyzed text.
*
* @param field
* the [[Field]] object representing the type-safe field for which query is specified for
* @param value
* the value to be matched, represented by an instance of type `A`
* @return
* an instance of [[MatchPhraseQuery]] that represents the match phrase query to be performed.
*/
final def matchPhrase(field: String, value: String): MatchPhraseQuery[Any] =
MatchPhrase(field = field, value = value, boost = None)

/**
* Constructs an instance of [[BoolQuery]] with queries that must satisfy the criteria using the specified parameters.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,14 +100,20 @@ sealed trait ExistsQuery[S] extends ElasticQuery[S]

private[elasticsearch] final case class Exists[S](field: String) extends ExistsQuery[S] {
def paramsToJson(fieldPath: Option[String]): Json =
Obj("exists" -> Obj("field" -> (fieldPath.map(_ + ".").getOrElse("") + field).toJson))
Obj("exists" -> Obj("field" -> fieldPath.foldRight(field)(_ + "." + _).toJson))
}

sealed trait MatchQuery[S] extends ElasticQuery[S]
sealed trait MatchQuery[S] extends ElasticQuery[S] with HasBoost[MatchQuery[S]]

private[elasticsearch] final case class Match[S, A: ElasticPrimitive](field: String, value: A) extends MatchQuery[S] {
def paramsToJson(fieldPath: Option[String]): Json =
Obj("match" -> Obj(fieldPath.map(_ + ".").getOrElse("") + field -> value.toJson))
private[elasticsearch] final case class Match[S, A: ElasticPrimitive](field: String, value: A, boost: Option[Double])
extends MatchQuery[S] { self =>
def boost(value: Double): MatchQuery[S] =
self.copy(boost = Some(value))

def paramsToJson(fieldPath: Option[String]): Json = {
val matchFields = Some(fieldPath.foldRight(field)(_ + "." + _) -> value.toJson) ++ boost.map("boost" -> Num(_))
Obj("match" -> Obj(matchFields.toList: _*))
}
}

sealed trait MatchAllQuery extends ElasticQuery[Any] with HasBoost[MatchAllQuery]
Expand All @@ -120,6 +126,20 @@ private[elasticsearch] final case class MatchAll(boost: Option[Double]) extends
Obj("match_all" -> Obj(boost.map("boost" -> Num(_)).toList: _*))
}

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])
extends MatchPhraseQuery[S] { self =>
def boost(value: Double): MatchPhraseQuery[S] =
self.copy(boost = Some(value))

def paramsToJson(fieldPath: Option[String]): Json = {
val matchPhraseFields =
Some(fieldPath.foldRight(field)(_ + "." + _) -> value.toJson) ++ boost.map("boost" -> Num(_))
Obj("match_phrase" -> Obj(matchPhraseFields.toList: _*))
}
}

sealed trait NestedQuery[S]
extends ElasticQuery[S]
with HasIgnoreUnmapped[NestedQuery[S]]
Expand Down Expand Up @@ -240,7 +260,7 @@ private[elasticsearch] final case class Range[S, A, LB <: LowerBound, UB <: Uppe

def paramsToJson(fieldPath: Option[String]): Json = {
val rangeFields = Some(
fieldPath.map(_ + ".").getOrElse("") + field -> Obj(List(lower.toJson, upper.toJson).flatten: _*)
fieldPath.foldRight(field)(_ + "." + _) -> Obj(List(lower.toJson, upper.toJson).flatten: _*)
) ++ boost.map("boost" -> Num(_))
Obj("range" -> Obj(rangeFields.toList: _*))
}
Expand Down Expand Up @@ -274,7 +294,7 @@ private[elasticsearch] final case class Term[S, A: ElasticPrimitive](
val termFields = Some("value" -> value.toJson) ++ boost.map("boost" -> Num(_)) ++ caseInsensitive.map(
"case_insensitive" -> Json.Bool(_)
)
Obj("term" -> Obj(fieldPath.map(_ + ".").getOrElse("") + field -> Obj(termFields.toList: _*)))
Obj("term" -> Obj(fieldPath.foldRight(field)(_ + "." + _) -> Obj(termFields.toList: _*)))
}
}

Expand All @@ -299,6 +319,6 @@ private[elasticsearch] final case class Wildcard[S](
val wildcardFields = Some("value" -> value.toJson) ++ boost.map("boost" -> Num(_)) ++ caseInsensitive.map(
"case_insensitive" -> Json.Bool(_)
)
Obj("wildcard" -> Obj(fieldPath.map(_ + ".").getOrElse("") + field -> Obj(wildcardFields.toList: _*)))
Obj("wildcard" -> Obj(fieldPath.foldRight(field)(_ + "." + _) -> Obj(wildcardFields.toList: _*)))
}
}
Loading

0 comments on commit 93966cb

Please sign in to comment.