---
title: "Hybrid search over contexts (PG tsvector + Elasticsearch BM25 + RRF fusion)"
method: POST
path: "/api/search"
tags: ["Search"]
---

# Hybrid search over contexts (PG tsvector + Elasticsearch BM25 + RRF fusion)

`POST /api/search`

## Request

- Base URL: `http://localhost:38200`
- URL: `http://localhost:38200/api/search`
- Auth: one of:
  - HTTP bearer
  - API key in cookie `token`

## Request body

- SearchRequest
  - `query` string, required
  - `workspace` string
  - `project` string
  - `type` 'reference' | 'feedback' | 'project' | 'incident' | 'lesson' | 'user' | 'synthesis' | 'map' | 'moc' | 'handoff' | 'endpoint' | 'trail'
  - `tags` string[]
  - `scope` 'personal' | 'workspace' | 'team'
  - `subjectId` string: Filter to a single end-user's memories (Mem0-parity user_id axis).
  - `include_subject_rows` boolean: Admin/owner (or API key scope=admin) only.
  - `memoryKind` 'episodic' | 'semantic' | 'procedural'
  - `epistemicMin` 'observed' | 'told' | 'inferred' | 'assumed'
  - `limit` integer
  - `offset` integer
  - `useMap` boolean
  - `includeMaps` boolean
  - `include_archived` boolean
  - `lifecycle_boost` boolean
  - `chunk_search` boolean
  - `level` 'full' | 'paragraph' | 'sentence'
  - `min_score` number
  - `rewrite_query` boolean
  - `no_cache` boolean
  - `grants` integer[]: Explicit opt-in cross-tenant memory-sharing grant ids.
  - `include_trace_events` boolean
  - `trace_session_id` integer
  - `latency_budget_ms` integer: T700: cross-stage wall-clock budget for the WHOLE request. Omit for unchanged behaviour -- this is strictly opt-in and nothing pays for it by default. Distinct from the per-stage timeouts (`SEARCH_QUERY_REWRITE_TIMEOUT_MS` default 6000, `SEARCH_RERANKING_TIMEOUT_MS` default 8000), which are independent and ADDITIVE -- 14 s of stage timeout alone, before PG/ES/vector run -- so no combination of them can express "answer within a second". Before each optional heavy stage (query-rewrite, graph-assist, cross-encoder rerank) the elapsed time is compared against the budget; a stage with too little room left is SKIPPED WITHOUT BEING ENTERED (no network call at all) and named in `searchMeta.degradedStages[]`. A stage that IS admitted has its own timeout clamped down to the remainder. The mandatory legs are never skipped -- a budget shapes which optional quality stages run, it is not a request-abort deadline, so the response is always well-formed.

## Response `200`

Ranked search results.

- SearchResponse
  - `results` Context[]
    - `id` integer
    - `name` string
    - `description` string
    - `content` string
    - `type` 'reference' | 'feedback' | 'project' | 'incident' | 'lesson' | 'user' | 'synthesis' | 'map' | 'moc' | 'handoff' | 'endpoint' | 'trail'
    - `scope` 'personal' | 'workspace' | 'team'
    - `workspace` string, nullable
    - `project` string, nullable
    - `lifecycle` string
    - `tags` string[]
    - `metadata` object
    - `createdAt` string, date-time
    - `updatedAt` string, date-time
    - `mergedInto` integer, nullable: T743, additive. Present ONLY on the response of `POST /api/contexts` when the T711 near-duplicate guard folded this write into an EXISTING row instead of inserting a new one -- the id of that existing (surviving) row, which is also `id` above. Absent/undefined on every other response shape (search, list, get, update) and on a genuine create. A caller that sees `mergedInto` set knows no new row was created and should treat the write as an update to the returned id, not a create.
  - `total` integer
  - `searchMeta` object
  - `grantedResults` object[]
  - `traceEvents` object[]

## Other responses

- `400`: Request body/query failed Zod validation.
- `401`: Missing or invalid credentials.
- `403`: include_subject_rows requested by a non-admin caller.

---

[API](https://skmtc.dev/contextq/apis/contextq-api.md) · [All operations](https://skmtc.dev/contextq/apis/contextq-api/llms.txt) · [OpenAPI document](https://skmtc.dev/contextq/apis/contextq-api/revisions/7afc44056a22?raw)
