Skip to content

Commit

Permalink
(dsl): Support term query (#25)
Browse files Browse the repository at this point in the history
  • Loading branch information
mvelimir authored Jan 4, 2023
1 parent 23404c1 commit 759d87b
Show file tree
Hide file tree
Showing 5 changed files with 267 additions and 48 deletions.
24 changes: 24 additions & 0 deletions modules/library/src/main/scala/zio/elasticsearch/Boost.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package zio.elasticsearch

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

object Boost {

trait WithBoost[EQT <: ElasticQueryType] {
def withBoost(query: ElasticQuery[EQT], value: Double): ElasticQuery[EQT]
}

object WithBoost {
implicit val matchAllWithBoost: WithBoost[MatchAll] = (query: ElasticQuery[MatchAll], value: Double) =>
query match {
case q: MatchAllQuery => q.copy(boost = Some(value))
}

implicit def termWithBoost[A: ElasticPrimitive]: WithBoost[Term[A]] =
(query: ElasticQuery[Term[A]], value: Double) =>
query match {
case q: TermQuery[A] => q.copy(boost = Some(value))
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package zio.elasticsearch

import zio.elasticsearch.ElasticQuery.TermQuery
import zio.elasticsearch.ElasticQueryType.Term

object CaseInsensitive {

trait WithCaseInsensitive[EQT <: ElasticQueryType] {
def withCaseInsensitive(query: ElasticQuery[EQT], value: Boolean): ElasticQuery[EQT]
}

object WithCaseInsensitive {
implicit val termWithCaseInsensitiveString: WithCaseInsensitive[Term[String]] =
(query: ElasticQuery[Term[String]], value: Boolean) =>
query match {
case q: TermQuery[String] => q.copy(caseInsensitive = Some(value))
}
}
}
92 changes: 68 additions & 24 deletions modules/library/src/main/scala/zio/elasticsearch/ElasticQuery.scala
Original file line number Diff line number Diff line change
@@ -1,19 +1,35 @@
package zio.elasticsearch

import zio.elasticsearch.Boost.WithBoost
import zio.elasticsearch.CaseInsensitive.WithCaseInsensitive
import zio.json.ast.Json
import zio.json.ast.Json.{Arr, Bool, Num, Obj, Str}
import zio.json.ast.Json.{Arr, Num, Obj, Str}

import scala.annotation.unused

sealed trait ElasticQuery { self =>
sealed trait ElasticQuery[EQT <: ElasticQueryType] { self =>

def toJson: Json

final def toJsonBody: Json = Obj("query" -> self.toJson)

final def boost(value: Double)(implicit wb: WithBoost[EQT]): ElasticQuery[EQT] =
wb.withBoost(query = self, value = value)

final def caseInsensitive(value: Boolean)(implicit wci: WithCaseInsensitive[EQT]): ElasticQuery[EQT] =
wci.withCaseInsensitive(query = self, value = value)

final def caseInsensitiveTrue(implicit wci: WithCaseInsensitive[EQT]): ElasticQuery[EQT] =
wci.withCaseInsensitive(query = self, value = true)

final def caseInsensitiveFalse(implicit wci: WithCaseInsensitive[EQT]): ElasticQuery[EQT] =
wci.withCaseInsensitive(query = self, value = false)
}

object ElasticQuery {

import ElasticQueryType._

sealed trait ElasticPrimitive[A] {
def toJson(value: A): Json
}
Expand All @@ -27,7 +43,7 @@ object ElasticQuery {
}

implicit object ElasticBool extends ElasticPrimitive[Boolean] {
override def toJson(value: Boolean): Json = Bool(value)
override def toJson(value: Boolean): Json = Json.Bool(value)
}

implicit object ElasticLong extends ElasticPrimitive[Long] {
Expand All @@ -38,44 +54,47 @@ object ElasticQuery {
def toJson(implicit EP: ElasticPrimitive[A]): Json = EP.toJson(value)
}

def matches[A: ElasticPrimitive](field: String, value: A): ElasticQuery =
Match(field, value)
def matches[A: ElasticPrimitive](field: String, value: A): ElasticQuery[Match] =
MatchQuery(field, value)

def boolQuery(): BoolQuery = BoolQuery.empty

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

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

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

def range(field: String): Range[Unbounded.type, Unbounded.type] = Range.empty(field)
def term[A: ElasticPrimitive](field: String, value: A): ElasticQuery[Term[A]] = TermQuery(field, value)

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

override def toJson: Json =
Obj("bool" -> Obj("must" -> Arr(must.map(_.toJson): _*), "should" -> Arr(should.map(_.toJson): _*)))

def must(queries: ElasticQuery*): BoolQuery =
def must(queries: ElasticQuery[_]*): BoolQuery =
self.copy(must = must ++ queries)

def should(queries: ElasticQuery*): BoolQuery =
def should(queries: ElasticQuery[_]*): BoolQuery =
self.copy(should = should ++ queries)
}

private[elasticsearch] object BoolQuery {
def empty: BoolQuery = BoolQuery(Nil, Nil)
}

private[elasticsearch] final case class Exists private (field: String) extends ElasticQuery {
private[elasticsearch] final case class ExistsQuery private (field: String) extends ElasticQuery[Exists] {
override def toJson: Json = Obj("exists" -> Obj("field" -> field.toJson))
}

private[elasticsearch] final case class Match[A: ElasticPrimitive](field: String, value: A) extends ElasticQuery {
private[elasticsearch] final case class MatchQuery[A: ElasticPrimitive](field: String, value: A)
extends ElasticQuery[Match] {
override def toJson: Json = Obj("match" -> Obj(field -> value.toJson))
}

private[elasticsearch] final case class MatchAll() extends ElasticQuery {
override def toJson: Json = Obj("match_all" -> Obj())
private[elasticsearch] final case class MatchAllQuery(boost: Option[Double] = None) extends ElasticQuery[MatchAll] {
override def toJson: Json = Obj("match_all" -> Obj(boost.map("boost" -> Num(_)).toList: _*))
}

sealed trait LowerBound {
Expand Down Expand Up @@ -106,32 +125,57 @@ object ElasticQuery {
override def toJson: Option[(String, Json)] = None
}

private[elasticsearch] final case class Range[LB <: LowerBound, UB <: UpperBound] private (
private[elasticsearch] final case class RangeQuery[LB <: LowerBound, UB <: UpperBound] private (
field: String,
lower: LB,
upper: UB
) extends ElasticQuery { self =>
) extends ElasticQuery[Range] { self =>

def gt[A: ElasticPrimitive](value: A)(implicit @unused ev: LB =:= Unbounded.type): Range[GreaterThan[A], UB] =
def gt[A: ElasticPrimitive](value: A)(implicit @unused ev: LB =:= Unbounded.type): RangeQuery[GreaterThan[A], UB] =
self.copy(lower = GreaterThan(value))

def gte[A: ElasticPrimitive](value: A)(implicit
@unused ev: LB =:= Unbounded.type
): Range[GreaterThanOrEqualTo[A], UB] =
): RangeQuery[GreaterThanOrEqualTo[A], UB] =
self.copy(lower = GreaterThanOrEqualTo(value))

def lt[A: ElasticPrimitive](value: A)(implicit @unused ev: UB =:= Unbounded.type): Range[LB, LessThan[A]] =
def lt[A: ElasticPrimitive](value: A)(implicit @unused ev: UB =:= Unbounded.type): RangeQuery[LB, LessThan[A]] =
self.copy(upper = LessThan(value))

def lte[A: ElasticPrimitive](value: A)(implicit
@unused ev: UB =:= Unbounded.type
): Range[LB, LessThanOrEqualTo[A]] =
): RangeQuery[LB, LessThanOrEqualTo[A]] =
self.copy(upper = LessThanOrEqualTo(value))

override def toJson: Json = Obj("range" -> Obj(field -> Obj(List(lower.toJson, upper.toJson).flatten: _*)))
}

private[elasticsearch] object Range {
def empty(field: String): Range[Unbounded.type, Unbounded.type] = Range(field, Unbounded, Unbounded)
private[elasticsearch] object RangeQuery {
def empty(field: String): RangeQuery[Unbounded.type, Unbounded.type] = RangeQuery(field, Unbounded, Unbounded)
}

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

sealed trait ElasticQueryType

object ElasticQueryType {
trait Bool extends ElasticQueryType
trait Exists extends ElasticQueryType
trait Match extends ElasticQueryType
trait MatchAll extends ElasticQueryType
trait Range extends ElasticQueryType
trait Term[A] extends ElasticQueryType
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ object ElasticRequest {
case None => Right(None)
}

def search(index: IndexName, query: ElasticQuery): ElasticRequest[ElasticQueryResponse, GetByQuery] =
def search(index: IndexName, query: ElasticQuery[_]): ElasticRequest[ElasticQueryResponse, GetByQuery] =
GetByQueryRequest(index, query)

def upsert[A: Schema](index: IndexName, id: DocumentId, doc: A): ElasticRequest[Unit, Upsert] =
Expand Down Expand Up @@ -115,7 +115,7 @@ object ElasticRequest {

private[elasticsearch] final case class GetByQueryRequest(
index: IndexName,
query: ElasticQuery,
query: ElasticQuery[_],
routing: Option[Routing] = None
) extends ElasticRequest[ElasticQueryResponse, GetByQuery]

Expand Down
Loading

0 comments on commit 759d87b

Please sign in to comment.