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) }