Skip to content

Commit

Permalink
(dsl): Support Wildcard query (#48)
Browse files Browse the repository at this point in the history
  • Loading branch information
dbulaja98 authored Jan 30, 2023
1 parent bd0c6cf commit 0e0525b
Show file tree
Hide file tree
Showing 5 changed files with 302 additions and 12 deletions.
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)

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

0 comments on commit 0e0525b

Please sign in to comment.