From 492705f59983bde78399acb10aa1e20630227212 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Fri, 10 Apr 2026 11:49:29 +0200 Subject: [PATCH 01/17] Add search query endpoint with request examples Added a new search endpoint for querying resources with detailed request and response structures, including examples for various search scenarios. Based on the MS Graph Search Api of course: https://learn.microsoft.com/en-us/graph/api/resources/search-api-overview?view=graph-rest-1.0 --- api/openapi-spec/v1.0.yaml | 453 +++++++++++++++++++++++++++++++++++++ 1 file changed, 453 insertions(+) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 6e10357..9ffcf61 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4541,6 +4541,158 @@ paths: '@libre.graph.weight': 4 default: $ref: '#/components/responses/error' + /v1beta1/search/query: + post: + tags: + - search + summary: Search for resources + operationId: SearchQuery + description: | + 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. + requestBody: + required: true + content: + application/json: + schema: + type: object + required: + - requests + properties: + requests: + type: array + description: A collection of one or more search requests. + items: + $ref: '#/components/schemas/searchRequest' + examples: + simple search: + summary: Simple keyword search + value: + requests: + - entityTypes: + - driveItem + query: + queryString: budget report + from: 0 + size: 25 + search with aggregations: + summary: Search with term aggregation + value: + requests: + - entityTypes: + - driveItem + query: + queryString: "mediatype:audio" + from: 0 + size: 0 + aggregations: + - field: audio.artist + size: 10 + bucketDefinition: + sortBy: count + isDescending: true + minimumCount: 1 + search with range aggregation: + summary: Search with numeric range aggregation + value: + requests: + - entityTypes: + - driveItem + query: + queryString: "mediatype:audio" + size: 0 + aggregations: + - field: audio.year + bucketDefinition: + sortBy: keyAsString + isDescending: true + minimumCount: 0 + ranges: + - from: "1970" + to: "1980" + - from: "1980" + to: "1990" + - from: "1990" + to: "2000" + - from: "2000" + to: "2010" + - from: "2010" + search with aggregation filter: + summary: Narrow results using an aggregation filter from a previous search + value: + requests: + - entityTypes: + - driveItem + query: + queryString: "mediatype:audio" + from: 0 + size: 25 + aggregationFilters: + - "audio.artist:\"Pink Floyd\"" + responses: + '200': + description: OK + content: + application/json: + schema: + type: object + properties: + value: + type: array + description: A collection of search response objects, one per request. + items: + $ref: '#/components/schemas/searchResponse' + examples: + search results with aggregations: + value: + value: + - searchTerms: + - "mediatype:audio" + hitsContainers: + - hits: + - hitId: "4b478257-c9e8-458a-b764-fd7ed6cc1087$5d58a5c3-b244-4e9c-b082-82c3f1adbe8a!9d94e3d7" + rank: 1 + resource: + name: Comfortably Numb.flac + size: 45678901 + audio: + artist: Pink Floyd + album: The Wall + title: Comfortably Numb + genre: Rock + year: 1979 + track: 6 + disc: 2 + duration: 382000 + total: 142 + moreResultsAvailable: true + aggregations: + - field: audio.artist + buckets: + - key: Pink Floyd + count: 42 + aggregationFilterToken: Pink Floyd + - key: Led Zeppelin + count: 35 + aggregationFilterToken: Led Zeppelin + - key: The Beatles + count: 28 + aggregationFilterToken: The Beatles + default: + $ref: '#/components/responses/error' components: schemas: tagAssignment: @@ -6334,6 +6486,307 @@ components: # type: array # items: # type: string + searchRequest: + type: object + description: | + 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. + required: + - entityTypes + - query + properties: + entityTypes: + type: array + description: | + One or more types of resources expected in the response. Currently + only `driveItem` is supported. + items: + type: string + enum: + - driveItem + query: + $ref: '#/components/schemas/searchQuery' + from: + type: integer + format: int32 + description: | + Specifies the offset for the search results. Offset 0 returns the + very first result. Used together with the `size` property for + pagination. + default: 0 + minimum: 0 + size: + type: integer + format: int32 + description: | + The size of the page to be retrieved. The maximum value is 500. + Set to 0 to return only aggregations without any hits. + default: 25 + minimum: 0 + maximum: 500 + aggregations: + type: array + description: | + Specifies aggregations (also known as refiners or facets) to be + returned alongside the search results. Optional. + items: + $ref: '#/components/schemas/aggregationOption' + aggregationFilters: + type: array + description: | + 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. + items: + type: string + searchQuery: + type: object + description: | + Represents the search query. Follows the + [MS Graph searchQuery](https://learn.microsoft.com/en-us/graph/api/resources/searchquery) + resource type. + required: + - queryString + properties: + queryString: + type: string + description: | + 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 + aggregationOption: + type: object + description: | + 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`. + required: + - field + - bucketDefinition + properties: + field: + type: string + description: | + 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`. + size: + type: integer + format: int32 + description: | + 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. + minimum: 1 + bucketDefinition: + $ref: '#/components/schemas/bucketDefinition' + bucketDefinition: + type: object + description: | + 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. + required: + - sortBy + - isDescending + properties: + sortBy: + type: string + description: | + 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. + enum: + - count + - keyAsString + - keyAsNumber + isDescending: + type: boolean + description: | + Set to `true` to specify the sort order as descending. The default + is `false`, with the sort order as ascending. Required. + default: false + minimumCount: + type: integer + format: int32 + description: | + 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. + minimum: 0 + default: 0 + ranges: + type: array + description: | + 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. + items: + $ref: '#/components/schemas/bucketAggregationRange' + bucketAggregationRange: + type: object + description: | + Specifies the lower and upper bound to compute a range aggregation + bucket. At least one of `from` or `to` must be provided. + properties: + from: + type: string + description: | + Defines the lower bound from which to compute the aggregation. This + can be a numeric value or a string representation of a date using + the format `YYYY-MM-DDTHH:mm:ssZ`. Optional if `to` is provided. + to: + type: string + description: | + Defines the upper bound up to which to compute the aggregation. This + can be a numeric value or a string representation of a date using + the format `YYYY-MM-DDTHH:mm:ssZ`. Optional if `from` is provided. + searchResponse: + type: object + description: | + 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. + properties: + searchTerms: + type: array + description: Contains the search terms sent in the initial search query. + items: + type: string + hitsContainers: + type: array + description: | + A collection of search result sets. One for each entity type that + was queried. + items: + $ref: '#/components/schemas/searchHitsContainer' + searchHitsContainer: + type: object + description: | + Contains a collection of search results. Follows the + [MS Graph searchHitsContainer](https://learn.microsoft.com/en-us/graph/api/resources/searchhitscontainer) + resource type. + properties: + hits: + type: array + description: A collection of the search results. + items: + $ref: '#/components/schemas/searchHit' + total: + type: integer + format: int64 + description: | + 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. + moreResultsAvailable: + type: boolean + description: | + Provides information if more results are available. Based on this + information, you can adjust the `from` and `size` properties of the + `searchRequest` accordingly. + aggregations: + type: array + description: | + Contains the collection of aggregations computed based on the + provided `aggregationOption` definitions in the request. + items: + $ref: '#/components/schemas/searchAggregation' + searchHit: + type: object + description: | + Represents an individual search result. Follows the + [MS Graph searchHit](https://learn.microsoft.com/en-us/graph/api/resources/searchhit) + resource type. + properties: + hitId: + type: string + description: The internal identifier for the item. + readOnly: true + rank: + type: integer + format: int32 + description: The rank or the order of the result. + readOnly: true + summary: + type: string + description: | + A summary of the result, if a summary is available. + readOnly: true + resource: + $ref: '#/components/schemas/driveItem' + searchAggregation: + type: object + description: | + 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. + properties: + field: + type: string + description: | + Defines the field in the request on which the aggregation was + computed. + buckets: + type: array + description: | + Defines the computed buckets for this aggregation. Buckets are + sorted according to the `sortBy` and `isDescending` specified in + the `bucketDefinition` of the corresponding `aggregationOption`. + items: + $ref: '#/components/schemas/searchBucket' + searchBucket: + type: object + description: | + 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. + properties: + key: + type: string + description: | + 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. + count: + type: integer + format: int64 + description: | + The approximate number of search matches that share the same value + specified in the `key` property. + aggregationFilterToken: + type: string + description: | + 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}\""`. odata.error: required: - error From 1fee2255fc12ef810dd4279e89cd1a877aef9d3b Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 11 Apr 2026 09:58:46 +0200 Subject: [PATCH 02/17] fix: copilot review findings --- api/openapi-spec/v1.0.yaml | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 9ffcf61..e70f544 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4575,6 +4575,7 @@ paths: requests: type: array description: A collection of one or more search requests. + minItems: 1 items: $ref: '#/components/schemas/searchRequest' examples: @@ -6501,6 +6502,7 @@ components: description: | One or more types of resources expected in the response. Currently only `driveItem` is supported. + minItems: 1 items: type: string enum: @@ -6610,7 +6612,6 @@ components: resource type. required: - sortBy - - isDescending properties: sortBy: type: string @@ -6626,8 +6627,8 @@ components: isDescending: type: boolean description: | - Set to `true` to specify the sort order as descending. The default - is `false`, with the sort order as ascending. Required. + Set to `true` to specify the sort order as descending. Optional, + defaults to `false` (ascending). default: false minimumCount: type: integer @@ -6653,6 +6654,11 @@ components: description: | Specifies the lower and upper bound to compute a range aggregation bucket. At least one of `from` or `to` must be provided. + anyOf: + - required: + - from + - required: + - to properties: from: type: string From 308f6b382dc155d8d723264fe527c7a824a2fb4e Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 11 Apr 2026 10:15:27 +0200 Subject: [PATCH 03/17] fix: copilot findings --- api/openapi-spec/v1.0.yaml | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index e70f544..0e7db56 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6663,15 +6663,17 @@ components: from: type: string description: | - Defines the lower bound from which to compute the aggregation. This - can be a numeric value or a string representation of a date using - the format `YYYY-MM-DDTHH:mm:ssZ`. Optional if `to` is provided. + 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. to: type: string description: | - Defines the upper bound up to which to compute the aggregation. This - can be a numeric value or a string representation of a date using - the format `YYYY-MM-DDTHH:mm:ssZ`. Optional if `from` is provided. + 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. searchResponse: type: object description: | From 4573d58e2f5fb61b9026220a984afd1392eab25e Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Sat, 11 Apr 2026 10:37:50 +0200 Subject: [PATCH 04/17] fix: some more improvements --- api/openapi-spec/v1.0.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 0e7db56..1024335 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6582,7 +6582,6 @@ components: defined using the `ranges` property of `bucketDefinition`. required: - field - - bucketDefinition properties: field: type: string @@ -6711,12 +6710,14 @@ components: description: | 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. + readOnly: true moreResultsAvailable: type: boolean description: | Provides information if more results are available. Based on this information, you can adjust the `from` and `size` properties of the `searchRequest` accordingly. + readOnly: true aggregations: type: array description: | From 0c182644467b2ce5006aa232452568902c3df059 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Wed, 6 May 2026 17:45:17 +0200 Subject: [PATCH 05/17] feat(search): drop aggregationFilterToken, document KQL filter syntax The MS Graph token is opaque and varies by bucket type (hex-encoded for terms, range(...) for ranges, with different quoting rules). Rather than commit to that contract, omit the field and let clients construct filters directly from field + key (or the original range definition). --- api/openapi-spec/v1.0.yaml | 24 ++++++------------------ 1 file changed, 6 insertions(+), 18 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 1024335..207c90d 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4685,13 +4685,10 @@ paths: buckets: - key: Pink Floyd count: 42 - aggregationFilterToken: Pink Floyd - key: Led Zeppelin count: 35 - aggregationFilterToken: Led Zeppelin - key: The Beatles count: 28 - aggregationFilterToken: The Beatles default: $ref: '#/components/responses/error' components: @@ -6537,13 +6534,12 @@ components: aggregationFilters: type: array description: | - 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}\""`. + Contains one or more filters to narrow search results to specific + values of a field. Each filter is a KQL expression of the form + `{field}:"{value}"` for term buckets, or + `{field}:range({from},{to})` for range buckets. Field and value + should match those returned in `searchBucket.key` and the original + `aggregationOption`. Multiple filters can be provided as separate array items. This results in a logical AND between the filters. @@ -6788,14 +6784,6 @@ components: description: | The approximate number of search matches that share the same value specified in the `key` property. - aggregationFilterToken: - type: string - description: | - 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}\""`. odata.error: required: - error From 9e8aa1b378cf8287b92c7c033f2badc8f61bf80f Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 13 Aug 2026 00:40:05 +0200 Subject: [PATCH 06/17] feat(search): sub-aggregations on aggregationOption / searchBucket Adds `subAggregations` as an optional array on both the request (`aggregationOption`) and response (`searchBucket`) sides of the search aggregation spec. Lets callers nest term aggregations, e.g. "group by audio.artist, then by audio.album", evaluated in a single request. Explicit libregraph extension beyond MS Graph. Bleve backend emulates via client-side fold over matched hits; OpenSearch would translate to native composite aggregations. --- api/openapi-spec/v1.0.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 207c90d..48fe5a3 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6598,6 +6598,18 @@ components: minimum: 1 bucketDefinition: $ref: '#/components/schemas/bucketDefinition' + subAggregations: + type: array + description: | + 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. + items: + $ref: '#/components/schemas/aggregationOption' bucketDefinition: type: object description: | @@ -6784,6 +6796,14 @@ components: description: | The approximate number of search matches that share the same value specified in the `key` property. + subAggregations: + type: array + description: | + Nested aggregation results, one per sub-aggregation requested + on the parent `aggregationOption`. Libregraph extension not + present in MS Graph. + items: + $ref: '#/components/schemas/searchAggregation' odata.error: required: - error From 35ab9eec1041b0cc5bba1df9137d5679a7ace8a1 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 13 Aug 2026 00:40:15 +0200 Subject: [PATCH 07/17] feat(search): metric aggregations (sum/min/max) Adds `metricKind` (sum|min|max) to aggregationOption and mirrors `value` + `metricKind` on searchAggregation. When set, the aggregation returns a scalar rather than a bucket list; `size` and `bucketDefinition` are ignored. Libregraph extension, not present in MS Graph. Composable with subAggregations: a metric appears alongside term sub-aggregations inside a parent bucket's subAggregations list, just like in Elasticsearch. The most common use is "for each term bucket, compute a scalar over a numeric field in that bucket's docs", e.g. sum(audio.duration) per audio.album gives total album runtime. Intentionally omits avg/cardinality: avg needs split (sum, count) transport to merge correctly across shards and lands in a follow-up; cardinality is already derivable from a terms sub-aggregation's bucket count. --- api/openapi-spec/v1.0.yaml | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 48fe5a3..1cb1b7d 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6610,6 +6610,19 @@ components: aggregations. items: $ref: '#/components/schemas/aggregationOption' + metricKind: + type: string + description: | + 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. + enum: + - sum + - min + - max bucketDefinition: type: object description: | @@ -6777,6 +6790,25 @@ components: the `bucketDefinition` of the corresponding `aggregationOption`. items: $ref: '#/components/schemas/searchBucket' + value: + type: number + format: double + description: | + Scalar result for metric aggregations (`metricKind` set on + the corresponding `aggregationOption`). Undefined for terms + or range aggregations. Libregraph extension not present in + MS Graph. + metricKind: + type: string + description: | + 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. + enum: + - sum + - min + - max searchBucket: type: object description: | From fd27eb71dd88ab3c13d44f4ec4de6796e36d1120 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 13 Aug 2026 00:40:40 +0200 Subject: [PATCH 08/17] feat(search): avg metric aggregation Adds `avg` as a permitted value on `aggregationOption.metricKind` and `searchAggregation.metricKind`. Distinct from sum/min/max because an average of two averages isn't mergeable: the backend has to carry (sum, count) internally per bucket and only collapse to the scalar `value` at the outermost merge. Wire shape stays symmetrical with sum/min/max: callers still just read `value` off the response; the (sum, count) bookkeeping is entirely service-side. --- api/openapi-spec/v1.0.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 1cb1b7d..6596ff4 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6619,10 +6619,15 @@ components: `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. enum: - sum - min - max + - avg bucketDefinition: type: object description: | @@ -6809,6 +6814,7 @@ components: - sum - min - max + - avg searchBucket: type: object description: | From d4bad17c4944424fb3c4ad141e6265902a4740e0 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Thu, 13 Aug 2026 00:40:40 +0200 Subject: [PATCH 09/17] docs(search): replace em dashes in KQL filter examples --- api/openapi-spec/v1.0.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 6596ff4..29df2b2 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6561,10 +6561,10 @@ components: 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 + - `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 aggregationOption: type: object description: | From 8f05719ee620e0e9131f0568e2e0a2171d61105f Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 7 Sep 2026 19:53:40 +0200 Subject: [PATCH 10/17] feat(search): MS Graph aggregationFilterToken on searchBucket MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Term buckets carry the key as a quoted, ǂǂ-prefixed hex token, range buckets a range(from,to) expression, both as issued by MS Graph. aggregationFilters only accept server-issued tokens in the {field}:{aggregationFilterToken} form, optionally combined with or(...). --- api/openapi-spec/v1.0.yaml | 40 +++++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 7 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 29df2b2..9ce57d4 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4642,7 +4642,7 @@ paths: from: 0 size: 25 aggregationFilters: - - "audio.artist:\"Pink Floyd\"" + - "audio.artist:\"ǂǂ50696e6b20466c6f7964\"" responses: '200': description: OK @@ -4685,10 +4685,13 @@ paths: buckets: - key: Pink Floyd count: 42 + aggregationFilterToken: "\"ǂǂ50696e6b20466c6f7964\"" - key: Led Zeppelin count: 35 + aggregationFilterToken: "\"ǂǂ4c6564205a657070656c696e\"" - key: The Beatles count: 28 + aggregationFilterToken: "\"ǂǂ54686520426561746c6573\"" default: $ref: '#/components/responses/error' components: @@ -6535,14 +6538,19 @@ components: type: array description: | Contains one or more filters to narrow search results to specific - values of a field. Each filter is a KQL expression of the form - `{field}:"{value}"` for term buckets, or - `{field}:range({from},{to})` for range buckets. Field and value - should match those returned in `searchBucket.key` and the original - `aggregationOption`. + buckets of a prior aggregation. Build each filter from the + response of a prior search that aggregated on the same field: take + the `aggregationFilterToken` of the wanted `searchBucket` and + combine it with the field as `{field}:{aggregationFilterToken}`, + e.g. `audio.artist:"ǂǂ50696e6b20466c6f7964"` for a terms bucket or + `audio.year:range(1980,1990)` for a range bucket. Several buckets + of the same field are combined with + `{field}:or({aggregationFilterToken},{aggregationFilterToken})`. Multiple filters can be provided as separate array items. This - results in a logical AND between the filters. + results in a logical AND between the filters. Filters that are + not built from server-issued tokens are rejected with + `invalidRequest`. items: type: string searchQuery: @@ -6834,6 +6842,24 @@ components: description: | The approximate number of search matches that share the same value specified in the `key` property. + aggregationFilterToken: + type: string + readOnly: true + description: | + A token containing the encoded filter that narrows search matches + to this bucket. To use it, pass it as part of the + `aggregationFilters` property of a subsequent `searchRequest` in + the format `{field}:{aggregationFilterToken}`. The filter matches + the bucket `key` exactly and case-sensitively, so the narrowed + result set is the set of matches counted in this bucket. + + For terms buckets the token is the key encoded as lowercase hex of + its UTF-8 bytes, prefixed with `ǂǂ` (U+01C2 twice) and wrapped in + double quotes, e.g. `"ǂǂ50696e6b20466c6f7964"` for the key + `Pink Floyd`. For range buckets the token is `range({from},{to})` + with the bounds of the matching `bucketAggregationRange`; an open + lower bound is written as `min`, an open upper bound as `max`. + This is the same encoding MS Graph uses. subAggregations: type: array description: | From 425d9b4fb253f2b107ccd9300905c4ea8cab6f42 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 7 Sep 2026 20:02:49 +0200 Subject: [PATCH 11/17] refactor(search): prefix libregraph extension attributes with @libre.graph. subAggregations, metricKind and value are not part of the MS Graph resource types; the annotation prefix marks them like the other libregraph additions. Enum values stay unprefixed. --- api/openapi-spec/v1.0.yaml | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 9ce57d4..2c551c9 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6606,7 +6606,7 @@ components: minimum: 1 bucketDefinition: $ref: '#/components/schemas/bucketDefinition' - subAggregations: + '@libre.graph.subAggregations': type: array description: | Nested aggregations computed within each bucket of this @@ -6618,19 +6618,20 @@ components: aggregations. items: $ref: '#/components/schemas/aggregationOption' - metricKind: + '@libre.graph.metricKind': type: string description: | 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. + `searchAggregation` in the response carries a + `@libre.graph.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. + internally and emits only `@libre.graph.value` on the + outermost merge. enum: - sum - min @@ -6803,18 +6804,18 @@ components: the `bucketDefinition` of the corresponding `aggregationOption`. items: $ref: '#/components/schemas/searchBucket' - value: + '@libre.graph.value': type: number format: double description: | - Scalar result for metric aggregations (`metricKind` set on - the corresponding `aggregationOption`). Undefined for terms - or range aggregations. Libregraph extension not present in + Scalar result for metric aggregations (`@libre.graph.metricKind` + set on the corresponding `aggregationOption`). Undefined for + terms or range aggregations. Libregraph extension not present in MS Graph. - metricKind: + '@libre.graph.metricKind': type: string description: | - Echoes the `metricKind` of the corresponding + Echoes the `@libre.graph.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. @@ -6860,7 +6861,7 @@ components: with the bounds of the matching `bucketAggregationRange`; an open lower bound is written as `min`, an open upper bound as `max`. This is the same encoding MS Graph uses. - subAggregations: + '@libre.graph.subAggregations': type: array description: | Nested aggregation results, one per sub-aggregation requested From 1eb29f924bb8cb05c74619c7b46aa9ae7ff44627 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 7 Sep 2026 20:50:26 +0200 Subject: [PATCH 12/17] refactor(search): metricDefinition and metric objects for metric aggregations Mirrors the bucketDefinition / buckets pair: @libre.graph.metricDefinition { kind } on aggregationOption, @libre.graph.metric { kind, value } on searchAggregation. --- api/openapi-spec/v1.0.yaml | 63 +++++++++++++++++++++++--------------- 1 file changed, 38 insertions(+), 25 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 2c551c9..834f016 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6618,20 +6618,28 @@ components: aggregations. items: $ref: '#/components/schemas/aggregationOption' - '@libre.graph.metricKind': + '@libre.graph.metricDefinition': + $ref: '#/components/schemas/metricDefinition' + metricDefinition: + type: object + description: | + Provides the details of how to compute a scalar metric over the + aggregation `field`, the counterpart of `bucketDefinition` for + metric aggregations. When set on an `aggregationOption`, `size` and + `bucketDefinition` are ignored, and the corresponding + `searchAggregation` in the response carries a `@libre.graph.metric` + rather than `buckets`. Libregraph extension not present in MS Graph. + required: + - kind + properties: + kind: type: string description: | - 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 - `@libre.graph.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 `@libre.graph.value` on the - outermost merge. + The reducer applied to the field values of all matches. Required. + + `avg` is not a simple reducer (averages of averages are not + averages): the backend carries `(sum, count)` internally and + emits only the final value on the outermost merge. enum: - sum - min @@ -6804,26 +6812,31 @@ components: the `bucketDefinition` of the corresponding `aggregationOption`. items: $ref: '#/components/schemas/searchBucket' - '@libre.graph.value': - type: number - format: double - description: | - Scalar result for metric aggregations (`@libre.graph.metricKind` - set on the corresponding `aggregationOption`). Undefined for - terms or range aggregations. Libregraph extension not present in - MS Graph. - '@libre.graph.metricKind': + '@libre.graph.metric': + $ref: '#/components/schemas/searchMetric' + searchMetric: + type: object + description: | + The result of a metric aggregation, the counterpart of `buckets` for + aggregations requested with a `@libre.graph.metricDefinition`. + Absent for terms and range aggregations. Libregraph extension not + present in MS Graph. + properties: + kind: type: string description: | - Echoes the `@libre.graph.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. + Echoes the `kind` of the corresponding `metricDefinition`, + allowing consumers (and the search service's cross-space merge + layer) to pick the right reducer when combining results. enum: - sum - min - max - avg + value: + type: number + format: double + description: The scalar result of the metric. searchBucket: type: object description: | From 9497b439f3551eacc955b7f7128fa9a0d1219893 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 7 Sep 2026 21:03:54 +0200 Subject: [PATCH 13/17] docs(search): range tokens with the exact MS Graph spelling Space after the comma and to="le" on an open upper bound, so the issued tokens are byte-identical to MS Graph; whitespace is optional when parsing. --- api/openapi-spec/v1.0.yaml | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 834f016..901668b 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -6543,9 +6543,11 @@ components: the `aggregationFilterToken` of the wanted `searchBucket` and combine it with the field as `{field}:{aggregationFilterToken}`, e.g. `audio.artist:"ǂǂ50696e6b20466c6f7964"` for a terms bucket or - `audio.year:range(1980,1990)` for a range bucket. Several buckets + `audio.year:range(1980, 1990)` for a range bucket. Several buckets of the same field are combined with `{field}:or({aggregationFilterToken},{aggregationFilterToken})`. + Whitespace after the commas of `range(...)` and `or(...)` is + optional. Multiple filters can be provided as separate array items. This results in a logical AND between the filters. Filters that are @@ -6870,10 +6872,12 @@ components: For terms buckets the token is the key encoded as lowercase hex of its UTF-8 bytes, prefixed with `ǂǂ` (U+01C2 twice) and wrapped in double quotes, e.g. `"ǂǂ50696e6b20466c6f7964"` for the key - `Pink Floyd`. For range buckets the token is `range({from},{to})` + `Pink Floyd`. For range buckets the token is `range({from}, {to})` with the bounds of the matching `bucketAggregationRange`; an open - lower bound is written as `min`, an open upper bound as `max`. - This is the same encoding MS Graph uses. + lower bound is written as `min`, an open upper bound as `max` + followed by `to="le"`, e.g. `range(min, 1980)`, + `range(1980, 1990)` and `range(2010, max, to="le")`. This is the + same encoding MS Graph uses. '@libre.graph.subAggregations': type: array description: | From d532050c909fbdf4b753c5385dcd2688f1456312 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 7 Sep 2026 21:05:35 +0200 Subject: [PATCH 14/17] docs(search): word the MS Graph relation like the other endpoints --- api/openapi-spec/v1.0.yaml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 901668b..391646d 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4560,9 +4560,10 @@ paths: 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. + Modeled on the MS Graph search query endpoint + (https://learn.microsoft.com/en-us/graph/api/search-query). Request and + response follow the MS Graph resource types; libregraph additions carry + the `@libre.graph.` prefix. requestBody: required: true content: From b83d8548e0ceb8acad14c0136712ae2ae416999a Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Mon, 7 Sep 2026 23:15:45 +0200 Subject: [PATCH 15/17] feat(search): allow $expand=thumbnails on the search query Add a $expand query parameter to /v1beta1/search/query so clients can pull the thumbnails relationship inline on each hit's driveItem, the graph equivalent of the WebDAV report's has-preview. Libregraph extension: MS Graph search has no $expand. Also switch the aggregation examples to Saxon - And the Bands Played On. --- api/openapi-spec/v1.0.yaml | 58 +++++++++++++++++++++++++------------- 1 file changed, 38 insertions(+), 20 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 391646d..5c22847 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4564,6 +4564,24 @@ paths: (https://learn.microsoft.com/en-us/graph/api/search-query). Request and response follow the MS Graph resource types; libregraph additions carry the `@libre.graph.` prefix. + parameters: + - name: $expand + in: query + description: | + Relationships to expand inline on each hit's driveItem. Only + `thumbnails` is supported, attaching a preview thumbnail set for + thumbnailable mime types. Libregraph extension: MS Graph search has no + $expand and returns no thumbnails on search hits. + required: false + style: form + explode: false + schema: + type: array + uniqueItems: true + items: + type: string + enum: + - thumbnails requestBody: required: true content: @@ -4643,7 +4661,7 @@ paths: from: 0 size: 25 aggregationFilters: - - "audio.artist:\"ǂǂ50696e6b20466c6f7964\"" + - "audio.artist:\"ǂǂ5361786f6e\"" responses: '200': description: OK @@ -4668,31 +4686,31 @@ paths: - hitId: "4b478257-c9e8-458a-b764-fd7ed6cc1087$5d58a5c3-b244-4e9c-b082-82c3f1adbe8a!9d94e3d7" rank: 1 resource: - name: Comfortably Numb.flac + name: And the Bands Played On.flac size: 45678901 audio: - artist: Pink Floyd - album: The Wall - title: Comfortably Numb - genre: Rock - year: 1979 - track: 6 - disc: 2 - duration: 382000 + artist: Saxon + album: Power & the Glory + title: And the Bands Played On + genre: Heavy Metal + year: 1983 + track: 1 + disc: 1 + duration: 235000 total: 142 moreResultsAvailable: true aggregations: - field: audio.artist buckets: - - key: Pink Floyd + - key: Saxon count: 42 - aggregationFilterToken: "\"ǂǂ50696e6b20466c6f7964\"" - - key: Led Zeppelin + aggregationFilterToken: "\"ǂǂ5361786f6e\"" + - key: Iron Maiden count: 35 - aggregationFilterToken: "\"ǂǂ4c6564205a657070656c696e\"" - - key: The Beatles + aggregationFilterToken: "\"ǂǂ49726f6e204d616964656e\"" + - key: Judas Priest count: 28 - aggregationFilterToken: "\"ǂǂ54686520426561746c6573\"" + aggregationFilterToken: "\"ǂǂ4a7564617320507269657374\"" default: $ref: '#/components/responses/error' components: @@ -6543,7 +6561,7 @@ components: response of a prior search that aggregated on the same field: take the `aggregationFilterToken` of the wanted `searchBucket` and combine it with the field as `{field}:{aggregationFilterToken}`, - e.g. `audio.artist:"ǂǂ50696e6b20466c6f7964"` for a terms bucket or + e.g. `audio.artist:"ǂǂ5361786f6e"` for a terms bucket or `audio.year:range(1980, 1990)` for a range bucket. Several buckets of the same field are combined with `{field}:or({aggregationFilterToken},{aggregationFilterToken})`. @@ -6574,7 +6592,7 @@ components: Examples: - `budget report`: free text search - `mediatype:audio`: filter by media type - - `audio.artist:"Pink Floyd"`: filter by audio metadata + - `audio.artist:"Saxon"`: filter by audio metadata - `audio.genre:Rock AND audio.year:1979`: combined filters aggregationOption: type: object @@ -6872,8 +6890,8 @@ components: For terms buckets the token is the key encoded as lowercase hex of its UTF-8 bytes, prefixed with `ǂǂ` (U+01C2 twice) and wrapped in - double quotes, e.g. `"ǂǂ50696e6b20466c6f7964"` for the key - `Pink Floyd`. For range buckets the token is `range({from}, {to})` + double quotes, e.g. `"ǂǂ5361786f6e"` for the key + `Saxon`. For range buckets the token is `range({from}, {to})` with the bounds of the matching `bucketAggregationRange`; an open lower bound is written as `min`, an open upper bound as `max` followed by `to="le"`, e.g. `range(min, 1980)`, From 7182b875b46da6bafe532dc5c541b15947604b29 Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 8 Sep 2026 09:22:29 +0200 Subject: [PATCH 16/17] docs(search): pair search-query response examples with their requests Split the single response example into two keyed to match the request examples: a size:0 facets-only response (empty hits, buckets with filter tokens) and a token drilldown whose total equals the Saxon bucket count. --- api/openapi-spec/v1.0.yaml | 39 ++++++++++++++++++++++++-------------- 1 file changed, 25 insertions(+), 14 deletions(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index 5c22847..c069bbb 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4676,7 +4676,30 @@ paths: items: $ref: '#/components/schemas/searchResponse' examples: - search results with aggregations: + search with aggregations: + summary: Facet counts for the size:0 request (no hits, only the buckets and their filter tokens) + value: + value: + - searchTerms: + - "mediatype:audio" + hitsContainers: + - hits: [] + total: 142 + moreResultsAvailable: true + aggregations: + - field: audio.artist + buckets: + - key: Saxon + count: 42 + aggregationFilterToken: "\"ǂǂ5361786f6e\"" + - key: Iron Maiden + count: 35 + aggregationFilterToken: "\"ǂǂ49726f6e204d616964656e\"" + - key: Judas Priest + count: 28 + aggregationFilterToken: "\"ǂǂ4a7564617320507269657374\"" + search with aggregation filter: + summary: Drilldown with the Saxon bucket's aggregationFilterToken from the previous response; total equals that bucket's count value: value: - searchTerms: @@ -4697,20 +4720,8 @@ paths: track: 1 disc: 1 duration: 235000 - total: 142 + total: 42 moreResultsAvailable: true - aggregations: - - field: audio.artist - buckets: - - key: Saxon - count: 42 - aggregationFilterToken: "\"ǂǂ5361786f6e\"" - - key: Iron Maiden - count: 35 - aggregationFilterToken: "\"ǂǂ49726f6e204d616964656e\"" - - key: Judas Priest - count: 28 - aggregationFilterToken: "\"ǂǂ4a7564617320507269657374\"" default: $ref: '#/components/responses/error' components: From 07d1f6cb91e9f88f300f02e36ba921fecef7028b Mon Sep 17 00:00:00 2001 From: Dominik Schmidt Date: Tue, 8 Sep 2026 10:38:04 +0200 Subject: [PATCH 17/17] docs(search): capitalize Libregraph in the endpoint description to match the rest of the spec --- api/openapi-spec/v1.0.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/openapi-spec/v1.0.yaml b/api/openapi-spec/v1.0.yaml index c069bbb..16c3445 100644 --- a/api/openapi-spec/v1.0.yaml +++ b/api/openapi-spec/v1.0.yaml @@ -4562,7 +4562,7 @@ paths: Modeled on the MS Graph search query endpoint (https://learn.microsoft.com/en-us/graph/api/search-query). Request and - response follow the MS Graph resource types; libregraph additions carry + response follow the MS Graph resource types; Libregraph additions carry the `@libre.graph.` prefix. parameters: - name: $expand