Skip to main content

Elasticsearch Dialect

The Elasticsearch dialect renders criteria as Elasticsearch Query DSL JSON. Like MongoDB, it extends CriteriaTag directly rather than the SQL trait, and each predicate maps to the appropriate Elasticsearch query type.

Dependency

libraryDependencies += "com.eff3ct" %% "criteria4s-elasticsearch" % "1.0.0"

Import Pattern

import com.eff3ct.criteria4s.core.*
import com.eff3ct.criteria4s.dialect.elasticsearch.{*, given}
import com.eff3ct.criteria4s.functions as F
import com.eff3ct.criteria4s.extensions.*

Column Quoting

Elasticsearch renders field names with double quotes, consistent with JSON:

val column = summon[Show[Column, Elasticsearch]]
// column: Show[Column, Elasticsearch] = com.eff3ct.criteria4s.core.Show$$$Lambda$2369/0x00007ff230779010@5cd6ff8f
column.show(Column("user_name"))
// res0: String = "\"user_name\""

Predicate Reference

Term Queries (Equality)

Equality predicates map to Elasticsearch term queries:

// term query
F.===[Elasticsearch, Column, Int](F.col("age"), F.lit(30)).value
// res1: String = "{\"term\": {\"age\": 30}}"

// negated term (bool must_not)
F.=!=[Elasticsearch, Column, Int](F.col("age"), F.lit(30)).value
// res2: String = "{\"bool\": {\"must_not\": [{\"term\": {\"age\": 30}}]}}"

Range Queries

Comparison predicates map to Elasticsearch range queries:

// gt
F.gt[Elasticsearch, Column, Int](F.col("age"), F.lit(18)).value
// res3: String = "{\"range\": {\"age\": {\"gt\": 18}}}"

// gte
F.geq[Elasticsearch, Column, Int](F.col("age"), F.lit(21)).value
// res4: String = "{\"range\": {\"age\": {\"gte\": 21}}}"

// lt
F.lt[Elasticsearch, Column, Int](F.col("age"), F.lit(65)).value
// res5: String = "{\"range\": {\"age\": {\"lt\": 65}}}"

// lte
F.leq[Elasticsearch, Column, Int](F.col("age"), F.lit(99)).value
// res6: String = "{\"range\": {\"age\": {\"lte\": 99}}}"

Wildcard Queries (Pattern Matching)

Pattern predicates map to Elasticsearch wildcard queries:

// wildcard (LIKE equivalent)
F.like[Elasticsearch, Column, String](F.col("name"), F.lit("Joh*")).value
// res7: String = "{\"wildcard\": {\"name\": {\"value\": Joh*}}}"

// startsWith, endsWith, contains also render as wildcard
F.startsWith[Elasticsearch, Column, String](F.col("name"), F.lit("A*")).value
// res8: String = "{\"wildcard\": {\"name\": {\"value\": A*}}}"

Terms Queries (Set Membership)

// terms query (IN)
F.in[Elasticsearch, Column, Seq[Int]](
F.col("id"), F.array[Elasticsearch, Int](1, 2, 3)
).value
// res9: String = "{\"terms\": {\"id\": [1, 2, 3]}}"

// negated terms (NOT IN)
F.notIn[Elasticsearch, Column, Seq[Int]](
F.col("id"), F.array[Elasticsearch, Int](4, 5)
).value
// res10: String = "{\"bool\": {\"must_not\": [{\"terms\": {\"id\": [4, 5]}}]}}"

Exists Queries (Null Checks)

// must_not exists (IS NULL)
F.isNull[Elasticsearch, Column](F.col("email")).value
// res11: String = "{\"bool\": {\"must_not\": [{\"exists\": {\"field\": \"email\"}}]}}"

// exists (IS NOT NULL)
F.isNotNull[Elasticsearch, Column](F.col("email")).value
// res12: String = "{\"exists\": {\"field\": \"email\"}}"

Boolean Term Queries

// term: true
F.isTrue[Elasticsearch, Column](F.col("active")).value
// res13: String = "{\"term\": {\"active\": true}}"

// term: false
F.isFalse[Elasticsearch, Column](F.col("active")).value
// res14: String = "{\"term\": {\"active\": false}}"

Range Queries (BETWEEN)

warning

Like MongoDB, Elasticsearch BETWEEN uses gte (inclusive left) and lt (exclusive right), which differs from SQL's fully inclusive BETWEEN.

// BETWEEN: gte (inclusive) and lt (exclusive right)
F.between[Elasticsearch, Column, (Int, Int)](
F.col("age"), F.range[Elasticsearch, Int](18, 65)
).value
// res15: String = "{\"range\": {\"age\": {\"gte\": 18, \"lt\": 65}}}"

// NOT BETWEEN (bool must_not range)
F.notBetween[Elasticsearch, Column, (Int, Int)](
F.col("age"), F.range[Elasticsearch, Int](0, 17)
).value
// res16: String = "{\"bool\": {\"must_not\": [{\"range\": {\"age\": {\"gte\": 0, \"lt\": 17}}}]}}"

Bool Queries (Conjunctions)

The Elasticsearch dialect maps logical operators to the bool query structure. Each conjunction wraps its operands inside a bool query with the appropriate clause:

val left  = F.===[Elasticsearch, Column, Int](F.col("a"), F.lit(1))
// left: Criteria[Elasticsearch] = {"term": {"a": 1}}
val right = F.===[Elasticsearch, Column, Int](F.col("b"), F.lit(2))
// right: Criteria[Elasticsearch] = {"term": {"b": 2}}

// bool must (AND)
F.and[Elasticsearch](left, right).value
// res17: String = "{\"bool\": {\"must\": [{\"term\": {\"a\": 1}}, {\"term\": {\"b\": 2}}]}}"

// bool should (OR)
F.or[Elasticsearch](left, right).value
// res18: String = "{\"bool\": {\"should\": [{\"term\": {\"a\": 1}}, {\"term\": {\"b\": 2}}]}}"

// bool must_not (NOT)
F.not[Elasticsearch](left).value
// res19: String = "{\"bool\": {\"must_not\": [{\"term\": {\"a\": 1}}]}}"

Practical Examples

Search for active adult users

val adults = F.col[Elasticsearch]("age")
.geq(F.lit[Elasticsearch, Int](18))
.and(F.col[Elasticsearch]("active").isTrue)
// adults: Criteria[Elasticsearch] = {"bool": {"must": [{"range": {"age": {"gte": 18}}}, {"term": {"active": true}}]}}

adults.value
// res20: String = "{\"bool\": {\"must\": [{\"range\": {\"age\": {\"gte\": 18}}}, {\"term\": {\"active\": true}}]}}"

Product catalog filter

val products = F.col[Elasticsearch]("price")
.between(F.range[Elasticsearch, Int](100, 500))
.and(F.col[Elasticsearch]("category") === F.lit[Elasticsearch, String]("electronics"))
// products: Criteria[Elasticsearch] = {"bool": {"must": [{"range": {"price": {"gte": 100, "lt": 500}}}, {"term": {"category": electronics}}]}}

products.value
// res21: String = "{\"bool\": {\"must\": [{\"range\": {\"price\": {\"gte\": 100, \"lt\": 500}}}, {\"term\": {\"category\": electronics}}]}}"

Find documents with missing fields

val incomplete = F.col[Elasticsearch]("email").isNull
.or(F.col[Elasticsearch]("phone").isNull)
// incomplete: Criteria[Elasticsearch] = {"bool": {"should": [{"bool": {"must_not": [{"exists": {"field": "email"}}]}}, {"bool": {"must_not": [{"exists": {"field": "phone"}}]}}]}}

incomplete.value
// res22: String = "{\"bool\": {\"should\": [{\"bool\": {\"must_not\": [{\"exists\": {\"field\": \"email\"}}]}}, {\"bool\": {\"must_not\": [{\"exists\": {\"field\": \"phone\"}}]}}]}}"