From 8581fbe253b9c65171543b6fab21d742ad701f43 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 12 Aug 2026 16:14:47 +0200 Subject: [PATCH 1/6] feat(search): add sortProperties plumbing (libregraph model, proto order_by) (cherry picked from commit 1b748fb757e2dde5b3cd1beaa543be4266342590) --- .../opencloud/services/search/v0/search.pb.go | 754 ++++++++++-------- .../services/search/v0/search.swagger.json | 27 + .../opencloud/services/search/v0/search.proto | 20 + 3 files changed, 481 insertions(+), 320 deletions(-) diff --git a/protogen/gen/opencloud/services/search/v0/search.pb.go b/protogen/gen/opencloud/services/search/v0/search.pb.go index 27f8cf5ab6..9f4b9832b7 100644 --- a/protogen/gen/opencloud/services/search/v0/search.pb.go +++ b/protogen/gen/opencloud/services/search/v0/search.pb.go @@ -185,7 +185,7 @@ func (x IndexSpaceResponse_Status) Number() protoreflect.EnumNumber { // Deprecated: Use IndexSpaceResponse_Status.Descriptor instead. func (IndexSpaceResponse_Status) EnumDescriptor() ([]byte, []int) { - return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{13, 0} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{14, 0} } type SearchRequest struct { @@ -208,6 +208,11 @@ type SearchRequest struct { // 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"` + // Optional. Fields to sort the matches by, in order of precedence. When + // empty, matches are sorted by relevance score. Only a subset of the + // indexed fields is sortable; the graph service validates this before + // forwarding the request. + OrderBy []*SortProperty `protobuf:"bytes,8,rep,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` } func (x *SearchRequest) Reset() { @@ -291,6 +296,13 @@ func (x *SearchRequest) GetFrom() int32 { return 0 } +func (x *SearchRequest) GetOrderBy() []*SortProperty { + if x != nil { + return x.OrderBy + } + return nil +} + type SearchResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -383,6 +395,10 @@ type SearchIndexRequest struct { // 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"` + // Optional. Fields to sort the matches by, in order of precedence. When + // empty, matches are sorted by relevance score. Each backend translates + // this to its native sort (bleve: SortBy, OpenSearch: sort clause). + OrderBy []*SortProperty `protobuf:"bytes,7,rep,name=order_by,json=orderBy,proto3" json:"order_by,omitempty"` } func (x *SearchIndexRequest) Reset() { @@ -459,6 +475,13 @@ func (x *SearchIndexRequest) GetAggregationFilters() []*AggregationFilter { return nil } +func (x *SearchIndexRequest) GetOrderBy() []*SortProperty { + if x != nil { + return x.OrderBy + } + return nil +} + type SearchIndexResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -673,6 +696,67 @@ func (x *MetricDefinition) GetKind() MetricKind { return MetricKind_METRIC_KIND_UNSPECIFIED } +type SortProperty struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Required. The field to sort on, in graph notation. Supported: "name", + // "size", "lastModifiedDateTime", "photo.takenDateTime". A field is only + // sortable if it is indexed as a sortable type in both backends AND + // present on the Match entity (the service layer needs the sort key to + // merge per-space result streams). + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + // Optional. Sort in descending order. Defaults to ascending. + IsDescending bool `protobuf:"varint,2,opt,name=is_descending,json=isDescending,proto3" json:"is_descending,omitempty"` +} + +func (x *SortProperty) Reset() { + *x = SortProperty{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[6] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *SortProperty) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*SortProperty) ProtoMessage() {} + +func (x *SortProperty) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[6] + 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 SortProperty.ProtoReflect.Descriptor instead. +func (*SortProperty) Descriptor() ([]byte, []int) { + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{6} +} + +func (x *SortProperty) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *SortProperty) GetIsDescending() bool { + if x != nil { + return x.IsDescending + } + return false +} + type BucketDefinition struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -691,7 +775,7 @@ type BucketDefinition struct { func (x *BucketDefinition) Reset() { *x = BucketDefinition{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[6] + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[7] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -704,7 +788,7 @@ func (x *BucketDefinition) String() string { func (*BucketDefinition) ProtoMessage() {} func (x *BucketDefinition) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[6] + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[7] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -717,7 +801,7 @@ func (x *BucketDefinition) ProtoReflect() protoreflect.Message { // Deprecated: Use BucketDefinition.ProtoReflect.Descriptor instead. func (*BucketDefinition) Descriptor() ([]byte, []int) { - return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{6} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{7} } func (x *BucketDefinition) GetSortBy() BucketSortBy { @@ -763,7 +847,7 @@ type BucketRange struct { func (x *BucketRange) Reset() { *x = BucketRange{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[7] + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[8] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -776,7 +860,7 @@ func (x *BucketRange) String() string { func (*BucketRange) ProtoMessage() {} func (x *BucketRange) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[7] + 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 { @@ -789,7 +873,7 @@ func (x *BucketRange) ProtoReflect() protoreflect.Message { // Deprecated: Use BucketRange.ProtoReflect.Descriptor instead. func (*BucketRange) Descriptor() ([]byte, []int) { - return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{7} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{8} } func (x *BucketRange) GetFrom() string { @@ -823,7 +907,7 @@ type AggregationFilter struct { func (x *AggregationFilter) Reset() { *x = AggregationFilter{} 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) } @@ -836,7 +920,7 @@ func (x *AggregationFilter) String() string { func (*AggregationFilter) ProtoMessage() {} func (x *AggregationFilter) 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 { @@ -849,7 +933,7 @@ func (x *AggregationFilter) ProtoReflect() protoreflect.Message { // Deprecated: Use AggregationFilter.ProtoReflect.Descriptor instead. func (*AggregationFilter) 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 *AggregationFilter) GetField() string { @@ -889,7 +973,7 @@ type AggregationResult struct { func (x *AggregationResult) Reset() { *x = AggregationResult{} 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) } @@ -902,7 +986,7 @@ func (x *AggregationResult) String() string { func (*AggregationResult) ProtoMessage() {} func (x *AggregationResult) 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 { @@ -915,7 +999,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{9} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{10} } func (x *AggregationResult) GetField() string { @@ -961,7 +1045,7 @@ type Metric struct { func (x *Metric) Reset() { *x = Metric{} 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) } @@ -974,7 +1058,7 @@ func (x *Metric) String() string { func (*Metric) ProtoMessage() {} func (x *Metric) 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 { @@ -987,7 +1071,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{10} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{11} } func (x *Metric) GetKind() MetricKind { @@ -1047,7 +1131,7 @@ type Bucket struct { func (x *Bucket) Reset() { *x = Bucket{} 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) } @@ -1060,7 +1144,7 @@ func (x *Bucket) String() string { func (*Bucket) ProtoMessage() {} func (x *Bucket) 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 { @@ -1073,7 +1157,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{11} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{12} } func (x *Bucket) GetKey() string { @@ -1111,7 +1195,7 @@ type IndexSpaceRequest struct { func (x *IndexSpaceRequest) Reset() { *x = IndexSpaceRequest{} 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) } @@ -1124,7 +1208,7 @@ func (x *IndexSpaceRequest) String() string { func (*IndexSpaceRequest) ProtoMessage() {} func (x *IndexSpaceRequest) 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 { @@ -1137,7 +1221,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{12} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{13} } func (x *IndexSpaceRequest) GetSpaceId() string { @@ -1190,7 +1274,7 @@ type IndexSpaceResponse struct { func (x *IndexSpaceResponse) Reset() { *x = IndexSpaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[13] + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1203,7 +1287,7 @@ func (x *IndexSpaceResponse) String() string { func (*IndexSpaceResponse) ProtoMessage() {} func (x *IndexSpaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[13] + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[14] if protoimpl.UnsafeEnabled && x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1216,7 +1300,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{13} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{14} } func (x *IndexSpaceResponse) GetSpaceId() string { @@ -1282,7 +1366,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, 0x9e, 0x03, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xeb, 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, @@ -1307,252 +1391,267 @@ var file_opencloud_services_search_v0_search_proto_rawDesc = []byte{ 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, - 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, - 0xeb, 0x02, 0x0a, 0x11, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, - 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x18, 0x0a, 0x04, 0x73, - 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, - 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, 0x61, 0x0a, 0x11, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x5f, - 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x2e, 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, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, - 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x10, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x65, - 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x61, 0x0a, 0x11, 0x6d, 0x65, 0x74, 0x72, - 0x69, 0x63, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, - 0x69, 0x6f, 0x6e, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x10, 0x6d, 0x65, 0x74, 0x72, 0x69, - 0x63, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x60, 0x0a, 0x10, 0x73, - 0x75, 0x62, 0x5f, 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, 0x0f, 0x73, 0x75, - 0x62, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x50, 0x0a, - 0x10, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, - 0x6e, 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, 0x22, - 0xe4, 0x01, 0x0a, 0x10, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, - 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, - 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 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, 0x53, 0x6f, 0x72, 0x74, 0x42, - 0x79, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x73, 0x5f, - 0x64, 0x65, 0x73, 0x63, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0c, 0x69, 0x73, 0x44, 0x65, 0x73, 0x63, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x12, 0x23, - 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, - 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x43, 0x6f, - 0x75, 0x6e, 0x74, 0x12, 0x41, 0x0a, 0x06, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, 0x18, 0x04, 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, 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, 0x82, 0x01, 0x0a, 0x11, 0x41, 0x67, - 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, 0x6c, 0x74, 0x65, 0x72, 0x12, + 0x01, 0x01, 0x52, 0x04, 0x66, 0x72, 0x6f, 0x6d, 0x12, 0x4b, 0x0a, 0x08, 0x6f, 0x72, 0x64, 0x65, + 0x72, 0x5f, 0x62, 0x79, 0x18, 0x08, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 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, 0x6f, 0x72, 0x74, 0x50, 0x72, + 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x07, 0x6f, 0x72, + 0x64, 0x65, 0x72, 0x42, 0x79, 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, 0xd6, 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, 0x12, 0x4b, 0x0a, 0x08, 0x6f, 0x72, 0x64, 0x65, 0x72, 0x5f, 0x62, + 0x79, 0x18, 0x07, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2a, 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, 0x6f, 0x72, 0x74, 0x50, 0x72, 0x6f, 0x70, 0x65, + 0x72, 0x74, 0x79, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x07, 0x6f, 0x72, 0x64, 0x65, 0x72, + 0x42, 0x79, 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, 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, 0xeb, 0x02, 0x0a, 0x11, 0x41, 0x67, + 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, - 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, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x12, 0x18, 0x0a, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x04, 0x73, 0x69, 0x7a, 0x65, 0x12, + 0x61, 0x0a, 0x11, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x5f, 0x64, 0x65, 0x66, 0x69, 0x6e, 0x69, + 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, + 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, + 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, + 0x52, 0x10, 0x62, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x61, 0x0a, 0x11, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x5f, 0x64, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 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, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x42, 0x04, 0xe2, + 0x41, 0x01, 0x01, 0x52, 0x10, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x44, 0x65, 0x66, 0x69, 0x6e, + 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x60, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x5f, 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, 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, 0xfa, 0x02, 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, 0x12, 0x4f, 0x0a, 0x06, - 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x37, 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, 0x2e, 0x53, - 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x22, 0x56, 0x0a, - 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, - 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, 0x0a, 0x0e, 0x53, - 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, 0x10, 0x01, 0x12, - 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x4b, 0x49, 0x50, 0x50, 0x45, - 0x44, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x45, 0x52, - 0x52, 0x4f, 0x52, 0x10, 0x03, 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, + 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4f, 0x70, 0x74, 0x69, 0x6f, 0x6e, + 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x0f, 0x73, 0x75, 0x62, 0x41, 0x67, 0x67, 0x72, 0x65, + 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x50, 0x0a, 0x10, 0x4d, 0x65, 0x74, 0x72, 0x69, + 0x63, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 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, 0x22, 0x47, 0x0a, 0x0c, 0x53, 0x6f, 0x72, + 0x74, 0x50, 0x72, 0x6f, 0x70, 0x65, 0x72, 0x74, 0x79, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, + 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x23, 0x0a, + 0x0d, 0x69, 0x73, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x73, 0x44, 0x65, 0x73, 0x63, 0x65, 0x6e, 0x64, 0x69, + 0x6e, 0x67, 0x22, 0xe4, 0x01, 0x0a, 0x10, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x44, 0x65, 0x66, + 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x43, 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, + 0x62, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x2a, 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, 0x53, 0x6f, + 0x72, 0x74, 0x42, 0x79, 0x52, 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x23, 0x0a, 0x0d, + 0x69, 0x73, 0x5f, 0x64, 0x65, 0x73, 0x63, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x18, 0x02, 0x20, + 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x73, 0x44, 0x65, 0x73, 0x63, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x12, 0x23, 0x0a, 0x0d, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, 0x6d, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0c, 0x6d, 0x69, 0x6e, 0x69, 0x6d, 0x75, + 0x6d, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x41, 0x0a, 0x06, 0x72, 0x61, 0x6e, 0x67, 0x65, 0x73, + 0x18, 0x04, 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, 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, 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, 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, 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, + 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, 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, + 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, 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, 0xfa, 0x02, 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, 0x12, + 0x4f, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0e, 0x32, + 0x37, 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, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x22, 0x56, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, + 0x41, 0x54, 0x55, 0x53, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x12, + 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x55, 0x43, 0x43, 0x45, 0x53, 0x53, + 0x10, 0x01, 0x12, 0x12, 0x0a, 0x0e, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, 0x5f, 0x53, 0x4b, 0x49, + 0x50, 0x50, 0x45, 0x44, 0x10, 0x02, 0x12, 0x10, 0x0a, 0x0c, 0x53, 0x54, 0x41, 0x54, 0x55, 0x53, + 0x5f, 0x45, 0x52, 0x52, 0x4f, 0x52, 0x10, 0x03, 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, 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 ( @@ -1568,7 +1667,7 @@ func file_opencloud_services_search_v0_search_proto_rawDescGZIP() []byte { } var file_opencloud_services_search_v0_search_proto_enumTypes = make([]protoimpl.EnumInfo, 3) -var file_opencloud_services_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 14) +var file_opencloud_services_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 15) 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 @@ -1579,53 +1678,56 @@ var file_opencloud_services_search_v0_search_proto_goTypes = []interface{}{ (*SearchIndexResponse)(nil), // 6: opencloud.services.search.v0.SearchIndexResponse (*AggregationOption)(nil), // 7: opencloud.services.search.v0.AggregationOption (*MetricDefinition)(nil), // 8: opencloud.services.search.v0.MetricDefinition - (*BucketDefinition)(nil), // 9: opencloud.services.search.v0.BucketDefinition - (*BucketRange)(nil), // 10: opencloud.services.search.v0.BucketRange - (*AggregationFilter)(nil), // 11: opencloud.services.search.v0.AggregationFilter - (*AggregationResult)(nil), // 12: opencloud.services.search.v0.AggregationResult - (*Metric)(nil), // 13: opencloud.services.search.v0.Metric - (*Bucket)(nil), // 14: opencloud.services.search.v0.Bucket - (*IndexSpaceRequest)(nil), // 15: opencloud.services.search.v0.IndexSpaceRequest - (*IndexSpaceResponse)(nil), // 16: opencloud.services.search.v0.IndexSpaceResponse - (*v0.Reference)(nil), // 17: opencloud.messages.search.v0.Reference - (*v0.Match)(nil), // 18: opencloud.messages.search.v0.Match - (*durationpb.Duration)(nil), // 19: google.protobuf.Duration + (*SortProperty)(nil), // 9: opencloud.services.search.v0.SortProperty + (*BucketDefinition)(nil), // 10: opencloud.services.search.v0.BucketDefinition + (*BucketRange)(nil), // 11: opencloud.services.search.v0.BucketRange + (*AggregationFilter)(nil), // 12: opencloud.services.search.v0.AggregationFilter + (*AggregationResult)(nil), // 13: opencloud.services.search.v0.AggregationResult + (*Metric)(nil), // 14: opencloud.services.search.v0.Metric + (*Bucket)(nil), // 15: opencloud.services.search.v0.Bucket + (*IndexSpaceRequest)(nil), // 16: opencloud.services.search.v0.IndexSpaceRequest + (*IndexSpaceResponse)(nil), // 17: opencloud.services.search.v0.IndexSpaceResponse + (*v0.Reference)(nil), // 18: opencloud.messages.search.v0.Reference + (*v0.Match)(nil), // 19: opencloud.messages.search.v0.Match + (*durationpb.Duration)(nil), // 20: google.protobuf.Duration } var file_opencloud_services_search_v0_search_proto_depIdxs = []int32{ - 17, // 0: opencloud.services.search.v0.SearchRequest.ref:type_name -> opencloud.messages.search.v0.Reference + 18, // 0: opencloud.services.search.v0.SearchRequest.ref:type_name -> opencloud.messages.search.v0.Reference 7, // 1: opencloud.services.search.v0.SearchRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption - 11, // 2: opencloud.services.search.v0.SearchRequest.aggregation_filters:type_name -> opencloud.services.search.v0.AggregationFilter - 18, // 3: opencloud.services.search.v0.SearchResponse.matches:type_name -> opencloud.messages.search.v0.Match - 12, // 4: opencloud.services.search.v0.SearchResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult - 17, // 5: opencloud.services.search.v0.SearchIndexRequest.ref:type_name -> opencloud.messages.search.v0.Reference - 7, // 6: opencloud.services.search.v0.SearchIndexRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption - 11, // 7: opencloud.services.search.v0.SearchIndexRequest.aggregation_filters:type_name -> opencloud.services.search.v0.AggregationFilter - 18, // 8: opencloud.services.search.v0.SearchIndexResponse.matches:type_name -> opencloud.messages.search.v0.Match - 12, // 9: opencloud.services.search.v0.SearchIndexResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult - 9, // 10: opencloud.services.search.v0.AggregationOption.bucket_definition:type_name -> opencloud.services.search.v0.BucketDefinition - 8, // 11: opencloud.services.search.v0.AggregationOption.metric_definition:type_name -> opencloud.services.search.v0.MetricDefinition - 7, // 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 - 10, // 15: opencloud.services.search.v0.BucketDefinition.ranges:type_name -> opencloud.services.search.v0.BucketRange - 10, // 16: opencloud.services.search.v0.AggregationFilter.ranges:type_name -> opencloud.services.search.v0.BucketRange - 14, // 17: opencloud.services.search.v0.AggregationResult.buckets:type_name -> opencloud.services.search.v0.Bucket - 13, // 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 - 12, // 20: opencloud.services.search.v0.Bucket.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationResult - 19, // 21: opencloud.services.search.v0.IndexSpaceResponse.space_duration:type_name -> google.protobuf.Duration - 2, // 22: opencloud.services.search.v0.IndexSpaceResponse.status:type_name -> opencloud.services.search.v0.IndexSpaceResponse.Status - 3, // 23: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest - 15, // 24: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest - 5, // 25: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest - 4, // 26: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse - 16, // 27: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse - 6, // 28: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse - 26, // [26:29] is the sub-list for method output_type - 23, // [23:26] is the sub-list for method input_type - 23, // [23:23] is the sub-list for extension type_name - 23, // [23:23] is the sub-list for extension extendee - 0, // [0:23] is the sub-list for field type_name + 12, // 2: opencloud.services.search.v0.SearchRequest.aggregation_filters:type_name -> opencloud.services.search.v0.AggregationFilter + 9, // 3: opencloud.services.search.v0.SearchRequest.order_by:type_name -> opencloud.services.search.v0.SortProperty + 19, // 4: opencloud.services.search.v0.SearchResponse.matches:type_name -> opencloud.messages.search.v0.Match + 13, // 5: opencloud.services.search.v0.SearchResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult + 18, // 6: opencloud.services.search.v0.SearchIndexRequest.ref:type_name -> opencloud.messages.search.v0.Reference + 7, // 7: opencloud.services.search.v0.SearchIndexRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption + 12, // 8: opencloud.services.search.v0.SearchIndexRequest.aggregation_filters:type_name -> opencloud.services.search.v0.AggregationFilter + 9, // 9: opencloud.services.search.v0.SearchIndexRequest.order_by:type_name -> opencloud.services.search.v0.SortProperty + 19, // 10: opencloud.services.search.v0.SearchIndexResponse.matches:type_name -> opencloud.messages.search.v0.Match + 13, // 11: opencloud.services.search.v0.SearchIndexResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult + 10, // 12: opencloud.services.search.v0.AggregationOption.bucket_definition:type_name -> opencloud.services.search.v0.BucketDefinition + 8, // 13: opencloud.services.search.v0.AggregationOption.metric_definition:type_name -> opencloud.services.search.v0.MetricDefinition + 7, // 14: opencloud.services.search.v0.AggregationOption.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationOption + 0, // 15: opencloud.services.search.v0.MetricDefinition.kind:type_name -> opencloud.services.search.v0.MetricKind + 1, // 16: opencloud.services.search.v0.BucketDefinition.sort_by:type_name -> opencloud.services.search.v0.BucketSortBy + 11, // 17: opencloud.services.search.v0.BucketDefinition.ranges:type_name -> opencloud.services.search.v0.BucketRange + 11, // 18: opencloud.services.search.v0.AggregationFilter.ranges:type_name -> opencloud.services.search.v0.BucketRange + 15, // 19: opencloud.services.search.v0.AggregationResult.buckets:type_name -> opencloud.services.search.v0.Bucket + 14, // 20: opencloud.services.search.v0.AggregationResult.metric:type_name -> opencloud.services.search.v0.Metric + 0, // 21: opencloud.services.search.v0.Metric.kind:type_name -> opencloud.services.search.v0.MetricKind + 13, // 22: opencloud.services.search.v0.Bucket.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationResult + 20, // 23: opencloud.services.search.v0.IndexSpaceResponse.space_duration:type_name -> google.protobuf.Duration + 2, // 24: opencloud.services.search.v0.IndexSpaceResponse.status:type_name -> opencloud.services.search.v0.IndexSpaceResponse.Status + 3, // 25: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest + 16, // 26: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest + 5, // 27: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest + 4, // 28: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse + 17, // 29: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse + 6, // 30: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse + 28, // [28:31] is the sub-list for method output_type + 25, // [25:28] is the sub-list for method input_type + 25, // [25:25] is the sub-list for extension type_name + 25, // [25:25] is the sub-list for extension extendee + 0, // [0:25] is the sub-list for field type_name } func init() { file_opencloud_services_search_v0_search_proto_init() } @@ -1707,7 +1809,7 @@ func file_opencloud_services_search_v0_search_proto_init() { } } file_opencloud_services_search_v0_search_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BucketDefinition); i { + switch v := v.(*SortProperty); i { case 0: return &v.state case 1: @@ -1719,7 +1821,7 @@ func file_opencloud_services_search_v0_search_proto_init() { } } file_opencloud_services_search_v0_search_proto_msgTypes[7].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*BucketRange); i { + switch v := v.(*BucketDefinition); i { case 0: return &v.state case 1: @@ -1731,7 +1833,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.(*AggregationFilter); i { + switch v := v.(*BucketRange); i { case 0: return &v.state case 1: @@ -1743,7 +1845,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.(*AggregationResult); i { + switch v := v.(*AggregationFilter); i { case 0: return &v.state case 1: @@ -1755,7 +1857,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.(*Metric); i { + switch v := v.(*AggregationResult); i { case 0: return &v.state case 1: @@ -1767,7 +1869,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.(*Bucket); i { + switch v := v.(*Metric); i { case 0: return &v.state case 1: @@ -1779,7 +1881,7 @@ 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 { + switch v := v.(*Bucket); i { case 0: return &v.state case 1: @@ -1791,6 +1893,18 @@ func file_opencloud_services_search_v0_search_proto_init() { } } file_opencloud_services_search_v0_search_proto_msgTypes[13].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[14].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IndexSpaceResponse); i { case 0: return &v.state @@ -1805,14 +1919,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[10].OneofWrappers = []interface{}{} + file_opencloud_services_search_v0_search_proto_msgTypes[11].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: 3, - NumMessages: 14, + NumMessages: 15, 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 3f6d56eab5..d792b10ecd 100644 --- a/protogen/gen/opencloud/services/search/v0/search.swagger.json +++ b/protogen/gen/opencloud/services/search/v0/search.swagger.json @@ -742,6 +742,13 @@ "$ref": "#/definitions/v0AggregationFilter" }, "description": "Optional. Filters narrowing the matches to buckets of a prior\naggregation; the engine ANDs them with `query`." + }, + "orderBy": { + "type": "array", + "items": { + "$ref": "#/definitions/v0SortProperty" + }, + "description": "Optional. Fields to sort the matches by, in order of precedence. When\nempty, matches are sorted by relevance score. Each backend translates\nthis to its native sort (bleve: SortBy, OpenSearch: sort clause)." } } }, @@ -807,6 +814,13 @@ "type": "integer", "format": "int32", "description": "Optional. Number of leading matches to skip in the globally sorted,\ncross-space merged result list." + }, + "orderBy": { + "type": "array", + "items": { + "$ref": "#/definitions/v0SortProperty" + }, + "description": "Optional. Fields to sort the matches by, in order of precedence. When\nempty, matches are sorted by relevance score. Only a subset of the\nindexed fields is sortable; the graph service validates this before\nforwarding the request." } } }, @@ -836,6 +850,19 @@ } } }, + "v0SortProperty": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Required. The field to sort on, in graph notation. Supported: \"name\",\n\"size\", \"lastModifiedDateTime\", \"photo.takenDateTime\". A field is only\nsortable if it is indexed as a sortable type in both backends AND\npresent on the Match entity (the service layer needs the sort key to\nmerge per-space result streams)." + }, + "isDescending": { + "type": "boolean", + "description": "Optional. Sort in descending order. Defaults to ascending." + } + } + }, "v0Video": { "type": "object", "properties": { diff --git a/protogen/proto/opencloud/services/search/v0/search.proto b/protogen/proto/opencloud/services/search/v0/search.proto index d11debe4a6..5a2c39c9aa 100644 --- a/protogen/proto/opencloud/services/search/v0/search.proto +++ b/protogen/proto/opencloud/services/search/v0/search.proto @@ -83,6 +83,11 @@ message SearchRequest { // Optional. Number of leading matches to skip in the globally sorted, // cross-space merged result list. int32 from = 7 [(google.api.field_behavior) = OPTIONAL]; + // Optional. Fields to sort the matches by, in order of precedence. When + // empty, matches are sorted by relevance score. Only a subset of the + // indexed fields is sortable; the graph service validates this before + // forwarding the request. + repeated SortProperty order_by = 8 [(google.api.field_behavior) = OPTIONAL]; } message SearchResponse { @@ -115,6 +120,10 @@ message SearchIndexRequest { // 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]; + // Optional. Fields to sort the matches by, in order of precedence. When + // empty, matches are sorted by relevance score. Each backend translates + // this to its native sort (bleve: SortBy, OpenSearch: sort clause). + repeated SortProperty order_by = 7 [(google.api.field_behavior) = OPTIONAL]; } message SearchIndexResponse { @@ -155,6 +164,17 @@ message MetricDefinition { MetricKind kind = 1; } +message SortProperty { + // Required. The field to sort on, in graph notation. Supported: "name", + // "size", "lastModifiedDateTime", "photo.takenDateTime". A field is only + // sortable if it is indexed as a sortable type in both backends AND + // present on the Match entity (the service layer needs the sort key to + // merge per-space result streams). + string name = 1; + // Optional. Sort in descending order. Defaults to ascending. + bool is_descending = 2; +} + enum MetricKind { METRIC_KIND_UNSPECIFIED = 0; METRIC_KIND_SUM = 1; From 4ef74e5232fb83521aec952ab8f5a3596aa01ef3 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 12 Aug 2026 16:21:14 +0200 Subject: [PATCH 2/6] feat(graph): accept sortProperties on search requests Validated against the sortable-field whitelist (name, size, lastModifiedDateTime, photo.takenDateTime) and forwarded to the search service as order_by. Also fixes the stub search service's IndexSpace signature (streaming response) so the suite builds again. (cherry picked from commit 9aa6a3492de3468da7a8480a3b9ef5748cee1f5d) (cherry picked from commit 35a6cad8ea6a7890bdc0bc98c7ac97bdcbf477cc) --- services/graph/pkg/service/v0/searchquery.go | 44 +++++++++++++++++-- .../graph/pkg/service/v0/searchquery_test.go | 19 +++++++- 2 files changed, 59 insertions(+), 4 deletions(-) diff --git a/services/graph/pkg/service/v0/searchquery.go b/services/graph/pkg/service/v0/searchquery.go index 409306710b..09b4fc6989 100644 --- a/services/graph/pkg/service/v0/searchquery.go +++ b/services/graph/pkg/service/v0/searchquery.go @@ -86,9 +86,6 @@ func validateSearchExpand(r *http.Request) error { // unsupportedProperty names a property of the spec this endpoint does not // evaluate yet. func unsupportedProperty(sr libregraph.SearchRequest) string { - if len(sr.SortProperties) > 0 { - return "sortProperties" - } var geohash func(aggs []libregraph.AggregationOption) bool geohash = func(aggs []libregraph.AggregationOption) bool { for _, a := range aggs { @@ -117,6 +114,9 @@ func searchRequestOf(sr libregraph.SearchRequest) (*searchsvc.SearchRequest, err if err := validateAggregations(sr.Aggregations); err != nil { return nil, err } + if err := validateSortProperties(sr.SortProperties); err != nil { + return nil, err + } aggregations := libregraphAggregationsToSearch(sr.Aggregations) filters, err := aggregationFiltersToSearch(sr.AggregationFilters) if err != nil { @@ -141,6 +141,7 @@ func searchRequestOf(sr libregraph.SearchRequest) (*searchsvc.SearchRequest, err PageSize: &size, Aggregations: aggregations, AggregationFilters: filters, + OrderBy: libregraphSortToSearch(sr.SortProperties), }, nil } @@ -262,6 +263,43 @@ func validateAggregations(aggs []libregraph.AggregationOption) error { return nil } +// sortableFields is the set of fields accepted in sortProperties (mirrored in +// the openapi spec). A field qualifies only if it is indexed as a sortable +// type in both search backends AND carried on the Match entity: the service +// layer re-sorts matches when merging the per-space result streams and needs +// the sort key on the match itself. +var sortableFields = map[string]struct{}{ + "name": {}, + "size": {}, + "lastModifiedDateTime": {}, + "photo.takenDateTime": {}, +} + +// validateSortProperties rejects sorting by fields outside sortableFields. +func validateSortProperties(sortProperties []libregraph.SortProperty) error { + for _, sp := range sortProperties { + if _, ok := sortableFields[sp.Name]; !ok { + return fmt.Errorf("field %q is not sortable; sortable fields: name, size, lastModifiedDateTime, photo.takenDateTime", sp.Name) + } + } + return nil +} + +func libregraphSortToSearch(in []libregraph.SortProperty) []*searchsvc.SortProperty { + if len(in) == 0 { + return nil + } + out := make([]*searchsvc.SortProperty, 0, len(in)) + for _, sp := range in { + p := &searchsvc.SortProperty{Name: sp.Name} + if sp.IsDescending != nil { + p.IsDescending = *sp.IsDescending + } + out = append(out, p) + } + return out +} + // renderSearchError answers with the status the search service failed with. func (g Graph) renderSearchError(w http.ResponseWriter, r *http.Request, err error) { e := merrors.Parse(err.Error()) diff --git a/services/graph/pkg/service/v0/searchquery_test.go b/services/graph/pkg/service/v0/searchquery_test.go index 993e3d8152..18c6cf6504 100644 --- a/services/graph/pkg/service/v0/searchquery_test.go +++ b/services/graph/pkg/service/v0/searchquery_test.go @@ -412,13 +412,30 @@ var _ = ginkgo.Describe("SearchQuery", func() { Expect(rr.Body.String()).To(ContainSubstring("notSupported")) Expect(rr.Body.String()).To(ContainSubstring(property)) }, - ginkgo.Entry("sortProperties", `"sortProperties": [{"name": "name"}]`, "sortProperties"), ginkgo.Entry("a geohash aggregation", `"aggregations": [{"field": "location", "@libre.graph.geohashDefinition": {"precision": 5}}]`, "geohashDefinition"), ginkgo.Entry("a nested geohash aggregation", `"aggregations": [{"field": "audio.artist", "@libre.graph.subAggregations": [{"field": "location", "@libre.graph.geohashDefinition": {"precision": 5}}]}]`, "geohashDefinition"), ) + ginkgo.It("forwards sortProperties to the search service as order_by", func() { + g, captured := graphWithSearchAnswer(&searchsvc.SearchResponse{}) + rr := postSearchQuery(g, searchQueryBody(`"sortProperties": [{"name": "photo.takenDateTime", "isDescending": true}, {"name": "name"}]`)) + Expect(rr.Code).To(Equal(http.StatusOK), rr.Body.String()) + + Expect(captured().GetOrderBy()).To(HaveLen(2)) + Expect(captured().GetOrderBy()[0].GetName()).To(Equal("photo.takenDateTime")) + Expect(captured().GetOrderBy()[0].GetIsDescending()).To(BeTrue()) + Expect(captured().GetOrderBy()[1].GetName()).To(Equal("name")) + Expect(captured().GetOrderBy()[1].GetIsDescending()).To(BeFalse()) + }) + + ginkgo.It("rejects sorting by an unsupported field with 400", func() { + rr := postSearchQuery(graphWithoutSearch(), searchQueryBody(`"sortProperties": [{"name": "photo.iso"}]`)) + Expect(rr.Code).To(Equal(http.StatusBadRequest), rr.Body.String()) + Expect(rr.Body.String()).To(ContainSubstring("photo.iso")) + }) + ginkgo.It("rejects an $expand it does not know with 400", func() { req := httptest.NewRequest(http.MethodPost, "/search/query?$expand=thumbnails,permissions", bytes.NewBufferString(searchQueryBody(`"from": 0`))) rr := httptest.NewRecorder() From 679f50fb61f3fe0f1e0ee7817696d3384e351631 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 12 Aug 2026 16:29:55 +0200 Subject: [PATCH 3/6] feat(search): allow sorting by any scalar hit field Sortable is every field that is indexed as a scalar and carried on the match entity: name, size, lastModifiedDateTime, mimeType and the scalar facet fields (photo.*, audio.*, image.*, location.*, ...). Both sets are derived by reflection, so new facet fields become sortable automatically. Multivalued fields (tags), bare facets and internal index fields are rejected with invalidRequest at the graph layer. CompareMatches provides the merge comparator for the service layer. (cherry picked from commit bd32795194cf3945e8416ede496a147ce8d6cb5c) --- .../opencloud/services/search/v0/search.pb.go | 11 +- .../services/search/v0/search.swagger.json | 2 +- .../opencloud/services/search/v0/search.proto | 11 +- services/graph/pkg/service/v0/searchquery.go | 21 +- .../graph/pkg/service/v0/searchquery_test.go | 33 ++- services/search/pkg/search/sort.go | 268 ++++++++++++++++++ services/search/pkg/search/sort_test.go | 125 ++++++++ 7 files changed, 440 insertions(+), 31 deletions(-) create mode 100644 services/search/pkg/search/sort.go create mode 100644 services/search/pkg/search/sort_test.go diff --git a/protogen/gen/opencloud/services/search/v0/search.pb.go b/protogen/gen/opencloud/services/search/v0/search.pb.go index 9f4b9832b7..4a17568df9 100644 --- a/protogen/gen/opencloud/services/search/v0/search.pb.go +++ b/protogen/gen/opencloud/services/search/v0/search.pb.go @@ -701,11 +701,12 @@ type SortProperty struct { sizeCache protoimpl.SizeCache unknownFields protoimpl.UnknownFields - // Required. The field to sort on, in graph notation. Supported: "name", - // "size", "lastModifiedDateTime", "photo.takenDateTime". A field is only - // sortable if it is indexed as a sortable type in both backends AND - // present on the Match entity (the service layer needs the sort key to - // merge per-space result streams). + // Required. The field to sort on, in graph notation ("name", "size", + // "lastModifiedDateTime", "mimeType" or a scalar facet field such as + // "photo.takenDateTime" or "audio.artist"). A field is sortable when it is + // indexed as a scalar in both backends AND carried on the Match entity + // (the service layer needs the sort key to merge per-space result + // streams); see the search package's IsSortableField. Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` // Optional. Sort in descending order. Defaults to ascending. IsDescending bool `protobuf:"varint,2,opt,name=is_descending,json=isDescending,proto3" json:"is_descending,omitempty"` diff --git a/protogen/gen/opencloud/services/search/v0/search.swagger.json b/protogen/gen/opencloud/services/search/v0/search.swagger.json index d792b10ecd..704a04997d 100644 --- a/protogen/gen/opencloud/services/search/v0/search.swagger.json +++ b/protogen/gen/opencloud/services/search/v0/search.swagger.json @@ -855,7 +855,7 @@ "properties": { "name": { "type": "string", - "description": "Required. The field to sort on, in graph notation. Supported: \"name\",\n\"size\", \"lastModifiedDateTime\", \"photo.takenDateTime\". A field is only\nsortable if it is indexed as a sortable type in both backends AND\npresent on the Match entity (the service layer needs the sort key to\nmerge per-space result streams)." + "description": "Required. The field to sort on, in graph notation (\"name\", \"size\",\n\"lastModifiedDateTime\", \"mimeType\" or a scalar facet field such as\n\"photo.takenDateTime\" or \"audio.artist\"). A field is sortable when it is\nindexed as a scalar in both backends AND carried on the Match entity\n(the service layer needs the sort key to merge per-space result\nstreams); see the search package's IsSortableField." }, "isDescending": { "type": "boolean", diff --git a/protogen/proto/opencloud/services/search/v0/search.proto b/protogen/proto/opencloud/services/search/v0/search.proto index 5a2c39c9aa..c542b904ec 100644 --- a/protogen/proto/opencloud/services/search/v0/search.proto +++ b/protogen/proto/opencloud/services/search/v0/search.proto @@ -165,11 +165,12 @@ message MetricDefinition { } message SortProperty { - // Required. The field to sort on, in graph notation. Supported: "name", - // "size", "lastModifiedDateTime", "photo.takenDateTime". A field is only - // sortable if it is indexed as a sortable type in both backends AND - // present on the Match entity (the service layer needs the sort key to - // merge per-space result streams). + // Required. The field to sort on, in graph notation ("name", "size", + // "lastModifiedDateTime", "mimeType" or a scalar facet field such as + // "photo.takenDateTime" or "audio.artist"). A field is sortable when it is + // indexed as a scalar in both backends AND carried on the Match entity + // (the service layer needs the sort key to merge per-space result + // streams); see the search package's IsSortableField. string name = 1; // Optional. Sort in descending order. Defaults to ascending. bool is_descending = 2; diff --git a/services/graph/pkg/service/v0/searchquery.go b/services/graph/pkg/service/v0/searchquery.go index 09b4fc6989..9ab01473e7 100644 --- a/services/graph/pkg/service/v0/searchquery.go +++ b/services/graph/pkg/service/v0/searchquery.go @@ -263,23 +263,14 @@ func validateAggregations(aggs []libregraph.AggregationOption) error { return nil } -// sortableFields is the set of fields accepted in sortProperties (mirrored in -// the openapi spec). A field qualifies only if it is indexed as a sortable -// type in both search backends AND carried on the Match entity: the service -// layer re-sorts matches when merging the per-space result streams and needs -// the sort key on the match itself. -var sortableFields = map[string]struct{}{ - "name": {}, - "size": {}, - "lastModifiedDateTime": {}, - "photo.takenDateTime": {}, -} - -// validateSortProperties rejects sorting by fields outside sortableFields. +// validateSortProperties rejects sorting by unknown or multivalued fields. +// Sortable are scalar fields carried on the search hit: name, size, +// lastModifiedDateTime, mimeType and the facet fields (photo.takenDateTime, +// audio.artist, image.width, ...); see search.IsSortableField. func validateSortProperties(sortProperties []libregraph.SortProperty) error { for _, sp := range sortProperties { - if _, ok := sortableFields[sp.Name]; !ok { - return fmt.Errorf("field %q is not sortable; sortable fields: name, size, lastModifiedDateTime, photo.takenDateTime", sp.Name) + if !search.IsSortableField(sp.Name) { + return fmt.Errorf("field %q is not sortable; sortable are scalar hit fields such as name, size, lastModifiedDateTime, mimeType or photo.takenDateTime", sp.Name) } } return nil diff --git a/services/graph/pkg/service/v0/searchquery_test.go b/services/graph/pkg/service/v0/searchquery_test.go index 18c6cf6504..72fa857c63 100644 --- a/services/graph/pkg/service/v0/searchquery_test.go +++ b/services/graph/pkg/service/v0/searchquery_test.go @@ -430,11 +430,34 @@ var _ = ginkgo.Describe("SearchQuery", func() { Expect(captured().GetOrderBy()[1].GetIsDescending()).To(BeFalse()) }) - ginkgo.It("rejects sorting by an unsupported field with 400", func() { - rr := postSearchQuery(graphWithoutSearch(), searchQueryBody(`"sortProperties": [{"name": "photo.iso"}]`)) - Expect(rr.Code).To(Equal(http.StatusBadRequest), rr.Body.String()) - Expect(rr.Body.String()).To(ContainSubstring("photo.iso")) - }) + ginkgo.DescribeTable("accepts sorting by scalar hit fields", + func(field string) { + g, _ := graphWithSearchAnswer(&searchsvc.SearchResponse{}) + rr := postSearchQuery(g, searchQueryBody(fmt.Sprintf(`"sortProperties": [{"name": %q}]`, field))) + Expect(rr.Code).To(Equal(http.StatusOK), rr.Body.String()) + }, + ginkgo.Entry("name", "name"), + ginkgo.Entry("size", "size"), + ginkgo.Entry("lastModifiedDateTime", "lastModifiedDateTime"), + ginkgo.Entry("mimeType", "mimeType"), + ginkgo.Entry("photo.takenDateTime", "photo.takenDateTime"), + ginkgo.Entry("photo.iso", "photo.iso"), + ginkgo.Entry("audio.artist", "audio.artist"), + ginkgo.Entry("image.width", "image.width"), + ) + + ginkgo.DescribeTable("rejects sorting by unsortable fields with 400", + func(field string) { + rr := postSearchQuery(graphWithoutSearch(), searchQueryBody(fmt.Sprintf(`"sortProperties": [{"name": %q}]`, field))) + Expect(rr.Code).To(Equal(http.StatusBadRequest), rr.Body.String()) + Expect(rr.Body.String()).To(ContainSubstring(field)) + }, + ginkgo.Entry("unknown field", "definitelyNotAField"), + ginkgo.Entry("multivalued field", "tags"), + ginkgo.Entry("internal index field name", "Mtime"), + ginkgo.Entry("bare audio facet", "audio"), + ginkgo.Entry("bare location facet", "location"), + ) ginkgo.It("rejects an $expand it does not know with 400", func() { req := httptest.NewRequest(http.MethodPost, "/search/query?$expand=thumbnails,permissions", bytes.NewBufferString(searchQueryBody(`"from": 0`))) diff --git a/services/search/pkg/search/sort.go b/services/search/pkg/search/sort.go new file mode 100644 index 0000000000..9829ac0719 --- /dev/null +++ b/services/search/pkg/search/sort.go @@ -0,0 +1,268 @@ +package search + +import ( + "reflect" + "strings" + "time" + + "google.golang.org/protobuf/reflect/protoreflect" + + searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" + searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" +) + +// Sorting support for search results (graph sortProperties / proto order_by). +// +// A field is sortable when both of the following hold: +// - it is indexed as a scalar (string, number, bool or time), so the +// engines can sort on it natively; multivalued fields like Tags are not +// sortable +// - it is carried on the match entity, so the service layer can read the +// sort key when merging the per-space result streams +// +// Both sets are derived by reflection (index side: the Resource type, match +// side: the Entity proto), so new facet fields become sortable automatically. + +// sortIndexAliases maps the graph-facing names of top-level fields to their +// index field names. Facet fields (photo.*, audio.*, ...) share the same +// dotted names in both worlds and need no alias. Top-level fields are only +// exposed under these graph names; internal fields like RootID or Deleted +// stay unsortable. +var sortIndexAliases = map[string]string{ + "name": "Name", + "size": "Size", + "lastModifiedDateTime": "Mtime", + "mimeType": "MimeType", +} + +// entityJSONAliases maps graph-facing names to the Entity proto's JSON names +// where the two disagree. +var entityJSONAliases = map[string]string{ + "lastModifiedDateTime": "lastModifiedTime", +} + +// IsSortableField reports whether results can be sorted by the field. +func IsSortableField(name string) bool { + _, ok := SortIndexField(name) + return ok +} + +// SortIndexField translates a graph sortProperties name into the index field +// name to sort on, reporting whether the field is sortable at all. +func SortIndexField(name string) (string, bool) { + field := name + if alias, ok := sortIndexAliases[name]; ok { + field = alias + } else if !strings.Contains(name, ".") { + return "", false + } + if !sortableIndexFields[field] { + return "", false + } + if !entityFieldResolvable(name) { + return "", false + } + return field, true +} + +// CompareMatches orders match a relative to b according to orderBy: -1 when a +// comes first, 1 when b comes first, 0 when the sort keys tie (callers fall +// back to the score). Matches missing a sort key sort after those that have +// it, regardless of direction. +func CompareMatches(a, b *searchmsg.Match, orderBy []*searchsvc.SortProperty) int { + for _, sp := range orderBy { + ka := matchSortKey(a, sp.GetName()) + kb := matchSortKey(b, sp.GetName()) + if !ka.present && !kb.present { + continue + } + if !ka.present { + return 1 + } + if !kb.present { + return -1 + } + c := 0 + switch { + case ka.isString: + c = strings.Compare(ka.str, kb.str) + case ka.num < kb.num: + c = -1 + case ka.num > kb.num: + c = 1 + } + if c == 0 { + continue + } + if sp.GetIsDescending() { + c = -c + } + return c + } + return 0 +} + +// sortableIndexFields is the set of scalar indexed fields, keyed by index +// field name. +var sortableIndexFields = buildSortableFieldSet() + +var timeType = reflect.TypeOf(time.Time{}) + +func jsonFieldName(f reflect.StructField) string { + tag := f.Tag.Get("json") + if tag == "" { + return f.Name + } + return strings.Split(tag, ",")[0] +} + +func buildSortableFieldSet() map[string]bool { + out := map[string]bool{} + collectScalarFields(out, "", reflect.TypeOf(Resource{})) + return out +} + +func collectScalarFields(out map[string]bool, prefix string, t reflect.Type) { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !f.IsExported() { + continue + } + if f.Anonymous { + collectScalarFields(out, prefix, f.Type) + continue + } + path := prefix + jsonFieldName(f) + ft := f.Type + for ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + switch ft.Kind() { + case reflect.String, reflect.Bool, + reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + out[path] = true + case reflect.Struct: + if ft == timeType { + out[path] = true + continue + } + collectScalarFields(out, path+".", ft) + } + } +} + +func entityPath(name string) []string { + if alias, ok := entityJSONAliases[name]; ok { + name = alias + } + return strings.Split(name, ".") +} + +const timestampFullName = protoreflect.FullName("google.protobuf.Timestamp") + +// entityFieldResolvable reports whether the graph field name resolves to a +// scalar (or timestamp) field on the match entity. +func entityFieldResolvable(name string) bool { + md := (&searchmsg.Entity{}).ProtoReflect().Descriptor() + segments := entityPath(name) + for i, seg := range segments { + fd := md.Fields().ByJSONName(seg) + if fd == nil || fd.IsList() || fd.IsMap() { + return false + } + if i < len(segments)-1 { + if fd.Kind() != protoreflect.MessageKind { + return false + } + md = fd.Message() + continue + } + switch fd.Kind() { + case protoreflect.StringKind, protoreflect.BoolKind, + protoreflect.Int32Kind, protoreflect.Int64Kind, + protoreflect.Sint32Kind, protoreflect.Sint64Kind, + protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind, + protoreflect.Uint32Kind, protoreflect.Uint64Kind, + protoreflect.Fixed32Kind, protoreflect.Fixed64Kind, + protoreflect.FloatKind, protoreflect.DoubleKind: + return true + case protoreflect.MessageKind: + return fd.Message().FullName() == timestampFullName + } + return false + } + return false +} + +// sortKey is the comparable value of a sort field on a concrete match. +type sortKey struct { + present bool + isString bool + str string + num float64 +} + +// matchSortKey extracts the sort key for the graph field name from a match by +// walking the entity proto along the field's JSON names. +func matchSortKey(m *searchmsg.Match, name string) sortKey { + entity := m.GetEntity() + if entity == nil { + return sortKey{} + } + msg := entity.ProtoReflect() + segments := entityPath(name) + for i, seg := range segments { + fd := msg.Descriptor().Fields().ByJSONName(seg) + if fd == nil || fd.IsList() || fd.IsMap() { + return sortKey{} + } + if i < len(segments)-1 { + if fd.Kind() != protoreflect.MessageKind || !msg.Has(fd) { + return sortKey{} + } + msg = msg.Get(fd).Message() + continue + } + if fd.HasPresence() && !msg.Has(fd) { + return sortKey{} + } + v := msg.Get(fd) + switch fd.Kind() { + case protoreflect.StringKind: + return sortKey{present: true, isString: true, str: v.String()} + case protoreflect.BoolKind: + num := 0.0 + if v.Bool() { + num = 1.0 + } + return sortKey{present: true, num: num} + case protoreflect.Int32Kind, protoreflect.Int64Kind, + protoreflect.Sint32Kind, protoreflect.Sint64Kind, + protoreflect.Sfixed32Kind, protoreflect.Sfixed64Kind: + return sortKey{present: true, num: float64(v.Int())} + case protoreflect.Uint32Kind, protoreflect.Uint64Kind, + protoreflect.Fixed32Kind, protoreflect.Fixed64Kind: + return sortKey{present: true, num: float64(v.Uint())} + case protoreflect.FloatKind, protoreflect.DoubleKind: + return sortKey{present: true, num: v.Float()} + case protoreflect.MessageKind: + if fd.Message().FullName() != timestampFullName { + return sortKey{} + } + ts := v.Message() + seconds := ts.Get(ts.Descriptor().Fields().ByName("seconds")).Int() + nanos := ts.Get(ts.Descriptor().Fields().ByName("nanos")).Int() + return sortKey{present: true, num: float64(seconds) + float64(nanos)/1e9} + } + return sortKey{} + } + return sortKey{} +} diff --git a/services/search/pkg/search/sort_test.go b/services/search/pkg/search/sort_test.go new file mode 100644 index 0000000000..dd294d65e2 --- /dev/null +++ b/services/search/pkg/search/sort_test.go @@ -0,0 +1,125 @@ +package search_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "google.golang.org/protobuf/types/known/timestamppb" + + 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/search/pkg/search" +) + +var _ = Describe("SortIndexField", func() { + DescribeTable("maps graph field names to index fields", + func(name string, wantField string, wantOK bool) { + field, ok := search.SortIndexField(name) + Expect(ok).To(Equal(wantOK)) + Expect(field).To(Equal(wantField)) + }, + // top-level fields are exposed under their graph names + Entry("name", "name", "Name", true), + Entry("size", "size", "Size", true), + Entry("lastModifiedDateTime", "lastModifiedDateTime", "Mtime", true), + Entry("mimeType", "mimeType", "MimeType", true), + // facet fields keep their dotted names + Entry("photo.takenDateTime", "photo.takenDateTime", "photo.takenDateTime", true), + Entry("photo.iso", "photo.iso", "photo.iso", true), + Entry("photo.cameraModel", "photo.cameraModel", "photo.cameraModel", true), + Entry("audio.artist", "audio.artist", "audio.artist", true), + Entry("audio.year", "audio.year", "audio.year", true), + Entry("image.width", "image.width", "image.width", true), + Entry("location.latitude", "location.latitude", "location.latitude", true), + // bare facets (message-typed, no scalar value) are not sortable + Entry("audio (bare facet)", "audio", "", false), + Entry("photo (bare facet)", "photo", "", false), + Entry("location (bare facet)", "location", "", false), + Entry("location (dotted but not scalar)", "location.", "", false), + // multivalued fields are not sortable + Entry("tags (repeated)", "tags", "", false), + Entry("Tags (index name, repeated)", "Tags", "", false), + // internal index fields are not exposed under their index names + Entry("Name (index name)", "Name", "", false), + Entry("Mtime (index name)", "Mtime", "", false), + Entry("RootID", "RootID", "", false), + Entry("Deleted", "Deleted", "", false), + // unknown fields + Entry("unknown", "definitelyNotAField", "", false), + Entry("unknown facet field", "photo.definitelyNotAField", "", false), + Entry("empty", "", "", false), + ) +}) + +var _ = Describe("CompareMatches", func() { + match := func(mutate func(e *searchmsg.Entity)) *searchmsg.Match { + e := &searchmsg.Entity{} + mutate(e) + return &searchmsg.Match{Entity: e} + } + asc := func(name string) []*searchsvc.SortProperty { + return []*searchsvc.SortProperty{{Name: name}} + } + desc := func(name string) []*searchsvc.SortProperty { + return []*searchsvc.SortProperty{{Name: name, IsDescending: true}} + } + + It("compares string fields lexicographically", func() { + a := match(func(e *searchmsg.Entity) { e.Name = "a.jpg" }) + b := match(func(e *searchmsg.Entity) { e.Name = "b.jpg" }) + Expect(search.CompareMatches(a, b, asc("name"))).To(Equal(-1)) + Expect(search.CompareMatches(b, a, asc("name"))).To(Equal(1)) + Expect(search.CompareMatches(a, b, desc("name"))).To(Equal(1)) + }) + + It("compares numeric fields numerically", func() { + small := match(func(e *searchmsg.Entity) { e.Size = 9 }) + big := match(func(e *searchmsg.Entity) { e.Size = 10 }) + Expect(search.CompareMatches(small, big, asc("size"))).To(Equal(-1)) + Expect(search.CompareMatches(small, big, desc("size"))).To(Equal(1)) + }) + + It("compares timestamps", func() { + older := match(func(e *searchmsg.Entity) { + e.Photo = &searchmsg.Photo{TakenDateTime: ×tamppb.Timestamp{Seconds: 100}} + }) + newer := match(func(e *searchmsg.Entity) { + e.Photo = &searchmsg.Photo{TakenDateTime: ×tamppb.Timestamp{Seconds: 200}} + }) + Expect(search.CompareMatches(older, newer, asc("photo.takenDateTime"))).To(Equal(-1)) + Expect(search.CompareMatches(older, newer, desc("photo.takenDateTime"))).To(Equal(1)) + }) + + It("compares lastModifiedDateTime via the entity's lastModifiedTime", func() { + older := match(func(e *searchmsg.Entity) { + e.LastModifiedTime = ×tamppb.Timestamp{Seconds: 100} + }) + newer := match(func(e *searchmsg.Entity) { + e.LastModifiedTime = ×tamppb.Timestamp{Seconds: 200} + }) + Expect(search.CompareMatches(older, newer, asc("lastModifiedDateTime"))).To(Equal(-1)) + }) + + It("sorts matches missing the field after those that have it, in both directions", func() { + has := match(func(e *searchmsg.Entity) { + e.Photo = &searchmsg.Photo{TakenDateTime: ×tamppb.Timestamp{Seconds: 100}} + }) + missing := match(func(e *searchmsg.Entity) {}) + Expect(search.CompareMatches(has, missing, asc("photo.takenDateTime"))).To(Equal(-1)) + Expect(search.CompareMatches(missing, has, asc("photo.takenDateTime"))).To(Equal(1)) + Expect(search.CompareMatches(has, missing, desc("photo.takenDateTime"))).To(Equal(-1)) + }) + + It("falls through to the next sort property on ties", func() { + a := match(func(e *searchmsg.Entity) { e.Size = 5; e.Name = "a" }) + b := match(func(e *searchmsg.Entity) { e.Size = 5; e.Name = "b" }) + orderBy := []*searchsvc.SortProperty{{Name: "size"}, {Name: "name"}} + Expect(search.CompareMatches(a, b, orderBy)).To(Equal(-1)) + }) + + It("returns 0 for full ties and empty orderBy", func() { + a := match(func(e *searchmsg.Entity) { e.Size = 5 }) + b := match(func(e *searchmsg.Entity) { e.Size = 5 }) + Expect(search.CompareMatches(a, b, asc("size"))).To(Equal(0)) + Expect(search.CompareMatches(a, b, nil)).To(Equal(0)) + }) +}) From 49f5f3c08b8b6e6d004de9ac9161e68130d99acd Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 12 Aug 2026 16:33:33 +0200 Subject: [PATCH 4/6] feat(search): merge per-space results in order_by order The fan-out passes order_by through to each engine query; the merge re-sorts the combined matches with CompareMatches and keeps the score as tiebreaker (and as the sole criterion when no order_by is given). (cherry picked from commit 66af40fc34fd9daffc028ab06f83f7ade8a360d1) --- services/search/pkg/search/service.go | 12 +++++- services/search/pkg/search/service_test.go | 43 ++++++++++++++++++++++ 2 files changed, 53 insertions(+), 2 deletions(-) diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index 07ece77eb3..1a50fb2096 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -313,8 +313,15 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se } aggregation.Finalize(req.GetAggregations(), aggregations) - // compile one sorted list of matches from all spaces and apply from/limit if needed - sort.Sort(matches) + // every engine answers in order_by order (by score without one); the merge + // re-establishes it across spaces, ties by score and id + orderBy := req.GetOrderBy() + sort.SliceStable(matches, func(i, j int) bool { + if c := CompareMatches(matches[i], matches[j], orderBy); c != 0 { + return c < 0 + } + return matches.Less(i, j) + }) limit := PageSizeOrDefault(req.PageSize) if from := int(req.GetFrom()); from > 0 { if from < len(matches) { @@ -469,6 +476,7 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest, Query: req.Query, Aggregations: req.GetAggregations(), AggregationFilters: req.GetAggregationFilters(), + OrderBy: req.GetOrderBy(), 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 668986e5a6..01096b88e7 100644 --- a/services/search/pkg/search/service_test.go +++ b/services/search/pkg/search/service_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/mock" "google.golang.org/grpc" "google.golang.org/protobuf/testing/protocmp" + "google.golang.org/protobuf/types/known/timestamppb" "github.com/opencloud-eu/opencloud/pkg/conversions" "github.com/opencloud-eu/opencloud/pkg/log" @@ -345,6 +346,48 @@ var _ = Describe("Searchprovider", func() { }) }) + Context("with two personal spaces returning photos", func() { + photoMatch := func(space, name string, taken int64, score float32) *searchmsg.Match { + return &searchmsg.Match{Score: score, Entity: &searchmsg.Entity{ + Id: &searchmsg.ResourceID{StorageId: "storageid", SpaceId: space, OpaqueId: name}, + Ref: &searchmsg.Reference{ResourceId: &searchmsg.ResourceID{StorageId: "storageid", SpaceId: space, OpaqueId: space}, Path: "./" + name}, + Name: name, + Photo: &searchmsg.Photo{TakenDateTime: ×tamppb.Timestamp{Seconds: taken}}, + }} + } + names := func(req *searchsvc.SearchRequest) []string { + res, err := s.Search(ctx, req) + Expect(err).ToNot(HaveOccurred()) + out := []string{} + for _, m := range res.Matches { + out = append(out, m.GetEntity().GetName()) + } + return out + } + + BeforeEach(func() { + listsSpacesAB() + searchOfSpace("a").Return(&searchsvc.SearchIndexResponse{TotalMatches: 2, Matches: []*searchmsg.Match{ + photoMatch("a", "a-old.jpg", 100, 0.9), photoMatch("a", "a-new.jpg", 300, 0.1), + }}, nil) + searchOfSpace("b").Return(&searchsvc.SearchIndexResponse{TotalMatches: 2, Matches: []*searchmsg.Match{ + photoMatch("b", "b-newest.jpg", 400, 0.5), photoMatch("b", "b-mid.jpg", 200, 0.4), + }}, nil) + }) + + It("forwards order_by to the engine and merges matches across spaces in that order", func() { + byTaken := []*searchsvc.SortProperty{{Name: "photo.takenDateTime", IsDescending: true}} + Expect(names(&searchsvc.SearchRequest{Query: "mediatype:image", OrderBy: byTaken})).To(Equal([]string{"b-newest.jpg", "a-new.jpg", "b-mid.jpg", "a-old.jpg"})) + indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool { + return len(req.GetOrderBy()) == 1 && req.GetOrderBy()[0].GetName() == "photo.takenDateTime" && req.GetOrderBy()[0].GetIsDescending() + })) + }) + + It("merges matches by score when no order_by is given", func() { + Expect(names(&searchsvc.SearchRequest{Query: "mediatype:image"})).To(Equal([]string{"a-old.jpg", "b-newest.jpg", "b-mid.jpg", "a-new.jpg"})) + }) + }) + Context("with two personal spaces returning matches of the same score", func() { match := func(space, name string) *searchmsg.Match { return &searchmsg.Match{Score: 1, Entity: &searchmsg.Entity{ From 6a541c0f9c24101326ae6fa444612f697b7706c7 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 12 Aug 2026 16:35:49 +0200 Subject: [PATCH 5/6] feat(search): native order_by sorting in the bleve backend (cherry picked from commit 66a0755ad69a6a1af07e0b8335b08d63793777ce) --- services/search/pkg/bleve/backend.go | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/services/search/pkg/bleve/backend.go b/services/search/pkg/bleve/backend.go index 80a0792d23..99874ed4cd 100644 --- a/services/search/pkg/bleve/backend.go +++ b/services/search/pkg/bleve/backend.go @@ -2,6 +2,7 @@ package bleve import ( "context" + "fmt" "math" "time" @@ -96,8 +97,19 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ bleveReq := bleve.NewSearchRequest(q) bleveReq.Highlight = bleve.NewHighlight() - // ties by id, like the cross-space merge - bleveReq.SortBy([]string{"-_score", "_id"}) + // order_by first, then like the cross-space merge: score, ties by id + sortOrder := make([]string, 0, len(sir.GetOrderBy())+2) + for _, sp := range sir.GetOrderBy() { + field, ok := search.SortIndexField(sp.GetName()) + if !ok { + return nil, errtypes.BadRequest(fmt.Sprintf("field %q is not sortable", sp.GetName())) + } + if sp.GetIsDescending() { + field = "-" + field + } + sortOrder = append(sortOrder, field) + } + bleveReq.SortBy(append(sortOrder, "-_score", "_id")) bleveReq.Size = size collector, err := newAggCollector(sir.GetAggregations()) From 2591484a98483a59bac139f7662f1ddce614dba2 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 12 Aug 2026 16:45:06 +0200 Subject: [PATCH 6/6] feat(search): native order_by sorting in the opensearch backend Name is analyzed (lowercaseKeyword) and therefore a text field, which OpenSearch refuses to sort on without fielddata; enable it in the index template. The keyword tokenizer emits one term per document, so the fielddata cache stays small. The schema version has not shipped yet, so no reindex is needed. (cherry picked from commit f1d0e7b773b170b119dbd59fa176d656237bed96) --- services/search/pkg/opensearch/backend.go | 21 ++++++++++++++++--- .../pkg/opensearch/internal/osu/request.go | 1 + 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/services/search/pkg/opensearch/backend.go b/services/search/pkg/opensearch/backend.go index f600194f45..7e4fbb21ef 100644 --- a/services/search/pkg/opensearch/backend.go +++ b/services/search/pkg/opensearch/backend.go @@ -126,14 +126,28 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ searchParams := opensearchgoAPI.SearchParams{ SourceExcludes: []string{"Content"}, // Do not send back the full content in the search response, as it is only needed for highlighting and can be large. The highlighted snippets will be sent back in the response instead. - // ties by id, like the cross-space merge - Sort: []string{"_score:desc", "ID:asc"}, - TrackScores: conversions.ToPointer(true), + TrackScores: conversions.ToPointer(true), // count every match, the default stops at 10000 TrackTotalHits: true, Size: conversions.ToPointer(size), } + // order_by first, missing values last in both directions, then like the + // cross-space merge: score, ties by id + sortClause := make([]map[string]any, 0, len(sir.GetOrderBy())+2) + for _, sp := range sir.GetOrderBy() { + field, ok := search.SortIndexField(sp.GetName()) + if !ok { + return nil, errtypes.BadRequest(fmt.Sprintf("field %q is not sortable", sp.GetName())) + } + order := "asc" + if sp.GetIsDescending() { + order = "desc" + } + sortClause = append(sortClause, map[string]any{field: map[string]any{"order": order, "missing": "_last"}}) + } + sortClause = append(sortClause, map[string]any{"_score": map[string]any{"order": "desc"}}, map[string]any{"ID": map[string]any{"order": "asc"}}) + aggregationsBody, err := aggs.Build(sir.GetAggregations()) if err != nil { return nil, errtypes.BadRequest(err.Error()) @@ -158,6 +172,7 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ }, }, Aggs: aggregationsBody, + Sort: sortClause, }, ) if err != nil { diff --git a/services/search/pkg/opensearch/internal/osu/request.go b/services/search/pkg/opensearch/internal/osu/request.go index f6f4e238ee..608773c165 100644 --- a/services/search/pkg/opensearch/internal/osu/request.go +++ b/services/search/pkg/opensearch/internal/osu/request.go @@ -96,6 +96,7 @@ func BuildSearchReq(req *opensearchgoAPI.SearchReq, q Builder, p ...SearchBodyPa type SearchBodyParams struct { Highlight *BodyParamHighlight `json:"highlight,omitempty"` Aggs map[string]any `json:"aggs,omitempty"` + Sort []map[string]any `json:"sort,omitempty"` } //----------------------------------------------------------------------------//