Skip to content

Commit b2e058c

Browse files
authored
feat: add support for query_string query (#210)
* feat: add support for `query_string` query * feat: add support for sum_bucket aggreation * fix: unit test * feat: add support for date range aggregation * chore: add timezone config for date range aggregation * chore: format code
1 parent a3aefe0 commit b2e058c

File tree

14 files changed

+294
-151
lines changed

14 files changed

+294
-151
lines changed

core/elastic/search.go

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -24,8 +24,9 @@
2424
package elastic
2525

2626
import (
27-
"infini.sh/framework/core/util"
2827
"time"
28+
29+
"infini.sh/framework/core/util"
2930
)
3031

3132
type SearchTemplate struct {
@@ -142,17 +143,23 @@ func BuildSearchTermFilter(filterParam SearchFilterParam) []util.MapStr {
142143

143144
func GetDateHistogramIntervalField(distribution, version string, bucketSize string) (string, error) {
144145
if distribution == Easysearch || distribution == Opensearch {
145-
return "interval", nil
146+
return Interval, nil
146147
}
147148
cr, err := util.VersionCompare(version, "8.0")
148149
if err != nil {
149150
return "", err
150151
}
151152
if cr > -1 {
152153
if util.StringInArray([]string{"1w", "week", "1M", "month", "1q", "quarter", "1y", "year"}, bucketSize) {
153-
return "calendar_interval", nil
154+
return CalendarInterval, nil
154155
}
155-
return "fixed_interval", nil
156+
return FixedInterval, nil
156157
}
157-
return "interval", nil
158+
return Interval, nil
158159
}
160+
161+
const (
162+
Interval string = "interval"
163+
CalendarInterval = "calendar_interval"
164+
FixedInterval = "fixed_interval"
165+
)

core/orm/aggs.go

Lines changed: 55 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -38,21 +38,23 @@ type Aggregation interface {
3838

3939
const (
4040
// Metric types
41-
MetricAvg = "avg"
42-
MetricSum = "sum"
43-
MetricMin = "min"
44-
MetricMax = "max"
45-
MetricCount = "count"
41+
MetricAvg = "avg"
42+
MetricSum = "sum"
43+
MetricMin = "min"
44+
MetricMax = "max"
45+
MetricCount = "count"
4646
MetricPercentiles = "percentiles"
47-
MetricTopHits = "top_hits"
47+
MetricTopHits = "top_hits"
4848
MetricCardinality = "cardinality"
49-
MetricMedian = "median_absolute_deviation"
49+
MetricMedian = "median_absolute_deviation"
5050
// Bucket types
5151
MetricBucketTerms = "terms"
5252
MetricBucketDateHistogram = "date_histogram"
53-
MetricBucketFilter = "filter"
53+
MetricBucketFilter = "filter"
54+
MetricDateRange = "date_range"
5455
// Pipeline types
5556
MetricPipelineDerivative = "derivative"
57+
MetricSumBucket = "sum_bucket"
5658
)
5759

5860
// baseAggregation provides common functionality for all aggregation types,
@@ -61,7 +63,7 @@ type baseAggregation struct {
6163
// NestedAggs holds any sub-aggregations.
6264
NestedAggs map[string]Aggregation `json:"-"`
6365
// Params can hold additional parameters specific to certain aggregation types.
64-
Params map[string]interface{} `json:"-"`
66+
Params map[string]interface{} `json:"-"`
6567
}
6668

6769
// AddNested adds a sub-aggregation to the base aggregation.
@@ -105,9 +107,9 @@ func (a *TermsAggregation) AddNested(name string, sub Aggregation) Aggregation {
105107

106108
// MetricAggregation represents a single-value metric calculation (avg, sum, etc.).
107109
type MetricAggregation struct {
108-
baseAggregation // Although metrics rarely have sub-aggs in ES, the model allows it.
109-
Type string `mapstructure:"-"` // Type of metric: "avg", "sum", etc. Not part of the decoded structure.
110-
Field string
110+
baseAggregation // Although metrics rarely have sub-aggs in ES, the model allows it.
111+
Type string `mapstructure:"-"` // Type of metric: "avg", "sum", etc. Not part of the decoded structure.
112+
Field string
111113
}
112114

113115
// NewMetricAggregation creates a new MetricAggregation of the specified type and field.
@@ -124,6 +126,27 @@ func NewMetricAggregation(metricType, field string) *MetricAggregation {
124126
}
125127
}
126128

129+
// PipelineAggregation represents a pipeline aggregation that processes the output of other aggregations.
130+
type PipelineAggregation struct {
131+
baseAggregation
132+
Type string `mapstructure:"-"` // Type of pipeline: "derivative", "sum_bucket", etc. Not part of the decoded structure.
133+
BucketsPath string
134+
}
135+
136+
// NewPipelineAggregation creates a new PipelineAggregation of the specified type and buckets path.
137+
func NewPipelineAggregation(pipelineType, bucketsPath string) *PipelineAggregation {
138+
switch pipelineType {
139+
case MetricSumBucket:
140+
// Valid pipeline types
141+
default:
142+
panic("invalid pipeline type: " + pipelineType)
143+
}
144+
return &PipelineAggregation{
145+
Type: pipelineType,
146+
BucketsPath: bucketsPath,
147+
}
148+
}
149+
127150
// AddNested provides a correctly typed chained call for MetricAggregation.
128151
func (a *MetricAggregation) AddNested(name string, sub Aggregation) Aggregation {
129152
a.baseAggregation.AddNested(name, sub)
@@ -133,10 +156,11 @@ func (a *MetricAggregation) AddNested(name string, sub Aggregation) Aggregation
133156
// DateHistogramAggregation represents bucketing documents by a date/time interval.
134157
type DateHistogramAggregation struct {
135158
baseAggregation
136-
Field string
137-
Interval string // A generic interval string like "1d", "1M", "1h".
138-
Format string
139-
TimeZone string
159+
Field string
160+
Interval string // A generic interval string like "1d", "1M", "1h".
161+
Format string
162+
TimeZone string
163+
IntervalField string // es-specific field name for backward compatibility
140164
}
141165

142166
// AddNested provides a correctly typed chained call for DateHistogramAggregation.
@@ -174,8 +198,23 @@ type FilterAggregation struct {
174198
// Query holds the filter criteria for this aggregation.
175199
Query map[string]interface{} `json:"query"`
176200
}
201+
177202
// AddNested provides a correctly typed chained call for FilterAggregation.
178203
func (a *FilterAggregation) AddNested(name string, sub Aggregation) Aggregation {
179204
a.baseAggregation.AddNested(name, sub)
180205
return a
181206
}
207+
208+
type DateRangeAggregation struct {
209+
baseAggregation
210+
Field string `json:"field"`
211+
TimeZone string `json:"time_zone,omitempty"`
212+
Format string `json:"format,omitempty"`
213+
Ranges []interface{} `json:"ranges"`
214+
}
215+
216+
// AddNested provides a correctly typed chained call for DateRangeAggregation.
217+
func (a *DateRangeAggregation) AddNested(name string, sub Aggregation) Aggregation {
218+
a.baseAggregation.AddNested(name, sub)
219+
return a
220+
}

core/orm/aggs_args_parser.go

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
// keyPartRegex is used to parse keys like "agg[key1][key2]" into parts: "agg", "key1", "key2".
1515
var keyPartRegex = regexp.MustCompile(`^([^\[\]]+)|\[([^\[\]]*)\]`)
1616

17-
1817
// ParseAggregationsFromQuery takes URL query values and converts them into a map of abstract aggregations.
1918
func ParseAggregationsFromQuery(values url.Values) (map[string]Aggregation, error) {
2019
// Step 1: Convert flat URL params into a nested map.
@@ -47,15 +46,15 @@ func parseToNestedMap(values url.Values) (map[string]interface{}, error) {
4746
} else {
4847
part = match[2]
4948
}
50-
49+
5150
decodedPart, err := url.QueryUnescape(part)
5251
if err != nil {
5352
// Fallback to the raw part if decoding fails
5453
decodedPart = part
5554
}
5655
parts = append(parts, decodedPart)
5756
}
58-
57+
5958
if len(parts) == 0 || parts[0] != "agg" {
6059
continue // Malformed key, skip.
6160
}
@@ -115,7 +114,7 @@ func stringToNumberHook() mapstructure.DecodeHookFunc {
115114
if !isNumeric {
116115
return data, nil
117116
}
118-
117+
119118
str := data.(string)
120119

121120
// Try to parse as integer first.

core/orm/aggs_args_parser_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ func TestParseAggregationsFromQuery_SingleTerms(t *testing.T) {
1010
// Arrange
1111
rawURL := "http://example.com?agg[types][terms][field]=product.keyword&agg[types][terms][size]=5"
1212
parsedURL, _ := url.Parse(rawURL)
13-
13+
1414
// Expected abstract structure
1515
expected := map[string]Aggregation{
1616
"types": &TermsAggregation{
@@ -142,4 +142,4 @@ func TestParseAggregationsFromQuery_URLEncoded(t *testing.T) {
142142
if !reflect.DeepEqual(result, expected) {
143143
t.Errorf("Resulting aggregation map does not match expected.\nGot: %#v\nWant: %#v", result, expected)
144144
}
145-
}
145+
}

core/orm/aggs_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ func TestTermsAggregation(t *testing.T) {
3333
// Create a terms aggregation
3434
termsAgg := TermsAggregation{
3535
Field: "field1",
36-
Size: 10,
36+
Size: 10,
3737
}
3838
termsAgg.AddNested("count_values", NewMetricAggregation(MetricCount, "field2")).AddNested("max_value", NewMetricAggregation(MetricMax, "field2"))
3939
assert.Equal(t, 2, len(termsAgg.GetNested()), "Expected one nested aggregation")
@@ -55,13 +55,13 @@ func TestDateHistogramAggregation(t *testing.T) {
5555
func TestAggregationWithParams(t *testing.T) {
5656
termsAgg := TermsAggregation{
5757
Field: "field1",
58-
Size: 10,
58+
Size: 10,
5959
}
6060
params := map[string]interface{}{
61-
"order": map[string]string{
62-
"_count": "desc",
63-
},
64-
}
61+
"order": map[string]string{
62+
"_count": "desc",
63+
},
64+
}
6565
termsAgg.SetParams(params)
6666
assert.Equal(t, "desc", termsAgg.Params["order"].(map[string]string)["_count"], "Expected order param to be set")
6767
}

core/orm/query.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ const (
5151
QueryIn QueryType = "in"
5252
QueryNotIn QueryType = "not_in"
5353
QueryMatchPhrase QueryType = "match_phrase"
54+
QueryQueryString QueryType = "query_string"
5455
)
5556

5657
type Clause struct {
@@ -113,7 +114,7 @@ type QueryBuilder struct {
113114
filters []*Clause
114115

115116
requestBodyBytes []byte
116-
Aggs map[string]Aggregation
117+
Aggs map[string]Aggregation
117118
}
118119

119120
func NewQuery() *QueryBuilder {
@@ -125,6 +126,7 @@ func NewQuery() *QueryBuilder {
125126
func (q *QueryBuilder) SetRequestBodyBytes(bytes []byte) {
126127
q.requestBodyBytes = bytes
127128
}
129+
128130
// SetAggregations sets the aggregations for the query builder.
129131
func (q *QueryBuilder) SetAggregations(aggs map[string]Aggregation) {
130132
q.Aggs = aggs
@@ -310,6 +312,7 @@ func RegexpQuery(field string, value interface{}) *Clause {
310312

311313
const fuzzyFuzziness = "fuzziness"
312314
const phraseSlop = "slop"
315+
const queryStringDefaultOperator = "default_operator"
313316

314317
func FuzzyQuery(field string, value interface{}, fuzziness int) *Clause {
315318
param := param.Parameters{}
@@ -335,6 +338,14 @@ func MatchPhraseQuery(field, value string, slop int) *Clause {
335338
return newLeaf(field, QueryMatchPhrase, value, &param)
336339
}
337340

341+
func QueryStringQuery(field string, value string, defaultOperator string) *Clause {
342+
param := param.Parameters{}
343+
if defaultOperator != "" {
344+
param.Set(queryStringDefaultOperator, defaultOperator)
345+
}
346+
return newLeaf(field, QueryQueryString, value, &param)
347+
}
348+
338349
type RangeQueryBuilder struct {
339350
field string
340351
}

core/orm/query_test.go

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,11 @@ package orm
2525

2626
import (
2727
"fmt"
28-
"github.com/stretchr/testify/assert"
29-
"infini.sh/framework/core/util"
3028
"reflect"
3129
"testing"
30+
31+
"github.com/stretchr/testify/assert"
32+
"infini.sh/framework/core/util"
3233
)
3334

3435
func TestTermQuery(t *testing.T) {
@@ -190,6 +191,16 @@ func TestRangeLtQuery(t *testing.T) {
190191
assertLeaf(t, q, "score", QueryRangeLt, 10)
191192
}
192193

194+
func TestQueryStringQuery(t *testing.T) {
195+
var (
196+
field = "name,description"
197+
value = "foo bar"
198+
defaultOperator = "OR"
199+
)
200+
q := QueryStringQuery(field, value, defaultOperator)
201+
assertLeaf(t, q, field, QueryQueryString, value)
202+
}
203+
193204
// Helper: check leaf structure
194205
func assertLeaf(t *testing.T, clause *Clause, field string, op QueryType, value interface{}) {
195206
t.Helper()

docs/content.en/docs/release-notes/_index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ Information about release notes of INFINI Framework is provided here.
1313
### 🚀 Features
1414
- feat: add delete by query v2 #194
1515
- feat: support aggregation queries in orm
16-
16+
- feat: add support for `query_string` query
1717
### 🐛 Bug fix
1818
- fix: localhost/127.0.0.1 with noproxy #185
1919
- fix: cluster metadata lost #200

lib/cache/cache_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -237,8 +237,8 @@ func Test_Cache_ReplaceChangesSize(t *testing.T) {
237237
}
238238

239239
func Test_Cache_ResizeOnTheFly(t *testing.T) {
240-
// On a busy system or during a slow run, the cleanup might take longer.
241-
// When this happens, the test continues
240+
// On a busy system or during a slow run, the cleanup might take longer.
241+
// When this happens, the test continues
242242
// and runs its assertions (e.g., assert.Equal(t, cache.GetDropped(), 2))
243243
// before the cache has actually been pruned, causing the test to fail.
244244
if os.Getenv("CI") == "true" {

lib/cache/layeredcache_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,8 @@ func Test_LayeredCache_RemovesOldestItemWhenFull(t *testing.T) {
225225
}
226226

227227
func Test_LayeredCache_ResizeOnTheFly(t *testing.T) {
228-
// On a busy system or during a slow run, the cleanup might take longer.
229-
// When this happens, the test continues
228+
// On a busy system or during a slow run, the cleanup might take longer.
229+
// When this happens, the test continues
230230
// and runs its assertions (e.g., assert.Equal(t, cache.GetDropped(), 2))
231231
// before the cache has actually been pruned, causing the test to fail.
232232
if os.Getenv("CI") == "true" {

0 commit comments

Comments
 (0)