diff --git a/protogen/gen/opencloud/services/search/v0/search.pb.go b/protogen/gen/opencloud/services/search/v0/search.pb.go index 6887541719..567d02e1de 100644 --- a/protogen/gen/opencloud/services/search/v0/search.pb.go +++ b/protogen/gen/opencloud/services/search/v0/search.pb.go @@ -24,6 +24,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 @@ -36,6 +91,8 @@ 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"` } func (x *SearchRequest) Reset() { @@ -98,6 +155,13 @@ func (x *SearchRequest) GetRef() *v0.Reference { return nil } +func (x *SearchRequest) GetAggregations() []*AggregationOption { + if x != nil { + return x.Aggregations + } + return nil +} + type SearchResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -108,6 +172,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() { @@ -163,6 +229,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 @@ -175,6 +248,8 @@ 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"` } func (x *SearchIndexRequest) Reset() { @@ -237,6 +312,13 @@ func (x *SearchIndexRequest) GetRef() *v0.Reference { return nil } +func (x *SearchIndexRequest) GetAggregations() []*AggregationOption { + if x != nil { + return x.Aggregations + } + return nil +} + type SearchIndexResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -247,6 +329,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() { @@ -302,6 +387,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 @@ -315,7 +799,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) } @@ -328,7 +812,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 { @@ -341,7 +825,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 { @@ -374,7 +858,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) } @@ -387,7 +871,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 { @@ -400,7 +884,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} } var File_opencloud_services_search_v0_search_proto protoreflect.FileDescriptor @@ -422,7 +906,7 @@ var file_opencloud_services_search_v0_search_proto_rawDesc = []byte{ 0x69, 0x2f, 0x61, 0x6e, 0x6e, 0x6f, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x1a, 0x20, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x5f, 0x6d, 0x61, 0x73, 0x6b, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0xae, 0x01, 0x0a, 0x0d, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, + 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x22, 0x89, 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, @@ -433,17 +917,28 @@ 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, + 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, 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, 0x8e, 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, @@ -454,79 +949,163 @@ var file_opencloud_services_search_v0_search_proto_rawDesc = []byte{ 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, - 0x6c, 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, 0x22, 0x14, 0x0a, - 0x12, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x32, 0xb1, 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, + 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, 0x96, - 0x01, 0x0a, 0x0a, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, 0x61, 0x63, 0x65, 0x12, 0x2f, 0x2e, + 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, 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, 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, 0x6c, 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, 0x22, 0x14, 0x0a, 0x12, 0x49, 0x6e, 0x64, 0x65, 0x78, 0x53, 0x70, + 0x61, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 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, 0xb1, 0x02, 0x0a, 0x0e, 0x53, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x50, 0x72, 0x6f, 0x76, 0x69, 0x64, 0x65, 0x72, 0x12, 0x85, 0x01, + 0x0a, 0x06, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x2b, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, + 0x6c, 0x6f, 0x75, 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, + 0x61, 0x72, 0x63, 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x6f, 0x70, 0x65, 0x6e, 0x63, 0x6c, 0x6f, 0x75, + 0x64, 0x2e, 0x73, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x73, 0x2e, 0x73, 0x65, 0x61, 0x72, 0x63, + 0x68, 0x2e, 0x76, 0x30, 0x2e, 0x53, 0x65, 0x61, 0x72, 0x63, 0x68, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x20, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x1a, 0x3a, 0x01, 0x2a, 0x22, 0x15, + 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x76, 0x30, 0x2f, 0x73, 0x65, 0x61, 0x72, 0x63, 0x68, 0x2f, 0x73, + 0x65, 0x61, 0x72, 0x63, 0x68, 0x12, 0x96, 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, 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, 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, + 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, 0x2d, 0x73, 0x70, 0x61, 0x63, 0x65, 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, + 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 ( @@ -541,33 +1120,51 @@ 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 + (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 } 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 - 0, // 4: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest - 4, // 5: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest - 2, // 6: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest - 1, // 7: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse - 5, // 8: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse - 3, // 9: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse - 7, // [7:10] is the sub-list for method output_type - 4, // [4:7] is the sub-list for method input_type - 4, // [4:4] is the sub-list for extension type_name - 4, // [4:4] is the sub-list for extension extendee - 0, // [0:4] 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 + 1, // 15: opencloud.services.search.v0.SearchProvider.Search:input_type -> opencloud.services.search.v0.SearchRequest + 10, // 16: opencloud.services.search.v0.SearchProvider.IndexSpace:input_type -> opencloud.services.search.v0.IndexSpaceRequest + 3, // 17: opencloud.services.search.v0.IndexProvider.Search:input_type -> opencloud.services.search.v0.SearchIndexRequest + 2, // 18: opencloud.services.search.v0.SearchProvider.Search:output_type -> opencloud.services.search.v0.SearchResponse + 11, // 19: opencloud.services.search.v0.SearchProvider.IndexSpace:output_type -> opencloud.services.search.v0.IndexSpaceResponse + 4, // 20: opencloud.services.search.v0.IndexProvider.Search:output_type -> opencloud.services.search.v0.SearchIndexResponse + 18, // [18:21] is the sub-list for method output_type + 15, // [15:18] is the sub-list for method input_type + 15, // [15:15] is the sub-list for extension type_name + 15, // [15:15] is the sub-list for extension extendee + 0, // [0:15] is the sub-list for field type_name } func init() { file_opencloud_services_search_v0_search_proto_init() } @@ -625,7 +1222,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: @@ -637,6 +1234,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 @@ -654,13 +1311,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.pb.web.go b/protogen/gen/opencloud/services/search/v0/search.pb.web.go index a6a5842fec..1a18063202 100644 --- a/protogen/gen/opencloud/services/search/v0/search.pb.web.go +++ b/protogen/gen/opencloud/services/search/v0/search.pb.web.go @@ -273,6 +273,186 @@ func (m *SearchIndexResponse) UnmarshalJSON(b []byte) error { var _ json.Unmarshaler = (*SearchIndexResponse)(nil) +// AggregationOptionJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of AggregationOption. This struct is safe to replace or modify but +// should not be done so concurrently. +var AggregationOptionJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *AggregationOption) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := AggregationOptionJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*AggregationOption)(nil) + +// AggregationOptionJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of AggregationOption. This struct is safe to replace or modify but +// should not be done so concurrently. +var AggregationOptionJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *AggregationOption) UnmarshalJSON(b []byte) error { + return AggregationOptionJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*AggregationOption)(nil) + +// BucketDefinitionJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of BucketDefinition. This struct is safe to replace or modify but +// should not be done so concurrently. +var BucketDefinitionJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *BucketDefinition) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := BucketDefinitionJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*BucketDefinition)(nil) + +// BucketDefinitionJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of BucketDefinition. This struct is safe to replace or modify but +// should not be done so concurrently. +var BucketDefinitionJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *BucketDefinition) UnmarshalJSON(b []byte) error { + return BucketDefinitionJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*BucketDefinition)(nil) + +// BucketRangeJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of BucketRange. This struct is safe to replace or modify but +// should not be done so concurrently. +var BucketRangeJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *BucketRange) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := BucketRangeJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*BucketRange)(nil) + +// BucketRangeJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of BucketRange. This struct is safe to replace or modify but +// should not be done so concurrently. +var BucketRangeJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *BucketRange) UnmarshalJSON(b []byte) error { + return BucketRangeJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*BucketRange)(nil) + +// AggregationResultJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of AggregationResult. This struct is safe to replace or modify but +// should not be done so concurrently. +var AggregationResultJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *AggregationResult) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := AggregationResultJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*AggregationResult)(nil) + +// AggregationResultJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of AggregationResult. This struct is safe to replace or modify but +// should not be done so concurrently. +var AggregationResultJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *AggregationResult) UnmarshalJSON(b []byte) error { + return AggregationResultJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*AggregationResult)(nil) + +// BucketJSONMarshaler describes the default jsonpb.Marshaler used by all +// instances of Bucket. This struct is safe to replace or modify but +// should not be done so concurrently. +var BucketJSONMarshaler = new(jsonpb.Marshaler) + +// MarshalJSON satisfies the encoding/json Marshaler interface. This method +// uses the more correct jsonpb package to correctly marshal the message. +func (m *Bucket) MarshalJSON() ([]byte, error) { + if m == nil { + return json.Marshal(nil) + } + + buf := &bytes.Buffer{} + + if err := BucketJSONMarshaler.Marshal(buf, m); err != nil { + return nil, err + } + + return buf.Bytes(), nil +} + +var _ json.Marshaler = (*Bucket)(nil) + +// BucketJSONUnmarshaler describes the default jsonpb.Unmarshaler used by all +// instances of Bucket. This struct is safe to replace or modify but +// should not be done so concurrently. +var BucketJSONUnmarshaler = new(jsonpb.Unmarshaler) + +// UnmarshalJSON satisfies the encoding/json Unmarshaler interface. This method +// uses the more correct jsonpb package to correctly unmarshal the message. +func (m *Bucket) UnmarshalJSON(b []byte) error { + return BucketJSONUnmarshaler.Unmarshal(bytes.NewReader(b), m) +} + +var _ json.Unmarshaler = (*Bucket)(nil) + // IndexSpaceRequestJSONMarshaler describes the default jsonpb.Marshaler used by all // instances of IndexSpaceRequest. This struct is safe to replace or modify but // should not be done so concurrently. diff --git a/protogen/gen/opencloud/services/search/v0/search.swagger.json b/protogen/gen/opencloud/services/search/v0/search.swagger.json index a8d7b9bd8f..97b8259e19 100644 --- a/protogen/gen/opencloud/services/search/v0/search.swagger.json +++ b/protogen/gen/opencloud/services/search/v0/search.swagger.json @@ -157,6 +157,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": { @@ -217,6 +278,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": { @@ -352,6 +470,17 @@ } } }, + "v0MetricKind": { + "type": "string", + "enum": [ + "METRIC_KIND_UNSPECIFIED", + "METRIC_KIND_SUM", + "METRIC_KIND_MIN", + "METRIC_KIND_MAX", + "METRIC_KIND_AVG" + ], + "default": "METRIC_KIND_UNSPECIFIED" + }, "v0Photo": { "type": "object", "properties": { @@ -433,6 +562,13 @@ }, "ref": { "$ref": "#/definitions/v0Reference" + }, + "aggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationOption" + }, + "description": "Optional. Per-space aggregations (facets) to compute alongside the matches." } } }, @@ -452,6 +588,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." } } }, @@ -472,6 +615,13 @@ }, "ref": { "$ref": "#/definitions/v0Reference" + }, + "aggregations": { + "type": "array", + "items": { + "$ref": "#/definitions/v0AggregationOption" + }, + "description": "Optional. Aggregations (facets) to compute alongside the matches." } } }, @@ -491,6 +641,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/services/search/v0/search.proto b/protogen/proto/opencloud/services/search/v0/search.proto index 25b085d660..b11433bb16 100644 --- a/protogen/proto/opencloud/services/search/v0/search.proto +++ b/protogen/proto/opencloud/services/search/v0/search.proto @@ -69,6 +69,9 @@ 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]; } message SearchResponse { @@ -78,6 +81,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 { @@ -90,6 +96,9 @@ 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]; } message SearchIndexResponse { @@ -99,6 +108,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/searchquery.go b/services/graph/pkg/service/v0/searchquery.go new file mode 100644 index 0000000000..3c957c771a --- /dev/null +++ b/services/graph/pkg/service/v0/searchquery.go @@ -0,0 +1,441 @@ +package svc + +import ( + "context" + "fmt" + "net/http" + "path" + "strings" + "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" + "github.com/opencloud-eu/opencloud/services/search/pkg/search" +) + +// 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 + } + + for _, sr := range req.Requests { + if err := validateAggregations(sr.Aggregations); err != nil { + errorcode.InvalidRequest.Render(w, r, http.StatusBadRequest, err.Error()) + return + } + } + + th := r.Header.Get(revaCtx.TokenHeader) + ctx := revaCtx.ContextSetToken(r.Context(), th) + ctx = metadata.Set(ctx, revaCtx.TokenHeader, th) + + responses := make([]libregraph.SearchResponse, 0, len(req.Requests)) + for _, sr := range req.Requests { + sresp, err := g.runSingleSearch(ctx, sr) + 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) (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: applyAggregationFilters(sr.Query.QueryString, sr.AggregationFilters), + PageSize: pageSize, + Aggregations: libregraphAggregationsToSearch(sr.Aggregations), + }) + if err != nil { + return libregraph.SearchResponse{}, err + } + + 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++ { + hits = append(hits, matchToSearchHit(rsp.Matches[i], int32(i+1))) + } + } + + 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), + }}, + }, 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 +} + +// validateAggregations rejects terms aggregations on numeric/time fields: bleve +// indexes them as prefix-coded binary, so term buckets are meaningless. Ranges +// are the supported alternative. Classification via search.IsNumericField. +func validateAggregations(aggs []libregraph.AggregationOption) error { + for _, a := range aggs { + if !search.IsNumericField(a.Field) { + continue + } + hasRanges := a.BucketDefinition != nil && len(a.BucketDefinition.Ranges) > 0 + if hasRanges { + continue + } + return fmt.Errorf("terms aggregation is not supported on numeric field %q; use bucketDefinition.ranges", a.Field) + } + return nil +} + +// applyAggregationFilters AND-combines the query string with prior-response KQL +// filter snippets like `audio.artist:"Pink Floyd"`. +func applyAggregationFilters(q string, filters []string) string { + if len(filters) == 0 { + return q + } + parts := make([]string, 0, len(filters)+1) + if q != "" { + parts = append(parts, "("+q+")") + } + for _, f := range filters { + if f == "" { + continue + } + parts = append(parts, "("+f+")") + } + return strings.Join(parts, " AND ") +} + +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.SubAggregations) > 0 { + agg.SubAggregations = libregraphAggregationsToSearch(a.SubAggregations) + } + if a.MetricKind != nil { + agg.MetricKind = metricKindFromLibregraph(*a.MetricKind) + } + 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) []libregraph.SearchAggregation { + if len(in) == 0 { + return nil + } + out := make([]libregraph.SearchAggregation, 0, len(in)) + for _, a := range in { + field := a.GetField() + // 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, + Value: &value, + MetricKind: metricKindToLibregraph(kind), + }) + continue + } + buckets := make([]libregraph.SearchBucket, 0, len(a.GetBuckets())) + for _, b := range a.GetBuckets() { + key := b.GetKey() + count := b.GetCount() + // aggregationFilterToken left empty until opaque token encoding is wired up. + lb := libregraph.SearchBucket{ + Key: &key, + Count: &count, + } + if subs := b.GetSubAggregations(); len(subs) > 0 { + lb.SubAggregations = searchAggregationsToLibregraph(subs) + } + 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) 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()) + 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) *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.Audio = searchAudioToLibregraph(e.GetAudio()) + di.Image = searchImageToLibregraph(e.GetImage()) + di.Photo = searchPhotoToLibregraph(e.GetPhoto()) + di.Location = searchLocationToLibregraph(e.GetLocation()) + return di +} + +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 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..aaf13389c0 --- /dev/null +++ b/services/graph/pkg/service/v0/searchquery_test.go @@ -0,0 +1,167 @@ +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" + 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.IndexSpaceResponse, 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 } + +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.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)), + ) + + ginkgo.It("rejects a terms aggregation on a numeric field with 400", func() { + g := graphWithSearch(stubSearchService{ + search: func(*searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + ginkgo.Fail("search service must not be called when validation fails") + return nil, nil + }, + }) + rr := postSearchQuery(g, `{ + "requests": [{ + "entityTypes": ["driveItem"], + "query": {"queryString": "mediatype:audio"}, + "size": 0, + "aggregations": [{"field": "audio.year"}] + }] + }`) + Expect(rr.Code).To(Equal(http.StatusBadRequest), rr.Body.String()) + }) + + ginkgo.It("allows a range aggregation on a numeric field", func() { + called := false + g := graphWithSearch(stubSearchService{ + search: func(*searchsvc.SearchRequest) (*searchsvc.SearchResponse, error) { + called = true + return &searchsvc.SearchResponse{}, nil + }, + }) + rr := postSearchQuery(g, `{ + "requests": [{ + "entityTypes": ["driveItem"], + "query": {"queryString": "mediatype:audio"}, + "size": 0, + "aggregations": [{ + "field": "audio.year", + "bucketDefinition": { + "sortBy": "keyAsString", + "ranges": [{"from": "1970", "to": "1980"}] + } + }] + }] + }`) + Expect(rr.Code).To(Equal(http.StatusOK), rr.Body.String()) + Expect(called).To(BeTrue()) + }) +}) diff --git a/services/graph/pkg/service/v0/service.go b/services/graph/pkg/service/v0/service.go index fb5e490851..c7ff881d65 100644 --- a/services/graph/pkg/service/v0/service.go +++ b/services/graph/pkg/service/v0/service.go @@ -124,6 +124,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. @@ -305,6 +307,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/search/pkg/bleve/backend.go b/services/search/pkg/bleve/backend.go index b4088918fe..fb8572b8b9 100644 --- a/services/search/pkg/bleve/backend.go +++ b/services/search/pkg/bleve/backend.go @@ -3,10 +3,12 @@ package bleve import ( "context" "math" + "strconv" "strings" "time" "github.com/blevesearch/bleve/v2" + bleveSearch "github.com/blevesearch/bleve/v2/search" "github.com/blevesearch/bleve/v2/search/query" storageProvider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" "github.com/opencloud-eu/reva/v2/pkg/errtypes" @@ -89,6 +91,16 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques bleveReq.Size = int(sir.PageSize) } + for _, agg := range sir.GetAggregations() { + bleveReq.AddFacet(agg.GetField(), newBleveFacetRequest(agg)) + } + + // Sub-aggregations need the matched hit set, not just count facets: widen + // the page so the emulator has enough docs. The caller's larger PageSize wins. + if needsSubAggScan(sir.GetAggregations()) && bleveReq.Size < subAggScanSize { + bleveReq.Size = subAggScanSize + } + bleveReq.Fields = []string{"*"} res, err := b.index.Search(bleveReq) if err != nil { @@ -154,9 +166,283 @@ func (b *Backend) Search(_ context.Context, sir *searchService.SearchIndexReques return &searchService.SearchIndexResponse{ Matches: matches, TotalMatches: int32(totalMatches), + Aggregations: extractBleveAggregations(res, sir.GetAggregations()), }, nil } +// subAggScanSize caps how many hits we walk when emulating sub-aggregations; +// math.MaxInt returns everything. +const subAggScanSize = math.MaxInt + +func needsSubAggScan(aggs []*searchService.AggregationOption) bool { + for _, agg := range aggs { + if len(agg.GetSubAggregations()) > 0 { + return true + } + } + return false +} + +// defaultFacetSize is used when no size is requested; the service layer trims +// after cross-space merge. +const defaultFacetSize = 1000 + +func newBleveFacetRequest(agg *searchService.AggregationOption) *bleve.FacetRequest { + size := int(agg.GetSize()) + if size <= 0 { + size = defaultFacetSize + } + fr := bleve.NewFacetRequest(agg.GetField(), size) + for _, r := range aggregationRanges(agg) { + minP := parseFloatPtr(r.GetFrom()) + maxP := parseFloatPtr(r.GetTo()) + fr.AddNumericRange(rangeBucketKey(r), minP, maxP) + } + return fr +} + +func extractBleveAggregations(res *bleve.SearchResult, aggs []*searchService.AggregationOption) []*searchService.AggregationResult { + if len(aggs) == 0 || len(res.Facets) == 0 { + return nil + } + out := make([]*searchService.AggregationResult, 0, len(aggs)) + for _, agg := range aggs { + fr, ok := res.Facets[agg.GetField()] + if !ok { + continue + } + buckets := make([]*searchService.Bucket, 0) + if len(aggregationRanges(agg)) > 0 { + for _, nr := range fr.NumericRanges { + buckets = append(buckets, &searchService.Bucket{ + Key: nr.Name, + Count: int64(nr.Count), + }) + } + } else { + for _, t := range fr.Terms.Terms() { + buckets = append(buckets, &searchService.Bucket{ + Key: t.Term, + Count: int64(t.Count), + }) + } + } + if subAggs := agg.GetSubAggregations(); len(subAggs) > 0 { + attachSubAggregations(res, agg.GetField(), subAggs, buckets) + } + out = append(out, &searchService.AggregationResult{ + Field: agg.GetField(), + Buckets: buckets, + }) + } + return out +} + +// subAcc is the recursive accumulator emulating composite aggregations: one +// node per sub-aggregation under a parent bucket. +type subAcc struct { + // terms: count + recursive accumulators per child value + termCount map[string]int64 + termSubs map[string][]*subAcc + + // metric + metricVal float64 // SUM/MIN/MAX + sum float64 // AVG transport: numerator + count int64 // AVG transport: denominator + + seen bool // at least one hit contributed +} + +// newSubAcc allocates an accumulator for the given sub-agg. +func newSubAcc(sa *searchService.AggregationOption) *subAcc { + a := &subAcc{} + if sa.GetMetricKind() == searchService.MetricKind_METRIC_KIND_UNSPECIFIED { + a.termCount = map[string]int64{} + if len(sa.GetSubAggregations()) > 0 { + a.termSubs = map[string][]*subAcc{} + } + } + return a +} + +// accumulateHit folds one hit into a sub-agg accumulator, recursing into +// grand-sub-aggregations. +func accumulateHit(a *subAcc, sa *searchService.AggregationOption, hit *bleveSearch.DocumentMatch) { + switch sa.GetMetricKind() { + case searchService.MetricKind_METRIC_KIND_UNSPECIFIED: + val, ok := hit.Fields[sa.GetField()].(string) + if !ok || val == "" { + return + } + a.termCount[val]++ + a.seen = true + if subs := sa.GetSubAggregations(); len(subs) > 0 { + childAccs, ok := a.termSubs[val] + if !ok { + childAccs = make([]*subAcc, len(subs)) + for i, ssa := range subs { + childAccs[i] = newSubAcc(ssa) + } + a.termSubs[val] = childAccs + } + for i, ssa := range subs { + accumulateHit(childAccs[i], ssa, hit) + } + } + default: + v, ok := numericFieldValue(hit.Fields[sa.GetField()]) + if !ok { + return + } + switch sa.GetMetricKind() { + case searchService.MetricKind_METRIC_KIND_SUM: + a.metricVal += v + case searchService.MetricKind_METRIC_KIND_MIN: + if !a.seen || v < a.metricVal { + a.metricVal = v + } + case searchService.MetricKind_METRIC_KIND_MAX: + if !a.seen || v > a.metricVal { + a.metricVal = v + } + case searchService.MetricKind_METRIC_KIND_AVG: + a.sum += v + a.count++ + } + a.seen = true + } +} + +// emitAcc materialises a sub-agg accumulator into the proto result. +func emitAcc(a *subAcc, sa *searchService.AggregationOption) *searchService.AggregationResult { + if sa.GetMetricKind() != searchService.MetricKind_METRIC_KIND_UNSPECIFIED { + if !a.seen { + return nil + } + r := &searchService.AggregationResult{ + Field: sa.GetField(), + MetricKind: sa.GetMetricKind(), + } + if sa.GetMetricKind() == searchService.MetricKind_METRIC_KIND_AVG { + r.Sum = a.sum + r.Count = a.count + } else { + r.Value = a.metricVal + } + return r + } + + subs := sa.GetSubAggregations() + childBuckets := make([]*searchService.Bucket, 0, len(a.termCount)) + for term, count := range a.termCount { + b := &searchService.Bucket{Key: term, Count: count} + if len(subs) > 0 { + if childAccs, ok := a.termSubs[term]; ok { + for i, ssa := range subs { + if sub := emitAcc(childAccs[i], ssa); sub != nil { + b.SubAggregations = append(b.SubAggregations, sub) + } + } + } + } + childBuckets = append(childBuckets, b) + } + if sz := int(sa.GetSize()); sz > 0 && len(childBuckets) > sz { + childBuckets = childBuckets[:sz] + } + return &searchService.AggregationResult{ + Field: sa.GetField(), + Buckets: childBuckets, + } +} + +// attachSubAggregations folds the matched hits into nested aggregation results +// per parent bucket, via a single hit walk dispatched through the accumulator tree. +func attachSubAggregations(res *bleve.SearchResult, parentField string, subAggs []*searchService.AggregationOption, buckets []*searchService.Bucket) { + bucketByKey := make(map[string]*searchService.Bucket, len(buckets)) + for _, b := range buckets { + bucketByKey[b.GetKey()] = b + } + + perParent := make(map[string][]*subAcc, len(buckets)) + for _, b := range buckets { + accs := make([]*subAcc, len(subAggs)) + for i, sa := range subAggs { + accs[i] = newSubAcc(sa) + } + perParent[b.GetKey()] = accs + } + + for _, hit := range res.Hits { + parentVal, ok := hit.Fields[parentField].(string) + if !ok || parentVal == "" { + continue + } + accs, ok := perParent[parentVal] + if !ok { + continue + } + for i, sa := range subAggs { + accumulateHit(accs[i], sa, hit) + } + } + + for key, accs := range perParent { + b := bucketByKey[key] + for i, sa := range subAggs { + if r := emitAcc(accs[i], sa); r != nil { + b.SubAggregations = append(b.SubAggregations, r) + } + } + } +} + +// numericFieldValue coerces a bleve stored value to float64 (also accepts +// string forms). +func numericFieldValue(raw interface{}) (float64, bool) { + switch v := raw.(type) { + case float64: + return v, true + case int64: + return float64(v), true + case int32: + return float64(v), true + case string: + f, err := strconv.ParseFloat(v, 64) + if err != nil { + return 0, false + } + return f, true + default: + return 0, false + } +} + +func aggregationRanges(agg *searchService.AggregationOption) []*searchService.BucketRange { + bd := agg.GetBucketDefinition() + if bd == nil { + return nil + } + return bd.GetRanges() +} + +// rangeBucketKey formats a range as "from-to" for stable merge keys; open sides +// render as "-N" or "N-". +func rangeBucketKey(r *searchService.BucketRange) string { + return r.GetFrom() + "-" + r.GetTo() +} + +func parseFloatPtr(s string) *float64 { + if s == "" { + return nil + } + v, err := strconv.ParseFloat(s, 64) + if err != nil { + return nil + } + return &v +} + func (b *Backend) DocCount() (uint64, error) { return b.index.DocCount() } diff --git a/services/search/pkg/bleve/backend_test.go b/services/search/pkg/bleve/backend_test.go index 8ecfa4bbbd..00abab9b47 100644 --- a/services/search/pkg/bleve/backend_test.go +++ b/services/search/pkg/bleve/backend_test.go @@ -3,8 +3,10 @@ package bleve_test import ( "context" "fmt" + "os" bleveSearch "github.com/blevesearch/bleve/v2" + "github.com/blevesearch/bleve/v2/index/scorch" sprovider "github.com/cs3org/go-cs3apis/cs3/storage/provider/v1beta1" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -62,7 +64,10 @@ var _ = Describe("Bleve", func() { mapping, err := bleve.NewMapping() Expect(err).ToNot(HaveOccurred()) - idx, err = bleveSearch.NewMemOnly(mapping) + tmpDir, err := os.MkdirTemp("", "bleve-test-") + Expect(err).ToNot(HaveOccurred()) + DeferCleanup(os.RemoveAll, tmpDir) + idx, err = bleveSearch.NewUsing(tmpDir, mapping, scorch.Name, bleveSearch.Config.DefaultKVStore, nil) Expect(err).ToNot(HaveOccurred()) eng = bleve.NewBackend(idx, bleveQuery.DefaultCreator, log.Logger{}) @@ -707,4 +712,161 @@ var _ = Describe("Bleve", func() { }) }) }) + + Describe("Aggregations", func() { + upsertAudio := func(id, name, artist, album, title string) { + r := search.Resource{ + ID: id, + ParentID: rootResource.ID, + RootID: rootResource.ID, + Path: "./" + name, + Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), + Document: content.Document{ + Name: name, + MimeType: "audio/mpeg", + Audio: &libregraph.Audio{ + Artist: libregraph.PtrString(artist), + Album: libregraph.PtrString(album), + Title: libregraph.PtrString(title), + }, + }, + } + Expect(eng.Upsert(r.ID, r)).To(Succeed()) + } + + searchWithAggs := func(query string, aggs ...*searchsvc.AggregationOption) *searchsvc.SearchIndexResponse { + rID, err := storagespace.ParseID(rootResource.ID) + Expect(err).ToNot(HaveOccurred()) + res, err := eng.Search(context.Background(), &searchsvc.SearchIndexRequest{ + Query: query, + Ref: &searchmsg.Reference{ + ResourceId: &searchmsg.ResourceID{ + StorageId: rID.StorageId, + SpaceId: rID.SpaceId, + OpaqueId: rID.OpaqueId, + }, + }, + Aggregations: aggs, + }) + Expect(err).ToNot(HaveOccurred()) + return res + } + + BeforeEach(func() { + Expect(eng.Upsert(rootResource.ID, rootResource)).To(Succeed()) + upsertAudio("1$2!1001", "a.mp3", "Pink Floyd", "The Wall", "Brick") + upsertAudio("1$2!1002", "b.mp3", "Pink Floyd", "The Wall", "Comfortably Numb") + upsertAudio("1$2!1003", "c.mp3", "Motörhead", "Bomber", "Bomber") + upsertAudio("1$2!1004", "d.mp3", "Motörhead", "Bomber", "Stone Dead Forever") + upsertAudio("1$2!1005", "e.mp3", "Motörhead", "Ace of Spades", "Ace of Spades") + }) + + It("returns term buckets on audio.artist", func() { + res := searchWithAggs("mediatype:audio", &searchsvc.AggregationOption{ + Field: "audio.artist", + Size: 10, + }) + Expect(res.Aggregations).To(HaveLen(1)) + agg := res.Aggregations[0] + Expect(agg.Field).To(Equal("audio.artist")) + + counts := map[string]int64{} + for _, b := range agg.Buckets { + counts[b.Key] = b.Count + } + Expect(counts).To(HaveKeyWithValue("Pink Floyd", int64(2))) + Expect(counts).To(HaveKeyWithValue("Motörhead", int64(3))) + }) + + It("returns nil aggregations when none are requested", func() { + res := searchWithAggs("mediatype:audio") + Expect(res.Aggregations).To(BeNil()) + }) + + It("handles multiple aggregations in one request", func() { + res := searchWithAggs("mediatype:audio", + &searchsvc.AggregationOption{Field: "audio.artist"}, + &searchsvc.AggregationOption{Field: "audio.album"}, + ) + fields := []string{} + for _, a := range res.Aggregations { + fields = append(fields, a.Field) + } + Expect(fields).To(ConsistOf("audio.artist", "audio.album")) + }) + + Describe("numeric range aggregations", func() { + upsertWithYear := func(id, name string, year int32) { + r := search.Resource{ + ID: id, + ParentID: rootResource.ID, + RootID: rootResource.ID, + Path: "./" + name, + Type: uint64(sprovider.ResourceType_RESOURCE_TYPE_FILE), + Document: content.Document{ + Name: name, + MimeType: "audio/mpeg", + Audio: &libregraph.Audio{Year: libregraph.PtrInt32(year)}, + }, + } + Expect(eng.Upsert(r.ID, r)).To(Succeed()) + } + + BeforeEach(func() { + upsertWithYear("1$2!2001", "70a.mp3", 1971) + upsertWithYear("1$2!2002", "70b.mp3", 1975) + upsertWithYear("1$2!2003", "80a.mp3", 1982) + upsertWithYear("1$2!2004", "90a.mp3", 1999) + upsertWithYear("1$2!2005", "00a.mp3", 2001) + upsertWithYear("1$2!2006", "00b.mp3", 2005) + upsertWithYear("1$2!2007", "00c.mp3", 2009) + }) + + It("returns buckets per decade range", func() { + res := searchWithAggs("mediatype:audio", + &searchsvc.AggregationOption{ + Field: "audio.year", + BucketDefinition: &searchsvc.BucketDefinition{ + Ranges: []*searchsvc.BucketRange{ + {From: "1970", To: "1980"}, + {From: "1980", To: "1990"}, + {From: "1990", To: "2000"}, + {From: "2000", To: "2010"}, + }, + }, + }, + ) + Expect(res.Aggregations).To(HaveLen(1)) + counts := map[string]int64{} + for _, b := range res.Aggregations[0].Buckets { + counts[b.Key] = b.Count + } + Expect(counts).To(HaveKeyWithValue("1970-1980", int64(2))) + Expect(counts).To(HaveKeyWithValue("1980-1990", int64(1))) + Expect(counts).To(HaveKeyWithValue("1990-2000", int64(1))) + Expect(counts).To(HaveKeyWithValue("2000-2010", int64(3))) + }) + + It("supports open-ended ranges", func() { + res := searchWithAggs("mediatype:audio", + &searchsvc.AggregationOption{ + Field: "audio.year", + BucketDefinition: &searchsvc.BucketDefinition{ + Ranges: []*searchsvc.BucketRange{ + {To: "1990"}, + {From: "2000"}, + }, + }, + }, + ) + Expect(res.Aggregations).To(HaveLen(1)) + counts := map[string]int64{} + for _, b := range res.Aggregations[0].Buckets { + counts[b.Key] = b.Count + } + Expect(counts).To(HaveKeyWithValue("-1990", int64(3))) + Expect(counts).To(HaveKeyWithValue("2000-", int64(3))) + }) + }) + }) }) diff --git a/services/search/pkg/opensearch/backend.go b/services/search/pkg/opensearch/backend.go index a7f28bbdfa..59bc4f6319 100644 --- a/services/search/pkg/opensearch/backend.go +++ b/services/search/pkg/opensearch/backend.go @@ -16,6 +16,7 @@ import ( "github.com/opencloud-eu/opencloud/pkg/log" searchMessage "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" searchService "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" + "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/aggs" "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/convert" "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/osu" "github.com/opencloud-eu/opencloud/services/search/pkg/search" @@ -124,6 +125,7 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ }, }, }, + Aggs: aggs.Build(sir.GetAggregations()), }, ) if err != nil { @@ -157,9 +159,15 @@ func (b *Backend) Search(ctx context.Context, sir *searchService.SearchIndexRequ matches = append(matches, match) } + aggResults, err := aggs.Parse(resp.Aggregations, sir.GetAggregations()) + if err != nil { + return nil, fmt.Errorf("failed to parse aggregations: %w", err) + } + return &searchService.SearchIndexResponse{ Matches: matches, TotalMatches: int32(totalMatches), + Aggregations: aggResults, }, nil } diff --git a/services/search/pkg/opensearch/internal/aggs/aggs.go b/services/search/pkg/opensearch/internal/aggs/aggs.go new file mode 100644 index 0000000000..5c3a8f88f3 --- /dev/null +++ b/services/search/pkg/opensearch/internal/aggs/aggs.go @@ -0,0 +1,258 @@ +// Package aggs translates proto aggregation options into the OpenSearch +// aggregation DSL and parses the response. Internal subpackage so its unit +// tests skip the parent package's Docker OpenSearch container. +package aggs + +import ( + "encoding/json" + "fmt" + "strconv" + + searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" +) + +// DefaultFacetSize matches the bleve backend: pull a generous bucket count per +// space, the service layer trims to top N after cross-space merge. +const DefaultFacetSize = 1000 + +// Build translates AggregationOptions into the OpenSearch aggregation DSL +// (terms, range, metric, nested). Entries get an index-derived name so repeated +// aggs on one field don't collide. +func Build(opts []*searchsvc.AggregationOption) map[string]any { + return buildLevel(opts, "a") +} + +func buildLevel(opts []*searchsvc.AggregationOption, prefix string) map[string]any { + if len(opts) == 0 { + return nil + } + aggs := map[string]any{} + for i, opt := range opts { + name := fmt.Sprintf("%s_%d", prefix, i) + if entry := buildOne(opt, name); entry != nil { + aggs[name] = entry + } + } + if len(aggs) == 0 { + return nil + } + return aggs +} + +func buildOne(opt *searchsvc.AggregationOption, name string) map[string]any { + field := opt.GetField() + if mk := opt.GetMetricKind(); mk != searchsvc.MetricKind_METRIC_KIND_UNSPECIFIED { + return buildMetric(field, mk) + } + var entry map[string]any + if ranges := rangesOf(opt); len(ranges) > 0 { + entry = map[string]any{ + "range": map[string]any{ + "field": field, + "ranges": buildRanges(ranges), + }, + } + } else { + size := int(opt.GetSize()) + if size <= 0 { + size = DefaultFacetSize + } + entry = map[string]any{ + "terms": map[string]any{ + "field": field, + "size": size, + }, + } + } + if subs := opt.GetSubAggregations(); len(subs) > 0 { + if nested := buildLevel(subs, name); nested != nil { + entry["aggs"] = nested + } + } + return entry +} + +// buildMetric emits the sum/min/max metric. AVG uses a stats agg to transport +// (sum, count) for the cross-space merge; the service layer collapses to the average. +func buildMetric(field string, kind searchsvc.MetricKind) map[string]any { + switch kind { + case searchsvc.MetricKind_METRIC_KIND_SUM: + return map[string]any{"sum": map[string]any{"field": field}} + case searchsvc.MetricKind_METRIC_KIND_MIN: + return map[string]any{"min": map[string]any{"field": field}} + case searchsvc.MetricKind_METRIC_KIND_MAX: + return map[string]any{"max": map[string]any{"field": field}} + case searchsvc.MetricKind_METRIC_KIND_AVG: + return map[string]any{"stats": map[string]any{"field": field}} + } + return nil +} + +func rangesOf(opt *searchsvc.AggregationOption) []*searchsvc.BucketRange { + bd := opt.GetBucketDefinition() + if bd == nil { + return nil + } + return bd.GetRanges() +} + +func buildRanges(ranges []*searchsvc.BucketRange) []map[string]any { + out := make([]map[string]any, 0, len(ranges)) + for _, r := range ranges { + entry := map[string]any{ + "key": RangeKey(r), + } + if v, err := strconv.ParseFloat(r.GetFrom(), 64); err == nil { + entry["from"] = v + } + if v, err := strconv.ParseFloat(r.GetTo(), 64); err == nil { + entry["to"] = v + } + out = append(out, entry) + } + return out +} + +// RangeKey mirrors the bleve backend so cross-space merging keys match. +func RangeKey(r *searchsvc.BucketRange) string { + return r.GetFrom() + "-" + r.GetTo() +} + +// Parse converts the response aggregations block into proto results, preserving +// request order and recursing into sub-aggregations. Empty input yields +// (nil, nil); invalid JSON yields an error. +func Parse(raw json.RawMessage, opts []*searchsvc.AggregationOption) ([]*searchsvc.AggregationResult, error) { + if len(raw) == 0 || len(opts) == 0 { + return nil, nil + } + node, err := parseNode(raw) + if err != nil { + return nil, err + } + return parseLevel(node, opts, "a"), nil +} + +// aggNode is a lazily-decoded cursor over one level of the aggs response. +type aggNode map[string]json.RawMessage + +func parseNode(raw json.RawMessage) (aggNode, error) { + var m aggNode + if err := json.Unmarshal(raw, &m); err != nil { + return nil, fmt.Errorf("decode opensearch aggregations: %w", err) + } + return m, nil +} + +func parseLevel(node aggNode, opts []*searchsvc.AggregationOption, prefix string) []*searchsvc.AggregationResult { + out := make([]*searchsvc.AggregationResult, 0, len(opts)) + for i, opt := range opts { + name := fmt.Sprintf("%s_%d", prefix, i) + raw, ok := node[name] + if !ok { + continue + } + if res := parseOne(raw, opt, name); res != nil { + out = append(out, res) + } + } + return out +} + +func parseOne(raw json.RawMessage, opt *searchsvc.AggregationOption, name string) *searchsvc.AggregationResult { + field := opt.GetField() + if mk := opt.GetMetricKind(); mk != searchsvc.MetricKind_METRIC_KIND_UNSPECIFIED { + return parseMetric(raw, field, mk) + } + var body struct { + Buckets []json.RawMessage `json:"buckets"` + } + if err := json.Unmarshal(raw, &body); err != nil { + return nil + } + buckets := make([]*searchsvc.Bucket, 0, len(body.Buckets)) + for _, b := range body.Buckets { + if bucket := parseBucket(b, opt.GetSubAggregations(), name); bucket != nil { + buckets = append(buckets, bucket) + } + } + return &searchsvc.AggregationResult{ + Field: field, + Buckets: buckets, + } +} + +func parseBucket(raw json.RawMessage, subs []*searchsvc.AggregationOption, prefix string) *searchsvc.Bucket { + var head struct { + Key any `json:"key"` + DocCount int64 `json:"doc_count"` + } + if err := json.Unmarshal(raw, &head); err != nil { + return nil + } + b := &searchsvc.Bucket{ + Key: bucketKeyToString(head.Key), + Count: head.DocCount, + } + if len(subs) > 0 { + node, err := parseNode(raw) + if err == nil { + b.SubAggregations = parseLevel(node, subs, prefix) + } + } + return b +} + +func parseMetric(raw json.RawMessage, field string, kind searchsvc.MetricKind) *searchsvc.AggregationResult { + switch kind { + case searchsvc.MetricKind_METRIC_KIND_SUM, + searchsvc.MetricKind_METRIC_KIND_MIN, + searchsvc.MetricKind_METRIC_KIND_MAX: + var body struct { + Value *float64 `json:"value"` + } + if err := json.Unmarshal(raw, &body); err != nil { + return nil + } + res := &searchsvc.AggregationResult{ + Field: field, + MetricKind: kind, + } + if body.Value != nil { + res.Value = *body.Value + } + return res + case searchsvc.MetricKind_METRIC_KIND_AVG: + var body struct { + Sum float64 `json:"sum"` + Count int64 `json:"count"` + } + if err := json.Unmarshal(raw, &body); err != nil { + return nil + } + return &searchsvc.AggregationResult{ + Field: field, + Sum: body.Sum, + Count: body.Count, + MetricKind: kind, + } + } + return nil +} + +// bucketKeyToString normalises a response key to a string (terms are strings, +// ranges use our "from-to" key, numeric terms come back as JSON numbers). +func bucketKeyToString(v any) string { + switch x := v.(type) { + case string: + return x + case float64: + // format without trailing zeros so keys match filter values + return strconv.FormatFloat(x, 'f', -1, 64) + case bool: + return strconv.FormatBool(x) + case nil: + return "" + default: + return "" + } +} diff --git a/services/search/pkg/opensearch/internal/aggs/aggs_suite_test.go b/services/search/pkg/opensearch/internal/aggs/aggs_suite_test.go new file mode 100644 index 0000000000..11337ad5d3 --- /dev/null +++ b/services/search/pkg/opensearch/internal/aggs/aggs_suite_test.go @@ -0,0 +1,13 @@ +package aggs_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestAggs(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Aggs Suite") +} diff --git a/services/search/pkg/opensearch/internal/aggs/aggs_test.go b/services/search/pkg/opensearch/internal/aggs/aggs_test.go new file mode 100644 index 0000000000..a1f080411d --- /dev/null +++ b/services/search/pkg/opensearch/internal/aggs/aggs_test.go @@ -0,0 +1,227 @@ +package aggs_test + +import ( + "encoding/json" + "fmt" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + searchsvc "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/services/search/v0" + "github.com/opencloud-eu/opencloud/services/search/pkg/opensearch/internal/aggs" +) + +var _ = Describe("Build", func() { + It("builds a terms aggregation", func() { + res := aggs.Build([]*searchsvc.AggregationOption{ + {Field: "audio.artist", Size: 10}, + }) + Expect(res).ToNot(BeNil()) + entry, ok := res["a_0"].(map[string]any) + Expect(ok).To(BeTrue()) + terms, ok := entry["terms"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(terms["field"]).To(Equal("audio.artist")) + Expect(terms["size"]).To(Equal(10)) + }) + + It("builds a range aggregation with open-ended bounds", func() { + res := aggs.Build([]*searchsvc.AggregationOption{{ + Field: "audio.year", + BucketDefinition: &searchsvc.BucketDefinition{ + Ranges: []*searchsvc.BucketRange{ + {From: "1970", To: "1980"}, + {To: "1970"}, + {From: "2020"}, + }, + }, + }}) + r := res["a_0"].(map[string]any)["range"].(map[string]any) + Expect(r["field"]).To(Equal("audio.year")) + ranges := r["ranges"].([]map[string]any) + Expect(ranges).To(HaveLen(3)) + Expect(ranges[0]).To(SatisfyAll( + HaveKeyWithValue("key", "1970-1980"), + HaveKeyWithValue("from", 1970.0), + HaveKeyWithValue("to", 1980.0), + )) + Expect(ranges[1]).ToNot(HaveKey("from")) // open lower bound + Expect(ranges[2]).ToNot(HaveKey("to")) // open upper bound + }) + + DescribeTable("builds single-value metric aggregations", + func(kind searchsvc.MetricKind, esKind string) { + res := aggs.Build([]*searchsvc.AggregationOption{ + {Field: "audio.duration", MetricKind: kind}, + }) + body, ok := res["a_0"].(map[string]any)[esKind].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(body["field"]).To(Equal("audio.duration")) + }, + Entry("sum", searchsvc.MetricKind_METRIC_KIND_SUM, "sum"), + Entry("min", searchsvc.MetricKind_METRIC_KIND_MIN, "min"), + Entry("max", searchsvc.MetricKind_METRIC_KIND_MAX, "max"), + ) + + It("uses a stats aggregation for AVG", func() { + res := aggs.Build([]*searchsvc.AggregationOption{ + {Field: "audio.duration", MetricKind: searchsvc.MetricKind_METRIC_KIND_AVG}, + }) + stats, ok := res["a_0"].(map[string]any)["stats"].(map[string]any) + Expect(ok).To(BeTrue()) + Expect(stats["field"]).To(Equal("audio.duration")) + }) + + It("nests sub-aggregations under their parent bucket", func() { + res := aggs.Build([]*searchsvc.AggregationOption{{ + Field: "audio.artist", Size: 5, + SubAggregations: []*searchsvc.AggregationOption{{ + Field: "audio.album", Size: 7, + SubAggregations: []*searchsvc.AggregationOption{ + {Field: "audio.duration", MetricKind: searchsvc.MetricKind_METRIC_KIND_SUM}, + }, + }}, + }}) + album := res["a_0"].(map[string]any)["aggs"].(map[string]any)["a_0_0"].(map[string]any) + albumTerms := album["terms"].(map[string]any) + Expect(albumTerms["field"]).To(Equal("audio.album")) + Expect(albumTerms["size"]).To(Equal(7)) + metric := album["aggs"].(map[string]any)["a_0_0_0"].(map[string]any) + Expect(metric["sum"].(map[string]any)["field"]).To(Equal("audio.duration")) + }) +}) + +var _ = Describe("Parse", func() { + It("parses flat term and range buckets, stringifying numeric keys", func() { + raw := json.RawMessage(`{ + "a_0": {"buckets": [ + {"key": "Pink Floyd", "doc_count": 42}, + {"key": "Motörhead", "doc_count": 35} + ]}, + "a_1": {"buckets": [ + {"key": "1970-1980", "from": 1970.0, "to": 1980.0, "doc_count": 12} + ]}, + "a_2": {"buckets": [ + {"key": 9, "doc_count": 3} + ]} + }`) + out, err := aggs.Parse(raw, []*searchsvc.AggregationOption{ + {Field: "audio.artist"}, + {Field: "audio.year"}, + {Field: "audio.track"}, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(HaveLen(3)) + + Expect(out[0].Field).To(Equal("audio.artist")) + Expect(out[0].Buckets).To(HaveLen(2)) + Expect(out[0].Buckets[0].Key).To(Equal("Pink Floyd")) + Expect(out[0].Buckets[0].Count).To(Equal(int64(42))) + + Expect(out[1].Buckets[0].Key).To(Equal("1970-1980")) + Expect(out[1].Buckets[0].Count).To(Equal(int64(12))) + + // numeric term key stringified without trailing zeros + Expect(out[2].Buckets[0].Key).To(Equal("9")) + }) + + It("parses nested buckets carrying a metric", func() { + raw := json.RawMessage(`{ + "a_0": {"buckets": [{ + "key": "Iron Maiden", "doc_count": 300, + "a_0_0": {"buckets": [ + {"key": "The Number of the Beast", "doc_count": 8, "a_0_0_0": {"value": 2756000.0}}, + {"key": "Powerslave", "doc_count": 8, "a_0_0_0": {"value": 3061000.0}} + ]} + }]} + }`) + out, err := aggs.Parse(raw, []*searchsvc.AggregationOption{{ + Field: "audio.artist", + SubAggregations: []*searchsvc.AggregationOption{{ + Field: "audio.album", + SubAggregations: []*searchsvc.AggregationOption{ + {Field: "audio.duration", MetricKind: searchsvc.MetricKind_METRIC_KIND_SUM}, + }, + }}, + }}) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(HaveLen(1)) + Expect(out[0].Field).To(Equal("audio.artist")) + Expect(out[0].Buckets).To(HaveLen(1)) + + artistBucket := out[0].Buckets[0] + Expect(artistBucket.Key).To(Equal("Iron Maiden")) + Expect(artistBucket.Count).To(Equal(int64(300))) + Expect(artistBucket.SubAggregations).To(HaveLen(1)) + + albumAgg := artistBucket.SubAggregations[0] + Expect(albumAgg.Field).To(Equal("audio.album")) + Expect(albumAgg.Buckets).To(HaveLen(2)) + + nob := albumAgg.Buckets[0] + Expect(nob.Key).To(Equal("The Number of the Beast")) + Expect(nob.Count).To(Equal(int64(8))) + Expect(nob.SubAggregations).To(HaveLen(1)) + + metric := nob.SubAggregations[0] + Expect(metric.Field).To(Equal("audio.duration")) + Expect(metric.MetricKind).To(Equal(searchsvc.MetricKind_METRIC_KIND_SUM)) + Expect(metric.Value).To(Equal(2756000.0)) + }) + + DescribeTable("parses single-value metrics", + func(kind searchsvc.MetricKind, value float64) { + raw := json.RawMessage(fmt.Sprintf(`{"a_0": {"value": %g}}`, value)) + out, err := aggs.Parse(raw, []*searchsvc.AggregationOption{ + {Field: "audio.duration", MetricKind: kind}, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(HaveLen(1)) + Expect(out[0].MetricKind).To(Equal(kind)) + Expect(out[0].Value).To(Equal(value)) + }, + Entry("sum", searchsvc.MetricKind_METRIC_KIND_SUM, 1234.5), + Entry("min", searchsvc.MetricKind_METRIC_KIND_MIN, 10.0), + Entry("max", searchsvc.MetricKind_METRIC_KIND_MAX, 99.0), + ) + + It("decodes a null metric value to zero", func() { + // OpenSearch returns value: null when a metric has no matching docs. + out, err := aggs.Parse(json.RawMessage(`{"a_0": {"value": null}}`), + []*searchsvc.AggregationOption{{Field: "audio.duration", MetricKind: searchsvc.MetricKind_METRIC_KIND_SUM}}) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(HaveLen(1)) + Expect(out[0].Value).To(BeZero()) + Expect(out[0].MetricKind).To(Equal(searchsvc.MetricKind_METRIC_KIND_SUM)) + }) + + It("carries avg transport (sum + count) from a stats response", func() { + raw := json.RawMessage(`{ + "a_0": {"count": 100, "min": 30000.0, "max": 500000.0, "avg": 245000.0, "sum": 24500000.0} + }`) + out, err := aggs.Parse(raw, []*searchsvc.AggregationOption{ + {Field: "audio.duration", MetricKind: searchsvc.MetricKind_METRIC_KIND_AVG}, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(out).To(HaveLen(1)) + Expect(out[0].MetricKind).To(Equal(searchsvc.MetricKind_METRIC_KIND_AVG)) + Expect(out[0].Sum).To(Equal(24500000.0)) + Expect(out[0].Count).To(Equal(int64(100))) + }) + + It("returns nil for empty raw or empty options", func() { + got, err := aggs.Parse(nil, []*searchsvc.AggregationOption{{Field: "x"}}) + Expect(err).ToNot(HaveOccurred()) + Expect(got).To(BeNil()) + + got, err = aggs.Parse(json.RawMessage(`{}`), nil) + Expect(err).ToNot(HaveOccurred()) + Expect(got).To(BeNil()) + }) + + It("errors on malformed json and returns no result", func() { + got, err := aggs.Parse(json.RawMessage(`not-json`), []*searchsvc.AggregationOption{{Field: "x"}}) + Expect(err).To(HaveOccurred()) + Expect(got).To(BeNil()) + }) +}) diff --git a/services/search/pkg/opensearch/internal/osu/request.go b/services/search/pkg/opensearch/internal/osu/request.go index 927da7ac54..f6f4e238ee 100644 --- a/services/search/pkg/opensearch/internal/osu/request.go +++ b/services/search/pkg/opensearch/internal/osu/request.go @@ -95,6 +95,7 @@ func BuildSearchReq(req *opensearchgoAPI.SearchReq, q Builder, p ...SearchBodyPa type SearchBodyParams struct { Highlight *BodyParamHighlight `json:"highlight,omitempty"` + Aggs map[string]any `json:"aggs,omitempty"` } //----------------------------------------------------------------------------// diff --git a/services/search/pkg/search/fields.go b/services/search/pkg/search/fields.go new file mode 100644 index 0000000000..adb81bc679 --- /dev/null +++ b/services/search/pkg/search/fields.go @@ -0,0 +1,71 @@ +package search + +import ( + "reflect" + "strings" + "time" +) + +// IsNumericField reports whether the indexed field at the dotted path holds a +// numeric or time value. Terms aggregations on those are rejected: bleve stores +// them as prefix-coded binary, so term buckets are meaningless. The set is built +// by walking the Resource type, so new facet fields are picked up automatically. +func IsNumericField(dottedPath string) bool { + return numericFields[dottedPath] +} + +var numericFields = buildNumericFieldSet() + +var timeType = reflect.TypeOf(time.Time{}) + +func buildNumericFieldSet() map[string]bool { + out := map[string]bool{} + walkStruct(out, "", reflect.TypeOf(Resource{})) + return out +} + +func walkStruct(out map[string]bool, prefix string, t reflect.Type) { + for t.Kind() == reflect.Ptr { + t = t.Elem() + } + if t.Kind() != reflect.Struct { + return + } + for i := 0; i < t.NumField(); i++ { + f := t.Field(i) + if !f.IsExported() { + continue + } + if f.Anonymous { + // embedded: promote fields into the current prefix, like encoding/json. + walkStruct(out, prefix, f.Type) + continue + } + path := prefix + jsonFieldName(f) + ft := f.Type + for ft.Kind() == reflect.Ptr { + ft = ft.Elem() + } + switch ft.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, + reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, + reflect.Float32, reflect.Float64: + out[path] = true + case reflect.Struct: + if ft == timeType { + // time.Time round-trips as RFC3339; treat as numeric. + out[path] = true + continue + } + walkStruct(out, path+".", ft) + } + } +} + +func jsonFieldName(f reflect.StructField) string { + tag := f.Tag.Get("json") + if tag == "" { + return f.Name + } + return strings.Split(tag, ",")[0] +} diff --git a/services/search/pkg/search/fields_test.go b/services/search/pkg/search/fields_test.go new file mode 100644 index 0000000000..f198c40e49 --- /dev/null +++ b/services/search/pkg/search/fields_test.go @@ -0,0 +1,46 @@ +package search_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/opencloud-eu/opencloud/services/search/pkg/search" +) + +var _ = Describe("IsNumericField", func() { + DescribeTable("reports whether a field maps to a numeric type", + func(field string, numeric bool) { + Expect(search.IsNumericField(field)).To(Equal(numeric)) + }, + // top-level numeric fields on Resource / Document + Entry("Size (uint64 via embedded Document)", "Size", true), + Entry("Type (uint64 on Resource)", "Type", true), + // top-level string fields + Entry("Name", "Name", false), + Entry("Path", "Path", false), + Entry("MimeType", "MimeType", false), + // nested audio + Entry("audio.artist", "audio.artist", false), + Entry("audio.album", "audio.album", false), + Entry("audio.year", "audio.year", true), + Entry("audio.bitrate", "audio.bitrate", true), + Entry("audio.track", "audio.track", true), + Entry("audio.hasDrm (bool, not numeric)", "audio.hasDrm", false), + // nested image + Entry("image.width", "image.width", true), + Entry("image.height", "image.height", true), + // nested photo + Entry("photo.cameraMake", "photo.cameraMake", false), + Entry("photo.iso", "photo.iso", true), + Entry("photo.focalLength (float32)", "photo.focalLength", true), + Entry("photo.exposureDenominator (float32)", "photo.exposureDenominator", true), + Entry("photo.takenDateTime (time.Time, treated as numeric)", "photo.takenDateTime", true), + // nested location + Entry("location.altitude", "location.altitude", true), + Entry("location.latitude", "location.latitude", true), + Entry("location.longitude", "location.longitude", true), + // unknown fields the caller may still aggregate on + Entry("nonexistent", "nonexistent", false), + Entry("audio.nonexistent", "audio.nonexistent", false), + ) +}) diff --git a/services/search/pkg/search/service.go b/services/search/pkg/search/service.go index 2c8221b2a7..cd65c23b2a 100644 --- a/services/search/pkg/search/service.go +++ b/services/search/pkg/search/service.go @@ -270,6 +270,7 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se return nil, err } + mergedAggregations := map[string]map[string]*searchmsgBucket{} for _, res := range responses { if res == nil { continue @@ -278,6 +279,26 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se for _, match := range res.Matches { matches = append(matches, match) } + for _, agg := range res.GetAggregations() { + field := agg.GetField() + if _, ok := mergedAggregations[field]; !ok { + mergedAggregations[field] = map[string]*searchmsgBucket{} + } + for _, b := range agg.GetBuckets() { + if existing, ok := mergedAggregations[field][b.GetKey()]; ok { + existing.Count += b.GetCount() + // union child buckets per sub-aggregation so counts stay + // right when a key spans multiple spaces + existing.SubAggregations = mergeSubAggregations(existing.GetSubAggregations(), b.GetSubAggregations()) + continue + } + mergedAggregations[field][b.GetKey()] = &searchsvc.Bucket{ + Key: b.GetKey(), + Count: b.GetCount(), + SubAggregations: b.GetSubAggregations(), + } + } + } } // compile one sorted list of matches from all spaces and apply the limit if needed @@ -290,13 +311,157 @@ func (s *Service) Search(ctx context.Context, req *searchsvc.SearchRequest) (*se matches = matches[0:limit] } + aggregations := make([]*searchsvc.AggregationResult, 0, len(req.GetAggregations())) + for _, opt := range req.GetAggregations() { + field := opt.GetField() + bucketMap := mergedAggregations[field] + buckets := make([]*searchsvc.Bucket, 0, len(bucketMap)) + for _, b := range bucketMap { + buckets = append(buckets, b) + } + aggregations = append(aggregations, &searchsvc.AggregationResult{ + Field: field, + Buckets: postProcessBuckets(buckets, opt), + }) + } + success = true return &searchsvc.SearchResponse{ Matches: matches, TotalMatches: total, + Aggregations: aggregations, }, nil } +// searchmsgBucket aliases the bucket type for the map-of-maps below. +type searchmsgBucket = searchsvc.Bucket + +// mergeSubAggregations unions two nested-aggregation lists by field: terms +// union child buckets by key (summing, recursing); metrics apply their reducer +// (sum/min/max). +func mergeSubAggregations(a, b []*searchsvc.AggregationResult) []*searchsvc.AggregationResult { + if len(a) == 0 { + return b + } + if len(b) == 0 { + return a + } + byField := make(map[string]*searchsvc.AggregationResult, len(a)) + for _, r := range a { + byField[r.GetField()] = r + } + for _, r := range b { + existing, ok := byField[r.GetField()] + if !ok { + byField[r.GetField()] = r + continue + } + if existing.GetMetricKind() != searchsvc.MetricKind_METRIC_KIND_UNSPECIFIED || + r.GetMetricKind() != searchsvc.MetricKind_METRIC_KIND_UNSPECIFIED { + // Metric result: apply the kind's reducer; prefer existing's kind. + kind := existing.GetMetricKind() + if kind == searchsvc.MetricKind_METRIC_KIND_UNSPECIFIED { + kind = r.GetMetricKind() + } + existing.MetricKind = kind + if kind == searchsvc.MetricKind_METRIC_KIND_AVG { + existing.Sum += r.GetSum() + existing.Count += r.GetCount() + } else { + existing.Value = reduceMetric(kind, existing.GetValue(), r.GetValue()) + } + continue + } + byKey := make(map[string]*searchsvc.Bucket, len(existing.Buckets)) + for _, bk := range existing.Buckets { + byKey[bk.GetKey()] = bk + } + for _, bk := range r.GetBuckets() { + if prev, ok := byKey[bk.GetKey()]; ok { + prev.Count += bk.GetCount() + prev.SubAggregations = mergeSubAggregations(prev.GetSubAggregations(), bk.GetSubAggregations()) + } else { + existing.Buckets = append(existing.Buckets, bk) + byKey[bk.GetKey()] = bk + } + } + } + out := make([]*searchsvc.AggregationResult, 0, len(byField)) + for _, r := range byField { + out = append(out, r) + } + return out +} + +// reduceMetric applies the metric's cross-shard reducer; only called when both +// sides carry a value. +func reduceMetric(kind searchsvc.MetricKind, a, b float64) float64 { + switch kind { + case searchsvc.MetricKind_METRIC_KIND_SUM: + return a + b + case searchsvc.MetricKind_METRIC_KIND_MIN: + if b < a { + return b + } + return a + case searchsvc.MetricKind_METRIC_KIND_MAX: + if b > a { + return b + } + return a + } + return a +} + +// postProcessBuckets applies the BucketDefinition (minimumCount filter, sort by +// count/keyAsString/keyAsNumber, trim to Size). Defaults to count-descending. +func postProcessBuckets(buckets []*searchsvc.Bucket, opt *searchsvc.AggregationOption) []*searchsvc.Bucket { + bd := opt.GetBucketDefinition() + sortBy := "count" + desc := true + var minCount int64 + if bd != nil { + if bd.GetSortBy() != "" { + sortBy = bd.GetSortBy() + } + desc = bd.GetIsDescending() + minCount = int64(bd.GetMinimumCount()) + } + + if minCount > 0 { + filtered := buckets[:0] + for _, b := range buckets { + if b.GetCount() >= minCount { + filtered = append(filtered, b) + } + } + buckets = filtered + } + + sort.SliceStable(buckets, func(i, j int) bool { + less := false + switch sortBy { + case "keyAsString": + less = buckets[i].GetKey() < buckets[j].GetKey() + case "keyAsNumber": + iv, _ := strconv.ParseFloat(buckets[i].GetKey(), 64) + jv, _ := strconv.ParseFloat(buckets[j].GetKey(), 64) + less = iv < jv + default: // "count" + less = buckets[i].GetCount() < buckets[j].GetCount() + } + if desc { + return !less + } + return less + }) + + if size := opt.GetSize(); size > 0 && int32(len(buckets)) > size { + buckets = buckets[:size] + } + return buckets +} + func (s *Service) searchIndex(ctx context.Context, req *searchsvc.SearchRequest, space *provider.StorageSpace, mountpointID string) (*searchsvc.SearchIndexResponse, error) { if req.Ref != nil && (req.Ref.ResourceId.StorageId != space.Root.StorageId || @@ -394,7 +559,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, diff --git a/services/search/pkg/search/service_test.go b/services/search/pkg/search/service_test.go index 301e9db031..d5c6597a2c 100644 --- a/services/search/pkg/search/service_test.go +++ b/services/search/pkg/search/service_test.go @@ -206,6 +206,159 @@ var _ = Describe("Searchprovider", func() { Expect(match.Entity.Ref.ResourceId.OpaqueId).To(Equal(personalSpace.Root.OpaqueId)) Expect(match.Entity.Ref.Path).To(Equal("./path/to/Foo.pdf")) }) + + It("forwards aggregations to the engine", func() { + _, err := s.Search(ctx, &searchsvc.SearchRequest{ + Query: "foo", + Aggregations: []*searchsvc.AggregationOption{ + {Field: "audio.artist", Size: 10}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + indexClient.AssertCalled(GinkgoT(), "Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool { + return len(req.Aggregations) == 1 && + req.Aggregations[0].Field == "audio.artist" && + req.Aggregations[0].Size == 10 + })) + }) + }) + + Context("with two personal spaces returning aggregations", func() { + var ( + spaceA = &sprovider.StorageSpace{ + Id: &sprovider.StorageSpaceId{OpaqueId: "storageid$a!a"}, + Root: &sprovider.ResourceId{StorageId: "storageid", SpaceId: "a", OpaqueId: "a"}, + Name: "space-a", + SpaceType: "personal", + } + spaceB = &sprovider.StorageSpace{ + Id: &sprovider.StorageSpaceId{OpaqueId: "storageid$b!b"}, + Root: &sprovider.ResourceId{StorageId: "storageid", SpaceId: "b", OpaqueId: "b"}, + Name: "space-b", + SpaceType: "personal", + } + ) + + BeforeEach(func() { + gatewayClient.On("ListStorageSpaces", mock.Anything, mock.Anything).Return(&sprovider.ListStorageSpacesResponse{ + Status: status.NewOK(ctx), + StorageSpaces: []*sprovider.StorageSpace{spaceA, spaceB}, + }, nil) + indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool { + return req.Ref != nil && req.Ref.ResourceId.SpaceId == "a" + })).Return(&searchsvc.SearchIndexResponse{ + TotalMatches: 2, + Aggregations: []*searchsvc.AggregationResult{{ + Field: "audio.artist", + Buckets: []*searchsvc.Bucket{ + {Key: "Pink Floyd", Count: 2}, + {Key: "Motörhead", Count: 1}, + }, + }}, + }, nil) + indexClient.On("Search", mock.Anything, mock.MatchedBy(func(req *searchsvc.SearchIndexRequest) bool { + return req.Ref != nil && req.Ref.ResourceId.SpaceId == "b" + })).Return(&searchsvc.SearchIndexResponse{ + TotalMatches: 3, + Aggregations: []*searchsvc.AggregationResult{{ + Field: "audio.artist", + Buckets: []*searchsvc.Bucket{ + {Key: "Pink Floyd", Count: 3}, + {Key: "Led Zeppelin", Count: 1}, + }, + }}, + }, nil) + }) + + It("merges bucket counts across spaces", func() { + res, err := s.Search(ctx, &searchsvc.SearchRequest{ + Query: "mediatype:audio", + Aggregations: []*searchsvc.AggregationOption{ + {Field: "audio.artist", Size: 10}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res.Aggregations).To(HaveLen(1)) + agg := res.Aggregations[0] + Expect(agg.Field).To(Equal("audio.artist")) + + counts := map[string]int64{} + for _, b := range agg.Buckets { + counts[b.Key] = b.Count + } + Expect(counts).To(HaveKeyWithValue("Pink Floyd", int64(5))) + Expect(counts).To(HaveKeyWithValue("Motörhead", int64(1))) + Expect(counts).To(HaveKeyWithValue("Led Zeppelin", int64(1))) + }) + + It("sorts buckets by count descending by default", func() { + res, err := s.Search(ctx, &searchsvc.SearchRequest{ + Query: "mediatype:audio", + Aggregations: []*searchsvc.AggregationOption{ + {Field: "audio.artist"}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + keys := []string{} + for _, b := range res.Aggregations[0].Buckets { + keys = append(keys, b.Key) + } + // Pink Floyd:5, Motörhead:1, Led Zeppelin:1 (count desc) + Expect(keys[0]).To(Equal("Pink Floyd")) + }) + + It("sorts buckets alphabetically ascending with sortBy keyAsString", func() { + res, err := s.Search(ctx, &searchsvc.SearchRequest{ + Query: "mediatype:audio", + Aggregations: []*searchsvc.AggregationOption{ + { + Field: "audio.artist", + BucketDefinition: &searchsvc.BucketDefinition{ + SortBy: "keyAsString", + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + keys := []string{} + for _, b := range res.Aggregations[0].Buckets { + keys = append(keys, b.Key) + } + Expect(keys).To(Equal([]string{"Led Zeppelin", "Motörhead", "Pink Floyd"})) + }) + + It("applies minimumCount filter and size cap", func() { + res, err := s.Search(ctx, &searchsvc.SearchRequest{ + Query: "mediatype:audio", + Aggregations: []*searchsvc.AggregationOption{ + { + Field: "audio.artist", + Size: 5, + BucketDefinition: &searchsvc.BucketDefinition{ + SortBy: "count", + IsDescending: true, + MinimumCount: 2, + }, + }, + }, + }) + Expect(err).ToNot(HaveOccurred()) + // only Pink Floyd has count >= 2 + Expect(res.Aggregations[0].Buckets).To(HaveLen(1)) + Expect(res.Aggregations[0].Buckets[0].Key).To(Equal("Pink Floyd")) + }) + + It("trims the bucket list to Size", func() { + res, err := s.Search(ctx, &searchsvc.SearchRequest{ + Query: "mediatype:audio", + Aggregations: []*searchsvc.AggregationOption{ + {Field: "audio.artist", Size: 1}, + }, + }) + Expect(err).ToNot(HaveOccurred()) + Expect(res.Aggregations[0].Buckets).To(HaveLen(1)) + Expect(res.Aggregations[0].Buckets[0].Key).To(Equal("Pink Floyd")) + }) }) Context("with a personal space with a filter", func() { diff --git a/services/search/pkg/service/grpc/v0/service.go b/services/search/pkg/service/grpc/v0/service.go index dddb64d4b6..d78896d59d 100644 --- a/services/search/pkg/service/grpc/v0/service.go +++ b/services/search/pkg/service/grpc/v0/service.go @@ -20,6 +20,7 @@ import ( merrors "go-micro.dev/v4/errors" "go-micro.dev/v4/metadata" grpcmetadata "google.golang.org/grpc/metadata" + "google.golang.org/protobuf/proto" "github.com/opencloud-eu/opencloud/pkg/log" v0 "github.com/opencloud-eu/opencloud/protogen/gen/opencloud/messages/search/v0" @@ -91,14 +92,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) { @@ -115,6 +117,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 } @@ -170,6 +173,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) } diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md b/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md index 3d0e9dc3cb..dd97b8c214 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/README.md @@ -158,6 +158,7 @@ Class | Method | HTTP request | Description *MeUserApi* | [**UpdateOwnUser**](docs/MeUserApi.md#updateownuser) | **Patch** /v1.0/me | Update the current user *RoleManagementApi* | [**GetPermissionRoleDefinition**](docs/RoleManagementApi.md#getpermissionroledefinition) | **Get** /v1beta1/roleManagement/permissions/roleDefinitions/{role-id} | Get unifiedRoleDefinition *RoleManagementApi* | [**ListPermissionRoleDefinitions**](docs/RoleManagementApi.md#listpermissionroledefinitions) | **Get** /v1beta1/roleManagement/permissions/roleDefinitions | List roleDefinitions +*SearchApi* | [**SearchQuery**](docs/SearchApi.md#searchquery) | **Post** /v1beta1/search/query | Search for resources *TagsApi* | [**AssignTags**](docs/TagsApi.md#assigntags) | **Put** /v1.0/extensions/org.libregraph/tags | Assign tags to a resource *TagsApi* | [**GetTags**](docs/TagsApi.md#gettags) | **Get** /v1.0/extensions/org.libregraph/tags | Get all known tags *TagsApi* | [**UnassignTags**](docs/TagsApi.md#unassigntags) | **Delete** /v1.0/extensions/org.libregraph/tags | Unassign tags from a resource @@ -178,10 +179,13 @@ Class | Method | HTTP request | Description - [Activity](docs/Activity.md) - [ActivityTemplate](docs/ActivityTemplate.md) - [ActivityTimes](docs/ActivityTimes.md) + - [AggregationOption](docs/AggregationOption.md) - [AppRole](docs/AppRole.md) - [AppRoleAssignment](docs/AppRoleAssignment.md) - [Application](docs/Application.md) - [Audio](docs/Audio.md) + - [BucketAggregationRange](docs/BucketAggregationRange.md) + - [BucketDefinition](docs/BucketDefinition.md) - [ClassMemberReference](docs/ClassMemberReference.md) - [ClassReference](docs/ClassReference.md) - [ClassTeacherReference](docs/ClassTeacherReference.md) @@ -243,6 +247,15 @@ Class | Method | HTTP request | Description - [Quota](docs/Quota.md) - [Recipient](docs/Recipient.md) - [RemoteItem](docs/RemoteItem.md) + - [SearchAggregation](docs/SearchAggregation.md) + - [SearchBucket](docs/SearchBucket.md) + - [SearchHit](docs/SearchHit.md) + - [SearchHitsContainer](docs/SearchHitsContainer.md) + - [SearchQuery](docs/SearchQuery.md) + - [SearchQuery200Response](docs/SearchQuery200Response.md) + - [SearchQueryRequest](docs/SearchQueryRequest.md) + - [SearchRequest](docs/SearchRequest.md) + - [SearchResponse](docs/SearchResponse.md) - [SharePointIdentitySet](docs/SharePointIdentitySet.md) - [SharingInvitation](docs/SharingInvitation.md) - [SharingLink](docs/SharingLink.md) diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/api_search.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_search.go new file mode 100644 index 0000000000..a1e3045fa0 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/api_search.go @@ -0,0 +1,156 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "bytes" + "context" + "io" + "net/http" + "net/url" +) + + +// SearchApiService SearchApi service +type SearchApiService service + +type ApiSearchQueryRequest struct { + ctx context.Context + ApiService *SearchApiService + searchQueryRequest *SearchQueryRequest +} + +func (r ApiSearchQueryRequest) SearchQueryRequest(searchQueryRequest SearchQueryRequest) ApiSearchQueryRequest { + r.searchQueryRequest = &searchQueryRequest + return r +} + +func (r ApiSearchQueryRequest) Execute() (*SearchQuery200Response, *http.Response, error) { + return r.ApiService.SearchQueryExecute(r) +} + +/* +SearchQuery Search for resources + +Run a specified search query. Search results are provided in the response. + +The search endpoint allows clients to search for resources across all +accessible spaces and retrieve aggregated metadata (facets) about the +result set. + +Aggregations can be used to group results by properties such as file type, +author, or any indexed metadata field. This is useful for building faceted +search UIs or computing statistics about the result set. + +The query string uses KQL (Keyword Query Language) syntax for filtering. + +This endpoint is inspired by the +[MS Graph Search API](https://learn.microsoft.com/en-us/graph/api/search-query) +and follows the same request/response structure for compatibility. + + + @param ctx context.Context - for authentication, logging, cancellation, deadlines, tracing, etc. Passed from http.Request or context.Background(). + @return ApiSearchQueryRequest +*/ +func (a *SearchApiService) SearchQuery(ctx context.Context) ApiSearchQueryRequest { + return ApiSearchQueryRequest{ + ApiService: a, + ctx: ctx, + } +} + +// Execute executes the request +// @return SearchQuery200Response +func (a *SearchApiService) SearchQueryExecute(r ApiSearchQueryRequest) (*SearchQuery200Response, *http.Response, error) { + var ( + localVarHTTPMethod = http.MethodPost + localVarPostBody interface{} + formFiles []formFile + localVarReturnValue *SearchQuery200Response + ) + + localBasePath, err := a.client.cfg.ServerURLWithContext(r.ctx, "SearchApiService.SearchQuery") + if err != nil { + return localVarReturnValue, nil, &GenericOpenAPIError{error: err.Error()} + } + + localVarPath := localBasePath + "/v1beta1/search/query" + + localVarHeaderParams := make(map[string]string) + localVarQueryParams := url.Values{} + localVarFormParams := url.Values{} + if r.searchQueryRequest == nil { + return localVarReturnValue, nil, reportError("searchQueryRequest is required and must be specified") + } + + // to determine the Content-Type header + localVarHTTPContentTypes := []string{"application/json"} + + // set Content-Type header + localVarHTTPContentType := selectHeaderContentType(localVarHTTPContentTypes) + if localVarHTTPContentType != "" { + localVarHeaderParams["Content-Type"] = localVarHTTPContentType + } + + // to determine the Accept header + localVarHTTPHeaderAccepts := []string{"application/json"} + + // set Accept header + localVarHTTPHeaderAccept := selectHeaderAccept(localVarHTTPHeaderAccepts) + if localVarHTTPHeaderAccept != "" { + localVarHeaderParams["Accept"] = localVarHTTPHeaderAccept + } + // body params + localVarPostBody = r.searchQueryRequest + req, err := a.client.prepareRequest(r.ctx, localVarPath, localVarHTTPMethod, localVarPostBody, localVarHeaderParams, localVarQueryParams, localVarFormParams, formFiles) + if err != nil { + return localVarReturnValue, nil, err + } + + localVarHTTPResponse, err := a.client.callAPI(req) + if err != nil || localVarHTTPResponse == nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + localVarBody, err := io.ReadAll(localVarHTTPResponse.Body) + localVarHTTPResponse.Body.Close() + localVarHTTPResponse.Body = io.NopCloser(bytes.NewBuffer(localVarBody)) + if err != nil { + return localVarReturnValue, localVarHTTPResponse, err + } + + if localVarHTTPResponse.StatusCode >= 300 { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: localVarHTTPResponse.Status, + } + var v OdataError + err = a.client.decode(&v, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr.error = err.Error() + return localVarReturnValue, localVarHTTPResponse, newErr + } + newErr.error = formatErrorMessage(localVarHTTPResponse.Status, &v) + newErr.model = v + return localVarReturnValue, localVarHTTPResponse, newErr + } + + err = a.client.decode(&localVarReturnValue, localVarBody, localVarHTTPResponse.Header.Get("Content-Type")) + if err != nil { + newErr := &GenericOpenAPIError{ + body: localVarBody, + error: err.Error(), + } + return localVarReturnValue, localVarHTTPResponse, newErr + } + + return localVarReturnValue, localVarHTTPResponse, nil +} diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go index 5b3222ebf3..0fb69b7e1f 100644 --- a/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/client.go @@ -93,6 +93,8 @@ type APIClient struct { RoleManagementApi *RoleManagementApiService + SearchApi *SearchApiService + TagsApi *TagsApiService UserApi *UserApiService @@ -142,6 +144,7 @@ func NewAPIClient(cfg *Configuration) *APIClient { c.MePhotoApi = (*MePhotoApiService)(&c.common) c.MeUserApi = (*MeUserApiService)(&c.common) c.RoleManagementApi = (*RoleManagementApiService)(&c.common) + c.SearchApi = (*SearchApiService)(&c.common) c.TagsApi = (*TagsApiService)(&c.common) c.UserApi = (*UserApiService)(&c.common) c.UserAppRoleAssignmentApi = (*UserAppRoleAssignmentApiService)(&c.common) diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_aggregation_option.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_aggregation_option.go new file mode 100644 index 0000000000..b15616c7fa --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_aggregation_option.go @@ -0,0 +1,306 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" + "bytes" + "fmt" +) + +// checks if the AggregationOption type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &AggregationOption{} + +// AggregationOption Specifies an aggregation that should be computed and returned alongside search results. Follows the [MS Graph aggregationOption](https://learn.microsoft.com/en-us/graph/api/resources/aggregationoption) resource type. For string fields, terms aggregations return the distinct values and their counts. For numeric and date fields, range aggregations can be defined using the `ranges` property of `bucketDefinition`. +type AggregationOption struct { + // Specifies the field in the schema of the specified entity type that the aggregation should be computed on. Required. Examples: `audio.artist`, `audio.genre`, `audio.year`, `mimeType`. + Field string `json:"field"` + // The number of `searchBucket` resources to be returned. This is optional and only applies to terms aggregations. Combined with `bucketDefinition.sortBy` and `bucketDefinition.isDescending` to produce the top N results by count or key. When not specified, all buckets are returned. + Size *int32 `json:"size,omitempty"` + BucketDefinition *BucketDefinition `json:"bucketDefinition,omitempty"` + // Nested aggregations computed within each bucket of this aggregation. Libregraph extension not present in MS Graph. Backends that don't support native composite aggregations (e.g. bleve) emulate them by walking the matched result set; OpenSearch translates them to native composite aggregations. + SubAggregations []AggregationOption `json:"subAggregations,omitempty"` + // When set, this aggregation is a scalar metric over `field` rather than a bucket aggregation: `size` and `bucketDefinition` are ignored, and the corresponding `searchAggregation` in the response carries a `value` rather than `buckets`. Libregraph extension not present in MS Graph. `avg` is not a simple reducer (averages of averages are not averages) — the backend carries `(sum, count)` internally and emits only `value` on the outermost merge. + MetricKind *string `json:"metricKind,omitempty"` +} + +type _AggregationOption AggregationOption + +// NewAggregationOption instantiates a new AggregationOption object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewAggregationOption(field string) *AggregationOption { + this := AggregationOption{} + this.Field = field + return &this +} + +// NewAggregationOptionWithDefaults instantiates a new AggregationOption object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewAggregationOptionWithDefaults() *AggregationOption { + this := AggregationOption{} + return &this +} + +// GetField returns the Field field value +func (o *AggregationOption) GetField() string { + if o == nil { + var ret string + return ret + } + + return o.Field +} + +// GetFieldOk returns a tuple with the Field field value +// and a boolean to check if the value has been set. +func (o *AggregationOption) GetFieldOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.Field, true +} + +// SetField sets field value +func (o *AggregationOption) SetField(v string) { + o.Field = v +} + +// GetSize returns the Size field value if set, zero value otherwise. +func (o *AggregationOption) GetSize() int32 { + if o == nil || IsNil(o.Size) { + var ret int32 + return ret + } + return *o.Size +} + +// GetSizeOk returns a tuple with the Size field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AggregationOption) GetSizeOk() (*int32, bool) { + if o == nil || IsNil(o.Size) { + return nil, false + } + return o.Size, true +} + +// HasSize returns a boolean if a field has been set. +func (o *AggregationOption) HasSize() bool { + if o != nil && !IsNil(o.Size) { + return true + } + + return false +} + +// SetSize gets a reference to the given int32 and assigns it to the Size field. +func (o *AggregationOption) SetSize(v int32) { + o.Size = &v +} + +// GetBucketDefinition returns the BucketDefinition field value if set, zero value otherwise. +func (o *AggregationOption) GetBucketDefinition() BucketDefinition { + if o == nil || IsNil(o.BucketDefinition) { + var ret BucketDefinition + return ret + } + return *o.BucketDefinition +} + +// GetBucketDefinitionOk returns a tuple with the BucketDefinition field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AggregationOption) GetBucketDefinitionOk() (*BucketDefinition, bool) { + if o == nil || IsNil(o.BucketDefinition) { + return nil, false + } + return o.BucketDefinition, true +} + +// HasBucketDefinition returns a boolean if a field has been set. +func (o *AggregationOption) HasBucketDefinition() bool { + if o != nil && !IsNil(o.BucketDefinition) { + return true + } + + return false +} + +// SetBucketDefinition gets a reference to the given BucketDefinition and assigns it to the BucketDefinition field. +func (o *AggregationOption) SetBucketDefinition(v BucketDefinition) { + o.BucketDefinition = &v +} + +// GetSubAggregations returns the SubAggregations field value if set, zero value otherwise. +func (o *AggregationOption) GetSubAggregations() []AggregationOption { + if o == nil || IsNil(o.SubAggregations) { + var ret []AggregationOption + return ret + } + return o.SubAggregations +} + +// GetSubAggregationsOk returns a tuple with the SubAggregations field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AggregationOption) GetSubAggregationsOk() ([]AggregationOption, bool) { + if o == nil || IsNil(o.SubAggregations) { + return nil, false + } + return o.SubAggregations, true +} + +// HasSubAggregations returns a boolean if a field has been set. +func (o *AggregationOption) HasSubAggregations() bool { + if o != nil && !IsNil(o.SubAggregations) { + return true + } + + return false +} + +// SetSubAggregations gets a reference to the given []AggregationOption and assigns it to the SubAggregations field. +func (o *AggregationOption) SetSubAggregations(v []AggregationOption) { + o.SubAggregations = v +} + +// GetMetricKind returns the MetricKind field value if set, zero value otherwise. +func (o *AggregationOption) GetMetricKind() string { + if o == nil || IsNil(o.MetricKind) { + var ret string + return ret + } + return *o.MetricKind +} + +// GetMetricKindOk returns a tuple with the MetricKind field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *AggregationOption) GetMetricKindOk() (*string, bool) { + if o == nil || IsNil(o.MetricKind) { + return nil, false + } + return o.MetricKind, true +} + +// HasMetricKind returns a boolean if a field has been set. +func (o *AggregationOption) HasMetricKind() bool { + if o != nil && !IsNil(o.MetricKind) { + return true + } + + return false +} + +// SetMetricKind gets a reference to the given string and assigns it to the MetricKind field. +func (o *AggregationOption) SetMetricKind(v string) { + o.MetricKind = &v +} + +func (o AggregationOption) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o AggregationOption) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["field"] = o.Field + if !IsNil(o.Size) { + toSerialize["size"] = o.Size + } + if !IsNil(o.BucketDefinition) { + toSerialize["bucketDefinition"] = o.BucketDefinition + } + if !IsNil(o.SubAggregations) { + toSerialize["subAggregations"] = o.SubAggregations + } + if !IsNil(o.MetricKind) { + toSerialize["metricKind"] = o.MetricKind + } + return toSerialize, nil +} + +func (o *AggregationOption) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "field", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err; + } + + for _, requiredProperty := range(requiredProperties) { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varAggregationOption := _AggregationOption{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varAggregationOption) + + if err != nil { + return err + } + + *o = AggregationOption(varAggregationOption) + + return err +} + +type NullableAggregationOption struct { + value *AggregationOption + isSet bool +} + +func (v NullableAggregationOption) Get() *AggregationOption { + return v.value +} + +func (v *NullableAggregationOption) Set(val *AggregationOption) { + v.value = val + v.isSet = true +} + +func (v NullableAggregationOption) IsSet() bool { + return v.isSet +} + +func (v *NullableAggregationOption) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableAggregationOption(val *AggregationOption) *NullableAggregationOption { + return &NullableAggregationOption{value: val, isSet: true} +} + +func (v NullableAggregationOption) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableAggregationOption) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_bucket_aggregation_range.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_bucket_aggregation_range.go new file mode 100644 index 0000000000..441a85736b --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_bucket_aggregation_range.go @@ -0,0 +1,164 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the BucketAggregationRange type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &BucketAggregationRange{} + +// BucketAggregationRange Specifies the lower and upper bound to compute a range aggregation bucket. At least one of `from` or `to` must be provided. +type BucketAggregationRange struct { + // Defines the lower bound from which to compute the aggregation. The value is always a string. Numeric bounds must be provided as their string representation (e.g. `\"1980\"`). Date bounds must use the `YYYY-MM-DDTHH:mm:ssZ` format. Optional if `to` is provided. + From *string + // Defines the upper bound up to which to compute the aggregation. The value is always a string. Numeric bounds must be provided as their string representation (e.g. `\"2000\"`). Date bounds must use the `YYYY-MM-DDTHH:mm:ssZ` format. Optional if `from` is provided. + To *string +} + +// NewBucketAggregationRange instantiates a new BucketAggregationRange object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewBucketAggregationRange() *BucketAggregationRange { + this := BucketAggregationRange{} + return &this +} + +// NewBucketAggregationRangeWithDefaults instantiates a new BucketAggregationRange object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewBucketAggregationRangeWithDefaults() *BucketAggregationRange { + this := BucketAggregationRange{} + return &this +} + +// GetFrom returns the From field value if set, zero value otherwise. +func (o *BucketAggregationRange) GetFrom() string { + if o == nil || IsNil(o.From) { + var ret string + return ret + } + return *o.From +} + +// GetFromOk returns a tuple with the From field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BucketAggregationRange) GetFromOk() (*string, bool) { + if o == nil || IsNil(o.From) { + return nil, false + } + return o.From, true +} + +// HasFrom returns a boolean if a field has been set. +func (o *BucketAggregationRange) HasFrom() bool { + if o != nil && !IsNil(o.From) { + return true + } + + return false +} + +// SetFrom gets a reference to the given string and assigns it to the From field. +func (o *BucketAggregationRange) SetFrom(v string) { + o.From = &v +} + +// GetTo returns the To field value if set, zero value otherwise. +func (o *BucketAggregationRange) GetTo() string { + if o == nil || IsNil(o.To) { + var ret string + return ret + } + return *o.To +} + +// GetToOk returns a tuple with the To field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BucketAggregationRange) GetToOk() (*string, bool) { + if o == nil || IsNil(o.To) { + return nil, false + } + return o.To, true +} + +// HasTo returns a boolean if a field has been set. +func (o *BucketAggregationRange) HasTo() bool { + if o != nil && !IsNil(o.To) { + return true + } + + return false +} + +// SetTo gets a reference to the given string and assigns it to the To field. +func (o *BucketAggregationRange) SetTo(v string) { + o.To = &v +} + +func (o BucketAggregationRange) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o BucketAggregationRange) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.From) { + toSerialize["from"] = o.From + } + if !IsNil(o.To) { + toSerialize["to"] = o.To + } + return toSerialize, nil +} + +type NullableBucketAggregationRange struct { + value *BucketAggregationRange + isSet bool +} + +func (v NullableBucketAggregationRange) Get() *BucketAggregationRange { + return v.value +} + +func (v *NullableBucketAggregationRange) Set(val *BucketAggregationRange) { + v.value = val + v.isSet = true +} + +func (v NullableBucketAggregationRange) IsSet() bool { + return v.isSet +} + +func (v *NullableBucketAggregationRange) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBucketAggregationRange(val *BucketAggregationRange) *NullableBucketAggregationRange { + return &NullableBucketAggregationRange{value: val, isSet: true} +} + +func (v NullableBucketAggregationRange) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBucketAggregationRange) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_bucket_definition.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_bucket_definition.go new file mode 100644 index 0000000000..69fc7e9b6b --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_bucket_definition.go @@ -0,0 +1,278 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" + "bytes" + "fmt" +) + +// checks if the BucketDefinition type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &BucketDefinition{} + +// BucketDefinition Provides the details of how to generate the aggregation buckets in the response. Follows the [MS Graph bucketAggregationDefinition](https://learn.microsoft.com/en-us/graph/api/resources/bucketaggregationdefinition) resource type. +type BucketDefinition struct { + // The possible values are `count` to sort by the number of matches in the aggregation, `keyAsString` to sort alphabetically based on the key in the aggregation, and `keyAsNumber` to sort numerically based on the key in the aggregation. Required. + SortBy string `json:"sortBy"` + // Set to `true` to specify the sort order as descending. Optional, defaults to `false` (ascending). + IsDescending *bool `json:"isDescending,omitempty"` + // The minimum number of items that should be present in the aggregation for the bucket to be returned in the response. Optional, default is 0. + MinimumCount *int32 `json:"minimumCount,omitempty"` + // Specifies the manual ranges to compute the aggregation buckets. This is only valid for non-string facets of date or numeric type. Optional. Follows the [MS Graph bucketAggregationRange](https://learn.microsoft.com/en-us/graph/api/resources/bucketaggregationrange) resource type. + Ranges []BucketAggregationRange `json:"ranges,omitempty"` +} + +type _BucketDefinition BucketDefinition + +// NewBucketDefinition instantiates a new BucketDefinition object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewBucketDefinition(sortBy string) *BucketDefinition { + this := BucketDefinition{} + this.SortBy = sortBy + var isDescending bool = false + this.IsDescending = &isDescending + var minimumCount int32 = 0 + this.MinimumCount = &minimumCount + return &this +} + +// NewBucketDefinitionWithDefaults instantiates a new BucketDefinition object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewBucketDefinitionWithDefaults() *BucketDefinition { + this := BucketDefinition{} + var isDescending bool = false + this.IsDescending = &isDescending + var minimumCount int32 = 0 + this.MinimumCount = &minimumCount + return &this +} + +// GetSortBy returns the SortBy field value +func (o *BucketDefinition) GetSortBy() string { + if o == nil { + var ret string + return ret + } + + return o.SortBy +} + +// GetSortByOk returns a tuple with the SortBy field value +// and a boolean to check if the value has been set. +func (o *BucketDefinition) GetSortByOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.SortBy, true +} + +// SetSortBy sets field value +func (o *BucketDefinition) SetSortBy(v string) { + o.SortBy = v +} + +// GetIsDescending returns the IsDescending field value if set, zero value otherwise. +func (o *BucketDefinition) GetIsDescending() bool { + if o == nil || IsNil(o.IsDescending) { + var ret bool + return ret + } + return *o.IsDescending +} + +// GetIsDescendingOk returns a tuple with the IsDescending field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BucketDefinition) GetIsDescendingOk() (*bool, bool) { + if o == nil || IsNil(o.IsDescending) { + return nil, false + } + return o.IsDescending, true +} + +// HasIsDescending returns a boolean if a field has been set. +func (o *BucketDefinition) HasIsDescending() bool { + if o != nil && !IsNil(o.IsDescending) { + return true + } + + return false +} + +// SetIsDescending gets a reference to the given bool and assigns it to the IsDescending field. +func (o *BucketDefinition) SetIsDescending(v bool) { + o.IsDescending = &v +} + +// GetMinimumCount returns the MinimumCount field value if set, zero value otherwise. +func (o *BucketDefinition) GetMinimumCount() int32 { + if o == nil || IsNil(o.MinimumCount) { + var ret int32 + return ret + } + return *o.MinimumCount +} + +// GetMinimumCountOk returns a tuple with the MinimumCount field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BucketDefinition) GetMinimumCountOk() (*int32, bool) { + if o == nil || IsNil(o.MinimumCount) { + return nil, false + } + return o.MinimumCount, true +} + +// HasMinimumCount returns a boolean if a field has been set. +func (o *BucketDefinition) HasMinimumCount() bool { + if o != nil && !IsNil(o.MinimumCount) { + return true + } + + return false +} + +// SetMinimumCount gets a reference to the given int32 and assigns it to the MinimumCount field. +func (o *BucketDefinition) SetMinimumCount(v int32) { + o.MinimumCount = &v +} + +// GetRanges returns the Ranges field value if set, zero value otherwise. +func (o *BucketDefinition) GetRanges() []BucketAggregationRange { + if o == nil || IsNil(o.Ranges) { + var ret []BucketAggregationRange + return ret + } + return o.Ranges +} + +// GetRangesOk returns a tuple with the Ranges field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *BucketDefinition) GetRangesOk() ([]BucketAggregationRange, bool) { + if o == nil || IsNil(o.Ranges) { + return nil, false + } + return o.Ranges, true +} + +// HasRanges returns a boolean if a field has been set. +func (o *BucketDefinition) HasRanges() bool { + if o != nil && !IsNil(o.Ranges) { + return true + } + + return false +} + +// SetRanges gets a reference to the given []BucketAggregationRange and assigns it to the Ranges field. +func (o *BucketDefinition) SetRanges(v []BucketAggregationRange) { + o.Ranges = v +} + +func (o BucketDefinition) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o BucketDefinition) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["sortBy"] = o.SortBy + if !IsNil(o.IsDescending) { + toSerialize["isDescending"] = o.IsDescending + } + if !IsNil(o.MinimumCount) { + toSerialize["minimumCount"] = o.MinimumCount + } + if !IsNil(o.Ranges) { + toSerialize["ranges"] = o.Ranges + } + return toSerialize, nil +} + +func (o *BucketDefinition) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "sortBy", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err; + } + + for _, requiredProperty := range(requiredProperties) { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varBucketDefinition := _BucketDefinition{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varBucketDefinition) + + if err != nil { + return err + } + + *o = BucketDefinition(varBucketDefinition) + + return err +} + +type NullableBucketDefinition struct { + value *BucketDefinition + isSet bool +} + +func (v NullableBucketDefinition) Get() *BucketDefinition { + return v.value +} + +func (v *NullableBucketDefinition) Set(val *BucketDefinition) { + v.value = val + v.isSet = true +} + +func (v NullableBucketDefinition) IsSet() bool { + return v.isSet +} + +func (v *NullableBucketDefinition) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableBucketDefinition(val *BucketDefinition) *NullableBucketDefinition { + return &NullableBucketDefinition{value: val, isSet: true} +} + +func (v NullableBucketDefinition) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableBucketDefinition) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_aggregation.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_aggregation.go new file mode 100644 index 0000000000..9520b9f235 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_aggregation.go @@ -0,0 +1,238 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the SearchAggregation type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchAggregation{} + +// SearchAggregation Provides the details of a search aggregation in the search response. Follows the [MS Graph searchAggregation](https://learn.microsoft.com/en-us/graph/api/resources/searchaggregation) resource type. +type SearchAggregation struct { + // Defines the field in the request on which the aggregation was computed. + Field *string `json:"field,omitempty"` + // Defines the computed buckets for this aggregation. Buckets are sorted according to the `sortBy` and `isDescending` specified in the `bucketDefinition` of the corresponding `aggregationOption`. + Buckets []SearchBucket `json:"buckets,omitempty"` + // Scalar result for metric aggregations (`metricKind` set on the corresponding `aggregationOption`). Undefined for terms or range aggregations. Libregraph extension not present in MS Graph. + Value *float64 `json:"value,omitempty"` + // Echoes the `metricKind` of the corresponding `aggregationOption`, allowing consumers (and the search service's cross-space merge layer) to pick the right reducer when combining results. Libregraph extension. + MetricKind *string `json:"metricKind,omitempty"` +} + +// NewSearchAggregation instantiates a new SearchAggregation object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchAggregation() *SearchAggregation { + this := SearchAggregation{} + return &this +} + +// NewSearchAggregationWithDefaults instantiates a new SearchAggregation object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchAggregationWithDefaults() *SearchAggregation { + this := SearchAggregation{} + return &this +} + +// GetField returns the Field field value if set, zero value otherwise. +func (o *SearchAggregation) GetField() string { + if o == nil || IsNil(o.Field) { + var ret string + return ret + } + return *o.Field +} + +// GetFieldOk returns a tuple with the Field field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchAggregation) GetFieldOk() (*string, bool) { + if o == nil || IsNil(o.Field) { + return nil, false + } + return o.Field, true +} + +// HasField returns a boolean if a field has been set. +func (o *SearchAggregation) HasField() bool { + if o != nil && !IsNil(o.Field) { + return true + } + + return false +} + +// SetField gets a reference to the given string and assigns it to the Field field. +func (o *SearchAggregation) SetField(v string) { + o.Field = &v +} + +// GetBuckets returns the Buckets field value if set, zero value otherwise. +func (o *SearchAggregation) GetBuckets() []SearchBucket { + if o == nil || IsNil(o.Buckets) { + var ret []SearchBucket + return ret + } + return o.Buckets +} + +// GetBucketsOk returns a tuple with the Buckets field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchAggregation) GetBucketsOk() ([]SearchBucket, bool) { + if o == nil || IsNil(o.Buckets) { + return nil, false + } + return o.Buckets, true +} + +// HasBuckets returns a boolean if a field has been set. +func (o *SearchAggregation) HasBuckets() bool { + if o != nil && !IsNil(o.Buckets) { + return true + } + + return false +} + +// SetBuckets gets a reference to the given []SearchBucket and assigns it to the Buckets field. +func (o *SearchAggregation) SetBuckets(v []SearchBucket) { + o.Buckets = v +} + +// GetValue returns the Value field value if set, zero value otherwise. +func (o *SearchAggregation) GetValue() float64 { + if o == nil || IsNil(o.Value) { + var ret float64 + return ret + } + return *o.Value +} + +// GetValueOk returns a tuple with the Value field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchAggregation) GetValueOk() (*float64, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *SearchAggregation) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given float64 and assigns it to the Value field. +func (o *SearchAggregation) SetValue(v float64) { + o.Value = &v +} + +// GetMetricKind returns the MetricKind field value if set, zero value otherwise. +func (o *SearchAggregation) GetMetricKind() string { + if o == nil || IsNil(o.MetricKind) { + var ret string + return ret + } + return *o.MetricKind +} + +// GetMetricKindOk returns a tuple with the MetricKind field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchAggregation) GetMetricKindOk() (*string, bool) { + if o == nil || IsNil(o.MetricKind) { + return nil, false + } + return o.MetricKind, true +} + +// HasMetricKind returns a boolean if a field has been set. +func (o *SearchAggregation) HasMetricKind() bool { + if o != nil && !IsNil(o.MetricKind) { + return true + } + + return false +} + +// SetMetricKind gets a reference to the given string and assigns it to the MetricKind field. +func (o *SearchAggregation) SetMetricKind(v string) { + o.MetricKind = &v +} + +func (o SearchAggregation) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchAggregation) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Field) { + toSerialize["field"] = o.Field + } + if !IsNil(o.Buckets) { + toSerialize["buckets"] = o.Buckets + } + if !IsNil(o.Value) { + toSerialize["value"] = o.Value + } + if !IsNil(o.MetricKind) { + toSerialize["metricKind"] = o.MetricKind + } + return toSerialize, nil +} + +type NullableSearchAggregation struct { + value *SearchAggregation + isSet bool +} + +func (v NullableSearchAggregation) Get() *SearchAggregation { + return v.value +} + +func (v *NullableSearchAggregation) Set(val *SearchAggregation) { + v.value = val + v.isSet = true +} + +func (v NullableSearchAggregation) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchAggregation) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchAggregation(val *SearchAggregation) *NullableSearchAggregation { + return &NullableSearchAggregation{value: val, isSet: true} +} + +func (v NullableSearchAggregation) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchAggregation) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_bucket.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_bucket.go new file mode 100644 index 0000000000..0d647bbf90 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_bucket.go @@ -0,0 +1,238 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the SearchBucket type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchBucket{} + +// SearchBucket Represents a single bucket in a search aggregation result. Follows the [MS Graph searchBucket](https://learn.microsoft.com/en-us/graph/api/resources/searchbucket) resource type. +type SearchBucket struct { + // The discrete value of the field that was used to compute the aggregation. For terms aggregations this is the field value. For range aggregations this is a string representation of the range. + Key *string `json:"key,omitempty"` + // The approximate number of search matches that share the same value specified in the `key` property. + Count *int64 `json:"count,omitempty"` + // A token containing the encoded filter to aggregate search matches to the specific key value. To use the filter, pass the token as part of the `aggregationFilters` property in a subsequent `searchRequest`, in the format `\"{field}:\\\"{aggregationFilterToken}\\\"\"`. + AggregationFilterToken *string `json:"aggregationFilterToken,omitempty"` + // Nested aggregation results, one per sub-aggregation requested on the parent `aggregationOption`. Libregraph extension not present in MS Graph. + SubAggregations []SearchAggregation `json:"subAggregations,omitempty"` +} + +// NewSearchBucket instantiates a new SearchBucket object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchBucket() *SearchBucket { + this := SearchBucket{} + return &this +} + +// NewSearchBucketWithDefaults instantiates a new SearchBucket object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchBucketWithDefaults() *SearchBucket { + this := SearchBucket{} + return &this +} + +// GetKey returns the Key field value if set, zero value otherwise. +func (o *SearchBucket) GetKey() string { + if o == nil || IsNil(o.Key) { + var ret string + return ret + } + return *o.Key +} + +// GetKeyOk returns a tuple with the Key field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchBucket) GetKeyOk() (*string, bool) { + if o == nil || IsNil(o.Key) { + return nil, false + } + return o.Key, true +} + +// HasKey returns a boolean if a field has been set. +func (o *SearchBucket) HasKey() bool { + if o != nil && !IsNil(o.Key) { + return true + } + + return false +} + +// SetKey gets a reference to the given string and assigns it to the Key field. +func (o *SearchBucket) SetKey(v string) { + o.Key = &v +} + +// GetCount returns the Count field value if set, zero value otherwise. +func (o *SearchBucket) GetCount() int64 { + if o == nil || IsNil(o.Count) { + var ret int64 + return ret + } + return *o.Count +} + +// GetCountOk returns a tuple with the Count field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchBucket) GetCountOk() (*int64, bool) { + if o == nil || IsNil(o.Count) { + return nil, false + } + return o.Count, true +} + +// HasCount returns a boolean if a field has been set. +func (o *SearchBucket) HasCount() bool { + if o != nil && !IsNil(o.Count) { + return true + } + + return false +} + +// SetCount gets a reference to the given int64 and assigns it to the Count field. +func (o *SearchBucket) SetCount(v int64) { + o.Count = &v +} + +// GetAggregationFilterToken returns the AggregationFilterToken field value if set, zero value otherwise. +func (o *SearchBucket) GetAggregationFilterToken() string { + if o == nil || IsNil(o.AggregationFilterToken) { + var ret string + return ret + } + return *o.AggregationFilterToken +} + +// GetAggregationFilterTokenOk returns a tuple with the AggregationFilterToken field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchBucket) GetAggregationFilterTokenOk() (*string, bool) { + if o == nil || IsNil(o.AggregationFilterToken) { + return nil, false + } + return o.AggregationFilterToken, true +} + +// HasAggregationFilterToken returns a boolean if a field has been set. +func (o *SearchBucket) HasAggregationFilterToken() bool { + if o != nil && !IsNil(o.AggregationFilterToken) { + return true + } + + return false +} + +// SetAggregationFilterToken gets a reference to the given string and assigns it to the AggregationFilterToken field. +func (o *SearchBucket) SetAggregationFilterToken(v string) { + o.AggregationFilterToken = &v +} + +// GetSubAggregations returns the SubAggregations field value if set, zero value otherwise. +func (o *SearchBucket) GetSubAggregations() []SearchAggregation { + if o == nil || IsNil(o.SubAggregations) { + var ret []SearchAggregation + return ret + } + return o.SubAggregations +} + +// GetSubAggregationsOk returns a tuple with the SubAggregations field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchBucket) GetSubAggregationsOk() ([]SearchAggregation, bool) { + if o == nil || IsNil(o.SubAggregations) { + return nil, false + } + return o.SubAggregations, true +} + +// HasSubAggregations returns a boolean if a field has been set. +func (o *SearchBucket) HasSubAggregations() bool { + if o != nil && !IsNil(o.SubAggregations) { + return true + } + + return false +} + +// SetSubAggregations gets a reference to the given []SearchAggregation and assigns it to the SubAggregations field. +func (o *SearchBucket) SetSubAggregations(v []SearchAggregation) { + o.SubAggregations = v +} + +func (o SearchBucket) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchBucket) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Key) { + toSerialize["key"] = o.Key + } + if !IsNil(o.Count) { + toSerialize["count"] = o.Count + } + if !IsNil(o.AggregationFilterToken) { + toSerialize["aggregationFilterToken"] = o.AggregationFilterToken + } + if !IsNil(o.SubAggregations) { + toSerialize["subAggregations"] = o.SubAggregations + } + return toSerialize, nil +} + +type NullableSearchBucket struct { + value *SearchBucket + isSet bool +} + +func (v NullableSearchBucket) Get() *SearchBucket { + return v.value +} + +func (v *NullableSearchBucket) Set(val *SearchBucket) { + v.value = val + v.isSet = true +} + +func (v NullableSearchBucket) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchBucket) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchBucket(val *SearchBucket) *NullableSearchBucket { + return &NullableSearchBucket{value: val, isSet: true} +} + +func (v NullableSearchBucket) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchBucket) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_hit.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_hit.go new file mode 100644 index 0000000000..393a273630 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_hit.go @@ -0,0 +1,237 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the SearchHit type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchHit{} + +// SearchHit Represents an individual search result. Follows the [MS Graph searchHit](https://learn.microsoft.com/en-us/graph/api/resources/searchhit) resource type. +type SearchHit struct { + // The internal identifier for the item. + HitId *string `json:"hitId,omitempty"` + // The rank or the order of the result. + Rank *int32 `json:"rank,omitempty"` + // A summary of the result, if a summary is available. + Summary *string `json:"summary,omitempty"` + Resource *DriveItem `json:"resource,omitempty"` +} + +// NewSearchHit instantiates a new SearchHit object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchHit() *SearchHit { + this := SearchHit{} + return &this +} + +// NewSearchHitWithDefaults instantiates a new SearchHit object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchHitWithDefaults() *SearchHit { + this := SearchHit{} + return &this +} + +// GetHitId returns the HitId field value if set, zero value otherwise. +func (o *SearchHit) GetHitId() string { + if o == nil || IsNil(o.HitId) { + var ret string + return ret + } + return *o.HitId +} + +// GetHitIdOk returns a tuple with the HitId field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHit) GetHitIdOk() (*string, bool) { + if o == nil || IsNil(o.HitId) { + return nil, false + } + return o.HitId, true +} + +// HasHitId returns a boolean if a field has been set. +func (o *SearchHit) HasHitId() bool { + if o != nil && !IsNil(o.HitId) { + return true + } + + return false +} + +// SetHitId gets a reference to the given string and assigns it to the HitId field. +func (o *SearchHit) SetHitId(v string) { + o.HitId = &v +} + +// GetRank returns the Rank field value if set, zero value otherwise. +func (o *SearchHit) GetRank() int32 { + if o == nil || IsNil(o.Rank) { + var ret int32 + return ret + } + return *o.Rank +} + +// GetRankOk returns a tuple with the Rank field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHit) GetRankOk() (*int32, bool) { + if o == nil || IsNil(o.Rank) { + return nil, false + } + return o.Rank, true +} + +// HasRank returns a boolean if a field has been set. +func (o *SearchHit) HasRank() bool { + if o != nil && !IsNil(o.Rank) { + return true + } + + return false +} + +// SetRank gets a reference to the given int32 and assigns it to the Rank field. +func (o *SearchHit) SetRank(v int32) { + o.Rank = &v +} + +// GetSummary returns the Summary field value if set, zero value otherwise. +func (o *SearchHit) GetSummary() string { + if o == nil || IsNil(o.Summary) { + var ret string + return ret + } + return *o.Summary +} + +// GetSummaryOk returns a tuple with the Summary field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHit) GetSummaryOk() (*string, bool) { + if o == nil || IsNil(o.Summary) { + return nil, false + } + return o.Summary, true +} + +// HasSummary returns a boolean if a field has been set. +func (o *SearchHit) HasSummary() bool { + if o != nil && !IsNil(o.Summary) { + return true + } + + return false +} + +// SetSummary gets a reference to the given string and assigns it to the Summary field. +func (o *SearchHit) SetSummary(v string) { + o.Summary = &v +} + +// GetResource returns the Resource field value if set, zero value otherwise. +func (o *SearchHit) GetResource() DriveItem { + if o == nil || IsNil(o.Resource) { + var ret DriveItem + return ret + } + return *o.Resource +} + +// GetResourceOk returns a tuple with the Resource field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHit) GetResourceOk() (*DriveItem, bool) { + if o == nil || IsNil(o.Resource) { + return nil, false + } + return o.Resource, true +} + +// HasResource returns a boolean if a field has been set. +func (o *SearchHit) HasResource() bool { + if o != nil && !IsNil(o.Resource) { + return true + } + + return false +} + +// SetResource gets a reference to the given DriveItem and assigns it to the Resource field. +func (o *SearchHit) SetResource(v DriveItem) { + o.Resource = &v +} + +func (o SearchHit) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchHit) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.HitId) { + toSerialize["hitId"] = o.HitId + } + if !IsNil(o.Rank) { + toSerialize["rank"] = o.Rank + } + if !IsNil(o.Summary) { + toSerialize["summary"] = o.Summary + } + if !IsNil(o.Resource) { + toSerialize["resource"] = o.Resource + } + return toSerialize, nil +} + +type NullableSearchHit struct { + value *SearchHit + isSet bool +} + +func (v NullableSearchHit) Get() *SearchHit { + return v.value +} + +func (v *NullableSearchHit) Set(val *SearchHit) { + v.value = val + v.isSet = true +} + +func (v NullableSearchHit) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchHit) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchHit(val *SearchHit) *NullableSearchHit { + return &NullableSearchHit{value: val, isSet: true} +} + +func (v NullableSearchHit) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchHit) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_hits_container.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_hits_container.go new file mode 100644 index 0000000000..7e549d3b09 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_hits_container.go @@ -0,0 +1,238 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the SearchHitsContainer type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchHitsContainer{} + +// SearchHitsContainer Contains a collection of search results. Follows the [MS Graph searchHitsContainer](https://learn.microsoft.com/en-us/graph/api/resources/searchhitscontainer) resource type. +type SearchHitsContainer struct { + // A collection of the search results. + Hits []SearchHit `json:"hits,omitempty"` + // The total number of results. Note this is not the number of results on the page, but the total number of results satisfying the query. + Total *int64 `json:"total,omitempty"` + // Provides information if more results are available. Based on this information, you can adjust the `from` and `size` properties of the `searchRequest` accordingly. + MoreResultsAvailable *bool `json:"moreResultsAvailable,omitempty"` + // Contains the collection of aggregations computed based on the provided `aggregationOption` definitions in the request. + Aggregations []SearchAggregation `json:"aggregations,omitempty"` +} + +// NewSearchHitsContainer instantiates a new SearchHitsContainer object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchHitsContainer() *SearchHitsContainer { + this := SearchHitsContainer{} + return &this +} + +// NewSearchHitsContainerWithDefaults instantiates a new SearchHitsContainer object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchHitsContainerWithDefaults() *SearchHitsContainer { + this := SearchHitsContainer{} + return &this +} + +// GetHits returns the Hits field value if set, zero value otherwise. +func (o *SearchHitsContainer) GetHits() []SearchHit { + if o == nil || IsNil(o.Hits) { + var ret []SearchHit + return ret + } + return o.Hits +} + +// GetHitsOk returns a tuple with the Hits field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHitsContainer) GetHitsOk() ([]SearchHit, bool) { + if o == nil || IsNil(o.Hits) { + return nil, false + } + return o.Hits, true +} + +// HasHits returns a boolean if a field has been set. +func (o *SearchHitsContainer) HasHits() bool { + if o != nil && !IsNil(o.Hits) { + return true + } + + return false +} + +// SetHits gets a reference to the given []SearchHit and assigns it to the Hits field. +func (o *SearchHitsContainer) SetHits(v []SearchHit) { + o.Hits = v +} + +// GetTotal returns the Total field value if set, zero value otherwise. +func (o *SearchHitsContainer) GetTotal() int64 { + if o == nil || IsNil(o.Total) { + var ret int64 + return ret + } + return *o.Total +} + +// GetTotalOk returns a tuple with the Total field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHitsContainer) GetTotalOk() (*int64, bool) { + if o == nil || IsNil(o.Total) { + return nil, false + } + return o.Total, true +} + +// HasTotal returns a boolean if a field has been set. +func (o *SearchHitsContainer) HasTotal() bool { + if o != nil && !IsNil(o.Total) { + return true + } + + return false +} + +// SetTotal gets a reference to the given int64 and assigns it to the Total field. +func (o *SearchHitsContainer) SetTotal(v int64) { + o.Total = &v +} + +// GetMoreResultsAvailable returns the MoreResultsAvailable field value if set, zero value otherwise. +func (o *SearchHitsContainer) GetMoreResultsAvailable() bool { + if o == nil || IsNil(o.MoreResultsAvailable) { + var ret bool + return ret + } + return *o.MoreResultsAvailable +} + +// GetMoreResultsAvailableOk returns a tuple with the MoreResultsAvailable field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHitsContainer) GetMoreResultsAvailableOk() (*bool, bool) { + if o == nil || IsNil(o.MoreResultsAvailable) { + return nil, false + } + return o.MoreResultsAvailable, true +} + +// HasMoreResultsAvailable returns a boolean if a field has been set. +func (o *SearchHitsContainer) HasMoreResultsAvailable() bool { + if o != nil && !IsNil(o.MoreResultsAvailable) { + return true + } + + return false +} + +// SetMoreResultsAvailable gets a reference to the given bool and assigns it to the MoreResultsAvailable field. +func (o *SearchHitsContainer) SetMoreResultsAvailable(v bool) { + o.MoreResultsAvailable = &v +} + +// GetAggregations returns the Aggregations field value if set, zero value otherwise. +func (o *SearchHitsContainer) GetAggregations() []SearchAggregation { + if o == nil || IsNil(o.Aggregations) { + var ret []SearchAggregation + return ret + } + return o.Aggregations +} + +// GetAggregationsOk returns a tuple with the Aggregations field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchHitsContainer) GetAggregationsOk() ([]SearchAggregation, bool) { + if o == nil || IsNil(o.Aggregations) { + return nil, false + } + return o.Aggregations, true +} + +// HasAggregations returns a boolean if a field has been set. +func (o *SearchHitsContainer) HasAggregations() bool { + if o != nil && !IsNil(o.Aggregations) { + return true + } + + return false +} + +// SetAggregations gets a reference to the given []SearchAggregation and assigns it to the Aggregations field. +func (o *SearchHitsContainer) SetAggregations(v []SearchAggregation) { + o.Aggregations = v +} + +func (o SearchHitsContainer) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchHitsContainer) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Hits) { + toSerialize["hits"] = o.Hits + } + if !IsNil(o.Total) { + toSerialize["total"] = o.Total + } + if !IsNil(o.MoreResultsAvailable) { + toSerialize["moreResultsAvailable"] = o.MoreResultsAvailable + } + if !IsNil(o.Aggregations) { + toSerialize["aggregations"] = o.Aggregations + } + return toSerialize, nil +} + +type NullableSearchHitsContainer struct { + value *SearchHitsContainer + isSet bool +} + +func (v NullableSearchHitsContainer) Get() *SearchHitsContainer { + return v.value +} + +func (v *NullableSearchHitsContainer) Set(val *SearchHitsContainer) { + v.value = val + v.isSet = true +} + +func (v NullableSearchHitsContainer) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchHitsContainer) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchHitsContainer(val *SearchHitsContainer) *NullableSearchHitsContainer { + return &NullableSearchHitsContainer{value: val, isSet: true} +} + +func (v NullableSearchHitsContainer) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchHitsContainer) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query.go new file mode 100644 index 0000000000..8eada60d93 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query.go @@ -0,0 +1,159 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" + "bytes" + "fmt" +) + +// checks if the SearchQuery type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchQuery{} + +// SearchQuery Represents the search query. Follows the [MS Graph searchQuery](https://learn.microsoft.com/en-us/graph/api/resources/searchquery) resource type. +type SearchQuery struct { + // The search query string in KQL (Keyword Query Language) format. The query string can contain free-text keywords and property filters. Examples: - `budget report` — free text search - `mediatype:audio` — filter by media type - `audio.artist:\"Pink Floyd\"` — filter by audio metadata - `audio.genre:Rock AND audio.year:1979` — combined filters + QueryString string `json:"queryString"` +} + +type _SearchQuery SearchQuery + +// NewSearchQuery instantiates a new SearchQuery object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchQuery(queryString string) *SearchQuery { + this := SearchQuery{} + this.QueryString = queryString + return &this +} + +// NewSearchQueryWithDefaults instantiates a new SearchQuery object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchQueryWithDefaults() *SearchQuery { + this := SearchQuery{} + return &this +} + +// GetQueryString returns the QueryString field value +func (o *SearchQuery) GetQueryString() string { + if o == nil { + var ret string + return ret + } + + return o.QueryString +} + +// GetQueryStringOk returns a tuple with the QueryString field value +// and a boolean to check if the value has been set. +func (o *SearchQuery) GetQueryStringOk() (*string, bool) { + if o == nil { + return nil, false + } + return &o.QueryString, true +} + +// SetQueryString sets field value +func (o *SearchQuery) SetQueryString(v string) { + o.QueryString = v +} + +func (o SearchQuery) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchQuery) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["queryString"] = o.QueryString + return toSerialize, nil +} + +func (o *SearchQuery) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "queryString", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err; + } + + for _, requiredProperty := range(requiredProperties) { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSearchQuery := _SearchQuery{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varSearchQuery) + + if err != nil { + return err + } + + *o = SearchQuery(varSearchQuery) + + return err +} + +type NullableSearchQuery struct { + value *SearchQuery + isSet bool +} + +func (v NullableSearchQuery) Get() *SearchQuery { + return v.value +} + +func (v *NullableSearchQuery) Set(val *SearchQuery) { + v.value = val + v.isSet = true +} + +func (v NullableSearchQuery) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchQuery) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchQuery(val *SearchQuery) *NullableSearchQuery { + return &NullableSearchQuery{value: val, isSet: true} +} + +func (v NullableSearchQuery) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchQuery) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query_200_response.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query_200_response.go new file mode 100644 index 0000000000..7a2fef6ac9 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query_200_response.go @@ -0,0 +1,127 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the SearchQuery200Response type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchQuery200Response{} + +// SearchQuery200Response struct for SearchQuery200Response +type SearchQuery200Response struct { + // A collection of search response objects, one per request. + Value []SearchResponse `json:"value,omitempty"` +} + +// NewSearchQuery200Response instantiates a new SearchQuery200Response object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchQuery200Response() *SearchQuery200Response { + this := SearchQuery200Response{} + return &this +} + +// NewSearchQuery200ResponseWithDefaults instantiates a new SearchQuery200Response object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchQuery200ResponseWithDefaults() *SearchQuery200Response { + this := SearchQuery200Response{} + return &this +} + +// GetValue returns the Value field value if set, zero value otherwise. +func (o *SearchQuery200Response) GetValue() []SearchResponse { + if o == nil || IsNil(o.Value) { + var ret []SearchResponse + return ret + } + return o.Value +} + +// GetValueOk returns a tuple with the Value field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchQuery200Response) GetValueOk() ([]SearchResponse, bool) { + if o == nil || IsNil(o.Value) { + return nil, false + } + return o.Value, true +} + +// HasValue returns a boolean if a field has been set. +func (o *SearchQuery200Response) HasValue() bool { + if o != nil && !IsNil(o.Value) { + return true + } + + return false +} + +// SetValue gets a reference to the given []SearchResponse and assigns it to the Value field. +func (o *SearchQuery200Response) SetValue(v []SearchResponse) { + o.Value = v +} + +func (o SearchQuery200Response) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchQuery200Response) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.Value) { + toSerialize["value"] = o.Value + } + return toSerialize, nil +} + +type NullableSearchQuery200Response struct { + value *SearchQuery200Response + isSet bool +} + +func (v NullableSearchQuery200Response) Get() *SearchQuery200Response { + return v.value +} + +func (v *NullableSearchQuery200Response) Set(val *SearchQuery200Response) { + v.value = val + v.isSet = true +} + +func (v NullableSearchQuery200Response) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchQuery200Response) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchQuery200Response(val *SearchQuery200Response) *NullableSearchQuery200Response { + return &NullableSearchQuery200Response{value: val, isSet: true} +} + +func (v NullableSearchQuery200Response) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchQuery200Response) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query_request.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query_request.go new file mode 100644 index 0000000000..f74fed3f76 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_query_request.go @@ -0,0 +1,159 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" + "bytes" + "fmt" +) + +// checks if the SearchQueryRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchQueryRequest{} + +// SearchQueryRequest struct for SearchQueryRequest +type SearchQueryRequest struct { + // A collection of one or more search requests. + Requests []SearchRequest `json:"requests"` +} + +type _SearchQueryRequest SearchQueryRequest + +// NewSearchQueryRequest instantiates a new SearchQueryRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchQueryRequest(requests []SearchRequest) *SearchQueryRequest { + this := SearchQueryRequest{} + this.Requests = requests + return &this +} + +// NewSearchQueryRequestWithDefaults instantiates a new SearchQueryRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchQueryRequestWithDefaults() *SearchQueryRequest { + this := SearchQueryRequest{} + return &this +} + +// GetRequests returns the Requests field value +func (o *SearchQueryRequest) GetRequests() []SearchRequest { + if o == nil { + var ret []SearchRequest + return ret + } + + return o.Requests +} + +// GetRequestsOk returns a tuple with the Requests field value +// and a boolean to check if the value has been set. +func (o *SearchQueryRequest) GetRequestsOk() ([]SearchRequest, bool) { + if o == nil { + return nil, false + } + return o.Requests, true +} + +// SetRequests sets field value +func (o *SearchQueryRequest) SetRequests(v []SearchRequest) { + o.Requests = v +} + +func (o SearchQueryRequest) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchQueryRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["requests"] = o.Requests + return toSerialize, nil +} + +func (o *SearchQueryRequest) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "requests", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err; + } + + for _, requiredProperty := range(requiredProperties) { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSearchQueryRequest := _SearchQueryRequest{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varSearchQueryRequest) + + if err != nil { + return err + } + + *o = SearchQueryRequest(varSearchQueryRequest) + + return err +} + +type NullableSearchQueryRequest struct { + value *SearchQueryRequest + isSet bool +} + +func (v NullableSearchQueryRequest) Get() *SearchQueryRequest { + return v.value +} + +func (v *NullableSearchQueryRequest) Set(val *SearchQueryRequest) { + v.value = val + v.isSet = true +} + +func (v NullableSearchQueryRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchQueryRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchQueryRequest(val *SearchQueryRequest) *NullableSearchQueryRequest { + return &NullableSearchQueryRequest{value: val, isSet: true} +} + +func (v NullableSearchQueryRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchQueryRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_request.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_request.go new file mode 100644 index 0000000000..5d17d3f37b --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_request.go @@ -0,0 +1,343 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" + "bytes" + "fmt" +) + +// checks if the SearchRequest type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchRequest{} + +// SearchRequest Represents an individual search request within a search query. Follows the [MS Graph searchRequest](https://learn.microsoft.com/en-us/graph/api/resources/searchrequest) resource type. +type SearchRequest struct { + // One or more types of resources expected in the response. Currently only `driveItem` is supported. + EntityTypes []string `json:"entityTypes"` + Query SearchQuery `json:"query"` + // Specifies the offset for the search results. Offset 0 returns the very first result. Used together with the `size` property for pagination. + From *int32 `json:"from,omitempty"` + // The size of the page to be retrieved. The maximum value is 500. Set to 0 to return only aggregations without any hits. + Size *int32 `json:"size,omitempty"` + // Specifies aggregations (also known as refiners or facets) to be returned alongside the search results. Optional. + Aggregations []AggregationOption `json:"aggregations,omitempty"` + // Contains one or more filters to obtain search results narrowed down to a specific value of a field. Build this filter based on a prior search that aggregates by the same field. From the response of the prior search, identify the `searchBucket` that filters results to the specific value of the field, use the string in its `aggregationFilterToken` property, and build an aggregation filter string in the format `\"{field}:\\\"{aggregationFilterToken}\\\"\"`. Multiple filters can be provided as separate array items. This results in a logical AND between the filters. + AggregationFilters []string `json:"aggregationFilters,omitempty"` +} + +type _SearchRequest SearchRequest + +// NewSearchRequest instantiates a new SearchRequest object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchRequest(entityTypes []string, query SearchQuery) *SearchRequest { + this := SearchRequest{} + this.EntityTypes = entityTypes + this.Query = query + var from int32 = 0 + this.From = &from + var size int32 = 25 + this.Size = &size + return &this +} + +// NewSearchRequestWithDefaults instantiates a new SearchRequest object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchRequestWithDefaults() *SearchRequest { + this := SearchRequest{} + var from int32 = 0 + this.From = &from + var size int32 = 25 + this.Size = &size + return &this +} + +// GetEntityTypes returns the EntityTypes field value +func (o *SearchRequest) GetEntityTypes() []string { + if o == nil { + var ret []string + return ret + } + + return o.EntityTypes +} + +// GetEntityTypesOk returns a tuple with the EntityTypes field value +// and a boolean to check if the value has been set. +func (o *SearchRequest) GetEntityTypesOk() ([]string, bool) { + if o == nil { + return nil, false + } + return o.EntityTypes, true +} + +// SetEntityTypes sets field value +func (o *SearchRequest) SetEntityTypes(v []string) { + o.EntityTypes = v +} + +// GetQuery returns the Query field value +func (o *SearchRequest) GetQuery() SearchQuery { + if o == nil { + var ret SearchQuery + return ret + } + + return o.Query +} + +// GetQueryOk returns a tuple with the Query field value +// and a boolean to check if the value has been set. +func (o *SearchRequest) GetQueryOk() (*SearchQuery, bool) { + if o == nil { + return nil, false + } + return &o.Query, true +} + +// SetQuery sets field value +func (o *SearchRequest) SetQuery(v SearchQuery) { + o.Query = v +} + +// GetFrom returns the From field value if set, zero value otherwise. +func (o *SearchRequest) GetFrom() int32 { + if o == nil || IsNil(o.From) { + var ret int32 + return ret + } + return *o.From +} + +// GetFromOk returns a tuple with the From field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchRequest) GetFromOk() (*int32, bool) { + if o == nil || IsNil(o.From) { + return nil, false + } + return o.From, true +} + +// HasFrom returns a boolean if a field has been set. +func (o *SearchRequest) HasFrom() bool { + if o != nil && !IsNil(o.From) { + return true + } + + return false +} + +// SetFrom gets a reference to the given int32 and assigns it to the From field. +func (o *SearchRequest) SetFrom(v int32) { + o.From = &v +} + +// GetSize returns the Size field value if set, zero value otherwise. +func (o *SearchRequest) GetSize() int32 { + if o == nil || IsNil(o.Size) { + var ret int32 + return ret + } + return *o.Size +} + +// GetSizeOk returns a tuple with the Size field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchRequest) GetSizeOk() (*int32, bool) { + if o == nil || IsNil(o.Size) { + return nil, false + } + return o.Size, true +} + +// HasSize returns a boolean if a field has been set. +func (o *SearchRequest) HasSize() bool { + if o != nil && !IsNil(o.Size) { + return true + } + + return false +} + +// SetSize gets a reference to the given int32 and assigns it to the Size field. +func (o *SearchRequest) SetSize(v int32) { + o.Size = &v +} + +// GetAggregations returns the Aggregations field value if set, zero value otherwise. +func (o *SearchRequest) GetAggregations() []AggregationOption { + if o == nil || IsNil(o.Aggregations) { + var ret []AggregationOption + return ret + } + return o.Aggregations +} + +// GetAggregationsOk returns a tuple with the Aggregations field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchRequest) GetAggregationsOk() ([]AggregationOption, bool) { + if o == nil || IsNil(o.Aggregations) { + return nil, false + } + return o.Aggregations, true +} + +// HasAggregations returns a boolean if a field has been set. +func (o *SearchRequest) HasAggregations() bool { + if o != nil && !IsNil(o.Aggregations) { + return true + } + + return false +} + +// SetAggregations gets a reference to the given []AggregationOption and assigns it to the Aggregations field. +func (o *SearchRequest) SetAggregations(v []AggregationOption) { + o.Aggregations = v +} + +// GetAggregationFilters returns the AggregationFilters field value if set, zero value otherwise. +func (o *SearchRequest) GetAggregationFilters() []string { + if o == nil || IsNil(o.AggregationFilters) { + var ret []string + return ret + } + return o.AggregationFilters +} + +// GetAggregationFiltersOk returns a tuple with the AggregationFilters field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchRequest) GetAggregationFiltersOk() ([]string, bool) { + if o == nil || IsNil(o.AggregationFilters) { + return nil, false + } + return o.AggregationFilters, true +} + +// HasAggregationFilters returns a boolean if a field has been set. +func (o *SearchRequest) HasAggregationFilters() bool { + if o != nil && !IsNil(o.AggregationFilters) { + return true + } + + return false +} + +// SetAggregationFilters gets a reference to the given []string and assigns it to the AggregationFilters field. +func (o *SearchRequest) SetAggregationFilters(v []string) { + o.AggregationFilters = v +} + +func (o SearchRequest) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchRequest) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + toSerialize["entityTypes"] = o.EntityTypes + toSerialize["query"] = o.Query + if !IsNil(o.From) { + toSerialize["from"] = o.From + } + if !IsNil(o.Size) { + toSerialize["size"] = o.Size + } + if !IsNil(o.Aggregations) { + toSerialize["aggregations"] = o.Aggregations + } + if !IsNil(o.AggregationFilters) { + toSerialize["aggregationFilters"] = o.AggregationFilters + } + return toSerialize, nil +} + +func (o *SearchRequest) UnmarshalJSON(data []byte) (err error) { + // This validates that all required properties are included in the JSON object + // by unmarshalling the object into a generic map with string keys and checking + // that every required field exists as a key in the generic map. + requiredProperties := []string{ + "entityTypes", + "query", + } + + allProperties := make(map[string]interface{}) + + err = json.Unmarshal(data, &allProperties) + + if err != nil { + return err; + } + + for _, requiredProperty := range(requiredProperties) { + if _, exists := allProperties[requiredProperty]; !exists { + return fmt.Errorf("no value given for required property %v", requiredProperty) + } + } + + varSearchRequest := _SearchRequest{} + + decoder := json.NewDecoder(bytes.NewReader(data)) + decoder.DisallowUnknownFields() + err = decoder.Decode(&varSearchRequest) + + if err != nil { + return err + } + + *o = SearchRequest(varSearchRequest) + + return err +} + +type NullableSearchRequest struct { + value *SearchRequest + isSet bool +} + +func (v NullableSearchRequest) Get() *SearchRequest { + return v.value +} + +func (v *NullableSearchRequest) Set(val *SearchRequest) { + v.value = val + v.isSet = true +} + +func (v NullableSearchRequest) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchRequest) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchRequest(val *SearchRequest) *NullableSearchRequest { + return &NullableSearchRequest{value: val, isSet: true} +} + +func (v NullableSearchRequest) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchRequest) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + + diff --git a/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_response.go b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_response.go new file mode 100644 index 0000000000..a148990531 --- /dev/null +++ b/vendor/github.com/opencloud-eu/libre-graph-api-go/model_search_response.go @@ -0,0 +1,164 @@ +/* +Libre Graph API + +Libre Graph is a free API for cloud collaboration inspired by the MS Graph API. + +API version: v1.0.8 +*/ + +// Code generated by OpenAPI Generator (https://openapi-generator.tech); DO NOT EDIT. + +package libregraph + +import ( + "encoding/json" +) + +// checks if the SearchResponse type satisfies the MappedNullable interface at compile time +var _ MappedNullable = &SearchResponse{} + +// SearchResponse Represents the response for an individual search request. Follows the [MS Graph searchResponse](https://learn.microsoft.com/en-us/graph/api/resources/searchresponse) resource type. +type SearchResponse struct { + // Contains the search terms sent in the initial search query. + SearchTerms []string `json:"searchTerms,omitempty"` + // A collection of search result sets. One for each entity type that was queried. + HitsContainers []SearchHitsContainer `json:"hitsContainers,omitempty"` +} + +// NewSearchResponse instantiates a new SearchResponse object +// This constructor will assign default values to properties that have it defined, +// and makes sure properties required by API are set, but the set of arguments +// will change when the set of required properties is changed +func NewSearchResponse() *SearchResponse { + this := SearchResponse{} + return &this +} + +// NewSearchResponseWithDefaults instantiates a new SearchResponse object +// This constructor will only assign default values to properties that have it defined, +// but it doesn't guarantee that properties required by API are set +func NewSearchResponseWithDefaults() *SearchResponse { + this := SearchResponse{} + return &this +} + +// GetSearchTerms returns the SearchTerms field value if set, zero value otherwise. +func (o *SearchResponse) GetSearchTerms() []string { + if o == nil || IsNil(o.SearchTerms) { + var ret []string + return ret + } + return o.SearchTerms +} + +// GetSearchTermsOk returns a tuple with the SearchTerms field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchResponse) GetSearchTermsOk() ([]string, bool) { + if o == nil || IsNil(o.SearchTerms) { + return nil, false + } + return o.SearchTerms, true +} + +// HasSearchTerms returns a boolean if a field has been set. +func (o *SearchResponse) HasSearchTerms() bool { + if o != nil && !IsNil(o.SearchTerms) { + return true + } + + return false +} + +// SetSearchTerms gets a reference to the given []string and assigns it to the SearchTerms field. +func (o *SearchResponse) SetSearchTerms(v []string) { + o.SearchTerms = v +} + +// GetHitsContainers returns the HitsContainers field value if set, zero value otherwise. +func (o *SearchResponse) GetHitsContainers() []SearchHitsContainer { + if o == nil || IsNil(o.HitsContainers) { + var ret []SearchHitsContainer + return ret + } + return o.HitsContainers +} + +// GetHitsContainersOk returns a tuple with the HitsContainers field value if set, nil otherwise +// and a boolean to check if the value has been set. +func (o *SearchResponse) GetHitsContainersOk() ([]SearchHitsContainer, bool) { + if o == nil || IsNil(o.HitsContainers) { + return nil, false + } + return o.HitsContainers, true +} + +// HasHitsContainers returns a boolean if a field has been set. +func (o *SearchResponse) HasHitsContainers() bool { + if o != nil && !IsNil(o.HitsContainers) { + return true + } + + return false +} + +// SetHitsContainers gets a reference to the given []SearchHitsContainer and assigns it to the HitsContainers field. +func (o *SearchResponse) SetHitsContainers(v []SearchHitsContainer) { + o.HitsContainers = v +} + +func (o SearchResponse) MarshalJSON() ([]byte, error) { + toSerialize,err := o.ToMap() + if err != nil { + return []byte{}, err + } + return json.Marshal(toSerialize) +} + +func (o SearchResponse) ToMap() (map[string]interface{}, error) { + toSerialize := map[string]interface{}{} + if !IsNil(o.SearchTerms) { + toSerialize["searchTerms"] = o.SearchTerms + } + if !IsNil(o.HitsContainers) { + toSerialize["hitsContainers"] = o.HitsContainers + } + return toSerialize, nil +} + +type NullableSearchResponse struct { + value *SearchResponse + isSet bool +} + +func (v NullableSearchResponse) Get() *SearchResponse { + return v.value +} + +func (v *NullableSearchResponse) Set(val *SearchResponse) { + v.value = val + v.isSet = true +} + +func (v NullableSearchResponse) IsSet() bool { + return v.isSet +} + +func (v *NullableSearchResponse) Unset() { + v.value = nil + v.isSet = false +} + +func NewNullableSearchResponse(val *SearchResponse) *NullableSearchResponse { + return &NullableSearchResponse{value: val, isSet: true} +} + +func (v NullableSearchResponse) MarshalJSON() ([]byte, error) { + return json.Marshal(v.value) +} + +func (v *NullableSearchResponse) UnmarshalJSON(src []byte) error { + v.isSet = true + return json.Unmarshal(src, &v.value) +} + +