Korely

Core operations

Search memories

Semantic vector retrieval that returns the raw memories closest to a query, in one call. For the recall path that assembles typed facts into a ready-to-inject block, reach for get_context first.

Reach for get_context first. The differentiator is fact-assembled recall: get_context resolves your end user's active typed facts, bi-temporal (subject, predicate, object) triples with contradiction resolution, and folds them, plus the most relevant memories, into a single prompt block. Raw search below is the secondary path: it ranks individual memories by semantic similarity when you want the verbatim source rather than an assembled answer.

Search returns the memories most similar to a query so an agent can surface source context before composing a reply. Given a natural-language or keyword query, Korely embeds the query and ranks stored memories by cosine similarity over their embeddings, then returns the top hits. Each hit carries a relevance score and a short snippet so your agent can decide whether to fetch the full memory or pass the snippet directly into the prompt. Search is semantic vector retrieval only, it does not run a keyword index or a graph walk. The typed-fact graph is reached through get_context and get_facts, not through this endpoint.

An agent typically calls search when it needs the verbatim memory behind a topic, scoped to the current end user. To assemble a compact, fact-aware memory block for the system prompt in one call, use get_context. For a full time-ordered list of every memory you've stored for a user, see get_all.

flowchart LR
    Q([query]) --> EMB[Embed<br/>query]
    EMB --> VEC[Cosine similarity<br/>over memory embeddings]
    VEC --> R([ranked hits])
A query is embedded and ranked by cosine similarity into a single list of memory hits.

Request

Endpoint: POST /v1/memories/search. SDK: korely.search(query, *, user_id=None, agent_id=None, run_id=None, metadata=None, limit=None, min_score=None, exclude_sessions=False) (the server default for limit is 15).

ParameterTypeRequiredDescription
query string Required The search query, 1-2,000 characters. It is embedded whole and ranked by cosine similarity against stored memories, so a short phrase or a full question are both accepted. A blank or whitespace-only query is refused with 422 invalid_request.
user_id string Optional Strongly recommended in multi-tenant products. Max 255 characters. Scopes results to one end user. Without it, the search spans all end users of the key's project.
agent_id string Optional Max 255 characters. Further narrows to a specific agent surface. Omit to search across all agents of the key's project.
run_id string Optional Max 255 characters. Scopes results to one run or session.
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. At most 32 keys and 8,192 bytes once serialized (the limit stored metadata has); past either it is a 422.
limit integer Optional Number of hits to return. Default 15, from 1 to 50. Hits are ordered by descending relevance score.
min_score number Optional From 0 to 1. Drops the hits whose score is below it. Absent: no floor. See A floor on the score.
exclude_sessions boolean Optional Leaves 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. See Work sessions.

Any other field, such as top_k or filters, is refused with 422 invalid_request.

Always pass user_id in customer-facing agents. A search without user_id spans every end user stored in the namespace. That is correct for an internal ops tool and wrong for a per-customer chat. Filters are additive (AND): adding agent_id on top of user_id narrows further, it does not broaden.

Example

from korely_memory import Korely
korely = Korely(api_key="kor_live_...", region="eu")
results = korely.search(
"northwind pricing",
user_id="customer-4812",
limit=5,
)
for hit in results:
print(hit.id, hit.score, hit.snippet)
# mem_8f2c1a4d9b3e47a6b05e2c7d1f3a9e84 0.91 Northwind Hosting costs 50 euro per month since the June upgrade.
# mem_3c77d94e1a7b4f2c8d05b9a63e1f7c20 0.74 The team agreed to renegotiate the hosting contract in Q3.

Response

{
"results": [
{
"id": "mem_8f2c1a4d9b3e47a6b05e2c7d1f3a9e84",
"score": 0.91,
"snippet": "Northwind Hosting costs 50 euro per month since the June upgrade.",
"user_id": "customer-4812",
"agent_id": "infra-bot",
"metadata": { "source": "slack" }
},
{
"id": "mem_3c77d94e1a7b4f2c8d05b9a63e1f7c20",
"score": 0.74,
"snippet": "The team agreed to renegotiate the hosting contract in Q3.",
"user_id": "customer-4812",
"agent_id": "infra-bot",
"metadata": {}
}
]
}

Response fields

FieldTypeDescription
results array Ranked list of memory hits, best score first.
results[].id string Memory ID. Pass to korely.get(id) to retrieve the full content and extracted facts.
results[].score float 1 minus the cosine distance between the query and the memory, rounded to 4 decimals: at most 1.0. Higher means closer in meaning. The nearest memories come back whatever their score, so a low score is not proof that the answer is absent; use min_score to set a floor.
results[].snippet string Short excerpt (up to 280 characters) of the stored memory, suitable for direct inclusion in a prompt or for display in a UI. For the complete text, fetch the memory by ID.
results[].user_id string or null The end user the memory belongs to.
results[].agent_id string or null The agent surface that wrote the memory, or null if none was set.
results[].metadata object The metadata object passed when the memory was written. Empty object if none was set.

