Retrieval
Filters, scoping, streaming and handling citations well.
The simplest useful call
curl https://api.uni-flow.ai/api/v1/memory/search \
-H "x-api-key: $CHORDIAN_API_KEY" \
-H "content-type: application/json" \
-d '{"query": "What is blocking the Acme deal?"}'Ask questions, not keywords
The query is classified before retrieval, and the classification determines how much weight goes to vectors, the graph and temporal recall. A question carries that signal; a bag of keywords does not.
| Better | Worse |
|---|---|
| "What is blocking the Acme deal?" | "acme blocker" |
| "Who at Acme has decision authority?" | "acme contacts" |
| "What changed on the Nexio contract last month?" | "nexio contract" |
Filters
querystringrequiredThe question.
memory_idstringDefaults to the active workspace's primary memory.
vault_idstringRestrict to a department.
space_idstringRestrict to a space.
sourcesstring[]Restrict to specific connected sources.
top_nintegerCandidates to consider before reranking. Default 10.
Discover valid sources values from
GET/api/v1/memory/sources — it returns only sources
that actually have indexed content, so a filter built from it cannot produce an
empty result by accident.
Streaming
Use it for anything a person watches.
event: sources data: { "citations": [...] }
event: token data: { "text": "The Acme" }
event: token data: { "text": " expansion deal" }
event: done data: { "answer": "...", "suggested_followups": [...] }Sources arrive first — guaranteed, not incidental. Render them immediately: the reader sees the grounding while the answer is still being written, which both lowers perceived latency and makes an ungrounded claim obvious.
const res = await fetch(url, { method: 'POST', headers, body });
const reader = res.body!.pipeThrough(new TextDecoderStream()).getReader();
let buffer = '';
while (true) {
const { value, done } = await reader.read();
if (done) break;
buffer += value;
const frames = buffer.split('\n\n');
buffer = frames.pop() ?? '';
for (const frame of frames) {
const event = /event: (.+)/.exec(frame)?.[1];
const data = /data: (.+)/.exec(frame)?.[1];
if (!event || !data) continue;
if (event === 'sources') renderCitations(JSON.parse(data).citations);
if (event === 'token') appendToken(JSON.parse(data).text);
}
}Citations
{
"answer": "Blocked on BAA approval and EU hosting confirmation [1]. Deal value is EUR 240k [2].",
"citations": [
{ "index": 1, "title": "Acme — Security Review notes", "source": "notion", "document_id": "ing_9f2c" },
{ "index": 2, "title": "Acme — proposal", "source": "gdrive", "document_id": "ing_7a1b" }
]
}Render [n] as a link to citations[n-1]. Two things to get right:
Show the source system. "From Notion" and "from an email" carry different weight to a reader deciding whether to trust a claim.
Treat an unmarked claim as unsupported. If the model asserts something with no citation, the retrieval did not support it. Surfacing that distinction is what makes the citations useful rather than decorative.
Caching
Repeat queries are served from cache. New content in the same scope invalidates it automatically, so re-asking after an ingest returns fresh results.
You do not need to bust the cache manually, and you should not try — the corpus version counter already handles the case that matters.
When answers are wrong
Do not delete the document. Correct it:
{
"learn_type": "correction",
"trigger": { "source_document_id": "ing_9f2c" },
"learning": { "correction": "The Acme deal closed on 14 September." }
}The correction is stored and retrievable, and the stale source is deprecated. Deleting removes the error and teaches nothing.