enhancement(search): implement os dsl range field

This commit is contained in:
fschade
2025-08-28 09:30:59 +02:00
parent 1c92f3db00
commit 48705c79f6
10 changed files with 282 additions and 15 deletions
@@ -11,6 +11,7 @@ type TableTest[G any, W any] struct {
Name string
Got G
Want W
Err error
}
func ToJSON(t *testing.T, data any) string {
+18
View File
@@ -43,6 +43,24 @@ func (f BuilderFunc) String() string {
return string(b)
}
func applyValue[T any](target map[string]any, key string, v T) {
if target == nil || isEmpty(key) || isEmpty(v) {
return
}
target[key] = v
}
func applyValues[T any](target map[string]any, values map[string]T) {
if target == nil || isEmpty(values) {
return
}
for k, v := range values {
applyValue[T](target, k, v)
}
}
func applyBuilder(target map[string]any, key string, builder Builder) error {
if target == nil || isEmpty(key) || isEmpty(builder) {
return nil
@@ -17,7 +17,7 @@ func TestBoolQuery(t *testing.T) {
Want: nil,
},
{
Name: "naked",
Name: "with-options",
Got: opensearch.NewBoolQuery(opensearch.BoolQueryOptions{
MinimumShouldMatch: 10,
Boost: 10,
@@ -31,9 +31,7 @@ func (q *MatchPhraseQuery) Map() (map[string]any, error) {
return nil, err
}
if !isEmpty(q.query) {
data["query"] = q.query
}
applyValue(data, "query", q.query)
if isEmpty(data) {
return nil, nil
@@ -24,9 +24,7 @@ func (q *IDsQuery) Map() (map[string]any, error) {
return nil, err
}
if !isEmpty(q.values) {
data["values"] = q.values
}
applyValue(data, "values", q.values)
if isEmpty(data) {
return nil, nil
@@ -0,0 +1,92 @@
package opensearch
import (
"encoding/json"
"errors"
"time"
)
type RangeQuery[T time.Time | string] struct {
field string
gt T
gte T
lt T
lte T
options RangeQueryOptions
}
type RangeQueryOptions struct {
Format string `json:"format,omitempty"`
Relation string `json:"relation,omitempty"`
Boost float32 `json:"boost,omitempty"`
TimeZone string `json:"time_zone,omitempty"`
}
func NewRangeQuery[T time.Time | string](field string, o ...RangeQueryOptions) *RangeQuery[T] {
return &RangeQuery[T]{field: field, options: merge(o...)}
}
func (q *RangeQuery[T]) Gt(v T) *RangeQuery[T] {
q.gt = v
return q
}
func (q *RangeQuery[T]) Gte(v T) *RangeQuery[T] {
q.gte = v
return q
}
func (q *RangeQuery[T]) Lt(v T) *RangeQuery[T] {
q.lt = v
return q
}
func (q *RangeQuery[T]) Lte(v T) *RangeQuery[T] {
q.lte = v
return q
}
func (q *RangeQuery[T]) Map() (map[string]any, error) {
data, err := convert[map[string]any](q.options)
if err != nil {
return nil, err
}
if !isEmpty(q.gt) && !isEmpty(q.gte) {
return nil, errors.New("cannot set both gt and gte in RangeQuery")
}
if !isEmpty(q.lt) && !isEmpty(q.lte) {
return nil, errors.New("cannot set both lt and lte in RangeQuery")
}
applyValues(data, map[string]T{
"gt": q.gt,
"gte": q.gte,
"lt": q.lt,
"lte": q.lte,
})
if isEmpty(data) {
return nil, nil
}
return map[string]any{
"range": map[string]any{
q.field: data,
},
}, nil
}
func (q *RangeQuery[T]) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
func (q *RangeQuery[T]) String() string {
b, _ := q.MarshalJSON()
return string(b)
}
@@ -0,0 +1,164 @@
package opensearch_test
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/opencloud-eu/opencloud/services/search/pkg/opensearch"
"github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/test"
)
func TestRangeQuery(t *testing.T) {
now := time.Now()
tests := []opensearchtest.TableTest[opensearch.Builder, map[string]any]{
{
Name: "empty",
Got: opensearch.NewRangeQuery[string]("empty"),
Want: nil,
},
{
Name: "gt string",
Got: opensearch.NewRangeQuery[string]("created").Gt("2023-01-01T00:00:00Z"),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"gt": "2023-01-01T00:00:00Z",
},
},
},
},
{
Name: "gt time",
Got: opensearch.NewRangeQuery[time.Time]("created").Gt(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"gt": now,
},
},
},
},
{
Name: "gte string",
Got: opensearch.NewRangeQuery[string]("created").Gte("2023-01-01T00:00:00Z"),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"gte": "2023-01-01T00:00:00Z",
},
},
},
},
{
Name: "gte time",
Got: opensearch.NewRangeQuery[time.Time]("created").Gte(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"gte": now,
},
},
},
},
{
Name: "gt & gte",
Got: opensearch.NewRangeQuery[time.Time]("created").Gt(now).Gte(now),
Want: nil,
Err: errors.New(""),
},
{
Name: "gt string",
Got: opensearch.NewRangeQuery[string]("created").Lt("2023-01-01T00:00:00Z"),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"lt": "2023-01-01T00:00:00Z",
},
},
},
},
{
Name: "lt time",
Got: opensearch.NewRangeQuery[time.Time]("created").Lt(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"lt": now,
},
},
},
},
{
Name: "lte string",
Got: opensearch.NewRangeQuery[string]("created").Lte("2023-01-01T00:00:00Z"),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"lte": "2023-01-01T00:00:00Z",
},
},
},
},
{
Name: "lte time",
Got: opensearch.NewRangeQuery[time.Time]("created").Lte(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"lte": now,
},
},
},
},
{
Name: "lt & lte",
Got: opensearch.NewRangeQuery[time.Time]("created").Lt(now).Lte(now),
Want: nil,
Err: errors.New(""),
},
{
Name: "options",
Got: opensearch.NewRangeQuery[time.Time]("created", opensearch.RangeQueryOptions{
Format: "strict_date_optional_time",
Relation: "within",
Boost: 1.0,
TimeZone: "UTC",
}).Lte(now).Gte(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"lte": now,
"gte": now,
"format": "strict_date_optional_time",
"relation": "within",
"boost": 1.0,
"time_zone": "UTC",
},
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
got, err := test.Got.MarshalJSON()
switch {
case test.Err != nil && test.Err.Error() == "": // Expecting any error
assert.Error(t, err)
assert.Nil(t, got)
return
case test.Err != nil && test.Err.Error() != "": // Expecting a specific error
assert.ErrorIs(t, test.Err, err)
assert.Nil(t, got)
return
}
require.NoError(t, err)
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), string(got))
})
}
}
@@ -31,9 +31,7 @@ func (q *TermQuery[T]) Map() (map[string]any, error) {
return nil, err
}
if !isEmpty(q.value) {
data["value"] = q.value
}
applyValue(data, "value", q.value)
if isEmpty(data) {
return nil, nil
@@ -17,7 +17,7 @@ func TestTermQuery(t *testing.T) {
Want: nil,
},
{
Name: "naked",
Name: "op-options",
Got: opensearch.NewTermQuery[bool]("deleted").Value(false),
Want: map[string]any{
"term": map[string]any{
@@ -28,7 +28,7 @@ func TestTermQuery(t *testing.T) {
},
},
{
Name: "term",
Name: "with-options",
Got: opensearch.NewTermQuery[bool]("deleted", opensearch.TermQueryOptions{
Boost: 1.0,
CaseInsensitive: true,
@@ -31,9 +31,7 @@ func (q *WildcardQuery) Map() (map[string]any, error) {
return nil, err
}
if !isEmpty(q.value) {
data["value"] = q.value
}
applyValue(data, "value", q.value)
if isEmpty(data) {
return nil, nil