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 Wildcard query #48

Merged
merged 6 commits into from
Jan 30, 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 @@ -204,6 +204,73 @@ object HttpExecutorSpec extends IntegrationSpec {
}
} @@ before(ElasticRequest.createIndex(secondSearchIndex, None).execute) @@ after(
ElasticRequest.deleteIndex(secondSearchIndex).execute.orDie
),
test("search for a document which contains a term using a wildcard query") {
checkOnce(genDocumentId, genCustomer, genDocumentId, genCustomer) {
(firstDocumentId, firstCustomer, secondDocumentId, secondCustomer) =>
val result =
for {
_ <- ElasticRequest.deleteByQuery(firstSearchIndex, matchAll()).execute
_ <-
ElasticRequest.upsert[CustomerDocument](firstSearchIndex, firstDocumentId, firstCustomer).execute
_ <-
ElasticRequest
.upsert[CustomerDocument](firstSearchIndex, secondDocumentId, secondCustomer)
.refreshTrue
.execute
query = ElasticQuery.contains("name.keyword", firstCustomer.name.take(3))
res <- ElasticRequest.search[CustomerDocument](firstSearchIndex, query).execute
} yield res

assertZIO(result)(Assertion.contains(firstCustomer))
}
} @@ before(ElasticRequest.createIndex(firstSearchIndex, None).execute) @@ after(
ElasticRequest.deleteIndex(firstSearchIndex).execute.orDie
),
test("search for a document which starts with a term using a wildcard query") {
checkOnce(genDocumentId, genCustomer, genDocumentId, genCustomer) {
(firstDocumentId, firstCustomer, secondDocumentId, secondCustomer) =>
val result =
for {
_ <- ElasticRequest.deleteByQuery(firstSearchIndex, matchAll()).execute
_ <-
ElasticRequest.upsert[CustomerDocument](firstSearchIndex, firstDocumentId, firstCustomer).execute
_ <-
ElasticRequest
.upsert[CustomerDocument](firstSearchIndex, secondDocumentId, secondCustomer)
.refreshTrue
.execute
query = ElasticQuery.startsWith("name.keyword", firstCustomer.name.take(3))
res <- ElasticRequest.search[CustomerDocument](firstSearchIndex, query).execute
} yield res

assertZIO(result)(Assertion.contains(firstCustomer))
}
} @@ before(ElasticRequest.createIndex(firstSearchIndex, None).execute) @@ after(
ElasticRequest.deleteIndex(firstSearchIndex).execute.orDie
),
test("search for a document which conforms to a pattern using a wildcard query") {
checkOnce(genDocumentId, genCustomer, genDocumentId, genCustomer) {
(firstDocumentId, firstCustomer, secondDocumentId, secondCustomer) =>
val result =
for {
_ <- ElasticRequest.deleteByQuery(firstSearchIndex, matchAll()).execute
_ <-
ElasticRequest.upsert[CustomerDocument](firstSearchIndex, firstDocumentId, firstCustomer).execute
_ <-
ElasticRequest
.upsert[CustomerDocument](firstSearchIndex, secondDocumentId, secondCustomer)
.refreshTrue
.execute
query =
wildcard("name.keyword", s"${firstCustomer.name.take(2)}*${firstCustomer.name.takeRight(2)}")
res <- ElasticRequest.search[CustomerDocument](firstSearchIndex, query).execute
} yield res

assertZIO(result)(Assertion.contains(firstCustomer))
}
} @@ before(ElasticRequest.createIndex(firstSearchIndex, None).execute) @@ after(
ElasticRequest.deleteIndex(firstSearchIndex).execute.orDie
)
) @@ shrinks(0),
suite("deleting by query")(
Expand Down
9 changes: 7 additions & 2 deletions modules/library/src/main/scala/zio/elasticsearch/Boost.scala
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package zio.elasticsearch

import zio.elasticsearch.ElasticQuery.{ElasticPrimitive, MatchAllQuery, TermQuery}
import zio.elasticsearch.ElasticQueryType.{MatchAll, Term}
import zio.elasticsearch.ElasticQuery.{ElasticPrimitive, MatchAllQuery, TermQuery, WildcardQuery}
import zio.elasticsearch.ElasticQueryType.{MatchAll, Term, Wildcard}

object Boost {

Expand All @@ -20,5 +20,10 @@ object Boost {
query match {
case q: TermQuery[A] => q.copy(boost = Some(value))
}

implicit val wildcardWithBoost: WithBoost[Wildcard] = (query: ElasticQuery[Wildcard], value: Double) =>
query match {
case q: WildcardQuery => q.copy(boost = Some(value))
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
package zio.elasticsearch

import zio.elasticsearch.ElasticQuery.TermQuery
import zio.elasticsearch.ElasticQueryType.Term
import zio.elasticsearch.ElasticQuery.{TermQuery, WildcardQuery}
import zio.elasticsearch.ElasticQueryType.{Term, Wildcard}

object CaseInsensitive {

Expand All @@ -15,5 +15,11 @@ object CaseInsensitive {
query match {
case q: TermQuery[String] => q.copy(caseInsensitive = Some(value))
}

implicit val wildcardWithCaseInsensitiveString: WithCaseInsensitive[Wildcard] =
(query: ElasticQuery[Wildcard], value: Boolean) =>
query match {
case q: WildcardQuery => q.copy(caseInsensitive = Some(value))
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,16 @@ object ElasticQuery {
def toJson(implicit EP: ElasticPrimitive[A]): Json = EP.toJson(value)
}

def boolQuery(): BoolQuery = BoolQuery.empty

def contains(field: String, value: String): ElasticQuery[Wildcard] = WildcardQuery(field, s"*$value*")

def exists(field: Field[_, _]): ElasticQuery[Exists] = ExistsQuery(field.toString)

def exists(field: String): ElasticQuery[Exists] = ExistsQuery(field)

def matchAll(): ElasticQuery[MatchAll] = MatchAllQuery()

def matches[A: ElasticPrimitive](
field: Field[_, A],
multiField: Option[String] = None,
Expand All @@ -64,14 +74,6 @@ object ElasticQuery {
def matches[A: ElasticPrimitive](field: String, value: A): ElasticQuery[Match] =
MatchQuery(field, value)

def boolQuery(): BoolQuery = BoolQuery.empty

def exists(field: Field[_, _]): ElasticQuery[Exists] = ExistsQuery(field.toString)

def exists(field: String): ElasticQuery[Exists] = ExistsQuery(field)

def matchAll(): ElasticQuery[MatchAll] = MatchAllQuery()

def range[A](
field: Field[_, A],
multiField: Option[String] = None
Expand All @@ -80,6 +82,8 @@ object ElasticQuery {

def range(field: String): RangeQuery[Any, Unbounded.type, Unbounded.type] = RangeQuery.empty[Any](field)

def startsWith(field: String, value: String): ElasticQuery[Wildcard] = WildcardQuery(field, s"$value*")

def term[A: ElasticPrimitive](
field: Field[_, A],
multiField: Option[String] = None,
Expand All @@ -89,6 +93,8 @@ object ElasticQuery {

def term[A: ElasticPrimitive](field: String, value: A): ElasticQuery[Term[A]] = TermQuery(field, value)

def wildcard(field: String, value: String): ElasticQuery[Wildcard] = WildcardQuery(field, value)
dbulaja98 marked this conversation as resolved.
Show resolved Hide resolved

private[elasticsearch] final case class BoolQuery(must: List[ElasticQuery[_]], should: List[ElasticQuery[_]])
extends ElasticQuery[Bool] { self =>

Expand Down Expand Up @@ -194,6 +200,21 @@ object ElasticQuery {
Obj("term" -> Obj(field -> Obj(termFields.toList: _*)))
}
}

private[elasticsearch] final case class WildcardQuery(
field: String,
value: String,
boost: Option[Double] = None,
caseInsensitive: Option[Boolean] = None
) extends ElasticQuery[Wildcard] { self =>
override def toJson: Json = {
val wildcardFields = Some("value" -> value.toJson) ++ boost.map("boost" -> Num(_)) ++ caseInsensitive.map(
"case_insensitive" -> Json.Bool(_)
)
Obj("wildcard" -> Obj(field -> Obj(wildcardFields.toList: _*)))
}
}

}

sealed trait ElasticQueryType
Expand All @@ -205,4 +226,5 @@ object ElasticQueryType {
trait MatchAll extends ElasticQueryType
trait Range extends ElasticQueryType
trait Term[A] extends ElasticQueryType
trait Wildcard extends ElasticQueryType
}
190 changes: 190 additions & 0 deletions modules/library/src/test/scala/zio/elasticsearch/QueryDSLSpec.scala
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,56 @@ object QueryDSLSpec extends ZIOSpecDefault {
assert(queryString)(
equalTo(TermQuery(field = "day_of_week", value = "Monday", boost = Some(1.0), caseInsensitive = Some(true)))
)
},
test("successfully create Wildcard Query") {
val wildcardQuery1 = contains(field = "day_of_week", value = "M")
val wildcardQuery2 = startsWith(field = "day_of_week", value = "M")
val wildcardQuery3 = wildcard(field = "day_of_week", value = "M*")

assert(wildcardQuery1)(equalTo(WildcardQuery(field = "day_of_week", value = "*M*"))) &&
assert(wildcardQuery2)(equalTo(WildcardQuery(field = "day_of_week", value = "M*"))) &&
assert(wildcardQuery3)(equalTo(WildcardQuery(field = "day_of_week", value = "M*")))
},
test("successfully create Wildcard Query with boost") {
val wildcardQuery1 = contains(field = "day_of_week", value = "M").boost(1.0)
val wildcardQuery2 = startsWith(field = "day_of_week", value = "M").boost(1.0)
val wildcardQuery3 = wildcard(field = "day_of_week", value = "M*").boost(1.0)

assert(wildcardQuery1)(equalTo(WildcardQuery(field = "day_of_week", value = "*M*", boost = Some(1.0)))) &&
assert(wildcardQuery2)(equalTo(WildcardQuery(field = "day_of_week", value = "M*", boost = Some(1.0)))) &&
assert(wildcardQuery3)(equalTo(WildcardQuery(field = "day_of_week", value = "M*", boost = Some(1.0))))
},
test("successfully create case insensitive Wildcard Query") {
val wildcardQuery1 = contains(field = "day_of_week", value = "M").caseInsensitiveTrue
val wildcardQuery2 = startsWith(field = "day_of_week", value = "M").caseInsensitiveTrue
val wildcardQuery3 = wildcard(field = "day_of_week", value = "M*").caseInsensitiveTrue

assert(wildcardQuery1)(
equalTo(WildcardQuery(field = "day_of_week", value = "*M*", caseInsensitive = Some(true)))
) &&
assert(wildcardQuery2)(
equalTo(WildcardQuery(field = "day_of_week", value = "M*", caseInsensitive = Some(true)))
) &&
assert(wildcardQuery3)(
equalTo(WildcardQuery(field = "day_of_week", value = "M*", caseInsensitive = Some(true)))
)
},
test("successfully create case insensitive Wildcard Query with boost") {
val wildcardQuery1 = contains(field = "day_of_week", value = "M").boost(1.0).caseInsensitiveTrue
val wildcardQuery2 = startsWith(field = "day_of_week", value = "M").boost(1.0).caseInsensitiveTrue
val wildcardQuery3 = wildcard(field = "day_of_week", value = "M*").boost(1.0).caseInsensitiveTrue

assert(wildcardQuery1)(
equalTo(
WildcardQuery(field = "day_of_week", value = "*M*", boost = Some(1.0), caseInsensitive = Some(true))
)
) &&
assert(wildcardQuery2)(
equalTo(WildcardQuery(field = "day_of_week", value = "M*", boost = Some(1.0), caseInsensitive = Some(true)))
) &&
assert(wildcardQuery3)(
equalTo(WildcardQuery(field = "day_of_week", value = "M*", boost = Some(1.0), caseInsensitive = Some(true)))
)
}
),
suite("encoding ElasticQuery as JSON")(
Expand Down Expand Up @@ -598,6 +648,146 @@ object QueryDSLSpec extends ZIOSpecDefault {

assert(query.toJsonBody)(equalTo(expected.toJson))
},
test("properly encode Wildcard query") {
val query1 = contains(field = "day_of_week", value = "M")
val query2 = startsWith(field = "day_of_week", value = "M")
val query3 = wildcard(field = "day_of_week", value = "M*")
val expected1 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "*M*"
| }
| }
| }
|}
|""".stripMargin
val expected23 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "M*"
| }
| }
| }
|}
|""".stripMargin

assert(query1.toJsonBody)(equalTo(expected1.toJson)) &&
assert(query2.toJsonBody)(equalTo(expected23.toJson)) &&
assert(query3.toJsonBody)(equalTo(expected23.toJson))
},
test("properly encode Wildcard query with boost") {
val query1 = contains(field = "day_of_week", value = "M").boost(1.0)
val query2 = startsWith(field = "day_of_week", value = "M").boost(1.0)
val query3 = wildcard(field = "day_of_week", value = "M*").boost(1.0)
val expected1 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "*M*",
| "boost": 1.0
| }
| }
| }
|}
|""".stripMargin
val expected23 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "M*",
| "boost": 1.0
| }
| }
| }
|}
|""".stripMargin

assert(query1.toJsonBody)(equalTo(expected1.toJson)) &&
assert(query2.toJsonBody)(equalTo(expected23.toJson)) &&
assert(query3.toJsonBody)(equalTo(expected23.toJson))
},
test("properly encode case insensitive Wildcard query") {
val query1 = contains(field = "day_of_week", value = "M").caseInsensitiveTrue
val query2 = startsWith(field = "day_of_week", value = "M").caseInsensitiveTrue
val query3 = wildcard(field = "day_of_week", value = "M*").caseInsensitiveTrue
val expected1 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "*M*",
| "case_insensitive": true
| }
| }
| }
|}
|""".stripMargin
val expected23 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "M*",
| "case_insensitive": true
| }
| }
| }
|}
|""".stripMargin

assert(query1.toJsonBody)(equalTo(expected1.toJson)) &&
assert(query2.toJsonBody)(equalTo(expected23.toJson)) &&
assert(query3.toJsonBody)(equalTo(expected23.toJson))
},
test("properly encode case insensitive Wildcard query with boost") {
val query1 = contains(field = "day_of_week", value = "M").boost(1.0).caseInsensitiveTrue
val query2 = startsWith(field = "day_of_week", value = "M").boost(1.0).caseInsensitiveTrue
val query3 = wildcard(field = "day_of_week", value = "M*").boost(1.0).caseInsensitiveTrue
val expected1 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "*M*",
| "boost": 1.0,
| "case_insensitive": true
| }
| }
| }
|}
|""".stripMargin
val expected23 =
"""
|{
| "query": {
| "wildcard": {
| "day_of_week": {
| "value": "M*",
| "boost": 1.0,
| "case_insensitive": true
| }
| }
| }
|}
|""".stripMargin

assert(query1.toJsonBody)(equalTo(expected1.toJson)) &&
assert(query2.toJsonBody)(equalTo(expected23.toJson)) &&
assert(query3.toJsonBody)(equalTo(expected23.toJson))
},
test("properly encode Bulk request body") {
val bulkQuery = IndexName.make("users").map { index =>
val user =
Expand Down