Core memory operations
Search memories
Semantic search over your memory store. Scope it to one end user and agent, or search across everyone you've stored.
/v1/memories/search
SDK: korely.search(query, ...). A search
ranks the stored memories by how close
their embedding is to the query's,
best first, and returns a snippet of each. Each match is a
hit with a score.
By default it reads the memories only. Send types to read the
facts too, or instead. For the facts
and the memories assembled into a block for a prompt, use
Get context. A search is a
read, so it counts against your monthly
query quota. The guide shows what a
good query looks like.
Authentication
HTTP header, required: Authorization: Bearer kor_live_.... The key must carry the memories:read permission.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
query | string | Required | The search query. 1-2,000 characters. |
user_id | string | Optional | Search one end user. Max 255 chars. When omitted, searches across all end users in the key's project (a key confined to some end users searches only those). |
agent_id | string | Optional | Search one agent (agent_id). Max 255 chars. |
run_id | string | Optional | Search one run or session (run_id). Max 255 chars. |
metadata | object | Optional | Filter on the metadata you stored at write time. Keys are ANDed; each value is compared as text with the stored one, e.g. {"tier": "pro"}: true matches a stored true, 5 a stored 5 (a number also matches an equal number, 5.0), "5" a stored 5 or "5", null a stored null. An object, an array, NaN or an infinity is refused with 422. |
limit | integer | Optional | Maximum number of results. Default 15, minimum 1, maximum 50. |
min_score | number | Optional | Drop the hits whose score is below this, from 0 to 1. Absent: no floor, every search returns its nearest memories. With a floor, a question whose answer is not stored can return {"results": []}, and fewer than limit results can come back. 0.2 is the value we measured: see Search. |
types | array<string> | Optional | The kinds the search reads, each named once: ["memories"] (the default, the search as it was), ["facts"], or ["memories", "facts"]. The facts are the ones true now that Get facts lists, those written with Write a fact included: a fact written that way has no memory, so a search of the memories never finds it. A fact that is no longer true is never found. A kind named twice, an empty list or another word is a 422. With facts in the list, run_id, metadata and min_score are each a 422: they narrow the memories only. See Facts and whole text. |
full_text | boolean | Optional | Add each result's whole text as content. Default false: a result shows its first 280 characters in snippet. |
exclude_sessions | boolean | Optional | Leave out the work sessions, the memories written with kind: "session" (what a coding assistant's hooks save), for a search that wants what was said and decided. Default false: everything. See Work sessions. |
Any other field is refused with 422 invalid_request (top_k and filters included): field names are snake_case.
Example request
curl -X POST https://api.korely.ai/v1/memories/search \ -H "Authorization: Bearer kor_live_..." \ -H "Content-Type: application/json" \ -d '{ "query": "communication preferences", "user_id": "customer-giulia-4812", "agent_id": "onboarding-bot", "limit": 15 }'Response
200 OK. A ranked list of matching memories, each with a
relevance score and a truncated snippet.
{ "results": [ { "id": "mem_8f2c1aab4d7e4f0c9a1b2c3d4e5f6a7b", "score": 0.873, "snippet": "Giulia prefers async standups and works in CET.", "user_id": "customer-giulia-4812", "agent_id": "onboarding-bot", "metadata": {"source": "slack"} } ]}| Field | Type | Description |
|---|---|---|
results | array<SearchResult> | Ranked list of matches. Each SearchResult has the fields below. |
results[].id | string | The memory id, e.g. mem_8f2c1aab4d7e4f0c9a1b2c3d4e5f6a7b. |
results[].score | float | Cosine similarity between the query and the memory, as the Glossary explains. Higher is more relevant. |
results[].snippet | string | The memory content, truncated to 280 characters. For a fact, the first 280 characters of its line: subject · predicate · object, then its dates. |
results[].type | string | Only when the request names types: memory or fact. |
results[].content | string | Only with full_text: true: the memory as it is stored (up to 16,000 characters), or the fact's line. |
results[].fact | object | Only for a fact: the fact as Get facts shows it. The result's id is the fact's fct_ id, and its metadata is {}. |
results[].user_id | string · null | The end user this memory belongs to (null if none). |
results[].agent_id | string · null | The agent sub-namespace (null if none). |
results[].metadata | object | The metadata stored with the memory ({} if none). |
Facts and whole text
With both kinds in types, each kind is ranked by its own score and
the two lists are merged by rank, not by score: the first memory, the first
fact, the second memory, and so on. At equal rank a memory comes first,
whatever order types names the kinds in, so with
limit: 1 the best memory is the answer whenever there is one.
Read each result's score against the others of its own
type only: how a fact's score compares with a memory's for the
same question has not been measured. The facts are ranked by meaning alone,
not by how recent they are.
The facts have no min_score of their own, because no value was
measured for them. A search with the facts and a min_score is a
422, so a search with a floor stays one that gives an empty list
when nothing is close.
A long memory is still found by what its beginning says: its vector is
computed from its first 8,000 characters, and the embedding model reads
at most 2,048 tokens of it, which is fewer characters in Chinese or Japanese
than in English.
full_text changes what comes back, not what is found. The REST
content is not bounded: a page of 50 whole memories can be large.
Whether facts in the results help an agent answer has not been measured. For an agent that answers questions, Get context is still the read to start from: it reads the facts and the memories together, within a budget.
The SDKs do not send types or full_text yet: call
this route directly, or the MCP tool korely_search. A search
that sends neither answers as before.
Errors
| Status | Code | Cause |
|---|---|---|
401 | invalid_key | Missing or invalid Authorization header, no valid kor_live_ key. |
403 | forbidden | The key lacks the memories:read scope. |
413 | payload_too_large | The body is larger than 2,097,152 bytes. Refused before it is read; nothing was done. On the Cloud a body over 1 MB gets a bare 413 first, without this code. |
422 | invalid_request | Validation failed: an empty or whitespace-only query, a query over 2,000 characters, limit outside the 1-50 range, min_score outside 0-1, a types list that is empty, names a kind twice or names another word, run_id, metadata or min_score sent with facts in types, a metadata value that is an object, an array, NaN or an infinity, a metadata filter with more than 32 keys or over 8,192 bytes, or an unknown field (the answer lists the fields the search takes). |
429 | quota_exceeded | Monthly query quota reached. No Retry-After; resets on the 1st of the month (UTC), or upgrade. |
429 | rate_limit_exceeded | Per-plan minute, hour or day request ceiling exceeded (rate limit); the message names the window, and Retry-After gives the seconds until it resets. |
503 | search_unavailable | The query could not be turned into an embedding: the embedding model did not answer, or, where an install embeds on its own server, a burst from one account made this search wait 20 seconds for its turn. The answer carries Retry-After: 5. It is not an empty result: retry. It takes no query of the month. |
503 | service_paused | The service itself does not call the models now (a spending stop or a provider brake), so the query cannot be embedded. Carries Retry-After: 3600: a retry within the hour cannot succeed. It takes no query of the month. |
503 | unavailable | The database did not answer while the key was checked, or the request waited more than 5 seconds for another change to the same records. Retry after the seconds in Retry-After. |
408 | request_timeout | The body did not arrive whole within 30 seconds. Send it again. |
500 | internal_error | The server failed on this request; the message holds no detail. |
Notes
- Counts as one query. Each search that answers counts against your monthly query quota. A search that ends in a
503or a422does not. - A forgotten memory is never returned. A deleted memory drops out of search at once.
- Snippets are truncated. The
snippetis the memory content cut to 280 characters. Sendfull_text: trueto get the whole text ascontent, or read one memory with Get a memory. - The facts a search shows are the facts you could already read. They are found behind the same walls as a memory: the key's project, a confined key's end users, the
user_idand theagent_idasked, and the work sessions left out byexclude_sessions.full_textgives a key the whole text of a memory it could already read whole with Get a memory, nothing more. The audit log lists the facts a search showed by id, never their text or the question. - One search counts as one query, whatever its
types. - Omit
user_idto search everyone. Leavinguser_idout searches across all end users in the key's project. Pass it to scope recall to a single end user.
Related
- Search memories, guide, the narrative walkthrough with context.
- Add a memory, write what you'll later recall.
- Get context, the assembled recall block, ready to drop into a prompt.