Korely

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.

POST /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

FieldTypeRequiredDescription
querystringRequiredThe search query. 1-2,000 characters.
user_idstringOptionalSearch 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_idstringOptionalSearch one agent (agent_id). Max 255 chars.
run_idstringOptionalSearch one run or session (run_id). Max 255 chars.
metadataobjectOptionalFilter 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.
limitintegerOptionalMaximum number of results. Default 15, minimum 1, maximum 50.
min_scorenumberOptionalDrop 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.
typesarray<string>OptionalThe 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_textbooleanOptionalAdd each result's whole text as content. Default false: a result shows its first 280 characters in snippet.
exclude_sessionsbooleanOptionalLeave 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

Terminal window
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"}
}
]
}
FieldTypeDescription
resultsarray<SearchResult>Ranked list of matches. Each SearchResult has the fields below.
results[].idstringThe memory id, e.g. mem_8f2c1aab4d7e4f0c9a1b2c3d4e5f6a7b.
results[].scorefloatCosine similarity between the query and the memory, as the Glossary explains. Higher is more relevant.
results[].snippetstringThe memory content, truncated to 280 characters. For a fact, the first 280 characters of its line: subject · predicate · object, then its dates.
results[].typestringOnly when the request names types: memory or fact.
results[].contentstringOnly with full_text: true: the memory as it is stored (up to 16,000 characters), or the fact's line.
results[].factobjectOnly 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_idstring · nullThe end user this memory belongs to (null if none).
results[].agent_idstring · nullThe agent sub-namespace (null if none).
results[].metadataobjectThe 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

StatusCodeCause
401invalid_keyMissing or invalid Authorization header, no valid kor_live_ key.
403forbiddenThe key lacks the memories:read scope.
413payload_too_largeThe 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.
422invalid_requestValidation 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).
429quota_exceededMonthly query quota reached. No Retry-After; resets on the 1st of the month (UTC), or upgrade.
429rate_limit_exceededPer-plan minute, hour or day request ceiling exceeded (rate limit); the message names the window, and Retry-After gives the seconds until it resets.
503search_unavailableThe 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.
503service_pausedThe 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.
503unavailableThe 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.
408request_timeoutThe body did not arrive whole within 30 seconds. Send it again.
500internal_errorThe 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 503 or a 422 does not.
  • A forgotten memory is never returned. A deleted memory drops out of search at once.
  • Snippets are truncated. The snippet is the memory content cut to 280 characters. Send full_text: true to get the whole text as content, 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_id and the agent_id asked, and the work sessions left out by exclude_sessions. full_text gives 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_id to search everyone. Leaving user_id out searches across all end users in the key's project. Pass it to scope recall to a single end user.

Related