From ec899aefd9d96acace21c258efabdd81b4ed2348 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 12 Sep 2026 15:29:08 +0000 Subject: [PATCH] feat(graph): POST /search/query MS-Graph-style search query endpoint: hits from all accessible spaces, with from/size paging, remote items for hits from shared spaces, thumbnails on $expand, and the effective permission actions on every hit. The search proto gains the aggregation types of the graph API and the endpoint passes aggregations through to the search service, which does not evaluate them yet. --- .../opencloud/messages/search/v0/search.pb.go | 43 +- .../opencloud/services/search/v0/search.pb.go | 967 +++++++++++++++--- .../services/search/v0/search.swagger.json | 178 ++++ .../opencloud/messages/search/v0/search.proto | 4 + .../opencloud/services/search/v0/search.proto | 95 ++ services/graph/pkg/service/v0/driveitems.go | 16 +- services/graph/pkg/service/v0/searchquery.go | 495 +++++++++ .../graph/pkg/service/v0/searchquery_test.go | 200 ++++ services/graph/pkg/service/v0/service.go | 3 + services/graph/pkg/service/v0/sharedbyme.go | 2 +- services/graph/pkg/service/v0/sharedwithme.go | 2 +- services/graph/pkg/service/v0/thumbnails.go | 7 +- services/search/pkg/search/service.go | 7 +- .../search/pkg/service/grpc/v0/service.go | 27 +- 14 files changed, 1876 insertions(+), 170 deletions(-) create mode 100644 services/graph/pkg/service/v0/searchquery.go create mode 100644 services/graph/pkg/service/v0/searchquery_test.go diff --git a/protogen/gen/opencloud/messages/search/v0/search.pb.go b/protogen/gen/opencloud/messages/search/v0/search.pb.go index 0a8bf9c1c2..2e8b6b53ab 100644 --- a/protogen/gen/opencloud/messages/search/v0/search.pb.go +++ b/protogen/gen/opencloud/messages/search/v0/search.pb.go @@ -824,6 +824,10 @@ type Entity struct { MotionPhoto *MotionPhoto `protobuf:"bytes,21,opt,name=motionPhoto,proto3" json:"motionPhoto,omitempty"` Video *Video `protobuf:"bytes,22,opt,name=video,proto3" json:"video,omitempty"` LivePhoto *LivePhoto `protobuf:"bytes,23,opt,name=livePhoto,proto3" json:"livePhoto,omitempty"` + // The effective permission actions of the caller, projected from the space + // root permission set at query time (the same source as `permissions`), for + // the driveItem `@libre.graph.permissions.actions.allowedValues` facet. + PermissionsActionsAllowedValues []string `protobuf:"bytes,24,rep,name=permissionsActionsAllowedValues,proto3" json:"permissionsActionsAllowedValues,omitempty"` } func (x *Entity) Reset() { @@ -1019,6 +1023,13 @@ func (x *Entity) GetLivePhoto() *LivePhoto { return nil } +func (x *Entity) GetPermissionsActionsAllowedValues() []string { + if x != nil { + return x.PermissionsActionsAllowedValues + } + return nil +} + type Match struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1259,7 +1270,7 @@ var file_opencloud_messages_search_v0_search_proto_rawDesc = []byte{ 0x5f, 0x61, 0x75, 0x74, 0x6f, 0x42, 0x10, 0x0a, 0x0e, 0x5f, 0x76, 0x69, 0x74, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x53, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x19, 0x0a, 0x17, 0x5f, 0x76, 0x69, 0x74, 0x61, 0x6c, 0x69, 0x74, 0x79, 0x53, 0x63, 0x6f, 0x72, 0x69, 0x6e, 0x67, 0x56, 0x65, 0x72, 0x73, 0x69, - 0x6f, 0x6e, 0x22, 0xc9, 0x08, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, + 0x6f, 0x6e, 0x22, 0x93, 0x09, 0x0a, 0x06, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x39, 0x0a, 0x03, 0x72, 0x65, 0x66, 0x18, 0x01, 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, @@ -1327,19 +1338,23 @@ var file_opencloud_messages_search_v0_search_proto_rawDesc = []byte{ 0x68, 0x6f, 0x74, 0x6f, 0x18, 0x17, 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, 0x4c, 0x69, 0x76, 0x65, 0x50, 0x68, - 0x6f, 0x74, 0x6f, 0x52, 0x09, 0x6c, 0x69, 0x76, 0x65, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x22, 0x5b, - 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, 0x68, 0x12, 0x3c, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, - 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x24, 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, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, - 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 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, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, - 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, - 0x6f, 0x33, + 0x6f, 0x74, 0x6f, 0x52, 0x09, 0x6c, 0x69, 0x76, 0x65, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x12, 0x48, + 0x0a, 0x1f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x73, 0x41, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x41, 0x6c, 0x6c, 0x6f, 0x77, 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, + 0x73, 0x18, 0x18, 0x20, 0x03, 0x28, 0x09, 0x52, 0x1f, 0x70, 0x65, 0x72, 0x6d, 0x69, 0x73, 0x73, + 0x69, 0x6f, 0x6e, 0x73, 0x41, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x41, 0x6c, 0x6c, 0x6f, 0x77, + 0x65, 0x64, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x73, 0x22, 0x5b, 0x0a, 0x05, 0x4d, 0x61, 0x74, 0x63, + 0x68, 0x12, 0x3c, 0x0a, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x24, 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, 0x45, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x52, 0x06, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x12, + 0x14, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x02, 0x52, 0x05, + 0x73, 0x63, 0x6f, 0x72, 0x65, 0x42, 0x4d, 0x5a, 0x4b, 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, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x2f, 0x76, 0x30, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/protogen/gen/opencloud/services/search/v0/search.pb.go b/protogen/gen/opencloud/services/search/v0/search.pb.go index 1346c98197..02e4f718b6 100644 --- a/protogen/gen/opencloud/services/search/v0/search.pb.go +++ b/protogen/gen/opencloud/services/search/v0/search.pb.go @@ -25,6 +25,61 @@ const ( _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) ) +type MetricKind int32 + +const ( + MetricKind_METRIC_KIND_UNSPECIFIED MetricKind = 0 + MetricKind_METRIC_KIND_SUM MetricKind = 1 + MetricKind_METRIC_KIND_MIN MetricKind = 2 + MetricKind_METRIC_KIND_MAX MetricKind = 3 + MetricKind_METRIC_KIND_AVG MetricKind = 4 +) + +// Enum value maps for MetricKind. +var ( + MetricKind_name = map[int32]string{ + 0: "METRIC_KIND_UNSPECIFIED", + 1: "METRIC_KIND_SUM", + 2: "METRIC_KIND_MIN", + 3: "METRIC_KIND_MAX", + 4: "METRIC_KIND_AVG", + } + MetricKind_value = map[string]int32{ + "METRIC_KIND_UNSPECIFIED": 0, + "METRIC_KIND_SUM": 1, + "METRIC_KIND_MIN": 2, + "METRIC_KIND_MAX": 3, + "METRIC_KIND_AVG": 4, + } +) + +func (x MetricKind) Enum() *MetricKind { + p := new(MetricKind) + *p = x + return p +} + +func (x MetricKind) String() string { + return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x)) +} + +func (MetricKind) Descriptor() protoreflect.EnumDescriptor { + return file_opencloud_services_search_v0_search_proto_enumTypes[0].Descriptor() +} + +func (MetricKind) Type() protoreflect.EnumType { + return &file_opencloud_services_search_v0_search_proto_enumTypes[0] +} + +func (x MetricKind) Number() protoreflect.EnumNumber { + return protoreflect.EnumNumber(x) +} + +// Deprecated: Use MetricKind.Descriptor instead. +func (MetricKind) EnumDescriptor() ([]byte, []int) { + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{0} +} + type SearchRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -37,6 +92,12 @@ type SearchRequest struct { PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` Ref *v0.Reference `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"` + // Optional. Aggregations (facets) to compute alongside the matches. + Aggregations []*AggregationOption `protobuf:"bytes,5,rep,name=aggregations,proto3" json:"aggregations,omitempty"` + // Optional. Decoded aggregation filters, one per selected bucket, as KQL + // fragments (e.g. `audio.artist:"Pink Floyd"`). Combined with `query` via AND + // and matched case-sensitively/exactly. Passed through from the graph layer. + AggregationFilters []string `protobuf:"bytes,6,rep,name=aggregation_filters,json=aggregationFilters,proto3" json:"aggregation_filters,omitempty"` } func (x *SearchRequest) Reset() { @@ -99,6 +160,20 @@ func (x *SearchRequest) GetRef() *v0.Reference { return nil } +func (x *SearchRequest) GetAggregations() []*AggregationOption { + if x != nil { + return x.Aggregations + } + return nil +} + +func (x *SearchRequest) GetAggregationFilters() []string { + if x != nil { + return x.AggregationFilters + } + return nil +} + type SearchResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -109,6 +184,8 @@ type SearchResponse struct { // more results in the list NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` TotalMatches int32 `protobuf:"varint,3,opt,name=total_matches,json=totalMatches,proto3" json:"total_matches,omitempty"` + // Aggregation results, one entry per requested aggregation. + Aggregations []*AggregationResult `protobuf:"bytes,4,rep,name=aggregations,proto3" json:"aggregations,omitempty"` } func (x *SearchResponse) Reset() { @@ -164,6 +241,13 @@ func (x *SearchResponse) GetTotalMatches() int32 { return 0 } +func (x *SearchResponse) GetAggregations() []*AggregationResult { + if x != nil { + return x.Aggregations + } + return nil +} + type SearchIndexRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -176,6 +260,11 @@ type SearchIndexRequest struct { PageToken string `protobuf:"bytes,2,opt,name=page_token,json=pageToken,proto3" json:"page_token,omitempty"` Query string `protobuf:"bytes,3,opt,name=query,proto3" json:"query,omitempty"` Ref *v0.Reference `protobuf:"bytes,4,opt,name=ref,proto3" json:"ref,omitempty"` + // Optional. Per-space aggregations (facets) to compute alongside the matches. + Aggregations []*AggregationOption `protobuf:"bytes,5,rep,name=aggregations,proto3" json:"aggregations,omitempty"` + // Optional. Decoded aggregation filters as KQL fragments; the engine parses + // each, forces exact/case-sensitive matching, and ANDs them with `query`. + AggregationFilters []string `protobuf:"bytes,6,rep,name=aggregation_filters,json=aggregationFilters,proto3" json:"aggregation_filters,omitempty"` } func (x *SearchIndexRequest) Reset() { @@ -238,6 +327,20 @@ func (x *SearchIndexRequest) GetRef() *v0.Reference { return nil } +func (x *SearchIndexRequest) GetAggregations() []*AggregationOption { + if x != nil { + return x.Aggregations + } + return nil +} + +func (x *SearchIndexRequest) GetAggregationFilters() []string { + if x != nil { + return x.AggregationFilters + } + return nil +} + type SearchIndexResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -248,6 +351,9 @@ type SearchIndexResponse struct { // more results in the list NextPageToken string `protobuf:"bytes,2,opt,name=next_page_token,json=nextPageToken,proto3" json:"next_page_token,omitempty"` TotalMatches int32 `protobuf:"varint,3,opt,name=total_matches,json=totalMatches,proto3" json:"total_matches,omitempty"` + // Per-space aggregation results. The service layer merges these across + // spaces before returning them to the caller. + Aggregations []*AggregationResult `protobuf:"bytes,4,rep,name=aggregations,proto3" json:"aggregations,omitempty"` } func (x *SearchIndexResponse) Reset() { @@ -303,6 +409,405 @@ func (x *SearchIndexResponse) GetTotalMatches() int32 { return 0 } +func (x *SearchIndexResponse) GetAggregations() []*AggregationResult { + if x != nil { + return x.Aggregations + } + return nil +} + +type AggregationOption struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Required. The indexed field to aggregate on (for terms/range + // aggregations) or to reduce (for metric aggregations). + Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"` + // Optional. Maximum number of buckets to return for a terms aggregation. + // Ignored for range and metric aggregations. + Size int32 `protobuf:"varint,2,opt,name=size,proto3" json:"size,omitempty"` + // Optional. Controls bucket selection, ordering and filtering. + // Ignored for metric aggregations. + BucketDefinition *BucketDefinition `protobuf:"bytes,3,opt,name=bucket_definition,json=bucketDefinition,proto3" json:"bucket_definition,omitempty"` + // Optional. Nested aggregations computed within each bucket of this + // aggregation. On bleve, sub-aggregations are emulated by walking the + // matched result set; on OpenSearch they translate to native composite + // aggregations. + SubAggregations []*AggregationOption `protobuf:"bytes,4,rep,name=sub_aggregations,json=subAggregations,proto3" json:"sub_aggregations,omitempty"` + // Optional. When set, this aggregation is a scalar metric over `field` + // rather than a bucket aggregation; the corresponding AggregationResult + // carries `value` instead of `buckets`. + MetricKind MetricKind `protobuf:"varint,5,opt,name=metric_kind,json=metricKind,proto3,enum=opencloud.services.search.v0.MetricKind" json:"metric_kind,omitempty"` +} + +func (x *AggregationOption) Reset() { + *x = AggregationOption{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[4] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AggregationOption) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AggregationOption) ProtoMessage() {} + +func (x *AggregationOption) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[4] + 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 AggregationOption.ProtoReflect.Descriptor instead. +func (*AggregationOption) Descriptor() ([]byte, []int) { + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{4} +} + +func (x *AggregationOption) GetField() string { + if x != nil { + return x.Field + } + return "" +} + +func (x *AggregationOption) GetSize() int32 { + if x != nil { + return x.Size + } + return 0 +} + +func (x *AggregationOption) GetBucketDefinition() *BucketDefinition { + if x != nil { + return x.BucketDefinition + } + return nil +} + +func (x *AggregationOption) GetSubAggregations() []*AggregationOption { + if x != nil { + return x.SubAggregations + } + return nil +} + +func (x *AggregationOption) GetMetricKind() MetricKind { + if x != nil { + return x.MetricKind + } + return MetricKind_METRIC_KIND_UNSPECIFIED +} + +type BucketDefinition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // One of "count", "keyAsString", "keyAsNumber". + SortBy string `protobuf:"bytes,1,opt,name=sort_by,json=sortBy,proto3" json:"sort_by,omitempty"` + IsDescending bool `protobuf:"varint,2,opt,name=is_descending,json=isDescending,proto3" json:"is_descending,omitempty"` + MinimumCount int32 `protobuf:"varint,3,opt,name=minimum_count,json=minimumCount,proto3" json:"minimum_count,omitempty"` + // Optional. When non-empty, the aggregation is computed over these numeric + // or date ranges instead of as a terms aggregation. + Ranges []*BucketRange `protobuf:"bytes,4,rep,name=ranges,proto3" json:"ranges,omitempty"` +} + +func (x *BucketDefinition) Reset() { + *x = BucketDefinition{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[5] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *BucketDefinition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BucketDefinition) ProtoMessage() {} + +func (x *BucketDefinition) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[5] + 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 BucketDefinition.ProtoReflect.Descriptor instead. +func (*BucketDefinition) Descriptor() ([]byte, []int) { + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{5} +} + +func (x *BucketDefinition) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *BucketDefinition) GetIsDescending() bool { + if x != nil { + return x.IsDescending + } + return false +} + +func (x *BucketDefinition) GetMinimumCount() int32 { + if x != nil { + return x.MinimumCount + } + return 0 +} + +func (x *BucketDefinition) GetRanges() []*BucketRange { + if x != nil { + return x.Ranges + } + return nil +} + +type BucketRange struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // At least one of `from` or `to` must be set. Both are string-encoded; + // numeric bounds are decimal strings, dates use RFC3339. + From string `protobuf:"bytes,1,opt,name=from,proto3" json:"from,omitempty"` + To string `protobuf:"bytes,2,opt,name=to,proto3" json:"to,omitempty"` +} + +func (x *BucketRange) Reset() { + *x = BucketRange{} + 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 *BucketRange) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*BucketRange) ProtoMessage() {} + +func (x *BucketRange) 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 BucketRange.ProtoReflect.Descriptor instead. +func (*BucketRange) Descriptor() ([]byte, []int) { + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{6} +} + +func (x *BucketRange) GetFrom() string { + if x != nil { + return x.From + } + return "" +} + +func (x *BucketRange) GetTo() string { + if x != nil { + return x.To + } + return "" +} + +type AggregationResult struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Field string `protobuf:"bytes,1,opt,name=field,proto3" json:"field,omitempty"` + Buckets []*Bucket `protobuf:"bytes,2,rep,name=buckets,proto3" json:"buckets,omitempty"` + // Scalar value for metric aggregations (metric_kind set on the + // corresponding AggregationOption). Unset / zero for terms/range + // aggregations. + Value float64 `protobuf:"fixed64,3,opt,name=value,proto3" json:"value,omitempty"` + // Echoes the metric_kind of the corresponding AggregationOption. Lets + // the cross-space merge layer pick the right reducer. + MetricKind MetricKind `protobuf:"varint,4,opt,name=metric_kind,json=metricKind,proto3,enum=opencloud.services.search.v0.MetricKind" json:"metric_kind,omitempty"` + // Accumulators used exclusively for AVG during cross-space merges. + // A backend computes (sum, count) per bucket so the service layer can + // merge them additively and emit `value = sum/count` only at the + // outermost collapse. Other metric kinds leave these unset. + Sum float64 `protobuf:"fixed64,5,opt,name=sum,proto3" json:"sum,omitempty"` + Count int64 `protobuf:"varint,6,opt,name=count,proto3" json:"count,omitempty"` +} + +func (x *AggregationResult) Reset() { + *x = AggregationResult{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[7] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *AggregationResult) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*AggregationResult) ProtoMessage() {} + +func (x *AggregationResult) ProtoReflect() protoreflect.Message { + 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 { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use AggregationResult.ProtoReflect.Descriptor instead. +func (*AggregationResult) Descriptor() ([]byte, []int) { + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{7} +} + +func (x *AggregationResult) GetField() string { + if x != nil { + return x.Field + } + return "" +} + +func (x *AggregationResult) GetBuckets() []*Bucket { + if x != nil { + return x.Buckets + } + return nil +} + +func (x *AggregationResult) GetValue() float64 { + if x != nil { + return x.Value + } + return 0 +} + +func (x *AggregationResult) GetMetricKind() MetricKind { + if x != nil { + return x.MetricKind + } + return MetricKind_METRIC_KIND_UNSPECIFIED +} + +func (x *AggregationResult) GetSum() float64 { + if x != nil { + return x.Sum + } + return 0 +} + +func (x *AggregationResult) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +type Bucket struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Key string `protobuf:"bytes,1,opt,name=key,proto3" json:"key,omitempty"` + Count int64 `protobuf:"varint,2,opt,name=count,proto3" json:"count,omitempty"` + AggregationFilterToken string `protobuf:"bytes,3,opt,name=aggregation_filter_token,json=aggregationFilterToken,proto3" json:"aggregation_filter_token,omitempty"` + // Nested aggregation results, one entry per sub_aggregation requested + // on the parent AggregationOption. + SubAggregations []*AggregationResult `protobuf:"bytes,4,rep,name=sub_aggregations,json=subAggregations,proto3" json:"sub_aggregations,omitempty"` +} + +func (x *Bucket) Reset() { + *x = Bucket{} + if protoimpl.UnsafeEnabled { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *Bucket) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*Bucket) ProtoMessage() {} + +func (x *Bucket) ProtoReflect() protoreflect.Message { + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[8] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use Bucket.ProtoReflect.Descriptor instead. +func (*Bucket) Descriptor() ([]byte, []int) { + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{8} +} + +func (x *Bucket) GetKey() string { + if x != nil { + return x.Key + } + return "" +} + +func (x *Bucket) GetCount() int64 { + if x != nil { + return x.Count + } + return 0 +} + +func (x *Bucket) GetAggregationFilterToken() string { + if x != nil { + return x.AggregationFilterToken + } + return "" +} + +func (x *Bucket) GetSubAggregations() []*AggregationResult { + if x != nil { + return x.SubAggregations + } + return nil +} + type IndexSpaceRequest struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -317,7 +822,7 @@ type IndexSpaceRequest struct { func (x *IndexSpaceRequest) Reset() { *x = IndexSpaceRequest{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[4] + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[9] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -330,7 +835,7 @@ func (x *IndexSpaceRequest) String() string { func (*IndexSpaceRequest) ProtoMessage() {} func (x *IndexSpaceRequest) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[4] + 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 { @@ -343,7 +848,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{4} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{9} } func (x *IndexSpaceRequest) GetSpaceId() string { @@ -394,7 +899,7 @@ type IndexSpaceResponse struct { func (x *IndexSpaceResponse) Reset() { *x = IndexSpaceResponse{} if protoimpl.UnsafeEnabled { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[5] + mi := &file_opencloud_services_search_v0_search_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -407,7 +912,7 @@ func (x *IndexSpaceResponse) String() string { func (*IndexSpaceResponse) ProtoMessage() {} func (x *IndexSpaceResponse) ProtoReflect() protoreflect.Message { - mi := &file_opencloud_services_search_v0_search_proto_msgTypes[5] + 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 { @@ -420,7 +925,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{5} + return file_opencloud_services_search_v0_search_proto_rawDescGZIP(), []int{10} } func (x *IndexSpaceResponse) GetSpaceId() string { @@ -479,7 +984,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, 0xae, 0x01, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xc0, 0x02, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, 0x23, 0x0a, 0x0a, 0x70, 0x61, @@ -490,115 +995,216 @@ var file_opencloud_services_search_v0_search_proto_rawDesc = []byte{ 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, 0x22, 0x9c, 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, 0x22, 0xb3, 0x01, 0x0a, 0x12, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, - 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x09, - 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, - 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 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, 0x22, 0xa1, 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, 0x22, - 0x94, 0x01, 0x0a, 0x11, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, - 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, - 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x66, 0x6f, 0x72, - 0x63, 0x65, 0x5f, 0x72, 0x65, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x0c, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x12, 0x26, - 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x18, 0x04, 0x20, - 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, - 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x22, 0xd1, 0x01, 0x0a, 0x12, 0x49, 0x6e, 0x64, 0x65, 0x78, - 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, - 0x08, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x07, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x40, 0x0a, 0x0e, 0x73, 0x70, 0x61, 0x63, - 0x65, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, - 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x0d, 0x73, 0x70, 0x61, - 0x63, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0x65, 0x64, 0x5f, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x64, 0x53, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x73, 0x70, 0x61, 0x63, 0x65, - 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x70, - 0x61, 0x63, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, - 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, 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, + 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, 0x35, 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, 0x09, 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, 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, 0xc5, 0x02, 0x0a, + 0x12, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, + 0x65, 0x73, 0x74, 0x12, 0x21, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x08, 0x70, 0x61, + 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 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, 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, + 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, 0x35, 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, 0x09, 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, 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, 0xd9, 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, 0x60, 0x0a, 0x10, 0x73, 0x75, 0x62, 0x5f, 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, 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, 0x12, 0x4f, 0x0a, 0x0b, 0x6d, 0x65, 0x74, 0x72, + 0x69, 0x63, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x05, 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, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, 0x0a, 0x6d, + 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x69, 0x6e, 0x64, 0x22, 0xb8, 0x01, 0x0a, 0x10, 0x42, 0x75, + 0x63, 0x6b, 0x65, 0x74, 0x44, 0x65, 0x66, 0x69, 0x6e, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x17, + 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 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, 0xf2, 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, - 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, + 0x2e, 0x76, 0x30, 0x2e, 0x42, 0x75, 0x63, 0x6b, 0x65, 0x74, 0x52, 0x07, 0x62, 0x75, 0x63, 0x6b, + 0x65, 0x74, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, + 0x28, 0x01, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x12, 0x49, 0x0a, 0x0b, 0x6d, 0x65, 0x74, + 0x72, 0x69, 0x63, 0x5f, 0x6b, 0x69, 0x6e, 0x64, 0x18, 0x04, 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, 0x0a, 0x6d, 0x65, 0x74, 0x72, 0x69, 0x63, + 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x10, 0x0a, 0x03, 0x73, 0x75, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, + 0x01, 0x52, 0x03, 0x73, 0x75, 0x6d, 0x12, 0x14, 0x0a, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xc6, 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, + 0x38, 0x0a, 0x18, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x5f, 0x66, + 0x69, 0x6c, 0x74, 0x65, 0x72, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x16, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x46, 0x69, + 0x6c, 0x74, 0x65, 0x72, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x12, 0x5a, 0x0a, 0x10, 0x73, 0x75, 0x62, + 0x5f, 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, 0x0f, 0x73, 0x75, 0x62, 0x41, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, + 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x94, 0x01, 0x0a, 0x11, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, + 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, + 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x17, 0x0a, 0x07, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, + 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x75, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, + 0x23, 0x0a, 0x0d, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x5f, 0x72, 0x65, 0x69, 0x6e, 0x64, 0x65, 0x78, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x52, 0x65, 0x69, + 0x6e, 0x64, 0x65, 0x78, 0x12, 0x26, 0x0a, 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, + 0x6e, 0x63, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x05, 0x42, 0x04, 0xe2, 0x41, 0x01, 0x01, 0x52, + 0x0b, 0x63, 0x6f, 0x6e, 0x63, 0x75, 0x72, 0x72, 0x65, 0x6e, 0x63, 0x79, 0x22, 0xd1, 0x01, 0x0a, + 0x12, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x19, 0x0a, 0x08, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x69, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x73, 0x70, 0x61, 0x63, 0x65, 0x49, 0x64, 0x12, 0x40, + 0x0a, 0x0e, 0x73, 0x70, 0x61, 0x63, 0x65, 0x5f, 0x64, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x6e, 0x52, 0x0d, 0x73, 0x70, 0x61, 0x63, 0x65, 0x44, 0x75, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x25, 0x0a, 0x0e, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, 0x64, 0x5f, 0x73, 0x70, 0x61, 0x63, + 0x65, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x69, 0x6e, 0x64, 0x65, 0x78, 0x65, + 0x64, 0x53, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x74, 0x6f, 0x74, 0x61, 0x6c, + 0x5f, 0x73, 0x70, 0x61, 0x63, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x74, + 0x6f, 0x74, 0x61, 0x6c, 0x53, 0x70, 0x61, 0x63, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x65, 0x72, + 0x72, 0x6f, 0x72, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x65, 0x72, 0x72, 0x6f, 0x72, + 0x2a, 0x7d, 0x0a, 0x0a, 0x4d, 0x65, 0x74, 0x72, 0x69, 0x63, 0x4b, 0x69, 0x6e, 0x64, 0x12, 0x1b, + 0x0a, 0x17, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x55, 0x4e, + 0x53, 0x50, 0x45, 0x43, 0x49, 0x46, 0x49, 0x45, 0x44, 0x10, 0x00, 0x12, 0x13, 0x0a, 0x0f, 0x4d, + 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x53, 0x55, 0x4d, 0x10, 0x01, + 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, + 0x4d, 0x49, 0x4e, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, 0x54, 0x52, 0x49, 0x43, 0x5f, + 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x4d, 0x41, 0x58, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x4d, 0x45, + 0x54, 0x52, 0x49, 0x43, 0x5f, 0x4b, 0x49, 0x4e, 0x44, 0x5f, 0x41, 0x56, 0x47, 0x10, 0x04, 0x32, + 0xb3, 0x02, 0x0a, 0x0e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, + 0x65, 0x72, 0x12, 0x85, 0x01, 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, - 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, - 0x31, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, - 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, - 0x65, 0x61, 0x72, 0x63, 0x68, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, - 0x73, 0x65, 0x22, 0x26, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x20, 0x3a, 0x01, 0x2a, 0x22, 0x1b, 0x2f, - 0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x69, 0x6e, - 0x64, 0x65, 0x78, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x42, 0xf2, 0x02, 0x5a, 0x4a, 0x67, - 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, - 0x6f, 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, - 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x67, 0x65, 0x6e, 0x2f, 0x67, 0x65, 0x6e, 0x2f, 0x6f, 0x70, - 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x2f, - 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x76, 0x30, 0x92, 0x41, 0xa2, 0x02, 0x12, 0xb7, 0x01, - 0x0a, 0x10, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, 0x73, 0x65, 0x61, 0x72, - 0x63, 0x68, 0x22, 0x51, 0x0a, 0x0e, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x6c, 0x6f, 0x75, 0x64, 0x20, - 0x47, 0x6d, 0x62, 0x48, 0x12, 0x29, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, - 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, - 0x75, 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x1a, - 0x14, 0x73, 0x75, 0x70, 0x70, 0x6f, 0x72, 0x74, 0x40, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, - 0x75, 0x64, 0x2e, 0x65, 0x75, 0x2a, 0x49, 0x0a, 0x0a, 0x41, 0x70, 0x61, 0x63, 0x68, 0x65, 0x2d, - 0x32, 0x2e, 0x30, 0x12, 0x3b, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, - 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, - 0x64, 0x2d, 0x65, 0x75, 0x2f, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2f, 0x62, - 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x69, 0x6e, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, - 0x32, 0x05, 0x31, 0x2e, 0x30, 0x2e, 0x30, 0x2a, 0x02, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, - 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x72, - 0x3e, 0x0a, 0x10, 0x44, 0x65, 0x76, 0x65, 0x6c, 0x6f, 0x70, 0x65, 0x72, 0x20, 0x4d, 0x61, 0x6e, - 0x75, 0x61, 0x6c, 0x12, 0x2a, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x64, 0x6f, 0x63, - 0x73, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x65, 0x75, 0x2f, 0x73, - 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x62, - 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 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 ( @@ -613,35 +1219,53 @@ func file_opencloud_services_search_v0_search_proto_rawDescGZIP() []byte { return file_opencloud_services_search_v0_search_proto_rawDescData } -var file_opencloud_services_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 6) +var file_opencloud_services_search_v0_search_proto_enumTypes = make([]protoimpl.EnumInfo, 1) +var file_opencloud_services_search_v0_search_proto_msgTypes = make([]protoimpl.MessageInfo, 11) var file_opencloud_services_search_v0_search_proto_goTypes = []interface{}{ - (*SearchRequest)(nil), // 0: opencloud.services.search.v0.SearchRequest - (*SearchResponse)(nil), // 1: opencloud.services.search.v0.SearchResponse - (*SearchIndexRequest)(nil), // 2: opencloud.services.search.v0.SearchIndexRequest - (*SearchIndexResponse)(nil), // 3: opencloud.services.search.v0.SearchIndexResponse - (*IndexSpaceRequest)(nil), // 4: opencloud.services.search.v0.IndexSpaceRequest - (*IndexSpaceResponse)(nil), // 5: opencloud.services.search.v0.IndexSpaceResponse - (*v0.Reference)(nil), // 6: opencloud.messages.search.v0.Reference - (*v0.Match)(nil), // 7: opencloud.messages.search.v0.Match - (*durationpb.Duration)(nil), // 8: google.protobuf.Duration + (MetricKind)(0), // 0: opencloud.services.search.v0.MetricKind + (*SearchRequest)(nil), // 1: opencloud.services.search.v0.SearchRequest + (*SearchResponse)(nil), // 2: opencloud.services.search.v0.SearchResponse + (*SearchIndexRequest)(nil), // 3: opencloud.services.search.v0.SearchIndexRequest + (*SearchIndexResponse)(nil), // 4: opencloud.services.search.v0.SearchIndexResponse + (*AggregationOption)(nil), // 5: opencloud.services.search.v0.AggregationOption + (*BucketDefinition)(nil), // 6: opencloud.services.search.v0.BucketDefinition + (*BucketRange)(nil), // 7: opencloud.services.search.v0.BucketRange + (*AggregationResult)(nil), // 8: opencloud.services.search.v0.AggregationResult + (*Bucket)(nil), // 9: opencloud.services.search.v0.Bucket + (*IndexSpaceRequest)(nil), // 10: opencloud.services.search.v0.IndexSpaceRequest + (*IndexSpaceResponse)(nil), // 11: opencloud.services.search.v0.IndexSpaceResponse + (*v0.Reference)(nil), // 12: opencloud.messages.search.v0.Reference + (*v0.Match)(nil), // 13: opencloud.messages.search.v0.Match + (*durationpb.Duration)(nil), // 14: google.protobuf.Duration } var file_opencloud_services_search_v0_search_proto_depIdxs = []int32{ - 6, // 0: opencloud.services.search.v0.SearchRequest.ref:type_name -> opencloud.messages.search.v0.Reference - 7, // 1: opencloud.services.search.v0.SearchResponse.matches:type_name -> opencloud.messages.search.v0.Match - 6, // 2: opencloud.services.search.v0.SearchIndexRequest.ref:type_name -> opencloud.messages.search.v0.Reference - 7, // 3: opencloud.services.search.v0.SearchIndexResponse.matches:type_name -> opencloud.messages.search.v0.Match - 8, // 4: opencloud.services.search.v0.IndexSpaceResponse.space_duration:type_name -> google.protobuf.Duration - 0, // 5: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest - 4, // 6: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest - 2, // 7: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest - 1, // 8: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse - 5, // 9: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse - 3, // 10: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse - 8, // [8:11] is the sub-list for method output_type - 5, // [5:8] is the sub-list for method input_type - 5, // [5:5] is the sub-list for extension type_name - 5, // [5:5] is the sub-list for extension extendee - 0, // [0:5] is the sub-list for field type_name + 12, // 0: opencloud.services.search.v0.SearchRequest.ref:type_name -> opencloud.messages.search.v0.Reference + 5, // 1: opencloud.services.search.v0.SearchRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption + 13, // 2: opencloud.services.search.v0.SearchResponse.matches:type_name -> opencloud.messages.search.v0.Match + 8, // 3: opencloud.services.search.v0.SearchResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult + 12, // 4: opencloud.services.search.v0.SearchIndexRequest.ref:type_name -> opencloud.messages.search.v0.Reference + 5, // 5: opencloud.services.search.v0.SearchIndexRequest.aggregations:type_name -> opencloud.services.search.v0.AggregationOption + 13, // 6: opencloud.services.search.v0.SearchIndexResponse.matches:type_name -> opencloud.messages.search.v0.Match + 8, // 7: opencloud.services.search.v0.SearchIndexResponse.aggregations:type_name -> opencloud.services.search.v0.AggregationResult + 6, // 8: opencloud.services.search.v0.AggregationOption.bucket_definition:type_name -> opencloud.services.search.v0.BucketDefinition + 5, // 9: opencloud.services.search.v0.AggregationOption.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationOption + 0, // 10: opencloud.services.search.v0.AggregationOption.metric_kind:type_name -> opencloud.services.search.v0.MetricKind + 7, // 11: opencloud.services.search.v0.BucketDefinition.ranges:type_name -> opencloud.services.search.v0.BucketRange + 9, // 12: opencloud.services.search.v0.AggregationResult.buckets:type_name -> opencloud.services.search.v0.Bucket + 0, // 13: opencloud.services.search.v0.AggregationResult.metric_kind:type_name -> opencloud.services.search.v0.MetricKind + 8, // 14: opencloud.services.search.v0.Bucket.sub_aggregations:type_name -> opencloud.services.search.v0.AggregationResult + 14, // 15: opencloud.services.search.v0.IndexSpaceResponse.space_duration:type_name -> google.protobuf.Duration + 1, // 16: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest + 10, // 17: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest + 3, // 18: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest + 2, // 19: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse + 11, // 20: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse + 4, // 21: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse + 19, // [19:22] is the sub-list for method output_type + 16, // [16:19] is the sub-list for method input_type + 16, // [16:16] is the sub-list for extension type_name + 16, // [16:16] is the sub-list for extension extendee + 0, // [0:16] is the sub-list for field type_name } func init() { file_opencloud_services_search_v0_search_proto_init() } @@ -699,7 +1323,7 @@ func file_opencloud_services_search_v0_search_proto_init() { } } file_opencloud_services_search_v0_search_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} { - switch v := v.(*IndexSpaceRequest); i { + switch v := v.(*AggregationOption); i { case 0: return &v.state case 1: @@ -711,6 +1335,66 @@ func file_opencloud_services_search_v0_search_proto_init() { } } file_opencloud_services_search_v0_search_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BucketDefinition); 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[6].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*BucketRange); 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[7].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*AggregationResult); 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[8].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*Bucket); 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[9].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[10].Exporter = func(v interface{}, i int) interface{} { switch v := v.(*IndexSpaceResponse); i { case 0: return &v.state @@ -728,13 +1412,14 @@ func file_opencloud_services_search_v0_search_proto_init() { File: protoimpl.DescBuilder{ GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_opencloud_services_search_v0_search_proto_rawDesc, - NumEnums: 0, - NumMessages: 6, + NumEnums: 1, + NumMessages: 11, NumExtensions: 0, NumServices: 2, }, GoTypes: file_opencloud_services_search_v0_search_proto_goTypes, DependencyIndexes: file_opencloud_services_search_v0_search_proto_depIdxs, + EnumInfos: file_opencloud_services_search_v0_search_proto_enumTypes, MessageInfos: file_opencloud_services_search_v0_search_proto_msgTypes, }.Build() File_opencloud_services_search_v0_search_proto = out.File diff --git a/protogen/gen/opencloud/services/search/v0/search.swagger.json b/protogen/gen/opencloud/services/search/v0/search.swagger.json index 43ff1e2c79..54e0b20a07 100644 --- a/protogen/gen/opencloud/services/search/v0/search.swagger.json +++ b/protogen/gen/opencloud/services/search/v0/search.swagger.json @@ -167,6 +167,67 @@ } } }, + "v0AggregationOption": { + "type": "object", + "properties": { + "field": { + "type": "string", + "description": "Required. The indexed field to aggregate on (for terms/range\naggregations) or to reduce (for metric aggregations)." + }, + "size": { + "type": "integer", + "format": "int32", + "description": "Optional. Maximum number of buckets to return for a terms aggregation.\nIgnored for range and metric aggregations." + }, + "bucketDefinition": { + "$ref": "#/definitions/v0BucketDefinition", + "description": "Optional. Controls bucket selection, ordering and filtering.\nIgnored for metric aggregations." + }, + "subAggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationOption" + }, + "description": "Optional. Nested aggregations computed within each bucket of this\naggregation. On bleve, sub-aggregations are emulated by walking the\nmatched result set; on OpenSearch they translate to native composite\naggregations." + }, + "metricKind": { + "$ref": "#/definitions/v0MetricKind", + "description": "Optional. When set, this aggregation is a scalar metric over `field`\nrather than a bucket aggregation; the corresponding AggregationResult\ncarries `value` instead of `buckets`." + } + } + }, + "v0AggregationResult": { + "type": "object", + "properties": { + "field": { + "type": "string" + }, + "buckets": { + "type": "array", + "items": { + "$ref": "#/definitions/v0Bucket" + } + }, + "value": { + "type": "number", + "format": "double", + "description": "Scalar value for metric aggregations (metric_kind set on the\ncorresponding AggregationOption). Unset / zero for terms/range\naggregations." + }, + "metricKind": { + "$ref": "#/definitions/v0MetricKind", + "description": "Echoes the metric_kind of the corresponding AggregationOption. Lets\nthe cross-space merge layer pick the right reducer." + }, + "sum": { + "type": "number", + "format": "double", + "description": "Accumulators used exclusively for AVG during cross-space merges.\nA backend computes (sum, count) per bucket so the service layer can\nmerge them additively and emit `value = sum/count` only at the\noutermost collapse. Other metric kinds leave these unset." + }, + "count": { + "type": "string", + "format": "int64" + } + } + }, "v0Audio": { "type": "object", "properties": { @@ -227,6 +288,63 @@ } } }, + "v0Bucket": { + "type": "object", + "properties": { + "key": { + "type": "string" + }, + "count": { + "type": "string", + "format": "int64" + }, + "aggregationFilterToken": { + "type": "string" + }, + "subAggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationResult" + }, + "description": "Nested aggregation results, one entry per sub_aggregation requested\non the parent AggregationOption." + } + } + }, + "v0BucketDefinition": { + "type": "object", + "properties": { + "sortBy": { + "type": "string", + "description": "One of \"count\", \"keyAsString\", \"keyAsNumber\"." + }, + "isDescending": { + "type": "boolean" + }, + "minimumCount": { + "type": "integer", + "format": "int32" + }, + "ranges": { + "type": "array", + "items": { + "$ref": "#/definitions/v0BucketRange" + }, + "description": "Optional. When non-empty, the aggregation is computed over these numeric\nor date ranges instead of as a terms aggregation." + } + } + }, + "v0BucketRange": { + "type": "object", + "properties": { + "from": { + "type": "string", + "description": "At least one of `from` or `to` must be set. Both are string-encoded;\nnumeric bounds are decimal strings, dates use RFC3339." + }, + "to": { + "type": "string" + } + } + }, "v0Entity": { "type": "object", "properties": { @@ -307,6 +425,13 @@ }, "livePhoto": { "$ref": "#/definitions/v0LivePhoto" + }, + "permissionsActionsAllowedValues": { + "type": "array", + "items": { + "type": "string" + }, + "description": "The effective permission actions of the caller, projected from the space\nroot permission set at query time (the same source as `permissions`), for\nthe driveItem `@libre.graph.permissions.actions.allowedValues` facet." } } }, @@ -422,6 +547,17 @@ } } }, + "v0MetricKind": { + "type": "string", + "enum": [ + "METRIC_KIND_UNSPECIFIED", + "METRIC_KIND_SUM", + "METRIC_KIND_MIN", + "METRIC_KIND_MAX", + "METRIC_KIND_AVG" + ], + "default": "METRIC_KIND_UNSPECIFIED" + }, "v0MotionPhoto": { "type": "object", "properties": { @@ -520,6 +656,20 @@ }, "ref": { "$ref": "#/definitions/v0Reference" + }, + "aggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationOption" + }, + "description": "Optional. Per-space aggregations (facets) to compute alongside the matches." + }, + "aggregationFilters": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional. Decoded aggregation filters as KQL fragments; the engine parses\neach, forces exact/case-sensitive matching, and ANDs them with `query`." } } }, @@ -539,6 +689,13 @@ "totalMatches": { "type": "integer", "format": "int32" + }, + "aggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationResult" + }, + "description": "Per-space aggregation results. The service layer merges these across\nspaces before returning them to the caller." } } }, @@ -559,6 +716,20 @@ }, "ref": { "$ref": "#/definitions/v0Reference" + }, + "aggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationOption" + }, + "description": "Optional. Aggregations (facets) to compute alongside the matches." + }, + "aggregationFilters": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional. Decoded aggregation filters, one per selected bucket, as KQL\nfragments (e.g. `audio.artist:\"Pink Floyd\"`). Combined with `query` via AND\nand matched case-sensitively/exactly. Passed through from the graph layer." } } }, @@ -578,6 +749,13 @@ "totalMatches": { "type": "integer", "format": "int32" + }, + "aggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationResult" + }, + "description": "Aggregation results, one entry per requested aggregation." } } }, diff --git a/protogen/proto/opencloud/messages/search/v0/search.proto b/protogen/proto/opencloud/messages/search/v0/search.proto index a2a25a8d5e..8814fb1e07 100644 --- a/protogen/proto/opencloud/messages/search/v0/search.proto +++ b/protogen/proto/opencloud/messages/search/v0/search.proto @@ -110,6 +110,10 @@ message Entity { MotionPhoto motionPhoto = 21; Video video = 22; LivePhoto livePhoto = 23; + // The effective permission actions of the caller, projected from the space + // root permission set at query time (the same source as `permissions`), for + // the driveItem `@libre.graph.permissions.actions.allowedValues` facet. + repeated string permissionsActionsAllowedValues = 24; } message Match { diff --git a/protogen/proto/opencloud/services/search/v0/search.proto b/protogen/proto/opencloud/services/search/v0/search.proto index 62b4b31436..79e8a75f1e 100644 --- a/protogen/proto/opencloud/services/search/v0/search.proto +++ b/protogen/proto/opencloud/services/search/v0/search.proto @@ -72,6 +72,14 @@ message SearchRequest { string query = 3; opencloud.messages.search.v0.Reference ref = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Aggregations (facets) to compute alongside the matches. + repeated AggregationOption aggregations = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Decoded aggregation filters, one per selected bucket, as KQL + // fragments (e.g. `audio.artist:"Pink Floyd"`). Combined with `query` via AND + // and matched case-sensitively/exactly. Passed through from the graph layer. + repeated string aggregation_filters = 6 [(google.api.field_behavior) = OPTIONAL]; } message SearchResponse { @@ -81,6 +89,9 @@ message SearchResponse { // more results in the list string next_page_token = 2; int32 total_matches = 3; + + // Aggregation results, one entry per requested aggregation. + repeated AggregationResult aggregations = 4; } message SearchIndexRequest { @@ -93,6 +104,13 @@ message SearchIndexRequest { string query = 3; opencloud.messages.search.v0.Reference ref = 4 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Per-space aggregations (facets) to compute alongside the matches. + repeated AggregationOption aggregations = 5 [(google.api.field_behavior) = OPTIONAL]; + + // Optional. Decoded aggregation filters as KQL fragments; the engine parses + // each, forces exact/case-sensitive matching, and ANDs them with `query`. + repeated string aggregation_filters = 6 [(google.api.field_behavior) = OPTIONAL]; } message SearchIndexResponse { @@ -102,6 +120,83 @@ message SearchIndexResponse { // more results in the list string next_page_token = 2; int32 total_matches = 3; + + // Per-space aggregation results. The service layer merges these across + // spaces before returning them to the caller. + repeated AggregationResult aggregations = 4; +} + +message AggregationOption { + // Required. The indexed field to aggregate on (for terms/range + // aggregations) or to reduce (for metric aggregations). + string field = 1; + // Optional. Maximum number of buckets to return for a terms aggregation. + // Ignored for range and metric aggregations. + int32 size = 2 [(google.api.field_behavior) = OPTIONAL]; + // Optional. Controls bucket selection, ordering and filtering. + // Ignored for metric aggregations. + BucketDefinition bucket_definition = 3 [(google.api.field_behavior) = OPTIONAL]; + // Optional. Nested aggregations computed within each bucket of this + // aggregation. On bleve, sub-aggregations are emulated by walking the + // matched result set; on OpenSearch they translate to native composite + // aggregations. + repeated AggregationOption sub_aggregations = 4 [(google.api.field_behavior) = OPTIONAL]; + // Optional. When set, this aggregation is a scalar metric over `field` + // rather than a bucket aggregation; the corresponding AggregationResult + // carries `value` instead of `buckets`. + MetricKind metric_kind = 5 [(google.api.field_behavior) = OPTIONAL]; +} + +enum MetricKind { + METRIC_KIND_UNSPECIFIED = 0; + METRIC_KIND_SUM = 1; + METRIC_KIND_MIN = 2; + METRIC_KIND_MAX = 3; + METRIC_KIND_AVG = 4; +} + +message BucketDefinition { + // One of "count", "keyAsString", "keyAsNumber". + string sort_by = 1; + bool is_descending = 2; + int32 minimum_count = 3; + // Optional. When non-empty, the aggregation is computed over these numeric + // or date ranges instead of as a terms aggregation. + repeated BucketRange ranges = 4; +} + +message BucketRange { + // At least one of `from` or `to` must be set. Both are string-encoded; + // numeric bounds are decimal strings, dates use RFC3339. + string from = 1; + string to = 2; +} + +message AggregationResult { + string field = 1; + repeated Bucket buckets = 2; + // Scalar value for metric aggregations (metric_kind set on the + // corresponding AggregationOption). Unset / zero for terms/range + // aggregations. + double value = 3; + // Echoes the metric_kind of the corresponding AggregationOption. Lets + // the cross-space merge layer pick the right reducer. + MetricKind metric_kind = 4; + // Accumulators used exclusively for AVG during cross-space merges. + // A backend computes (sum, count) per bucket so the service layer can + // merge them additively and emit `value = sum/count` only at the + // outermost collapse. Other metric kinds leave these unset. + double sum = 5; + int64 count = 6; +} + +message Bucket { + string key = 1; + int64 count = 2; + string aggregation_filter_token = 3; + // Nested aggregation results, one entry per sub_aggregation requested + // on the parent AggregationOption. + repeated AggregationResult sub_aggregations = 4; } message IndexSpaceRequest { diff --git a/services/graph/pkg/service/v0/driveitems.go b/services/graph/pkg/service/v0/driveitems.go index 167f8b1b62..a7bbacdd19 100644 --- a/services/graph/pkg/service/v0/driveitems.go +++ b/services/graph/pkg/service/v0/driveitems.go @@ -500,6 +500,18 @@ func cs3TimestampToTime(t *types.Timestamp) time.Time { return time.Unix(int64(t.GetSeconds()), int64(t.GetNanos())) } +// webURLForID builds an item's private link, {publicBaseURL}/f/{id}, which the +// web client reads as privateLink. Used by the drive item listing and the +// search hits so the field is identical in both. +func webURLForID(publicBaseURL *url.URL, id string) *string { + if publicBaseURL == nil { + return nil + } + u := *publicBaseURL + u.Path = path.Join(u.Path, "f", id) + return libregraph.PtrString(u.String()) +} + func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *storageprovider.ResourceInfo) (*libregraph.DriveItem, error) { size := new(int64) *size = int64(res.GetSize()) // TODO lurking overflow: make size of libregraph drive item use uint64 @@ -509,9 +521,7 @@ func cs3ResourceToDriveItem(logger *log.Logger, publicBaseURL *url.URL, res *sto Size: size, } - webURL := *publicBaseURL - webURL.Path = path.Join(webURL.Path, "f", storagespace.FormatResourceID(res.GetId())) - driveItem.WebUrl = libregraph.PtrString(webURL.String()) + driveItem.WebUrl = webURLForID(publicBaseURL, storagespace.FormatResourceID(res.GetId())) if name := path.Base(res.GetPath()); name != "" { driveItem.Name = &name diff --git a/services/graph/pkg/service/v0/searchquery.go b/services/graph/pkg/service/v0/searchquery.go new file mode 100644 index 0000000000..b407948cf3 --- /dev/null +++ b/services/graph/pkg/service/v0/searchquery.go @@ -0,0 +1,495 @@ +package svc + +import ( + "context" + "net/http" + "path" + "slices" + "time" + + storageprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" + "github.com/go-chi/render" + libregraph "github.com/opencloud-eu/libre-graph-api-go" + revaCtx "github.com/opencloud-eu/reva/v2/pkg/ctx" + "github.com/opencloud-eu/reva/v2/pkg/storagespace" + merrors "go-micro.dev/v4/errors" + "go-micro.dev/v4/metadata" + + searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" + searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" + "github.com/opencloud-eu/opencloud/services/graph/pkg/errorcode" +) + +// SearchQuery runs the search requests and returns results grouped by request +// (MS Graph searchQuery). +func (g Graph) SearchQuery(w http.ResponseWriter, r *http.Request) { + var req libregraph.SearchQueryRequest + if err := StrictJSONUnmarshal(r.Body, &req); err != nil { + g.logger.Debug().Err(err).Msg("could not decode search query request") + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "invalid body schema definition") + return + } + + if len(req.Requests) == 0 { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, "requests array must not be empty") + return + } + + th := r.Header.Get(revaCtx.TokenHeader) + ctx := revaCtx.ContextSetToken(r.Context(), th) + ctx = metadata.Set(ctx, revaCtx.TokenHeader, th) + + expandThumbnails := driveItemRelationExpanded(r, _expandThumbnails) + + responses := make([]libregraph.SearchResponse, 0, len(req.Requests)) + for _, sr := range req.Requests { + sresp, err := g.runSingleSearch(ctx, sr, expandThumbnails) + if err != nil { + g.renderSearchError(w, r, err) + return + } + responses = append(responses, sresp) + } + + render.Status(r, http.StatusOK) + render.JSON(w, r, libregraph.SearchQuery200Response{Value: responses}) +} + +func (g Graph) runSingleSearch(ctx context.Context, sr libregraph.SearchRequest, expandThumbnails bool) (libregraph.SearchResponse, error) { + from, size := clampPagination(sr.From, sr.Size) + + // The gRPC layer has no from field: request from+size matches and slice + // client-side. int64 avoids int32 overflow. + pageSize := int32(int64(from) + int64(size)) + if size == 0 { + pageSize = 0 + } + + rsp, err := g.searchService.Search(ctx, &searchsvc.SearchRequest{ + Query: sr.Query.QueryString, + PageSize: pageSize, + Aggregations: libregraphAggregationsToSearch(sr.Aggregations), + }) + if err != nil { + return libregraph.SearchResponse{}, err + } + + // the current user id decides the @libre.graph.me.following (favorite) flag, + // mirroring the WebDAV report's oc:favorite (favorited by the caller). + uid := "" + if u, ok := revaCtx.ContextGetUser(ctx); ok { + uid = u.GetId().GetOpaqueId() + } + + hits := make([]libregraph.SearchHit, 0) + if size > 0 { + start := min(int(from), len(rsp.Matches)) + end := min(start+int(size), len(rsp.Matches)) + for i := start; i < end; i++ { + hit := matchToSearchHit(rsp.Matches[i], int32(i+1), uid) + hit.Resource.WebUrl = webURLForID(g.publicBaseURL, hit.Resource.GetId()) + if expandThumbnails { + setDriveItemThumbnailsByID(hit.Resource, hit.Resource.GetId(), g.config.Commons.OpenCloudURL) + } + hits = append(hits, hit) + } + } + + total := int64(rsp.TotalMatches) + more := int64(from+size) < total + return libregraph.SearchResponse{ + SearchTerms: []string{sr.Query.QueryString}, + HitsContainers: []libregraph.SearchHitsContainer{{ + Hits: hits, + Total: &total, + MoreResultsAvailable: &more, + Aggregations: searchAggregationsToLibregraph(rsp.Aggregations, sr.Aggregations), + }}, + }, nil +} + +// maxPageSize mirrors the openapi spec's upper bound on SearchRequest.size. +const maxPageSize = 500 + +// clampPagination normalises the from/size JSON pointers into safe non-negative +// int32s; openapi-generator does not enforce the spec's [0,500]/[0,inf) bounds. +func clampPagination(fromP, sizeP *int32) (int32, int32) { + from := int32(0) + if fromP != nil && *fromP > 0 { + from = *fromP + } + size := int32(25) + if sizeP != nil { + size = *sizeP + if size < 0 { + size = 0 + } + if size > maxPageSize { + size = maxPageSize + } + } + // from+size is sent as a single int32 PageSize; guard against overflow. + const maxInt32 = int32(1<<31 - 1) + if int64(from)+int64(size) > int64(maxInt32) { + if from > maxInt32-size { + from = maxInt32 - size + } + } + return from, size +} + +func libregraphAggregationsToSearch(in []libregraph.AggregationOption) []*searchsvc.AggregationOption { + if len(in) == 0 { + return nil + } + out := make([]*searchsvc.AggregationOption, 0, len(in)) + for _, a := range in { + agg := &searchsvc.AggregationOption{Field: a.Field} + if a.Size != nil { + agg.Size = *a.Size + } + if a.BucketDefinition != nil { + agg.BucketDefinition = libregraphBucketDefinitionToSearch(*a.BucketDefinition) + } + if len(a.LibreGraphSubAggregations) > 0 { + agg.SubAggregations = libregraphAggregationsToSearch(a.LibreGraphSubAggregations) + } + if a.LibreGraphMetricDefinition != nil { + agg.MetricKind = metricKindFromLibregraph(a.LibreGraphMetricDefinition.Kind) + } + out = append(out, agg) + } + return out +} + +// metricKindFromLibregraph maps the OpenAPI string enum to the proto enum; +// unknown values degrade to UNSPECIFIED. +func metricKindFromLibregraph(kind string) searchsvc.MetricKind { + switch kind { + case "sum": + return searchsvc.MetricKind_METRIC_KIND_SUM + case "min": + return searchsvc.MetricKind_METRIC_KIND_MIN + case "max": + return searchsvc.MetricKind_METRIC_KIND_MAX + case "avg": + return searchsvc.MetricKind_METRIC_KIND_AVG + } + return searchsvc.MetricKind_METRIC_KIND_UNSPECIFIED +} + +// metricKindToLibregraph is the inverse of metricKindFromLibregraph. +func metricKindToLibregraph(kind searchsvc.MetricKind) *string { + var s string + switch kind { + case searchsvc.MetricKind_METRIC_KIND_SUM: + s = "sum" + case searchsvc.MetricKind_METRIC_KIND_MIN: + s = "min" + case searchsvc.MetricKind_METRIC_KIND_MAX: + s = "max" + case searchsvc.MetricKind_METRIC_KIND_AVG: + s = "avg" + default: + return nil + } + return &s +} + +func libregraphBucketDefinitionToSearch(in libregraph.BucketDefinition) *searchsvc.BucketDefinition { + bd := &searchsvc.BucketDefinition{SortBy: in.SortBy} + if in.IsDescending != nil { + bd.IsDescending = *in.IsDescending + } + if in.MinimumCount != nil { + bd.MinimumCount = *in.MinimumCount + } + if len(in.Ranges) > 0 { + bd.Ranges = make([]*searchsvc.BucketRange, 0, len(in.Ranges)) + for _, r := range in.Ranges { + br := &searchsvc.BucketRange{} + if r.From != nil { + br.From = *r.From + } + if r.To != nil { + br.To = *r.To + } + bd.Ranges = append(bd.Ranges, br) + } + } + return bd +} + +func searchAggregationsToLibregraph(in []*searchsvc.AggregationResult, defs []libregraph.AggregationOption) []libregraph.SearchAggregation { + if len(in) == 0 { + return nil + } + defsByField := make(map[string]libregraph.AggregationOption, len(defs)) + for _, d := range defs { + defsByField[d.Field] = d + } + out := make([]libregraph.SearchAggregation, 0, len(in)) + for _, a := range in { + field := a.GetField() + def := defsByField[field] + // Metric result: a scalar, no buckets. For AVG the backend + // transported (sum, count); collapse to the average here. + if kind := a.GetMetricKind(); kind != searchsvc.MetricKind_METRIC_KIND_UNSPECIFIED { + value := a.GetValue() + if kind == searchsvc.MetricKind_METRIC_KIND_AVG && a.GetCount() > 0 { + value = a.GetSum() / float64(a.GetCount()) + } + out = append(out, libregraph.SearchAggregation{ + Field: &field, + LibreGraphMetric: &libregraph.SearchMetric{Kind: metricKindToLibregraph(kind), Value: &value}, + }) + continue + } + buckets := make([]libregraph.SearchBucket, 0, len(a.GetBuckets())) + for _, b := range a.GetBuckets() { + key := b.GetKey() + count := b.GetCount() + lb := libregraph.SearchBucket{ + Key: &key, + Count: &count, + } + if subs := b.GetSubAggregations(); len(subs) > 0 { + lb.LibreGraphSubAggregations = searchAggregationsToLibregraph(subs, def.LibreGraphSubAggregations) + } + buckets = append(buckets, lb) + } + out = append(out, libregraph.SearchAggregation{ + Field: &field, + Buckets: buckets, + }) + } + return out +} + +func (g Graph) renderSearchError(w http.ResponseWriter, r *http.Request, err error) { + e := merrors.Parse(err.Error()) + switch e.Code { + case http.StatusBadRequest: + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, e.Detail) + default: + g.logger.Error().Err(err).Msg("search service call failed") + errorcode.GeneralException.Render(w, r, http.StatusInternalServerError, err.Error()) + } +} + +func matchToSearchHit(m *searchmsg.Match, rank int32, uid string) libregraph.SearchHit { + hit := libregraph.SearchHit{ + HitId: libregraph.PtrString(searchEntityHitID(m.GetEntity())), + Rank: &rank, + } + if h := m.GetEntity().GetHighlights(); h != "" { + hit.Summary = libregraph.PtrString(h) + } + di := searchEntityToDriveItem(m.GetEntity(), uid) + hit.Resource = di + return hit +} + +func searchEntityHitID(e *searchmsg.Entity) string { + return storagespace.FormatResourceID(&storageprovider.ResourceId{ + StorageId: e.GetId().GetStorageId(), + SpaceId: e.GetId().GetSpaceId(), + OpaqueId: e.GetId().GetOpaqueId(), + }) +} + +func searchEntityToDriveItem(e *searchmsg.Entity, uid string) *libregraph.DriveItem { + size := int64(e.GetSize()) + di := &libregraph.DriveItem{ + Id: libregraph.PtrString(searchEntityHitID(e)), + Name: libregraph.PtrString(e.GetName()), + Size: &size, + } + if etag := e.GetEtag(); etag != "" { + di.ETag = &etag + } + if mt := e.GetLastModifiedTime(); mt != nil { + lm := time.Unix(mt.GetSeconds(), int64(mt.GetNanos())).UTC() + di.LastModifiedDateTime = &lm + } + if e.GetType() == uint64(storageprovider.ResourceType_RESOURCE_TYPE_FILE) && e.GetMimeType() != "" { + mt := e.GetMimeType() + di.File = &libregraph.OpenGraphFile{MimeType: &mt} + } + if e.GetType() == uint64(storageprovider.ResourceType_RESOURCE_TYPE_CONTAINER) { + di.Folder = &libregraph.Folder{} + } + if p := e.GetParentId(); p != nil { + ref := libregraph.NewItemReference() + ref.SetDriveId(storagespace.FormatStorageID(p.GetStorageId(), p.GetSpaceId())) + ref.SetId(storagespace.FormatResourceID(&storageprovider.ResourceId{ + StorageId: p.GetStorageId(), + SpaceId: p.GetSpaceId(), + OpaqueId: p.GetOpaqueId(), + })) + if refPath := e.GetRef().GetPath(); refPath != "" { + ref.SetName(path.Base(path.Dir(refPath))) + ref.SetPath(path.Dir(refPath)) + } + di.ParentReference = ref + } + di.RemoteItem = searchEntityToRemoteItem(e) + di.Audio = searchAudioToLibregraph(e.GetAudio()) + di.Image = searchImageToLibregraph(e.GetImage()) + di.Photo = searchPhotoToLibregraph(e.GetPhoto()) + di.Location = searchLocationToLibregraph(e.GetLocation()) + di.Video = searchVideoToLibregraph(e.GetVideo()) + di.LibreGraphMotionPhoto = searchMotionPhotoToLibregraph(e.GetMotionPhoto()) + di.LibreGraphLivePhoto = searchLivePhotoToLibregraph(e.GetLivePhoto()) + if tags := e.GetTags(); len(tags) > 0 { + di.LibreGraphTags = tags + } + if av := e.GetPermissionsActionsAllowedValues(); len(av) > 0 { + di.LibreGraphPermissionsActionsAllowedValues = av + } + // @libre.graph.me.following mirrors the WebDAV report's oc:favorite: the + // report emits it only when the current user has favorited the item, so set + // it to true only in that case and leave it unset otherwise. + if uid != "" && slices.Contains(e.GetFavorites(), uid) { + di.LibreGraphMeFollowing = libregraph.PtrBool(true) + } + return di +} + +// searchEntityToRemoteItem describes a hit that lives in a space shared with the +// caller: the item id in the owner's drive and the mountpoint it is reached +// through. Absent for hits from the caller's own spaces. +func searchEntityToRemoteItem(e *searchmsg.Entity) *libregraph.RemoteItem { + id := e.GetRemoteItemId() + if id == nil { + return nil + } + + item := libregraph.NewRemoteItem() + item.SetId(storagespace.FormatResourceID(&storageprovider.ResourceId{ + StorageId: id.GetStorageId(), + SpaceId: id.GetSpaceId(), + OpaqueId: id.GetOpaqueId(), + })) + + if root := e.GetShareRootName(); root != "" { + item.SetPath(root) + item.SetName(path.Base(root)) + } + + return item +} + +func searchAudioToLibregraph(a *searchmsg.Audio) *libregraph.Audio { + if a == nil { + return nil + } + out := &libregraph.Audio{ + Album: a.Album, + AlbumArtist: a.AlbumArtist, + Artist: a.Artist, + Bitrate: a.Bitrate, + Composers: a.Composers, + Copyright: a.Copyright, + Disc: a.Disc, + DiscCount: a.DiscCount, + Duration: a.Duration, + Genre: a.Genre, + HasDrm: a.HasDrm, + IsVariableBitrate: a.IsVariableBitrate, + Title: a.Title, + Track: a.Track, + TrackCount: a.TrackCount, + Year: a.Year, + } + return out +} + +func searchImageToLibregraph(i *searchmsg.Image) *libregraph.Image { + if i == nil { + return nil + } + return &libregraph.Image{Width: i.Width, Height: i.Height} +} + +func searchPhotoToLibregraph(p *searchmsg.Photo) *libregraph.Photo { + if p == nil { + return nil + } + out := &libregraph.Photo{ + CameraMake: p.CameraMake, + CameraModel: p.CameraModel, + ExposureDenominator: f32ToF64(p.ExposureDenominator), + ExposureNumerator: f32ToF64(p.ExposureNumerator), + FNumber: f32ToF64(p.FNumber), + FocalLength: f32ToF64(p.FocalLength), + Iso: p.Iso, + Orientation: p.Orientation, + } + if p.TakenDateTime != nil { + t := time.Unix(p.TakenDateTime.GetSeconds(), int64(p.TakenDateTime.GetNanos())).UTC() + out.TakenDateTime = &t + } + return out +} + +func searchLocationToLibregraph(l *searchmsg.GeoCoordinates) *libregraph.GeoCoordinates { + if l == nil { + return nil + } + return &libregraph.GeoCoordinates{ + Altitude: l.Altitude, + Latitude: l.Latitude, + Longitude: l.Longitude, + } +} + +func searchVideoToLibregraph(v *searchmsg.Video) *libregraph.Video { + if v == nil { + return nil + } + return &libregraph.Video{ + AudioBitsPerSample: v.AudioBitsPerSample, + AudioChannels: v.AudioChannels, + AudioFormat: v.AudioFormat, + AudioSamplesPerSecond: v.AudioSamplesPerSecond, + Bitrate: v.Bitrate, + Duration: v.Duration, + FourCC: v.FourCC, + FrameRate: v.FrameRate, + Height: v.Height, + Width: v.Width, + } +} + +func searchMotionPhotoToLibregraph(m *searchmsg.MotionPhoto) *libregraph.MotionPhoto { + if m == nil { + return nil + } + return &libregraph.MotionPhoto{ + Version: m.Version, + PresentationTimestampUs: m.PresentationTimestampUs, + VideoSize: m.VideoSize, + } +} + +func searchLivePhotoToLibregraph(l *searchmsg.LivePhoto) *libregraph.LivePhoto { + if l == nil { + return nil + } + return &libregraph.LivePhoto{ + ContentId: l.GetContentId(), + StillImageTimeUs: l.StillImageTimeUs, + Auto: l.Auto, + VitalityScore: l.VitalityScore, + VitalityScoringVersion: l.VitalityScoringVersion, + } +} + +func f32ToF64(v *float32) *float64 { + if v == nil { + return nil + } + f := float64(*v) + return &f +} diff --git a/services/graph/pkg/service/v0/searchquery_test.go b/services/graph/pkg/service/v0/searchquery_test.go new file mode 100644 index 0000000000..11eba9e021 --- /dev/null +++ b/services/graph/pkg/service/v0/searchquery_test.go @@ -0,0 +1,200 @@ +package svc + +import ( + "bytes" + "context" + "encoding/json" + "net/http" + "net/http/httptest" + + // ginkgo qualified: the svc package declares Context (option.go), which + // would collide with a dot-import. + ginkgo "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + "go-micro.dev/v4/client" + + "github.com/opencloud-eu/opencloud/pkg/log" + searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" + searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" +) + +type stubSearchService struct { + search func(*searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) +} + +func (s stubSearchService) Search(_ context.Context, req *searchsvc.SearchRequest, _ ...client.CallOption) (*searchsvc.SearchResponse, error) { + return s.search(req) +} + +func (s stubSearchService) IndexSpace(_ context.Context, _ *searchsvc.IndexSpaceRequest, _ ...client.CallOption) (searchsvc.SearchProvider_IndexSpaceService, error) { + return nil, nil +} + +func graphWithSearch(stub stubSearchService) Graph { + logger := log.NewLogger() + return Graph{ + BaseGraphService: BaseGraphService{logger: &logger}, + searchService: stub, + } +} + +func postSearchQuery(g Graph, body string) *httptest.ResponseRecorder { + req := httptest.NewRequest(http.MethodPost, "/search/query", bytes.NewBufferString(body)) + req.Header.Set("Content-Type", "application/json") + rr := httptest.NewRecorder() + g.SearchQuery(rr, req) + return rr +} + +func int32Ptr(v int32) *int32 { return &v } + +// searchHitRemoteItem decodes the remoteItem of the first hit, nil when absent. +func searchHitRemoteItem(rr *httptest.ResponseRecorder) *struct { + Id *string `json:"id"` + Name *string `json:"name"` + Path *string `json:"path"` +} { + var decoded struct { + Value []struct { + HitsContainers []struct { + Hits []struct { + Resource struct { + RemoteItem *struct { + Id *string `json:"id"` + Name *string `json:"name"` + Path *string `json:"path"` + } `json:"remoteItem"` + } `json:"resource"` + } `json:"hits"` + } `json:"hitsContainers"` + } `json:"value"` + } + Expect(json.Unmarshal(rr.Body.Bytes(), &decoded)).To(Succeed()) + Expect(decoded.Value).To(HaveLen(1)) + Expect(decoded.Value[0].HitsContainers).To(HaveLen(1)) + Expect(decoded.Value[0].HitsContainers[0].Hits).To(HaveLen(1)) + + return decoded.Value[0].HitsContainers[0].Hits[0].Resource.RemoteItem +} + +var _ = ginkgo.Describe("SearchQuery", func() { + ginkgo.It("forwards aggregations to the search service and groups results by request", func() { + var captured *searchsvc.SearchRequest + g := graphWithSearch(stubSearchService{ + search: func(req *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + captured = req + return &searchsvc.SearchResponse{ + TotalMatches: 10, + Aggregations: []*searchsvc.AggregationResult{{ + Field: "audio.artist", + Buckets: []*searchsvc.Bucket{ + {Key: "Pink Floyd", Count: 7}, + {Key: "Motörhead", Count: 3}, + }, + }}, + }, nil + }, + }) + + rr := postSearchQuery(g, `{ + "requests": [{ + "entityTypes": ["driveItem"], + "query": {"queryString": "mediatype:audio"}, + "size": 0, + "aggregations": [{ + "field": "audio.artist", + "size": 5, + "bucketDefinition": {"sortBy": "count", "isDescending": true} + }] + }] + }`) + + Expect(rr.Code).To(Equal(http.StatusOK), rr.Body.String()) + Expect(captured).ToNot(BeNil()) + Expect(captured.Aggregations).To(HaveLen(1)) + Expect(captured.Aggregations[0].Field).To(Equal("audio.artist")) + + var decoded struct { + Value []struct { + HitsContainers []struct { + Aggregations []struct { + Field *string `json:"field"` + Buckets []struct { + Key *string `json:"key"` + Count *int64 `json:"count"` + } `json:"buckets"` + } `json:"aggregations"` + } `json:"hitsContainers"` + } `json:"value"` + } + Expect(json.Unmarshal(rr.Body.Bytes(), &decoded)).To(Succeed()) + Expect(decoded.Value).To(HaveLen(1)) + Expect(decoded.Value[0].HitsContainers).To(HaveLen(1)) + + aggs := decoded.Value[0].HitsContainers[0].Aggregations + Expect(aggs).To(HaveLen(1)) + Expect(aggs[0].Field).To(HaveValue(Equal("audio.artist"))) + Expect(aggs[0].Buckets).To(HaveLen(2)) + }) + + ginkgo.It("describes a hit from a shared space as a remote item", func() { + g := graphWithSearch(stubSearchService{ + search: func(_ *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + return &searchsvc.SearchResponse{ + TotalMatches: 1, + Matches: []*searchmsg.Match{{ + Entity: &searchmsg.Entity{ + Id: &searchmsg.ResourceID{StorageId: "1", SpaceId: "2", OpaqueId: "3"}, + Name: "contract.pdf", + ShareRootName: "/Project X", + RemoteItemId: &searchmsg.ResourceID{StorageId: "4", SpaceId: "5", OpaqueId: "6"}, + }, + }}, + }, nil + }, + }) + + rr := postSearchQuery(g, `{"requests": [{"entityTypes": ["driveItem"], "query": {"queryString": "contract"}}]}`) + Expect(rr.Code).To(Equal(http.StatusOK)) + + remote := searchHitRemoteItem(rr) + Expect(remote).ToNot(BeNil()) + Expect(remote.Id).To(HaveValue(Equal("4$5!6"))) + Expect(remote.Path).To(HaveValue(Equal("/Project X"))) + Expect(remote.Name).To(HaveValue(Equal("Project X")), "the mountpoint name the caller sees") + }) + + ginkgo.It("leaves the remote item out for hits from the caller's own spaces", func() { + g := graphWithSearch(stubSearchService{ + search: func(_ *searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + return &searchsvc.SearchResponse{ + TotalMatches: 1, + Matches: []*searchmsg.Match{{ + Entity: &searchmsg.Entity{ + Id: &searchmsg.ResourceID{StorageId: "1", SpaceId: "2", OpaqueId: "3"}, + Name: "notes.txt", + }, + }}, + }, nil + }, + }) + + rr := postSearchQuery(g, `{"requests": [{"entityTypes": ["driveItem"], "query": {"queryString": "notes"}}]}`) + Expect(rr.Code).To(Equal(http.StatusOK)) + Expect(searchHitRemoteItem(rr)).To(BeNil()) + }) + + ginkgo.DescribeTable("clampPagination keeps from/size within valid bounds", + func(from, size *int32, wantFrom, wantSize int32) { + gotFrom, gotSize := clampPagination(from, size) + Expect(gotFrom).To(Equal(wantFrom)) + Expect(gotSize).To(Equal(wantSize)) + }, + ginkgo.Entry("defaults", nil, nil, int32(0), int32(25)), + ginkgo.Entry("zero size", int32Ptr(5), int32Ptr(0), int32(5), int32(0)), + ginkgo.Entry("negative from clamps to zero", int32Ptr(-10), int32Ptr(5), int32(0), int32(5)), + ginkgo.Entry("negative size clamps to zero", int32Ptr(10), int32Ptr(-1), int32(10), int32(0)), + ginkgo.Entry("oversized size clamps to max", int32Ptr(0), int32Ptr(1000), int32(0), int32(500)), + ginkgo.Entry("from+size overflow collapses", int32Ptr(1<<31-1), int32Ptr(500), int32(1<<31-1-500), int32(500)), + ) +}) diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index 9b9636e370..f8f3496efe 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -110,6 +110,8 @@ type Service interface { //nolint:interfacebloat GetTags(w http.ResponseWriter, r *http.Request) AssignTags(w http.ResponseWriter, r *http.Request) UnassignTags(w http.ResponseWriter, r *http.Request) + + SearchQuery(w http.ResponseWriter, r *http.Request) } // NewService returns a service implementation for Service. @@ -288,6 +290,7 @@ func NewService(opts ...Option) (Graph, error) { //nolint:maintidx r.Get("/", svc.GetRoleDefinitions) r.Get("/{roleID}", svc.GetRoleDefinition) }) + r.Post("/search/query", svc.SearchQuery) }) r.Route("/v1.0", func(r chi.Router) { r.Route("/extensions/org.libregraph", func(r chi.Router) { diff --git a/services/graph/pkg/service/v0/sharedbyme.go b/services/graph/pkg/service/v0/sharedbyme.go index cc61df8b75..a3c385096c 100644 --- a/services/graph/pkg/service/v0/sharedbyme.go +++ b/services/graph/pkg/service/v0/sharedbyme.go @@ -42,7 +42,7 @@ func (g Graph) GetSharedByMe(w http.ResponseWriter, r *http.Request) { expandThumbnails := strings.Contains(expand, "thumbnails") if expandThumbnails { for k, item := range driveItems { - setShareThumbnails(&item, item.GetId(), g.config.Commons.OpenCloudURL) + setDriveItemThumbnailsByID(&item, item.GetId(), g.config.Commons.OpenCloudURL) driveItems[k] = item } } diff --git a/services/graph/pkg/service/v0/sharedwithme.go b/services/graph/pkg/service/v0/sharedwithme.go index 87aa96a887..c4c28a9d50 100644 --- a/services/graph/pkg/service/v0/sharedwithme.go +++ b/services/graph/pkg/service/v0/sharedwithme.go @@ -71,7 +71,7 @@ func (g Graph) listSharedWithMe(ctx context.Context, expandThumbnails bool) ([]l if expandThumbnails { for k, item := range driveItems { - setShareThumbnails(&item, item.RemoteItem.GetId(), g.config.Commons.OpenCloudURL) + setDriveItemThumbnailsByID(&item, item.RemoteItem.GetId(), g.config.Commons.OpenCloudURL) driveItems[k] = item } } diff --git a/services/graph/pkg/service/v0/thumbnails.go b/services/graph/pkg/service/v0/thumbnails.go index 75d28bd3a7..82632e6f02 100644 --- a/services/graph/pkg/service/v0/thumbnails.go +++ b/services/graph/pkg/service/v0/thumbnails.go @@ -57,9 +57,10 @@ func previewThumbnail(base string, box int32) *libregraph.Thumbnail { return &libregraph.Thumbnail{Url: &url} } -// setShareThumbnails works off the driveItem, the share listings have no resource -// info. The id comes separately, a received share carries it on its remote item. -func setShareThumbnails(item *libregraph.DriveItem, itemID, baseURL string) { +// setDriveItemThumbnailsByID works off the driveItem itself (mime type from the +// item, id passed separately), for callers that have no CS3 resource info: the +// share listings and the search results. +func setDriveItemThumbnailsByID(item *libregraph.DriveItem, itemID, baseURL string) { mimeType := item.GetFile().MimeType if itemID == "" || mimeType == nil || !thumbnail.IsMimeTypeSupported(*mimeType) { return diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index 47caee4070..1b7ff47285 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -30,6 +30,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" searchmsg "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" + "github.com/opencloud-eu/opencloud/services/graph/pkg/unifiedrole" "github.com/opencloud-eu/opencloud/services/search/pkg/config" "github.com/opencloud-eu/opencloud/services/search/pkg/content" "github.com/opencloud-eu/opencloud/services/search/pkg/metrics" @@ -410,7 +411,8 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest, } searchRequest := &searchsvc.SearchIndexRequest{ - Query: req.Query, + Query: req.Query, + Aggregations: req.GetAggregations(), Ref: &searchmsg.Reference{ ResourceId: searchRootID, Path: searchPathPrefix, @@ -446,6 +448,9 @@ func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest, isMountpoint := isShared && match.GetEntity().GetRef().GetPath() == "." isDir := match.GetEntity().GetMimeType() == "httpd/unix-directory" match.Entity.Permissions = convertToWebDAVPermissions(isShared, isMountpoint, isDir, permissions) + // allowedValues is the same effective permission set the WebDAV report's + // oc:permissions string projects, in libregraph action notation. + match.Entity.PermissionsActionsAllowedValues = unifiedrole.CS3ResourcePermissionsToLibregraphActions(permissions) if req.Ref != nil && searchPathPrefix == "/"+match.Entity.Name { continue diff --git a/services/search/pkg/service/grpc/v0/service.go b/services/search/pkg/service/grpc/v0/service.go index 6cb258ee43..72f02ffa95 100644 --- a/services/search/pkg/service/grpc/v0/service.go +++ b/services/search/pkg/service/grpc/v0/service.go @@ -22,6 +22,7 @@ import ( "go-micro.dev/v4/metadata" "golang.org/x/sync/errgroup" grpcmetadata "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/durationpb" "github.com/opencloud-eu/opencloud/pkg/log" @@ -94,14 +95,15 @@ func (s Service) Search(ctx context.Context, in *searchsvc.SearchRequest, out *s } ctx = revactx.ContextSetUser(ctx, u) - key := cacheKey(in.Query, in.PageSize, in.Ref, u) + key := cacheKey(in.Query, in.PageSize, in.Ref, u, in.Aggregations) res, ok := s.FromCache(key) if !ok { var err error res, err = s.searcher.Search(ctx, &searchsvc.SearchRequest{ - Query: in.Query, - PageSize: in.PageSize, - Ref: in.Ref, + Query: in.Query, + PageSize: in.PageSize, + Ref: in.Ref, + Aggregations: in.Aggregations, }) if err != nil { switch err.(type) { @@ -118,6 +120,7 @@ func (s Service) Search(ctx context.Context, in *searchsvc.SearchRequest, out *s out.Matches = res.Matches out.TotalMatches = res.TotalMatches out.NextPageToken = res.NextPageToken + out.Aggregations = res.Aggregations return nil } @@ -242,6 +245,18 @@ func (s Service) Cache(key string, res *searchsvc.SearchResponse) { _ = s.cache.Set(key, res) } -func cacheKey(query string, pagesize int32, ref *v0.Reference, user *user.User) string { - return fmt.Sprintf("%s|%d|%s$%s!%s/%s|%s", query, pagesize, ref.GetResourceId().GetStorageId(), ref.GetResourceId().GetSpaceId(), ref.GetResourceId().GetOpaqueId(), ref.GetPath(), user.GetId().GetOpaqueId()) +// cacheKey builds the cache identity for a search. Every result-affecting field +// must be in the key, including aggregations (serialised via deterministic proto +// marshalling). If the aggregation proto ever gains a map field, determinism +// requires all writers to set Deterministic=true. +func cacheKey(query string, pagesize int32, ref *v0.Reference, user *user.User, aggs []*searchsvc.AggregationOption) string { + aggPart := "" + if len(aggs) > 0 { + b, _ := proto.MarshalOptions{Deterministic: true}.Marshal(&searchsvc.SearchRequest{Aggregations: aggs}) + aggPart = string(b) + } + return fmt.Sprintf("%s|%d|%s$%s!%s/%s|%s|%s", + query, pagesize, + ref.GetResourceId().GetStorageId(), ref.GetResourceId().GetSpaceId(), ref.GetResourceId().GetOpaqueId(), + ref.GetPath(), user.GetId().GetOpaqueId(), aggPart) }