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 term query #25

Merged
merged 14 commits into from
Jan 4, 2023
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