Skip to content

feat: add search query endpoint with request examples - #34

Open
dschmidt wants to merge 17 commits into
mainfrom
feat/search
Open

feat: add search query endpoint with request examples#34
dschmidt wants to merge 17 commits into
mainfrom
feat/search

Conversation

@dschmidt

@dschmidt dschmidt commented Apr 10, 2026

Copy link
Copy Markdown
Contributor

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

I'm also willing to implement that endpoint: opencloud-eu/opencloud#3211

Libregraph extensions beyond MS Graph

Everything follows the MS Graph resource types under the same names, except these additions, which carry the @libre.graph. prefix like the other libregraph annotations:

  • @libre.graph.subAggregations on aggregationOption and searchBucket: nested aggregations, evaluated in a single request
  • @libre.graph.metricDefinition ({ kind }, kind being sum / min / max / avg) on aggregationOption, mirroring bucketDefinition, with a @libre.graph.metric ({ kind, value }) on searchAggregation instead of buckets

Example request with a sub-aggregation and a metric:

{
  "requests": [{
    "entityTypes": ["driveItem"],
    "query": { "queryString": "mediatype:audio" },
    "size": 0,
    "aggregations": [{
      "field": "audio.artist",
      "size": 10,
      "bucketDefinition": { "sortBy": "count", "isDescending": true },
      "@libre.graph.subAggregations": [
        { "field": "audio.album", "size": 5, "bucketDefinition": { "sortBy": "count" } },
        { "field": "audio.duration", "@libre.graph.metricDefinition": { "kind": "sum" } }
      ]
    }]
  }]
}

And the matching response:

{
  "value": [{
    "searchTerms": ["mediatype:audio"],
    "hitsContainers": [{
      "hits": [],
      "total": 142,
      "moreResultsAvailable": false,
      "aggregations": [{
        "field": "audio.artist",
        "buckets": [{
          "key": "Saxon",
          "count": 42,
          "aggregationFilterToken": "\"ǂǂ5361786f6e\"",
          "@libre.graph.subAggregations": [
            {
              "field": "audio.album",
              "buckets": [
                { "key": "Power & the Glory", "count": 26, "aggregationFilterToken": "\"ǂǂ506f77657220262074686520476c6f7279\"" },
                { "key": "Wheels of Steel", "count": 5, "aggregationFilterToken": "\"ǂǂ576865656c73206f6620537465656c\"" }
              ]
            },
            {
              "field": "audio.duration",
              "@libre.graph.metric": { "kind": "sum", "value": 16073000 }
            }
          ]
        }]
      }]
    }]
  }]
}

aggregationFilterToken

searchBucket.aggregationFilterToken uses the MS Graph encoding: terms buckets carry the key as a quoted, ǂǂ-prefixed hex token, range buckets a range(from, to) expression with min / max for open bounds and to="le" on an open upper bound, spelled exactly like MS Graph does (whitespace is optional when parsing). aggregationFilters only accept server-issued tokens in the {field}:{aggregationFilterToken} form, optionally combined with or(...). The filter matches the bucket key exactly and case-sensitively, so the narrowed result set is the set of matches counted in the bucket.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds an OpenAPI definition for a new beta search endpoint modeled after Microsoft Graph Search, including request/response schemas and multiple example payloads to document common search scenarios.

Changes:

  • Introduces POST /v1beta1/search/query with detailed description and request/response examples.
  • Adds search-related schemas (searchRequest, searchQuery, searchResponse, aggregation/bucket schemas) under components/schemas.
  • Wires the new endpoint to the new schemas via $ref to keep the spec structured.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/openapi-spec/v1.0.yaml
Comment thread api/openapi-spec/v1.0.yaml
Comment thread api/openapi-spec/v1.0.yaml
Comment thread api/openapi-spec/v1.0.yaml Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated 1 comment.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread api/openapi-spec/v1.0.yaml Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@aduffeck

Copy link
Copy Markdown
Member

@dschmidt Since the search endpoint returns plain drive items I'm wondering how we would handle the additional information the results are currently being enriched with (see https://github.com/opencloud-eu/opencloud/blob/main/services/webdav/pkg/service/v0/search.go#L150-L263 for the current state).

