Initial QSfera import

This commit is contained in:
Курнат Андрей
2026-06-07 10:20:04 +03:00
commit 2315f25754
16485 changed files with 4826827 additions and 0 deletions
@@ -0,0 +1,128 @@
package osu
import (
"encoding/json"
"fmt"
"reflect"
"dario.cat/mergo"
"github.com/qsfera/server/pkg/conversions"
)
type Builder interface {
json.Marshaler
Map() (map[string]any, error)
}
func newBase(v ...any) (map[string]any, error) {
base := make(map[string]any)
for _, value := range v {
data, err := conversions.To[map[string]any](value)
if err != nil {
return nil, fmt.Errorf("failed to convert value to map: %w", err)
}
if isEmpty(data) {
continue
}
if err := mergo.Merge(&base, data); err != nil {
return nil, fmt.Errorf("failed to merge value into base: %w", err)
}
}
return base, nil
}
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
}
data, err := builder.Map()
if err != nil {
return fmt.Errorf("failed to map builder %s: %w", key, err)
}
if !isEmpty(data) {
target[key] = data
}
return nil
}
func applyBuilders(target map[string]any, key string, bs ...Builder) error {
if target == nil || isEmpty(key) || isEmpty(bs) {
return nil
}
builders := make([]map[string]any, 0, len(bs))
for _, builder := range bs {
data, err := builder.Map()
switch {
case err != nil:
return fmt.Errorf("failed to map builder %s: %w", key, err)
case isEmpty(data):
continue
default:
builders = append(builders, data)
}
}
if len(builders) > 0 {
target[key] = builders
}
return nil
}
func isEmpty(x any) bool {
switch {
case x == nil:
return true
case reflect.ValueOf(x).Kind() == reflect.Bool:
return false
case reflect.DeepEqual(x, reflect.Zero(reflect.TypeOf(x)).Interface()):
return true
case reflect.ValueOf(x).Kind() == reflect.Map && reflect.ValueOf(x).Len() == 0:
return true
default:
return false
}
}
func merge[T any](vals ...T) T {
base := make(map[string]any)
for _, val := range vals {
data, err := conversions.To[map[string]any](val)
if err != nil {
continue
}
_ = mergo.Merge(&base, data)
}
data, _ := conversions.To[T](base)
return data
}
@@ -0,0 +1,87 @@
package osu
import (
"encoding/json"
)
type BoolQuery struct {
must []Builder
mustNot []Builder
should []Builder
filter []Builder
params *BoolQueryParams
}
type BoolQueryParams struct {
MinimumShouldMatch int16 `json:"minimum_should_match,omitempty"`
Boost float32 `json:"boost,omitempty"`
Name string `json:"_name,omitempty"`
}
func NewBoolQuery() *BoolQuery {
return &BoolQuery{}
}
func (q *BoolQuery) Params(v *BoolQueryParams) *BoolQuery {
q.params = v
return q
}
func (q *BoolQuery) Must(v ...Builder) *BoolQuery {
q.must = append(q.must, v...)
return q
}
func (q *BoolQuery) MustNot(v ...Builder) *BoolQuery {
q.mustNot = append(q.mustNot, v...)
return q
}
func (q *BoolQuery) Should(v ...Builder) *BoolQuery {
q.should = append(q.should, v...)
return q
}
func (q *BoolQuery) Filter(v ...Builder) *BoolQuery {
q.filter = append(q.filter, v...)
return q
}
func (q *BoolQuery) Map() (map[string]any, error) {
base, err := newBase(q.params)
if err != nil {
return nil, err
}
if err := applyBuilders(base, "must", q.must...); err != nil {
return nil, err
}
if err := applyBuilders(base, "must_not", q.mustNot...); err != nil {
return nil, err
}
if err := applyBuilders(base, "should", q.should...); err != nil {
return nil, err
}
if err := applyBuilders(base, "filter", q.filter...); err != nil {
return nil, err
}
if isEmpty(base) {
return nil, nil
}
return map[string]any{
"bool": base,
}, nil
}
func (q *BoolQuery) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
@@ -0,0 +1,158 @@
package osu_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/osu"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/test"
)
func TestBoolQuery(t *testing.T) {
tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
{
Name: "empty",
Got: osu.NewBoolQuery(),
Want: nil,
},
{
Name: "with params",
Got: osu.NewBoolQuery().Params(&osu.BoolQueryParams{
MinimumShouldMatch: 10,
Boost: 10,
Name: "some-name",
}),
Want: map[string]any{
"bool": map[string]any{
"minimum_should_match": 10,
"boost": 10,
"_name": "some-name",
},
},
},
{
Name: "must",
Got: osu.NewBoolQuery().Must(osu.NewTermQuery[string]("name").Value("tom")),
Want: map[string]any{
"bool": map[string]any{
"must": []map[string]any{
{
"term": map[string]any{
"name": map[string]any{
"value": "tom",
},
},
},
},
},
},
},
{
Name: "must_not",
Got: osu.NewBoolQuery().MustNot(osu.NewTermQuery[string]("name").Value("tom")),
Want: map[string]any{
"bool": map[string]any{
"must_not": []map[string]any{
{
"term": map[string]any{
"name": map[string]any{
"value": "tom",
},
},
},
},
},
},
},
{
Name: "should",
Got: osu.NewBoolQuery().Should(osu.NewTermQuery[string]("name").Value("tom")),
Want: map[string]any{
"bool": map[string]any{
"should": []map[string]any{
{
"term": map[string]any{
"name": map[string]any{
"value": "tom",
},
},
},
},
},
},
},
{
Name: "filter",
Got: osu.NewBoolQuery().Filter(osu.NewTermQuery[string]("name").Value("tom")),
Want: map[string]any{
"bool": map[string]any{
"filter": []map[string]any{
{
"term": map[string]any{
"name": map[string]any{
"value": "tom",
},
},
},
},
},
},
},
{
Name: "full",
Got: osu.NewBoolQuery().
Must(osu.NewTermQuery[string]("name").Value("tom")).
MustNot(osu.NewTermQuery[bool]("deleted").Value(true)).
Should(osu.NewTermQuery[string]("gender").Value("male")).
Filter(osu.NewTermQuery[int]("age").Value(42)),
Want: map[string]any{
"bool": map[string]any{
"must": []map[string]any{
{
"term": map[string]any{
"name": map[string]any{
"value": "tom",
},
},
},
},
"must_not": []map[string]any{
{
"term": map[string]any{
"deleted": map[string]any{
"value": true,
},
},
},
},
"should": []map[string]any{
{
"term": map[string]any{
"gender": map[string]any{
"value": "male",
},
},
},
},
"filter": []map[string]any{
{
"term": map[string]any{
"age": map[string]any{
"value": 42,
},
},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
})
}
}
@@ -0,0 +1,58 @@
package osu
import (
"encoding/json"
)
type MatchPhraseQuery struct {
field string
query string
params *MatchPhraseQueryParams
}
type MatchPhraseQueryParams struct {
Analyzer string `json:"analyzer,omitempty"`
Slop int `json:"slop,omitempty"`
ZeroTermsQuery string `json:"zero_terms_query,omitempty"`
}
func NewMatchPhraseQuery(field string) *MatchPhraseQuery {
return &MatchPhraseQuery{field: field}
}
func (q *MatchPhraseQuery) Params(v *MatchPhraseQueryParams) *MatchPhraseQuery {
q.params = v
return q
}
func (q *MatchPhraseQuery) Query(v string) *MatchPhraseQuery {
q.query = v
return q
}
func (q *MatchPhraseQuery) Map() (map[string]any, error) {
base, err := newBase(q.params)
if err != nil {
return nil, err
}
applyValue(base, "query", q.query)
if isEmpty(base) {
return nil, nil
}
return map[string]any{
"match_phrase": map[string]any{
q.field: base,
},
}, nil
}
func (q *MatchPhraseQuery) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
@@ -0,0 +1,72 @@
package osu_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/osu"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/test"
)
func TestNewMatchPhraseQuery(t *testing.T) {
tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
{
Name: "empty",
Got: osu.NewMatchPhraseQuery("empty"),
Want: nil,
},
{
Name: "with params",
Got: osu.NewMatchPhraseQuery("name").Params(&osu.MatchPhraseQueryParams{
Analyzer: "analyzer",
Slop: 2,
ZeroTermsQuery: "all",
}),
Want: map[string]any{
"match_phrase": map[string]any{
"name": map[string]any{
"analyzer": "analyzer",
"slop": 2,
"zero_terms_query": "all",
},
},
},
},
{
Name: "query",
Got: osu.NewMatchPhraseQuery("name").Query("some match query"),
Want: map[string]any{
"match_phrase": map[string]any{
"name": map[string]any{
"query": "some match query",
},
},
},
},
{
Name: "full",
Got: osu.NewMatchPhraseQuery("name").Params(&osu.MatchPhraseQueryParams{
Analyzer: "analyzer",
Slop: 2,
ZeroTermsQuery: "all",
}).Query("some match query"),
Want: map[string]any{
"match_phrase": map[string]any{
"name": map[string]any{
"query": "some match query",
"analyzer": "analyzer",
"slop": 2,
"zero_terms_query": "all",
},
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
})
}
}
@@ -0,0 +1,49 @@
package osu
import (
"encoding/json"
"slices"
)
type IDsQuery struct {
values []string
params *IDsQueryParams
}
type IDsQueryParams struct {
Boost float32 `json:"boost,omitempty"`
}
func NewIDsQuery(v ...string) *IDsQuery {
return &IDsQuery{values: slices.Compact(v)}
}
func (q *IDsQuery) Params(v *IDsQueryParams) *IDsQuery {
q.params = v
return q
}
func (q *IDsQuery) Map() (map[string]any, error) {
base, err := newBase(q.params)
if err != nil {
return nil, err
}
applyValue(base, "values", q.values)
if isEmpty(base) {
return nil, nil
}
return map[string]any{
"ids": base,
}, nil
}
func (q *IDsQuery) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
@@ -0,0 +1,45 @@
package osu_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/osu"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/test"
)
func TestIDsQuery(t *testing.T) {
tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
{
Name: "empty",
Got: osu.NewIDsQuery(),
Want: nil,
},
{
Name: "no params",
Got: osu.NewIDsQuery("1", "2", "3", "3"),
Want: map[string]any{
"ids": map[string]any{
"values": []string{"1", "2", "3"},
},
},
},
{
Name: "ids",
Got: osu.NewIDsQuery("1", "2", "3", "3").Params(&osu.IDsQueryParams{Boost: 1.0}),
Want: map[string]any{
"ids": map[string]any{
"values": []string{"1", "2", "3"},
"boost": 1.0,
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
})
}
}
@@ -0,0 +1,92 @@
package osu
import (
"encoding/json"
"errors"
"time"
)
type RangeQuery[T time.Time | string] struct {
field string
gt T
gte T
lt T
lte T
params *RangeQueryParams
}
type RangeQueryParams 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) *RangeQuery[T] {
return &RangeQuery[T]{field: field}
}
func (q *RangeQuery[T]) Params(v *RangeQueryParams) *RangeQuery[T] {
q.params = v
return q
}
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) {
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")
}
base, err := newBase(q.params)
if err != nil {
return nil, err
}
applyValues(base, map[string]T{
"gt": q.gt,
"gte": q.gte,
"lt": q.lt,
"lte": q.lte,
})
if isEmpty(base) {
return nil, nil
}
return map[string]any{
"range": map[string]any{
q.field: base,
},
}, nil
}
func (q *RangeQuery[T]) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
@@ -0,0 +1,164 @@
package osu_test
import (
"errors"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/osu"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/test"
)
func TestRangeQuery(t *testing.T) {
now := time.Now()
tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
{
Name: "empty",
Got: osu.NewRangeQuery[string]("empty"),
Want: nil,
},
{
Name: "gt string",
Got: osu.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: osu.NewRangeQuery[time.Time]("created").Gt(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"gt": now,
},
},
},
},
{
Name: "gte string",
Got: osu.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: osu.NewRangeQuery[time.Time]("created").Gte(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"gte": now,
},
},
},
},
{
Name: "gt & gte",
Got: osu.NewRangeQuery[time.Time]("created").Gt(now).Gte(now),
Want: nil,
Err: errors.New(""),
},
{
Name: "gt string",
Got: osu.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: osu.NewRangeQuery[time.Time]("created").Lt(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"lt": now,
},
},
},
},
{
Name: "lte string",
Got: osu.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: osu.NewRangeQuery[time.Time]("created").Lte(now),
Want: map[string]any{
"range": map[string]any{
"created": map[string]any{
"lte": now,
},
},
},
},
{
Name: "lt & lte",
Got: osu.NewRangeQuery[time.Time]("created").Lt(now).Lte(now),
Want: nil,
Err: errors.New(""),
},
{
Name: "with params",
Got: osu.NewRangeQuery[time.Time]("created").Params(&osu.RangeQueryParams{
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))
})
}
}
@@ -0,0 +1,58 @@
package osu
import (
"encoding/json"
)
type TermQuery[T comparable] struct {
field string
value T
params *TermQueryParams
}
type TermQueryParams struct {
Boost float32 `json:"boost,omitempty"`
CaseInsensitive bool `json:"case_insensitive,omitempty"`
Name string `json:"_name,omitempty"`
}
func NewTermQuery[T comparable](field string) *TermQuery[T] {
return &TermQuery[T]{field: field}
}
func (q *TermQuery[T]) Params(v *TermQueryParams) *TermQuery[T] {
q.params = v
return q
}
func (q *TermQuery[T]) Value(v T) *TermQuery[T] {
q.value = v
return q
}
func (q *TermQuery[T]) Map() (map[string]any, error) {
base, err := newBase(q.params)
if err != nil {
return nil, err
}
applyValue(base, "value", q.value)
if isEmpty(base) {
return nil, nil
}
return map[string]any{
"term": map[string]any{
q.field: base,
},
}, nil
}
func (q *TermQuery[T]) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
@@ -0,0 +1,55 @@
package osu_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/osu"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/test"
)
func TestTermQuery(t *testing.T) {
tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
{
Name: "empty",
Got: osu.NewTermQuery[string]("empty"),
Want: nil,
},
{
Name: "no params",
Got: osu.NewTermQuery[bool]("deleted").Value(false),
Want: map[string]any{
"term": map[string]any{
"deleted": map[string]any{
"value": false,
},
},
},
},
{
Name: "with params",
Got: osu.NewTermQuery[bool]("deleted").Params(&osu.TermQueryParams{
Boost: 1.0,
CaseInsensitive: true,
Name: "is-deleted",
}).Value(true),
Want: map[string]any{
"term": map[string]any{
"deleted": map[string]any{
"value": true,
"boost": 1.0,
"case_insensitive": true,
"_name": "is-deleted",
},
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
})
}
}
@@ -0,0 +1,58 @@
package osu
import (
"encoding/json"
)
type WildcardQuery struct {
field string
value string
params *WildcardQueryParams
}
type WildcardQueryParams struct {
Boost float32 `json:"boost,omitempty"`
CaseInsensitive bool `json:"case_insensitive,omitempty"`
Rewrite string `json:"rewrite,omitempty"`
}
func NewWildcardQuery(field string) *WildcardQuery {
return &WildcardQuery{field: field}
}
func (q *WildcardQuery) Params(v *WildcardQueryParams) *WildcardQuery {
q.params = v
return q
}
func (q *WildcardQuery) Value(v string) *WildcardQuery {
q.value = v
return q
}
func (q *WildcardQuery) Map() (map[string]any, error) {
base, err := newBase(q.params)
if err != nil {
return nil, err
}
applyValue(base, "value", q.value)
if isEmpty(base) {
return nil, nil
}
return map[string]any{
"wildcard": map[string]any{
q.field: base,
},
}, nil
}
func (q *WildcardQuery) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
@@ -0,0 +1,44 @@
package osu_test
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/osu"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/test"
)
func TestWildcardQuery(t *testing.T) {
tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
{
Name: "empty",
Got: osu.NewWildcardQuery("empty"),
Want: nil,
},
{
Name: "wildcard",
Got: osu.NewWildcardQuery("name").Params(&osu.WildcardQueryParams{
Boost: 1.0,
CaseInsensitive: true,
Rewrite: "top_terms_blended_freqs_N",
}).Value("opencl*"),
Want: map[string]any{
"wildcard": map[string]any{
"name": map[string]any{
"value": "opencl*",
"boost": 1.0,
"case_insensitive": true,
"rewrite": "top_terms_blended_freqs_N",
},
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
})
}
}
@@ -0,0 +1,135 @@
package osu
import (
"bytes"
"encoding/json"
opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
)
type QueryReqBody[P any] struct {
query Builder
params P
}
func NewQueryReqBody[P any](q Builder, p ...P) *QueryReqBody[P] {
return &QueryReqBody[P]{query: q, params: merge(p...)}
}
func (q QueryReqBody[O]) Map() (map[string]any, error) {
base, err := newBase(q.params)
if err != nil {
return nil, err
}
if err := applyBuilder(base, "query", q.query); err != nil {
return nil, err
}
return base, nil
}
func (q QueryReqBody[O]) MarshalJSON() ([]byte, error) {
data, err := q.Map()
if err != nil {
return nil, err
}
return json.Marshal(data)
}
//----------------------------------------------------------------------------//
type BodyParamHighlight struct {
HighlightOptions
Fields map[string]HighlightOptions `json:"fields,omitempty"`
}
type HighlightType string
const (
HighlightTypeUnified HighlightType = "unified"
HighlightTypeFvh HighlightType = "fvh"
HighlightTypePlain HighlightType = "plain"
HighlightTypeSemantic HighlightType = "semantic"
)
type HighlightOptions struct {
Type HighlightType `json:"type,omitempty"`
FragmentSize int `json:"fragment_size,omitempty"`
NumberOfFragments int `json:"number_of_fragments,omitempty"`
FragmentOffset int `json:"fragment_offset,omitempty"`
BoundaryChars string `json:"boundary_chars,omitempty"`
BoundaryMaxScan int `json:"boundary_max_scan,omitempty"`
BoundaryScanner string `json:"boundary_scanner,omitempty"`
BoundaryScannerLocale string `json:"boundary_scanner_locale,omitempty"`
Encoder string `json:"encoder,omitempty"`
ForceSource bool `json:"force_source,omitempty"`
Fragmenter string `json:"fragmenter,omitempty"`
HighlightQuery Builder `json:"highlight_query,omitempty"`
Order string `json:"order,omitempty"`
NoMatchSize int `json:"no_match_size,omitempty"`
RequireFieldMatch bool `json:"require_field_match,omitempty"`
MatchedFields []string `json:"matched_fields,omitempty"`
PhraseLimit int `json:"phrase_limit,omitempty"`
PreTags []string `json:"pre_tags,omitempty"`
PostTags []string `json:"post_tags,omitempty"`
}
type BodyParamScript struct {
Source string `json:"source,omitempty"`
Lang string `json:"lang,omitempty"`
Params map[string]any `json:"params,omitempty"`
}
//----------------------------------------------------------------------------//
func BuildSearchReq(req *opensearchgoAPI.SearchReq, q Builder, p ...SearchBodyParams) (*opensearchgoAPI.SearchReq, error) {
body, err := json.Marshal(NewQueryReqBody(q, p...))
if err != nil {
return nil, err
}
req.Body = bytes.NewReader(body)
return req, nil
}
type SearchBodyParams struct {
Highlight *BodyParamHighlight `json:"highlight,omitempty"`
}
//----------------------------------------------------------------------------//
func BuildDocumentDeleteByQueryReq(req opensearchgoAPI.DocumentDeleteByQueryReq, q Builder) (opensearchgoAPI.DocumentDeleteByQueryReq, error) {
body, err := json.Marshal(NewQueryReqBody[any](q))
if err != nil {
return req, err
}
req.Body = bytes.NewReader(body)
return req, nil
}
//----------------------------------------------------------------------------//
func BuildUpdateByQueryReq(req opensearchgoAPI.UpdateByQueryReq, q Builder, o ...UpdateByQueryBodyParams) (opensearchgoAPI.UpdateByQueryReq, error) {
body, err := json.Marshal(NewQueryReqBody(q, o...))
if err != nil {
return req, err
}
req.Body = bytes.NewReader(body)
return req, nil
}
type UpdateByQueryBodyParams struct {
Script *BodyParamScript `json:"script,omitempty"`
}
//----------------------------------------------------------------------------//
func BuildIndicesCountReq(req *opensearchgoAPI.IndicesCountReq, q Builder) (*opensearchgoAPI.IndicesCountReq, error) {
body, err := json.Marshal(NewQueryReqBody[any](q))
if err != nil {
return nil, err
}
req.Body = bytes.NewReader(body)
return req, nil
}
@@ -0,0 +1,94 @@
package osu_test
import (
"io"
"testing"
opensearchgoAPI "github.com/opensearch-project/opensearch-go/v4/opensearchapi"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/qsfera/server/services/search/pkg/opensearch/internal/osu"
opensearchtest "github.com/qsfera/server/services/search/pkg/opensearch/internal/test"
)
func TestRequestBody(t *testing.T) {
tests := []opensearchtest.TableTest[osu.Builder, map[string]any]{
{
Name: "simple",
Got: osu.NewQueryReqBody[any](osu.NewTermQuery[string]("name").Value("tom")),
Want: map[string]any{
"query": map[string]any{
"term": map[string]any{
"name": map[string]any{
"value": "tom",
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), opensearchtest.JSONMustMarshal(t, test.Got))
})
}
}
func TestBuildSearchReq(t *testing.T) {
tests := []opensearchtest.TableTest[io.Reader, map[string]any]{
{
Name: "highlight",
Got: func() io.Reader {
req, _ := osu.BuildSearchReq(
&opensearchgoAPI.SearchReq{},
osu.NewTermQuery[string]("content").Value("content"),
osu.SearchBodyParams{
Highlight: &osu.BodyParamHighlight{
HighlightOptions: osu.HighlightOptions{
PreTags: []string{"<b>"},
PostTags: []string{"</b>"},
},
Fields: map[string]osu.HighlightOptions{
"content": {
PreTags: []string{"<strong>"},
PostTags: []string{"</strong>"},
},
},
},
},
)
return req.Body
}(),
Want: map[string]any{
"query": map[string]any{
"term": map[string]any{
"content": map[string]any{
"value": "content",
},
},
},
"highlight": map[string]any{
"pre_tags": []string{"<b>"},
"post_tags": []string{"</b>"},
"fields": map[string]any{
"content": map[string]any{
"pre_tags": []string{"<strong>"},
"post_tags": []string{"</strong>"},
},
},
},
},
},
}
for _, test := range tests {
t.Run(test.Name, func(t *testing.T) {
body, err := io.ReadAll(test.Got)
require.NoError(t, err)
assert.JSONEq(t, opensearchtest.JSONMustMarshal(t, test.Want), string(body))
})
}
}