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

(dsl): Support 'matchPhrase' query #172

Merged
merged 9 commits into from
Apr 23, 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
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)
Copy link
Collaborator

@arnoldlacko arnoldlacko Apr 24, 2023

Choose a reason for hiding this comment

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

Does this result in a structure like:

{
   errors: {
      body: [
         "This is an error"
      ]
   }
}

?
Can we simplify that and also avoid naming a class with Data?


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))
Copy link
Collaborator

Choose a reason for hiding this comment

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

Why do we need to delete everything if we're recreating the index between each test?

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
mvelimir marked this conversation as resolved.
Show resolved Hide resolved
* @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