---
title: "Search the workspace vault (hybrid by default)"
method: GET
path: "/v1/vault/search"
tags: ["vault"]
---

# Search the workspace vault (hybrid by default)

`GET /v1/vault/search`

## Request

- Base URL: `https://api.hyperresearch.ai`
- URL: `https://api.hyperresearch.ai/v1/vault/search`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `hr_session`

Natural-language search over the workspace’s evidence notes AND its finished reports.

**Just send `text`.** The default mode is `hybrid` — bm25 over title/body/tags/summary fused with embedding similarity by reciprocal rank, then weighted by each note’s lifecycle and quality grade. A whole question is a fine query; it is sanitised into a safe FTS5 match, so no punctuation, quoting or operator can fail the request. Quoted spans are honoured as phrases (`EU "AI Act" enforcement`). A blank `text` is a filtered listing, not an error.

**Ask it as a question if you like.** The interrogative frame is stripped before anything is embedded or matched — `what did the report on agent protocols conclude?` retrieves on `agent protocols` — so a question and its subject find the same notes. A quoted span survives the strip and is still honoured as a phrase.

**Scope it.** `project` (or `tag`), `type`, `status`, `domain`, `since`/`until` all narrow the same search. Finished runs’ reports come back as `kind: "report"` hits by default, above the notes and inside the same `limit`; repeated runs of one question collapse to the newest, carrying `versions`. `include_reports=false` leaves them out.

**Then read.** A hit’s `id` is a note id for `POST /v1/vault/notes/batch` (up to 50 at once) or `GET /v1/vault/notes/{id}`; a report hit’s `run_id` is for `GET /v1/runs/{id}/result`. Cite `source_url`.

`score` is relative to the best hit in THIS result set (1.0 = best here), not an absolute confidence. Results are deduplicated server-side: two runs that read the same page are one row. There is no cursor — raise `limit` (max 200) or narrow the scope.

Hits carry `display_tags` beside `tags`, resolved the same way `GET /v1/vault/notes` resolves them (E2E5-08). `snippet` is the excerpt to show: the note’s own `summary` with the source site’s chrome removed — the same text `GET /v1/vault/notes` returns as `display_summary` — falling back to the matched window of the stored body, also cleaned, for a note that has no usable summary, and `null` when neither holds anything but the site’s furniture (E2E16-01).

## Query parameters

- `text` string
- `q` string: Alias for `text`, accepted for core-interface and MCP clients. `text` wins if both are sent.
- `mode` 'fts' | 'semantic' | 'hybrid': Omit this. The default is `hybrid` (bm25 + embeddings, reciprocal-rank fused), which is the right answer for a natural-language question and degrades to lexical by itself when the workspace has no embeddings. `fts` is lexical only and cheaper. `semantic` is vectors only and fails closed when no embedder is configured.
- `tag` string: One vault tag. `project:<id>` is a project’s scope — see `GET /v1/projects`.
- `project` string: A `proj_…` id, resolved to its `project:<id>` tag. The readable spelling of `tag` for the common case; pass one or the other, not both.
- `status` string: One note lifecycle: `draft|review|evergreen|stale|deprecated|archive`.
- `type` string: One note kind: `note|raw|interim|source-analysis|moc|index`.
- `domain` string: Only notes read from this publisher, e.g. `eur-lex.europa.eu`. A leading dot (`.europa.eu`) includes subdomains.
- `since` string: Only notes created at or after this ISO timestamp (a bare date works).
- `until` string: Only notes created at or before this ISO timestamp. Pairs with `since` to make a range.
- `include_reports` '0' | '1' | 'true' | 'false': Whether this workspace’s finished run REPORTS are returned as hits alongside the notes, matched on the question each run asked. **Default true**: the report is the thing the research produced, and a vault search that returns forty of its sources and not the document itself is answering the wrong question. A report hit carries `kind: "report"`, `type: "report"` and a `run_id`/`report_id` to pass to `GET /v1/runs/{id}/result`; it is NOT a note id and `GET /v1/vault/notes/{id}` will not resolve it. Report hits sort above notes of equal score and SHARE the `limit` with them — a page of six is six rows, however they divide. Repeated runs of one question are one row: it is the newest finished run, and `versions` says how many runs stand behind it (`version_group` matches the runs API). Reports take at most half the page unless the query itself says "report". Pass `include_reports=false` to get notes only — the thing to do when every id you receive is going straight to a note endpoint.
- `rank` 'relevance' | 'quality'
- `limit` integer: Rows in the response, notes and reports together (default 20, max 200). The two kinds share this budget; they are not two lists of `limit`.

## Response `200`

Hits

- unknown[]
  - unknown

## Other responses

- `400`: Invalid request
- `401`: Authentication failed
- `403`: Insufficient scope or plan
- `404`: Not found
- `429`: Rate limited or over quota
- `500`: Internal error

---

[API](https://skmtc.dev/hyperresearch/apis/hyperresearch-cloud-api.md) · [All operations](https://skmtc.dev/hyperresearch/apis/hyperresearch-cloud-api/llms.txt) · [OpenAPI document](https://skmtc.dev/hyperresearch/apis/hyperresearch-cloud-api/revisions/726ef831ebf4?raw)