The response contains only the results array. There is no total field and no memories wrapper. An empty result is {"results": []}.

The read path is zero-generation. Search turns your query into one vector (on Korely Cloud, EmbeddingGemma on Korely's own server) and ranks stored memories by cosine similarity. No model ever composes or rewrites the snippets you receive. Your agent's own model does the reasoning over what comes back. Reads are retrieval, not generation.

A floor on the score

Without min_score, a search always returns its nearest memories, even for a question whose answer was never stored. A sentence that is close to nothing still has a nearest neighbour. Pass min_score to drop the hits below a score:

{
"query": "what is the customer's dog called?",
"user_id": "customer-4812",
"min_score": 0.2
}

If no memory reaches 0.2, the answer is {"results": []}. The right floor depends on your texts, so read your own scores before you choose one; 0.2 is only an example. /v1/context takes no floor: it ranks the facts and memories by relevance to the question and packs the nearest ones into the block within its token budget, and, with a token_budget of 400 or more, the reader note in the block tells the model to rely on the memories when a fact does not answer the question.

Fetching full content

Snippets are truncated for speed. When a hit's score is high enough to include in the prompt, fetch the complete memory to avoid cutting off important context:

results = korely.search("northwind pricing", user_id="customer-4812", limit=3)
# Pull full content for the top hit
if results and results[0].score > 0.8:
memory = korely.get(results[0].id)
print(memory.content)
# Northwind Hosting costs 50 euro per month since the June upgrade.

Timeline and fact history

Search returns only current, active memories. There is no include_history or time_filter parameter on this endpoint. For the full lifecycle of a single memory (edits, supersessions, deletion), use history. To query the typed fact store with temporal filters, including superseded facts, use get_facts(include_invalidated=True).

Errors

Search is a read-only operation, it counts against your monthly query quota, never the write quota. The table below lists every status code this endpoint can return.

StatusCode stringCause
200 None Success. Results may be an empty array if no memories match; this is not an error.
401 invalid_key The Authorization header is missing, malformed, or the key has been revoked.
403 forbidden The key lacks the memories:read scope, or it is confined to some end users and the user_id is not one of them.
422 invalid_request Request validation failed. The most common cause is a missing or empty query field, limit outside the 1-50 range, min_score outside 0-1, a metadata value that is an object, an array, NaN or an infinity, or an unknown field. The body has "code": "invalid_request" and a message naming the field, e.g. "query: Field required".
429 quota_exceeded Monthly query quota is exhausted (past the grace allowance). The quota 429 carries no Retry-After, it resets on the 1st of the month (UTC). Upgrade your plan or wait for the monthly reset.
429 rate_limit_exceeded Too many requests in the current minute, hour or day. Carries a Retry-After header, in seconds.
500 internal_error The request failed on our side. Retry.
503 search_unavailable The query could not be embedded: the embedding provider did not answer, or your account already has four requests at the embedder (searches, context calls and the vectors of writes, running or waiting), or this one waited 20 seconds for its turn. Retry shortly; it is not an empty result.

Every error response carries code (stable, branch on it) and message (for a person). A 401 has "code": "invalid_key" and "message": "Invalid or missing API key: no live key matches it (a revoked key stops at once); your keys are at https://agent.korely.ai/keys". Every error also carries detail, kept for older clients. On the monthly-quota 429, detail also holds limit, used and resets_at.

Notes

  • Read-only, non-destructive. Search never modifies stored memories or facts. Calling it any number of times with the same parameters is safe and produces the same ranked result set given the same corpus.
  • Scoping is additive. Filters narrow results: user_id alone returns all memories for that user across every agent; adding agent_id on top narrows to that agent's memories for the user. You cannot broaden results by combining filters.
  • No cross-workspace access. The API key determines the namespace. A search with user_id="customer-4812" only touches end users stored under that key's workspace, it cannot reach another customer's data even if they share the same user_id string.
  • Semantic vector ranking. Hits are ranked by cosine similarity between the query embedding and stored memory embeddings. There is no keyword index and no graph walk on this endpoint; for typed-fact recall use get_context or get_facts.
  • Rate-limit behaviour. Each search call counts as one query against your plan's monthly query quota. Hobby (25k/month), Developer (250k/month), Team (1M/month), Scale (10M/month). When the monthly quota is used up, 10% grace included, the API returns 429 quota_exceeded with no Retry-After; its detail says limit, used and resets_at (00:00 UTC on the 1st). A separate per-minute, per-hour or per-day rate limit can also return 429, that one carries a Retry-After header in seconds.
  • Empty results are not errors. A 200 response with "results": [] means no memories matched the query for the given scope. This is normal for a new end user or a very specific query.

Related

  • Add a memory, store content and run the full write pipeline before searching for it.
  • Get context, the primary recall path: assembles a ready-to-inject prompt block from active typed facts plus the most relevant memories.
  • Delete a memory, forget a memory by id so it no longer appears in search results.
  • API reference, full endpoint contract including the GET /v1/memories/{id}/history and GET /v1/facts endpoints.