Files
opencloud/services/search/pkg/aggregation/validate_test.go
T
Dominik Schmidt b2d8a13c24 feat(search): add range aggregations
Numeric and date ranges on both engines: bleve folds them from doc values, OpenSearch uses range and date_range aggregations. The proto gains BucketRange and bucketDefinition.ranges, the aggregation package the range parser (bounds are numbers or RFC3339 dates, one kind per aggregation, anything else is rejected), the kind of an option and the validation of ranges against the field type. Every requested range is answered in request order, an empty one with a count of zero, with no space answering too; size does not cut ranges, keyAsNumber sorts them by their lower bound. Range keys read from..to. AGG-04, 05 and 07 to 09 in the parity suite.
2026-10-06 10:50:28 +02:00

57 lines
2.8 KiB
Go

package aggregation_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/search/pkg/aggregation"
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
)
// fieldTypes stands in for the index mapping.
func fieldTypes(field string) string {
return map[string]string{
"audio.artist": mapping.TypeKeyword,
"audio.album": mapping.TypeKeyword,
"audio.hasDrm": mapping.TypeBool,
"audio.year": mapping.TypeNumeric,
"photo.takenDateTime": mapping.TypeDatetime,
"Content": mapping.TypeFulltext,
"Path": mapping.TypePath,
"audio": mapping.TypeObject,
"location": mapping.TypeGeopoint,
}[field]
}
var _ = Describe("ValidateOptions", func() {
numeric := &searchsvc.BucketDefinition{Ranges: []*searchsvc.BucketRange{{From: "1980", To: "1990"}}}
dates := &searchsvc.BucketDefinition{Ranges: []*searchsvc.BucketRange{{From: "2018-08-11T00:00:00Z"}}}
DescribeTable("accepts an aggregation that fits its field",
func(opt *searchsvc.AggregationOption) {
Expect(aggregation.ValidateOptions([]*searchsvc.AggregationOption{opt}, fieldTypes)).To(Succeed())
},
Entry("terms on a keyword", &searchsvc.AggregationOption{Field: "audio.artist"}),
Entry("terms on a bool", &searchsvc.AggregationOption{Field: "audio.hasDrm"}),
Entry("terms on a number", &searchsvc.AggregationOption{Field: "audio.year"}),
Entry("numeric ranges on a number", &searchsvc.AggregationOption{Field: "audio.year", BucketDefinition: numeric}),
Entry("date ranges on a date", &searchsvc.AggregationOption{Field: "photo.takenDateTime", BucketDefinition: dates}),
)
DescribeTable("rejects an aggregation the index cannot answer",
func(opt *searchsvc.AggregationOption) {
Expect(aggregation.ValidateOptions([]*searchsvc.AggregationOption{opt}, fieldTypes)).ToNot(Succeed())
},
Entry("an unknown field", &searchsvc.AggregationOption{Field: "audio.nonexistent"}),
Entry("terms on a date", &searchsvc.AggregationOption{Field: "photo.takenDateTime"}),
Entry("terms on full text", &searchsvc.AggregationOption{Field: "Content"}),
Entry("terms on a path", &searchsvc.AggregationOption{Field: "Path"}),
Entry("terms on a facet object", &searchsvc.AggregationOption{Field: "audio"}),
Entry("terms on a geopoint", &searchsvc.AggregationOption{Field: "location"}),
Entry("numeric ranges on a keyword", &searchsvc.AggregationOption{Field: "audio.artist", BucketDefinition: numeric}),
Entry("numeric ranges on a date", &searchsvc.AggregationOption{Field: "photo.takenDateTime", BucketDefinition: numeric}),
Entry("date ranges on a number", &searchsvc.AggregationOption{Field: "audio.year", BucketDefinition: dates}),
)
})