diff --git a/api-reference/v2/endpoint/ingest-context.mdx b/api-reference/v2/endpoint/ingest-context.mdx
index 6ed7c315..b06b3064 100644
--- a/api-reference/v2/endpoint/ingest-context.mdx
+++ b/api-reference/v2/endpoint/ingest-context.mdx
@@ -1,19 +1,14 @@
---
title: "Ingest Context"
-description: "Ingestion endpoint for knowledge (documents, app sources) and user memories."
-openapi: "api-reference/v2/openapi.json POST /context/ingest"
+description: "Send text and conversations to a database as context items. Split databases keep documents, app_knowledge and memories."
---
import { Field } from "/snippets/field.jsx";
+import LegacyLine from "/snippets/legacy-line.mdx";
-When context is of `type=knowledge`:
+`POST /context/ingest` takes a list of **context items**, each a `text` or a `conversation`, and queues them for chunking, embedding, enrichment and graph extraction. That is the only ingest shape a unified database accepts. The guide is [Ingest context](/essentials/v2/ingest); this page is the field reference.
-1. **Documents** - Use the `documents` field for binary uploads that HydraDB should parse: PDFs, Office and iWork files, spreadsheets, images and plain text. See [Supported file formats](#supported-file-formats) for the full list.
-2. **App Sources** - Use `app_knowledge` for pre-extracted JSON content (Slack threads, Notion pages, emails, tickets). Read more about [ingesting knowledge from your apps](/essentials/v2/app-sources).
-
-When context is of `type=memory`
-
-Use `memories` for per-user content, scoped with `collection`. Set `infer: true` to let HydraDB extract preferences from raw signals, or `infer: false` to store the text verbatim. Read more about [ingesting memories](/essentials/v2/memories).
+
`database` and `collection` are the current field names (formerly `tenant_id` and `sub_tenant_id`). The old names remain accepted as deprecated aliases for full backward compatibility.
@@ -24,6 +19,206 @@ Use `memories` for per-user content, scoped with `collection`. Set `infer: true`
```python Python SDK
import json
+# The SDK sends a multipart form; the item list goes in the `items` form field.
+result = client.context.ingest(
+ database="acme_corp",
+ collection="company",
+ upsert=True,
+ items=json.dumps([
+ {
+ "context_id": "refund-policy",
+ "title": "Refund policy",
+ "text": "Refunds are processed within 5 business days.",
+ "context_category": "business_knowledge",
+ "attributes": {"department": "support"},
+ "custom_attributes": {"owner": "sam@acme.com"},
+ },
+ {
+ "context_id": "chat-alex-001",
+ "conversation": [
+ {"role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex"},
+ {"role": "assistant", "content": "Got it, short answers."},
+ ],
+ "context_category": "user_preference",
+ "happened_at": "2026-09-01",
+ "forceful_relations": {"ids": ["refund-policy"]},
+ },
+ ]),
+)
+
+print([r.source_id for r in result.data.results])
+```
+
+```typescript TypeScript SDK
+// The SDK sends a multipart form; the item list goes in the `items` form field.
+const result = await client.context.ingest({
+ database: "acme_corp",
+ collection: "company",
+ upsert: true,
+ items: JSON.stringify([
+ {
+ context_id: "refund-policy",
+ title: "Refund policy",
+ text: "Refunds are processed within 5 business days.",
+ context_category: "business_knowledge",
+ attributes: { department: "support" },
+ custom_attributes: { owner: "sam@acme.com" },
+ },
+ {
+ context_id: "chat-alex-001",
+ conversation: [
+ { role: "user", content: "Keep answers short, I read on my phone.", name: "alex" },
+ { role: "assistant", content: "Got it, short answers." },
+ ],
+ context_category: "user_preference",
+ happened_at: "2026-09-01",
+ forceful_relations: { ids: ["refund-policy"] },
+ },
+ ]),
+});
+
+console.log(result.data.results.map((r) => r.sourceId));
+```
+
+```bash cURL
+curl -X POST 'https://api.hydradb.com/context/ingest' \
+ -H "Authorization: Bearer " \
+ -H "API-Version: 2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "database": "acme_corp",
+ "collection": "company",
+ "enrich": true,
+ "upsert": true,
+ "context": [
+ {
+ "context_id": "refund-policy",
+ "title": "Refund policy",
+ "text": "Refunds are processed within 5 business days.",
+ "context_category": "business_knowledge",
+ "attributes": { "department": "support" },
+ "custom_attributes": { "owner": "sam@acme.com" }
+ },
+ {
+ "context_id": "chat-alex-001",
+ "conversation": [
+ { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" },
+ { "role": "assistant", "content": "Got it, short answers." }
+ ],
+ "context_category": "user_preference",
+ "happened_at": "2026-09-01",
+ "forceful_relations": { "ids": ["refund-policy"] }
+ }
+ ]
+ }'
+```
+
+
+
+## Request body
+
+Send `application/json`. The SDKs send `multipart/form-data` instead: the same array goes in the `items` form field, with `database`, `collection`, `upsert`, `enrich`, `instructions` and `graph_payload` as form fields. Both entry points run the same validation.
+
+| Name | Description |
+| --- | --- |
+| | Target database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). |
+| | Collection inside the database. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=the default collection) |
+| | The items to ingest, at most 100. `items` and `contexts` are accepted as aliases; send `context`. In the multipart form the field is `items`, a JSON-encoded array. |
+| | Request-level default for every item's `enrich`. (default=`true`) |
+| | Request-level default for every item's `upsert`. (default=`true`) |
+| | Request-level default for every item's `instructions`. (default=empty) |
+| | Bring your own graph: a map of `context_id` to `{ entities, relations }` that replaces extraction for that item. Every key must match an item's `context_id` in the same request, otherwise `400`. See [Bring Your Own Graph](/essentials/v2/bring-your-own-graph). |
+
+### Item fields
+
+Each item is exactly one of `text` or `conversation`.
+
+| Name | Description |
+| --- | --- |
+| | Your id for the item; the upsert key. Generated from `title` when omitted. Must not contain a comma (`,`), which is the id separator on `/context/status?ids=`. |
+| | Readable name. Searchable with `titles` on `/query`. |
+| | Plain text. Send exactly one of `text` or `conversation`. `content` is accepted as an alias. |
+| | Turns of `{ role, content, name? }`; roles are `user`, `assistant` and `system`. `system` turns shape enrichment but are never stored as facts. `messages` is accepted as an alias. |
+| | Extract entities, relations and preferences from this item into the graph; the output is stored separately and returned as `enrichment` on query. (default=the request's `enrich`, else `true`) |
+| | Replace an existing item with the same `context_id`, deleting its chunks and graph contribution first. (default=the request's `upsert`, else `true`) |
+| | Steer enrichment for this item. `custom_instructions` is accepted as an alias. (default=the request's `instructions`) |
+| | The date the item is about, `YYYY-MM-DD` only. A timestamp is a `400`. HydraDB records when it received the item separately. |
+| | Declared, filterable fields; keys must be in `database_metadata_schema`. Filter with `attributes` on `/query`. |
+| | Free-form fields. Stored with the item; not filterable and not returned on query chunks. |
+| | What kind of context this is. You set it; nothing infers it, and a misspelling is a `400`. Returned on query as `enrichment_kind` (omitted for `auto`). (default=`"auto"`) |
+| | Relations you declare to other items: `{ "ids": ["", ...], "properties": {} }`. Followed on `/query` with `follow_forceful_relations` and returned in `forceful_relations[]`. `relations` is accepted as an alias for the field; `context_ids` or `source_ids` for the `ids` key. |
+| | Principals allowed to retrieve the item: bare emails or `user_email:`, `group:`, `domain:` principals, or `__public__`. Omit for unrestricted; `[]` for nobody. A malformed principal rejects the whole request with `400`. |
+| | Chunk `text` on its markdown structure instead of as flat prose. (default=`false`) |
+| | The speaker for a text item. On a conversation each turn's `name` wins. (default=`"User"`) |
+
+### Limits
+
+- At most **100 items** per request, **1 MiB** of text per item, **8 MiB** of text per request.
+- A validation error names the item as `context[N]`.
+
+### Refused on a unified database
+
+`type`, `documents` (file uploads), `app_knowledge`, `memories`, `evidence_kind`, `evidence_subject`, `expiry_time` and `retain_source` return `400 CORPUS_TYPE_UNSUPPORTED`. Extract text from files and send it as an item; structured app sources arrive through [connectors](/essentials/v2/connectors). Any other key an item does not recognise is dropped without an error.
+
+## Response
+
+`202 Accepted`, with one result per item:
+
+```json
+{
+ "success": true,
+ "data": {
+ "success": true,
+ "message": "Context queued for ingestion successfully. Ingestion is asynchronous: this 202 means the sources were accepted and queued, not indexed. Poll GET /context/status?database=&id= until each source's indexing_status reaches a terminal state (completed or errored) before querying. See https://docs.hydradb.com/api-reference/v2/endpoint/source-status for usage details. ",
+ "results": [
+ { "source_id": "refund-policy", "title": "Refund policy", "status": "queued", "infer": true, "error": null, "error_code": null },
+ { "source_id": "chat-alex-001", "title": null, "status": "queued", "infer": true, "error": null, "error_code": null }
+ ],
+ "success_count": 2,
+ "failed_count": 0
+ },
+ "error": null,
+ "meta": { "request_id": "bcd03673-174d-4a73-83e6-73bfcdc16061", "api_version": "2.0.1" }
+}
+```
+
+| Field | Description |
+| --- | --- |
+| `message` | `Context queued for ingestion successfully` (`Context ingestion completed with some failures` when an item failed), followed by a reminder to poll status. |
+| `results[].source_id` | The item's `context_id`, sent or generated. The result item keeps the name `source_id`; read it as the context id and pass it to [`GET /context/status`](/api-reference/v2/endpoint/source-status). |
+| `results[].title` | The item's `title`, or `null`. |
+| `results[].status` | `queued` or `failed`. A failed item does not stop the others. |
+| `results[].infer` | Mirrors the item's `enrich`. |
+| `results[].error`, `results[].error_code` | Why the item failed; `null` on success. |
+| `success_count`, `failed_count` | Totals across `results`. |
+
+
+**`202 Accepted` means queued, not indexed.** Ingestion is asynchronous. Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the returned ids until each item reaches `completed` or `errored` (`graph_creation` is already searchable), or register a webhook for `indexing.status_changed` events (see [Webhooks](/essentials/v2/webhooks)).
+
+
+
+
+
+ **Related Resources**
+
+ - **Always check** [ingestion status](/api-reference/v2/endpoint/source-status) to ensure context is ready to be retrieved
+ - [Query](/api-reference/v2/endpoint/query) once context is ready
+ - **Inspect:** [List Documents](/api-reference/v2/endpoint/list-documents) helps you fetch titles and descriptions of ingested context
+ - **Inspect:** [Fetch Content](/api-reference/v2/endpoint/fetch-content) returns the title, attributes and content behind a `context_id`
+ - **Cleanup:** [Delete Context](/api-reference/v2/endpoint/delete-source)
+
+
+## Split databases
+
+Everything below applies only to a database created with `type: "split"`. There, `type` picks the corpus and the payload is one of the older shapes: `documents` (files HydraDB parses) or `app_knowledge` (pre-extracted app sources) under `type=knowledge`, or `memories` under `type=memory`. Set `infer: true` on a memory to extract preferences from raw signals, or `infer: false` to store the text verbatim. Read more in [Knowledge](/essentials/v2/knowledge), [App sources](/essentials/v2/app-sources) and [Memories](/essentials/v2/memories); the mapping from each split field to its item field is on [Split databases and legacy fields](/essentials/v2/split-databases#3-split-ingest-fields).
+
+### Split request examples
+
+
+
+```python Python SDK
+import json
+
with open("/path/to/policy.pdf", "rb") as policy:
knowledge_result = client.context.ingest(
# Knowledge requests can include documents, app_knowledge, or both.
@@ -216,9 +411,9 @@ curl -X POST 'https://api.hydradb.com/context/ingest' \
]'
```
-
+
-## Upload in-memory text as a `.txt` file
+### Upload in-memory text as a `.txt` file
If you already have text in memory, create a file-like object and upload it through `documents` as a `text/plain` `.txt` file.
@@ -307,7 +502,7 @@ response.raise_for_status()
```
-## Important form fields
+### Important form fields
| Name | Description |
| --- | --- |
@@ -328,7 +523,7 @@ response.raise_for_status()
-## Supported file formats
+### Supported file formats
Anything in this table can be uploaded through `documents` and HydraDB will read it.
@@ -352,7 +547,7 @@ What HydraDB extracts differs by type, and it is worth knowing which one you are
A scanned page with nothing readable on it, or a photo that happens to contain no text, will index as an empty document rather than fail.
-### Formats we cannot read
+#### Formats we cannot read
These are rejected the moment you upload them, before anything is queued. You get the answer in the upload response rather than minutes later.
@@ -369,7 +564,7 @@ These are rejected the moment you upload them, before anything is queued. You ge
`.heic` is the format iPhones use for photos by default, so it is the one people hit most often without realizing. On iOS you can change this under **Settings > Camera > Formats > Most Compatible**, which makes the camera save JPEGs instead. Or export the photo as JPEG or PDF before uploading.
-### How we decide the format
+#### How we decide the format
The file extension is what counts. `report.pdf` is treated as a PDF because of the `.pdf`, not because of what is inside it.
@@ -379,11 +574,11 @@ If the filename has no extension at all, the `Content-Type` you send with that p
Renaming a file does not convert it. A `.heic` photo renamed to `photo.pdf` passes this check, because the check reads the name, then fails later during parsing and comes back as `errored` on [`GET /context/status`](/api-reference/v2/endpoint/source-status). Upload files under their real extension.
-### Size limit
+#### Size limit
Each file in `documents` can be up to **50 MB**. A larger file is rejected with `413` and a message naming the file, and no part of the request is processed. Split large documents, or upload them separately.
-## When a file is not supported
+### When a file is not supported
A rejected file does not stop the rest of your upload. Each file is checked on its own, so a batch of ten with one bad file still indexes the other nine.
@@ -417,9 +612,9 @@ The request returns `202` as usual. The rejected file comes back with `status: "
-## Common use-cases and their configurations
+### Common use-cases and their configurations
-### Document metadata
+#### Document metadata
Per-document metadata (`id`, `metadata`, `additional_metadata`, `relations`) can be passed alongside each uploaded document to control indexing, filtering, and display. The key list is closed - an item carrying any other key is rejected with a `400` naming it, rather than being silently dropped. See the field reference below.
@@ -794,22 +989,10 @@ Per-document metadata (`id`, `metadata`, `additional_metadata`, `relations`) can
-## Some important notes
+### Some important notes
- **Async indexing.** `202 Accepted` means HydraDB queued the work, not that content is searchable. Poll [Ingestion Status](/api-reference/v2/endpoint/source-status) until `indexing_status` reaches `graph_creation` (searchable) or `completed` (graph-ready).
- **Multipart, not JSON.** This endpoint uses `multipart/form-data`. Stringify all JSON arrays (`metadata`, `app_knowledge`, `memories`) before placing them in the form field.
- **Declare hot schema fields upfront.** Put frequently filtered fields in `metadata`, define them in `database_metadata_schema` with `enable_match: true`, and use `additional_metadata` for free-form display/bookkeeping fields. Define filterable fields when creating the database via [Create Database](/api-reference/v2/endpoint/create-tenant). Additive schema updates exist, but data already ingested is not re-indexed for newly added dense/sparse metadata fields.
- **Memory vs knowledge.** Use `type: "memory"` for memory ingestion, listing, and deletion. Use `type: "all"` on `POST /query` when results should combine both. The multipart field name for memories is always `memories`.
- **Collection defaulting.** Omitting `collection` writes to the default collection. List available collections with [List Collections](/api-reference/v2/endpoint/list-sub-tenants).
-
-
-
-
- **Related Resources**
-
- - **Always check** [ingestion status](/api-reference/v2/endpoint/source-status) to ensure context is ready to be retrieved
- - [Query](/api-reference/v2/endpoint/query) once context is ready
- - **Inspect:** [List Documents](/api-reference/v2/endpoint/list-documents) helps you fetch titles and descriptions of ingested context
- - **Inspect:** [Fetch Content](/api-reference/v2/endpoint/fetch-content) helps you fetch full context of a document, memory, knowledge item
- - **Cleanup:** [Delete Context](/api-reference/v2/endpoint/delete-source)
-
diff --git a/api-reference/v2/endpoint/query-overview.mdx b/api-reference/v2/endpoint/query-overview.mdx
index edf9f0b1..aab96cff 100644
--- a/api-reference/v2/endpoint/query-overview.mdx
+++ b/api-reference/v2/endpoint/query-overview.mdx
@@ -1,11 +1,11 @@
---
title: "Query - Overview"
-description: "Quick reference for query modes, type selection, and when to call each."
+description: "Quick reference for scoping, matching and retrieval modes, and what comes back."
---
import { Field } from "/snippets/field.jsx";
-Use this page to choose the right query shape before opening the full [Query](/api-reference/v2/endpoint/query) endpoint reference. Query has three main decisions: what to query (`type`), how to match (`query_by`), and how much retrieval work to spend (`mode`).
+Use this page to choose the right query shape before opening the full [Query](/api-reference/v2/endpoint/query) endpoint reference. A query has three decisions: where to look (`collection` or `collections`), how to match (`query_by`), and how much retrieval work to spend (`mode`). There is no corpus selector on a unified database: one database is one corpus.
```mermaid
flowchart LR
@@ -14,15 +14,14 @@ classDef standard fill:#0f172a,stroke:#334155,stroke-width:2px,color:#f8fafc;
classDef innovation fill:#CC4515,stroke:#FF571A,stroke-width:3px,color:#ffffff,font-weight:bold;
Q([I want to retrieve context])
-S([What should I query?])
+S([Where should I look?])
M([What kind of match?])
H([query_by: hybrid])
T([query_by: text])
Q --> S
-S -- "Documents / files / app sources" --> M
-S -- "User memories" --> M
-S -- "Both" --> M
+S -- "One collection" --> M
+S -- "Several collections, weighted" --> M
M -- "Best overall relevance" --> H
M -- "Exact term or phrase" --> T
@@ -35,44 +34,44 @@ linkStyle default stroke:#64748b,stroke-width:2px;
| Parameter | Values | Use it for |
|---|---|---|
-| | `"knowledge"`, `"memory"`, `"all"` | Choose the collection. Use `"knowledge"` for shared docs/app sources, `"memory"` for user context, and `"all"` when an answer should use both. |
+| | `string[]` or weighted object | Where to look. A list uses equal normalized weights; an object like `{ "user_alex": 2, "company": 1 }` ranks one scope above another without excluding either. Max 100 collections. `collection` selects a single one. |
| | `"hybrid"`, `"text"` | Choose the matching method. Use `"hybrid"` by default and `"text"` for exact terms or phrases. |
-| | `"fast"`, `"thinking"`, `"auto"` | Choose latency vs quality, or let HydraDB decide. Use `"fast"` for low-latency paths, `"thinking"` for multi-query retrieval, reranking, and forceful-relation context, and `"auto"` to score the query and route to one of the two automatically (defaults to `"thinking"` when the signal is inconclusive; also overrides `graph_context` to match - **the default if `mode` is omitted**). |
+| | `"fast"`, `"thinking"`, `"auto"` | Choose latency vs quality, or let HydraDB decide. `"fast"` for low-latency paths, `"thinking"` for multi-query retrieval, reranking and declared relations, `"auto"` to score the query and route to one of the two (defaults to `"thinking"` when the signal is inconclusive; also overrides `graph_context` to match; **the default if `mode` is omitted**). |
| | integer | Control prompt size. Start with `10`, reduce for tight context windows, increase only when you rerank or summarize downstream. |
-| | `0.0`-`1.0` or `"auto"` | Tune hybrid query. Lower values favor BM25 keywords; higher values favor semantic similarity. |
-| | object | Narrow candidates before ranking. Top-level keys match `metadata`; nested `additional_metadata` filters free-form per-source fields. |
-| | `string[]` or weighted object | Query one or more user/workspace/team scopes. A list uses equal normalized weights; an object like `{ "workspace_42": 2, "user_alex": 1 }` applies relative ranking weights with at most one decimal place. Max 100 collections. |
-| | boolean | Include entity/relation context with the chunks. On by default; set `false` for chunk-only responses. |
-| | boolean | Adds app-aware retrieval while still querying the full selected knowledge scope. Use it for better app-source matching; it does not restrict retrieval to app sources only. |
+| | `0.0` to `1.0` or `"auto"` | Tune hybrid query. Lower values favor BM25 keywords; higher values favor semantic similarity. |
+| | object | Narrow candidates with operators (`$eq`, `$in`, `$gte`, `$and`, ...) on the fields declared in `database_metadata_schema`. |
+| | boolean | Include graph paths in `graph[]`. On by default; set `false` for chunk-only responses. |
+| | boolean | Pull items linked with `forceful_relations` at ingest into `forceful_relations[]`. On by default. |
+| | boolean | Adds app-aware retrieval for connector content while still querying the full selected scope. |
-For filter design, read [Usage - Metadata](/essentials/v2/attributes) before creating database schemas. For exact request fields, defaults, and response shape, use [Query](/api-reference/v2/endpoint/query).
+For filter design, read [Attributes](/essentials/v2/attributes) before creating database schemas. For exact request fields, defaults, and response shape, use [Query](/api-reference/v2/endpoint/query).
## Recommended configurations
| User intent | Recommended config |
|---|---|
-| Fast document RAG | `type="knowledge"`, `query_by="hybrid"`, `mode="fast"`, `max_results=5-10`, `graph_context=false` |
-| Highest-quality document RAG | `type="knowledge"`, `query_by="hybrid"`, `mode="thinking"`, `graph_context=true`, `alpha="auto"` |
-| Personalized answer | `type="all"`, include `collection`, `query_by="hybrid"`, `mode="thinking"` |
-| User preferences only | `type="memory"`, include `collection`, `query_by="hybrid"` |
-| Exact keyword or phrase | `type="knowledge"`, `query_by="text"`, `operator="phrase"` |
-| Recent operational updates | `query_by="hybrid"`, `recency_bias=0.2-0.4`, filter to the right document type |
-| Mixed or unpredictable query complexity | `query_by="hybrid"`, `mode="auto"` - let HydraDB route each query to `fast` or `thinking` |
+| Fast RAG over shared context | `collection` (the shared one), `query_by="hybrid"`, `mode="fast"`, `max_results=5-10`, `graph_context=false` |
+| Highest-quality RAG | `query_by="hybrid"`, `mode="thinking"`, `graph_context=true`, `alpha="auto"` |
+| Personalized answer | `collections={ "": 2, "": 1 }`, `query_by="hybrid"`, `mode="thinking"` |
+| A person's preferences only | `collection=""`, `query_by="hybrid"` |
+| Exact keyword or phrase | `query_by="text"`, `operator="phrase"` |
+| Recent operational updates | `query_by="hybrid"`, `recency_bias=0.2-0.4`, `attributes` on the right kind of item |
+| Mixed or unpredictable query complexity | `query_by="hybrid"`, `mode="auto"`; let HydraDB route each query to `fast` or `thinking` |
## Typical patterns
-
+
-Use this for standard RAG over docs, PDFs, tickets, pages, or app sources.
+Use this for standard RAG over docs, policies, tickets, pages, or connector content in a shared collection.
```json
{
"database": "acme",
+ "collection": "company",
"query": "What is our refund policy?",
- "type": "knowledge",
"query_by": "hybrid",
"mode": "thinking",
"max_results": 10,
@@ -82,16 +81,15 @@ Use this for standard RAG over docs, PDFs, tickets, pages, or app sources.
-
+
-Use this when the answer should combine shared knowledge with user-specific context. Always pass the same `collection` used at memory ingestion.
+Use this when the answer should combine shared context with a person's own. Weight the person's collection above the shared one; the weights rank, they do not exclude.
```json
{
"database": "acme",
- "collection": "user_alex",
+ "collections": { "user_alex": 2, "company": 1 },
"query": "What is our refund policy, and how should I explain it to this user?",
- "type": "all",
"query_by": "hybrid",
"mode": "thinking"
}
@@ -111,7 +109,6 @@ Use this when the same question should search several collection scopes and retu
"user_alex": 1
},
"query": "What renewal risks should we discuss?",
- "type": "all",
"query_by": "hybrid",
"mode": "thinking"
}
@@ -121,19 +118,18 @@ Use this when the same question should search several collection scopes and retu
-Use `metadata_filters` when you already know the slice you want. Top-level keys match schema-backed `metadata` fields; declare hot filters in `database_metadata_schema` with `enable_match: true`. Free-form per-source fields go under `additional_metadata` (`document_metadata` is a legacy alias). Multiple filters are ANDed exact-match constraints.
+Use `attributes` when you already know the slice you want. Keys are the fields declared in `database_metadata_schema` and sent as `attributes` at ingest; clauses combine with `$and` and `$or`.
```json
{
"database": "acme",
"query": "What launch constraints apply to enterprise customers?",
- "type": "knowledge",
"query_by": "hybrid",
- "metadata_filters": {
- "department": "product",
- "additional_metadata": {
- "source": "launch_plan"
- }
+ "attributes": {
+ "$and": [
+ { "department": { "$eq": "product" } },
+ { "region": { "$in": ["us", "eu"] } }
+ ]
}
}
```
@@ -148,7 +144,6 @@ Use text query when literal wording matters: legal clauses, SKUs, error codes, I
{
"database": "acme",
"query": "GDPR Article 17",
- "type": "knowledge",
"query_by": "text",
"operator": "phrase"
}
@@ -159,11 +154,24 @@ Use text query when literal wording matters: legal clauses, SKUs, error codes, I
## Response summary
-`POST /query` returns ranked `data.chunks[]`, deduplicated `data.sources[]`, optional `data.graph_context`, and optional `data.additional_context` from forceful relations. Preserve `data.chunks[]` order when building prompts; HydraDB has already ranked the results. For prompt formatting and citation patterns, see [How to Use API Results](/essentials/v2/api-results).
+`POST /query` returns exactly four keys under `data`:
+
+| Key | Contents |
+| --- | --- |
+| `chunks[]` | Ranked matches: `chunk_id`, `context_id`, `score`, `content`, optional `enrichment` (a string), `enrichment_kind` and `temporal`. No source details; call `GET /context/inspect` with the `context_id` for those. |
+| `graph[]` | Paths through the context graph: `origin` (`query_path` or `chunk_relation`), `triplets[]` and a `path_summary`, which is never empty. Each hop's `relation.chunk_id` names the chunk it came from; a `chunk_relation` path is only returned when one of its hops came from a returned chunk or a `forceful_relations` chunk. |
+| `forceful_relations[]` | Chunks linked with `forceful_relations` at ingest, each with the `via` that brought it in. |
+| `llm_prompt` | A server-built markdown string, ready to inject into a model call: results cited `[1]`, forceful relations `[R1]`, related facts labelled `[P1]` in `graph[]` order with each path's relevance when it has one, then temporal facts and sources. |
+
+Inject `llm_prompt` for the model; preserve `chunks[]` order when you render results yourself. See [How to Use API Results](/essentials/v2/api-results).
+
+## Split databases
+
+A database created with `type: "split"` adds one decision: `type` (`"knowledge"`, `"memory"` or `"all"`) picks the corpus, and the response is the older shape (`chunk_content`, `sources[]`, `graph_context`, `additional_context`). See the [Split databases](/api-reference/v2/endpoint/query#split-databases) section of the endpoint reference.
## Related sections
-- [Query](/api-reference/v2/endpoint/query) - full endpoint reference
-- [Usage - Query](/essentials/v2/query) - conceptual overview, retrieval modes, and ranking behavior
-- [Usage - Metadata](/essentials/v2/attributes) - filtering with database and document metadata
-- [Concepts - Context Graphs](/essentials/v2/context-graphs) - graph context and relation paths
+- [Query](/api-reference/v2/endpoint/query): full endpoint reference
+- [Usage: Query](/essentials/v2/query): conceptual overview, retrieval modes, and ranking behavior
+- [Usage: Attributes](/essentials/v2/attributes): filtering with declared attributes
+- [Concepts: Context Graphs](/essentials/v2/context-graphs): graph paths and relations
diff --git a/api-reference/v2/endpoint/query.mdx b/api-reference/v2/endpoint/query.mdx
index bc134bf2..739b66c3 100644
--- a/api-reference/v2/endpoint/query.mdx
+++ b/api-reference/v2/endpoint/query.mdx
@@ -1,20 +1,21 @@
---
title: "Query"
-description: "Unified retrieval over knowledge, memories, or both."
-openapi: "api-reference/v2/openapi.json POST /query"
+description: "Retrieve ranked chunks, graph paths, forceful relations and a prompt-ready string from a database in one call."
---
import { Field } from "/snippets/field.jsx";
+import LegacyLine from "/snippets/legacy-line.mdx";
-The single retrieval endpoint for everything. Use it any time you need to feed an LLM with grounded context, surface user preferences, or fetch chunks ranked by relevance.
+The single retrieval endpoint. Use it any time you need to feed an LLM with grounded, personalized context, or fetch chunks ranked by relevance.
-Three independent dimensions control behavior:
+Two dimensions control behavior on a unified database:
-- **`type`** picks **what** to query: `"knowledge"`, `"memory"`, or `"all"` (both, merged and re-ranked together).
-- **`query_by`** picks **how** to match: `"hybrid"` (semantic + BM25, the default) or `"text"` (BM25 only - pair with `operator`).
-- **`mode`** picks **how** to rank results: `"fast"` (single-pass, low-latency), `"thinking"` (expands query, reranks, and can include forceful-relation context), or `"auto"` (scores the query and routes to `"fast"` or `"thinking"` automatically, defaulting to `"thinking"` when the signal is inconclusive - **the default if `mode` is omitted**).
+- **`query_by`** picks **how** to match: `"hybrid"` (semantic + BM25, the default) or `"text"` (BM25 only, pair with `operator`).
+- **`mode`** picks **how much** retrieval work to spend: `"fast"` (single pass, low latency), `"thinking"` (expands the query, reranks, follows declared relations) or `"auto"` (scores the query and routes to one of the two, defaulting to `"thinking"` when the signal is inconclusive; **the default when `mode` is omitted**).
-Read more about choosing the perfect mode for your use case [here](/api-reference/v2/endpoint/query-overview#recommended-configurations).
+There is no `type`: a unified database is one corpus. Scope with `collection` or `collections`. The guide is [Query](/essentials/v2/query); this page is the field reference.
+
+
`database` and `collection` are the current field names (formerly `tenant_id` and `sub_tenant_id`). The old names remain accepted as deprecated aliases for full backward compatibility.
@@ -25,11 +26,8 @@ Read more about choosing the perfect mode for your use case [here](/api-referenc
```python Python SDK
result = client.query(
database="acme_corp",
- collection="user_alex",
- query="What is our refund policy, and how should I explain it to this user?",
-
- # What to query: "knowledge", "memory", or "all".
- type="all",
+ collection="support",
+ query="How are refunds processed, and how should I answer this user?",
# How to match: "hybrid" (default) or "text" (BM25).
query_by="hybrid",
@@ -43,31 +41,21 @@ result = client.query(
recency_bias=0.2,
graph_context=True,
- # Pull author-declared related sources into additional_context.
- # Only applies when mode="thinking".
- query_forceful_relations=True,
-
- # Top-level keys match metadata; additional_metadata is per-source.
- metadata_filters={
- "department": "support",
- "additional_metadata": {
- "source": "policy",
- },
- },
+ # Pull in the items each hit declared with forceful_relations at ingest.
+ follow_forceful_relations=True,
- # Short factual hint; not a hard filter.
- additional_context="User is asking from the billing help center.",
+ # Hard filter on declared attributes.
+ attributes={"department": {"$eq": "support"}},
)
+
+print(result.data.llm_prompt)
```
```typescript TypeScript SDK
const result = await client.query({
database: "acme_corp",
- collection: "user_alex",
- query: "What is our refund policy, and how should I explain it to this user?",
-
- // What to query: "knowledge", "memory", or "all".
- type: "all",
+ collection: "support",
+ query: "How are refunds processed, and how should I answer this user?",
// How to match: "hybrid" (default) or "text" (BM25).
queryBy: "hybrid",
@@ -81,21 +69,14 @@ const result = await client.query({
recencyBias: 0.2,
graphContext: true,
- // Pull author-declared related sources into additional_context.
- // Only applies when mode: "thinking".
- queryForcefulRelations: true,
+ // Pull in the items each hit declared with forceful_relations at ingest.
+ followForcefulRelations: true,
- // Top-level keys match metadata; additional_metadata is per-source.
- metadataFilters: {
- department: "support",
- additional_metadata: {
- source: "policy",
- },
- },
-
- // Short factual hint; not a hard filter.
- additionalContext: "User is asking from the billing help center.",
+ // Hard filter on declared attributes.
+ attributes: { department: { $eq: "support" } },
});
+
+console.log(result.data.llmPrompt);
```
```bash cURL
@@ -105,23 +86,16 @@ curl -X POST 'https://api.hydradb.com/query' \
-H "Content-Type: application/json" \
-d '{
"database": "acme_corp",
- "collection": "user_alex",
- "query": "What is our refund policy, and how should I explain it to this user?",
- "type": "all",
+ "collection": "support",
+ "query": "How are refunds processed, and how should I answer this user?",
"query_by": "hybrid",
"mode": "thinking",
"max_results": 10,
"alpha": "auto",
"recency_bias": 0.2,
"graph_context": true,
- "query_forceful_relations": true,
- "metadata_filters": {
- "department": "support",
- "additional_metadata": {
- "source": "policy"
- }
- },
- "additional_context": "User is asking from the billing help center."
+ "follow_forceful_relations": true,
+ "attributes": { "department": { "$eq": "support" } }
}'
```
@@ -135,8 +109,7 @@ Use `collections` when one query should fan out across multiple user, workspace,
{
"database": "acme_corp",
"collections": ["workspace_42", "user_alex"],
- "query": "What renewal risks should I know about?",
- "type": "all"
+ "query": "What renewal risks should I know about?"
}
```
@@ -147,8 +120,7 @@ Use `collections` when one query should fan out across multiple user, workspace,
"workspace_42": 2,
"user_alex": 1
},
- "query": "What renewal risks should I know about?",
- "type": "all"
+ "query": "What renewal risks should I know about?"
}
```
@@ -156,58 +128,21 @@ A list gives every collection equal normalized weight. An object treats values a
> **Caching tip:** `collections` list order is not semantically significant for fanout selection. Sort list values before constructing cache keys; for weighted objects, sort keys and keep weights at the documented one-decimal precision so equivalent calls share the same cache entry.
-### Transforming the response into LLM context
-
-Use `build_string` / `buildString` from the SDK. It takes any `POST /query` result and returns a formatted plain string.
-
-
-```python Python SDK
-from hydra_db import HydraDB
-from hydra_db.helpers import build_string
-
-client = HydraDB(token="YOUR_API_KEY")
+### Using the response
-result = client.query(
- database="your-database",
- collection="your-collection",
- query="How does authentication work?",
- type="knowledge",
- query_by="hybrid",
- max_results=5,
- mode="fast",
- graph_context=True,
-)
+`data.llm_prompt` is the whole context block as one markdown document: `# Query results`, then `## Results` (cited `[1]`), `## Forceful relations` (`[R1]`), `## Related facts` (`[P1]`, one per path in `graph[]`), `## Temporal facts` and `## Sources`. The SDK `build_string` / `buildString` helpers return it verbatim on a unified database. Inject it directly:
-context = build_string(result)
+```python
+messages = [{"role": "system", "content": result.data.llm_prompt},
+ {"role": "user", "content": question}]
```
-```typescript TypeScript SDK
-import { HydraDBClient } from "@hydradb/sdk";
-import { buildString } from "@hydradb/sdk/helpers";
-
-const client = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY });
-
-const result = await client.query({
- database: "your-database",
- collection: "your-collection",
- query: "How does authentication work?",
- type: "knowledge",
- queryBy: "hybrid",
- maxResults: 5,
- mode: "fast",
- graphContext: true,
-});
-
-const context = buildString(result);
-```
-
-
-
+Read `data.chunks`, `data.graph` and `data.forceful_relations` when you need structure. See [How to Use API Results](/essentials/v2/api-results).
## Common use-cases and their configurations
-
+
```bash cURL
@@ -217,8 +152,8 @@ curl -X POST 'https://api.hydradb.com/query' \
-H "Content-Type: application/json" \
-d '{
"database": "acme_corp",
+ "collection": "company",
"query": "What is our refund policy?",
- "type": "knowledge",
"query_by": "hybrid",
"mode": "thinking",
"max_results": 10,
@@ -229,8 +164,8 @@ curl -X POST 'https://api.hydradb.com/query' \
```typescript TypeScript SDK
const result = await client.query({
database: "acme_corp",
+ collection: "company",
query: "What is our refund policy?",
- type: "knowledge",
queryBy: "hybrid",
mode: "thinking",
maxResults: 10,
@@ -241,8 +176,8 @@ const result = await client.query({
```python Python SDK
result = client.query(
database="acme_corp",
+ collection="company",
query="What is our refund policy?",
- type="knowledge",
query_by="hybrid",
mode="thinking",
max_results=10,
@@ -253,7 +188,7 @@ result = client.query(
-
+
```bash cURL
@@ -263,9 +198,8 @@ curl -X POST 'https://api.hydradb.com/query' \
-H "Content-Type: application/json" \
-d '{
"database": "acme_corp",
- "collection": "user_alex",
+ "collections": { "user_alex": 2, "company": 1 },
"query": "What is our refund policy, and how should I explain it to this user?",
- "type": "all",
"query_by": "hybrid",
"mode": "thinking"
}'
@@ -274,9 +208,8 @@ curl -X POST 'https://api.hydradb.com/query' \
```typescript TypeScript SDK
const result = await client.query({
database: "acme_corp",
- collection: "user_alex",
+ collections: { user_alex: 2, company: 1 },
query: "What is our refund policy, and how should I explain it to this user?",
- type: "all",
queryBy: "hybrid",
mode: "thinking",
});
@@ -285,9 +218,8 @@ const result = await client.query({
```python Python SDK
result = client.query(
database="acme_corp",
- collection="user_alex",
+ collections={"user_alex": 2, "company": 1},
query="What is our refund policy, and how should I explain it to this user?",
- type="all",
query_by="hybrid",
mode="thinking",
)
@@ -296,7 +228,7 @@ result = client.query(
-
+
```bash cURL
@@ -308,9 +240,7 @@ curl -X POST 'https://api.hydradb.com/query' \
"database": "acme_corp",
"collection": "user_alex",
"query": "Does the user have any specific preferences for tone or response length?",
- "type": "memory",
- "query_by": "hybrid",
- "query_apps": true
+ "query_by": "hybrid"
}'
```
@@ -319,9 +249,7 @@ const result = await client.query({
database: "acme_corp",
collection: "user_alex",
query: "Does the user have any specific preferences for tone or response length?",
- type: "memory",
queryBy: "hybrid",
- queryApps: true,
});
```
@@ -330,16 +258,14 @@ result = client.query(
database="acme_corp",
collection="user_alex",
query="Does the user have any specific preferences for tone or response length?",
- type="memory",
query_by="hybrid",
- query_apps=True,
)
```
-
+
```bash cURL
@@ -350,7 +276,6 @@ curl -X POST 'https://api.hydradb.com/query' \
-d '{
"database": "acme_corp",
"query": "GDPR Article 17",
- "type": "knowledge",
"query_by": "text",
"operator": "phrase"
}'
@@ -360,7 +285,6 @@ curl -X POST 'https://api.hydradb.com/query' \
const result = await client.query({
database: "acme_corp",
query: "GDPR Article 17",
- type: "knowledge",
queryBy: "text",
operator: "phrase",
});
@@ -370,7 +294,6 @@ const result = await client.query({
result = client.query(
database="acme_corp",
query="GDPR Article 17",
- type="knowledge",
query_by="text",
operator="phrase",
)
@@ -379,7 +302,7 @@ result = client.query(
-
+
```bash cURL
@@ -390,7 +313,6 @@ curl -X POST 'https://api.hydradb.com/query' \
-d '{
"database": "acme_corp",
"query": "How does the Q2 partnership between Acme and Globex affect our SLA with Initech?",
- "type": "knowledge",
"query_by": "hybrid",
"mode": "auto"
}'
@@ -400,7 +322,6 @@ curl -X POST 'https://api.hydradb.com/query' \
const result = await client.query({
database: "acme_corp",
query: "How does the Q2 partnership between Acme and Globex affect our SLA with Initech?",
- type: "knowledge",
queryBy: "hybrid",
mode: "auto",
});
@@ -410,15 +331,13 @@ const result = await client.query({
result = client.query(
database="acme_corp",
query="How does the Q2 partnership between Acme and Globex affect our SLA with Initech?",
- type="knowledge",
query_by="hybrid",
mode="auto",
)
```
-
- HydraDB scores the query before retrieval and routes it to `"fast"` or `"thinking"` - a query naming several distinct entities like this one is likely to route to `"thinking"`. Use `"auto"` for traffic where query complexity varies call-to-call and you don't want to hand-pick per request. This is also the default: an omitted `mode` field behaves exactly like `mode: "auto"`. Set `mode` to `"fast"` or `"thinking"` explicitly if you want a deterministic pipeline instead.
+ HydraDB scores the query before retrieval and routes it to `"fast"` or `"thinking"`; a query naming several distinct entities like this one is likely to route to `"thinking"`. Use `"auto"` for traffic where query complexity varies call-to-call and you do not want to hand-pick per request. This is also the default: an omitted `mode` field behaves exactly like `mode: "auto"`. Set `mode` to `"fast"` or `"thinking"` explicitly if you want a deterministic pipeline instead.
@@ -427,27 +346,36 @@ result = client.query(
| Name | Description |
| --- | --- |
| | Owning database. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). |
-| | Single collection scope. Required for per-user memory queries. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=default collection) |
-| | Multi-collection scope. Send a list of collection IDs for equal weighting, or an object mapping collection ID to a positive relative weight (at most one decimal place, e.g. `{"finance": 1.5, "legal": 0.8}`) to bias ranking. Up to 100 collections. Do not combine with `collection`/`sub_tenant_id`. Formerly `sub_tenant_ids`; the `sub_tenant_ids` alias is still accepted (deprecated since 2.0.1). |
+| | Single collection scope. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). (default=default collection) |
+| | Multi-collection scope. Send a list of collection IDs for equal weighting, or an object mapping collection ID to a positive relative weight (at most one decimal place, e.g. `{"user_alex": 2, "company": 1}`) to bias ranking. Up to 100 collections. Do not combine with `collection`. Formerly `sub_tenant_ids` (deprecated). |
| | Query terms or natural-language question. Cannot be empty. |
-| | What collection to query. `"all"` runs knowledge and memory in parallel and merges by `relevancy_score`. (default=`"knowledge"`) |
| | Retrieval method. See [Query methods](#decision-matrix). (default=`"hybrid"`) |
-| | Adds an app-aware retrieval lane for app sources while still querying the full selected knowledge scope. Set `true` for better app-source matching, thread/relation traversal, exact IDs, and actor/provider hints. It does **not** limit query to only app sources. (default=`false`) |
| | BM25 operator for `query_by: "text"`. Ignored for `hybrid`. (default=`"or"`) |
-| | Retrieval pipeline. Applies to `hybrid` only; ignored for `text`. `"auto"` scores the query before retrieval and resolves it to `"fast"` or `"thinking"`, defaulting to `"thinking"` when the signal is inconclusive; it also overrides whatever `graph_context` you sent to match that resolved mode. (default=`"auto"`) |
+| | Retrieval pipeline. Applies to `hybrid` only; ignored for `text`. `"auto"` scores the query and resolves it to `"fast"` or `"thinking"`, defaulting to `"thinking"` when the signal is inconclusive; it also overrides whatever `graph_context` you sent to match that resolved mode. (default=`"auto"`) |
| | Maximum chunks to return. Default `10`; maximum `50`. Start with `10`, use `5` for tight prompts, and increase only when reranking downstream. |
-| | Hybrid weight (`1.0` = pure semantic, `0.0` = pure BM25). Applies to `query_by: "hybrid"` only. (default=`0.8`) |
-| | Boost newer content. (default=`0.0`) |
-| | When `true`, includes the entity/relation graph slice in the response under `graph_context`. Set to `false` when you only need ranked chunks. Relations you supplied via [Bring Your Own Graph](/essentials/v2/bring-your-own-graph) appear here identically to extracted ones. (default=`true`) - **under `mode: "auto"`, this value is overridden by the resolved mode regardless of what you send.** |
-| | Pull author-declared related sources into `additional_context`. **Only takes effect when `mode` resolves to `"thinking"`** - silently ignored in `fast` mode, and under `mode: "auto"` whether it takes effect depends on the automatic routing decision. (default=`true`) |
-| | Request-time hint to guide retrieval (e.g., "user is on the billing page"). This is different from the response `additional_context` map. (default=`null`) |
-| | Deterministic narrowing before ranking. See [Filters](#decision-matrix). Each list holds at most 500 values, and the whole object is capped at 64 KiB of compact JSON, measured after operator objects are reduced to their values; over either returns `400`. (default=`null`) |
+| | Hybrid weight (`1.0` = pure semantic, `0.0` = pure BM25). Applies to `query_by: "hybrid"` only. (default=`0.8`) |
+| | Boost newer content. Send `0` to disable recency entirely. |
+| | Restrict retrieval to these `context_id`s. A scoped search that matches nothing returns nothing. |
+| | Restrict retrieval to items with one of these exact titles (case-insensitive, ORed). Intersected with `ids` when both are sent. |
+| | Adds an app-aware retrieval lane for connector content (exact IDs, actors, thread and parent traversal) while still querying the full selected scope. Set `false` to skip it. |
+| | Principals to answer as: only items they may retrieve are returned. Omit, or send `[]` or `["*"]`, for no access scoping. See [Access control](/essentials/v2/access-control). |
+| | Hard filter on declared attributes with operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`, `$or`, `$not`, `$exists`. Applies to chunks, forceful relations and graph paths alike. See [Filters](#decision-matrix). |
+| | When `true`, includes graph paths in `graph[]`. Set to `false` when you only need ranked chunks; `graph` is then `[]`. Relations you supplied via [Bring Your Own Graph](/essentials/v2/bring-your-own-graph) appear identically to extracted ones. (default=`true`) **Under `mode: "auto"`, this value is overridden by the resolved mode.** |
+| | Pull the items each hit declared with `forceful_relations` at ingest into `forceful_relations[]`. Set to `false` for `forceful_relations: []`. `query_forceful_relations` is the deprecated alias. (default=`true`) |
+| | Resolve time-based questions (current, as of, ranges, upcoming) and return matched facts in `chunks[].temporal`. Never changes which chunks are returned. (default=`true`) |
+| | ISO 8601 time to treat as now for temporal reasoning. Set it when replaying past conversations. |
+| | Override the temporal intent HydraDB would infer from the query. |
+| | Deprecated. The older filter language; still accepted on every database and ANDed with `attributes` when both are sent. See [Split databases](#split-databases). |
+
+
+**Do not send `type` on a unified database.** Absent, `"all"` and `"unified"` are accepted and mean the one corpus. `"knowledge"` and `"memory"` return `400 CORPUS_TYPE_UNSUPPORTED`. Detect a database's layout once from `GET /databases` (`details[].type`) rather than branching on a request flag.
+
**Tuning heuristics.**
- alpha: start at 0.8. Lower toward 0.3–0.5 when the query contains literal tokens (error codes, SKUs, product names). Raise toward 0.9 for conceptual questions. Use "auto" when query shape varies.
- recency_bias: leave at 0 for static reference material. Set 0.2–0.4 for mixed content, 0.6–0.8 for changelogs, news, or status updates.
+ alpha: start at 0.8. Lower toward 0.3 to 0.5 when the query contains literal tokens (error codes, SKUs, product names). Raise toward 0.9 for conceptual questions. Use "auto" when query shape varies.
+ recency_bias: send 0 for static reference material. Set 0.2 to 0.4 for mixed content, 0.6 to 0.8 for changelogs, news, or status updates.
max_results: start at 10. Drop to 5 for tight context windows; raise to 20 if you rerank downstream.
@@ -455,15 +383,6 @@ result = client.query(
### Decision matrix
-
- | Value | Queries | Best for |
- |---|---|---|
- | `"knowledge"` *(default)* | Knowledge documents, files, and app sources | Document Q&A, RAG context. |
- | `"query_apps=true"` | Full selected knowledge scope plus app-aware retrieval | App-specific Q&A that should still query non-app knowledge documents. |
- | `"memory"` | User memories | Personalization and user preferences. |
- | `"all"` | Both, merged in one ranked result set | Personalized answers grounded in both shared and user-specific context. |
-
-
| Method | Pipeline | Best for |
|---|---|---|
@@ -477,66 +396,36 @@ result = client.query(
| Mode | Behavior | When to use |
|---|---|---|
| `"fast"` | Single query pass | Real-time chat, autocomplete, simple lookups. |
- | `"thinking"` | Multi-query expansion + reranking + forceful-relation context | Complex queries, customer-facing answers, anything where quality matters. |
- | `"auto"` *(default if `mode` is omitted)* | Scores the query before retrieval and routes to `"fast"` or `"thinking"`; defaults to `"thinking"` when the signal is inconclusive. Also overrides `graph_context` to match whichever mode it picks. | Mixed or unpredictable query traffic where you don't want to hand-pick per request. |
+ | `"thinking"` | Multi-query expansion + reranking + declared relations | Complex queries, customer-facing answers, anything where quality matters. |
+ | `"auto"` *(default if `mode` is omitted)* | Scores the query before retrieval and routes to `"fast"` or `"thinking"`; defaults to `"thinking"` when the signal is inconclusive. Also overrides `graph_context` to match whichever mode it picks. | Mixed or unpredictable query traffic where you do not want to hand-pick per request. |
- `"auto"`'s resolved pipeline isn't reported back in the response, so budget latency as thinking-level in the worst case. Omitting `mode` behaves exactly like `mode: "auto"` - set it explicitly to `"fast"` or `"thinking"` if you want a deterministic pipeline instead.
+ `"auto"`'s resolved pipeline is not reported back in the response, so budget latency as thinking-level in the worst case.
- `metadata_filters` are hard exact-match constraints applied before ranking and re-checked after hydration. The shape combines two filter scopes:
+ `attributes` is a hard constraint applied during retrieval, on the fields declared in the database's `database_metadata_schema` and sent as `attributes` at ingest. It uses operators:
```json
{
- "metadata_filters": {
- "department": "engineering",
- "region": "us-east",
- "additional_metadata": {
- "source": "account_plan",
- "author": "alex"
- }
+ "attributes": {
+ "$and": [
+ { "department": { "$eq": "support" } },
+ { "region": { "$in": ["us", "eu"] } },
+ { "priority": { "$gte": 3 } }
+ ]
}
}
```
- | Where | What it matches |
- |---|---|
- | **Top-level keys** (`department`, `region`) | The source's schema-backed `metadata`. Keys must be declared in the database's `database_metadata_schema` with `enable_match: true`, otherwise they are silently ignored. |
- | **Nested under `additional_metadata`** | The source's free-form per-document fields. No schema declaration required. `document_metadata` is accepted as a legacy alias. |
-
- Separate keys are ANDed. Each `metadata` (top-level) key takes an operator object naming the comparison:
-
- | Operator | Value | Matches |
- | --- | --- | --- |
- | `equals` | a single value | sources whose field is **exactly** that value |
- | `contains` | a single value | sources whose field **holds** that value. Multi-value fields are stored comma-joined, so this matches one member of that list |
- | `contains_any` | an array | sources holding **any one** of the listed values (OR/IN) |
-
- ```json
- "metadata_filters": {
- "department": { "equals": "legal" },
- "attendee_emails": { "contains": "b@company.com" },
- "tags": { "contains_any": ["alpha", "beta"] }
- }
- ```
-
- Adding values to `contains_any` **widens** the result set. There is no ALL/AND operator within a single key, and range and fuzzy operators are not supported; run multiple queries or post-process client-side for those cases.
-
- Operators apply to `metadata` (top-level keys) only. Inside `additional_metadata`, use a bare scalar for an exact match or a bare array to match any listed value.
+ | Operator | Meaning |
+ | --- | --- |
+ | `$eq`, `$ne` | equal, not equal |
+ | `$gt`, `$gte`, `$lt`, `$lte` | comparisons |
+ | `$in`, `$nin` | value is (not) one of a list |
+ | `$exists` | the field is present |
+ | `$and`, `$or`, `$not` | combine or negate clauses |
-
- An operator used inside `additional_metadata` is **not** rejected. It is read as an exact-match filter against a stored object, so on a normal field it matches nothing and the request returns `200` with an empty result rather than an error.
-
-
-
- The bare forms still work and are unchanged, but are **deprecated** in favour of the operators, because the comparison they perform is inferred from the JSON shape rather than stated. A bare scalar behaves as `equals`, a bare array as `contains_any`, and a bare single-element array as `contains` - so `{"emails": "a@x"}` and `{"emails": ["a@x"]}` differ by one character and return different results.
-
-
- `contains`, `contains_any` and lists are supported on `VARCHAR` fields only: any of them passed for a declared field of another type is rejected with `400 VALIDATION_ERROR`. `equals` works on every declared type, so `{"priority": {"equals": 7}}` is valid on an `INT64` field.
-
- A known operator given the wrong operand type, or several operators in one object, is rejected with `400 VALIDATION_ERROR`. A **misspelled** operator is not: `{"contian": "x"}` is indistinguishable from a filter for a stored object with that key, so it is left alone and matches nothing.
-
- `contains`, `contains_any` and `equals` are reserved key names: an object built only from them is read as an operator and can no longer exact-match a stored object, and an object whose keys are ALL operator names is rejected with `400`. Mixing an operator name with any other key (`{"contains": "a", "other": 1}`) is unaffected. A caller needing the reserved shape must rename the nested key or the field.
+ Filters apply to chunks, forceful relations and graph paths alike; a valid filter that matches nothing returns an empty result, never a widened search. `custom_attributes` are not filterable. See [Attributes](/essentials/v2/attributes).
@@ -548,153 +437,98 @@ result = client.query(
"data": {
"chunks": [
{
- "chunk_uuid": "policy_main_chunk_3",
- "id": "policy_main",
- "chunk_content": "Refunds are issued within 30 days...",
- "source_type": "pdf",
- "source_title": "Compliance Policy",
- "source_upload_time": "2026-05-12T08:14:00Z",
- "source_last_updated_time": "2026-05-12T08:14:00Z",
- "layout": "{\"offsets\":{\"document_level_start_index\":1024},\"page\":3}",
- "relevancy_score": 0.91,
- "extra_context_ids": ["pref-tone"],
- "metadata": { "department": "legal" },
- "additional_metadata": { "author": "Legal Team" }
- }
- ],
- "sources": [
+ "chunk_id": "ck_policy_3",
+ "context_id": "refund-policy",
+ "score": 0.91,
+ "content": "Refunds are processed within 30 days of purchase by the Finance Department.",
+ "enrichment": "Refund window is 30 days; Finance owns refund processing.",
+ "enrichment_kind": "business_knowledge",
+ "temporal": [
+ {
+ "content": "Refund policy effective_from June 2026. Start: 2026-06-01",
+ "start_date": "2026-06-01",
+ "end_date": null
+ }
+ ]
+ },
{
- "id": "policy_main",
- "title": "Compliance Policy",
- "type": "pdf",
- "description": "",
- "url": "",
- "timestamp": "2026-05-12T08:14:00Z",
- "metadata": { "department": "legal" },
- "additional_metadata": { "author": "Legal Team" },
- "app_kind": null,
- "app_provider": null,
- "app_external_id": null
+ "chunk_id": "ck_chat_1",
+ "context_id": "chat-2026-07-29",
+ "score": 0.84,
+ "content": "user: Keep refund answers short please\nassistant: Got it.",
+ "enrichment": "User prefers short answers about refunds.",
+ "enrichment_kind": "user_preference"
}
],
- "graph_context": {
- "query_paths": [
- {
- "triplets": [
- {
- "source": {
- "name": "Compliance Policy",
- "type": "DOCUMENT",
- "namespace": "default",
- "entity_id": "entity_compliance_policy",
- "identifier": "https://api.hydradb.com/docs/compliance_policy"
- },
- "relation": {
- "canonical_predicate": "GOVERNS",
- "raw_predicate": "governs and regulates",
- "context": "The compliance policy governs the refund processing timeline of 30 days.",
- "confidence": 0.95,
- "temporal_details": null,
- "timestamp": 1778573640.0,
- "relationship_id": "rel_governs_refunds",
- "chunk_id": "policy_main_chunk_3",
- "source_entity_id": "entity_compliance_policy",
- "target_entity_id": "entity_refund_processing"
- },
- "target": {
- "name": "Refund Processing",
- "type": "PROCESS",
- "namespace": "default",
- "entity_id": "entity_refund_processing",
- "identifier": null
- }
+ "graph": [
+ {
+ "origin": "query_path",
+ "triplets": [
+ {
+ "source": {
+ "entity_id": "ent_refunds",
+ "name": "Refund Processing"
+ },
+ "relation": {
+ "predicate": "managed by",
+ "context": "Refund processing is managed by the Finance Department.",
+ "relationship_id": "rel_managed_by",
+ "chunk_id": "ck_policy_3"
+ },
+ "target": {
+ "entity_id": "ent_finance",
+ "name": "Finance Department"
}
- ],
- "relevancy_score": 0.89,
- "combined_context": "The Compliance Policy governs the Refund Processing, which regulates refunds.",
- "group_id": null,
- "source_chunk_ids": ["policy_main_chunk_3"]
- }
- ],
- "chunk_relations": [
- {
- "triplets": [
- {
- "source": {
- "name": "Refund Processing",
- "type": "PROCESS",
- "namespace": "default",
- "entity_id": "entity_refund_processing",
- "identifier": null
- },
- "relation": {
- "canonical_predicate": "MANAGED_BY",
- "raw_predicate": "is managed by",
- "context": "Refund processing is managed by the Finance Department.",
- "confidence": 0.9,
- "temporal_details": "Q2 2026 onwards",
- "timestamp": 1778573640.0,
- "relationship_id": "rel_managed_by_finance",
- "chunk_id": "policy_main_chunk_3",
- "source_entity_id": "entity_refund_processing",
- "target_entity_id": "entity_finance_dept"
- },
- "target": {
- "name": "Finance Department",
- "type": "ORGANIZATION",
- "namespace": "default",
- "entity_id": "entity_finance_dept",
- "identifier": "finance@hydradb.com"
- }
+ }
+ ],
+ "path_summary": "Refund processing is managed by the Finance Department."
+ },
+ {
+ "origin": "chunk_relation",
+ "triplets": [
+ {
+ "source": {
+ "entity_id": "ent_user",
+ "name": "User"
+ },
+ "relation": {
+ "predicate": "prefers",
+ "context": "The user prefers short answers about refunds.",
+ "relationship_id": "rel_prefers",
+ "chunk_id": "ck_chat_1"
+ },
+ "target": {
+ "entity_id": "ent_short",
+ "name": "short answers"
}
- ],
- "relevancy_score": 0.82,
- "combined_context": "Refund Processing is managed by the Finance Department.",
- "group_id": "p_0",
- "source_chunk_ids": ["policy_main_chunk_3"]
- }
- ],
- "chunk_id_to_group_ids": {
- "policy_main_chunk_3": ["p_0"]
+ }
+ ],
+ "path_summary": "The user prefers short answers about refunds."
}
- },
- "additional_context": {
- "pref-tone": {
- "chunk_uuid": "pref-tone",
- "id": "mem_user_alex_tone",
- "chunk_content": "Prefers concise answers.",
- "source_type": "memory",
- "source_title": "User preferences",
- "source_upload_time": "2026-05-12T08:14:00Z",
- "source_last_updated_time": "2026-05-12T08:14:00Z"
+ ],
+ "forceful_relations": [
+ {
+ "via": {
+ "from": "refund-policy",
+ "to": "refund-faq"
+ },
+ "chunk": {
+ "chunk_id": "ck_faq_1",
+ "context_id": "refund-faq",
+ "score": 0,
+ "content": "FAQ: refunds to a card take 5 to 7 business days to appear."
+ }
}
- }
- },
- "error": null,
- "meta": {
- "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d",
- "latency_ms": 12.3
- }
-}
-```
-
-```json Zero results
-{
- "success": true,
- "data": {
- "chunks": [],
- "sources": [],
- "graph_context": {
- "query_paths": [],
- "chunk_relations": [],
- "chunk_id_to_group_ids": {}
- },
- "additional_context": {}
+ ],
+ "llm_prompt": "# Query results\n\n**Query:** who owns refund processing?\n**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation\nCite a result by its number in brackets, e.g. [1].\n\n## Results\n\n### 1. Refund policy\n- **Relevance:** 0.91 · **Collection:** support · **Type:** file · **Category:** business_knowledge\n- **Id:** refund-policy · **Last updated:** 2026-07-02\n\nRefunds are processed within 30 days of purchase by the Finance Department.\n\n**Enrichment:** Refund window is 30 days; Finance owns refund processing.\n\n---\n\n### 2. Support chat with Priya\n- **Relevance:** 0.84 · **Collection:** support · **Type:** message · **Category:** user_preference\n- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29\n\nuser: Keep refund answers short please\nassistant: Got it.\n\n**Enrichment:** User prefers short answers about refunds.\n\n## Forceful relations\n\nLinked to a result by the author at ingest time (forceful_relations), not by relevance to this query.\n\n### R1. Refund FAQ\n- **Linked from:** refund-policy · **Collection:** support\n- **Id:** refund-faq\n\nFAQ: refunds to a card take 5 to 7 business days to appear.\n\n## Related facts\n\n- [P1] **Refund Processing** -managed by→ **Finance Department** (query path, relevance 0.81) [1]\n Refund processing is managed by the Finance Department.\n- [P2] **User** -prefers→ **short answers** (chunk relation, relevance 0.74) [2]\n The user prefers short answers about refunds.\n\n## Temporal facts\n\n- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: \"from June\") [1]\n\n## Sources\n\n1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02\n2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29\n3. **Refund FAQ** (id: refund-faq)"
},
"error": null,
"meta": {
"request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d",
- "latency_ms": 12.3
+ "api_version": "2.0.1",
+ "latency_ms": 412.7,
+ "database": "acme_corp",
+ "collection": "support"
}
}
```
@@ -709,6 +543,7 @@ result = client.query(
},
"meta": {
"request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d",
+ "api_version": "2.0.1",
"latency_ms": 4.8
}
}
@@ -716,47 +551,147 @@ result = client.query(
-A zero-result query returns empty arrays/maps rather than an error, as shown in the **Zero results** tab.
+`data` is exactly four keys. A query that matches nothing returns `200` with empty `chunks`, `graph` and `forceful_relations` and an empty `llm_prompt` rather than an error.
+
+| Key | Contents |
+| --- | --- |
+| `chunks[]` | Ranked matches: `chunk_id`, `context_id`, `score`, `content` (verbatim), `enrichment` (the extracted statement as a string, omitted when empty), `enrichment_kind` (the item's declared `context_category`: `user_preference`, `business_knowledge` or `decision_trace`; omitted when none was declared, present even without `enrichment`), `temporal[]` (only when temporal reasoning engaged; `{ content, start_date, end_date }`, dates may be `null`). |
+| `graph[]` | Paths through the context graph, query paths first then chunk expansions, deduplicated: `origin`, `triplets[]` of `source` / `relation` / `target`, plus `path_summary`. `origin` is `"query_path"` (grown from the entities in the query) or `"chunk_relation"` (the neighbourhood of a returned chunk, only returned when one of its hops came from a returned chunk or a `forceful_relations` chunk). `path_summary` is never empty: when the server wrote no summary, it narrates the hops. Entities are `{ entity_id, name }`; relations are `{ predicate, context, temporal_details?, relationship_id, chunk_id }`. `[]` when `graph_context` is `false`. |
+| `forceful_relations[]` | Chunks pulled in through `forceful_relations` declared at ingest: `via.from` (the context whose declaration pulled it in, may be `""`), `via.to` (the chunk's own `context_id`), `chunk` (same shape as `chunks[]`). `[]` when none or `follow_forceful_relations` is `false`. |
+| `llm_prompt` | A server-built markdown string ready to inject into a model call: `# Query results`, then `## Results`, `## Forceful relations`, `## Related facts`, `## Temporal facts` and `## Sources`, each left out when empty. Results are cited `[1]` and forceful relations `[R1]`; related facts are labelled `[P1]`, `[P2]`, ... in `graph[]` order, carry the path's relevance after reranking when it has one, and end with the results they were extracted from. Sources print only web (`http` or `https`) links. `""` when nothing matched. The layout is on [Query](/essentials/v2/query#llm_prompt). |
+
+To show a chunk's graph paths under that chunk, group hops by `triplets[].relation.chunk_id` and match it against `chunks[].chunk_id` (and `forceful_relations[].chunk.chunk_id`). This replaces the split response's `chunk_id_to_group_ids`. See [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks).
+
+`meta` on a unified response carries `request_id`, `api_version`, `latency_ms`, `database` and `collection`, plus a `deprecation` list when the request used a deprecated name. It has no `tenant_id`, `sub_tenant_id` or `source_type`. `collection` is present when the query searched one collection (named, or the default); a `collections` fan-out omits it.
+
+
+**Chunks carry no source details.** No title, url, collection, timestamps or attributes. `llm_prompt` prints the title, url, collection and last-updated date for the model; call [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) with `id=` to show them yourself.
+
## Behavior notes
**Default Behaviors**
-- **`mode` defaults to `"auto"`.** Omitting `mode` entirely behaves exactly like `mode: "auto"` - set it explicitly to `"fast"` or `"thinking"` if you want a deterministic pipeline.
-- **`graph_context` is on by default.** Set it to `false` if you only need ranked chunks and want to drop the graph slice from the response.
-- **`recency_bias` is off by default.** Defaults to `0.0` - no recency boost is applied unless you set it.
+- **`mode` defaults to `"auto"`.** Omitting `mode` entirely behaves exactly like `mode: "auto"`; set it explicitly to `"fast"` or `"thinking"` if you want a deterministic pipeline.
+- **`graph_context` is on by default.** Set it to `false` if you only need ranked chunks; `graph` is then `[]`.
+- **`follow_forceful_relations` is on by default.** Set it to `false` if you never want declared relations; `forceful_relations` is then `[]`.
**Important Considerations & Common Mistakes**
-- **`query_forceful_relations` requires `mode` to resolve to `"thinking"`.** In `fast` mode the flag is silently ignored. The server does not error or warn - your `additional_context` will simply be empty. Under `mode: "auto"` this depends on that request's routing decision, not on what you asked for.
-- **`mode: "auto"` overrides `graph_context`.** Whatever you send for `graph_context` is replaced to match the resolved mode - `true` if auto escalates to `thinking`, `false` if it resolves to `fast`. This also applies when `mode` is omitted, since it defaults to `"auto"`. Set `graph_context` explicitly only when calling `"fast"` or `"thinking"` directly.
-- **Want a deterministic pipeline instead of automatic routing?** Set `mode` explicitly to `"fast"` or `"thinking"` - an omitted `mode` field now defaults to `"auto"`, not `"fast"`.
-- **Relation `timestamp` is a Unix epoch float here.** In the `graph_context` slice returned by `/query` - and in the passthrough relations returned by [List Documents](/api-reference/v2/endpoint/list-documents) with `include_fields: ["relations"]` - each relation's `timestamp` is a Unix epoch value in seconds (a float, e.g. `1778573640.0`). The dedicated [Context Relations](/api-reference/v2/endpoint/source-relations) endpoint returns the same field as an ISO-8601 string instead. Normalize before comparing relation timestamps across endpoints.
-- **Use the right metadata namespace.** Top-level `metadata_filters` keys match `metadata`; free-form per-document fields must be nested under `additional_metadata` (`document_metadata` is only a legacy alias). Declare hot top-level filter fields in `database_metadata_schema` with `enable_match: true`.
-- **Common mistakes.** Check [Ingestion Status](/api-reference/v2/endpoint/source-status) for recently ingested documents before querying. If you omit `collection`, HydraDB queries the default collection; use [List Collections](/api-reference/v2/endpoint/list-sub-tenants) to discover available IDs.
+- **`mode: "auto"` overrides `graph_context`.** Whatever you send for `graph_context` is replaced to match the resolved mode: `true` if auto escalates to `thinking`, `false` if it resolves to `fast`. This also applies when `mode` is omitted. Set `graph_context` explicitly only when calling `"fast"` or `"thinking"` directly.
+- **`type` is refused.** `"knowledge"` or `"memory"` on a unified database is a `400`. Scope with `collection` or `collections`.
+- **Filter with `attributes`, on declared fields.** A key that is not in `database_metadata_schema`, or a value sent in `custom_attributes`, never matches. `metadata_filters` still works but is deprecated.
+- **Parse by shape.** A response with `llm_prompt` and a `graph` array is this shape; one with `graph_context` or `chunk_content` came from a split database. Stored logs and split databases keep producing the old one.
+- **Common mistakes.** Check [Ingestion Status](/api-reference/v2/endpoint/source-status) for recently ingested items before querying. If you omit `collection` and `collections`, HydraDB queries the default collection; use [List Collections](/api-reference/v2/endpoint/list-sub-tenants) to discover available IDs.
## Errors
-Common codes: `400 INVALID_PARAMETERS` (empty `query`), `404 DATABASE_NOT_FOUND`, `422 VALIDATION_ERROR`, `500 INTERNAL_ERROR`. See [Error Responses](/api-reference/v2/error-responses) for the full list.
+Common codes: `400 INVALID_PARAMETERS` (empty `query`), `400 CORPUS_TYPE_UNSUPPORTED` (`type: "knowledge"` or `"memory"` on a unified database), `404 DATABASE_NOT_FOUND`, `422 VALIDATION_ERROR`, `500 INTERNAL_ERROR`. See [Error Responses](/api-reference/v2/error-responses) for the full list.
+
+`400` also covers oversized filters: a `metadata_filters` list above 500 values, or a `metadata_filters` object above 64 KiB of compact JSON. The message names the offending key or reports the actual byte count. See [Filter size limits](/essentials/v2/attributes#filter-size-limits).
+
+## Split databases
+
+Everything above describes a unified database. A database created with `type: "split"` keeps the request and response it always had. The differences:
+
+### `type` selects the corpus
+
+| Value | Queries | Best for |
+|---|---|---|
+| `"knowledge"` *(default)* | Knowledge documents, files, and app sources | Document Q&A, RAG context. |
+| `"memory"` | User memories | Personalization and user preferences. |
+| `"all"` | Both, merged in one ranked result set | Personalized answers grounded in both shared and user-specific context. |
+
+```json
+{
+ "database": "legacy-app",
+ "collection": "user_alex",
+ "query": "What is our refund policy, and how should I explain it to this user?",
+ "type": "all",
+ "query_by": "hybrid",
+ "mode": "thinking",
+ "query_forceful_relations": true,
+ "metadata_filters": {
+ "department": "support",
+ "additional_metadata": { "source": "policy" }
+ },
+ "additional_context": "User is asking from the billing help center."
+}
+```
+
+- `query_forceful_relations` pulls author-declared related sources into the response's `additional_context`. It only takes effect when `mode` resolves to `"thinking"`; in `fast` mode the flag is silently ignored.
+- `additional_context` on the request is a short factual hint to guide retrieval. It is different from the response `additional_context` map.
+- `metadata_filters` is the split-era filter language. Top-level keys match schema-backed `metadata` and take an operator object (`equals`, `contains`, `contains_any`); free-form per-source fields go under `additional_metadata`, where a bare scalar is an exact match and a bare array matches any listed value. Each list holds at most 500 values and the whole object is capped at 64 KiB. The full semantics are on [Split databases and legacy fields](/essentials/v2/split-databases#metadata_filters).
+
+### The split response
+
+```json
+{
+ "success": true,
+ "data": {
+ "chunks": [
+ {
+ "chunk_uuid": "policy_main_chunk_3",
+ "id": "policy_main",
+ "chunk_content": "Refunds are issued within 30 days...",
+ "source_type": "pdf",
+ "source_title": "Compliance Policy",
+ "source_upload_time": "2026-05-12T08:14:00Z",
+ "source_last_updated_time": "2026-05-12T08:14:00Z",
+ "relevancy_score": 0.91,
+ "extra_context_ids": ["pref-tone"],
+ "metadata": { "department": "legal" },
+ "additional_metadata": { "author": "Legal Team" }
+ }
+ ],
+ "sources": [
+ { "id": "policy_main", "title": "Compliance Policy", "type": "pdf", "timestamp": "2026-05-12T08:14:00Z" }
+ ],
+ "graph_context": {
+ "query_paths": [
+ {
+ "triplets": [
+ {
+ "source": { "name": "Compliance Policy", "type": "DOCUMENT", "entity_id": "entity_compliance_policy" },
+ "relation": { "canonical_predicate": "GOVERNS", "context": "The compliance policy governs the refund timeline.", "timestamp": 1778573640.0, "chunk_id": "policy_main_chunk_3" },
+ "target": { "name": "Refund Processing", "type": "PROCESS", "entity_id": "entity_refund_processing" }
+ }
+ ],
+ "relevancy_score": 0.89,
+ "combined_context": "The Compliance Policy governs the Refund Processing.",
+ "source_chunk_ids": ["policy_main_chunk_3"]
+ }
+ ],
+ "chunk_relations": [],
+ "chunk_id_to_group_ids": { "policy_main_chunk_3": ["p_0"] }
+ },
+ "additional_context": {
+ "pref-tone": { "chunk_uuid": "pref-tone", "id": "mem_user_alex_tone", "chunk_content": "Prefers concise answers.", "source_type": "memory", "source_title": "User preferences" }
+ }
+ },
+ "error": null,
+ "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d", "latency_ms": 12.3 }
+}
+```
+
+- Relation `timestamp` in `graph_context` is a Unix epoch float (seconds). The dedicated [Context Relations](/api-reference/v2/endpoint/source-relations) endpoint returns the same field as an ISO-8601 string; normalize before comparing across endpoints.
+- There is no `llm_prompt`. Format the split response for a model with the SDK's `build_string` / `buildString` helper; see [How to Use API Results](/essentials/v2/api-results#7-split-databases).
-`400` also covers oversized filters: a `metadata_filters` list above 500 values, or
-a `metadata_filters` object above 64 KiB of compact JSON. The message names the
-offending key or reports the actual byte count. See
-[Filter size limits](/essentials/v2/attributes#filter-size-limits).
+The field-by-field mapping between the two shapes is on [Split databases and legacy fields](/essentials/v2/split-databases#4-legacy-query-and-response-fields).
**Related Resources**
-- **Setup first:** [Ingest Context](/api-reference/v2/endpoint/ingest-context) - content must be indexed
-- **Confirm indexing:** [Ingestion Status](/api-reference/v2/endpoint/source-status) - wait for `completed` (or `graph_creation`)
-- **Graph follow-up:** [Context Relations](/api-reference/v2/endpoint/source-relations) - inspect relationships in detail
-- **Concepts:** [Usage → Query](/essentials/v2/query)
-- **Concepts:** [Concepts → Semantic Search](/essentials/v2/semantic-search)
-- **Concepts:** [Concepts → Context Graphs](/essentials/v2/context-graphs)
-- **Response handling:** [Usage → How to Use API Results](/essentials/v2/api-results)
-- **Read more:** [Query - Overview](/api-reference/v2/endpoint/query-overview)
+- **Setup first:** [Ingest Context](/api-reference/v2/endpoint/ingest-context): content must be indexed
+- **Confirm indexing:** [Ingestion Status](/api-reference/v2/endpoint/source-status): wait for `completed` (or `graph_creation`)
+- **Source details:** [Inspect Context](/api-reference/v2/endpoint/fetch-content): title, attributes and content for a `context_id`
+- **Graph follow-up:** [Context Relations](/api-reference/v2/endpoint/source-relations): inspect relationships in detail
+- **Concepts:** [Usage: Query](/essentials/v2/query)
+- **Concepts:** [Concepts: Context Graphs](/essentials/v2/context-graphs)
+- **Response handling:** [Usage: How to Use API Results](/essentials/v2/api-results)
+- **Read more:** [Query: Overview](/api-reference/v2/endpoint/query-overview)
diff --git a/api-reference/v2/endpoint/sources-overview.mdx b/api-reference/v2/endpoint/sources-overview.mdx
index 6237ffb3..2adf6c29 100644
--- a/api-reference/v2/endpoint/sources-overview.mdx
+++ b/api-reference/v2/endpoint/sources-overview.mdx
@@ -7,28 +7,21 @@ description: "Quick reference for context management endpoints, their lifecycle,
| Task | Endpoint |
| :-- | :-- |
-| Upload PDFs, DOCX, CSVs, and other documents HydraDB should parse | `/context/ingest` with `type=knowledge` and `documents` |
-| Upload Slack messages, Notion pages, Gmail threads, or webpages with pre-extracted text | `/context/ingest` with `type=knowledge` and `app_knowledge` |
-| Upload user preferences, conversation history, or inline notes | `/context/ingest` with `type=memory` and `memories` |
-| Poll indexing progress | `/context/status` |
-| Browse stored sources or memories | `/context/list` |
-| Inspect original source content | `/context/inspect` |
-| Delete sources or memories | `DELETE /context` |
-| Inspect graph relations | `/context/relations` |
+| Send text and conversations as context items | `POST /context/ingest` with `context[]` |
+| Poll indexing progress | `GET /context/status` |
+| Browse stored items | `POST /context/list` |
+| Inspect an item's title, attributes and original content | `GET /context/inspect` |
+| Delete items | `DELETE /context` |
+| Inspect graph relations | `GET /context/relations` |
+| Walk everything connected to one item | `GET /context/{id}/subgraph` |
+
+On a split database, `POST /context/ingest` also takes `documents` and `app_knowledge` under `type=knowledge`, and `memories` under `type=memory`. See [Split databases](#split-databases).
## Lifecycle
```mermaid
flowchart LR
- A([Choose content type]) --> B{Knowledge or memory?}
-
- subgraph Ingestion [" "]
- direction LR
- SI([POST /context/ingest])
- end
-
- B -- Documents / app sources --> SI
- B -- User memories --> SI
+ A([Text or conversation]) --> SI([POST /context/ingest])
SI --> Q([queued])
Q --> P([processing])
@@ -42,7 +35,6 @@ flowchart LR
linkStyle default stroke:#64748b,stroke-width:2px,color:#f8fafc
style A fill:#0f172a,stroke:#334155,stroke-width:2px,color:#f8fafc
- style B fill:#1e293b,stroke:#334155,stroke-width:2px,color:#f8fafc
style SI fill:#1e293b,stroke:#334155,stroke-width:2px,color:#f8fafc
style Q fill:#1e293b,stroke:#334155,stroke-width:2px,color:#f8fafc
style P fill:#1e293b,stroke:#334155,stroke-width:2px,color:#f8fafc
@@ -53,49 +45,59 @@ flowchart LR
style E fill:#0f172a,stroke:#ef4444,stroke-width:2px,color:#f8fafc
```
-
- **Why both** `type=knowledge `**and** `app_knowledge`**?** They have a theoretical differentiation.
-
- - `type` picks the **bucket**: `knowledge` (shared documents) or `memory` (per-user context). It routes the ingest to the right store.
- - Within `type=knowledge`, you pick the **payload shape**: `documents` (binary documents HydraDB will parse - PDFs, DOCX, CSV) or `app_knowledge` (a JSON array of already-extracted content from your app - Slack messages, Notion pages, web pages). You can send both in the same request.
-
-
-## Core Ingestion Concepts
-
-- **Knowledge vs. Memories**: [Knowledge](/essentials/v2/knowledge) is shared, database-wide content (documents, app pages, Slack messages). [Memories](/essentials/v2/memories) are user-specific preferences and conversational traits scoped by `collection`. Both can be searched together via `type: "all"` on `POST /query`.
-- **IDs**: Unique identifiers returned by `/context/ingest`. You can assign custom IDs using `id` in metadata or `id` in `app_knowledge` items. Use them for polling status, inspecting content, and deleting context.
-- **Metadata Filtering**: You can scope queries using `metadata` (structured fields defined in your database schema) or `additional_metadata` (free-form per-document JSON). For detailed guidelines on structuring metadata, see the [Scoping using metadata](/essentials/v2/attributes) guide.
-- **Forceful Relations**: Relationships between sources can be declared at ingestion time to construct a robust knowledge graph. For more details on the graph layer, see the [Context Graphs](/essentials/v2/context-graphs) guide.
+## Core concepts
+
+- **Items**: everything you ingest is a piece of context, a `text` or a `conversation`, with an optional `context_category` (`user_preference`, `business_knowledge`, `decision_trace`) that you set. One database holds all of them; collections partition them per user, team or project. See [Ingest context](/essentials/v2/ingest).
+- **IDs**: each item has a `context_id`, yours or generated. The ingest response reports it as `results[].source_id`. Use it for polling status, inspecting content, deleting, and inspecting relations.
+- **Attributes**: `attributes` are the declared, filterable fields from `database_metadata_schema`; `custom_attributes` are free-form and stored with the item. Filter queries with `attributes`. See [Attributes](/essentials/v2/attributes).
+- **Enrichment**: on by default (`enrich: true`). HydraDB extracts entities, relations and preferences from each item into the [context graph](/essentials/v2/context-graphs); the extracted text comes back on query as `enrichment`, separate from the item's own `content`.
+- **Declared relations**: any item can name the items it relates to with `forceful_relations`, so they surface together at query time in `forceful_relations[]`.
+
+## Declared relations and attributes
+
+Declared relations pre-wire item relationships at ingestion time so that related items surface together during retrieval, before the graph layer discovers connections on its own. Think of them as explicit "see also" links between your items.
+
+Paired with declared attributes, you get deterministic control over how results are filtered and ranked.
+
+```json
+{
+ "database": "acme_corp",
+ "collection": "ops",
+ "context": [
+ {
+ "context_id": "runbook_deploy",
+ "title": "Deploy runbook",
+ "text": "1. Merge to main. 2. Wait for the image build. 3. Promote in ArgoCD.",
+ "attributes": { "department": "ops" },
+ "custom_attributes": { "owner": "platform-team" },
+ "forceful_relations": { "ids": ["monitoring_guide"] }
+ }
+ ]
+}
+```
-## Forceful relations and metadata
+## Split databases
-Forceful relations let you pre-wire document relationships at ingestion time so that relevant documents surface together during retrieval - even before the graph layer discovers connections organically. Think of them as explicit "see also" links between your documents.
+A database created with `type: "split"` keeps two corpora and the older ingest shapes:
-Paired with document-level metadata, you get deterministic control over how results are filtered and ranked.
+| Task | Endpoint |
+| :-- | :-- |
+| Upload PDFs, DOCX, CSVs, and other documents HydraDB should parse | `/context/ingest` with `type=knowledge` and `documents` |
+| Upload Slack messages, Notion pages, Gmail threads, or webpages with pre-extracted text | `/context/ingest` with `type=knowledge` and `app_knowledge` |
+| Upload user preferences, conversation history, or inline notes | `/context/ingest` with `type=memory` and `memories` |
-```python Python SDK
-result = client.context.ingest(
- type="knowledge",
- database="acme_corp",
- documents=[("runbook.pdf", f, "application/pdf")],
- document_metadata=json.dumps([{
- "id": "runbook_deploy",
- "metadata": {"department": "ops"},
- "additional_metadata": {"owner": "platform-team"},
- "relations": {"ids": ["monitoring_guide"]},
- }]),
-)
-```
+There, `type` picks the bucket (`knowledge` for shared documents, `memory` for per-user context) and routes the write to the right store; within `type=knowledge` you pick the payload shape (`documents` or `app_knowledge`, or both in one request). Both corpora can be searched together with `type: "all"` on `POST /query`. See [Knowledge](/essentials/v2/knowledge), [Memories](/essentials/v2/memories) and [Split databases and legacy fields](/essentials/v2/split-databases).
## Related sections
-- [Usage - Forceful Relations](/essentials/v2/knowledge) - linking sources at ingestion (see §7)
-- [Query](/api-reference/v2/endpoint/query-overview) - retrieve ingested content
+- [Ingest Context](/api-reference/v2/endpoint/ingest-context): the field reference
+- [Usage: Ingest context](/essentials/v2/ingest): every item field, conversations, enrichment, declared relations
+- [Query](/api-reference/v2/endpoint/query-overview): retrieve ingested content
Related Resources
- - [Usage - Memories](/essentials/v2/memories) - memories vs knowledge, when to use which
+ - [Usage: Context categories](/essentials/v2/context-categories): preferences, knowledge and decisions
- - [Usage - Metadata](/essentials/v2/attributes) - database-level vs document-level metadata
+ - [Usage: Attributes](/essentials/v2/attributes): declared versus free-form fields
diff --git a/api-reference/v2/index.mdx b/api-reference/v2/index.mdx
index 5041ee32..d09775af 100644
--- a/api-reference/v2/index.mdx
+++ b/api-reference/v2/index.mdx
@@ -17,7 +17,7 @@ description: "Single reference to all HydraDB endpoints"
| Group | Purpose | When to reach for it |
|---|---|---|
| [Databases](/api-reference/v2/endpoint/tenants-overview) | Create, monitor, and manage isolated workspaces | First step in any integration - and any time you need usage stats, provisioning status, or to tear down a workspace |
-| [Context](/api-reference/v2/endpoint/sources-overview) | Ingest, list, fetch, delete, and inspect knowledge or memories | Every time data flows into HydraDB - document uploads, app sources, user memories, and lifecycle ops |
+| [Context](/api-reference/v2/endpoint/sources-overview) | Ingest, list, fetch, delete, and inspect context items | Every time data flows into HydraDB: text, conversations, and lifecycle ops |
| [Query](/api-reference/v2/endpoint/query-overview) | Retrieve context with hybrid or text query | At query time - the only endpoint you call to feed an LLM |
## Core concepts
@@ -26,10 +26,10 @@ description: "Single reference to all HydraDB endpoints"
|---|---|---|
| `database` | Your isolated workspace for data, metadata schema, and query. | Send it on every API call so HydraDB knows which workspace to read or write. Formerly `tenant_id`; the `tenant_id` alias is still accepted (deprecated). |
| `collection` | Optional partition inside a database, often a user, team, account, or customer. | Use it when one database contains data for multiple users or customers. Formerly `sub_tenant_id`; the `sub_tenant_id` alias is still accepted (deprecated). Read more about our [multi-tenant architecture](/essentials/v2/databases-and-collections) |
-| [Knowledge](/essentials/v2/knowledge) | Shared source material such as PDFs, docs, app pages, tickets, Slack threads, or webpages. | Use `type=knowledge` when many users or agents should query the same content. |
-| [Memory](/essentials/v2/memories) | User-specific context such as preferences, conversation history, notes, and inferred traits. | Use `type=memory` when the content should personalize answers for a specific user or collection. |
+| [Context items](/essentials/v2/ingest) | A `text` or a `conversation`, sent in the `context` list of `POST /context/ingest`, optionally labelled with a `context_category`. | Everything you ingest. Shared context goes in a shared collection; a person's preferences go in their own. |
+| [Split databases](/essentials/v2/split-databases) | Databases created with `type: "split"` keep two corpora, knowledge and memory, selected with `type`. | Only for integrations that still use the older `documents`, `app_knowledge` and `memories` shapes. |
| `database_metadata_schema` | Database-level fields you define up front so metadata can be filtered or queried consistently. | Use it for stable fields like department, customer, region, plan, category, or compliance label. |
-| `document_metadata` | JSON-stringified per-document metadata array sent during file ingestion. | Use it to attach source IDs, titles, schema-backed `metadata`, free-form `additional_metadata`, or forceful relations to each uploaded document. |
+| `attributes` | Declared, filterable fields on an item, matching `database_metadata_schema`; `custom_attributes` are free-form. | Send them at ingest; filter with `attributes` on `/query`. |
| `ids` | IDs returned by ingestion or visible from `/context/list`. | Use them when polling processing status, inspecting content, listing a specific subset, deleting sources, or inspecting relations. |
## End-to-end lifecycle
@@ -40,7 +40,7 @@ flowchart LR
subgraph Database Lifecycle [" "]
direction LR
A([Create Database])-->B([Wait for Provisioning])
- B-->C([Ingest Knowledge / Memories])
+ B-->C([Ingest Context Items])
C-->D([Verify Processing])
D-->E([Query Context])
E-->F([Pass to LLM])
@@ -100,14 +100,14 @@ SDK methods mirror the API: `client..()` maps to the correspondin
| [`/databases/status`](/api-reference/v2/endpoint/tenant-status) | `GET` | `databases.status` | Check provisioning readiness | You just created a database and need to wait before ingesting data. |
| [`/databases/collections`](/api-reference/v2/endpoint/list-sub-tenants) | `GET` | `databases.collections` / `databases.collections` | List active collections | You partition data by user, team, customer, or account and need to inspect those partitions. |
| [`/databases/stats`](/api-reference/v2/endpoint/tenant-stats) | `GET` | `databases.stats` | Get usage statistics | You want to monitor object counts for a database. |
-| [`/context/ingest`](/api-reference/v2/endpoint/ingest-context) | `POST` | `context.ingest` | Ingest knowledge or memories | You are uploading documents, app sources, or user memories. |
+| [`/context/ingest`](/api-reference/v2/endpoint/ingest-context) | `POST` | `context.ingest` | Ingest context items | You are sending text or conversations. |
| [`/context/status`](/api-reference/v2/endpoint/source-status) | `GET` | `context.status` | Check processing status | You have IDs from ingestion and need to know when they are queryable. |
-| [`/context/inspect`](/api-reference/v2/endpoint/fetch-content) | `GET` | `context.inspect` | Inspect original source content or presigned URL | You need to display or inspect the original ingested content. |
-| [`/context/list`](/api-reference/v2/endpoint/list-documents) | `POST` | `context.list` | Browse knowledge or memories | You need pagination, filters, field projection, or a specific subset by `ids`. |
+| [`/context/inspect`](/api-reference/v2/endpoint/fetch-content) | `GET` | `context.inspect` | Inspect an item's title, attributes and original content | Query chunks carry no source details; look them up here by `context_id`. |
+| [`/context/list`](/api-reference/v2/endpoint/list-documents) | `POST` | `context.list` | Browse items | You need pagination, filters, field projection, or a specific subset by `ids`. |
| [`/context/{id}/metadata`](/api-reference/v2/endpoint/update-source-metadata) | `PATCH` | REST | Update source metadata | You need to merge `metadata` or `additional_metadata` onto one existing source without re-ingesting. |
-| [`/context`](/api-reference/v2/endpoint/delete-source) | `DELETE` | `context.delete` | Delete sources or memories | You need to remove one or more knowledge sources or memories by ID. |
+| [`/context`](/api-reference/v2/endpoint/delete-source) | `DELETE` | `context.delete` | Delete items | You need to remove one or more items by ID. |
| [`/context/relations`](/api-reference/v2/endpoint/source-relations) | `GET` | `context.relations` | Inspect entity relationships | You need graph relations for a source or collection. |
-| [`/query`](/api-reference/v2/endpoint/query) | `POST` | `query` | Unified query over knowledge, memories, or both | You need retrieval with `hybrid` or `text` query across `type: "knowledge"`, `type: "memory"`, or `type: "all"`. |
+| [`/query`](/api-reference/v2/endpoint/query) | `POST` | `query` | Retrieve context | You need ranked chunks, graph paths, declared relations and a prompt-ready `llm_prompt`, with `hybrid` or `text` matching across one or more collections. |
## Conventions
@@ -123,7 +123,6 @@ curl -X POST 'https://api.hydradb.com/query' \
-d '{
"database": "my_first_database",
"query": "What are the pricing tiers?",
- "type": "knowledge",
"query_by": "hybrid"
}'
```
@@ -156,7 +155,7 @@ Errors use the same envelope with `success: false`, `data: null`, and an `error`
- **Parameter casing.** The REST API uses snake_case (`database`). The TypeScript SDK accepts the same snake_case keys; method names are camelCase when generated for TypeScript. The Python SDK uses snake_case throughout.
-- **Query modes.** `POST /query` supports `query_by: "hybrid"` or `"text"` and `type: "knowledge"`, `"memory"`, or `"all"`. The same `type` enum is used across ingestion, listing, deletion, and query; query additionally accepts `"all"`.
+- **Query modes.** `POST /query` supports `query_by: "hybrid"` or `"text"` and `mode: "auto"`, `"fast"` or `"thinking"`. There is no `type` on a unified database; a database created with `type: "split"` takes `type: "knowledge"`, `"memory"` or `"all"` on context and query calls.
**Status codes:** Successful responses return `200` (or `202` for async accepts). Errors follow standard HTTP semantics:
@@ -185,6 +184,6 @@ Rate limits apply per API key. For production deployments, build retry logic wit
Existing v1 endpoints remain available under the v1 API Reference.
- **Build something:** [Quickstart](/get-started/v2/quickstart) walks through your first integration in five minutes
-- **Understand the model:** [Core Concepts](/get-started/v2/core-concepts) explains databases, memories, Query, and metadata
+- **Understand the model:** [Core Concepts](/get-started/v2/core-concepts) explains databases, items, categories, query and attributes
- **Go deeper:** [Usage](/essentials/v2/query) covers each primitive in depth
- **Install an SDK:** [Python](https://pypi.org/project/hydradb-sdk/) · [TypeScript](https://www.npmjs.com/package/@hydradb/sdk)
diff --git a/essentials/v2/access-control.mdx b/essentials/v2/access-control.mdx
index 225fd626..3c57e57a 100644
--- a/essentials/v2/access-control.mdx
+++ b/essentials/v2/access-control.mdx
@@ -48,21 +48,19 @@ Limits: 1000 principals per document, 256 characters per principal. Past that, u
## 3. Set an ACL
-### At ingest, on an app source
+### At ingest, on any item
-Each item in `app_knowledge` accepts an `acl` list:
+Each item in `context` accepts an `acl` list, whether it is a `text` or a `conversation`:
```json
{
- "id": "slack-C0123-1712345678",
- "app_kind": "message",
- "app_provider": "slack",
- "content": { "text": "Q3 comp bands are attached." },
+ "context_id": "comp-bands-q3",
+ "text": "Q3 comp bands are attached.",
"acl": ["user_email:grace@acme.com", "group:slack:C0123"]
}
```
-Omit `acl` and the document is unrestricted. A malformed principal rejects the whole request with `400` rather than ingesting the document unprotected.
+Omit `acl` and the item is unrestricted. A malformed principal rejects the whole request with `400` rather than ingesting the document unprotected.
### On an existing source, without re-ingesting
@@ -232,7 +230,8 @@ Check the principal forms on both sides. `group:slack:C0123` on the document onl
## Related
- [Connectors](/essentials/v2/connectors) - syncing app data, and per-resource ACL rules
-- [App Sources](/essentials/v2/app-sources) - the ingest shape that carries `acl`
+- [Ingest context](/essentials/v2/ingest#10-restricting-an-item): the `acl` item field
+- [App Sources](/essentials/v2/app-sources) - the split-database app source shape, which also carries `acl`
- [Query](/essentials/v2/query) - the `acl` field alongside every other retrieval parameter
- [Metadata](/essentials/v2/attributes) - filtering by attributes, a different question from permission
- [Multi-Tenant Support](/essentials/v2/databases-and-collections) - databases and collections, the isolation boundary ACLs work inside
diff --git a/essentials/v2/api-results.mdx b/essentials/v2/api-results.mdx
index 5b52b65a..a7bff006 100644
--- a/essentials/v2/api-results.mdx
+++ b/essentials/v2/api-results.mdx
@@ -1,198 +1,42 @@
---
title: "How to Use API Results"
-description: "Turn a query response into an LLM prompt."
+description: "Inject llm_prompt into your model call, read the four response keys when you need structure, and map citations back to context."
---
-`POST /query` returns structured JSON. Before you can pass it to an LLM, you need to convert the retrieval payload into a plain string. This page shows how.
+import LegacyLine from "/snippets/legacy-line.mdx";
-The same pattern works regardless of whether you set `type: "knowledge"`, `type: "memory"`, or `type: "all"` - the response shape is identical.
+[`POST /query`](/essentials/v2/query) on a unified database returns four keys: `chunks`, `graph`, `forceful_relations` and `llm_prompt`. The last one is the whole context block, already formatted for a model. This page shows how to use it, and what to read when you need structure instead of a string.
----
-
-## 1. What the response looks like
-
-The SDKs return the full response envelope from `POST /query`; the retrieval payload is on its `data` field (e.g. `result.data`). Raw HTTP responses use the same envelope, with the payload under `data`. The `build_string` / `buildString` helper accepts either the full envelope or just the `data` payload, so you can pass the SDK return value to it directly.
-
-The retrieval payload has the same core shape regardless of `type` or `query_by`:
-
-```json
-{
- "chunks": [
- {
- "chunk_uuid": "doc-001_chunk_0",
- "source_title": "my_document.pdf",
- "chunk_content": "Text content of the retrieved chunk...",
- "relevancy_score": 1.09,
- "extra_context_ids": ["ctx-id-1"],
- "additional_metadata": {
- "author": "Support Team"
- }
- }
- ],
- "graph_context": {
- "query_paths": [
- {
- "triplets": [
- {
- "source": { "name": "EntityA" },
- "target": { "name": "EntityB" },
- "relation": {
- "canonical_predicate": "DEPENDS_ON",
- "context": "reason text",
- "temporal_details": "2024-01"
- }
- }
- ]
- }
- ],
- "chunk_id_to_group_ids": {
- "doc-001_chunk_0": ["group-1"]
- },
- "chunk_relations": [
- {
- "group_id": "group-1",
- "triplets": [
- {
- "source": { "name": "EntityA" },
- "target": { "name": "EntityB" },
- "relation": {
- "canonical_predicate": "DEPENDS_ON",
- "context": "reason text"
- }
- }
- ]
- }
- ]
- },
- "additional_context": {
- "ctx-id-1": {
- "source_title": "related_doc.pdf",
- "chunk_content": "Extra related content..."
- }
- }
-}
-```
-
-Four things matter for prompt construction:
-
-- **`chunks`** - the primary retrieval output. Ranked by relevance; preserve the order HydraDB returns.
-- **`graph_context.query_paths`** - entity traversal paths derived from your query. Useful for relational reasoning. See [Context Graphs](/essentials/v2/context-graphs).
-- **`graph_context.chunk_relations`** + **`chunk_id_to_group_ids`** - per-chunk graph relations grouped by `group_id`, so you can attach the right triplets to each chunk.
-- **`additional_context`** - a map keyed by `chunk_uuid`. When a chunk includes `extra_context_ids`, use those IDs to look up related chunks here.
-
-The raw object has too much noise for an LLM - IDs, timestamps, metadata. Section 2 shows how to convert it into a clean string.
+
---
-## 2. Transforming the response into LLM context
+## 1. Inject `llm_prompt`
-Use `build_string` / `buildString` from the SDK. It takes any `POST /query` result and returns a formatted plain string.
+The server builds `llm_prompt`, one markdown document, from the chunks, the forceful relations, the graph paths and the dated facts, labelled for citation. Pass it to your model as is:
```python Python SDK
from hydra_db import HydraDB
-from hydra_db.helpers import build_string
-
-client = HydraDB(token="YOUR_API_KEY")
-
-result = client.query(
- database="your-database",
- collection="your-collection",
- query="How does authentication work?",
- type="knowledge",
- query_by="hybrid",
- max_results=5,
- mode="fast",
- graph_context=True,
-)
-
-context = build_string(result)
-```
-```typescript TypeScript SDK
-import { HydraDBClient } from "@hydradb/sdk";
-import { buildString } from "@hydradb/sdk/helpers";
-
-const client = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY });
-
-const result = await client.query({
- database: "your-database",
- collection: "your-collection",
- query: "How does authentication work?",
- type: "knowledge",
- queryBy: "hybrid",
- maxResults: 5,
- mode: "fast",
- graphContext: true,
-});
-
-const context = buildString(result);
-```
-```python Python SDK (Async)
-import asyncio
-
-from hydra_db import AsyncHydraDB
-from hydra_db.helpers import build_string
-
-client = AsyncHydraDB(token="YOUR_API_KEY")
-
-
-async def main():
- result = await client.query(
- database="your-database",
- collection="your-collection",
- query="How does authentication work?",
- type="knowledge",
- query_by="hybrid",
- max_results=5,
- mode="fast",
- graph_context=True,
- )
-
- context = build_string(result)
- return context
-
-
-asyncio.run(main())
-```
-
-
----
-
-## 3. Feeding the context into your LLM
-
-
-```python Python SDK
-from hydra_db import HydraDB
-from hydra_db.helpers import build_string
from openai import OpenAI
hydra = HydraDB(token="YOUR_HYDRA_DB_API_KEY")
openai_client = OpenAI(api_key="YOUR_OPENAI_API_KEY")
-question = "How does authentication work?"
+question = "How should I explain our refund policy to Alex?"
result = hydra.query(
- database="your-database",
+ database="acme",
+ collections={"user_alex": 2, "company": 1},
query=question,
- type="knowledge",
- query_by="hybrid",
- mode="fast",
- graph_context=True,
+ mode="thinking",
)
-context = build_string(result)
-
completion = openai_client.chat.completions.create(
model="gpt-4o",
messages=[
- {
- "role": "system",
- "content": "You are a helpful assistant. Answer the user's question using only the context provided. If the answer is not in the context, say you don't know.",
- },
- {
- "role": "user",
- "content": f"Context:\n{context}\n\nQuestion: {question}",
- },
+ {"role": "system", "content": "Answer using only the context below. Cite the bracketed labels you rely on. If the answer is not in the context, say so.\n\n" + result.data.llm_prompt},
+ {"role": "user", "content": question},
],
)
@@ -200,513 +44,245 @@ print(completion.choices[0].message.content)
```
```typescript TypeScript SDK
import { HydraDBClient } from "@hydradb/sdk";
-import { buildString } from "@hydradb/sdk/helpers";
import OpenAI from "openai";
const hydra = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY });
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
-const question = "How does authentication work?";
+const question = "How should I explain our refund policy to Alex?";
const result = await hydra.query({
- database: "your-database",
+ database: "acme",
+ collections: { user_alex: 2, company: 1 },
query: question,
- type: "knowledge",
- queryBy: "hybrid",
- mode: "fast",
- graphContext: true,
+ mode: "thinking",
});
-const context = buildString(result);
-
const completion = await openai.chat.completions.create({
model: "gpt-4o",
messages: [
- {
- role: "system",
- content: "You are a helpful assistant. Answer the user's question using only the context provided. If the answer is not in the context, say you don't know.",
- },
- {
- role: "user",
- content: `Context:\n${context}\n\nQuestion: ${question}`,
- },
+ { role: "system", content: "Answer using only the context below. Cite the bracketed labels you rely on. If the answer is not in the context, say so.\n\n" + result.data.llmPrompt },
+ { role: "user", content: question },
],
});
console.log(completion.choices[0].message.content);
```
-```python Python SDK (Async)
-import asyncio
+```bash cURL
+curl -s -X POST 'https://api.hydradb.com/query' \
+ -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
+ -H "API-Version: 2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "database": "acme",
+ "collections": { "user_alex": 2, "company": 1 },
+ "query": "How should I explain our refund policy to Alex?",
+ "mode": "thinking"
+ }' | jq -r '.data.llm_prompt'
+```
+
-from hydra_db import AsyncHydraDB
-from hydra_db.helpers import build_string
-from openai import AsyncOpenAI
+There is no client-side string building on a unified database. Do not concatenate `chunks[].content` yourself; the prompt already contains it, in ranked order, with labels the model can cite. The SDK `build_string` / `buildString` helpers return `llm_prompt` verbatim on a unified database.
-hydra = AsyncHydraDB(token="YOUR_HYDRA_DB_API_KEY")
-openai_client = AsyncOpenAI(api_key="YOUR_OPENAI_API_KEY")
+---
-question = "How does authentication work?"
+## 2. What is in the prompt
+For the [example response on Query](/essentials/v2/query#1-one-call), `llm_prompt` is:
-async def main():
- result = await hydra.query(
- database="your-database",
- query=question,
- type="knowledge",
- query_by="hybrid",
- mode="fast",
- graph_context=True,
- )
+```markdown
+# Query results
- context = build_string(result)
+**Query:** who owns refund processing?
+**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation
+Cite a result by its number in brackets, e.g. [1].
- completion = await openai_client.chat.completions.create(
- model="gpt-4o",
- messages=[
- {
- "role": "system",
- "content": "You are a helpful assistant. Answer the user's question using only the context provided. If the answer is not in the context, say you don't know.",
- },
- {
- "role": "user",
- "content": f"Context:\n{context}\n\nQuestion: {question}",
- },
- ],
- )
+## Results
- print(completion.choices[0].message.content)
+### 1. Refund policy
+- **Relevance:** 0.91 · **Collection:** support · **Type:** file · **Category:** business_knowledge
+- **Id:** refund-policy · **Last updated:** 2026-07-02
+Refunds are processed within 30 days of purchase by the Finance Department.
-asyncio.run(main())
-```
-
+**Enrichment:** Refund window is 30 days; Finance owns refund processing.
---
-## 4. Combining Knowledge and Memories
+### 2. Support chat with Priya
+- **Relevance:** 0.84 · **Collection:** support · **Type:** message · **Category:** user_preference
+- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29
-The simplest path is one `POST /query` with `type: "all"` - HydraDB queries both stores in parallel and returns one merged, ranked result set.
+user: Keep refund answers short please
+assistant: Got it.
-
-```python Python SDK
-from hydra_db import HydraDB
-from hydra_db.helpers import build_string
-from openai import OpenAI
+**Enrichment:** User prefers short answers about refunds.
-hydra = HydraDB(token="YOUR_HYDRA_DB_API_KEY")
-openai_client = OpenAI(api_key="YOUR_OPENAI_API_KEY")
+## Forceful relations
-question = "What is our refund policy?"
+Linked to a result by the author at ingest time (forceful_relations), not by relevance to this query.
-result = hydra.query(
- database="acme_corp",
- collection="user_123",
- query=question,
- type="all",
- query_by="hybrid",
- mode="thinking",
-)
+### R1. Refund FAQ
+- **Linked from:** refund-policy · **Collection:** support
+- **Id:** refund-faq
-context = build_string(result)
+FAQ: refunds to a card take 5 to 7 business days to appear.
-completion = openai_client.chat.completions.create(
- model="gpt-4o",
- messages=[
- {"role": "system", "content": "Answer using only the context provided."},
- {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"},
- ],
-)
+## Related facts
-print(completion.choices[0].message.content)
-```
-```typescript TypeScript SDK
-import { HydraDBClient } from "@hydradb/sdk";
-import { buildString } from "@hydradb/sdk/helpers";
-import OpenAI from "openai";
+- [P1] **Refund Processing** -managed by→ **Finance Department** (query path, relevance 0.81) [1]
+ Refund processing is managed by the Finance Department.
+- [P2] **User** -prefers→ **short answers** (chunk relation, relevance 0.74) [2]
+ The user prefers short answers about refunds.
-const hydra = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY });
-const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
+## Temporal facts
-const question = "What is our refund policy?";
+- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: "from June") [1]
-const result = await hydra.query({
- database: "acme_corp",
- collection: "user_123",
- query: question,
- type: "all",
- queryBy: "hybrid",
- mode: "thinking",
-});
-
-const context = buildString(result);
-
-const completion = await openai.chat.completions.create({
- model: "gpt-4o",
- messages: [
- { role: "system", content: "Answer using only the context provided." },
- { role: "user", content: `Context:\n${context}\n\nQuestion: ${question}` },
- ],
-});
+## Sources
-console.log(completion.choices[0].message.content);
+1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02
+2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29
+3. **Refund FAQ** (id: refund-faq)
```
-```python Python SDK (Async)
-import asyncio
-
-from hydra_db import AsyncHydraDB
-from hydra_db.helpers import build_string
-from openai import AsyncOpenAI
-hydra = AsyncHydraDB(token="YOUR_HYDRA_DB_API_KEY")
-openai_client = AsyncOpenAI(api_key="YOUR_OPENAI_API_KEY")
+| Section | Built from | Labels |
+| --- | --- | --- |
+| `# Query results` | The query, a `**Found:**` line counting what follows, and the instruction to cite a result by its number | None |
+| `## Results` | `chunks[]`, in ranked order: a `### 1. title` heading, a line with relevance (`score`), collection, type and category (`enrichment_kind`), a line with the id (`context_id`) and last-updated date, the `content`, then `**Enrichment:**` (`enrichment`). Results are separated by `---`. | `[1]`, `[2]`, ... |
+| `## Forceful relations` | `forceful_relations[]`, the items the hits declared with `forceful_relations` at ingest: a guide line, then `### R1. title` blocks laid out like results, with `**Linked from:**` (`via.from`) in place of relevance | `[R1]`, `[R2]`, ... |
+| `## Related facts` | `graph[]`, one line per path: its chain of hops (`**A** -pred→ **B**`), `query path` or `chunk relation` for its `origin` with its relevance after reranking when it has one (`(query path, relevance 0.81)`), and the results its hops were extracted from, with the `path_summary` indented under it unless it only narrates the chain | `[P1]`, `[P2]`, ... in `graph[]` order; each line also cites its results |
+| `## Temporal facts` | The dated facts behind `chunks[].temporal`, with window, precision and status, then the evidence phrase after a `;` | None; each fact cites its result |
+| `## Sources` | Each context once, in order of first appearance: title, type, id, url (web links only, never a storage location such as `s3://...`) and last-updated date | None; the numbers count contexts, not results |
-question = "What is our refund policy?"
+A section with nothing in it is left out, and a query that returns nothing gets an empty `llm_prompt`. Ask the model to cite the labels and you get answers you can trace: a `[1]` in the reply is result 1, whose `**Id:**` is `refund-policy`, which you can open with [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content); a `[P1]` is the first path in `graph[]`. How a multi-hop chain reads is on [Query](/essentials/v2/query#llm_prompt).
+---
-async def main():
- result = await hydra.query(
- database="acme_corp",
- collection="user_123",
- query=question,
- type="all",
- query_by="hybrid",
- mode="thinking",
- )
+## 3. When you need structure
- context = build_string(result)
+Render a UI, rerank, or apply your own rules from the three structured keys. The full field reference is on [Query](/essentials/v2/query#3-response).
- completion = await openai_client.chat.completions.create(
- model="gpt-4o",
- messages=[
- {"role": "system", "content": "Answer using only the context provided."},
- {"role": "user", "content": f"Context:\n{context}\n\nQuestion: {question}"},
- ],
- )
+| Read | For |
+| --- | --- |
+| `chunks[].content` | The matched text, verbatim. |
+| `chunks[].enrichment` | What enrichment extracted from that chunk (a preference, a fact), as a string. |
+| `chunks[].enrichment_kind` | The item's declared `context_category` (`user_preference`, `business_knowledge` or `decision_trace`); omitted when none was declared. |
+| `chunks[].score` | Relevance, for your own thresholds. |
+| `graph[].path_summary` | One sentence per graph path; `graph[].triplets` for the steps and `graph[].origin` for the lane that found it. To show a path under its chunk, see [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks). |
+| `forceful_relations[]` | Chunks linked at ingest with `forceful_relations`, each with the `via` link that brought it in. |
- print(completion.choices[0].message.content)
+
+```python Python SDK
+for chunk in result.data.chunks:
+ print(chunk.score, chunk.context_id, chunk.content)
+ if chunk.enrichment:
+ print(" enrichment:", chunk.enrichment)
+ if chunk.enrichment_kind:
+ print(" category:", chunk.enrichment_kind)
+
+for path in result.data.graph:
+ print("path:", path.path_summary)
+
+for rel in result.data.forceful_relations:
+ print("linked:", rel.via.to, ":", rel.chunk.content)
+```
+```typescript TypeScript SDK
+for (const chunk of result.data.chunks) {
+ console.log(chunk.score, chunk.contextId, chunk.content);
+ if (chunk.enrichment) console.log(" enrichment:", chunk.enrichment);
+ if (chunk.enrichmentKind) console.log(" category:", chunk.enrichmentKind);
+}
+for (const path of result.data.graph) console.log("path:", path.pathSummary);
-asyncio.run(main())
+for (const rel of result.data.forcefulRelations) {
+ console.log("related via", rel.via.from, "->", rel.via.to, ":", rel.chunk.content);
+}
```
-If you need to format knowledge and memories in separate labeled sections, call `POST /query` twice in parallel:
-
-
-```python Python SDK
-import asyncio
-from hydra_db import AsyncHydraDB
-from hydra_db.helpers import build_string
+---
-hydra = AsyncHydraDB(token="YOUR_HYDRA_DB_API_KEY")
-
-async def main():
- knowledge_result, memory_result = await asyncio.gather(
- hydra.query(
- database="acme_corp",
- query="refund policy",
- type="knowledge",
- query_by="hybrid",
- mode="thinking",
- ),
- hydra.query(
- database="acme_corp",
- collection="user_123",
- query="answer style preferences",
- type="memory",
- query_by="hybrid",
- ),
- )
-
- prompt = (
- f"User preferences:\n{build_string(memory_result)}\n\n"
- f"Relevant docs:\n{build_string(knowledge_result)}\n\n"
- f"Question: {question}"
- )
- return prompt
-
-
-asyncio.run(main())
-```
-```typescript TypeScript SDK
-import { HydraDBClient } from "@hydradb/sdk";
-import { buildString } from "@hydradb/sdk/helpers";
+## 4. Showing source details
-const hydra = new HydraDBClient({ token: process.env.HYDRA_DB_API_KEY });
+A chunk carries only `chunk_id`, `context_id`, `score`, `content`, `enrichment`, `enrichment_kind` and `temporal`. It has no title, url, collection, timestamps or attributes. `llm_prompt` prints the title, url, collection and last-updated date for the model; to show any of that in your own UI, call [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content) with the chunk's `context_id`:
-const [knowledgeResult, memoryResult] = await Promise.all([
- hydra.query({
- database: "acme_corp",
- query: "refund policy",
- type: "knowledge",
- queryBy: "hybrid",
- mode: "thinking",
- }),
- hydra.query({
- database: "acme_corp",
- collection: "user_123",
- query: "answer style preferences",
- type: "memory",
- queryBy: "hybrid",
- }),
-]);
-
-const prompt =
- `User preferences:\n${buildString(memoryResult)}\n\n` +
- `Relevant docs:\n${buildString(knowledgeResult)}\n\n` +
- `Question: ${question}`;
+```bash
+curl -G 'https://api.hydradb.com/context/inspect' \
+ -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
+ -H "API-Version: 2" \
+ --data-urlencode "database=acme" \
+ --data-urlencode "id=refund-policy"
```
-
-If memory query fails or times out, fall back to the knowledge-only prompt.
+Fetch it lazily, when a citation is opened, rather than for every chunk on every query.
---
## 5. Practical guidance
-- **Preserve server order.** Don't re-sort chunks client-side.
-- **Start small on chunks.** `max_results: 10` is a reasonable default. Drop to 5 if you hit token limits, raise to 20 if you rerank downstream.
-- **Use graph context selectively.** It improves relational queries and bloats simple lookups. See [Context Graphs](/essentials/v2/context-graphs).
-- **Always give the model a grounding instruction.** A system prompt like "answer only from the provided context" prevents the model from inventing answers when retrieval is thin.
-- **Format consistently.** Whatever section delimiters you choose (`=== CONTEXT ===`, `Chunk N`, `Source:`), keep them stable across calls so the model learns the structure.
-- **`type: "all"` is the simplest path when you need both knowledge and memories.** One call, one result set, one `build_string` call.
+- **Use `llm_prompt` for the model, the arrays for your code.** Both describe the same result.
+- **Give the model a grounding instruction.** "Answer only from the provided context" prevents invented answers when retrieval is thin. Ask it to cite labels.
+- **Preserve server order.** `chunks[]` is ranked; do not re-sort it client-side.
+- **Start small on chunks.** `max_results: 10` is a reasonable default. Drop to `5` if you hit token limits, raise to `20` if you rerank downstream.
+- **Keep graph context on for relational questions.** It adds size to `llm_prompt`; set `graph_context: false` for simple lookups.
---
## 6. Common mistakes
| Mistake | What goes wrong | Fix |
-|---|---|---|
-| Passing the raw result object to the LLM | Wastes tokens on IDs and metadata | Use `build_string` / `buildString`. |
-| Not including `collection` (formerly `sub_tenant_id`) when querying memories | Queries the default collection instead of the user's | Always pass the same `collection` used at ingestion. |
-| Including too many chunks | Token overflow or answer quality drops | Start at `max_results: 10`; reduce if needed. |
-| Re-sorting chunks client-side | Overrides HydraDB's ranking | Preserve the server-returned order. |
+| --- | --- | --- |
+| Building your own context string | Duplicates what the server already did, without the labels | Inject `llm_prompt`. |
+| Passing the raw `data` object to the model | Wastes tokens on ids and scores | Inject `llm_prompt`. |
+| Expecting a title or url on a chunk | Chunks carry no source details | Call `GET /context/inspect` with `context_id`. |
+| Concatenating `content` and `enrichment` | Enrichment is stored separately on purpose | Use `content` for what was said, `enrichment` for what was extracted. |
+| Re-sorting chunks client-side | Overrides HydraDB's ranking | Preserve the returned order. |
| Missing a grounding instruction | The model invents answers when retrieval is thin | System prompt: answer only from the provided context. |
-| Setting `graph_context: false` and expecting graph fields | Graph context will be omitted | `graph_context` is on by default; only set it to `false` when you explicitly don't want graph data. |
-| Using `query_forceful_relations` with `mode: "fast"` | Flag is silently ignored | `query_forceful_relations` only takes effect when `mode: "thinking"`. |
-| Looking up `chunk_relations` without `chunk_id_to_group_ids` | The right relations don't attach to the right chunk | Use `chunk_id_to_group_ids[chunk_uuid]` to find the `group_id`s, then filter `chunk_relations` by them. |
-
----
-
-## Related
-
-- [Query](/essentials/v2/query) - request parameters and response shape
-- [Memories](/essentials/v2/memories) - what `POST /query` with `type: "memory"` queries
-- [Knowledge](/essentials/v2/knowledge) - what `POST /query` with `type: "knowledge"` queries
-- [Context Graphs](/essentials/v2/context-graphs) - what `graph_context` fields contain
+| Reading `graph_context` or `chunk_content` | Those are split-database fields | On a unified database read `graph[]` and `chunks[].content`. |
---
-
+## 7. Split databases
-If you are not using the SDK, use the helper below to call `POST /query`, format the response, and pass the result to your LLM.
+A database created with `type: "split"` returns the old response (`chunks[].chunk_content`, `sources[]`, `graph_context`, `additional_context`) and has no `llm_prompt`. There, build the context string with the SDK's `build_string` / `buildString` helper, which accepts the full envelope or just `data`:
-```python Python
-from __future__ import annotations
-from typing import Any
-
-import httpx
-
-
-def _format_path_chain(path: Any) -> str:
- triplets = path.get("triplets") or []
- parts = []
- for t in triplets:
- src = t.get("source", {}).get("name", "")
- tgt = t.get("target", {}).get("name", "")
- rel = t.get("relation", {})
- pred = rel.get("canonical_predicate", "")
- line = f"[{src}] -> {pred} -> [{tgt}]"
- ctx = rel.get("context")
- if ctx:
- line += f": {ctx}"
- temporal = rel.get("temporal_details")
- if temporal:
- line += f" [Time: {temporal}]"
- parts.append(line)
- return "\n ↳ ".join(parts)
-
-
-def build_context_string(result: dict) -> str:
- lines: list[str] = []
- gc = result.get("graph_context") or {}
-
- query_paths = gc.get("query_paths") or []
- if query_paths:
- lines.append("=== ENTITY PATHS ===")
- for path in query_paths:
- lines.append(_format_path_chain(path))
- lines.append("")
-
- chunks = result.get("chunks") or []
- additional_context = result.get("additional_context") or {}
- chunk_id_to_group_ids = gc.get("chunk_id_to_group_ids") or {}
- chunk_relations = gc.get("chunk_relations") or []
-
- if chunks:
- lines.append("=== CONTEXT ===")
- for i, chunk in enumerate(chunks):
- lines.append(f"Chunk {i + 1}")
- source = chunk.get("source_title", "")
- if source:
- lines.append(f"Source: {source}")
- lines.append(chunk.get("chunk_content", ""))
-
- chunk_uuid = chunk.get("chunk_uuid", "")
- if chunk_uuid and chunk_id_to_group_ids and chunk_relations:
- group_ids = chunk_id_to_group_ids.get(chunk_uuid, [])
- relevant = [r for r in chunk_relations if r.get("group_id") in group_ids]
- if relevant:
- lines.append("Graph Relations:")
- for rel in relevant:
- for triplet in rel.get("triplets") or []:
- src = triplet.get("source", {}).get("name", "")
- tgt = triplet.get("target", {}).get("name", "")
- pred = triplet.get("relation", {}).get("canonical_predicate", "")
- ctx = triplet.get("relation", {}).get("context", "")
- line = f" [{src}] -> {pred} -> [{tgt}]: {ctx}"
- temporal = triplet.get("relation", {}).get("temporal_details")
- if temporal:
- line += f" [Time: {temporal}]"
- lines.append(line)
-
- extra_ids = chunk.get("extra_context_ids") or []
- if extra_ids and additional_context:
- extras = [additional_context[eid] for eid in extra_ids if eid in additional_context]
- if extras:
- lines.append("Extra Context:")
- for extra in extras:
- lines.append(f" Related Context ({extra.get('source_title', '')}): {extra.get('chunk_content', '')}")
-
- lines.append("---")
- lines.append("")
-
- return "\n".join(lines)
-
-
-# Call the API and format the result
-response = httpx.post(
- "https://api.hydradb.com/query",
- headers={
- "Authorization": "Bearer YOUR_HYDRA_DB_API_KEY",
- "API-Version": "2",
- },
- json={
- "database": "your-database",
- "query": "How does authentication work?",
- "type": "knowledge",
- "query_by": "hybrid",
- "mode": "fast",
- "graph_context": True,
- },
-)
-
-response.raise_for_status()
-envelope = response.json()
-if not envelope.get("success"):
- error = envelope.get("error") or {}
- raise RuntimeError(error.get("message", "HydraDB search failed"))
+```python Python SDK
+from hydra_db.helpers import build_string
-context = build_context_string(envelope["data"])
+result = client.query(
+ database="legacy-app",
+ collection="user_123",
+ query="What is our refund policy?",
+ type="all",
+ mode="thinking",
+)
-# Pass context to your LLM
-print(context)
+context = build_string(result)
```
-```typescript TypeScript
-function formatPathChain(path: any): string {
- return (path.triplets || [])
- .map((t: any) => {
- let str = `[${t.source?.name}] -> ${t.relation?.canonical_predicate} -> [${t.target?.name}]`;
- if (t.relation?.context) str += `: ${t.relation.context}`;
- if (t.relation?.temporal_details) str += ` [Time: ${t.relation.temporal_details}]`;
- return str;
- })
- .join("\n ↳ ");
-}
-
-function buildContextString(result: any): string {
- const lines: string[] = [];
- const gc = result.graph_context;
-
- if (gc?.query_paths?.length) {
- lines.push("=== ENTITY PATHS ===");
- for (const path of gc.query_paths) lines.push(formatPathChain(path));
- lines.push("");
- }
-
- if (result.chunks?.length) {
- lines.push("=== CONTEXT ===");
- for (let i = 0; i < result.chunks.length; i++) {
- const chunk = result.chunks[i];
- lines.push(`Chunk ${i + 1}`);
- lines.push(`Source: ${chunk.source_title}`);
- lines.push(chunk.chunk_content);
-
- if (gc?.chunk_id_to_group_ids && gc.chunk_relations) {
- const groupIds = gc.chunk_id_to_group_ids[chunk.chunk_uuid] || [];
- const relations = gc.chunk_relations.filter(
- (r: any) => r.group_id && groupIds.includes(r.group_id)
- );
- if (relations.length) {
- lines.push("Graph Relations:");
- for (const rel of relations) {
- for (const t of rel.triplets) {
- let line = ` [${t.source.name}] -> ${t.relation.canonical_predicate} -> [${t.target.name}]: ${t.relation.context}`;
- if (t.relation.temporal_details) line += ` [Time: ${t.relation.temporal_details}]`;
- lines.push(line);
- }
- }
- }
- }
-
- if (chunk.extra_context_ids?.length && result.additional_context) {
- const extras = chunk.extra_context_ids
- .map((id: string) => result.additional_context[id])
- .filter(Boolean);
- if (extras.length) {
- lines.push("Extra Context:");
- for (const extra of extras) {
- lines.push(` Related Context (${extra.source_title}): ${extra.chunk_content}`);
- }
- }
- }
-
- lines.push("---");
- lines.push("");
- }
- }
- return lines.join("\n");
-}
+```typescript TypeScript SDK
+import { buildString } from "@hydradb/sdk/helpers";
-// Call the API and format the result
-const res = await fetch("https://api.hydradb.com/query", {
- method: "POST",
- headers: {
- "Content-Type": "application/json",
- Authorization: "Bearer YOUR_HYDRA_DB_API_KEY",
- "API-Version": "2",
- },
- body: JSON.stringify({
- database: "your-database",
- query: "How does authentication work?",
- type: "knowledge",
- query_by: "hybrid",
- mode: "fast",
- graph_context: true,
- }),
+const result = await client.query({
+ database: "legacy-app",
+ collection: "user_123",
+ query: "What is our refund policy?",
+ type: "all",
+ mode: "thinking",
});
-const envelope = await res.json();
-if (!res.ok || !envelope.success) {
- throw new Error(envelope.error?.message ?? "HydraDB search failed");
-}
-
-const context = buildContextString(envelope.data);
-
-// Pass context to your LLM
-console.log(context);
+const context = buildString(result);
```
-
+Detect which shape you are holding by its keys: `llm_prompt` and a `graph` array mean unified; `graph_context` or `chunk_content` mean split. See [Split databases and legacy fields](/essentials/v2/split-databases#4-legacy-query-and-response-fields) for the field-by-field mapping.
+
+---
+
+## Related
+
+- [Query](/essentials/v2/query): request parameters and the four response keys
+- [Context graphs](/essentials/v2/context-graphs): what `graph[]` contains
+- [Ingest context](/essentials/v2/ingest): `context_category`, `enrich` and `forceful_relations` decide what comes back here
diff --git a/essentials/v2/architecture.mdx b/essentials/v2/architecture.mdx
index cc450e2e..bae8aec0 100644
--- a/essentials/v2/architecture.mdx
+++ b/essentials/v2/architecture.mdx
@@ -63,8 +63,7 @@ flowchart LR
Client --|HTTPS + Bearer token| API
API --> Databases
API --> Status
- API --|POST /context/ingest (type=knowledge)|--> Queue
- API --|POST /context/ingest (type=memory)|--> Queue
+ API --|POST /context/ingest|--> Queue
Queue --> Parser
Parser --> Sources
Parser --> Embed
@@ -84,8 +83,8 @@ flowchart LR
Two details to notice in the diagram:
-- **Two vector stores, one ingest endpoint.** [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context) routes to the [Knowledge](/essentials/v2/knowledge) store when `type=knowledge` and to the [Memories](/essentials/v2/memories) store when `type=memory`. Same endpoint, different bucket.
-- **One query endpoint, every retrieval method.** [`POST /query`](/api-reference/v2/endpoint/query) is the only retrieval entry point. The `type` and `query_by` parameters decide what gets queried and how - see [Query](/essentials/v2/query) for the full picture.
+- **One ingest endpoint, one corpus.** [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context) takes `context` items, text or conversations, and writes them to the database. Collections partition them per user, team or project. A database created with `type: "split"` keeps two stores instead, [Knowledge](/essentials/v2/knowledge) and [Memories](/essentials/v2/memories), selected with `type`.
+- **One query endpoint, every retrieval method.** [`POST /query`](/api-reference/v2/endpoint/query) is the only retrieval entry point. `collections` decides where to look and `query_by` how to match; see [Query](/essentials/v2/query) for the full picture.
---
@@ -131,12 +130,12 @@ The full status table and polling pattern lives at [Ingestion Status](/api-refer
Here's the canonical end-to-end flow. Each step links to the endpoint that owns it:
1. **Create a database** with [`POST /databases`](/api-reference/v2/endpoint/create-tenant) - your isolated workspace, optionally with a [metadata schema](/essentials/v2/attributes) declared up front.
-2. **Wait for provisioning** by polling [`GET /databases/status`](/api-reference/v2/endpoint/tenant-status) until `vectorstore_status.knowledge`, `vectorstore_status.memories`, and `graph_status` are all `true`.
-3. **Ingest content** with [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context) - pick `type=knowledge` for shared documents or `type=memory` for per-user context. See [Knowledge](/essentials/v2/knowledge) and [Memories](/essentials/v2/memories) for the content models.
+2. **Wait for provisioning** by polling [`GET /databases/status`](/api-reference/v2/endpoint/tenant-status) until `infra.ready_for_ingestion` is `true`.
+3. **Ingest content** with [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context): `context` items, text or conversations, into a shared collection or a person's own. See [Ingest context](/essentials/v2/ingest) for every item field.
4. **Watch indexing finish** by polling [`GET /context/status`](/api-reference/v2/endpoint/source-status) until each `id` reaches `completed` (or `graph_creation` if you don't need graph traversal).
-5. **Query** with [`POST /query`](/api-reference/v2/endpoint/query). Pick `type: "knowledge"` for Knowledge, `type: "memory"` for Memories, or `type: "all"` for both. Pair with `query_by: "hybrid"` (default) or `"text"` (BM25, with `operator`). The mechanics live in [Query](/essentials/v2/query).
+5. **Query** with [`POST /query`](/api-reference/v2/endpoint/query). Name the collections to search, weighted if you like, and pair with `query_by: "hybrid"` (default) or `"text"` (BM25, with `operator`). Inject the returned `llm_prompt` into your model call. The mechanics live in [Query](/essentials/v2/query).
-That's the whole loop. Most of what changes between integrations is *what* you put in the metadata, *what* you query with, and *which* `type` and `query_by` combination fits the task.
+That's the whole loop. Most of what changes between integrations is *what* you put in the attributes, *which* collections you query, and *which* `query_by` and `mode` combination fits the task.
---
@@ -158,16 +157,16 @@ The deeper trade-offs - when to spin up a new database vs. a new collection, h
## Retrieval pipeline
-[`POST /query`](/api-reference/v2/endpoint/query) is the single retrieval endpoint. Two parameters describe the request: `type` picks the collection, `query_by` picks the method. From there, every call goes through the same pipeline:
+[`POST /query`](/api-reference/v2/endpoint/query) is the single retrieval endpoint. Two parameters describe the request: `collections` picks where to look, `query_by` picks the method. From there, every call goes through the same pipeline:
1. **Authenticate and scope.** Validate `database`, resolve the database, and apply the requested `collection`.
-2. **Filter before ranking.** Apply `metadata_filters` to narrow the candidate set (see [Metadata](/essentials/v2/attributes)).
+2. **Filter before ranking.** Apply `attributes` to narrow the candidate set (see [Attributes](/essentials/v2/attributes)).
3. **Retrieve.** Run hybrid retrieval over the semantic vector store and the keyword bm25 index, or BM25-only retrieval when `query_by: "text"`.
4. **Blend.** Use `alpha` to weight semantic vs. keyword bm25 contributions (`1.0` = pure semantic, `0.0` = pure BM25).
-5. **Enrich.** When `graph_context: true`, traverse the [context graph](/essentials/v2/context-graphs) and attach related paths. When `mode: "thinking"`, expand the query, rerank, and pull in author-declared forceful relations.
-6. **Shape the response.** Return ranked `chunks`, deduplicated `sources`, optional `graph_context`, and any `additional_context`.
+5. **Enrich.** When `graph_context: true`, traverse the [context graph](/essentials/v2/context-graphs) and attach related paths. When `mode: "thinking"`, expand the query, rerank, and pull in the relations items declared at ingest.
+6. **Shape the response.** Return ranked `chunks`, graph paths in `graph`, declared links in `forceful_relations`, and `llm_prompt`, the same context as one prompt-ready string with citation labels.
-The response is *retrieved context*, not a final LLM answer. You pass the chunks (and any graph context) into your own agent or model prompt - see [How to Use API Results](/essentials/v2/api-results) for the helper that formats it.
+The response is *retrieved context*, not a final LLM answer. You inject `llm_prompt` into your own agent or model prompt; see [How to Use API Results](/essentials/v2/api-results).
---
@@ -180,9 +179,9 @@ A short cheat sheet for the parameters you'll touch most often, and where each o
| `database` | Every call | Selects the isolated workspace. Required on every request. |
| `collection` | Ingest + query | Narrows data to a user, team, or workspace inside the database. Use a single collection for writes and single-scope queries. |
| `collections` | Query | Preferred query-time collection selector. Use a single-item list, a multi-scope list with equal weights, or a weighted object for fanout ranking. |
-| `metadata` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Schema-aligned metadata indexed at ingest. Must match the [database metadata schema](/essentials/v2/attributes) declared at database creation. |
-| `additional_metadata` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Free-form per-document metadata. Stored alongside the source; filterable at query via `metadata_filters.additional_metadata`. |
-| `metadata_filters` | [Query](/api-reference/v2/endpoint/query) | Deterministic narrowing before ranking. Top-level keys match `metadata`; nested `additional_metadata` filters free-form fields. |
+| `attributes` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Declared, filterable fields on an item. Must match the [database metadata schema](/essentials/v2/attributes) declared at database creation. |
+| `custom_attributes` | [Ingest](/api-reference/v2/endpoint/ingest-context) | Free-form per-item fields. Stored with the item; not filterable. |
+| `attributes` | [Query](/api-reference/v2/endpoint/query) | Deterministic narrowing with operators (`$eq`, `$in`, `$gte`, `$and`, ...) on declared attributes. |
| `alpha` | [Query](/api-reference/v2/endpoint/query) | Blends semantic vs. keyword bm25 scores in `query_by: "hybrid"`. |
| `graph_context` | [Query](/api-reference/v2/endpoint/query) | When `true`, attaches the relevant slice of the [context graph](/essentials/v2/context-graphs) to the response. |
| `mode` | [Query](/api-reference/v2/endpoint/query) | `"fast"` for low-latency single-pass retrieval; `"thinking"` for multi-query expansion and reranking. |
@@ -195,7 +194,7 @@ HydraDB separates **write-time** work from **query-time** work. Uploads return q
1. **Status first.** Did every `id` reach `completed` (or at least `graph_creation`)? Check [Ingestion Status](/api-reference/v2/endpoint/source-status).
2. **Scope second.** Is the `database` correct? Did you write under one `collection` and read under another? See [Multi-Tenant](/essentials/v2/databases-and-collections).
-3. **Filters third.** Are you using the right namespace? Top-level `metadata_filters` keys match `metadata`; free-form fields belong under `additional_metadata`. For hot top-level filters, declare the field in `database_metadata_schema` with `enable_match: true`. See [Metadata](/essentials/v2/attributes).
+3. **Filters third.** Is the key declared? `attributes` filters only match fields declared in `database_metadata_schema` and sent as `attributes` at ingest; `custom_attributes` are never filterable. For hot filters, declare the field with `enable_match: true`. See [Attributes](/essentials/v2/attributes).
This order catches almost every "I uploaded but query returns nothing" debugging session.
diff --git a/essentials/v2/attributes.mdx b/essentials/v2/attributes.mdx
index 7ee3aaa8..750faa7c 100644
--- a/essentials/v2/attributes.mdx
+++ b/essentials/v2/attributes.mdx
@@ -1,9 +1,13 @@
---
title: "Attributes"
-description: "How HydraDB uses metadata to scope query results with schema-backed fields, free-form additional metadata, source metadata edits, and exact metadata filters."
+description: "Declared, filterable attributes versus free-form custom attributes, how the database schema defines them, and how query filters run."
---
-Metadata is structured data attached to [Knowledge](/essentials/v2/knowledge) and [Memories](/essentials/v2/memories). Use it when you already know a hard constraint before retrieval runs, such as `department=legal`, `region=us`, `status=published`, or `author=alice`.
+
+**Names on a unified database.** An item carries `attributes` (declared, filterable; keys from `database_metadata_schema`) and `custom_attributes` (free-form, never filterable). A query filters with `attributes`, an operator object (`$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`, `$or`, `$not`, `$exists`); see [Query](/essentials/v2/query#2-request). Neither is returned on query chunks; read them with `GET /context/inspect`. The `metadata`, `additional_metadata` and `metadata_filters` names used below are the split-database and deprecated spellings of the same three things: the schema rules, size caps and filter semantics are the same.
+
+
+Metadata is structured data attached to every item you ingest. Use it when you already know a hard constraint before retrieval runs, such as `department=legal`, `region=us`, `status=published`, or `author=alice`.
HydraDB has two metadata layers:
diff --git a/essentials/v2/context-categories.mdx b/essentials/v2/context-categories.mdx
index 17120ba5..3afdee7b 100644
--- a/essentials/v2/context-categories.mdx
+++ b/essentials/v2/context-categories.mdx
@@ -1,6 +1,6 @@
---
title: "Context categories"
-description: "Label each item as a user preference, business knowledge or a decision trace, or let HydraDB decide."
+description: "Label each item as a user preference, business knowledge or a decision trace. The label is yours to set; nothing infers it."
---
A **context category** says what kind of context an item is. You set it per item with `context_category` on [`POST /context/ingest`](/essentials/v2/ingest). Every category lives in the same database and is searched by the same [query](/essentials/v2/query); the category describes the item, it does not decide where it is stored.
@@ -10,7 +10,7 @@ A **context category** says what kind of context an item is. You set it per item
| `user_preference` | What a person likes, chose or asked for | Conversations, product events | That person's collection |
| `business_knowledge` | Documentation, policy, product and domain facts | Docs, wikis, connector syncs | A shared collection |
| `decision_trace` | What an agent or a team decided, and why | Agent runs, ADRs, postmortems, approvals | A shared or per-team collection |
-| `auto` | The default: HydraDB decides | Anything | Wherever you send it |
+| `auto` | The default: no label | Anything | Wherever you send it |
---
@@ -24,7 +24,7 @@ A database holds three kinds of context:
Labelling an item tells HydraDB which of these it is, so enrichment extracts the right things from it. A conversation labelled `user_preference` is read for preferences; a postmortem labelled `decision_trace` is read for the decision, its outcome and the evidence behind it.
-A category you set is never relabelled. The value is validated strictly: a misspelling is a `400`, never an item filed under nothing.
+The label is yours. HydraDB never infers a category and never relabels one you set. The value is validated strictly: a misspelling is a `400`, never an item filed under nothing. On query, the label comes back as `enrichment_kind` on the item's chunks.
---
@@ -33,6 +33,25 @@ A category you set is never relabelled. The value is validated strictly: a missp
Likes, dislikes, choices and requests a person has made. They usually arrive as conversations or product events, and they belong in that person's own collection, so they personalize answers for that person only.
+```bash cURL
+curl -X POST 'https://api.hydradb.com/context/ingest' \
+ -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
+ -H "API-Version: 2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "database": "acme",
+ "collection": "user_alex",
+ "context": [{
+ "context_id": "chat-alex-001",
+ "conversation": [
+ { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" },
+ { "role": "assistant", "content": "Got it, short answers." }
+ ],
+ "context_category": "user_preference",
+ "happened_at": "2026-09-01"
+ }]
+ }'
+```
```python Python SDK
import json
@@ -65,27 +84,10 @@ await client.context.ingest({
}]),
});
```
-```bash cURL
-curl -X POST 'https://api.hydradb.com/context/ingest' \
- -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
- -H "API-Version: 2" \
- -H "Content-Type: application/json" \
- -d '{
- "database": "acme",
- "collection": "user_alex",
- "items": [{
- "context_id": "chat-alex-001",
- "conversation": [
- { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" },
- { "role": "assistant", "content": "Got it, short answers." }
- ],
- "context_category": "user_preference",
- "happened_at": "2026-09-01"
- }]
- }'
-```
+The SDKs send the array in the `items` form field; the JSON body calls the list `context`. See [Ingest context](/essentials/v2/ingest#1-one-call-for-text-and-conversations).
+
Recall it by querying that person's collection:
```json
@@ -96,6 +98,19 @@ Recall it by querying that person's collection:
}
```
+The matching chunk comes back with the preference enrichment extracted from the conversation:
+
+```json
+{
+ "chunk_id": "ck_9f2",
+ "context_id": "chat-alex-001",
+ "score": 0.91,
+ "content": "user: Keep answers short, I read on my phone.\nassistant: Got it, short answers.",
+ "enrichment": "Alex prefers short answers.",
+ "enrichment_kind": "user_preference"
+}
+```
+
---
## 3. `business_knowledge`
@@ -106,7 +121,7 @@ Facts that are true for everyone in the database: documentation, policies, produ
{
"database": "acme",
"collection": "company",
- "items": [{
+ "context": [{
"context_id": "refund-policy",
"title": "Refund policy",
"text": "Refunds are processed within 5 business days. Enterprise customers can request an expedited refund.",
@@ -120,13 +135,13 @@ Facts that are true for everyone in the database: documentation, policies, produ
## 4. `decision_trace`
-The record of an agent or a team reasoning to an outcome: an agent run, an architecture decision record, a postmortem, an approval. Set `happened_at` to when the decision was made, and `user_name` to the agent or person who made it.
+The record of an agent or a team reasoning to an outcome: an agent run, an architecture decision record, a postmortem, an approval. Send it as text with the label. Set `happened_at` to when the decision was made, and `user_name` to the agent or person who made it.
```json
{
"database": "acme",
"collection": "platform",
- "items": [{
+ "context": [{
"context_id": "adr-012",
"title": "ADR 012: ledger datastore",
"text": "We chose Postgres over DynamoDB for the ledger because every write needs transactional reads across accounts, and the month-end batch touches every account at once.",
@@ -139,37 +154,39 @@ The record of an agent or a team reasoning to an outcome: an agent run, an archi
### How a decision appears in query results
-A decision trace becomes a node in the [context graph](/essentials/v2/context-graphs) of type `DECISION_TRACE`. The node is named by the decision, connected to the agent or team that made it, and carries the evidence behind it. The outcome rides on the relation. When a query reaches the decision, it comes back as a path in `graph.paths[]`:
+A decision trace becomes part of the [context graph](/essentials/v2/context-graphs), connected to the agent or team that made it and to what it decided. When a query reaches the decision, it comes back as a path in `graph[]`, and the decision's own chunk carries `enrichment_kind: "decision_trace"`:
```json
{
- "graph": {
- "paths": [
- {
- "triplets": [
- {
- "source": { "name": "platform-team", "type": "AGENT" },
- "relation": { "predicate": "decided", "context": "Postgres chosen for the ledger" },
- "target": { "name": "Choose the ledger datastore", "type": "DECISION_TRACE" }
- }
- ],
- "relevancy_score": 0.87,
- "chunk_ids": ["adr-012_chunk_0"]
- }
- ]
- }
+ "graph": [
+ {
+ "origin": "query_path",
+ "triplets": [
+ {
+ "source": { "entity_id": "ent_pt1", "name": "platform-team" },
+ "relation": {
+ "predicate": "decided",
+ "context": "Postgres chosen over DynamoDB for the ledger.",
+ "temporal_details": "2026-07-14",
+ "relationship_id": "rel_88",
+ "chunk_id": "ck_adr12"
+ },
+ "target": { "entity_id": "ent_ldg", "name": "ledger datastore" }
+ }
+ ],
+ "path_summary": "platform-team chose Postgres for the ledger datastore because writes need transactional reads across accounts."
+ }
+ ]
}
```
-The fields stored for a decision are `activity` (what was decided), `outcome`, `evidence`, `decided_at` and `agent`.
-
---
## 5. `auto`
-Leave `context_category` out, or send `auto`, and HydraDB classifies the item itself. That is the right choice when you do not know ahead of time what an item holds, for example a mixed export or a long chat log.
+Leave `context_category` out, or send `auto`, and the item carries no label: it is stored and enriched as general context, and its chunks come back without `enrichment_kind`. HydraDB does not classify the item for you.
-Pin a category when you do know. A pinned category is never relabelled, and it tells enrichment exactly what to look for.
+Use it for context that is none of the three kinds, or when you genuinely do not know. Pin a category whenever you do: a pinned category is never relabelled, and it tells enrichment exactly what to look for.
---
@@ -189,7 +206,7 @@ One request can carry any mix of categories:
{
"database": "acme",
"collection": "company",
- "items": [
+ "context": [
{ "text": "We chose Postgres over DynamoDB for the ledger.", "context_category": "decision_trace" },
{ "text": "Refunds are processed within 5 business days.", "context_category": "business_knowledge" },
{ "text": "Weekly sync notes, 2026-09-08" }
@@ -197,7 +214,7 @@ One request can carry any mix of categories:
}
```
-The last item has no category, so HydraDB decides.
+The last item has no category, so it is stored as general context.
### Preferences per person, knowledge shared
diff --git a/essentials/v2/context-graphs.mdx b/essentials/v2/context-graphs.mdx
index fab3924f..68555ef8 100644
--- a/essentials/v2/context-graphs.mdx
+++ b/essentials/v2/context-graphs.mdx
@@ -1,23 +1,27 @@
---
title: "Context Graphs"
-description: "How HydraDB models relationships between chunks using triplets to improve query quality."
+description: "How HydraDB models relationships between the entities in your context as triplets, and returns them as paths on every query."
---
+import LegacyLine from "/snippets/legacy-line.mdx";
+
## 1. What it is
-A context graph is a structured map of relationships between stored pieces of context in your database.
+A context graph is a structured map of the entities in your database and the relationships between them, built from everything you ingest.
-It represents those relationships as **triplets**: `source → relation → target`. Each triplet is a directional connection between two pieces of context, with `source`, `relation`, and `target` returned as structured objects - not plain strings.
+It represents those relationships as **triplets**: `source`, `relation`, `target`. Each triplet is a directional connection between two entities, returned as structured objects, not plain strings. A query returns the triplets that connect what you asked about to what is relevant, grouped into **paths**.
Context graphs augment retrieval. They do not replace it.
+
+
---
## 2. What it does
-When `graph_context: true` is set on a query call (the default), HydraDB returns relationship data alongside the retrieved chunks - showing how those chunks connect to each other and to your query. Set `graph_context: false` to drop the graph slice when you only need ranked chunks.
+With `graph_context: true` on a query (the default), HydraDB returns `graph[]` alongside the retrieved chunks: the paths through the graph that connect the query to the results and the results to each other. Set `graph_context: false` when you only need ranked chunks; `graph` is then `[]`.
-This helps your LLM reason about questions that require connecting information across multiple chunks or sources. Similarity query returns relevant content; the context graph surfaces how that content fits together.
+This helps your LLM reason about questions that require connecting information across several items. Similarity retrieval returns relevant content; the context graph surfaces how that content fits together.
---
@@ -25,8 +29,8 @@ This helps your LLM reason about questions that require connecting information a
Use context graphs when:
-- Answers require synthesising information across multiple chunks.
-- Relational context matters for correctness - cause and effect, ownership, sequence, dependency.
+- Answers require synthesising information across several chunks.
+- Relational context matters for correctness: cause and effect, ownership, sequence, dependency.
- You need multi-hop reasoning ("What team owns the service that failed?", "What depends on this API?").
Skip them for direct factual lookups. Graph traversal adds response size and can add latency, so reserve it for queries where relational structure materially improves the answer.
@@ -37,33 +41,33 @@ Skip them for direct factual lookups. Graph traversal adds response size and can
Context graphs are hybrid: relationships are extracted at ingestion time and traversed at query time.
-**At ingestion**, HydraDB extracts relationships from your data and stores them in the graph. Sources can also declare explicit relationships to other sources via a `relations` payload at ingestion. Or skip extraction for a document and supply the entities and relations yourself with [Bring Your Own Graph](/essentials/v2/bring-your-own-graph).
+**At ingestion**, with `enrich: true` (the default), HydraDB extracts entities and relations from each item and stores them in the graph. An item can also declare its own links to other items with [`forceful_relations`](/essentials/v2/ingest#11-declared-relations), or skip extraction and supply its entities and relations with [`graph_payload`](/essentials/v2/ingest#12-bring-your-own-graph).
-**At query**, when `graph_context: true` is set (the default):
+**At query**, with `graph_context: true`:
1. HydraDB runs hybrid retrieval to find relevant chunks.
-2. It traverses the graph to discover relevant relationships between retrieved context.
-3. It returns multi-hop paths from the query (`query_paths`), relationship paths between retrieved chunks (`chunk_relations`), and a chunk-to-path-group mapping (`chunk_id_to_group_ids`).
+2. It traverses the graph from the query and from the retrieved chunks.
+3. It returns the paths it found in `graph[]`: paths grown from the query first (`origin: "query_path"`), then paths expanded from the returned chunks (`origin: "chunk_relation"`), deduplicated.
-When no relevant relationships are found, graph fields may be empty.
+When no relevant relationships are found, `graph` is `[]`. That is not an error; it is the absence of structure for that query.
---
## 5. Key concepts
-**Triplets.** The unit of the graph. Each triplet is `source → relation → target`, where `source` and `target` are entity objects and `relation` describes the connection between them.
+**Triplets.** The unit of the graph. `source` and `target` are entities, `{ entity_id, name }`. `relation` describes the connection: `predicate`, the sentence it was extracted from (`context`), when it holds (`temporal_details`, absent when unknown), its `relationship_id`, and the `chunk_id` of the chunk that is evidence for it.
-Example: `billing_policy → governs → failed_payment_handling`
+Example: `Alex`, `prefers`, `short answers`, from chunk `ck_9f2`.
-**`query_paths`.** Multi-hop chains of triplets connecting the query to retrieved chunks. Each path carries a relevancy score and the chunk IDs whose traversal produced it.
+**Paths.** A chain of triplets plus a `path_summary`, one sentence that states what the chain means (never empty: when the server wrote no summary, it narrates the hops), and an `origin`: `query_path` when it was grown from the entities in the query, `chunk_relation` when it is the neighbourhood of a returned chunk. `graph[]` is one flat, ordered list of paths.
-**`chunk_relations`.** Paths describing how returned chunks relate to one another. Same shape as `query_paths`; the difference is the anchor - query-driven vs chunk-to-chunk.
+**Evidence.** `relation.chunk_id` names the chunk every step of a path was extracted from. Group hops by it against `chunks[].chunk_id` to show a chunk's relations under that chunk: a `chunk_relation` path is only returned when one of its hops came from a returned chunk (or a `forceful_relations` chunk), and hangs under that chunk; a `query_path` hop may also sit under the chunk it came from. See [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks).
-**`chunk_id_to_group_ids`.** Maps each chunk ID to the path-group identifiers (e.g. `p_0`, `p_1`) it belongs to. Use it to group retrieved chunks by which graph path produced them.
+**Forceful relations.** Links between items rather than between entities, which you declare at ingest with `forceful_relations`. They come back in `forceful_relations[]`, not in `graph[]`.
-**Connected subgraph.** The graph also holds relations between *items* rather than between entities: a Slack reply and the message it answers, a page and the pages it links to, a comment and its ticket. Given one item's id, [Connected Subgraph](/api-reference/v2/endpoint/subgraph) walks those links breadth-first and returns everything reachable (the rest of the thread, the hierarchy above and below, the items it references) with the relations among them. Reach for it when one query result is not enough and you need what surrounds it; it is also what the dashboard's **Subgraph** button opens.
+**Connected subgraph.** The graph also holds relations between items themselves: a Slack reply and the message it answers, a page and the pages it links to, a comment and its ticket. Given one item's id, [Connected Subgraph](/api-reference/v2/endpoint/subgraph) walks those links breadth-first and returns everything reachable with the relations among them. Reach for it when one query result is not enough and you need what surrounds it; it is also what the dashboard's **Subgraph** button opens.
-For full field schemas, see the [Query API Reference](/api-reference/v2/endpoint/query-overview).
+For the full field reference, see [Query](/essentials/v2/query#graph).
---
@@ -77,10 +81,8 @@ curl -X POST 'https://api.hydradb.com/query' \
-H "API-Version: 2" \
-H "Content-Type: application/json" \
-d '{
- "database": "acme_corp",
+ "database": "acme",
"query": "How does the billing system handle failed payments?",
- "type": "knowledge",
- "query_by": "hybrid",
"mode": "thinking",
"graph_context": true
}'
@@ -88,112 +90,125 @@ curl -X POST 'https://api.hydradb.com/query' \
```typescript TypeScript SDK
const result = await client.query({
- database: "acme_corp",
+ database: "acme",
query: "How does the billing system handle failed payments?",
- type: "knowledge",
- queryBy: "hybrid",
mode: "thinking",
graphContext: true,
});
-if (result.data?.graphContext) {
- for (const path of result.data.graphContext.queryPaths) {
- for (const t of path.triplets) {
- console.log(
- `${t.source.name} - ${t.relation.canonicalPredicate} → ${t.target.name}`,
- );
- }
+for (const path of result.data.graph) {
+ console.log(path.pathSummary);
+ for (const t of path.triplets) {
+ console.log(` ${t.source.name} -> ${t.relation.predicate} -> ${t.target.name} [${t.relation.chunkId}]`);
}
}
```
```python Python SDK
result = client.query(
- database="acme_corp",
+ database="acme",
query="How does the billing system handle failed payments?",
- type="knowledge",
- query_by="hybrid",
mode="thinking",
graph_context=True,
)
-if result.data.graph_context:
- for path in result.data.graph_context.query_paths:
- for t in path.triplets:
- print(
- t.source.name,
- " - ",
- t.relation.canonical_predicate,
- "→",
- t.target.name,
- )
+for path in result.data.graph:
+ print(path.path_summary)
+ for t in path.triplets:
+ print(f" {t.source.name} -> {t.relation.predicate} -> {t.target.name} [{t.relation.chunk_id}]")
```
-A response with graph context looks like:
+The `graph` key of the response looks like:
```json
{
- "chunks": [ /* ranked chunks */ ],
- "graph_context": {
- "query_paths": [
- {
- "triplets": [
- {
- "source": { "name": "billing_policy", "type": "POLICY" },
- "relation": { "canonical_predicate": "governs" },
- "target": { "name": "failed_payment_handling", "type": "PROCESS" }
- }
- ],
- "relevancy_score": 0.84,
- "group_id": "p_0",
- "source_chunk_ids": ["chunk_abc", "chunk_def"]
- }
- ],
- "chunk_relations": [],
- "chunk_id_to_group_ids": { "chunk_abc": ["p_0"] }
- }
+ "graph": [
+ {
+ "origin": "query_path",
+ "triplets": [
+ {
+ "source": { "entity_id": "ent_b10", "name": "billing policy" },
+ "relation": {
+ "predicate": "governs",
+ "context": "The billing policy governs how failed payments are retried.",
+ "relationship_id": "rel_41",
+ "chunk_id": "ck_2aa"
+ },
+ "target": { "entity_id": "ent_f77", "name": "failed payment handling" }
+ },
+ {
+ "source": { "entity_id": "ent_f77", "name": "failed payment handling" },
+ "relation": {
+ "predicate": "triggers",
+ "context": "A failed payment triggers a customer notification.",
+ "temporal_details": "after the third retry",
+ "relationship_id": "rel_42",
+ "chunk_id": "ck_2ab"
+ },
+ "target": { "entity_id": "ent_n03", "name": "notification service" }
+ }
+ ],
+ "path_summary": "The billing policy governs failed payment handling, which notifies the customer after the third retry."
+ }
+ ]
}
```
-When no relevant relationships are found, graph fields may be empty.
-
---
## 7. Using graph context in your prompt
-To include graph relationships in your LLM prompt, use the `buildContextString` / `build_context_string` helper from [How to Use API Results](/essentials/v2/api-results). It handles `query_paths`, `chunk_relations`, and `chunk_id_to_group_ids` automatically.
+You do not format the graph yourself. The `llm_prompt` returned by the same query already contains a `## Related facts` section with one line per path in `graph[]`: its label (`[P1]`, `[P2]`, ... in `graph[]` order, so an agent can cite a fact by it), its chain of hops, `query path` or `chunk relation` for its `origin` with the path's relevance after reranking when it has one, and the numbers of the results its hops were extracted from. The `path_summary` is indented on the line under it, unless it only narrates the hops the chain already shows. From the [example response on Query](/essentials/v2/query#1-one-call):
-The helper formats triplets as:
+```markdown
+## Related facts
+- [P1] **Refund Processing** -managed by→ **Finance Department** (query path, relevance 0.81) [1]
+ Refund processing is managed by the Finance Department.
+- [P2] **User** -prefers→ **short answers** (chunk relation, relevance 0.74) [2]
+ The user prefers short answers about refunds.
```
-[billing_policy] -> governs -> [failed_payment_handling]: policy covering retry logic
- ↳ [failed_payment_handling] -> triggers -> [notification_service]: sends customer alert
-```
+
+How a chain reads:
+
+- Each hop is `**source** -predicate→ **target**`. The next hop continues the chain only from the entity the chain ended on, so the two-hop billing path in [section 6](#6-minimal-working-example) reads `**billing policy** -governs→ **failed payment handling** -triggers→ **notification service** (after the third retry)`.
+- A hop walked against its edge reads `←predicate-`: in `**A** -p→ **B** ←q- **C**`, the edge is `C -q→ B`.
+- A hop that shares no entity with the chain starts a new segment after `; `.
+- A hop's `temporal_details` follows it in parentheses, as `(after the third retry)` does in that path.
+- A citation `[1]` is a result the hop was extracted from (`[R1]` for a forceful relation). A hop whose chunk is not in the response carries none.
+
+Inject `llm_prompt` and the model can reason over the paths and cite them. See [How to Use API Results](/essentials/v2/api-results).
---
## 8. Common mistakes
-**Setting `graph_context: false` and then expecting the graph slice.** Graph context is on by default - only set the flag to `false` if you explicitly don't want graph data.
+**Setting `graph_context: false` and then expecting paths.** Graph context is on by default; only set the flag to `false` if you explicitly do not want graph data.
+
+**Assuming the graph replaces retrieval.** It does not. `chunks` is still the primary result; the graph enriches it with relationships. Use both.
+
+**Forgetting to disable when you do not need it.** Graph context adds response size and a small traversal cost. If your code path only consumes `chunks`, set `graph_context: false`.
-**Assuming the graph replaces retrieval.** It doesn't. `chunks` is still the primary result; the graph enriches it with relationships. Use both.
+**Treating triplets as flat strings.** `source`, `relation` and `target` are objects with their own fields. Read them as structured data.
-**Forgetting to disable when you don't need it.** Graph context adds response size and a small traversal cost. If your code path only consumes `chunks`, set `graph_context: false` to drop it.
+**Looking for forceful relations in `graph[]`.** Items you linked with `forceful_relations` come back in `forceful_relations[]`, with the `via` link that brought them in.
+
+**Expecting relationships that do not exist.** If the retrieved chunks do not share entities or declared relations, `graph` is `[]`.
+
+---
-**Treating triplets as flat strings.** `source`, `relation`, and `target` are objects with their own fields. Read them as structured data.
+## 9. Split databases
-**Expecting relationships that don't exist.** If the retrieved chunks don't share entities or declared relations, the graph fields will be empty. That's not an error - it's the absence of structure for that query.
+A database created with `type: "split"` returns the graph under `graph_context` as `query_paths`, `chunk_relations` and `chunk_id_to_group_ids`, with `canonical_predicate` and `relevancy_score` on each path. The mapping to `graph[]` is on [Split databases and legacy fields](/essentials/v2/split-databases#4-legacy-query-and-response-fields).
---
## Related
-- [Query](/essentials/v2/query) - how chunks and graph context are retrieved together
-- [Bring Your Own Graph](/essentials/v2/bring-your-own-graph) - supply your own entities and relations instead of auto-extraction
-- [Memories](/essentials/v2/memories) - user-scoped context for personalization
-- [Knowledge](/essentials/v2/knowledge) - document-level context for shared retrieval
-- [How to Use API Results](/essentials/v2/api-results) - formatting graph context for LLM prompts
-- [Full Query API Reference](/api-reference/v2/endpoint/query-overview) - full graph response schema
-- [Connected Subgraph](/api-reference/v2/endpoint/subgraph) - everything connected to one item, walked breadth-first
+- [Query](/essentials/v2/query): how chunks, paths and forceful relations are retrieved together
+- [Ingest context](/essentials/v2/ingest): `enrich`, `forceful_relations` and `graph_payload`
+- [Bring Your Own Graph](/essentials/v2/bring-your-own-graph): supply your own entities and relations instead of auto-extraction
+- [How to Use API Results](/essentials/v2/api-results): the `## Related facts` section of `llm_prompt`
+- [Connected Subgraph](/api-reference/v2/endpoint/subgraph): everything connected to one item, walked breadth-first
diff --git a/essentials/v2/databases-and-collections.mdx b/essentials/v2/databases-and-collections.mdx
index d5c5376e..bc0ba1e2 100644
--- a/essentials/v2/databases-and-collections.mdx
+++ b/essentials/v2/databases-and-collections.mdx
@@ -73,15 +73,19 @@ Typical flow:
- Workspace-specific data uses a workspace `collection`.
- User-specific Memories use a user-level `collection`.
-### Shared Knowledge + user personalization
+### Shared context + user personalization
-For personalized answers grounded in shared Knowledge, use the `type` parameter on `POST /query`:
+For personalized answers grounded in shared context, put the shared context in a shared collection and each person's preferences in their own, then query both with weighted `collections` on `POST /query`:
-- `type: "knowledge"` retrieves shared Knowledge (Knowledge vector store, `vectorstore_status.knowledge`).
-- `type: "memory"` retrieves user-specific Memories (Memories vector store, `vectorstore_status.memories`).
-- `type: "all"` runs both in parallel and returns one merged, re-ranked result set - usually what you want for personalized answers.
+```json
+{
+ "database": "acme_corp",
+ "collections": { "user_john": 2, "company": 1 },
+ "query": "How should I explain our refund policy to this user?"
+}
+```
-When you need different formatting for shared vs personal context in the LLM prompt, call `POST /query` twice (once with `type: "knowledge"`, once with `type: "memory"`) and combine in your application.
+The weights rank the person's own context above the shared context without excluding either. There is no `type` on a unified database; the collections are the only scope you choose. The returned `llm_prompt` already merges both into one prompt-ready string.
---
@@ -107,17 +111,11 @@ Use the same scoping values on query that you used when writing the data. A quer
If your application needs to combine data from multiple scopes, prefer one query call with `collections` unless you need separate response formatting or client-side treatment per scope.
-Use `collection` for partitioning data. Use `metadata_filters` for narrowing results within that scope.
-
-### Memories and Knowledge
+Use `collection` for partitioning data. Use `attributes` for narrowing results within that scope.
-Memories and Knowledge live in separate stores, both reached through `POST /query`:
+### Personal and shared context
-- `type: "memory"` retrieves **Memories** (user-scoped, personal).
-- `type: "knowledge"` retrieves **Knowledge** (shared documents and app sources).
-- `type: "all"` retrieves both in one call.
-
-For a personalized answer, the common pattern is one `POST /query` with `type: "all"` and the user's `collection`. HydraDB runs both stores in parallel and merges results. Use two separate calls only when you need to format the two streams differently in your LLM prompt.
+A person's preferences and the company's shared context live in the same database and are read by the same `POST /query`; collections are what keep them apart. Write each person's context under their own `collection` and shared context under a shared one, and name both in `collections` when you query. Use two separate calls only when you need to format the two streams differently in your LLM prompt; otherwise one call and its `llm_prompt` is the whole pattern.
Use the same collection on writes and reads for the same logical scope.
@@ -128,37 +126,37 @@ For a personalized answer, the common pattern is one `POST /query` with `type: "
## 5. Minimal working example
-The example below shows a common personalized-answer flow: write a user Memory with a single `collection`, then query Knowledge and Memories together with `collections`.
+The example below shows a common personalized-answer flow: write a person's preference under their own `collection`, then query it together with the shared collection using `collections`.
```bash cURL
-# 1. Write a per-user memory under the user's collection.
+# 1. Write a person's preference under their own collection.
curl -X POST 'https://api.hydradb.com/context/ingest' \
-H "Authorization: Bearer $HYDRA_DB_API_KEY" \
-H "API-Version: 2" \
- -F "type=memory" \
- -F "database=acme_corp" \
- -F "collection=user_123" \
- -F "upsert=true" \
- -F 'memories=[
- {
- "text": "Prefers dark mode and short answers.",
- "infer": true,
- "user_name": "John"
- }
- ]'
+ -H "Content-Type: application/json" \
+ -d '{
+ "database": "acme_corp",
+ "collection": "user_123",
+ "context": [
+ {
+ "text": "Prefers dark mode and short answers.",
+ "context_category": "user_preference",
+ "user_name": "John"
+ }
+ ]
+ }'
-# 2. Search Knowledge + Memories together with type: "all".
+# 2. Query the person's collection and the shared one together.
curl -X POST 'https://api.hydradb.com/query' \
-H "Authorization: Bearer $HYDRA_DB_API_KEY" \
-H "API-Version: 2" \
-H "Content-Type: application/json" \
-d '{
"database": "acme_corp",
- "collection": "user_123",
+ "collections": { "user_123": 2, "company": 1 },
"query": "refund policy",
- "type": "all",
"query_by": "hybrid",
"mode": "thinking"
}'
@@ -171,27 +169,25 @@ const client = new HydraDBClient({
token: process.env.HYDRA_DB_API_KEY,
});
-// 1. Write a per-user memory under the user's collection.
+// 1. Write a person's preference under their own collection.
+// The SDK sends the item list in the `items` form field.
await client.context.ingest({
- type: "memory",
database: "acme_corp",
collection: "user_123",
- upsert: true,
- memories: JSON.stringify([
+ items: JSON.stringify([
{
text: "Prefers dark mode and short answers.",
- infer: true,
+ context_category: "user_preference",
user_name: "John",
},
]),
});
-// 2. Search Knowledge + Memories together with type: "all".
+// 2. Query the person's collection and the shared one together.
const result = await client.query({
database: "acme_corp",
- collection: "user_123",
+ collections: { user_123: 2, company: 1 },
query: "refund policy",
- type: "all",
queryBy: "hybrid",
mode: "thinking",
});
@@ -204,27 +200,25 @@ from hydra_db import HydraDB
client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])
-# 1. Write a per-user memory under the user's collection.
+# 1. Write a person's preference under their own collection.
+# The SDK sends the item list in the `items` form field.
client.context.ingest(
- type="memory",
database="acme_corp",
collection="user_123",
- upsert=True,
- memories=json.dumps([
+ items=json.dumps([
{
"text": "Prefers dark mode and short answers.",
- "infer": True,
+ "context_category": "user_preference",
"user_name": "John",
}
]),
)
-# 2. Search Knowledge + Memories together with type: "all".
+# 2. Query the person's collection and the shared one together.
result = client.query(
database="acme_corp",
- collection="user_123",
+ collections={"user_123": 2, "company": 1},
query="refund policy",
- type="all",
query_by="hybrid",
mode="thinking",
)
@@ -251,8 +245,8 @@ A query call uses the scope you provide. If your application needs data from mul
**Writing shared Knowledge under a user scope by accident.**
If broadly shared Knowledge is written with a user-specific `collection`, it may not appear where other users expect it. Choose the write scope based on where the content should be queried later.
-**Using metadata filters as a substitute for collections.**
-Metadata filters narrow results inside a scope. They are not a replacement for choosing the right `database` and `collection`.
+**Using attribute filters as a substitute for collections.**
+Attribute filters narrow results inside a scope. They are not a replacement for choosing the right `database` and `collection`.
**Using unstable identifiers.**
Avoid display names, emails that may change, or user-provided labels as long-term scope identifiers. Prefer stable internal IDs such as `user_123`, `workspace_42`, or `org_acme`.
diff --git a/essentials/v2/glossary.mdx b/essentials/v2/glossary.mdx
index e4b5abde..a898c174 100644
--- a/essentials/v2/glossary.mdx
+++ b/essentials/v2/glossary.mdx
@@ -16,6 +16,30 @@ it and HydraDB uses the database's default collection.
See [Multi tenancy](/essentials/v2/databases-and-collections) for how scoping affects writes
and reads.
+## Context item
+
+One piece of context you ingest: a `text` or a `conversation`, identified by its
+`context_id`. Items are sent in the `context` list of `POST /context/ingest` and come
+back from `POST /query` as `chunks`, each carrying the `context_id` it came from. See
+[Ingest context](/essentials/v2/ingest).
+
+## Forceful relations
+
+Links you declare between context items at ingest, with an item's `forceful_relations`
+field. When a query hits an item, HydraDB follows its declared links
+(`follow_forceful_relations`, on by default) and returns the linked chunks in the
+response's `forceful_relations[]`, each with the `via` link that brought it in,
+separately from the ranked `chunks` and the `graph` paths. See
+[Declared relations](/essentials/v2/ingest#11-declared-relations).
+
+## Unified and split databases
+
+A database is `unified` (the default: one corpus, no `type` on any call, four-key
+query response with `llm_prompt`) or `split` (created with `type: "split"`: a
+knowledge corpus and a memory corpus, selected with `type`, with the older request and
+response shapes). `GET /databases` reports `details[].type`. See
+[Split databases and legacy fields](/essentials/v2/split-databases).
+
## Deprecated aliases
`database` and `collection` were previously called `tenant_id` and
diff --git a/essentials/v2/ingest.mdx b/essentials/v2/ingest.mdx
index f36c729c..8b8bf932 100644
--- a/essentials/v2/ingest.mdx
+++ b/essentials/v2/ingest.mdx
@@ -1,11 +1,13 @@
---
title: "Ingest context"
-description: "Send text and conversations to HydraDB as items in one call, label what kind of context each one is, and confirm it is searchable."
+description: "Send text and conversations to HydraDB as context items in one call, label what kind of context each one is, and confirm it is searchable."
---
import LegacyLine from "/snippets/legacy-line.mdx";
-Everything you put into HydraDB is an **item**: a piece of text, or a conversation. You send items to one endpoint, [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), and HydraDB chunks them, embeds them, extracts entities and relations into the [context graph](/essentials/v2/context-graphs), and makes them searchable through [`POST /query`](/essentials/v2/query).
+Everything you put into HydraDB is a piece of **context**: a text, or a conversation. You send a list of them to one endpoint, [`POST /context/ingest`](/api-reference/v2/endpoint/ingest-context), and HydraDB chunks each one, embeds it, enriches it, extracts entities and relations into the [context graph](/essentials/v2/context-graphs), and makes it searchable through [`POST /query`](/essentials/v2/query).
+
+This page describes ingest on a **unified** database, which is every database unless it was created with `type: "split"`.
@@ -13,9 +15,38 @@ Everything you put into HydraDB is an **item**: a piece of text, or a conversati
## 1. One call for text and conversations
-A single request can mix text items and conversation items, in any collection of a database.
+A single request can mix text and conversation items, in any collection of a database. The body is JSON, and the list is called `context`.
+```bash cURL
+curl -X POST 'https://api.hydradb.com/context/ingest' \
+ -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
+ -H "API-Version: 2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "database": "acme",
+ "collection": "company",
+ "context": [
+ {
+ "context_id": "refund-policy",
+ "title": "Refund policy",
+ "text": "Refunds are processed within 5 business days.",
+ "context_category": "business_knowledge",
+ "attributes": { "department": "support" },
+ "custom_attributes": { "owner": "sam@acme.com" }
+ },
+ {
+ "context_id": "chat-alex-001",
+ "conversation": [
+ { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" },
+ { "role": "assistant", "content": "Got it, short answers." }
+ ],
+ "context_category": "user_preference",
+ "happened_at": "2026-09-01"
+ }
+ ]
+ }'
+```
```python Python SDK
import json
@@ -43,7 +74,7 @@ ingest = client.context.ingest(
]),
)
-print([r.id for r in ingest.data.results])
+print([r.source_id for r in ingest.data.results])
```
```typescript TypeScript SDK
const ingest = await client.context.ingest({
@@ -70,40 +101,13 @@ const ingest = await client.context.ingest({
]),
});
-console.log(ingest.data.results.map((r) => r.id));
-```
-```bash cURL
-curl -X POST 'https://api.hydradb.com/context/ingest' \
- -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
- -H "API-Version: 2" \
- -H "Content-Type: application/json" \
- -d '{
- "database": "acme",
- "collection": "company",
- "items": [
- {
- "context_id": "refund-policy",
- "title": "Refund policy",
- "text": "Refunds are processed within 5 business days.",
- "context_category": "business_knowledge",
- "attributes": { "department": "support" },
- "custom_attributes": { "owner": "sam@acme.com" }
- },
- {
- "context_id": "chat-alex-001",
- "conversation": [
- { "role": "user", "content": "Keep answers short, I read on my phone.", "name": "alex" },
- { "role": "assistant", "content": "Got it, short answers." }
- ],
- "context_category": "user_preference",
- "happened_at": "2026-09-01"
- }
- ]
- }'
+console.log(ingest.data.results.map((r) => r.sourceId));
```
-The request body is JSON. The SDKs send the same array as the `items` form field, which is why `items` is a JSON string there. Keys inside each item stay `snake_case` in every language.
+
+**SDK users: the form field is still called `items`.** The SDKs send a multipart form rather than a JSON body, and the array goes in the `items` form field, which is why `items` is a JSON string there. The form also takes `database`, `collection`, `upsert`, `enrich`, `instructions` and `graph_payload` as fields. Both entry points run the same validation. Prefer the JSON body with `context` when you call the API directly. Keys inside each item stay `snake_case` in every language.
+
The response is `202 Accepted`:
@@ -111,43 +115,77 @@ The response is `202 Accepted`:
{
"success": true,
"data": {
+ "success": true,
+ "message": "Context queued for ingestion successfully. Ingestion is asynchronous: this 202 means the sources were accepted and queued, not indexed. Poll GET /context/status?database=&id= until each source's indexing_status reaches a terminal state (completed or errored) before querying. See https://docs.hydradb.com/api-reference/v2/endpoint/source-status for usage details. ",
"results": [
- { "id": "refund-policy", "status": "queued" },
- { "id": "chat-alex-001", "status": "queued" }
+ { "source_id": "refund-policy", "title": "Refund policy", "status": "queued", "infer": true, "error": null, "error_code": null },
+ { "source_id": "chat-alex-001", "title": null, "status": "queued", "infer": true, "error": null, "error_code": null }
],
"success_count": 2,
"failed_count": 0
- }
+ },
+ "error": null,
+ "meta": { "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d" }
}
```
-A `202` means the items were accepted and queued, not that they are searchable yet. See [Verify processing](#11-verify-processing).
+- `message` starts with `Context queued for ingestion successfully` (`Context ingestion completed with some failures` when an item failed), followed by a reminder to poll status.
+- `results[].source_id` is the item's `context_id`: the one you sent, or the generated one. The result item keeps the name `source_id`; read it as the context id and pass it to [`GET /context/status`](/api-reference/v2/endpoint/source-status).
+- `results[].infer` mirrors the item's `enrich`.
+- `results[].status` is `queued` or `failed`. A failed item carries `error` and `error_code`; the other items in the request are still queued.
+
+A `202` means the items were accepted and queued, not that they are searchable yet. See [Verify processing](#14-verify-processing).
---
-## 2. Item fields
+## 2. Request body
| Field | Notes |
| --- | --- |
-| `context_id` | Your id for the item. Generated from `title` when omitted, so two items with the same text, no title and no id collide. Must not contain commas. |
-| `title` | Stored as the item's title. Searchable with `titles` on [query](/essentials/v2/query). |
-| `text` | Plain text. Send exactly one of `text` or `conversation`. |
-| `conversation` | A list of `{ role, content, name }` turns. See [Conversation items](#4-conversation-items). |
-| `context_category` | `auto` (default), `user_preference`, `business_knowledge` or `decision_trace`. Validated strictly, so a typo is a `400`. See [Context categories](/essentials/v2/context-categories). |
-| `attributes` | Filterable fields declared in the database's `database_metadata_schema`. See [Attributes](/essentials/v2/attributes). |
-| `custom_attributes` | Free-form fields returned with results. Not filterable with `attributes`. |
-| `happened_at` | A date, `YYYY-MM-DD`. When the item is about, as opposed to when you sent it. A timestamp is a `400`. |
-| `enrich` | Default `true`. Extract entities, relations and preferences into the graph. |
-| `custom_instructions` | Guidance for enrichment on this item. |
-| `is_markdown` | Chunk the text on its markdown structure instead of as flat prose. |
+| `database` | Required. The database to write to. |
+| `collection` | Optional. The collection to write to; the default collection when omitted. |
+| `context` | The list of items, at most 100. `items` and `contexts` are accepted as aliases; send `context`. |
+| `enrich` | Request-level default for every item's `enrich`. Default `true`. |
+| `upsert` | Request-level default for every item's `upsert`. Default `true`. |
+| `instructions` | Request-level default for every item's `instructions`. Default empty. |
+| `graph_payload` | Optional. A graph you built yourself, keyed by `context_id`. See [Bring your own graph](#12-bring-your-own-graph). |
+
+The three request-level defaults apply to any item that does not set the field itself, so one call can enrich some items and store others verbatim, or replace some items and append others.
+
+---
+
+## 3. Item fields
+
+Each item is exactly one of `text` or `conversation`.
+
+| Field | Notes |
+| --- | --- |
+| `context_id` | Your id for the item. Generated when omitted (from `title`, so two items with the same text, no title and no id collide). Must not contain commas. |
+| `title` | Optional readable name. Searchable with `titles` on [query](/essentials/v2/query). |
+| `text` | Plain text. Shape A. See [Text items](#4-text-items). |
+| `conversation` | A list of `{ role, content, name? }` turns; roles are `user`, `assistant` and `system`. Shape B. See [Conversation items](#5-conversation-items). |
+| `enrich` | Extract entities, relations and preferences from this item. Default: the request's `enrich`, else `true`. |
+| `upsert` | Replace an existing item with the same `context_id`. Default: the request's `upsert`, else `true`. |
+| `instructions` | Steer enrichment for this item. Default: the request's `instructions`. `custom_instructions` is accepted as an alias; send `instructions`. |
+| `happened_at` | The date the item is about, `YYYY-MM-DD` only. A timestamp is a `400`. HydraDB records when it received the item separately. |
+| `attributes` | Declared, filterable fields from the database's `database_metadata_schema`. See [Attributes](/essentials/v2/attributes). |
+| `custom_attributes` | Free-form fields. Not filterable. |
+| `context_category` | Optional label: `auto` (the default), `user_preference`, `business_knowledge` or `decision_trace`. You set it; nothing infers it. Validated strictly, so a typo is a `400`. See [Context categories](/essentials/v2/context-categories). |
+| `forceful_relations` | Relations you declare to other items: `{ "ids": ["chat-w1"], "properties": {} }`. `relations` is accepted as an alias for the field, and `context_ids` or `source_ids` for the `ids` key; send `forceful_relations` with `ids`. See [Declared relations](#11-declared-relations). |
+| `acl` | Principals allowed to retrieve the item, such as `user_email:a@x.com` or `domain:acme.com`. Omit for unrestricted, `[]` for nobody. A malformed principal is a `400`. See [Restricting an item](#10-restricting-an-item). |
+| `is_markdown` | Chunk `text` on its markdown structure instead of as flat prose. |
| `user_name` | The speaker for a text item. On a conversation, the per-turn `name` wins. |
-| `acl` | Principals allowed to retrieve the item. Omit for unrestricted, `[]` for nobody. A malformed principal is a `400`. |
-A key an item does not recognise is dropped without an error, so check spelling against this table.
+### Limits and refused fields
+
+- At most **100 items** per request, **1 MiB** of text per item, and **8 MiB** of text per request.
+- A validation error names the item it refers to as `context[N]`.
+- These split-era fields are refused with a `400` on a unified database: `type`, `documents` (file uploads), `app_knowledge`, `memories`, `evidence_kind`, `evidence_subject`, `expiry_time` and `retain_source`.
+- Any other key an item does not recognise is dropped without an error, so check spelling against the table above.
---
-## 3. Text items
+## 4. Text items
A text item is a document, a note, a policy, an agent log line: anything you already have as a string.
@@ -161,7 +199,7 @@ A text item is a document, a note, a policy, an agent log line: anything you alr
}
```
-- Set `title` so the item has a readable name in results and so `titles` filters can find it.
+- Set `title` so the item has a readable name and so `titles` filters can find it.
- Set `is_markdown: true` for markdown, so headings and lists shape the chunks.
- Set `user_name` when the text has an author the graph should attribute facts to.
@@ -171,7 +209,7 @@ A text item is a document, a note, a policy, an agent log line: anything you alr
---
-## 4. Conversation items
+## 5. Conversation items
```json
"conversation": [
@@ -192,24 +230,30 @@ This is the message list you already build for OpenAI or Anthropic, so you can u
---
-## 5. Enrichment
+## 6. Enrichment and instructions
With `enrich: true` (the default), HydraDB reads each item and extracts entities, the relations between them, and preferences, and writes them into the context graph. That is what lets a query about "Alex" reach a conversation where Alex never used the word the query used.
+The enriched output is stored **separately** from the item's own text. On [query](/essentials/v2/query) it comes back as `chunks[].enrichment`, next to the chunk's verbatim `content`; the two are never concatenated.
+
Turn it off with `enrich: false` when the item is already exactly what you want stored and you only need it searchable, for example a raw transcript you keep for reference.
-Use `custom_instructions` to steer extraction for one item:
+Use `instructions` to steer extraction. Set it on the request to apply it to every item, or on one item to override it there:
```json
{
- "text": "Q3 planning notes ...",
- "custom_instructions": "Extract owners and due dates for every action item. Ignore small talk."
+ "database": "acme",
+ "instructions": "Extract owners and due dates for every action item.",
+ "context": [
+ { "text": "Q3 planning notes ..." },
+ { "text": "Support chat transcript ...", "instructions": "Extract the customer's stated preferences. Ignore small talk." }
+ ]
}
```
---
-## 6. Context categories
+## 7. Context categories
Each item can say what kind of context it is:
@@ -219,11 +263,11 @@ Each item can say what kind of context it is:
| `business_knowledge` | Documentation, policy, product and domain facts |
| `decision_trace` | What an agent or a team decided, and why |
-Leave `context_category` out, or send `auto`, and HydraDB decides. See [Context categories](/essentials/v2/context-categories).
+The label is yours to set; HydraDB never infers or changes it. Leave `context_category` out, or send `auto`, and the item is stored and enriched as general context with no category on its chunks. A preference or a decision is sent as text (or a conversation) with the matching label. See [Context categories](/essentials/v2/context-categories).
---
-## 7. Attributes and custom attributes
+## 8. Attributes and custom attributes
```json
{
@@ -233,23 +277,23 @@ Leave `context_category` out, or send `auto`, and HydraDB decides. See [Context
}
```
-`attributes` are the fields you declared in the database's `database_metadata_schema`, and you can filter on them at query time. `custom_attributes` are free-form: they come back with results but cannot be filtered. See [Attributes](/essentials/v2/attributes).
+`attributes` are the fields you declared in the database's `database_metadata_schema`, and you can filter on them at query time with `attributes` on [`POST /query`](/essentials/v2/query#2-request). `custom_attributes` are free-form: they are stored with the item and cannot be filtered. Neither is returned on query chunks; read them with [`GET /context/inspect`](/api-reference/v2/endpoint/fetch-content). See [Attributes](/essentials/v2/attributes).
---
-## 8. Time
+## 9. Time
`happened_at` is when the item is about: the meeting date, the decision date, the day a preference was stated. HydraDB records when it received the item separately. Set `happened_at` whenever it differs from ingest time, so recency and temporal reasoning at query time use the right date.
---
-## 9. Restricting an item
+## 10. Restricting an item
```json
{
"context_id": "pricing-q3",
"text": "The Q3 pricing plan raises the Scale tier.",
- "acl": ["grace@acme.com", "group:slack:C0123"]
+ "acl": ["user_email:grace@acme.com", "group:slack:C0123"]
}
```
@@ -257,18 +301,63 @@ Omit `acl` and the item is unrestricted. Send `[]` and nobody can retrieve it. A
---
-## 10. IDs and replacement
+## 11. Declared relations
+
+Any item, text or conversation, can declare which other items it relates to:
+
+```json
+{
+ "context_id": "linear-PRO-1169-comment-4",
+ "text": "Agreed: ship the fix behind the existing flag.",
+ "forceful_relations": { "ids": ["linear-PRO-1169"], "properties": {} }
+}
+```
+
+`ids` are the `context_id`s of the related items. At query time, with `follow_forceful_relations` on (the default), a hit on one item pulls its declared relations into the response's `forceful_relations[]`, each with the `via` link that brought it in. See [Query](/essentials/v2/query#forceful_relations).
+
+---
+
+## 12. Bring your own graph
+
+Skip extraction for an item and supply its entities and relations yourself with `graph_payload`, a map keyed by `context_id`:
+
+```json
+{
+ "database": "acme",
+ "context": [
+ { "context_id": "billing-policy", "text": "Alice Carter owns the billing policy. ..." }
+ ],
+ "graph_payload": {
+ "billing-policy": {
+ "entities": {
+ "alice": { "name": "Alice Carter", "type": "PERSON" },
+ "billing": { "name": "Billing Policy", "type": "POLICY" }
+ },
+ "relations": [
+ { "source": "alice", "target": "billing", "predicate": "OWNS", "context": "Alice Carter owns the billing policy." }
+ ]
+ }
+ }
+}
+```
+
+Every key in `graph_payload` must match the `context_id` of an item in the same request; a key that matches nothing is a `400`, so a typo cannot silently drop a graph. A keyed item is still chunked and embedded, so it stays searchable. The entity and relation shapes, caps and replace semantics are on [Bring your own graph](/essentials/v2/bring-your-own-graph).
+
+---
+
+## 13. IDs and replacement
- `context_id` is yours. Reuse it to replace an item.
-- `upsert` defaults to `true`, and it **replaces**: re-ingesting a `context_id` deletes everything derived from the previous version (its chunks and its graph contribution) before writing the new one. It does not merge.
+- `upsert: true` (the default) **replaces**: re-ingesting a `context_id` deletes everything derived from the previous version (its chunks and its graph contribution) before writing the new one. It does not merge.
+- `upsert` is per item, with the request value as the default, so one call can replace some items and append others.
- When you omit `context_id`, the id is generated from `title`. Give repeated text either a `context_id` or a distinct `title`, or the second item replaces the first.
- `context_id` must not contain commas.
---
-## 11. Verify processing
+## 14. Verify processing
-Ingestion is asynchronous. Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the ids from the ingest response until each item reaches `completed` or `errored`.
+Ingestion is asynchronous. Poll [`GET /context/status`](/api-reference/v2/endpoint/source-status) with the `source_id`s from the ingest response until each item reaches `completed` or `errored`.
```python Python SDK
@@ -319,18 +408,47 @@ To be notified instead of polling, register a [webhook](/essentials/v2/webhooks)
---
-## 12. Other ways context arrives
+## 15. Before and now
+
+If you built against the split shapes (`documents`, `app_knowledge`, `memories`), this is what changed per field and why.
+
+| Before | Now | Why |
+| --- | --- | --- |
+| `source_id` / `id` | `context_id` | An item is a piece of context, not a source. |
+| `text` / `user_assistant_pairs` | `text` / `conversation` | One item is one context. `conversation` is `{ role, content, name? }`, the shape you already send to OpenAI or Anthropic. |
+| `infer` (default `false`) | `enrich` (default `true`) | Enrichment improves results, so it is on unless you turn it off. The enriched output is stored separately and comes back as `enrichment`. |
+| `custom_instructions` | `instructions` (item and request level) | Steers enrichment on every shape. |
+| `observation_date` | `happened_at` | The event time you state. HydraDB records the time it received the item separately. |
+| `metadata` / `additional_metadata` | `attributes` / `custom_attributes` | Declared-and-filterable versus free-form. |
+| `upsert` (request only) | `upsert` (item; the request value is the default) | Replace some items and append others in one call. |
+| `relations` (knowledge only) | `forceful_relations` (any item) | A conversation can declare its links too. |
+| `acl` (app sources only) | `acl` (any item) | Any item can be restricted. |
+| (none) | `context_category` | An optional label for what kind of context this is. |
+| `type` | removed | The database is unified or split, decided when it is created. |
+| `documents` (file uploads), `app_knowledge`, `evidence_kind`, `evidence_subject`, `expiry_time`, `retain_source` | removed | Extract text from files and send it as an item. Structured app sources come through [connectors](/essentials/v2/connectors). |
+
+Split databases keep the old fields unchanged; see [Split databases and legacy fields](/essentials/v2/split-databases).
+
+---
+
+## 16. Other ways context arrives
[Connectors](/essentials/v2/connectors) sync Slack, Notion, Google Drive, GitHub and other tools into a database on a schedule. Synced context lands in the same database as your items and is queried together with them.
---
-## 13. Common mistakes
+## 17. Common mistakes
An item carries exactly one of `text` or `conversation`. Sending both, or neither, is a `400`. Split them into two items.
+
+A unified database has one corpus and takes text only. `type`, `documents`, `app_knowledge`, `memories`, `evidence_kind`, `evidence_subject`, `expiry_time` and `retain_source` are refused with a `400`. Send `context` items instead.
+
+
+`source_id`, `infer`, `custom_instructions`, `observation_date`, `metadata` and `additional_metadata` are split-era names. Some are accepted as aliases, the rest are dropped. Use the names in [Item fields](#3-item-fields).
+
`context_category` is validated strictly. `"business-knowledge"` is a `400`, not a silent fallback to `auto`. Use `user_preference`, `business_knowledge`, `decision_trace` or `auto`.
@@ -338,7 +456,10 @@ An item carries exactly one of `text` or `conversation`. Sending both, or neithe
Only `user`, `assistant` and `system` are accepted. Map roles like `tool` or `human` before sending.
-`custom_attributes` are returned with results but cannot be filtered. Filtering on one is a `400`. Declare the field in `database_metadata_schema` and send it in `attributes` instead.
+`custom_attributes` are stored but cannot be filtered. Declare the field in `database_metadata_schema` and send it in `attributes` instead.
+
+
+Every key must equal the `context_id` of an item in the same request. Anything else is a `400`.
A `202` means queued. Poll status until `graph_creation` or `completed` before expecting the item in results.
@@ -353,3 +474,4 @@ A `202` means queued. Poll status until `graph_creation` or `completed` before e
- [Query](/essentials/v2/query)
- [Attributes](/essentials/v2/attributes)
- [Ingest context API reference](/api-reference/v2/endpoint/ingest-context)
+- [Split databases and legacy fields](/essentials/v2/split-databases)
diff --git a/essentials/v2/query.mdx b/essentials/v2/query.mdx
index 288f9d40..ac570b38 100644
--- a/essentials/v2/query.mdx
+++ b/essentials/v2/query.mdx
@@ -1,194 +1,468 @@
---
title: "Query"
-description: "How HydraDB retrieves the right context for each query - Knowledge, Memories, and the context graph, all behind a single /query endpoint."
+description: "One call to POST /query returns ranked chunks, graph paths, forceful relations and a prompt-ready string. Every request and response field on a unified database."
---
-Query turns stored context into the *right* context for a specific query. One endpoint - `POST /query` - queries [Knowledge](/essentials/v2/knowledge) (documents and app sources), [Memories](/essentials/v2/memories) (user preferences, conversation history, inferred content), or both. Three signals drive relevance: dense-vector similarity, BM25 keyword matching, and [context-graph](/essentials/v2/context-graphs) traversal. The parameters below control all of it.
-For the full request and response schema, see [Query - API Reference](/api-reference/v2/endpoint/query-overview).
+import LegacyLine from "/snippets/legacy-line.mdx";
+
+Query turns stored context into the *right* context for one question. One endpoint, [`POST /query`](/api-reference/v2/endpoint/query), searches everything in the collections you name: your text and conversation items, connector content, and the [context graph](/essentials/v2/context-graphs) built from all of it. Three signals drive relevance: dense-vector similarity, BM25 keyword matching and graph traversal.
+
+On a unified database there is no corpus selector. You never send `type`, and the response is one shape with four keys.
+
+
---
-## 1. Use-case recipes
+## 1. One call
-Pick the row that matches your goal and use the parameters as a starting point:
+Ask one collection a question:
-| I want to… | `type` | `query_by` | `mode` | Notes |
-| --- | --- | --- | --- | --- |
-| Document Q&A / RAG | `"knowledge"` | `"hybrid"` | `"thinking"` | Set `graph_context: true` for a richer context graph as part of the API response |
-| Exact keyword match | `"knowledge"` | `"text"` | - | Optionally set `operator: "and"` or `"or"` to control BM25 term matching |
-| Personalized response | `"memory"` | `"hybrid"` | `"thinking"` | |
-| Personalized + grounded | `"all"` | `"hybrid"` | `"thinking"` | One call merges both stores |
-| Query over apps | `"knowledge"` | `"hybrid"` | `"thinking"` | Set `query_apps: true` to enable the app-aware retrieval lane (IDs, actors, thread & parent traversal) while still querying the full selected knowledge scope |
+
+```bash cURL
+curl -X POST 'https://api.hydradb.com/query' \
+ -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
+ -H "API-Version: 2" \
+ -H "Content-Type: application/json" \
+ -d '{
+ "database": "acme_corp",
+ "collection": "support",
+ "query": "How are refunds processed, and how should I answer this user?",
+ "mode": "thinking",
+ "max_results": 8
+ }'
+```
+```python Python SDK
+result = client.query(
+ database="acme_corp",
+ collection="support",
+ query="How are refunds processed, and how should I answer this user?",
+ mode="thinking",
+ max_results=8,
+)
----
+print(result.data.llm_prompt)
+```
+```typescript TypeScript SDK
+const result = await client.query({
+ database: "acme_corp",
+ collection: "support",
+ query: "How are refunds processed, and how should I answer this user?",
+ mode: "thinking",
+ maxResults: 8,
+});
-## 2. Parameter reference
+console.log(result.data.llmPrompt);
+```
+
-[Follow this for when to use `database` and `collection`](./databases-and-collections#2-when-to-use-each)
+To search a person's collection and a shared one together, send `collections` instead, for example `{ "user_alex": 2, "company": 1 }` (see [Scope](#scope)).
-The `database` field was formerly `tenant_id` and `collection` was formerly `sub_tenant_id`; the old names remain accepted as deprecated aliases.
+The response `data` is exactly these four keys, inside the usual envelope:
+
+```json
+{
+ "success": true,
+ "data": {
+ "chunks": [
+ {
+ "chunk_id": "ck_policy_3",
+ "context_id": "refund-policy",
+ "score": 0.91,
+ "content": "Refunds are processed within 30 days of purchase by the Finance Department.",
+ "enrichment": "Refund window is 30 days; Finance owns refund processing.",
+ "enrichment_kind": "business_knowledge",
+ "temporal": [
+ {
+ "content": "Refund policy effective_from June 2026. Start: 2026-06-01",
+ "start_date": "2026-06-01",
+ "end_date": null
+ }
+ ]
+ },
+ {
+ "chunk_id": "ck_chat_1",
+ "context_id": "chat-2026-07-29",
+ "score": 0.84,
+ "content": "user: Keep refund answers short please\nassistant: Got it.",
+ "enrichment": "User prefers short answers about refunds.",
+ "enrichment_kind": "user_preference"
+ }
+ ],
+ "graph": [
+ {
+ "origin": "query_path",
+ "triplets": [
+ {
+ "source": {
+ "entity_id": "ent_refunds",
+ "name": "Refund Processing"
+ },
+ "relation": {
+ "predicate": "managed by",
+ "context": "Refund processing is managed by the Finance Department.",
+ "relationship_id": "rel_managed_by",
+ "chunk_id": "ck_policy_3"
+ },
+ "target": {
+ "entity_id": "ent_finance",
+ "name": "Finance Department"
+ }
+ }
+ ],
+ "path_summary": "Refund processing is managed by the Finance Department."
+ },
+ {
+ "origin": "chunk_relation",
+ "triplets": [
+ {
+ "source": {
+ "entity_id": "ent_user",
+ "name": "User"
+ },
+ "relation": {
+ "predicate": "prefers",
+ "context": "The user prefers short answers about refunds.",
+ "relationship_id": "rel_prefers",
+ "chunk_id": "ck_chat_1"
+ },
+ "target": {
+ "entity_id": "ent_short",
+ "name": "short answers"
+ }
+ }
+ ],
+ "path_summary": "The user prefers short answers about refunds."
+ }
+ ],
+ "forceful_relations": [
+ {
+ "via": {
+ "from": "refund-policy",
+ "to": "refund-faq"
+ },
+ "chunk": {
+ "chunk_id": "ck_faq_1",
+ "context_id": "refund-faq",
+ "score": 0,
+ "content": "FAQ: refunds to a card take 5 to 7 business days to appear."
+ }
+ }
+ ],
+ "llm_prompt": "# Query results\n\n**Query:** who owns refund processing?\n**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation\nCite a result by its number in brackets, e.g. [1].\n\n## Results\n\n### 1. Refund policy\n- **Relevance:** 0.91 · **Collection:** support · **Type:** file · **Category:** business_knowledge\n- **Id:** refund-policy · **Last updated:** 2026-07-02\n\nRefunds are processed within 30 days of purchase by the Finance Department.\n\n**Enrichment:** Refund window is 30 days; Finance owns refund processing.\n\n---\n\n### 2. Support chat with Priya\n- **Relevance:** 0.84 · **Collection:** support · **Type:** message · **Category:** user_preference\n- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29\n\nuser: Keep refund answers short please\nassistant: Got it.\n\n**Enrichment:** User prefers short answers about refunds.\n\n## Forceful relations\n\nLinked to a result by the author at ingest time (forceful_relations), not by relevance to this query.\n\n### R1. Refund FAQ\n- **Linked from:** refund-policy · **Collection:** support\n- **Id:** refund-faq\n\nFAQ: refunds to a card take 5 to 7 business days to appear.\n\n## Related facts\n\n- [P1] **Refund Processing** -managed by→ **Finance Department** (query path, relevance 0.81) [1]\n Refund processing is managed by the Finance Department.\n- [P2] **User** -prefers→ **short answers** (chunk relation, relevance 0.74) [2]\n The user prefers short answers about refunds.\n\n## Temporal facts\n\n- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: \"from June\") [1]\n\n## Sources\n\n1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02\n2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29\n3. **Refund FAQ** (id: refund-faq)"
+ },
+ "error": null,
+ "meta": {
+ "request_id": "9d13aef4-02f4-4e73-8c62-4c2601d04f9d",
+ "api_version": "2.0.1",
+ "latency_ms": 412.7,
+ "database": "acme_corp",
+ "collection": "support"
+ }
+}
+```
+
+Most integrations only need `llm_prompt`: put it in the model call and you are done. Section 3 describes every field.
+
+---
+
+## 2. Request
+
+[Follow this for when to use `database` and `collection`](./databases-and-collections#2-when-to-use-each). `database` was formerly `tenant_id` and `collection` was formerly `sub_tenant_id`; the old names remain accepted as deprecated aliases.
### Scope
| Parameter | Type / values | Purpose |
| --- | --- | --- |
-| `collections` | `string[]` or `{ [collection]: positive number (max one decimal place) }` | Preferred query-time scope selector. Use a single-item list for one user/workspace, a longer list to fan out with equal normalized weights, or an object to provide relative ranking weights with at most one decimal place. Maximum 100 collections. |
-| `collection` | string | Single-scope query selector; also used at ingest. Send one collection ID to scope the query to it. |
+| `database` | string | Required. The database to search. |
+| `collection` | string | Single-scope selector. Send one collection to search only it; the default collection when neither this nor `collections` is sent. |
+| `collections` | `string[]` or `{ [collection]: positive number (max one decimal place) }` | Preferred selector. A list fans out with equal normalized weights; an object applies relative ranking weights. Maximum 100 collections. `max_results` caps the merged result. |
+| `ids` | `string[]` | Restrict retrieval to these `context_id`s. |
+| `titles` | `string[]` | Restrict retrieval to items with one of these exact titles (case-insensitive, ORed). Intersected with `ids` when both are sent. |
+| `acl` | `string[]` | Query on behalf of an identity: results are restricted to items that identity may retrieve. Omitted, empty or `["*"]` disables filtering. See [Access control](/essentials/v2/access-control). |
+| `attributes` | object | Filter on declared attributes with operators: `$eq`, `$ne`, `$gt`, `$gte`, `$lt`, `$lte`, `$in`, `$nin`, `$and`, `$or`, `$not`, `$exists`. Applies to chunks, forceful relations and graph paths alike. See [Attributes](/essentials/v2/attributes). |
-### Graph
+```json
+{
+ "database": "acme",
+ "collection": "company",
+ "query": "What is the refund window for enterprise customers?",
+ "attributes": {
+ "$and": [
+ { "department": { "$eq": "support" } },
+ { "region": { "$in": ["us", "eu"] } }
+ ]
+ }
+}
+```
+
+### Retrieval
| Parameter | Type / values | Purpose |
| --- | --- | --- |
-| `graph_context` | boolean | When `true` (default), includes the entity/relation graph slice in the response. Pair with `mode: "thinking"` for richer multi-hop traversals. See [Context Graphs](/essentials/v2/context-graphs). Set to `false` to drop it when you only need ranked chunks. Default: `true`. |
-| `query_forceful_relations` | boolean | Whether to fetch author-declared related sources (see [`relations` on ingest](/api-reference/v2/endpoint/ingest-context)) into `additional_context`. **Only takes effect in** `mode: "thinking"`**.** Default: `true`. |
+| `query` | string | Required. The question or search terms. |
+| `query_by` | `"hybrid"` or `"text"` | `hybrid` (default) blends semantic and BM25; `text` is BM25 only, paired with `operator`. |
+| `operator` | `"or"`, `"and"`, `"phrase"` | BM25 term matching for `query_by: "text"`. Default `"or"`. |
+| `mode` | `"auto"`, `"fast"`, `"thinking"` | `auto` (default) scores the query and routes to `fast` or `thinking`. `thinking` expands the query, reranks and follows declared relations; `fast` is one pass. |
+| `alpha` | float `0.0` to `1.0`, or `"auto"` | Semantic versus keyword blend for `hybrid`. Default `0.8`. |
+| `max_results` | integer | Maximum chunks to return. Default `10`, maximum `50`. |
+| `recency_bias` | float `0.0` to `1.0` | Boost for newer content. Send `0` to disable recency entirely. |
+| `query_apps` | boolean | Adds the app-aware lane (exact IDs, actors, thread and parent traversal) for connector content, on top of normal retrieval. Set `false` to skip it. |
-### Shaping results
+### Graph and relations
| Parameter | Type / values | Purpose |
| --- | --- | --- |
-| `max_results` | integer | Maximum chunks to return. Default `10`; maximum `50`. Start with `10`, reduce for tight context windows, increase only when you rerank or summarize downstream. |
-| `recency_bias` | float `0.0`–`1.0` | Boost for newer content. Default: `0.0` (no boost). |
-| `query_apps` | boolean | Set `true` to enable the app-aware retrieval lane alongside normal retrieval (reconstructed threads, parent/child traversal, exact ID/actor lookups). This improves app-source query but does not restrict the query to only app sources; HydraDB still queries the full selected knowledge scope. See [App Sources](/essentials/v2/app-sources). Default: `false`. |
-| `additional_context` | string | Request-time hint to guide retrieval (e.g., "user is on the billing page"). This is different from the response `additional_context`, which carries forceful-relation results. Default: `null`. |
-| `metadata_filters` | object | Deterministic narrowing before ranking. See [Metadata](/essentials/v2/attributes). Default: `null`. |
+| `graph_context` | boolean | Default `true`. Include graph paths in `graph[]`. Set `false` for chunks only; `graph` is then `[]`. |
+| `follow_forceful_relations` | boolean | Default `true`. Pull in the items each hit declared with `forceful_relations` at ingest, into `forceful_relations[]`. Set `false` for `forceful_relations: []`. `query_forceful_relations` is the deprecated alias. |
-### Access control
+### Time
| Parameter | Type / values | Purpose |
| --- | --- | --- |
-| `acl` | `string[]` | Query on behalf of an identity: results are restricted to documents that identity may retrieve. Send the caller's email (`["grace@acme.com"]`); `__public__` and the caller's `domain:` principal are added automatically. Omitted, empty, or `["*"]` disables filtering entirely, which is the default. See [Access Control](/essentials/v2/access-control). Default: `null`. |
+| `temporal_reasoning` | boolean | Default `true`. Resolve time-based questions (current, as of, ranges, upcoming). Matched facts come back in `chunks[].temporal`. Never changes which chunks are returned. |
+| `temporal_now` | ISO 8601 string | The time to treat as now. Set it when replaying past conversations. |
+| `temporal_intent` | object | Override the temporal intent HydraDB would infer from the query. |
+
+### Do not send
+
+- **`type`.** Absent, `"all"` and `"unified"` are accepted and mean the one corpus; `"knowledge"` and `"memory"` are a `400`. A client branches on the database's layout from `GET /databases` (`details[].type`), never on a request flag.
+- **`metadata_filters`.** Still accepted, deprecated. Use `attributes`.
---
-## 3. Tuning heuristics
+## 3. Response
-Most of the time the defaults are right. When they aren't, here's where to start:
+`data` is exactly `chunks`, `graph`, `forceful_relations` and `llm_prompt`. Nothing else: no `sources`, no `graph_context`, no `additional_context`, no `temporal_facts`.
-- `mode` - If omitted, defaults to `"auto"`: it scores the query before retrieval and routes to `"fast"` or `"thinking"` (defaulting to `"thinking"` when the signal is inconclusive), and overrides `graph_context` to match whichever it picks. Pick `"fast"` or `"thinking"` explicitly instead when you know your traffic shape and want a deterministic pipeline.
-- `alpha` - Start at `0.8`. Lower toward `0.3–0.5` when the query contains literal tokens (error codes, SKUs, product names). Raise toward `0.9` for conceptual questions. Use `"auto"` when query shape varies across calls.
-- `max_results` - Start at `10`. Drop to `5` for tight context windows; raise to `20` if you rerank downstream.
-- `additional_context` - Use it when the query alone is ambiguous. Keep it short and factual.
-- `graph_context` - Set to `true` when answers benefit from entity relationships (multi-hop questions, "how does X relate to Y"). Pair with `mode: "thinking"` - in `"fast"` mode the graph slice is shallow.
-- `query_apps` - Set to `true` when querying ingested app data (Slack, Gmail, Confluence, Jira, Salesforce) to activate exact ID and actor lookups, same-thread expansion, and parent/child hierarchy traversal. This adds better app-aware retrieval on top of the normal knowledge query; it does not filter out non-app knowledge. Pair with `mode: "thinking"`.
+`meta` on the envelope carries `request_id`, `api_version`, `latency_ms`, `database` and `collection`, plus a `deprecation` list when the request used a deprecated name. It has no `tenant_id`, `sub_tenant_id` or `source_type`. `collection` is present when the query searched one collection (named, or the default); a `collections` fan-out omits it.
----
+### `chunks[]`
-## 4. Minimal working example
+The matched pieces of your items, ranked. Preserve the order.
-The canonical personalized-answer flow takes a single call: `POST /query` with `type: "all"` returns merged knowledge and per-user memory in one ranked result set.
+| Field | Type | Meaning |
+| --- | --- | --- |
+| `chunk_id` | string | The chunk's id. Referenced from `graph[].triplets[].relation.chunk_id`. |
+| `context_id` | string | The item this chunk came from. Pass it to `GET /context/inspect`. |
+| `score` | number | Relevance. Always present. |
+| `content` | string | The chunk's own text, verbatim. Enrichment is not concatenated into it. |
+| `enrichment` | string | What enrichment extracted from this chunk: the extracted statement (a preference, a fact). Omitted when enrichment extracted nothing. |
+| `enrichment_kind` | string | The item's declared `context_category`: `user_preference`, `business_knowledge` or `decision_trace`. Omitted when none was declared (`auto`); present even when `enrichment` is omitted. |
+| `temporal` | array | Present only when the query engaged temporal reasoning. Each entry is `{ content, start_date, end_date }`; dates may be `null`. |
-### Setup
+
+**Chunks carry nothing about their source.** No title, url, collection, timestamps or attributes. `llm_prompt` prints the title, collection, type, last-updated date and url for the model; to show any of that yourself, call [`GET /context/inspect?database=acme&id=`](/api-reference/v2/endpoint/fetch-content) with the chunk's `context_id`.
+
-```bash
-# Set your key as an environment variable - used in every request below
-export HYDRA_DB_API_KEY="your_api_key"
-# All requests: -H "Authorization: Bearer $HYDRA_DB_API_KEY" -H "API-Version: 2"
-```
+### `graph[]`
-```typescript
-import { HydraDBClient } from "@hydradb/sdk";
+One flat array of paths through the [context graph](/essentials/v2/context-graphs): paths grown from the query first, then paths expanded from the returned chunks, deduplicated. A path both lanes found is reported once, as a `query_path`. `[]` when `graph_context` is `false` or nothing connects.
-const client = new HydraDBClient({
- token: process.env.HYDRA_DB_API_KEY,
-});
+| Field | Type | Meaning |
+| --- | --- | --- |
+| `origin` | string | Which lane found the path. `"query_path"`: grown from the entities in the query. `"chunk_relation"`: the neighbourhood of a returned chunk. |
+| `triplets[]` | array | The chain of `source`, `relation`, `target` steps that make up the path. |
+| `triplets[].source` | object | `{ entity_id, name }`. |
+| `triplets[].target` | object | `{ entity_id, name }`. |
+| `triplets[].relation.predicate` | string | The relation, for example `subscribed to`. |
+| `triplets[].relation.context` | string | The sentence the relation was extracted from. |
+| `triplets[].relation.temporal_details` | string | When the relation holds, for example `since June`. Absent when unknown. |
+| `triplets[].relation.relationship_id` | string | The relation's id. |
+| `triplets[].relation.chunk_id` | string | The chunk this relation was extracted from. Use it to attach the hop to a chunk, below. |
+| `path_summary` | string | One sentence summarizing the whole path. Never empty: when the server wrote no summary for a path, it narrates the hops, such as `Priya owns refund processing.` |
+
+### Attaching graph paths to chunks
+
+Every hop carries `relation.chunk_id`, the chunk its edge was extracted from. That is all you need to show a chunk's relations under the chunk; it replaces the split response's `chunk_id_to_group_ids`.
+
+- **Group hops by `relation.chunk_id`** and match it against `chunks[].chunk_id` (and `forceful_relations[].chunk.chunk_id`).
+- **A `chunk_relation` path hangs under a chunk in the answer.** It is only returned when one of its hops came from a returned chunk (or a `forceful_relations` chunk); hang it under that chunk.
+- **Show `query_path` paths as their own group.** A query-path hop whose `chunk_id` matches a returned chunk may also be shown under that chunk.
+- **Resolve a hop to its context** with the `chunk_id` to `context_id` map built from `chunks[]` and `forceful_relations[].chunk`. Do not parse the chunk id string.
+
+In the [example response](#1-one-call), `P1` is a query path whose hop came from `ck_policy_3`, so it can also sit under the `refund-policy` chunk. `P2` is the neighbourhood of `ck_chat_1` and hangs under the `chat-2026-07-29` chunk.
+
+
+```python Python SDK
+data = result.data
+context_of = {c.chunk_id: c.context_id for c in data.chunks}
+context_of.update({r.chunk.chunk_id: r.chunk.context_id for r in data.forceful_relations})
+
+hops_under = {} # chunk_id -> hops extracted from that chunk
+query_paths = []
+for path in data.graph:
+ if path.origin == "query_path":
+ query_paths.append(path)
+ for hop in path.triplets:
+ if hop.relation.chunk_id in context_of:
+ hops_under.setdefault(hop.relation.chunk_id, []).append(hop)
```
+```typescript TypeScript SDK
+const data = result.data;
+const contextOf = new Map();
+for (const c of data.chunks) contextOf.set(c.chunkId, c.contextId);
+for (const r of data.forcefulRelations) contextOf.set(r.chunk.chunkId, r.chunk.contextId);
+
+const hopsUnder = new Map(); // chunkId -> hops extracted from that chunk
+const queryPaths = [];
+for (const path of data.graph) {
+ if (path.origin === "query_path") queryPaths.push(path);
+ for (const hop of path.triplets) {
+ const id = hop.relation.chunkId;
+ if (!contextOf.has(id)) continue;
+ if (!hopsUnder.has(id)) hopsUnder.set(id, []);
+ hopsUnder.get(id).push(hop);
+ }
+}
+```
+
-```python
-import os
-from hydra_db import HydraDB
+### `forceful_relations[]`
-client = HydraDB(token=os.environ["HYDRA_DB_API_KEY"])
-```
+Chunks pulled in because the caller declared `forceful_relations` at ingest: the same name as the ingest field and the `follow_forceful_relations` switch. They are linked to a result by the author, not ranked for this query. `[]` when none were declared or `follow_forceful_relations` is `false`.
-### One call - Knowledge and Memories together
+| Field | Type | Meaning |
+| --- | --- | --- |
+| `via.from` | string | The `context_id` whose declared relation pulled this chunk in. May be `""`. |
+| `via.to` | string | The returned chunk's own `context_id`. |
+| `chunk` | object | The same shape as an entry of `chunks[]`. |
-```bash
-curl -X POST 'https://api.hydradb.com/query' \
- -H "Authorization: Bearer $HYDRA_DB_API_KEY" \
- -H "API-Version: 2" \
- -H "Content-Type: application/json" \
- -d '{
- "database": "acme_corp",
- "collection": "user_john_123",
- "query": "How do I reset my password?",
- "type": "all",
- "query_by": "hybrid",
- "mode": "thinking",
- "max_results": 8,
- "graph_context": true
- }'
+```json
+{
+ "via": { "from": "refund-policy", "to": "refund-faq" },
+ "chunk": {
+ "chunk_id": "ck_faq_1",
+ "context_id": "refund-faq",
+ "score": 0,
+ "content": "FAQ: refunds to a card take 5 to 7 business days to appear."
+ }
+}
```
-```typescript
-const result = await client.query({
- database: "acme_corp",
- collection: "user_john_123",
- query: "How do I reset my password?",
- type: "all",
- queryBy: "hybrid",
- mode: "thinking",
- maxResults: 8,
- graphContext: true,
-});
-```
+### `llm_prompt`
-```python
-result = client.query(
- database="acme_corp",
- collection="user_john_123",
- query="How do I reset my password?",
- type="all",
- query_by="hybrid",
- mode="thinking",
- max_results=8,
- graph_context=True,
-)
+A server-built markdown document, ready to inject into a model call. It renders the chunks, the forceful relations, the graph paths and the dated facts, with the source details the chunk objects leave out (title, collection, type, last-updated date, url). A section with nothing in it is left out. When the query returns no chunks, forceful relations or graph paths, `llm_prompt` is `""`.
+
+The sections, in order:
+
+| Section | Contents |
+| --- | --- |
+| `# Query results` | `**Query:**` (the query), a `**Found:**` line counting what follows, and the line telling the model to cite a result by its number. |
+| `## Results` | One block per entry of `chunks[]`, in ranked order, separated by `---`: a `### 1. title` heading; a line with `**Relevance:**` (the `score`), `**Collection:**`, `**Type:**` and `**Category:**` (the `enrichment_kind`); a line with `**Id:**` (the `context_id`) and `**Last updated:**`; the chunk's `content`; then `**Enrichment:**` with the `enrichment`. |
+| `## Forceful relations` | A guide line, then one `### R1. title` block per entry of `forceful_relations[]`, laid out like a result, with `**Linked from:**` (the `via.from` context, when it is not `""`) in place of `**Relevance:**`. |
+| `## Related facts` | One line per path in `graph[]`, such as `- [P1] **A** -pred→ **B** (query path, relevance 0.81) [1]`: the path's label, its chain of hops, `query path` or `chunk relation` for its `origin` with the path's relevance after reranking (printed only here: `graph[]` carries no score), and the results its hops were extracted from. A path with no reranked score, such as a graph summary a `thinking` query builds, prints no relevance. The `path_summary` is indented on the line under it, unless it only narrates the hops the chain already shows. |
+| `## Temporal facts` | One line per dated fact the query engaged (the facts behind `chunks[].temporal`): subject, relation and object, then the resolved window, precision and status, with the evidence phrase set apart after a `;`, citing its result. |
+| `## Sources` | Each context once, in order of first appearance: title, type, id, url and last-updated date. Only web (`http` or `https`) links are printed; a storage location such as `s3://...` never is. |
+
+`**Type:**` is what the item is: the connector's word for it (a Slack `message`, a Jira `ticket`) when a connector set one, otherwise its source type, such as `file`. It is never `memory` or `knowledge`: a unified database has no such split. A field with no value is left out of its line.
+
+| Label | Refers to |
+| --- | --- |
+| `[1]`, `[2]`, ... | Result `### 1.`, `### 2.`, ...: that entry of `chunks[]`. Its `**Id:**` is the `context_id` to pass to `GET /context/inspect`. |
+| `[R1]`, `[R2]`, ... | Forceful relation `### R1.`, `### R2.`, ...: that entry of `forceful_relations[]`. |
+| `[P1]`, `[P2]`, ... | A related fact: path 1, 2, ... of `graph[]`, the same numbering the dashboard and CLI show next to each hop. An agent can cite a fact by its label. |
+
+A related fact or a temporal fact ends with the labels of the results it was extracted from (`[1]`, or `[R1]` for a forceful relation); a fact extracted from a chunk that is not in the response carries none. The numbers in `## Sources` count contexts, not results, and are not citation labels.
+
+A related fact's chain reads left to right:
+
+- `**A** -pred→ **B**` is one hop. The next hop continues the chain only from the entity the chain ended on: `**A** -p→ **B** -q→ **C**`.
+- A hop walked against its edge reads `←pred-`: in `**A** -p→ **B** ←q- **C**`, the edge is `C -q→ B`.
+- A hop that shares no entity with the chain starts a new segment after `; `.
+- A hop's `temporal_details`, when set, follows it in parentheses.
+
+The `llm_prompt` from the [example response](#1-one-call), unescaped:
+
+```markdown
+# Query results
+
+**Query:** who owns refund processing?
+**Found:** 2 results across 2 sources · 2 related facts · 1 temporal fact · 1 forceful relation
+Cite a result by its number in brackets, e.g. [1].
+
+## Results
+
+### 1. Refund policy
+- **Relevance:** 0.91 · **Collection:** support · **Type:** file · **Category:** business_knowledge
+- **Id:** refund-policy · **Last updated:** 2026-07-02
+
+Refunds are processed within 30 days of purchase by the Finance Department.
+
+**Enrichment:** Refund window is 30 days; Finance owns refund processing.
+
+---
+
+### 2. Support chat with Priya
+- **Relevance:** 0.84 · **Collection:** support · **Type:** message · **Category:** user_preference
+- **Id:** chat-2026-07-29 · **Last updated:** 2026-07-29
+
+user: Keep refund answers short please
+assistant: Got it.
+
+**Enrichment:** User prefers short answers about refunds.
+
+## Forceful relations
+
+Linked to a result by the author at ingest time (forceful_relations), not by relevance to this query.
+
+### R1. Refund FAQ
+- **Linked from:** refund-policy · **Collection:** support
+- **Id:** refund-faq
+
+FAQ: refunds to a card take 5 to 7 business days to appear.
+
+## Related facts
+
+- [P1] **Refund Processing** -managed by→ **Finance Department** (query path, relevance 0.81) [1]
+ Refund processing is managed by the Finance Department.
+- [P2] **User** -prefers→ **short answers** (chunk relation, relevance 0.74) [2]
+ The user prefers short answers about refunds.
+
+## Temporal facts
+
+- **Refund policy** *effective_from* → **June 2026** (from 2026-06-01, precision: month, status: ongoing; evidence: "from June") [1]
+
+## Sources
+
+1. **Refund policy** (file, id: refund-policy) · https://docs.acme.com/refunds · updated 2026-07-02
+2. **Support chat with Priya** (message, id: chat-2026-07-29) · updated 2026-07-29
+3. **Refund FAQ** (id: refund-faq)
```
-### Merge into the LLM prompt
+Inject it directly. It is the whole context block; you do not build a string from the other three keys:
-The response is a single `RetrievalResult` containing `chunks[]`, `sources[]`, and - when applicable - `graph_context` and `additional_context`. Chunks from Knowledge and Memories are already interleaved and ranked by relevance, so no manual merging is required. Pass the result through the helper in [How to Use API Results](/essentials/v2/api-results) to turn it into a context string for your prompt:
+```python
+messages = [{"role": "system", "content": result.data.llm_prompt},
+ {"role": "user", "content": question}]
+```
```typescript
-const context = buildContextString(result);
-
-const completion = await openai.chat.completions.create({
- model: "gpt-4o",
- messages: [
- {
- role: "system",
- content: "Answer using only the provided context. Match the user's preferred style.",
- },
- {
- role: "user",
- content: `${context}\n\nQuestion: How do I reset my password?`,
- },
- ],
-});
+const messages = [{ role: "system", content: result.data.llmPrompt },
+ { role: "user", content: question }];
```
-If you need to query several users, teams, or workspaces at once, pass `collections`. A list gives every scope equal normalized weight; an object applies relative ranking weights with at most one decimal place before the final merged ranking. When `max_results` is set, it caps the final merged response across all selected collections:
+Surface it to your agent verbatim, and let the model cite the labels. The SDK `build_string` / `buildString` helpers return `llm_prompt` verbatim on a unified database. When you need structured output instead, read `chunks[].content`, `chunks[].enrichment`, `chunks[].enrichment_kind`, `graph[].path_summary` and `forceful_relations[]`. See [How to Use API Results](/essentials/v2/api-results).
-```json
-{
- "database": "acme_corp",
- "collections": {
- "workspace_42": 2,
- "user_john_123": 1
- },
- "query": "What context matters for this renewal?",
- "type": "all"
-}
-```
+---
+
+## 4. Tuning heuristics
-If you need to keep Knowledge and Memories formatted differently in the prompt, call `/query` twice in parallel with `type: "knowledge"` and `type: "memory"`, then merge client-side.
+Most of the time the defaults are right. When they are not, here is where to start:
+
+- `collections`: put the person's collection above the shared one (`{ "user_alex": 2, "company": 1 }`) for personalized answers. The weights rank, they do not exclude.
+- `mode`: `auto` routes each query; pick `fast` or `thinking` explicitly when you know your traffic shape and want a deterministic pipeline. `auto` also sets `graph_context` to match the pipeline it picks.
+- `alpha`: start at `0.8`. Lower toward `0.3` to `0.5` when the query contains literal tokens (error codes, SKUs, product names). Raise toward `0.9` for conceptual questions. Use `"auto"` when query shape varies across calls.
+- `max_results`: start at `10`. Drop to `5` for tight context windows; raise to `20` if you rerank downstream.
+- `graph_context`: keep it on when answers benefit from entity relationships (multi-hop questions, "how does X relate to Y"). Pair with `mode: "thinking"`; in `fast` mode the graph slice is shallow.
+- `query_apps`: keep it on when querying connector content (Slack, Gmail, Confluence, Jira, Salesforce) so exact IDs, actors and threads resolve.
+- `attributes`: use them when the query has a scope that must not be violated. They are hard constraints, not hints.
### Production checklist
-- **Default to** `type: "all"` for personalized answers - one call instead of two, scoped with `collections`.
-- **Set per-call timeouts.** Generous for `thinking` (3–5 s), tight for `fast` (≤500 ms). For `mode: "auto"`, size the timeout for the `thinking` case - it can resolve to either pipeline and defaults toward `thinking` when the routing signal is inconclusive.
-- **Enable `query_apps: true`** when querying app sources to activate app-aware retrieval and thread/relation traversal.
-- **Pass** `additional_context` with known session state (page, feature, role). It sharpens retrieval without extra calls.
+- **Set per-call timeouts.** Generous for `thinking` (3 to 5 s), tight for `fast` (500 ms or less). For `auto`, size the timeout for the `thinking` case.
+- **Detect the layout once per database** (`GET /databases`, `details[].type`) and cache it. Unified: this page. Split: the old request and response, byte for byte.
+- **Parse by shape.** A response with `llm_prompt` and a `graph` array is the unified shape; one with `graph_context` or `chunk_content` is the split shape. Stored logs and split databases keep producing the old one.
+- **Cache by request.** If the same query repeats inside a session, cache `data` keyed by `(database, collections, query, query_by, mode)`. Sort list values and object keys before building the key.
---
@@ -196,33 +470,27 @@ If you need to keep Knowledge and Memories formatted differently in the prompt,
| Symptom | Cause | Fix |
| --- | --- | --- |
-| Empty `query_paths` / `chunk_relations` | `graph_context` not set, or no relations exist for the result set | Set `graph_context: true`. Empty arrays are normal when there's nothing to return - see [Context Graphs](/essentials/v2/context-graphs). |
-| Recent uploads don't appear in results | Indexing not finished | Poll `GET /context/status?ids=...&database=...` - chunks are invisible until processing reaches at least `graph_creation`. |
-| `metadata_filters` doesn't narrow results | Filter key is in the wrong namespace, the value doesn't match exactly, or the hot top-level field was not declared with `enable_match` | Top-level keys match `metadata`; free-form per-document fields must be nested under `additional_metadata`. Declare hot filter keys in `database_metadata_schema` with `enable_match: true` before production ingest. |
-| Memories missing from a `type: "knowledge"` query | Wrong store selected | Use `type: "memory"` or `type: "all"`. |
-| Recency doesn't seem to matter | `recency_bias` defaults to `0` | Set it explicitly between `0.1` and `1.0`. |
-| `operator: "phrase"` ignored | `query_by` not set to `"text"` | `operator` only applies to BM25 text query - switch `query_by` to `"text"`. |
-| `query_forceful_relations` ignored | Request uses `mode: "fast"` | Forceful-relation context is fetched only in `mode: "thinking"`. |
-| `graph_context` value ignored | Request uses `mode: "auto"` (or `mode` was omitted) | `auto` overrides `graph_context` to match whichever pipeline it routes to; set `graph_context` explicitly only under `mode: "fast"` or `"thinking"`. |
-| Expected a deterministic `fast`/`thinking` pipeline, got auto-routed instead | `mode` was omitted | Omitting `mode` now defaults to `"auto"` - set `mode` explicitly to `"fast"` or `"thinking"` if you don't want automatic routing. |
+| `400` mentioning `type` | `type: "knowledge"` or `"memory"` sent to a unified database | Drop `type`. Scope with `collection` or `collections`. |
+| `graph` is `[]` | `graph_context: false`, or `mode` resolved to `fast`, or nothing connects the results | Set `graph_context: true` with `mode: "thinking"`. An empty array is normal when there is nothing to return. |
+| `forceful_relations` is `[]` | Nothing in the hits declared `forceful_relations`, or `follow_forceful_relations: false` | Declare relations at ingest; leave the flag on. |
+| Recent items do not appear | Indexing not finished | Poll `GET /context/status?ids=...&database=...`; chunks are invisible until processing reaches at least `graph_creation`. |
+| `attributes` does not narrow results | The key is not declared in `database_metadata_schema`, or the value does not match | Declare the field and send it in `attributes` at ingest; filter with an operator such as `$eq`. `custom_attributes` are never filterable. |
+| Chunk has no title or url | Chunks carry no source details by design | Call `GET /context/inspect` with the chunk's `context_id`. |
+| `operator: "phrase"` ignored | `query_by` is not `"text"` | `operator` only applies to BM25 text query. |
+| `graph_context` value ignored | `mode` is `auto` (or omitted) | `auto` overrides `graph_context` to match the pipeline it picks. Set `mode` explicitly. |
---
-## 6. Advanced patterns
-
-**Hybrid + text in two parallel calls.** When a query mixes a literal token (error code, SKU, function name) with natural-language intent, run `query_by: "hybrid"` and `query_by: "text"` in parallel, dedupe by chunk ID, and treat text hits as a "must include" floor.
-
-**Recall-then-rerank.** Ask for more chunks than you actually need (`max_results: 20`) and apply your own reranker - recency windows, compliance filters, business rules - before picking the final top-k for the prompt.
+## 6. Split databases
-**Cache the prompt context.** If the same query repeats inside a session, cache the `RetrievalResult` keyed by `(database, collection, query, type, query_by)`. Recall is fast, but skipping it entirely is faster.
+A database created with `type: "split"` keeps the old query contract exactly as it was: `type` selects `knowledge`, `memory` or `all`, `query_forceful_relations` fetches declared relations into `additional_context`, and the response carries `chunks[].chunk_content`, `sources[]`, `graph_context` and `additional_context`. None of that appears on a unified database, and the SDK `build_string` helper is only needed there. The full mapping from each old field to its unified counterpart is on [Split databases and legacy fields](/essentials/v2/split-databases#4-legacy-query-and-response-fields).
---
## Related
-- [Knowledge](/essentials/v2/knowledge) - shared document context
-- [Memories](/essentials/v2/memories) - user-scoped dynamic context
-- [Metadata](/essentials/v2/attributes) - designing filterable fields
-- [Context Graphs](/essentials/v2/context-graphs) - how graph traversal enriches recall
-- [How to Use API Results](/essentials/v2/api-results) - turning `RetrievalResult` into an LLM prompt
-- [Query - API Reference](/api-reference/v2/endpoint/query) - full parameter and response schema
+- [How to Use API Results](/essentials/v2/api-results): injecting `llm_prompt` and reading the four keys
+- [Context graphs](/essentials/v2/context-graphs): how `graph[]` is built
+- [Attributes](/essentials/v2/attributes): designing filterable fields
+- [Ingest context](/essentials/v2/ingest): the items a query searches
+- [Query API reference](/api-reference/v2/endpoint/query): full parameter and response schema
diff --git a/essentials/v2/semantic-search.mdx b/essentials/v2/semantic-search.mdx
index cce1582e..b49ef8e9 100644
--- a/essentials/v2/semantic-search.mdx
+++ b/essentials/v2/semantic-search.mdx
@@ -15,9 +15,9 @@ Semantic search is useful because it retrieves by meaning instead of exact wordi
| Keyword BM25 | Text with matching tokens | Error codes, identifiers, names, SKUs, exact phrases |
| Graph | Related entities and relationships | Multi-hop questions, dependencies, ownership, project context |
-`POST /query` is the unified retrieval endpoint. Two parameters decide what runs:
+`POST /query` is the single retrieval endpoint. Two parameters decide what runs:
-- **`type`** picks the collection: `"knowledge"` (Knowledge), `"memory"` (user-scoped Memories), or `"all"` (both, merged and re-ranked together).
+- **`collections`** (or `collection`) picks where to look: one collection, or several with weights. There is no corpus selector; a unified database is one corpus.
- **`query_by`** picks the retrieval method: `"hybrid"` (semantic + BM25, the default) or `"text"` (BM25 only, with `operator: "or" | "and" | "phrase"`).
---
@@ -65,15 +65,12 @@ curl -X POST 'https://api.hydradb.com/query' \
"database": "acme",
"collection": "team-mobile",
"query": "How do we rotate API keys?",
- "type": "knowledge",
"query_by": "hybrid",
"max_results": 8,
"alpha": 0.8,
"recency_bias": 0.2,
"graph_context": true,
- "metadata_filters": {
- "project": "phoenix"
- }
+ "attributes": { "project": { "$eq": "phoenix" } }
}'
```
@@ -82,13 +79,12 @@ const result = await client.query({
database: "acme",
collection: "team-mobile",
query: "How do we rotate API keys?",
- type: "knowledge",
queryBy: "hybrid",
maxResults: 8,
alpha: 0.8,
recencyBias: 0.2,
graphContext: true,
- metadataFilters: { project: "phoenix" },
+ attributes: { project: { $eq: "phoenix" } },
});
```
@@ -97,19 +93,18 @@ result = client.query(
database="acme",
collection="team-mobile",
query="How do we rotate API keys?",
- type="knowledge",
query_by="hybrid",
max_results=8,
alpha=0.8,
recency_bias=0.2,
graph_context=True,
- metadata_filters={"project": "phoenix"},
+ attributes={"project": {"$eq": "phoenix"}},
)
```
-`metadata_filters` are exact constraints that run before ranking and are re-checked after hydration. Use them whenever the query has a scope that should not be violated. Top-level keys match `metadata`; nest under `additional_metadata` to filter free-form per-document fields.
+`attributes` are exact constraints applied during retrieval. Use them whenever the query has a scope that should not be violated. Keys are the fields declared in `database_metadata_schema` and sent as `attributes` at ingest; `custom_attributes` are never filterable.
---
@@ -129,7 +124,6 @@ curl -X POST 'https://api.hydradb.com/query' \
-d '{
"database": "acme",
"query": "How does onboarding work?",
- "type": "knowledge",
"query_by": "hybrid",
"max_results": 8,
"alpha": 0.8
@@ -140,7 +134,6 @@ curl -X POST 'https://api.hydradb.com/query' \
const result = await client.query({
database: "acme",
query: "How does onboarding work?",
- type: "knowledge",
queryBy: "hybrid",
maxResults: 8,
alpha: 0.8,
@@ -151,7 +144,6 @@ const result = await client.query({
result = client.query(
database="acme",
query="How does onboarding work?",
- type="knowledge",
query_by="hybrid",
max_results=8,
alpha=0.8,
@@ -174,7 +166,6 @@ curl -X POST 'https://api.hydradb.com/query' \
-d '{
"database": "acme",
"query": "TimeoutError in payments-worker v4.2.1",
- "type": "knowledge",
"query_by": "hybrid",
"max_results": 5,
"alpha": 0.3,
@@ -186,7 +177,6 @@ curl -X POST 'https://api.hydradb.com/query' \
const result = await client.query({
database: "acme",
query: "TimeoutError in payments-worker v4.2.1",
- type: "knowledge",
queryBy: "hybrid",
maxResults: 5,
alpha: 0.3,
@@ -198,7 +188,6 @@ const result = await client.query({
result = client.query(
database="acme",
query="TimeoutError in payments-worker v4.2.1",
- type="knowledge",
query_by="hybrid",
max_results=5,
alpha=0.3,
@@ -210,7 +199,7 @@ result = client.query(
### Scoped Retrieval
-Use metadata filters to keep retrieval inside a project, team, customer, data class, or source.
+Use `attributes` to keep retrieval inside a project, team, customer, data class, or source.
@@ -223,11 +212,8 @@ curl -X POST 'https://api.hydradb.com/query' \
"database": "acme",
"collection": "team-mobile",
"query": "What is the current sprint status?",
- "type": "knowledge",
"query_by": "hybrid",
- "metadata_filters": {
- "project": "phoenix"
- }
+ "attributes": { "project": { "$eq": "phoenix" } }
}'
```
@@ -236,9 +222,8 @@ const result = await client.query({
database: "acme",
collection: "team-mobile",
query: "What is the current sprint status?",
- type: "knowledge",
queryBy: "hybrid",
- metadataFilters: { project: "phoenix" },
+ attributes: { project: { $eq: "phoenix" } },
});
```
@@ -247,9 +232,8 @@ result = client.query(
database="acme",
collection="team-mobile",
query="What is the current sprint status?",
- type="knowledge",
query_by="hybrid",
- metadata_filters={"project": "phoenix"},
+ attributes={"project": {"$eq": "phoenix"}},
)
```
@@ -269,7 +253,6 @@ curl -X POST 'https://api.hydradb.com/query' \
-d '{
"database": "acme",
"query": "mechanical engineer",
- "type": "knowledge",
"query_by": "text",
"operator": "phrase",
"max_results": 10
@@ -280,7 +263,6 @@ curl -X POST 'https://api.hydradb.com/query' \
const result = await client.query({
database: "acme",
query: "mechanical engineer",
- type: "knowledge",
queryBy: "text",
operator: "phrase",
maxResults: 10,
@@ -291,7 +273,6 @@ const result = await client.query({
result = client.query(
database="acme",
query="mechanical engineer",
- type="knowledge",
query_by="text",
operator="phrase",
max_results=10,
@@ -304,20 +285,20 @@ result = client.query(
## Reading The Response
-`POST /query` returns ranked chunks and source metadata, not an answer. A typical application flow is:
+`POST /query` returns retrieved context, not an answer: ranked `chunks`, graph paths in `graph`, declared links in `forceful_relations`, and `llm_prompt`, the same context as one prompt-ready string. A typical application flow is:
-1. Call `POST /query` with the right `type` and `query_by` for the query.
-2. Keep the chunks that are relevant enough for your use case.
-3. Format `chunk_content`, source titles, and graph context into a prompt.
-4. Ask your LLM to answer using only that context.
+1. Call `POST /query` with the right `collections` and `query_by` for the query.
+2. Inject `llm_prompt` into your model call, with a grounding instruction.
+3. Ask your LLM to answer using only that context and to cite the bracketed labels.
+4. Read `chunks`, `graph` and `forceful_relations` when you need structure, and `GET /context/inspect` for a chunk's source details.
-See [How to Use API Results](/essentials/v2/api-results) for complete context-building examples.
+See [How to Use API Results](/essentials/v2/api-results) for complete examples.
---
## Mental Model
-Semantic search finds text that means the same thing. Keyword BM25 search finds text that says the same thing. Graph context finds connected entities. Metadata filters decide what is allowed to be queried. `POST /query` combines all four behind one endpoint via `type`, `query_by`, `metadata_filters`, and `graph_context` so your agents get context that is scoped, relevant, and explainable.
+Semantic search finds text that means the same thing. Keyword BM25 search finds text that says the same thing. Graph context finds connected entities. Attribute filters decide what is allowed to be queried. `POST /query` combines all four behind one endpoint via `collections`, `query_by`, `attributes`, and `graph_context` so your agents get context that is scoped, relevant, and explainable.
---
@@ -325,5 +306,5 @@ Semantic search finds text that means the same thing. Keyword BM25 search finds
- [Query](/essentials/v2/query) - full parameter reference and parallel query patterns
- [Context Graphs](/essentials/v2/context-graphs) - how graph context enriches retrieval
-- [Metadata](/essentials/v2/attributes) - designing filterable schemas
+- [Attributes](/essentials/v2/attributes) - designing filterable schemas
- [How to Use API Results](/essentials/v2/api-results) - turning the response into an LLM prompt
diff --git a/essentials/v2/split-databases.mdx b/essentials/v2/split-databases.mdx
index 782b46f7..7608470a 100644
--- a/essentials/v2/split-databases.mdx
+++ b/essentials/v2/split-databases.mdx
@@ -3,7 +3,7 @@ title: "Split databases and legacy fields"
description: "How databases created as type split work, the knowledge, memory and all selector, and every older field name with its current replacement."
---
-Every other page in these docs describes a **unified** database: you ingest `items` and query without a corpus selector. This page covers the older model, for integrations that still use it:
+Every other page in these docs describes a **unified** database: you send `context` items and query without a corpus selector. This page covers the older model, for integrations that still use it:
- databases created with `type: "split"`, including every database created before unified became the default
- the older field names HydraDB still accepts on any database
@@ -16,7 +16,7 @@ Nothing on this page is going away. Existing databases keep working unchanged an
| Type | How you get one | What it holds |
| --- | --- | --- |
-| `unified` | `POST /databases` without `type` (the default) | One corpus. Ingest `items`; query without `type`. |
+| `unified` | `POST /databases` without `type` (the default) | One corpus. Send `context` items; query without `type`. |
| `split` | `POST /databases` with `"type": "split"`, or any database created before unified became the default | Two corpora, knowledge and memory, selected with `type` on every context and query call. |
```bash
@@ -29,10 +29,10 @@ curl -X POST 'https://api.hydradb.com/databases' \
To find out which type a database is:
-- `GET /databases` returns `details: [{ "database": "acme", "type": "unified" }, ...]`.
+- `GET /databases` returns `details: [{ "database": "acme", "type": "unified" }, ...]`. When `type` is absent, treat the database as split.
- `GET /databases/status?database=acme` returns `type` alongside the readiness flags.
-The type is fixed when the database is created. There is no conversion: to move a split database to unified, create a new database, re-ingest your content as items, and point your client at it.
+The type is fixed when the database is created. There is no conversion: to move a split database to unified, create a new database, re-ingest your content as items, and point your client at it. A client detects the layout once per database and caches it; it never branches on a request flag.
---
@@ -68,7 +68,7 @@ Relations, subgraph and chunks read one corpus at a time, so `all` reads knowled
`DELETE /context` with `type: "all"` deletes from **both** corpora. The two deletes are not atomic: if the knowledge delete succeeds and the memory delete then fails, the knowledge deletions have already happened. To delete from one corpus only, send `knowledge` or `memory`.
-An ingest that carries `items` on a split database writes to its **memory** corpus, because items are memory-shaped. Absent and `type: "memory"` both land there; `type: "knowledge"` together with `items` is a `400`.
+An ingest that carries `context` items on a split database writes to its **memory** corpus, because items are memory-shaped. Absent and `type: "memory"` both land there; `type: "knowledge"` together with `context` is a `400`.
---
@@ -83,27 +83,36 @@ A split database also accepts the ingest shapes that predate items. Each has its
| `memories` | Memory items: `text` or `user_assistant_pairs`, with `infer`, `expiry_time` and `relations` | [Memories](/essentials/v2/memories) |
| `graph_payload` | A graph you built yourself | [Bring your own graph](/essentials/v2/bring-your-own-graph) |
-A unified database rejects `documents`, `app_knowledge` and `memories` with a `400` naming `items`. It takes text only, so extract text from files and send it as an item.
+A unified database rejects `type`, `documents`, `app_knowledge` and `memories` with a `400` naming `context`. It takes text only, so extract text from files and send it as an item. `graph_payload` works on both, keyed by `context_id` on a unified database.
### Mapping split fields to items
| Split field | Item field |
| --- | --- |
-| `memories[]`, `app_knowledge[]`, `documents` | `items[]` |
-| `id` | `context_id` |
+| `memories[]`, `app_knowledge[]`, `documents` | `context[]` |
+| `id` / `source_id` | `context_id` |
| `text` (memory), `content.text` (app knowledge) | `text` |
-| `user_assistant_pairs` | `conversation`, as `[{ role, content, name }]` |
+| `user_assistant_pairs` | `conversation`, as `[{ role, content, name? }]` |
+| `infer` (default `false`) | `enrich` (default `true`) |
+| `custom_instructions` | `instructions`, on the item or on the request |
+| `observation_date` | `happened_at` |
| `metadata` | `attributes` |
| `additional_metadata` | `custom_attributes` |
-| `observation_date` | `happened_at` |
-| `infer` | `enrich` |
+| `upsert` (request only) | `upsert` on the item, with the request value as the default |
+| `relations` (knowledge only) | `forceful_relations`, on any item |
+| `acl` (app sources only) | `acl`, on any item |
+| `evidence_kind`, `evidence_subject`, `expiry_time`, `retain_source` | removed; a `400` on a unified database |
-An item reads only the names in the right-hand column. A left-hand name sent on an item is dropped without an error.
+A few split names are accepted as aliases on a unified database: `items` and `contexts` for `context`, `custom_instructions` for `instructions`, `relations` for `forceful_relations`, and `context_ids` or `source_ids` for the `ids` key inside it. Send the item-field names; any other unknown key on an item is dropped without an error. The full item reference is on [Ingest context](/essentials/v2/ingest#3-item-fields).
---
## 4. Legacy query and response fields
+### `type` and `query_forceful_relations` on the request
+
+On a unified database, do not send `type`. `follow_forceful_relations` is the current name of the flag that pulls declared relations into the response; `query_forceful_relations` is its deprecated alias and still works.
+
### `metadata_filters`
`metadata_filters` is the older filter language. It still works on every database; new integrations should use [`attributes`](/essentials/v2/attributes), which supports `$eq`, `$in`, `$gt`, `$and`, `$or` and the rest.
@@ -133,27 +142,29 @@ Each declared (top-level) field in `metadata_filters` takes one operator object:
**Storing several values in one field.** A declared field holds one value. To store several, declare it `VARCHAR`, join the values with commas (`"a@company.com,b@company.com"`), and filter with `contains`. A value that itself contains a comma will not match as expected, and `max_length` cannot be raised after the field is created.
-### `graph_context`
-
-| Legacy response field | Current field |
-| --- | --- |
-| `graph_context.query_paths` | `graph.paths[]` |
-| `graph_context.chunk_relations` | `graph.paths[]` |
-| `graph_context.chunk_id_to_group_ids` | `graph.paths[].chunk_ids` |
-
-`graph_context` is still returned. `graph.paths[]` is the same result as one ordered list: paths reached from the query first, then paths expanded from retrieved chunks, each carrying the chunk ids it is evidence for.
+### The query response
-### Forceful relations
+A split database returns the old response exactly as before. A unified database returns four keys: `chunks`, `graph`, `forceful_relations` and `llm_prompt`, described on [Query](/essentials/v2/query#3-response). Field by field:
-| Legacy field | Current field |
+| Split response field | Unified response field |
| --- | --- |
-| `additional_context` (response) | `forceful_relations.declared` |
-
-`relations` on `memories`, `documents` and `app_knowledge`, and `query_forceful_relations` on query, are split-era fields for relations you declare yourself. See [Memories](/essentials/v2/memories#connect-related-memories-with-forceful-relations). Items have no field for declared relations yet.
-
-### `is_memory`
-
-On a unified database every chunk carries `is_memory`, and it is filterable in `attributes`. On a split database it is not filterable, because context ingested before the field existed does not have it.
+| `chunks[].chunk_uuid` | `chunks[].chunk_id` |
+| `chunks[].id` | `chunks[].context_id` |
+| `chunks[].chunk_content` (with enrichment concatenated in) | `chunks[].content` (verbatim) plus `chunks[].enrichment` (a string, stored separately) and `chunks[].enrichment_kind` (the declared `context_category`) |
+| `chunks[].relevancy_score` | `chunks[].score` |
+| `chunks[].source_title`, `source_type`, `source_upload_time`, `metadata`, `additional_metadata`, `collection`, ... | removed; call `GET /context/inspect` with the `context_id` |
+| `chunks[].extra_context_ids` | removed |
+| `sources[]` | removed; call `GET /context/inspect` |
+| `graph_context.query_paths[]` and `graph_context.chunk_relations[]` | `graph[]`, one flat list: query paths first, then chunk expansions, deduplicated; each path's `origin` is `query_path` or `chunk_relation` |
+| `graph_context.chunk_id_to_group_ids` | removed; group hops by `triplets[].relation.chunk_id` against `chunks[].chunk_id` (see [Attaching graph paths to chunks](/essentials/v2/query#attaching-graph-paths-to-chunks)) |
+| path `relevancy_score`, `group_id`, `source_chunk_ids`, `combined_context` | `path_summary` only (the path's summary, or its hops narrated when it has none); the reranked relevance is printed in `llm_prompt`, not returned as a field |
+| triplet `source.type`, `source.namespace`, `relation.canonical_predicate`, `relation.raw_predicate`, `relation.confidence`, `relation.source_entity_id`, `relation.target_entity_id` | removed; an entity is `{ entity_id, name }`, a relation is `{ predicate, context, temporal_details?, relationship_id, chunk_id }` |
+| `additional_context` and `forceful_relations.declared` | `forceful_relations[]`, a list of `{ via: { from, to }, chunk }` (no longer an object with `declared`) |
+| `temporal_facts` | `chunks[].temporal[]` |
+| (none) | `llm_prompt`, a server-built markdown string: results cited `[1]`, forceful relations `[R1]`, related facts labelled `[P1]` in `graph[]` order with each path's relevance when it has one |
+| `meta.tenant_id`, `meta.sub_tenant_id`, `meta.source_type` | removed; `meta` is `request_id`, `api_version`, `latency_ms`, `database` and `collection`, plus `deprecation` when a deprecated name was used |
+
+The SDK `build_string` / `buildString` helper formats the split response for a model. On a unified database it returns `llm_prompt` verbatim, so inject `llm_prompt` directly. A parser tells the two shapes apart by their keys: `llm_prompt` and a `graph` array mean unified, `graph_context` or `chunk_content` mean split.
---
@@ -206,18 +217,18 @@ In the **indexing webhook payload**, `tenant_id` and `database` do not carry the
---
-## 6. Names that differ between items and responses
+## 6. Names that differ between items and other responses
-Responses still use the original field names, so a few things are spelled differently on the way in and on the way out:
+`POST /query` on a unified database returns `context_id` on every chunk and no attributes at all. The context management endpoints keep their existing response shapes, so a few things are spelled differently on the way in and on the way out there:
-| On an item | In a response |
+| On an item | On `POST /context/list`, `GET /context/inspect` and `PATCH /context/{id}/metadata` |
| --- | --- |
-| `attributes` | `metadata` |
-| `custom_attributes` | `additional_metadata` |
| `context_id` | `id` |
+| `attributes` | `metadata` (`database_metadata` on the PATCH body) |
+| `custom_attributes` | `additional_metadata` |
-- `PATCH /context/{id}/metadata` takes `database_metadata` for attributes and `additional_metadata` for custom attributes.
-- `contexts`, `content` and `messages` are accepted on ingest as aliases of `items`, `text` and `conversation`.
+- The ingest `202` also reports each item as `results[].source_id`; read it as the `context_id`.
+- `items` and `contexts` are accepted on ingest as aliases of `context`; `content` as an alias of `text`; `messages` as an alias of `conversation`.
- On a unified database, `GET /databases/stats` reports the database's indexed chunk count in `knowledge_collection.row_count`, and `memory_collection` repeats the same number.
---
@@ -226,5 +237,5 @@ Responses still use the original field names, so a few things are spelled differ
| Code | Status | When | Fix |
| --- | --- | --- | --- |
-| `CORPUS_TYPE_UNSUPPORTED` | `400` | A `type` or split ingest field the database does not accept: `knowledge` or `memory` on a unified database, `unified` on a split one, `documents`, `app_knowledge` or `memories` on a unified database, or `all` on ingest | Omit `type` on a unified database and send `items`; on a split database send `knowledge` or `memory` |
+| `CORPUS_TYPE_UNSUPPORTED` | `400` | A `type` or split ingest field the database does not accept: `knowledge` or `memory` on a unified database, `unified` on a split one, `documents`, `app_knowledge` or `memories` on a unified database, or `all` on ingest | Omit `type` on a unified database and send `context`; on a split database send `knowledge` or `memory` |
| `CONTEXT_CATEGORY_UNSUPPORTED` | `400` | A `context_category` other than `auto` on a split database | Drop `context_category`, or send `auto` |
diff --git a/get-started/v2/core-concepts.mdx b/get-started/v2/core-concepts.mdx
index c739ecf5..4d99fd78 100644
--- a/get-started/v2/core-concepts.mdx
+++ b/get-started/v2/core-concepts.mdx
@@ -37,7 +37,7 @@ An item is either plain `text` or a `conversation`: a document, a policy, a supp
{
"database": "acme",
"collection": "company",
- "items": [
+ "context": [
{ "context_id": "refund-policy", "title": "Refund policy", "text": "Refunds are processed within 5 business days." },
{
"context_id": "chat-alex-001",
@@ -50,7 +50,7 @@ An item is either plain `text` or a `conversation`: a document, a policy, a supp
}
```
-Enrichment is on by default (`enrich: true`): send raw conversations and logs, and HydraDB extracts the preferences and facts in them. Turn it off for items you want stored exactly as sent.
+Enrichment is on by default (`enrich: true`): send raw conversations and logs, and HydraDB extracts the preferences and facts in them, stored separately from the item's own text. Turn it off for items you want stored exactly as sent. The SDKs send the same list in the `items` form field.
Read more: [Ingest context](/essentials/v2/ingest)
@@ -66,7 +66,7 @@ Each item can carry a `context_category`:
| `business_knowledge` | Documentation, policy, product and domain facts. Usually in a shared collection. |
| `decision_trace` | Agent runs, architecture decisions, postmortems, approvals. They show up in the graph as decisions connected to who made them. |
-Leave it out, or send `auto`, and HydraDB decides.
+The label is yours to set. Leave it out, or send `auto`, and the item carries no label; HydraDB never infers one.
Read more: [Context categories](/essentials/v2/context-categories)
@@ -88,7 +88,9 @@ Personalize by querying several collections with weights:
}
```
-Tune it with `query_by` (`hybrid` or `text`), `mode` (`auto`, `fast` or `thinking`) and `graph_context`.
+Tune it with `query_by` (`hybrid` or `text`), `mode` (`auto`, `fast` or `thinking`) and `graph_context`. There is no `type`: a unified database is one corpus.
+
+The response is four keys: `chunks` (ranked matches with `content`, `score` and `enrichment`), `graph` (relation paths, each with a `path_summary`), `forceful_relations` (items you linked at ingest) and `llm_prompt`, a server-built string with citation labels that you inject into your model call as is.
Read more: [Query](/essentials/v2/query)
@@ -141,7 +143,7 @@ Read more: [Attributes](/essentials/v2/attributes)
## Context graph
-As items are enriched, HydraDB builds a graph of the entities they mention and the relations between them. Query results include `graph.paths[]`: chains of relations that connect what you asked about to what is relevant, including decisions and who made them. That is how an answer reaches context that shares no words with the query.
+As items are enriched, HydraDB builds a graph of the entities they mention and the relations between them. Query results include `graph[]`: paths of relations that connect what you asked about to what is relevant, including decisions and who made them, each summarized in one sentence. That is how an answer reaches context that shares no words with the query.
Read more: [Context graphs](/essentials/v2/context-graphs)
diff --git a/get-started/v2/introduction.mdx b/get-started/v2/introduction.mdx
index 8590350a..1af7ff3f 100644
--- a/get-started/v2/introduction.mdx
+++ b/get-started/v2/introduction.mdx
@@ -72,4 +72,4 @@ For enterprise onboarding, contact [founders@hydradb.com](mailto:founders@hydrad
## For AI agents
-For AI coding agents and IDE assistants, use the [HydraDB Agent Integration Guide](/AGENTS) and the [v2 OpenAPI spec](/api-reference/v2/openapi.json). Ingest `items`, and query without a corpus selector.
\ No newline at end of file
+For AI coding agents and IDE assistants, use the [HydraDB Agent Integration Guide](/AGENTS) and the [v2 OpenAPI spec](/api-reference/v2/openapi.json). Ingest `context` items, and query without a corpus selector; the query returns `llm_prompt`, ready to inject.
\ No newline at end of file
diff --git a/get-started/v2/quickstart.mdx b/get-started/v2/quickstart.mdx
index ebb42d34..ddf66f9c 100644
--- a/get-started/v2/quickstart.mdx
+++ b/get-started/v2/quickstart.mdx
@@ -60,6 +60,7 @@ while True:
time.sleep(5)
# 3. Ingest a policy into the shared collection and a conversation into Alex's.
+# The SDK sends the item list in the `items` form field.
client.context.ingest(
database=database,
collection="company",
@@ -102,10 +103,13 @@ results = client.query(
query="How should I explain our refund policy to this user?",
)
-for chunk in results.data.chunks or []:
- print(chunk.chunk_content)
-for path in results.data.graph.paths or []:
- print(path.combined_context)
+for chunk in results.data.chunks:
+ print(chunk.score, chunk.content)
+for path in results.data.graph:
+ print(path.path_summary)
+
+# 6. The same context as one prompt-ready string, with citation labels.
+print(results.data.llm_prompt)
```
```typescript TypeScript SDK
const database = "acme";
@@ -121,6 +125,7 @@ while (true) {
}
// 3. Ingest a policy into the shared collection and a conversation into Alex's.
+// The SDK sends the item list in the `items` form field.
await client.context.ingest({
database,
collection: "company",
@@ -163,8 +168,11 @@ const results = await client.query({
query: "How should I explain our refund policy to this user?",
});
-console.log((results.data.chunks ?? []).map((c) => c.chunkContent));
-console.log((results.data.graph?.paths ?? []).map((p) => p.combinedContext));
+console.log(results.data.chunks.map((c) => [c.score, c.content]));
+console.log(results.data.graph.map((p) => p.pathSummary));
+
+// 6. The same context as one prompt-ready string, with citation labels.
+console.log(results.data.llmPrompt);
```
```bash cURL
export HYDRA_DB_API_KEY="your_api_key"
@@ -189,7 +197,7 @@ curl -s -X POST "$API/context/ingest" "${AUTH[@]}" \
-d "{
\"database\": \"${DATABASE}\",
\"collection\": \"company\",
- \"items\": [{
+ \"context\": [{
\"context_id\": \"refund-policy\",
\"title\": \"Refund policy\",
\"text\": \"Refunds are processed within 5 business days.\",
@@ -201,7 +209,7 @@ curl -s -X POST "$API/context/ingest" "${AUTH[@]}" \
-d "{
\"database\": \"${DATABASE}\",
\"collection\": \"user_alex\",
- \"items\": [{
+ \"context\": [{
\"context_id\": \"chat-alex-001\",
\"conversation\": [
{ \"role\": \"user\", \"content\": \"Keep answers short, I read on my phone.\", \"name\": \"alex\" },
@@ -234,17 +242,22 @@ curl -s -X POST "$API/query" "${AUTH[@]}" \
"database": "'"${DATABASE}"'",
"collections": { "user_alex": 2, "company": 1 },
"query": "How should I explain our refund policy to this user?"
- }' | jq '{chunks: [.data.chunks[]?.chunk_content], paths: [.data.graph.paths[]?.combined_context]}'
+ }' | jq '{chunks: [.data.chunks[]?.content], paths: [.data.graph[]?.path_summary], llm_prompt: .data.llm_prompt}'
```
-`chunks` are the pieces of your items that matched, ranked. `graph.paths` are chains of relations HydraDB found between them, such as Alex, their preference for short answers, and the refund they asked about.
+The response has four keys. `chunks` are the pieces of your items that matched, ranked, each with a `score` and its `context_id`. `graph` is the list of relation paths HydraDB found between them, such as Alex, their preference for short answers, and the refund they asked about, each with a one-sentence `path_summary`. `forceful_relations` holds items you linked at ingest (none here). `llm_prompt` is all of that as one string with citation labels, ready to drop into your model call:
+
+```python
+messages = [{"role": "system", "content": results.data.llm_prompt},
+ {"role": "user", "content": "How should I explain our refund policy to this user?"}]
+```
---
## What you have built
-You have built the full retrieval loop: create an isolated database, ingest context, wait for indexing, query it, and pass the returned context to an LLM. HydraDB handles chunking, embedding, graph construction and hybrid retrieval; your application sends the query and gets back the right slice of context.
+You have built the full retrieval loop: create an isolated database, ingest context, wait for indexing, query it, and pass the returned context to an LLM. HydraDB handles chunking, embedding, enrichment, graph construction, hybrid retrieval and prompt assembly; your application sends the query and injects `llm_prompt`.
```mermaid
flowchart LR
@@ -270,9 +283,10 @@ Steps 1 and 3 are **asynchronous**: HydraDB provisions infrastructure and indexe
|---|---|
| See every item field, conversations and enrichment | [Ingest context](/essentials/v2/ingest) |
| Label preferences, knowledge and decisions | [Context categories](/essentials/v2/context-categories) |
+| Read every field of the query response | [Query](/essentials/v2/query) |
| Filter on declared fields | [Attributes](/essentials/v2/attributes) |
| Scope data per user or workspace | [Databases and collections](/essentials/v2/databases-and-collections) |
-| Format the LLM prompt from the query result | [How to Use API Results](/essentials/v2/api-results) |
+| Inject `llm_prompt` and map citations back | [How to Use API Results](/essentials/v2/api-results) |
| See the full endpoint reference | [API Reference](/api-reference/v2) |
| Pick from real-world recipes | [Cookbooks](/cookbooks/v2/index) |