Things like the tags could be derived from the arbitrary metadata (which currently isn't exposed afaict), but do you have an idea how we would handle the oc:has-preview (depends on a server-side list of supported mimetypes) and oc:favorite (depends on the user running the search and the favorites metadata which is not part of the arbitrary metadata) flags.

@dschmidt

Copy link
Copy Markdown
Contributor Author

Very good questions, thanks!

I assume hasPreview could be expressed using the thumbnails relationship: https://learn.microsoft.com/en-us/graph/api/driveitem-list-thumbnails?view=graph-rest-1.0&tabs=http

favorite is trickier, as it's a user specific value and usually driveItems and all their data are not bound to a user.
Afaict MS Graph has nothing like that - the closest as a concept might be "following", c.f. https://learn.microsoft.com/en-us/graph/api/drive-list-following?view=graph-rest-1.0&tabs=http
But that does not seem to have a prop on the item. Which is very unhandy to work with.

Although this introduces user specific data to the driveItem, We could introduce a custom prop like @libre.graph.favorite or similar.
Maybe even @libre.graph.user.favorite to make it obvious it's user specific.

@dschmidt

Copy link
Copy Markdown
Contributor Author

The thumbnails relationship is already in the spec, it might still need implementation tho 😅

@dschmidt

dschmidt commented Apr 14, 2026

Copy link
Copy Markdown
Contributor Author

Aha, I just realized the follow endpoints map to setting the favorite state... so just @libre.graph.following or @libre.graph.user.following on the driveItem?

If you agree, I can send another PR for that, to keep this one scoped

@dschmidt

Copy link
Copy Markdown
Contributor Author

#37 👀

@dschmidt

Copy link
Copy Markdown
Contributor Author

What's also potentially missing: short permissions/allowed actions without loading all shares

Not sure how we want to do that? Thoughts?

@butonic

butonic commented Apr 21, 2026

Copy link
Copy Markdown
Member

What's also potentially missing: short permissions/allowed actions without loading all shares

Not sure how we want to do that? Thoughts?

we introduced @libre.graph.permissions.roles.allowedValues and @libre.graph.permissions.actions.allowedValues to let the web ui build the sharing dialog.

see https://docs.opencloud.eu/docs/dev/server/apis/http/graph/permissions for what that looks like or just monitor your browsers network tab when opening the sharing dialog in the web ui.

Let me know if that covers what you have in mind

dschmidt added a commit that referenced this pull request Apr 21, 2026
Adds an optional, `$select`-gated instance annotation on `driveItem` that
carries the list of libre.graph actions the caller is allowed to perform on
the item. Mirrors the annotation of the same name on the `/permissions`
endpoint so clients (e.g. a sharing dialog in a search/listing UI) can get
the effective-actions view inline without a separate round-trip per item.

- Adds `@libre.graph.permissions.actions.allowedValues` to the `driveItem`
  schema, marked read-only and documented as only populated when requested.
- Adds a reusable `driveItemSelect` component parameter with a narrow enum,
  following the same pattern as PR #38 (feat/download-url). When both land,
  the enum values merge.
- Wires the new parameter to `GetDriveItem`.

Rationale discussed in #34.
@dschmidt

dschmidt commented Jul 2, 2026

Copy link
Copy Markdown
Contributor Author

@butonic @aduffeck do we want to hold this back until the additional properties are spec'ed and merged or should we progress here independently? They are not hard to add later and merging this would unblock further work on the search service.

edit: Then again, they are "half approved". If we can settle on the me infix, we can just merge them, I guess?
edit:
As it might not have been completely obvious, I'm talking about

dschmidt added a commit that referenced this pull request Jul 11, 2026
Adds an optional, `$select`-gated instance annotation on `driveItem` that
carries the list of libre.graph actions the caller is allowed to perform on
the item. Mirrors the annotation of the same name on the `/permissions`
endpoint so clients (e.g. a sharing dialog in a search/listing UI) can get
the effective-actions view inline without a separate round-trip per item.

- Adds `@libre.graph.permissions.actions.allowedValues` to the `driveItem`
  schema, marked read-only and documented as only populated when requested.
- Adds a reusable `driveItemSelect` component parameter with a narrow enum,
  following the same pattern as PR #38 (feat/download-url). When both land,
  the enum values merge.
- Wires the new parameter to `GetDriveItem`.

Rationale discussed in #34.
@dschmidt

Copy link
Copy Markdown
Contributor Author

Implementations for the issues raised above:

I think with those in place we can fully replace the WebDAV search...

@dschmidt

dschmidt commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

I've found more missing properties that WebDAV PROPFIND provides but driveItems don't cover yet. Spec PRs:

Together with the already merged following/tags/allowedValues annotations these cover everything web currently reads from PROPFIND (share indicators, lock state, processing state) afaict

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
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).
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.
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.
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.
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(...).
…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.
…egations

Mirrors the bucketDefinition / buckets pair: @libre.graph.metricDefinition
{ kind } on aggregationOption, @libre.graph.metric { kind, value } on
searchAggregation.
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.
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.
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants