Skip to content
Open
Show file tree
Hide file tree
Changes from 39 commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
861a986
Done: matches case for IntervalQuery
markok4 Jun 12, 2025
7bb2df6
Disable empty chunk in interval allof, Wildcard (with cointains, star…
markok4 Jun 13, 2025
907980b
Implement IntervalRange test
markok4 Jun 13, 2025
3b97832
Implement: export methods from Queries.scala to ElasticIntervalQuery.…
markok4 Jun 13, 2025
ee269c2
Implement: documentation in ElasticQuery.scala
markok4 Jun 13, 2025
441fe9f
Implement: move methods to another file, fix tests
markok4 Jun 16, 2025
af9b2fe
Fix formater bug, create ElasticIntervalQuerySpec
markok4 Jun 16, 2025
805f6bd
Fix IntervalRange, Change order of methods in ElasticQuery
markok4 Jun 16, 2025
f9a4a13
Add ElasticIntervalQuery.scala
markok4 Jun 16, 2025
84c908b
Add HasAnalyzer and HasUseField, correct some mistakes
markok4 Jun 16, 2025
d5b8bc8
Error: Scalafmt
markok4 Jun 16, 2025
7af2c13
Work on It tests
markok4 Jun 16, 2025
ba54dc1
Add integration tests for IntervalMatch
markok4 Jun 16, 2025
593f575
Refactor code in HttpExecutorSpec and create suite for IntervalMatch
markok4 Jun 17, 2025
5f42ed1
Add elastic_interval_query.md file
markok4 Jun 17, 2025
ff1916d
Complete changes on Boosting, refactor on type-safe useField
markok4 Jun 17, 2025
ca243e9
Use fieldPath
markok4 Jun 17, 2025
640500d
Implemented methods for gt,gte,lt,lte
markok4 Jun 17, 2025
6308187
Fix PR comments
markok4 Jun 18, 2025
41df198
Revert boost, add another type parameter into HasUseField
markok4 Jun 18, 2025
f7cb287
Fix some warnings about casting.
markok4 Jun 18, 2025
a136f4f
Try to remove strange chars.
markok4 Jun 18, 2025
7391cec
Done sbt prepared
markok4 Jun 18, 2025
45bc91e
Remove manually strange chars
markok4 Jun 18, 2025
cf31617
Resolve some comments.
markok4 Jun 19, 2025
3f44d44
Change elastic_interval_query.md file.
markok4 Jun 19, 2025
dfece62
Fix line endings and cleanup .gitattributes
markok4 Jun 19, 2025
500610f
Remove strange chars.
markok4 Jun 19, 2025
72715df
Remove space from ElasticQuerySpec.
markok4 Jun 19, 2025
7941fe3
Return copyright to HttpExecutorSpec.
markok4 Jun 19, 2025
26150f1
Add copyright in intervalSpec and some changes.
markok4 Jun 19, 2025
4d60d5f
Try to fix.
markok4 Jun 19, 2025
c7eab4e
Fix space in copyright
markok4 Jun 19, 2025
377bf8c
Fix space in copyright
markok4 Jun 19, 2025
b21a202
Fix space in copyright
markok4 Jun 19, 2025
b14e4d9
Add to sidebars.js
markok4 Jun 19, 2025
72e782b
Move ElasticInterval object in separated file, fix some problems.
markok4 Jun 20, 2025
626dfda
Add line in elastic_query_interval.md and do sbt prepare
markok4 Jun 20, 2025
61a4949
Done.
markok4 Jun 20, 2025
1aae089
Some fixes.
markok4 Jun 20, 2025
689ab2e
Add unit tests for query.
markok4 Jun 20, 2025
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
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
* text=auto eol=lf
sbt linguist-vendored
61 changes: 61 additions & 0 deletions docs/overview/queries/elastic_query_interval.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
id: elastic_interval_query
title: "Overview"
---

The `Intervals` query allows for advanced search queries based on intervals between words in specific fields.
This query provides flexibility for conditions.

To use the `Intervals` query, import the following:
```scala
import zio.elasticsearch.query.IntervalQuery
import zio.elasticsearch.ElasticQuery._
```

You can create a basic `Intervals` query` using the `intervals` method:
```scala
val query: IntervalQuery[Any] = intervals(field = "content", rule = intervalMatch("targetWord"))
```

To define `field` in a type-safe manner, use the overloaded `useField` method with field definitions from your document:
```scala
val queryWithSafeField: IntervalQuery[Document] =
intervals(field = Document.stringField, rule = intervalMatch("targetWord"))
```

If you want to specify which fields should be searched, you can use the `useField` method:
```scala
val queryWithField: IntervalQuery[Document] =
intervals(field = "content", rule = intervalMatch("targetWord").useField(Document.stringField))
```

If you want to define the `maxGaps` parameter, use the `maxGaps` method:
```scala
val queryWithMaxGaps: IntervalQuery[Document] =
intervals(field = "content", rule = intervalMatch("targetWord").maxGaps(2))
```

If you want to specify the word order requirement, use the `orderedOn` method:
```scala
val queryWithOrder: IntervalQuery[Document] =
intervals(field = "content", rule = intervalMatch("targetWord").orderedOn)
```

You can also apply additional filters to the query:
```scala
val queryWithFilter: IntervalQuery[Document] =
intervals(field = "content", rule = intervalMatch("targetWord").filter(IntervalFilter.someFilter))
```

Alternatively, you can construct the query manually with all parameters:
```scala
val queryManually: IntervalQuery[Document] =
IntervalQuery(
field = "content",
rule = intervalMatch("targetWord")
.maxGaps(2)
.orderedOn
.filter(IntervalFilter.someFilter)
.analyzer("standard")
)
```
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ package zio.elasticsearch
import zio.Chunk
import zio.elasticsearch.ElasticAggregation._
import zio.elasticsearch.ElasticHighlight.highlight
import zio.elasticsearch.ElasticQuery.{script => _, _}
import zio.elasticsearch.ElasticIntervalQuery.intervalMatch
import zio.elasticsearch.ElasticQuery.{contains => _, _}
import zio.elasticsearch.ElasticSort.sortBy
import zio.elasticsearch.aggregation.AggregationOrder
import zio.elasticsearch.data.GeoPoint
Expand Down Expand Up @@ -2771,6 +2772,67 @@ object HttpExecutorSpec extends IntegrationSpec {
}
} @@ after(Executor.execute(ElasticRequest.deleteIndex(geoPolygonIndex)).orDie)
),
suite("intervals query")(
test("intervalMatch query returns only matching document") {
checkOnce(genDocumentId, genTestDocument, Gen.alphaNumericString.filter(_.nonEmpty)) {
(idMatch, docMatch, targetWord) =>
val docShouldMatch = docMatch.copy(stringField = s"prefix $targetWord suffix")

for {
_ <- Executor.execute(ElasticRequest.deleteByQuery(firstSearchIndex, matchAll))
_ <- Executor.execute(ElasticRequest.upsert(firstSearchIndex, idMatch, docShouldMatch).refreshTrue)
res <- Executor
.execute(
ElasticRequest.search(
firstSearchIndex,
intervals(TestDocument.stringField, intervalMatch(targetWord))
)
)
.documentAs[TestDocument]
} yield assert(res)(Assertion.hasSameElements(Chunk(docShouldMatch)))
}
} @@ around(
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
),
test("intervalMatch query finds document with exact matching term") {
checkOnce(genDocumentId, genTestDocument) { (docId, doc) =>
val term = "apple"
val docWithTerm = doc.copy(stringField = s"$term banana orange")

val query = intervals(
"stringField",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please use TestDocument.stringField.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any issue with this?

intervalMatch(term)
)

for {
_ <- Executor.execute(ElasticRequest.upsert(firstSearchIndex, docId, docWithTerm))
_ <- Executor.execute(ElasticRequest.refresh(firstSearchIndex))
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
} yield assert(res)(Assertion.contains(docWithTerm))
}
} @@ around(
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
),
test("intervalMatch query does not find document if term is absent") {
checkOnce(genDocumentId, genTestDocument) { (docId, doc) =>
val query = intervals(
"stringField",
intervalMatch("nonexistentterm")
)

for {
_ <- Executor.execute(ElasticRequest.upsert(firstSearchIndex, docId, doc))
_ <- Executor.execute(ElasticRequest.refresh(firstSearchIndex))
res <- Executor.execute(ElasticRequest.search(firstSearchIndex, query)).documentAs[TestDocument]
} yield assert(res)(Assertion.isEmpty)
}
} @@ around(
Executor.execute(ElasticRequest.createIndex(firstSearchIndex)),
Executor.execute(ElasticRequest.deleteIndex(firstSearchIndex)).orDie
)
),
suite("search for documents using FunctionScore query")(
test("using randomScore function") {
checkOnce(genTestDocument, genTestDocument) { (firstDocument, secondDocument) =>
Expand Down
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this be also renamed to Rule?

Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Copyright 2022 LambdaWorks
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.elasticsearch

import zio.NonEmptyChunk
import zio.elasticsearch.query.{
IntervalAllOf,
IntervalAnyOf,
IntervalFilter,
IntervalFuzzy,
IntervalMatch,
IntervalPrefix,
IntervalRange,
IntervalRegexp,
IntervalRule,
IntervalWildcard,
Regexp
}
import zio.json.ast.Json

object ElasticIntervalQuery {

def intervalAllOf[S](intervals: NonEmptyChunk[IntervalRule]): IntervalAllOf[S] =
IntervalAllOf(intervals = intervals, maxGaps = None, ordered = None, filter = None)

def intervalAnyOf[S](intervals: NonEmptyChunk[IntervalRule]): IntervalAnyOf[S] =
IntervalAnyOf(intervals = intervals, filter = None)

def intervalContains[S](pattern: String): IntervalWildcard[S] =
IntervalWildcard(s"*$pattern*", analyzer = None, useField = None)

def intervalEndsWith[S](pattern: String): IntervalWildcard[S] =
IntervalWildcard(s"*$pattern", analyzer = None, useField = None)

def intervalFilter[S](
after: Option[IntervalRule] = None,
before: Option[IntervalRule] = None,
containedBy: Option[IntervalRule] = None,
containing: Option[IntervalRule] = None,
notContainedBy: Option[IntervalRule] = None,
notContaining: Option[IntervalRule] = None,
notOverlapping: Option[IntervalRule] = None,
overlapping: Option[IntervalRule] = None,
script: Option[Json] = None
): Option[IntervalFilter[S]] = {

val filter: IntervalFilter[S] = IntervalFilter(
after = after,
before = before,
containedBy = containedBy,
containing = containing,
notContainedBy = notContainedBy,
notContaining = notContaining,
notOverlapping = notOverlapping,
overlapping = overlapping,
script = script
)

Some(filter).filterNot(_ =>
List(after, before, containedBy, containing, notContainedBy, notContaining, notOverlapping, overlapping, script)
.forall(_.isEmpty)
)
}

def intervalFuzzy[S](term: String): IntervalFuzzy[S] =
IntervalFuzzy(
term = term,
prefixLength = None,
transpositions = None,
fuzziness = None,
analyzer = None,
useField = None
)

def intervalMatch[S](query: String): IntervalMatch[S] =
IntervalMatch(query = query, analyzer = None, useField = None, maxGaps = None, ordered = None, filter = None)

def intervalPrefix[S](prefix: String): IntervalPrefix[S] =
IntervalPrefix(prefix = prefix, analyzer = None, useField = None)

def intervalRange[S](
lower: Option[IntervalRule] = None,
upper: Option[IntervalRule] = None,
analyzer: Option[String] = None,
useField: Option[String] = None
): IntervalRange[S] =
IntervalRange(lower = lower, upper = upper, analyzer = analyzer, useField = useField)

def intervalRegexp[S](pattern: Regexp[S]): IntervalRegexp[S] =
IntervalRegexp(pattern = pattern, analyzer = None, useField = None)

def intervalStartsWith[S](pattern: String): IntervalWildcard[S] =
IntervalWildcard(s"$pattern*", analyzer = None, useField = None)

def intervalWildcard[S](pattern: String): IntervalWildcard[S] =
IntervalWildcard(pattern = pattern, analyzer = None, useField = None)
}
Original file line number Diff line number Diff line change
Expand Up @@ -548,6 +548,37 @@ object ElasticQuery {
final def ids(value: String, values: String*): IdsQuery[Any] =
Ids(values = Chunk.fromIterable(value +: values))

/**
* Constructs a type-safe intervals query by combining a field and an interval rule.
*
* The resulting query wraps the specified interval query under the given type-safe field in the intervals query
* structure.
*
* @param field
* the type-safe field on which the query is executed
* @param rule
* an instance of [[zio.elasticsearch.query.IntervalRule]] representing the interval query rule
* @tparam S
* the document type for which the query is defined
* @return
* an [[zio.elasticsearch.ElasticQuery]] instance representing the intervals query.
*/
final def intervals[S](field: Field[S, _], rule: IntervalRule): ElasticQuery[S] = Intervals(field.toString, rule)

/**
* Constructs an intervals query by combining a field and an interval query.
*
* The resulting query wraps the specified interval query under the given field in the intervals query structure.
*
* @param field
* the name of the field to be queried
* @param rule
* an instance of [[zio.elasticsearch.query.IntervalRule]] representing the interval query rule
* @return
* an [[zio.elasticsearch.ElasticQuery]] instance representing the intervals query.
*/
final def intervals(field: String, rule: IntervalRule): ElasticQuery[Any] = Intervals(field = field, rule = rule)

/**
* Constructs a type-safe instance of [[zio.elasticsearch.query.KNNQuery]] using the specified parameters.
* [[zio.elasticsearch.query.KNNQuery]] is used to perform a k-nearest neighbor (kNN) search and returns the matching
Expand Down
Loading
Loading