diff --git a/protogen/gen/opencloud/services/search/v0/search.pb.go b/protogen/gen/opencloud/services/search/v0/search.pb.go index 7f5a84efdb..ee1fb16cb0 100644 --- a/protogen/gen/opencloud/services/search/v0/search.pb.go +++ b/protogen/gen/opencloud/services/search/v0/search.pb.go @@ -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, }, diff --git a/protogen/gen/opencloud/services/search/v0/search.swagger.json b/protogen/gen/opencloud/services/search/v0/search.swagger.json index d932e3c7da..f2e261363b 100644 --- a/protogen/gen/opencloud/services/search/v0/search.swagger.json +++ b/protogen/gen/opencloud/services/search/v0/search.swagger.json @@ -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", diff --git a/protogen/proto/opencloud/services/search/v0/search.proto b/protogen/proto/opencloud/services/search/v0/search.proto index 2bc2ff12cf..035276f7b5 100644 --- a/protogen/proto/opencloud/services/search/v0/search.proto +++ b/protogen/proto/opencloud/services/search/v0/search.proto @@ -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; diff --git a/services/graph/pkg/filtertoken/filtertoken_suite_test.go b/services/graph/pkg/filtertoken/filtertoken_suite_test.go new file mode 100644 index 0000000000..69965ae5fe --- /dev/null +++ b/services/graph/pkg/filtertoken/filtertoken_suite_test.go @@ -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") +} diff --git a/services/graph/pkg/filtertoken/token.go b/services/graph/pkg/filtertoken/token.go new file mode 100644 index 0000000000..ecd52319d4 --- /dev/null +++ b/services/graph/pkg/filtertoken/token.go @@ -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:])) +} diff --git a/services/graph/pkg/filtertoken/token_test.go b/services/graph/pkg/filtertoken/token_test.go new file mode 100644 index 0000000000..0bc7d0b8b2 --- /dev/null +++ b/services/graph/pkg/filtertoken/token_test.go @@ -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"}})) + }) + }) +}) diff --git a/services/graph/pkg/service/v0/searchquery.go b/services/graph/pkg/service/v0/searchquery.go index a899b52393..409306710b 100644 --- a/services/graph/pkg/service/v0/searchquery.go +++ b/services/graph/pkg/service/v0/searchquery.go @@ -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 } diff --git a/services/graph/pkg/service/v0/searchquery_mapping.go b/services/graph/pkg/service/v0/searchquery_mapping.go index d5344fbf42..354d25269c 100644 --- a/services/graph/pkg/service/v0/searchquery_mapping.go +++ b/services/graph/pkg/service/v0/searchquery_mapping.go @@ -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(), diff --git a/services/graph/pkg/service/v0/searchquery_test.go b/services/graph/pkg/service/v0/searchquery_test.go index ae94c90fb5..993e3d8152 100644 --- a/services/graph/pkg/service/v0/searchquery_test.go +++ b/services/graph/pkg/service/v0/searchquery_test.go @@ -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"}}}), + ) }) diff --git a/services/search/pkg/aggregation/filter.go b/services/search/pkg/aggregation/filter.go new file mode 100644 index 0000000000..63e116ea63 --- /dev/null +++ b/services/search/pkg/aggregation/filter.go @@ -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 +} diff --git a/services/search/pkg/aggregation/filter_test.go b/services/search/pkg/aggregation/filter_test.go new file mode 100644 index 0000000000..605e64a523 --- /dev/null +++ b/services/search/pkg/aggregation/filter_test.go @@ -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"}}}), + ) +}) diff --git a/services/search/pkg/aggregation/validate.go b/services/search/pkg/aggregation/validate.go index 14d72e1035..9169245a5a 100644 --- a/services/search/pkg/aggregation/validate.go +++ b/services/search/pkg/aggregation/validate.go @@ -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 +} diff --git a/services/search/pkg/bleve/backend.go b/services/search/pkg/bleve/backend.go index 45f778e774..80a0792d23 100644 --- a/services/search/pkg/bleve/backend.go +++ b/services/search/pkg/bleve/backend.go @@ -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, diff --git a/services/search/pkg/bleve/filters.go b/services/search/pkg/bleve/filters.go new file mode 100644 index 0000000000..280855abe1 --- /dev/null +++ b/services/search/pkg/bleve/filters.go @@ -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 +} diff --git a/services/search/pkg/opensearch/backend.go b/services/search/pkg/opensearch/backend.go index da9e1115d4..f600194f45 100644 --- a/services/search/pkg/opensearch/backend.go +++ b/services/search/pkg/opensearch/backend.go @@ -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), diff --git a/services/search/pkg/opensearch/filters.go b/services/search/pkg/opensearch/filters.go new file mode 100644 index 0000000000..91ead49d8b --- /dev/null +++ b/services/search/pkg/opensearch/filters.go @@ -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 +} diff --git a/services/search/pkg/parity/README.md b/services/search/pkg/parity/README.md index 0272eb4ff2..047a4fd5ae 100644 --- a/services/search/pkg/parity/README.md +++ b/services/search/pkg/parity/README.md @@ -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 diff --git a/services/search/pkg/parity/aggregations_test.go b/services/search/pkg/parity/aggregations_test.go index 1bb94fdab0..2e896747e1 100644 --- a/services/search/pkg/parity/aggregations_test.go +++ b/services/search/pkg/parity/aggregations_test.go @@ -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()) diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index fae6814535..07ece77eb3 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -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, diff --git a/services/search/pkg/search/service_test.go b/services/search/pkg/search/service_test.go index d68323e9a5..668986e5a6 100644 --- a/services/search/pkg/search/service_test.go +++ b/services/search/pkg/search/service_test.go @@ -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