feat(search): add aggregation filter tokens

Server-issued aggregationFilterToken on buckets, consumed verbatim via searchRequest.aggregationFilters. The graph layer decodes a filter into its buckets (a term, a range, or an or(...) of them), the proto carries them as AggregationFilter, and the engines match them natively: a key as the exact value of the field, a range from-inclusive and to-exclusive, so a drilldown returns exactly the matches the bucket counted. Filters are validated like aggregations, in both layers. Range tokens use the MS Graph spelling (range(min, 1980), range(2010, max, to="le")). AGG-22 to AGG-33, 36, 38 and 45 in the parity suite.
This commit is contained in:
Dominik Schmidt committed 2026-10-05 12:11:56 +02:00
1 parent 993951430b
commit f14a92d67a
20 files changed
+1120 -237

No files matched your search

@@ -146,6 +146,9 @@ type SearchRequest struct {
Ref *v0.Reference `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"`
// Optional. Aggregations (facets) to compute alongside the matches.
Aggregations []*AggregationOption `protobuf:"bytes,5,rep,name=aggregations,proto3" json:"aggregations,omitempty"`
// Optional. Filters narrowing the matches to buckets of a prior
// aggregation. Combined with `query` and with each other via AND.
AggregationFilters []*AggregationFilter `protobuf:"bytes,6,rep,name=aggregation_filters,json=aggregationFilters,proto3" json:"aggregation_filters,omitempty"`
// Optional. Number of leading matches to skip in the globally sorted,
// cross-space merged result list.
From int32 `protobuf:"varint,7,opt,name=from,proto3" json:"from,omitempty"`
@@ -218,6 +221,13 @@ func (x *SearchRequest) GetAggregations() []*AggregationOption {
return nil
}
func (x *SearchRequest) GetAggregationFilters() []*AggregationFilter {
if x != nil {
return x.AggregationFilters
}
return nil
}
func (x *SearchRequest) GetFrom() int32 {
if x != nil {
return x.From
@@ -314,6 +324,9 @@ type SearchIndexRequest struct {
Ref *v0.Reference `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"`
// Optional. Per-space aggregations (facets) to compute alongside the matches.
Aggregations []*AggregationOption `protobuf:"bytes,5,rep,name=aggregations,proto3" json:"aggregations,omitempty"`
// Optional. Filters narrowing the matches to buckets of a prior
// aggregation; the engine ANDs them with `query`.
AggregationFilters []*AggregationFilter `protobuf:"bytes,6,rep,name=aggregation_filters,json=aggregationFilters,proto3" json:"aggregation_filters,omitempty"`
}
func (x *SearchIndexRequest) Reset() {
@@ -383,6 +396,13 @@ func (x *SearchIndexRequest) GetAggregations() []*AggregationOption {
return nil
}
func (x *SearchIndexRequest) GetAggregationFilters() []*AggregationFilter {
if x != nil {
return x.AggregationFilters
}
return nil
}
type SearchIndexResponse struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
@@ -730,6 +750,73 @@ func (x *BucketRange) GetTo() string {
return ""
}
// AggregationFilter narrows the matches to buckets of one field. A match has
// to be in any of the listed buckets.
type AggregationFilter struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"`
// Bucket keys of a terms aggregation, matched exactly and case-sensitively.
Terms []string `protobuf:"bytes,2,rep,name=terms,proto3" json:"terms,omitempty"`
// Buckets of a range aggregation.
Ranges []*BucketRange `protobuf:"bytes,3,rep,name=ranges,proto3" json:"ranges,omitempty"`
}
func (x *AggregationFilter) Reset() {
*x = AggregationFilter{}
if protoimpl.UnsafeEnabled {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[8]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
}
func (x *AggregationFilter) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*AggregationFilter) ProtoMessage() {}
func (x *AggregationFilter) ProtoReflect() protoreflect.Message {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[8]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use AggregationFilter.ProtoReflect.Descriptor instead.
func (*AggregationFilter) Descriptor() ([]byte, []int) {
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{8}
}
func (x *AggregationFilter) GetField() string {
if x != nil {
return x.Field
}
return ""
}
func (x *AggregationFilter) GetTerms() []string {
if x != nil {
return x.Terms
}
return nil
}
func (x *AggregationFilter) GetRanges() []*BucketRange {
if x != nil {
return x.Ranges
}
return nil
}
// One result per AggregationOption, at the same position.
type AggregationResult struct {
state protoimpl.MessageState
@@ -746,7 +833,7 @@ type AggregationResult struct {
func (x *AggregationResult) Reset() {
*x = AggregationResult{}
if protoimpl.UnsafeEnabled {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[8]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[9]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -759,7 +846,7 @@ func (x *AggregationResult) String() string {
func (*AggregationResult) ProtoMessage() {}
func (x *AggregationResult) ProtoReflect() protoreflect.Message {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[8]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[9]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -772,7 +859,7 @@ func (x *AggregationResult) ProtoReflect() protoreflect.Message {
// Deprecated: Use AggregationResult.ProtoReflect.Descriptor instead.
func (*AggregationResult) Descriptor() ([]byte, []int) {
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{8}
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{9}
}
func (x *AggregationResult) GetField() string {
@@ -818,7 +905,7 @@ type Metric struct {
func (x *Metric) Reset() {
*x = Metric{}
if protoimpl.UnsafeEnabled {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[9]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[10]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -831,7 +918,7 @@ func (x *Metric) String() string {
func (*Metric) ProtoMessage() {}
func (x *Metric) ProtoReflect() protoreflect.Message {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[9]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[10]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -844,7 +931,7 @@ func (x *Metric) ProtoReflect() protoreflect.Message {
// Deprecated: Use Metric.ProtoReflect.Descriptor instead.
func (*Metric) Descriptor() ([]byte, []int) {
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{9}
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{10}
}
func (x *Metric) GetKind() MetricKind {
@@ -904,7 +991,7 @@ type Bucket struct {
func (x *Bucket) Reset() {
*x = Bucket{}
if protoimpl.UnsafeEnabled {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[10]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[11]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -917,7 +1004,7 @@ func (x *Bucket) String() string {
func (*Bucket) ProtoMessage() {}
func (x *Bucket) ProtoReflect() protoreflect.Message {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[10]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[11]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -930,7 +1017,7 @@ func (x *Bucket) ProtoReflect() protoreflect.Message {
// Deprecated: Use Bucket.ProtoReflect.Descriptor instead.
func (*Bucket) Descriptor() ([]byte, []int) {
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{10}
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{11}
}
func (x *Bucket) GetKey() string {
@@ -968,7 +1055,7 @@ type IndexSpaceRequest struct {
func (x *IndexSpaceRequest) Reset() {
*x = IndexSpaceRequest{}
if protoimpl.UnsafeEnabled {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[11]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[12]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -981,7 +1068,7 @@ func (x *IndexSpaceRequest) String() string {
func (*IndexSpaceRequest) ProtoMessage() {}
func (x *IndexSpaceRequest) ProtoReflect() protoreflect.Message {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[11]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[12]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -994,7 +1081,7 @@ func (x *IndexSpaceRequest) ProtoReflect() protoreflect.Message {
// Deprecated: Use IndexSpaceRequest.ProtoReflect.Descriptor instead.
func (*IndexSpaceRequest) Descriptor() ([]byte, []int) {
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{11}
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{12}
}
func (x *IndexSpaceRequest) GetSpaceId() string {
@@ -1045,7 +1132,7 @@ type IndexSpaceResponse struct {
func (x *IndexSpaceResponse) Reset() {
*x = IndexSpaceResponse{}
if protoimpl.UnsafeEnabled {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[12]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[13]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
@@ -1058,7 +1145,7 @@ func (x *IndexSpaceResponse) String() string {
func (*IndexSpaceResponse) ProtoMessage() {}
func (x *IndexSpaceResponse) ProtoReflect() protoreflect.Message {
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[12]
mi := &file_opencloud_services_search_v0_search_proto_msgTypes[13]
if protoimpl.UnsafeEnabled && x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
@@ -1071,7 +1158,7 @@ func (x *IndexSpaceResponse) ProtoReflect() protoreflect.Message {
// Deprecated: Use IndexSpaceResponse.ProtoReflect.Descriptor instead.
func (*IndexSpaceResponse) Descriptor() ([]byte, []int) {
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{12}
return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{13}
}
func (x *IndexSpaceResponse) GetSpaceId() string {
@@ -1130,7 +1217,7 @@ var file_opencloud_services_search_v0_search_proto_rawDesc = []byte{
0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x1e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2e,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb6, 0x02, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x9e, 0x03, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f,
0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01,
0x48, 0x00, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x88, 0x01, 0x01, 0x12,
@@ -1147,42 +1234,55 @@ var file_opencloud_services_search_v0_search_proto_rawDesc = []byte{
0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30,
0x2e, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69,
0x6f, 0x6e, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x0c, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x18, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x07,
0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d,
0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0xf1,
0x01, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
0x65, 0x12, 0x3d, 0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03,
0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d,
0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76,
0x30, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73,
0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f,
0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50,
0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x74, 0x61,
0x6c, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52,
0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x53, 0x0a,
0x0c, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x66, 0x0a, 0x13, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e,
0x76, 0x30, 0x2e, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
0x73, 0x75, 0x6c, 0x74, 0x52, 0x0c, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x73, 0x22, 0xa1, 0x02, 0x0a, 0x12, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64,
0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x61, 0x67,
0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41,
0x01, 0x01, 0x48, 0x00, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x88, 0x01,
0x01, 0x12, 0x23, 0x0a, 0x0a, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x09, 0x70, 0x61, 0x67,
0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18,
0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x12, 0x3f, 0x0a, 0x03,
0x72, 0x65, 0x66, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e,
0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73,
0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e,
0x63, 0x65, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x03, 0x72, 0x65, 0x66, 0x12, 0x59, 0x0a,
0x0c, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20,
0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e,
0x76, 0x30, 0x2e, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70,
0x74, 0x69, 0x6f, 0x6e, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x0c, 0x61, 0x67, 0x67, 0x72,
0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x61, 0x67,
0x76, 0x30, 0x2e, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69,
0x6c, 0x74, 0x65, 0x72, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x12, 0x61, 0x67, 0x67, 0x72,
0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x12, 0x18,
0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41,
0x01, 0x01, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x61, 0x67,
0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0xf1, 0x01, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63,
0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d, 0x0a, 0x07, 0x6d, 0x61, 0x74,
0x63, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x23, 0x2e, 0x6f, 0x70, 0x65,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e,
0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x52,
0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x6e, 0x65, 0x78, 0x74,
0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x0d, 0x6e, 0x65, 0x78, 0x74, 0x50, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e,
0x12, 0x23, 0x0a, 0x0d, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65,
0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x4d, 0x61,
0x74, 0x63, 0x68, 0x65, 0x73, 0x12, 0x53, 0x0a, 0x0c, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x6f, 0x70,
0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73,
0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x67, 0x67, 0x72, 0x65,
0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x0c, 0x61, 0x67,
0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x89, 0x03, 0x0a, 0x12, 0x53,
0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
0x74, 0x12, 0x26, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01,
0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x48, 0x00, 0x52, 0x08, 0x70, 0x61,
0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0a, 0x70, 0x61, 0x67,
0x65, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x42, 0x04, 0xe2,
0x41, 0x01, 0x01, 0x52, 0x09, 0x70, 0x61, 0x67, 0x65, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x14,
0x0a, 0x05, 0x71, 0x75, 0x65, 0x72, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x71,
0x75, 0x65, 0x72, 0x79, 0x12, 0x3f, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x04, 0x20, 0x01, 0x28,
0x0b, 0x32, 0x27, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x6d, 0x65,
0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30,
0x2e, 0x52, 0x65, 0x66, 0x65, 0x72, 0x65, 0x6e, 0x63, 0x65, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01,
0x52, 0x03, 0x72, 0x65, 0x66, 0x12, 0x59, 0x0a, 0x0c, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61,
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x6f, 0x70,
0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73,
0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x67, 0x67, 0x72, 0x65,
0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x04, 0xe2, 0x41,
0x01, 0x01, 0x52, 0x0c, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
0x12, 0x66, 0x0a, 0x13, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f,
0x66, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e,
0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x67, 0x67,
0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x42, 0x04,
0xe2, 0x41, 0x01, 0x01, 0x52, 0x12, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f,
0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x73, 0x42, 0x0c, 0x0a, 0x0a, 0x5f, 0x70, 0x61, 0x67,
0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x22, 0xf6, 0x01, 0x0a, 0x13, 0x53, 0x65, 0x61, 0x72, 0x63,
0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x3d,
0x0a, 0x07, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32,
@@ -1244,132 +1344,140 @@ var file_opencloud_services_search_v0_search_proto_rawDesc = []byte{
0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0x31, 0x0a, 0x0b, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74,
0x52, 0x61, 0x6e, 0x67, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x0e, 0x0a, 0x02, 0x74, 0x6f, 0x18,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x22, 0xa7, 0x01, 0x0a, 0x11, 0x41, 0x67,
0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x12,
0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x74, 0x6f, 0x22, 0x82, 0x01, 0x0a, 0x11, 0x41, 0x67,
0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12,
0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3e, 0x0a, 0x07, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73,
0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72,
0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x07, 0x62, 0x75,
0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x3c, 0x0a, 0x06, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x18,
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75,
0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63,
0x68, 0x2e, 0x76, 0x30, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x52, 0x06, 0x6d, 0x65, 0x74,
0x72, 0x69, 0x63, 0x22, 0xb7, 0x01, 0x0a, 0x06, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x12, 0x3c,
0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x28, 0x2e, 0x6f,
0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65,
0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x4d, 0x65, 0x74, 0x72,
0x69, 0x63, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x12, 0x19, 0x0a, 0x05,
0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01, 0x48, 0x00, 0x52, 0x05, 0x76,
0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, 0x03,
0x20, 0x01, 0x28, 0x01, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75,
0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12,
0x10, 0x0a, 0x03, 0x6d, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28, 0x01, 0x52, 0x03, 0x6d, 0x69,
0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x78, 0x18, 0x06, 0x20, 0x01, 0x28, 0x01, 0x52, 0x03,
0x6d, 0x61, 0x78, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x22, 0x8c, 0x01,
0x0a, 0x06, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79, 0x18,
0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f,
0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74,
0x12, 0x5a, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x5f, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x6f, 0x70, 0x65,
0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x18, 0x02,
0x20, 0x03, 0x28, 0x09, 0x52, 0x05, 0x74, 0x65, 0x72, 0x6d, 0x73, 0x12, 0x41, 0x0a, 0x06, 0x72,
0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x29, 0x2e, 0x6f, 0x70,
0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73,
0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x42, 0x75, 0x63, 0x6b, 0x65,
0x74, 0x52, 0x61, 0x6e, 0x67, 0x65, 0x52, 0x06, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x22, 0xa7,
0x01, 0x0a, 0x11, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65,
0x73, 0x75, 0x6c, 0x74, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20,
0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x3e, 0x0a, 0x07, 0x62, 0x75,
0x63, 0x6b, 0x65, 0x74, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70,
0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73,
0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x42, 0x75, 0x63, 0x6b, 0x65,
0x74, 0x52, 0x07, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x73, 0x12, 0x3c, 0x0a, 0x06, 0x6d, 0x65,
0x74, 0x72, 0x69, 0x63, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 0x2e, 0x6f, 0x70, 0x65,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e,
0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74, 0x52, 0x0f, 0x73, 0x75, 0x62,
0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x94, 0x01, 0x0a,
0x11, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65,
0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01,
0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a,
0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f,
0x72, 0x65, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x66,
0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x26, 0x0a, 0x0b, 0x63,
0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05,
0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65,
0x6e, 0x63, 0x79, 0x22, 0xd1, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61,
0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70,
0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70,
0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x0e, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x64,
0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e,
0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e,
0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x70, 0x61, 0x63, 0x65, 0x44,
0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, 0x64, 0x65, 0x78,
0x65, 0x64, 0x5f, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52,
0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x64, 0x53, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x21,
0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x04,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x70, 0x61, 0x63, 0x65,
0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09,
0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2a, 0x7d, 0x0a, 0x0a, 0x4d, 0x65, 0x74, 0x72, 0x69,
0x63, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x17, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f,
0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44,
0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e,
0x44, 0x5f, 0x53, 0x55, 0x4d, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49,
0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f,
0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x41, 0x58, 0x10,
0x03, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44,
0x5f, 0x41, 0x56, 0x47, 0x10, 0x04, 0x2a, 0x8c, 0x01, 0x0a, 0x0c, 0x42, 0x75, 0x63, 0x6b, 0x65,
0x74, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x1e, 0x0a, 0x1a, 0x42, 0x55, 0x43, 0x4b, 0x45,
0x54, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x42, 0x59, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43,
0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14, 0x42, 0x55, 0x43, 0x4b, 0x45,
0x54, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x42, 0x59, 0x5f, 0x43, 0x4f, 0x55, 0x4e, 0x54, 0x10,
0x01, 0x12, 0x20, 0x0a, 0x1c, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x53, 0x4f, 0x52, 0x54,
0x5f, 0x42, 0x59, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x53, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x4e,
0x47, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x53, 0x4f,
0x52, 0x54, 0x5f, 0x42, 0x59, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x53, 0x5f, 0x4e, 0x55, 0x4d,
0x42, 0x45, 0x52, 0x10, 0x03, 0x32, 0xb3, 0x02, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x85, 0x01, 0x0a, 0x06, 0x53, 0x65, 0x61,
0x72, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e,
0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e,
0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x2c, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e,
0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x20,
0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, 0x2f, 0x61, 0x70, 0x69, 0x2f,
0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68,
0x12, 0x98, 0x01, 0x0a, 0x0a, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x12,
0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63,
0x52, 0x06, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x22, 0xb7, 0x01, 0x0a, 0x06, 0x4d, 0x65, 0x74,
0x72, 0x69, 0x63, 0x12, 0x3c, 0x0a, 0x04, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x0e, 0x32, 0x28, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65,
0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30,
0x2e, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x69, 0x6e, 0x64, 0x52, 0x04, 0x6b, 0x69, 0x6e,
0x64, 0x12, 0x19, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x01,
0x48, 0x00, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x88, 0x01, 0x01, 0x12, 0x10, 0x0a, 0x03,
0x73, 0x75, 0x6d, 0x18, 0x03, 0x20, 0x01, 0x28, 0x01, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x12, 0x14,
0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63,
0x6f, 0x75, 0x6e, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x69, 0x6e, 0x18, 0x05, 0x20, 0x01, 0x28,
0x01, 0x52, 0x03, 0x6d, 0x69, 0x6e, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x61, 0x78, 0x18, 0x06, 0x20,
0x01, 0x28, 0x01, 0x52, 0x03, 0x6d, 0x61, 0x78, 0x42, 0x08, 0x0a, 0x06, 0x5f, 0x76, 0x61, 0x6c,
0x75, 0x65, 0x22, 0x8c, 0x01, 0x0a, 0x06, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x12, 0x10, 0x0a,
0x03, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12,
0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05,
0x63, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x5a, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x5f, 0x61, 0x67, 0x67,
0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0b, 0x32,
0x2f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x49,
0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74,
0x1a, 0x30, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72,
0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e,
0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x3a, 0x01, 0x2a, 0x22, 0x1a, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x69, 0x6e,
0x64, 0x65, 0x78, 0x2d, 0x73, 0x70, 0x61, 0x63, 0x65, 0x30, 0x01, 0x32, 0xa7, 0x01, 0x0a, 0x0d,
0x49, 0x6e, 0x64, 0x65, 0x78, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x95, 0x01,
0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x30, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63,
0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x41,
0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x75, 0x6c, 0x74,
0x52, 0x0f, 0x73, 0x75, 0x62, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e,
0x73, 0x22, 0x94, 0x01, 0x0a, 0x11, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65,
0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70, 0x61, 0x63, 0x65,
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70, 0x61, 0x63, 0x65,
0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20,
0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66,
0x6f, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01,
0x28, 0x08, 0x52, 0x0c, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x69, 0x6e, 0x64, 0x65, 0x78,
0x12, 0x26, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18,
0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x0b, 0x63, 0x6f, 0x6e,
0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x22, 0xd1, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x64,
0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
0x19, 0x0a, 0x08, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28,
0x09, 0x52, 0x07, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x0e, 0x73, 0x70,
0x61, 0x63, 0x65, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01,
0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73,
0x70, 0x61, 0x63, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e,
0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x64, 0x5f, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x03,
0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x64, 0x53, 0x70, 0x61,
0x63, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x70, 0x61,
0x63, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c,
0x53, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18,
0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x2a, 0x7d, 0x0a, 0x0a,
0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, 0x0a, 0x17, 0x4d, 0x45,
0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, 0x53, 0x50, 0x45, 0x43,
0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49,
0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x53, 0x55, 0x4d, 0x10, 0x01, 0x12, 0x13, 0x0a, 0x0f,
0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x49, 0x4e, 0x10,
0x02, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44,
0x5f, 0x4d, 0x41, 0x58, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43,
0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x41, 0x56, 0x47, 0x10, 0x04, 0x2a, 0x8c, 0x01, 0x0a, 0x0c,
0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x53, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x1e, 0x0a, 0x1a,
0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x42, 0x59, 0x5f, 0x55,
0x4e, 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x18, 0x0a, 0x14,
0x42, 0x55, 0x43, 0x4b, 0x45, 0x54, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x42, 0x59, 0x5f, 0x43,
0x4f, 0x55, 0x4e, 0x54, 0x10, 0x01, 0x12, 0x20, 0x0a, 0x1c, 0x42, 0x55, 0x43, 0x4b, 0x45, 0x54,
0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x42, 0x59, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41, 0x53, 0x5f,
0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x02, 0x12, 0x20, 0x0a, 0x1c, 0x42, 0x55, 0x43, 0x4b,
0x45, 0x54, 0x5f, 0x53, 0x4f, 0x52, 0x54, 0x5f, 0x42, 0x59, 0x5f, 0x4b, 0x45, 0x59, 0x5f, 0x41,
0x53, 0x5f, 0x4e, 0x55, 0x4d, 0x42, 0x45, 0x52, 0x10, 0x03, 0x32, 0xb3, 0x02, 0x0a, 0x0e, 0x53,
0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x85, 0x01,
0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63,
0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65,
0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e,
0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, 0x6f, 0x70, 0x65,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e,
0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68,
0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x26, 0x82,
0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76,
0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2f, 0x73,
0x65, 0x61, 0x72, 0x63, 0x68, 0x42, 0xf2, 0x02, 0x5a, 0x4a, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62,
0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65,
0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63,
0x68, 0x2f, 0x76, 0x30, 0x92, 0x41, 0xa2, 0x02, 0x12, 0xb7, 0x01, 0x0a, 0x10, 0x4f, 0x70, 0x65,
0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x22, 0x51, 0x0a,
0x0e, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x47, 0x6d, 0x62, 0x48, 0x12,
0x29, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e,
0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75,
0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x1a, 0x14, 0x73, 0x75, 0x70, 0x70,
0x6f, 0x72, 0x74, 0x40, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x65, 0x75,
0x2a, 0x49, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, 0x32, 0x2e, 0x30, 0x12, 0x3b,
0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63,
0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f,
0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d,
0x61, 0x69, 0x6e, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x32, 0x05, 0x31, 0x2e, 0x30,
0x2e, 0x30, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74,
0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63,
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72, 0x3e, 0x0a, 0x10, 0x44, 0x65,
0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, 0x75, 0x61, 0x6c, 0x12, 0x2a,
0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2e, 0x6f, 0x70, 0x65,
0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x65, 0x75, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74,
0x6f, 0x33,
0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75,
0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63,
0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f,
0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15,
0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x73,
0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x98, 0x01, 0x0a, 0x0a, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53,
0x70, 0x61, 0x63, 0x65, 0x12, 0x2f, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68,
0x2e, 0x76, 0x30, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65,
0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75,
0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63,
0x68, 0x2e, 0x76, 0x30, 0x2e, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52,
0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x25, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1f, 0x3a,
0x01, 0x2a, 0x22, 0x1a, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72,
0x63, 0x68, 0x2f, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x2d, 0x73, 0x70, 0x61, 0x63, 0x65, 0x30, 0x01,
0x32, 0xa7, 0x01, 0x0a, 0x0d, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64,
0x65, 0x72, 0x12, 0x95, 0x01, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x30, 0x2e,
0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63,
0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61,
0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a,
0x31, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76,
0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53,
0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e,
0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f,
0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x69, 0x6e,
0x64, 0x65, 0x78, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x42, 0xf2, 0x02, 0x5a, 0x4a, 0x67,
0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c,
0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64,
0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70,
0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f,
0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x92, 0x41, 0xa2, 0x02, 0x12, 0xb7, 0x01,
0x0a, 0x10, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x73, 0x65, 0x61, 0x72,
0x63, 0x68, 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20,
0x47, 0x6d, 0x62, 0x48, 0x12, 0x29, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69,
0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x1a,
0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f,
0x75, 0x64, 0x2e, 0x65, 0x75, 0x2a, 0x49, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d,
0x32, 0x2e, 0x30, 0x12, 0x3b, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74,
0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75,
0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x62,
0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x69, 0x6e, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45,
0x32, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70,
0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61,
0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72,
0x3e, 0x0a, 0x10, 0x44, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e,
0x75, 0x61, 0x6c, 0x12, 0x2a, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x6f, 0x63,
0x73, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x65, 0x75, 0x2f, 0x73,
0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x62,
0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
@@ -1385,7 +1493,7 @@ func file_opencloud_services_search_v0_search_proto_rawDescGZIP() []byte {
}
var file_opencloud_services_search_v0_search_proto_enumTypes = make([]protoimpl.EnumInfo, 2)
var file_opencloud_services_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 13)
var file_opencloud_services_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
var file_opencloud_services_search_v0_search_proto_goTypes = []interface{}{
(MetricKind)(0), // 0: opencloud.services.search.v0.MetricKind
(BucketSortBy)(0), // 1: opencloud.services.search.v0.BucketSortBy
@@ -1397,46 +1505,50 @@ var file_opencloud_services_search_v0_search_proto_goTypes = []interface{}{
(*MetricDefinition)(nil), // 7: opencloud.services.search.v0.MetricDefinition
(*BucketDefinition)(nil), // 8: opencloud.services.search.v0.BucketDefinition
(*BucketRange)(nil), // 9: opencloud.services.search.v0.BucketRange
(*AggregationResult)(nil), // 10: opencloud.services.search.v0.AggregationResult
(*Metric)(nil), // 11: opencloud.services.search.v0.Metric
(*Bucket)(nil), // 12: opencloud.services.search.v0.Bucket
(*IndexSpaceRequest)(nil), // 13: opencloud.services.search.v0.IndexSpaceRequest
(*IndexSpaceResponse)(nil), // 14: opencloud.services.search.v0.IndexSpaceResponse
(*v0.Reference)(nil), // 15: opencloud.messages.search.v0.Reference
(*v0.Match)(nil), // 16: opencloud.messages.search.v0.Match
(*durationpb.Duration)(nil), // 17: google.protobuf.Duration
(*AggregationFilter)(nil), // 10: opencloud.services.search.v0.AggregationFilter
(*AggregationResult)(nil), // 11: opencloud.services.search.v0.AggregationResult
(*Metric)(nil), // 12: opencloud.services.search.v0.Metric
(*Bucket)(nil), // 13: opencloud.services.search.v0.Bucket
(*IndexSpaceRequest)(nil), // 14: opencloud.services.search.v0.IndexSpaceRequest
(*IndexSpaceResponse)(nil), // 15: opencloud.services.search.v0.IndexSpaceResponse
(*v0.Reference)(nil), // 16: opencloud.messages.search.v0.Reference
(*v0.Match)(nil), // 17: opencloud.messages.search.v0.Match
(*durationpb.Duration)(nil), // 18: google.protobuf.Duration
}
var file_opencloud_services_search_v0_search_proto_depIdxs = []int32{
15, // 0: opencloud.services.search.v0.SearchRequest.ref:type_name -> opencloud.messages.search.v0.Reference
16, // 0: opencloud.services.search.v0.SearchRequest.ref:type_name -> opencloud.messages.search.v0.Reference
6, // 1: opencloud.services.search.v0.SearchRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption
16, // 2: opencloud.services.search.v0.SearchResponse.matches:type_name -> opencloud.messages.search.v0.Match
10, // 3: opencloud.services.search.v0.SearchResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult
15, // 4: opencloud.services.search.v0.SearchIndexRequest.ref:type_name -> opencloud.messages.search.v0.Reference
6, // 5: opencloud.services.search.v0.SearchIndexRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption
16, // 6: opencloud.services.search.v0.SearchIndexResponse.matches:type_name -> opencloud.messages.search.v0.Match
10, // 7: opencloud.services.search.v0.SearchIndexResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult
8, // 8: opencloud.services.search.v0.AggregationOption.bucket_definition:type_name -> opencloud.services.search.v0.BucketDefinition
7, // 9: opencloud.services.search.v0.AggregationOption.metric_definition:type_name -> opencloud.services.search.v0.MetricDefinition
6, // 10: opencloud.services.search.v0.AggregationOption.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationOption
0, // 11: opencloud.services.search.v0.MetricDefinition.kind:type_name -> opencloud.services.search.v0.MetricKind
1, // 12: opencloud.services.search.v0.BucketDefinition.sort_by:type_name -> opencloud.services.search.v0.BucketSortBy
9, // 13: opencloud.services.search.v0.BucketDefinition.ranges:type_name -> opencloud.services.search.v0.BucketRange
12, // 14: opencloud.services.search.v0.AggregationResult.buckets:type_name -> opencloud.services.search.v0.Bucket
11, // 15: opencloud.services.search.v0.AggregationResult.metric:type_name -> opencloud.services.search.v0.Metric
0, // 16: opencloud.services.search.v0.Metric.kind:type_name -> opencloud.services.search.v0.MetricKind
10, // 17: opencloud.services.search.v0.Bucket.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationResult
17, // 18: opencloud.services.search.v0.IndexSpaceResponse.space_duration:type_name -> google.protobuf.Duration
2, // 19: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest
13, // 20: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest
4, // 21: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest
3, // 22: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse
14, // 23: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse
5, // 24: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse
22, // [22:25] is the sub-list for method output_type
19, // [19:22] is the sub-list for method input_type
19, // [19:19] is the sub-list for extension type_name
19, // [19:19] is the sub-list for extension extendee
0, // [0:19] is the sub-list for field type_name
10, // 2: opencloud.services.search.v0.SearchRequest.aggregation_filters:type_name -> opencloud.services.search.v0.AggregationFilter
17, // 3: opencloud.services.search.v0.SearchResponse.matches:type_name -> opencloud.messages.search.v0.Match
11, // 4: opencloud.services.search.v0.SearchResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult
16, // 5: opencloud.services.search.v0.SearchIndexRequest.ref:type_name -> opencloud.messages.search.v0.Reference
6, // 6: opencloud.services.search.v0.SearchIndexRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption
10, // 7: opencloud.services.search.v0.SearchIndexRequest.aggregation_filters:type_name -> opencloud.services.search.v0.AggregationFilter
17, // 8: opencloud.services.search.v0.SearchIndexResponse.matches:type_name -> opencloud.messages.search.v0.Match
11, // 9: opencloud.services.search.v0.SearchIndexResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult
8, // 10: opencloud.services.search.v0.AggregationOption.bucket_definition:type_name -> opencloud.services.search.v0.BucketDefinition
7, // 11: opencloud.services.search.v0.AggregationOption.metric_definition:type_name -> opencloud.services.search.v0.MetricDefinition
6, // 12: opencloud.services.search.v0.AggregationOption.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationOption
0, // 13: opencloud.services.search.v0.MetricDefinition.kind:type_name -> opencloud.services.search.v0.MetricKind
1, // 14: opencloud.services.search.v0.BucketDefinition.sort_by:type_name -> opencloud.services.search.v0.BucketSortBy
9, // 15: opencloud.services.search.v0.BucketDefinition.ranges:type_name -> opencloud.services.search.v0.BucketRange
9, // 16: opencloud.services.search.v0.AggregationFilter.ranges:type_name -> opencloud.services.search.v0.BucketRange
13, // 17: opencloud.services.search.v0.AggregationResult.buckets:type_name -> opencloud.services.search.v0.Bucket
12, // 18: opencloud.services.search.v0.AggregationResult.metric:type_name -> opencloud.services.search.v0.Metric
0, // 19: opencloud.services.search.v0.Metric.kind:type_name -> opencloud.services.search.v0.MetricKind
11, // 20: opencloud.services.search.v0.Bucket.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationResult
18, // 21: opencloud.services.search.v0.IndexSpaceResponse.space_duration:type_name -> google.protobuf.Duration
2, // 22: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest
14, // 23: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest
4, // 24: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest
3, // 25: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse
15, // 26: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse
5, // 27: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse
25, // [25:28] is the sub-list for method output_type
22, // [22:25] is the sub-list for method input_type
22, // [22:22] is the sub-list for extension type_name
22, // [22:22] is the sub-list for extension extendee
0, // [0:22] is the sub-list for field type_name
}
func init() { file_opencloud_services_search_v0_search_proto_init() }
@@ -1542,7 +1654,7 @@ func file_opencloud_services_search_v0_search_proto_init() {
}
}
file_opencloud_services_search_v0_search_proto_msgTypes[8].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*AggregationResult); i {
switch v := v.(*AggregationFilter); i {
case 0:
return &v.state
case 1:
@@ -1554,7 +1666,7 @@ func file_opencloud_services_search_v0_search_proto_init() {
}
}
file_opencloud_services_search_v0_search_proto_msgTypes[9].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Metric); i {
switch v := v.(*AggregationResult); i {
case 0:
return &v.state
case 1:
@@ -1566,7 +1678,7 @@ func file_opencloud_services_search_v0_search_proto_init() {
}
}
file_opencloud_services_search_v0_search_proto_msgTypes[10].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*Bucket); i {
switch v := v.(*Metric); i {
case 0:
return &v.state
case 1:
@@ -1578,7 +1690,7 @@ func file_opencloud_services_search_v0_search_proto_init() {
}
}
file_opencloud_services_search_v0_search_proto_msgTypes[11].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IndexSpaceRequest); i {
switch v := v.(*Bucket); i {
case 0:
return &v.state
case 1:
@@ -1590,6 +1702,18 @@ func file_opencloud_services_search_v0_search_proto_init() {
}
}
file_opencloud_services_search_v0_search_proto_msgTypes[12].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IndexSpaceRequest); i {
case 0:
return &v.state
case 1:
return &v.sizeCache
case 2:
return &v.unknownFields
default:
return nil
}
}
file_opencloud_services_search_v0_search_proto_msgTypes[13].Exporter = func(v interface{}, i int) interface{} {
switch v := v.(*IndexSpaceResponse); i {
case 0:
return &v.state
@@ -1604,14 +1728,14 @@ func file_opencloud_services_search_v0_search_proto_init() {
}
file_opencloud_services_search_v0_search_proto_msgTypes[0].OneofWrappers = []interface{}{}
file_opencloud_services_search_v0_search_proto_msgTypes[2].OneofWrappers = []interface{}{}
file_opencloud_services_search_v0_search_proto_msgTypes[9].OneofWrappers = []interface{}{}
file_opencloud_services_search_v0_search_proto_msgTypes[10].OneofWrappers = []interface{}{}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_opencloud_services_search_v0_search_proto_rawDesc,
NumEnums: 2,
NumMessages: 13,
NumMessages: 14,
NumExtensions: 0,
NumServices: 2,
},
@@ -167,6 +167,29 @@
}
}
},
"v0AggregationFilter": {
"type": "object",
"properties": {
"field": {
"type": "string"
},
"terms": {
"type": "array",
"items": {
"type": "string"
},
"description": "Bucket keys of a terms aggregation, matched exactly and case-sensitively."
},
"ranges": {
"type": "array",
"items": {
"$ref": "#/definitions/v0BucketRange"
},
"description": "Buckets of a range aggregation."
}
},
"description": "AggregationFilter narrows the matches to buckets of one field. A match has\nto be in any of the listed buckets."
},
"v0AggregationOption": {
"type": "object",
"properties": {
@@ -697,6 +720,13 @@
"$ref": "#/definitions/v0AggregationOption"
},
"description": "Optional. Per-space aggregations (facets) to compute alongside the matches."
},
"aggregationFilters": {
"type": "array",
"items": {
"$ref": "#/definitions/v0AggregationFilter"
},
"description": "Optional. Filters narrowing the matches to buckets of a prior\naggregation; the engine ANDs them with `query`."
}
}
},
@@ -751,6 +781,13 @@
},
"description": "Optional. Aggregations (facets) to compute alongside the matches."
},
"aggregationFilters": {
"type": "array",
"items": {
"$ref": "#/definitions/v0AggregationFilter"
},
"description": "Optional. Filters narrowing the matches to buckets of a prior\naggregation. Combined with `query` and with each other via AND."
},
"from": {
"type": "integer",
"format": "int32",
@@ -76,6 +76,10 @@ message SearchRequest {
// Optional. Aggregations (facets) to compute alongside the matches.
repeated AggregationOption aggregations = 5 [(google.api.field_behavior) = OPTIONAL];
// Optional. Filters narrowing the matches to buckets of a prior
// aggregation. Combined with `query` and with each other via AND.
repeated AggregationFilter aggregation_filters = 6 [(google.api.field_behavior) = OPTIONAL];
// Optional. Number of leading matches to skip in the globally sorted,
// cross-space merged result list.
int32 from = 7 [(google.api.field_behavior) = OPTIONAL];
@@ -107,6 +111,10 @@ message SearchIndexRequest {
// Optional. Per-space aggregations (facets) to compute alongside the matches.
repeated AggregationOption aggregations = 5 [(google.api.field_behavior) = OPTIONAL];
// Optional. Filters narrowing the matches to buckets of a prior
// aggregation; the engine ANDs them with `query`.
repeated AggregationFilter aggregation_filters = 6 [(google.api.field_behavior) = OPTIONAL];
}
message SearchIndexResponse {
@@ -181,6 +189,16 @@ message BucketRange {
string to = 2;
}
// AggregationFilter narrows the matches to buckets of one field. A match has
// to be in any of the listed buckets.
message AggregationFilter {
string field = 1;
// Bucket keys of a terms aggregation, matched exactly and case-sensitively.
repeated string terms = 2;
// Buckets of a range aggregation.
repeated BucketRange ranges = 3;
}
// One result per AggregationOption, at the same position.
message AggregationResult {
string field = 1;
@@ -0,0 +1,13 @@
package filtertoken_test
import (
"testing"
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)
func TestFilterToken(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Filter Token Suite")
}
+151
View File
@@ -0,0 +1,151 @@
// Package filtertoken encodes and decodes the MS Graph aggregationFilterToken:
// quoted hex terms tokens behind a U+01C2 pair, and range(from, to) range tokens.
package filtertoken
import (
"encoding/hex"
"fmt"
"strings"
"unicode/utf8"
)
// U+01C2 LATIN LETTER ALVEOLAR CLICK, twice
const termPrefix = "ǂǂ"
const (
openLower = "min"
openUpper = "max"
// MS Graph appends this to an open upper bound; the bound itself is
// still max, so the marker carries no extra information.
openUpperMarker = `to="le"`
)
// Filter is a decoded aggregationFilters entry: the buckets of one field a
// match has to be in, any of them.
type Filter struct {
Field string
Terms []string
Ranges []Range
}
// Range has an empty bound on an open side.
type Range struct {
From, To string
}
// EncodeTermsToken hex-encodes the bucket key; the quotes are part of the token value.
func EncodeTermsToken(key string) string {
return `"` + termPrefix + hex.EncodeToString([]byte(key)) + `"`
}
// EncodeRangeToken uses the MS Graph spelling: open bounds are min/max, an open
// upper bound carries to="le".
func EncodeRangeToken(from, to string) string {
if from == "" {
from = openLower
}
if to == "" {
return "range(" + from + ", " + openUpper + ", " + openUpperMarker + ")"
}
return "range(" + from + ", " + to + ")"
}
// DecodeFilter reads {field}:{token}, the token being a terms token, a range
// token or an or(...) of them. Anything not shaped like a server-issued token
// is rejected; the bounds of a range are returned as written.
func DecodeFilter(filter string) (Filter, error) {
field, token, ok := strings.Cut(filter, ":")
if !ok || field == "" || token == "" {
return Filter{}, fmt.Errorf("invalid aggregation filter %q", filter)
}
f := Filter{Field: field}
tokens := []string{token}
if inner, ok := trimCall(token, "or"); ok {
tokens = splitArguments(inner)
}
for _, t := range tokens {
if inner, ok := trimCall(t, "range"); ok {
r, err := decodeRange(inner)
if err != nil {
return Filter{}, fmt.Errorf("invalid range token %q: %w", t, err)
}
f.Ranges = append(f.Ranges, r)
continue
}
term, err := decodeTerm(t)
if err != nil {
return Filter{}, err
}
f.Terms = append(f.Terms, term)
}
// the server issues one kind of token per field
if len(f.Terms) > 0 && len(f.Ranges) > 0 {
return Filter{}, fmt.Errorf("invalid aggregation filter %q: terms and ranges in one or()", filter)
}
return f, nil
}
func decodeTerm(token string) (string, error) {
inner, ok := strings.CutPrefix(token, `"`+termPrefix)
if ok {
inner, ok = strings.CutSuffix(inner, `"`)
}
// the encoder writes lowercase hex, and no bucket has an empty key
if !ok || inner == "" || inner != strings.ToLower(inner) {
return "", fmt.Errorf("invalid terms token %q", token)
}
b, err := hex.DecodeString(inner)
if err != nil || !utf8.Valid(b) {
return "", fmt.Errorf("invalid terms token %q", token)
}
return string(b), nil
}
func decodeRange(arguments string) (Range, error) {
segments := splitArguments(arguments)
if len(segments) == 3 && segments[1] == openUpper && segments[2] == openUpperMarker {
segments = segments[:2]
}
if len(segments) != 2 || segments[0] == "" || segments[1] == "" {
return Range{}, fmt.Errorf("expected a lower and an upper bound")
}
var r Range
if segments[0] != openLower {
r.From = segments[0]
}
if segments[1] != openUpper {
r.To = segments[1]
}
if r.From == "" && r.To == "" {
return Range{}, fmt.Errorf("no bounds")
}
return r, nil
}
func trimCall(token, name string) (string, bool) {
if !strings.HasPrefix(token, name+"(") || !strings.HasSuffix(token, ")") {
return "", false
}
return token[len(name)+1 : len(token)-1], true
}
// splitArguments splits on the commas outside of parentheses and trims the
// optional whitespace around each argument.
func splitArguments(s string) []string {
var out []string
depth, start := 0, 0
for i, r := range s {
switch r {
case '(':
depth++
case ')':
depth--
case ',':
if depth == 0 {
out = append(out, strings.TrimSpace(s[start:i]))
start = i + 1
}
}
}
return append(out, strings.TrimSpace(s[start:]))
}
@@ -0,0 +1,102 @@
package filtertoken_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencloud-eu/opencloud/services/graph/pkg/filtertoken"
)
var _ = Describe("Token", func() {
Describe("EncodeTermsToken", func() {
It("encodes the key as quoted ǂǂ-prefixed lowercase hex", func() {
Expect(filtertoken.EncodeTermsToken("And the Bands Played On")).To(Equal(`"ǂǂ416e64207468652042616e647320506c61796564204f6e"`))
})
})
Describe("EncodeRangeToken", func() {
DescribeTable("bounds, and the token decodes back to them",
func(from, to, want string) {
Expect(filtertoken.EncodeRangeToken(from, to)).To(Equal(want))
got, err := filtertoken.DecodeFilter("Size:" + want)
Expect(err).ToNot(HaveOccurred())
Expect(got).To(Equal(filtertoken.Filter{Field: "Size", Ranges: []filtertoken.Range{{From: from, To: to}}}))
},
Entry("closed", "0", "100", "range(0, 100)"),
Entry("open lower", "", "100", "range(min, 100)"),
Entry("open upper", "0", "", `range(0, max, to="le")`),
)
})
Describe("DecodeFilter", func() {
DescribeTable("valid tokens",
func(filter string, want filtertoken.Filter) {
got, err := filtertoken.DecodeFilter(filter)
Expect(err).ToNot(HaveOccurred())
Expect(got).To(Equal(want))
},
Entry("terms", `audio.artist:"ǂǂ5361786f6e"`,
filtertoken.Filter{Field: "audio.artist", Terms: []string{"Saxon"}}),
Entry("closed range", "Size:range(0,100)",
filtertoken.Filter{Field: "Size", Ranges: []filtertoken.Range{{From: "0", To: "100"}}}),
Entry("open lower range", "Size:range(min,100)",
filtertoken.Filter{Field: "Size", Ranges: []filtertoken.Range{{To: "100"}}}),
Entry("open upper range", "Size:range(0,max)",
filtertoken.Filter{Field: "Size", Ranges: []filtertoken.Range{{From: "0"}}}),
Entry("open upper range with le marker", `Size:range(0, max, to="le")`,
filtertoken.Filter{Field: "Size", Ranges: []filtertoken.Range{{From: "0"}}}),
Entry("date range, the colons of the bounds are no field separator",
"photo.takenDateTime:range(2018-08-11T00:00:00Z, 2018-08-12T00:00:00Z)",
filtertoken.Filter{Field: "photo.takenDateTime", Ranges: []filtertoken.Range{{From: "2018-08-11T00:00:00Z", To: "2018-08-12T00:00:00Z"}}}),
Entry("or of two terms with spaces",
`audio.artist:or("ǂǂ5361786f6e", "ǂǂ49726f6e204d616964656e")`,
filtertoken.Filter{Field: "audio.artist", Terms: []string{"Saxon", "Iron Maiden"}}),
Entry("or of two ranges, the commas of a range do not split it",
`audio.year:or(range(min, 1980),range(2010, max, to="le"))`,
filtertoken.Filter{Field: "audio.year", Ranges: []filtertoken.Range{{To: "1980"}, {From: "2010"}}}),
)
DescribeTable("rejected tokens",
func(filter string) {
_, err := filtertoken.DecodeFilter(filter)
Expect(err).To(HaveOccurred())
},
Entry("no colon", `audio.artist"ǂǂ00"`),
Entry("empty field", `:"ǂǂ00"`),
Entry("missing ǂǂ prefix", `audio.artist:"deadbeef"`),
Entry("missing quotes", `audio.artist:ǂǂ5361786f6e`),
Entry("odd hex", `audio.artist:"ǂǂabc"`),
Entry("uppercase hex, the server issues lowercase", `audio.artist:"ǂǂ5361786F6E"`),
Entry("empty key", `audio.artist:"ǂǂ"`),
Entry("hex that is no UTF-8", `audio.artist:"ǂǂff"`),
Entry("terms and ranges in one or", `audio.year:or("ǂǂ31393830", range(1990, max, to="le"))`),
Entry("range without bounds", "Size:range(min,max)"),
Entry("range with one argument", "Size:range(100)"),
Entry("range with an empty bound", "Size:range(,100)"),
Entry("le marker on a closed range", `Size:range(0, 100, to="le")`),
Entry("empty or", "audio.artist:or()"),
Entry("nested or", `audio.artist:or(or("ǂǂ5361786f6e"))`),
Entry("plain value", "audio.artist:Saxon"),
)
DescribeTable("round-trips every terms key, a term is data and no query syntax",
func(key string) {
got, err := filtertoken.DecodeFilter("audio.artist:" + filtertoken.EncodeTermsToken(key))
Expect(err).ToNot(HaveOccurred())
Expect(got).To(Equal(filtertoken.Filter{Field: "audio.artist", Terms: []string{key}}))
},
Entry("slash", "AC/DC"),
Entry("double quote", `The "Chirping" Crickets`),
Entry("wildcards", "Wh*t?"),
Entry("comma and parentheses", "Emerson, Lake (and Palmer)"),
Entry("colon", "Sunn O))): live"),
Entry("umlaut", "Motörhead"),
)
It("returns range bounds as written, they never become query syntax", func() {
got, err := filtertoken.DecodeFilter("audio.year:range(1980 OR Name:*, 1990)")
Expect(err).ToNot(HaveOccurred())
Expect(got.Ranges).To(Equal([]filtertoken.Range{{From: "1980 OR Name:*", To: "1990"}}))
})
})
})
+15 -7
View File
@@ -89,9 +89,6 @@ func unsupportedProperty(sr libregraph.SearchRequest) string {
if len(sr.SortProperties) > 0 {
return "sortProperties"
}
if len(sr.AggregationFilters) > 0 {
return "aggregationFilters"
}
var geohash func(aggs []libregraph.AggregationOption) bool
geohash = func(aggs []libregraph.AggregationOption) bool {
for _, a := range aggs {
@@ -121,18 +118,29 @@ func searchRequestOf(sr libregraph.SearchRequest) (*searchsvc.SearchRequest, err
return nil, err
}
aggregations := libregraphAggregationsToSearch(sr.Aggregations)
filters, err := aggregationFiltersToSearch(sr.AggregationFilters)
if err != nil {
return nil, err
}
requestFieldType := func(field string) string { return search.AggregatableFieldType(indexField(field)) }
if err := aggregation.ValidateOptions(aggregations, requestFieldType); err != nil {
return nil, err
}
if err := aggregation.ValidateFilters(filters, requestFieldType); err != nil {
return nil, err
}
resolveFields(aggregations)
for _, f := range filters {
f.Field = indexField(f.Field)
}
from, size := pagination(sr.From, sr.Size)
return &searchsvc.SearchRequest{
Query: sr.Query.QueryString,
From: from,
PageSize: &size,
Aggregations: aggregations,
Query: sr.Query.QueryString,
From: from,
PageSize: &size,
Aggregations: aggregations,
AggregationFilters: filters,
}, nil
}
@@ -12,6 +12,8 @@ import (
searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/graph/pkg/filtertoken"
"github.com/opencloud-eu/opencloud/services/search/pkg/aggregation"
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
)
@@ -59,6 +61,24 @@ func resolveFields(opts []*searchsvc.AggregationOption) {
}
}
// the token format is graph API surface: decode here, the search service only
// sees the selected buckets
func aggregationFiltersToSearch(filters []string) ([]*searchsvc.AggregationFilter, error) {
out := make([]*searchsvc.AggregationFilter, 0, len(filters))
for _, f := range filters {
decoded, err := filtertoken.DecodeFilter(f)
if err != nil {
return nil, err
}
filter := &searchsvc.AggregationFilter{Field: decoded.Field, Terms: decoded.Terms}
for _, r := range decoded.Ranges {
filter.Ranges = append(filter.Ranges, &searchsvc.BucketRange{From: r.From, To: r.To})
}
out = append(out, filter)
}
return out, nil
}
func libregraphAggregationsToSearch(in []libregraph.AggregationOption) []*searchsvc.AggregationOption {
out := make([]*searchsvc.AggregationOption, 0, len(in))
for _, a := range in {
@@ -124,11 +144,13 @@ func searchAggregationsToLibregraph(in []*searchsvc.AggregationResult, defs []li
out = append(out, agg)
continue
}
ranges := libregraphRangesToSearch(def.BucketDefinition.GetRanges())
agg.Buckets = make([]libregraph.SearchBucket, 0, len(in[i].GetBuckets()))
for _, b := range in[i].GetBuckets() {
agg.Buckets = append(agg.Buckets, libregraph.SearchBucket{
Key: libregraph.PtrString(b.GetKey()),
Count: libregraph.PtrInt64(b.GetCount()),
AggregationFilterToken: libregraph.PtrString(aggregationFilterToken(b.GetKey(), ranges)),
LibreGraphSubAggregations: searchAggregationsToLibregraph(b.GetSubAggregations(), def.LibreGraphSubAggregations),
})
}
@@ -137,6 +159,17 @@ func searchAggregationsToLibregraph(in []*searchsvc.AggregationResult, defs []li
return out
}
// aggregationFilterToken issues a range token for the bucket of a range
// aggregation, found by its key, and a terms token otherwise.
func aggregationFilterToken(key string, ranges []*searchsvc.BucketRange) string {
for _, r := range ranges {
if aggregation.RangeKey(r) == key {
return filtertoken.EncodeRangeToken(r.GetFrom(), r.GetTo())
}
}
return filtertoken.EncodeTermsToken(key)
}
func searchResourceID(id *searchmsg.ResourceID) string {
return storagespace.FormatResourceID(&storageprovider.ResourceId{
StorageId: id.GetStorageId(),
@@ -94,6 +94,7 @@ type searchAggregationJSON struct {
Buckets []struct {
Key string `json:"key"`
Count int64 `json:"count"`
Token string `json:"aggregationFilterToken"`
SubAggregations []searchAggregationJSON `json:"@libre.graph.subAggregations"`
} `json:"buckets"`
}
@@ -314,6 +315,9 @@ var _ = ginkgo.Describe("SearchQuery", func() {
`"aggregations": [{"field": "audio.artist", "@libre.graph.subAggregations": [{"field": "audio.album", "@libre.graph.subAggregations": [{"field": "audio.year", "@libre.graph.metricDefinition": {"kind": "median"}}]}]}]`),
ginkgo.Entry("a field the index does not know", `"aggregations": [{"field": "audio.nonexistent"}]`),
ginkgo.Entry("a range bound that is no number", `"aggregations": [{"field": "audio.year", "bucketDefinition": {"sortBy": "count", "ranges": [{"from": "1970", "to": "198o"}]}}]`),
// the token grammar is pinned in the filtertoken package
ginkgo.Entry("an aggregation filter that is no server-issued token", `"aggregationFilters": ["audio.artist:\"not a token\""]`),
ginkgo.Entry("an aggregation filter on an internal field", `"aggregationFilters": ["favorites:\"ǂǂ5361786f6e\""]`),
)
ginkgo.It("translates the bucket definition for the search service", func() {
@@ -338,10 +342,12 @@ var _ = ginkgo.Describe("SearchQuery", func() {
Field: indexed,
Buckets: []*searchsvc.Bucket{{Key: "audio/mpeg", Count: 3}},
})
rr := postSearchQuery(g, searchQueryBody(fmt.Sprintf(`"aggregations": [{"field": %q}]`, requested)))
rr := postSearchQuery(g, searchQueryBody(fmt.Sprintf(`"aggregations": [{"field": %q}], "aggregationFilters": [%q]`, requested, requested+`:"ǂǂ617564696f2f6d706567"`)))
Expect(rr.Code).To(Equal(http.StatusOK), rr.Body.String())
Expect(captured().GetAggregations()[0].GetField()).To(Equal(indexed))
Expect(captured().GetAggregationFilters()[0].GetField()).To(Equal(indexed))
Expect(captured().GetAggregationFilters()[0].GetTerms()).To(Equal([]string{"audio/mpeg"}))
Expect(hitsContainer(rr).Aggregations[0].Field).To(Equal(requested))
},
ginkgo.Entry("mimeType", "mimeType", "MimeType"),
@@ -407,7 +413,6 @@ var _ = ginkgo.Describe("SearchQuery", func() {
Expect(rr.Body.String()).To(ContainSubstring(property))
},
ginkgo.Entry("sortProperties", `"sortProperties": [{"name": "name"}]`, "sortProperties"),
ginkgo.Entry("aggregationFilters", `"aggregationFilters": ["audio.artist:\"ǂǂ5361786f6e\""]`, "aggregationFilters"),
ginkgo.Entry("a geohash aggregation",
`"aggregations": [{"field": "location", "@libre.graph.geohashDefinition": {"precision": 5}}]`, "geohashDefinition"),
ginkgo.Entry("a nested geohash aggregation",
@@ -450,4 +455,66 @@ var _ = ginkgo.Describe("SearchQuery", func() {
ginkgo.Entry("an error that is no service error", errors.New("connection refused"),
http.StatusInternalServerError, "generalException", "connection refused"),
)
ginkgo.It("issues terms and range tokens by the definition at the position of the result", func() {
// the range aggregation and the metric share a field: the result at
// position 0 is the range aggregation, whatever follows on that field
g, _ := graphWithAggregations(
&searchsvc.AggregationResult{Field: "audio.year", Buckets: []*searchsvc.Bucket{
{Key: "..1980", Count: 2},
{Key: "1980..1990", Count: 1},
{Key: "1990..", Count: 4},
}},
&searchsvc.AggregationResult{Field: "audio.artist", Buckets: []*searchsvc.Bucket{
{Key: "Saxon", Count: 2, SubAggregations: []*searchsvc.AggregationResult{
{Field: "audio.album", Buckets: []*searchsvc.Bucket{{Key: "Wheels of Steel", Count: 2}}},
{Field: "audio.year", Buckets: []*searchsvc.Bucket{{Key: "1980..", Count: 2}}},
}},
}},
&searchsvc.AggregationResult{Field: "audio.year", Metric: &searchsvc.Metric{Kind: searchsvc.MetricKind_METRIC_KIND_MAX}},
)
rr := postSearchQuery(g, searchQueryBody(`"aggregations": [
{"field": "audio.year", "bucketDefinition": {"sortBy": "keyAsNumber", "ranges": [{"to": "1980"}, {"from": "1980", "to": "1990"}, {"from": "1990"}]}},
{"field": "audio.artist", "@libre.graph.subAggregations": [
{"field": "audio.album"},
{"field": "audio.year", "bucketDefinition": {"sortBy": "keyAsNumber", "ranges": [{"from": "1980"}]}}
]},
{"field": "audio.year", "@libre.graph.metricDefinition": {"kind": "max"}}
]`))
Expect(rr.Code).To(Equal(http.StatusOK), rr.Body.String())
aggs := hitsContainer(rr).Aggregations
Expect(aggs).To(HaveLen(3))
years := aggs[0].Buckets
Expect(years).To(HaveLen(3))
Expect(years[0].Token).To(Equal("range(min, 1980)"))
Expect(years[1].Token).To(Equal("range(1980, 1990)"))
Expect(years[2].Token).To(Equal(`range(1990, max, to="le")`))
saxon := aggs[1].Buckets[0]
Expect(saxon.Token).To(Equal(`"ǂǂ5361786f6e"`))
Expect(saxon.SubAggregations).To(HaveLen(2))
Expect(saxon.SubAggregations[0].Buckets[0].Token).To(Equal(`"ǂǂ576865656c73206f6620537465656c"`))
Expect(saxon.SubAggregations[1].Buckets[0].Token).To(Equal(`range(1980, max, to="le")`))
Expect(aggs[2].Metric).ToNot(BeNil())
Expect(aggs[2].Buckets).To(BeEmpty())
})
ginkgo.DescribeTable("hands the buckets of an aggregation filter to the search service",
func(filter string, want *searchsvc.AggregationFilter) {
g, captured := graphWithAggregations()
rr := postSearchQuery(g, searchQueryBody(fmt.Sprintf(`"aggregationFilters": [%q]`, filter)))
Expect(rr.Code).To(Equal(http.StatusOK), rr.Body.String())
Expect(captured().GetAggregationFilters()).To(BeComparableTo([]*searchsvc.AggregationFilter{want}, protocmp.Transform()))
},
ginkgo.Entry("several terms", `audio.artist:or("ǂǂ5361786f6e", "ǂǂ49726f6e204d616964656e")`,
&searchsvc.AggregationFilter{Field: "audio.artist", Terms: []string{"Saxon", "Iron Maiden"}}),
ginkgo.Entry("several ranges", `audio.year:or(range(min, 1980),range(2010, max, to="le"))`,
&searchsvc.AggregationFilter{Field: "audio.year", Ranges: []*searchsvc.BucketRange{{To: "1980"}, {From: "2010"}}}),
ginkgo.Entry("a date range", `photo.takenDateTime:range(2018-08-11T00:00:00Z, 2018-08-12T00:00:00Z)`,
&searchsvc.AggregationFilter{Field: "photo.takenDateTime", Ranges: []*searchsvc.BucketRange{{From: "2018-08-11T00:00:00Z", To: "2018-08-12T00:00:00Z"}}}),
)
})
+78
View File
@@ -0,0 +1,78 @@
package aggregation
import (
"fmt"
"strconv"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/search/pkg/mapping"
)
// Buckets are the buckets an aggregation filter selects, read as the type of
// its field.
type Buckets struct {
Terms []Term
Ranges Ranges
}
// Term is a bucket key of a terms aggregation. A numeric or bool field also
// carries the key as its value.
type Term struct {
Key string
Number *float64
Bool *bool
}
// ParseFilter reads the buckets of a filter on a field of the given mapping
// type and rejects what no aggregation on that field could have answered.
func ParseFilter(f *searchsvc.AggregationFilter, fieldType string) (Buckets, error) {
field := f.GetField()
if fieldType == "" {
return Buckets{}, fmt.Errorf("unknown aggregation filter field %q", field)
}
if len(f.GetTerms())+len(f.GetRanges()) == 0 {
return Buckets{}, fmt.Errorf("aggregation filter on field %q selects no bucket", field)
}
ranges, err := ParseRanges(field, f.GetRanges())
if err != nil {
return Buckets{}, err
}
if err := validateRangeField(field, fieldType, ranges); err != nil {
return Buckets{}, err
}
buckets := Buckets{Ranges: ranges}
if len(f.GetTerms()) == 0 {
return buckets, nil
}
if err := validateTermsField(field, fieldType); err != nil {
return Buckets{}, err
}
for _, key := range f.GetTerms() {
term, err := parseTerm(fieldType, key)
if err != nil {
return Buckets{}, fmt.Errorf("invalid bucket key on field %q: %w", field, err)
}
buckets.Terms = append(buckets.Terms, term)
}
return buckets, nil
}
func parseTerm(fieldType, key string) (Term, error) {
term := Term{Key: key}
switch fieldType {
case mapping.TypeNumeric:
// the key as a bucket spells it, no other spelling of the number
v, err := parseNumber(key)
if err != nil || v == nil || NumberKey(*v) != key {
return Term{}, fmt.Errorf("%q is no bucket key of a number", key)
}
term.Number = v
case mapping.TypeBool:
v, err := strconv.ParseBool(key)
if err != nil || key != BoolKey(v) {
return Term{}, fmt.Errorf("%q is neither true nor false", key)
}
term.Bool = &v
}
return term, nil
}
@@ -0,0 +1,57 @@
package aggregation_test
import (
. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
"github.com/opencloud-eu/opencloud/pkg/conversions"
searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0"
"github.com/opencloud-eu/opencloud/services/search/pkg/aggregation"
)
var _ = Describe("ParseFilter", func() {
parse := func(f *searchsvc.AggregationFilter) (aggregation.Buckets, error) {
return aggregation.ParseFilter(f, fieldTypes(f.GetField()))
}
DescribeTable("reads a key in the type of the field",
func(field string, keys []string, want []aggregation.Term) {
got, err := parse(&searchsvc.AggregationFilter{Field: field, Terms: keys})
Expect(err).ToNot(HaveOccurred())
Expect(got.Terms).To(Equal(want))
},
Entry("a keyword as it is", "audio.artist", []string{`Wh*t? "Live"`, "true", "1980"},
[]aggregation.Term{{Key: `Wh*t? "Live"`}, {Key: "true"}, {Key: "1980"}}),
Entry("a number", "audio.year", []string{"1980", "4.5"},
[]aggregation.Term{{Key: "1980", Number: conversions.ToPointer(1980.0)}, {Key: "4.5", Number: conversions.ToPointer(4.5)}}),
Entry("a bool", "audio.hasDrm", []string{"true", "false"},
[]aggregation.Term{{Key: "true", Bool: conversions.ToPointer(true)}, {Key: "false", Bool: conversions.ToPointer(false)}}),
)
It("reads the ranges of a filter", func() {
got, err := parse(&searchsvc.AggregationFilter{Field: "audio.year", Ranges: []*searchsvc.BucketRange{{To: "1980"}, {From: "2010"}}})
Expect(err).ToNot(HaveOccurred())
Expect(got.Terms).To(BeEmpty())
Expect(got.Ranges.Numeric).To(HaveLen(2))
})
DescribeTable("rejects what no aggregation on the field could have answered",
func(f *searchsvc.AggregationFilter) {
_, err := parse(f)
Expect(err).To(HaveOccurred())
},
Entry("an unknown field", &searchsvc.AggregationFilter{Field: "audio.nonexistent", Terms: []string{"Saxon"}}),
Entry("no bucket at all", &searchsvc.AggregationFilter{Field: "audio.artist"}),
Entry("a key that is no number on a numeric field", &searchsvc.AggregationFilter{Field: "audio.year", Terms: []string{"nineteen-eighty"}}),
Entry("an empty key on a numeric field", &searchsvc.AggregationFilter{Field: "audio.year", Terms: []string{""}}),
Entry("a number spelled otherwise than its bucket key", &searchsvc.AggregationFilter{Field: "audio.year", Terms: []string{"1980.0"}}),
Entry("a number in scientific notation", &searchsvc.AggregationFilter{Field: "audio.year", Terms: []string{"1e3"}}),
Entry("a number with a plus sign", &searchsvc.AggregationFilter{Field: "audio.year", Terms: []string{"+1980"}}),
Entry("the bleve spelling of a bool", &searchsvc.AggregationFilter{Field: "audio.hasDrm", Terms: []string{"T"}}),
Entry("another spelling of a bool", &searchsvc.AggregationFilter{Field: "audio.hasDrm", Terms: []string{"1"}}),
Entry("a key on a date field", &searchsvc.AggregationFilter{Field: "photo.takenDateTime", Terms: []string{"2018-08-11T00:00:00Z"}}),
Entry("a key on full text", &searchsvc.AggregationFilter{Field: "Content", Terms: []string{"fox"}}),
Entry("a numeric range on a keyword", &searchsvc.AggregationFilter{Field: "audio.artist", Ranges: []*searchsvc.BucketRange{{From: "1980"}}}),
Entry("a date range on a number", &searchsvc.AggregationFilter{Field: "audio.year", Ranges: []*searchsvc.BucketRange{{From: "2018-08-11T00:00:00Z"}}}),
)
})
@@ -65,3 +65,13 @@ func validateRangeField(field, fieldType string, ranges Ranges) error {
}
return nil
}
// ValidateFilters holds filters to the same rules as ValidateOptions.
func ValidateFilters(filters []*searchsvc.AggregationFilter, fieldType func(string) string) error {
for _, f := range filters {
if _, err := ParseFilter(f, fieldType(f.GetField())); err != nil {
return err
}
}
return nil
}
+6
View File
@@ -61,6 +61,12 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ
createdQuery,
)
filters, err := aggregationFilterQueries(sir.GetAggregationFilters())
if err != nil {
return nil, errtypes.BadRequest(err.Error())
}
q.Conjuncts = append(q.Conjuncts, filters...)
if sir.Ref != nil {
q.Conjuncts = append(
q.Conjuncts,
+57
View File
@@ -0,0 +1,57 @@
package bleve
import (
"github.com/blevesearch/bleve/v2"
"github.com/blevesearch/bleve/v2/search/query"
searchService "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/search"
)
// fieldQuery is a bleve query on one field.
type fieldQuery interface {
query.Query
SetField(string)
SetBoost(float64)
}
// aggregationFilterQueries builds one query per filter, matching any of its
// buckets the way the aggregation counted them: a key is the exact value of
// the field, a range is from-inclusive and to-exclusive. A filter narrows and
// does not rank: bleve has no filter context, a boost of zero keeps the
// filters out of the score.
func aggregationFilterQueries(filters []*searchService.AggregationFilter) ([]query.Query, error) {
inclusive, exclusive := true, false
out := make([]query.Query, 0, len(filters))
for _, f := range filters {
buckets, err := aggregation.ParseFilter(f, search.FieldType(f.GetField()))
if err != nil {
return nil, err
}
var selected []query.Query
add := func(q fieldQuery) {
q.SetField(f.GetField())
q.SetBoost(0)
selected = append(selected, q)
}
for _, term := range buckets.Terms {
switch {
case term.Number != nil:
add(bleve.NewNumericRangeInclusiveQuery(term.Number, term.Number, &inclusive, &inclusive))
case term.Bool != nil:
add(bleve.NewBoolFieldQuery(*term.Bool))
default:
add(bleve.NewTermQuery(term.Key))
}
}
for _, r := range buckets.Ranges.Numeric {
add(bleve.NewNumericRangeInclusiveQuery(r.From, r.To, &inclusive, &exclusive))
}
for _, r := range buckets.Ranges.Dates {
add(bleve.NewDateRangeInclusiveQuery(r.From, r.To, &inclusive, &exclusive))
}
out = append(out, bleve.NewDisjunctionQuery(selected...))
}
return out, nil
}
@@ -84,6 +84,12 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ
return nil, fmt.Errorf("failed to convert KQL query to OpenSearch bool query: %w", err)
}
filters, err := aggregationFilterQueries(sir.GetAggregationFilters())
if err != nil {
return nil, errtypes.BadRequest(err.Error())
}
boolQuery.Filter(filters...)
// filter out deleted resources
boolQuery.Filter(
osu.NewTermQuery[bool]("Deleted").Value(false),
+35
View File
@@ -0,0 +1,35 @@
package opensearch
import (
searchService "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/opensearch/internal/osu"
"github.com/opencloud-eu/opencloud/services/search/pkg/search"
)
// Keys and bounds stay strings, OpenSearch reads them as the type of the field.
func aggregationFilterQueries(filters []*searchService.AggregationFilter) ([]osu.Builder, error) {
out := make([]osu.Builder, 0, len(filters))
for _, f := range filters {
if _, err := aggregation.ParseFilter(f, search.FieldType(f.GetField())); err != nil {
return nil, err
}
// a bool query of only should clauses needs one of them to match
selected := osu.NewBoolQuery()
for _, key := range f.GetTerms() {
selected.Should(osu.NewTermQuery[string](f.GetField()).Value(key))
}
for _, r := range f.GetRanges() {
q := osu.NewRangeQuery[string](f.GetField())
if r.GetFrom() != "" {
q = q.Gte(r.GetFrom())
}
if r.GetTo() != "" {
q = q.Lt(r.GetTo())
}
selected.Should(q)
}
out = append(out, selected)
}
return out, nil
}
+18 -2
View File
@@ -722,8 +722,8 @@ Fixtures:
Fixtures:
- `a.mp3`, MimeType = audio/mpeg
- `b.mp3`, MimeType = audio/mpeg
- `a.mp3`, MimeType = audio/mpeg, Tags = live, remaster
- `b.mp3`, MimeType = audio/mpeg, Tags = live
- `c.mp3`, MimeType = audio/mpeg
- `d.mp3`, MimeType = audio/mpeg
- `e.mp3`, MimeType = audio/mpeg
@@ -733,6 +733,7 @@ Fixtures:
- `b.jpg`, MimeType = image/jpeg
- `c.jpg`, MimeType = image/jpeg
- `d.jpg`, MimeType = image/jpeg
- `live.txt`
| Case | Query | expected | bleve | OpenSearch | same? |
|---|---|---|---|---|---|
@@ -754,12 +755,27 @@ Fixtures:
| AGG-18 | `mediatype:audio` reads `two range aggregations and a metric on audio.year stay apart` | audio.year ..1980=2, audio.year ..2000=4, audio.year 1980..=5, audio.year 2000..=3, audio.year max=2009 | audio.year ..1980=2, audio.year ..2000=4, audio.year 1980..=5, audio.year 2000..=3, audio.year max=2009 | audio.year ..1980=2, audio.year ..2000=4, audio.year 1980..=5, audio.year 2000..=3, audio.year max=2009 | ✅ |
| AGG-20 | `mediatype:audio` reads `album buckets in audio.year ranges in artist buckets` | audio.arti... Steel=2, audio.arti.....1980=0, audio.arti.....1980=2, audio.arti...1980..=0, audio.arti...1980..=3, audio.arti...Bomber=2, audio.arti...Spades=1, audio.artist Motörhead=3, … +1 more | audio.arti... Steel=2, audio.arti.....1980=0, audio.arti.....1980=2, audio.arti...1980..=0, audio.arti...1980..=3, audio.arti...Bomber=2, audio.arti...Spades=1, audio.artist Motörhead=3, … +1 more | audio.arti... Steel=2, audio.arti.....1980=0, audio.arti.....1980=2, audio.arti...1980..=0, audio.arti...1980..=3, audio.arti...Bomber=2, audio.arti...Spades=1, audio.artist Motörhead=3, … +1 more | ✅ |
| AGG-21 | `mediatype:image` reads `a metric without a single value has none` | audio.year avg none, audio.year min none, audio.year sum none | audio.year avg none, audio.year min none, audio.year sum none | audio.year avg none, audio.year min none, audio.year sum none | ✅ |
| AGG-22 | `mediatype:audio` reads `filtered to the Saxon bucket, as many matches as the bucket counted` | a.mp3, audio.artist Saxon=2, b.mp3 | a.mp3, audio.artist Saxon=2, b.mp3 | a.mp3, audio.artist Saxon=2, b.mp3 | ✅ |
| AGG-23 | `mediatype:audio` reads `filtered to saxon, a bucket key matches case-sensitively` | no match | no match | no match | ✅ |
| AGG-24 | `mediatype:audio` reads `filtered to the Saxon or the Motörhead bucket` | a.mp3, b.mp3, c.mp3, d.mp3, e.mp3 | a.mp3, b.mp3, c.mp3, d.mp3, e.mp3 | a.mp3, b.mp3, c.mp3, d.mp3, e.mp3 | ✅ |
| AGG-25 | `mediatype:audio` reads `filtered to audio.year 1980-1990` | c.mp3 | c.mp3 | c.mp3 | ✅ |
| AGG-26 | `mediatype:audio` reads `filtered to audio.year 1982-1999, from-inclusive and to-exclusive` | c.mp3 | c.mp3 | c.mp3 | ✅ |
| AGG-27 | `mediatype:audio` reads `filtered to audio.year 2000-` | e.mp3, f.mp3, g.mp3 | e.mp3, f.mp3, g.mp3 | e.mp3, f.mp3, g.mp3 | ✅ |
| AGG-28 | `mediatype:audio` reads `filtered to audio.year -1980 or 2005-` | a.mp3, b.mp3, f.mp3, g.mp3 | a.mp3, b.mp3, f.mp3, g.mp3 | a.mp3, b.mp3, f.mp3, g.mp3 | ✅ |
| AGG-29 | `mediatype:image` reads `filtered to photo.takenDateTime on 2018-08-11` | a.jpg, b.jpg | a.jpg, b.jpg | a.jpg, b.jpg | ✅ |
| AGG-30 | `mediatype:audio` reads `filtered to the Motörhead bucket and to audio.year 1990-2010` | d.mp3, e.mp3 | d.mp3, e.mp3 | d.mp3, e.mp3 | ✅ |
| AGG-31 | `*` reads `filtered to the bucket Wh*t? "Live", a key is data` | live.txt | live.txt | live.txt | ✅ |
| AGG-32 | `mediatype:audio` reads `filtered to Sax*, a key is no wildcard pattern` | no match | no match | no match | ✅ |
| AGG-33 | `mediatype:audio` reads `filtered to a malformed range` | bad request | bad request | bad request | ✅ |
| AGG-34 | `mediatype:audio` reads `term buckets on the numeric audio.year` | audio.year 1971=1, audio.year 1975=1, audio.year 1982=1, audio.year 1999=1, audio.year 2001=1, audio.year 2005=1, audio.year 2009=1 | audio.year 1971=1, audio.year 1975=1, audio.year 1982=1, audio.year 1999=1, audio.year 2001=1, audio.year 2005=1, audio.year 2009=1 | audio.year 1971=1, audio.year 1975=1, audio.year 1982=1, audio.year 1999=1, audio.year 2001=1, audio.year 2005=1, audio.year 2009=1 | ✅ |
| AGG-35 | `mediatype:audio` reads `audio.year buckets nested in artist buckets` | audio.arti...r 1971=1, audio.arti...r 1975=1, audio.arti...r 1982=1, audio.arti...r 1999=1, audio.arti...r 2001=1, audio.artist Motörhead=3, audio.artist Saxon=2 | audio.arti...r 1971=1, audio.arti...r 1975=1, audio.arti...r 1982=1, audio.arti...r 1999=1, audio.arti...r 2001=1, audio.artist Motörhead=3, audio.artist Saxon=2 | audio.arti...r 1971=1, audio.arti...r 1975=1, audio.arti...r 1982=1, audio.arti...r 1999=1, audio.arti...r 2001=1, audio.artist Motörhead=3, audio.artist Saxon=2 | ✅ |
| AGG-36 | `mediatype:audio` reads `filtered to the audio.year bucket 1982` | c.mp3 | c.mp3 | c.mp3 | ✅ |
| AGG-37 | `mediatype:audio` reads `term buckets on the bool audio.hasDrm, spelled true and false` | audio.hasDrm false=1, audio.hasDrm true=1 | audio.hasDrm false=1, audio.hasDrm true=1 | audio.hasDrm false=1, audio.hasDrm true=1 | ✅ |
| AGG-38 | `mediatype:audio` reads `filtered to the audio.hasDrm bucket true` | a.mp3 | a.mp3 | a.mp3 | ✅ |
| AGG-39 | `mediatype:audio` reads `no bucket for the empty Title of every match` | no match | no match | no match | ✅ |
| AGG-40 | `mediatype:audio` reads `no bucket for the empty Title, with sub-aggregations neither` | no match | no match | no match | ✅ |
| AGG-41 | `mediatype:audio` reads `matches of the same score in the order of their ids, page after page` | 3 of 7 matches, a.mp3, b.mp3, c.mp3 | 3 of 7 matches, a.mp3, b.mp3, c.mp3 | 3 of 7 matches, a.mp3, b.mp3, c.mp3 | ✅ |
| AGG-45 | `mediatype:audio` reads `filtered to the Tags bucket live, a filter narrows and does not rank` | a.mp3, b.mp3 | a.mp3, b.mp3 | a.mp3, b.mp3 | ✅ |
### cardinality
@@ -21,9 +21,12 @@ import (
// answer is rendered to strings (one per non-empty bucket or metric), so the
// matrix machinery can carry it like any query answer.
type aggCase struct {
id int
query string
aggs []*searchService.AggregationOption
id int
query string
aggs []*searchService.AggregationOption
// filters narrow the matches to buckets; the answer then starts with the
// names of the matches
filters []*searchService.AggregationFilter
reads string
want []string
wantCount *int
@@ -80,8 +83,8 @@ func withDrm(r search.Resource, drm bool) search.Resource {
func aggregationFixtures() []search.Resource {
return []search.Resource{
// years: 1971, 1975, 1982, 1999, 2001, 2005, 2009
withDrm(song("a.mp3", "Saxon", "Wheels of Steel", 1971), true),
withDrm(song("b.mp3", "Saxon", "Wheels of Steel", 1975), false),
withDrm(song("a.mp3", "Saxon", "Wheels of Steel", 1971, withTags("live", "remaster")), true),
withDrm(song("b.mp3", "Saxon", "Wheels of Steel", 1975, withTags("live")), false),
song("c.mp3", "Motörhead", "Bomber", 1982),
song("d.mp3", "Motörhead", "Bomber", 1999),
song("e.mp3", "Motörhead", "Ace of Spades", 2001),
@@ -91,6 +94,8 @@ func aggregationFixtures() []search.Resource {
photo("b.jpg", "2018-08-11T19:42:00Z"),
photo("c.jpg", "2018-09-01T12:00:00Z"),
photo("d.jpg", "2021-08-11T08:00:00Z"),
// no audio mime type: stays out of the mediatype:audio cases
fixtureDoc("live.txt", withAudio(&libregraph.Audio{Artist: libregraph.PtrString(`Wh*t? "Live"`)})),
}
}
@@ -101,6 +106,12 @@ func aggregationCases() []aggCase {
metric := func(field string, kind searchService.MetricKind) *searchService.AggregationOption {
return &searchService.AggregationOption{Field: field, MetricDefinition: &searchService.MetricDefinition{Kind: kind}}
}
terms := func(field string, keys ...string) *searchService.AggregationFilter {
return &searchService.AggregationFilter{Field: field, Terms: keys}
}
within := func(field string, rs ...*searchService.BucketRange) *searchService.AggregationFilter {
return &searchService.AggregationFilter{Field: field, Ranges: rs}
}
return []aggCase{
{id: 1, query: "mediatype:audio", reads: "term buckets on audio.artist",
@@ -256,6 +267,44 @@ func aggregationCases() []aggCase {
metric("audio.year", searchService.MetricKind_METRIC_KIND_AVG),
},
want: []string{"audio.year sum none", "audio.year min none", "audio.year avg none"}},
{id: 22, query: "mediatype:audio", reads: "filtered to the Saxon bucket, as many matches as the bucket counted",
filters: []*searchService.AggregationFilter{terms("audio.artist", "Saxon")},
aggs: []*searchService.AggregationOption{{Field: "audio.artist"}},
want: []string{"a.mp3", "b.mp3", "audio.artist Saxon=2"}},
{id: 23, query: "mediatype:audio", reads: "filtered to saxon, a bucket key matches case-sensitively",
filters: []*searchService.AggregationFilter{terms("audio.artist", "saxon")}},
{id: 24, query: "mediatype:audio", reads: "filtered to the Saxon or the Motörhead bucket",
filters: []*searchService.AggregationFilter{terms("audio.artist", "Saxon", "Motörhead")},
want: []string{"a.mp3", "b.mp3", "c.mp3", "d.mp3", "e.mp3"}},
{id: 25, query: "mediatype:audio", reads: "filtered to audio.year 1980-1990",
filters: []*searchService.AggregationFilter{within("audio.year", &searchService.BucketRange{From: "1980", To: "1990"})},
want: []string{"c.mp3"}},
{id: 26, query: "mediatype:audio", reads: "filtered to audio.year 1982-1999, from-inclusive and to-exclusive",
filters: []*searchService.AggregationFilter{within("audio.year", &searchService.BucketRange{From: "1982", To: "1999"})},
want: []string{"c.mp3"}},
{id: 27, query: "mediatype:audio", reads: "filtered to audio.year 2000-",
filters: []*searchService.AggregationFilter{within("audio.year", &searchService.BucketRange{From: "2000"})},
want: []string{"e.mp3", "f.mp3", "g.mp3"}},
{id: 28, query: "mediatype:audio", reads: "filtered to audio.year -1980 or 2005-",
filters: []*searchService.AggregationFilter{within("audio.year", &searchService.BucketRange{To: "1980"}, &searchService.BucketRange{From: "2005"})},
want: []string{"a.mp3", "b.mp3", "f.mp3", "g.mp3"}},
{id: 29, query: "mediatype:image", reads: "filtered to photo.takenDateTime on 2018-08-11",
filters: []*searchService.AggregationFilter{within("photo.takenDateTime", &searchService.BucketRange{From: "2018-08-11T00:00:00Z", To: "2018-08-12T00:00:00Z"})},
want: []string{"a.jpg", "b.jpg"}},
{id: 30, query: "mediatype:audio", reads: "filtered to the Motörhead bucket and to audio.year 1990-2010",
filters: []*searchService.AggregationFilter{
terms("audio.artist", "Motörhead"),
within("audio.year", &searchService.BucketRange{From: "1990", To: "2010"}),
},
want: []string{"d.mp3", "e.mp3"}},
{id: 31, query: "*", reads: `filtered to the bucket Wh*t? "Live", a key is data`,
filters: []*searchService.AggregationFilter{terms("audio.artist", `Wh*t? "Live"`)},
want: []string{"live.txt"}},
{id: 32, query: "mediatype:audio", reads: "filtered to Sax*, a key is no wildcard pattern",
filters: []*searchService.AggregationFilter{terms("audio.artist", "Sax*")}},
{id: 33, query: "mediatype:audio", reads: "filtered to a malformed range",
filters: []*searchService.AggregationFilter{within("audio.year", &searchService.BucketRange{From: "1980 OR Name:*", To: "1990"})},
wantBadRequest: true},
{id: 34, query: "mediatype:audio", reads: "term buckets on the numeric audio.year",
aggs: []*searchService.AggregationOption{{Field: "audio.year"}},
want: []string{
@@ -269,9 +318,15 @@ func aggregationCases() []aggCase {
"audio.artist Motörhead=3", "audio.artist Motörhead=3 / audio.year 1982=1",
"audio.artist Motörhead=3 / audio.year 1999=1", "audio.artist Motörhead=3 / audio.year 2001=1",
}},
{id: 36, query: "mediatype:audio", reads: "filtered to the audio.year bucket 1982",
filters: []*searchService.AggregationFilter{terms("audio.year", "1982")},
want: []string{"c.mp3"}},
{id: 37, query: "mediatype:audio", reads: "term buckets on the bool audio.hasDrm, spelled true and false",
aggs: []*searchService.AggregationOption{{Field: "audio.hasDrm"}},
want: []string{"audio.hasDrm true=1", "audio.hasDrm false=1"}},
{id: 38, query: "mediatype:audio", reads: "filtered to the audio.hasDrm bucket true",
filters: []*searchService.AggregationFilter{terms("audio.hasDrm", "true")},
want: []string{"a.mp3"}},
{id: 39, query: "mediatype:audio", reads: "no bucket for the empty Title of every match",
aggs: []*searchService.AggregationOption{{Field: "Title"}}},
{id: 40, query: "mediatype:audio", reads: "no bucket for the empty Title, with sub-aggregations neither",
@@ -279,6 +334,9 @@ func aggregationCases() []aggCase {
{id: 41, query: "mediatype:audio", reads: "matches of the same score in the order of their ids, page after page",
pageSize: 3, listMatches: true,
want: []string{"3 of 7 matches", "a.mp3", "b.mp3", "c.mp3"}},
{id: 45, query: "mediatype:audio", reads: "filtered to the Tags bucket live, a filter narrows and does not rank",
filters: []*searchService.AggregationFilter{terms("Tags", "live")}, listMatches: true,
want: []string{"a.mp3", "b.mp3"}},
}
}
@@ -390,15 +448,16 @@ var _ = Describe("Aggregations", func() {
}
request := &searchService.SearchIndexRequest{
Query: c.query,
Aggregations: c.aggs,
Query: c.query,
Aggregations: c.aggs,
AggregationFilters: c.filters,
}
if c.pageSize != 0 {
request.PageSize = conversions.ToPointer(c.pageSize)
}
resp, err := e.backend.Search(context.Background(), request)
answer := renderAggregations(resp, err)
if err == nil && c.listMatches {
if err == nil && (len(c.filters) > 0 || c.listMatches) {
names := make([]string, 0, len(resp.Matches))
for _, m := range resp.Matches {
names = append(names, m.GetEntity().GetName())
+6 -2
View File
@@ -106,6 +106,9 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se
if err := aggregation.ValidateOptions(req.GetAggregations(), AggregatableFieldType); err != nil {
return nil, errtypes.BadRequest(err.Error())
}
if err := aggregation.ValidateFilters(req.GetAggregationFilters(), AggregatableFieldType); err != nil {
return nil, errtypes.BadRequest(err.Error())
}
s.logger.Debug().Str("query", req.Query).Msg("performing a search")
// collect metrics
@@ -463,8 +466,9 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest,
}
searchRequest := &searchsvc.SearchIndexRequest{
Query: req.Query,
Aggregations: req.GetAggregations(),
Query: req.Query,
Aggregations: req.GetAggregations(),
AggregationFilters: req.GetAggregationFilters(),
Ref: &searchmsg.Reference{
ResourceId: searchRootID,
Path: searchPathPrefix,
@@ -289,6 +289,8 @@ var _ = Describe("Searchprovider", func() {
// the rules are pinned in the aggregation package
Entry("an aggregation on an internal field", &searchsvc.SearchRequest{Query: "foo",
Aggregations: []*searchsvc.AggregationOption{{Field: "Favorites"}}}),
Entry("a filter on an internal field", &searchsvc.SearchRequest{Query: "foo",
AggregationFilters: []*searchsvc.AggregationFilter{{Field: "Favorites", Terms: []string{"user-a"}}}}),
)
// two personal spaces, the engine answers each by its space id