Guides

Searching Memory

Natural-language queries, thresholds, scoping, and score interpretation.

search() takes a natural-language query and returns ranked memories.

Basic usage

results = client.search("what does the user prefer?")

for r in results.results:
    print(r.score, r.content)

Parameters

client.search(
    query,                              # required, natural language
    *,
    actor_id=None,                      # optional — filter to one actor
    session_id=None,                    # optional — only this conversation
    exclude_session_id=None,            # optional — every conversation but this one
    limit=10,                           # max results (default 10)
    threshold=0.0,                      # minimum similarity (default 0.0)
    include_source_events=False,        # attach source events to results
)

query

Plain English. You don't need to phrase it like a keyword search — "things the user mentioned about their dog" works.

actor_id / actorId

Pass it to scope the search to one user/agent. Omit it to search across every actor in the organization. In most user-facing flows you want to pass the current actor.

session_id / sessionId

Scope the search to a single conversation. Pass the same conversation id you used when ingesting those events — session_id is required at ingest, so you already have it.

Search accepts a narrower set of ids than ingest does. Searchable ids may contain only letters, digits and _ - : . @ /, up to 256 characters. Ingest checks the length but not the character set — so an id containing a space, #, +, ~, an emoji or any non-ASCII character stores fine and returns a 422 the first time you search it. This bites whenever a conversation id comes from something a human wrote: a chat title, a thread name, a channel description. If you don't control the id's shape, slugify it before ingest rather than finding out at search time.

exclude_session_id / excludeSessionId

The mirror image: return everything except one conversation. Useful for "what have we discussed before?" when you want previous conversations and not the one you're already in.

Three ways to ask, then:

You wantSend
only this conversationsession_id
every conversation but this oneexclude_session_id
this one and the previous onesneither — the unchanged default

Three things to know:

One conversation at a time. Both fields take a single id, not a list — unlike role_ids / team_ids alongside them. Passing a list returns a 422. There is no way to ask for several named conversations in one search; run one search per conversation, or use exclude_session_id if what you want is "all but one".

Memories that belong to no conversation always come through. Promoted role, team and org knowledge is stored without a conversation on purpose — it's general, not from one chat. Both filters let it through, so scoping a search never hides what Memsy knows about the actor generally.

The two are mutually exclusive. Sending both non-empty returns a 422. They don't cancel out into "search everything" — for that, send neither.

An empty string is not the same as sending both. The server trims each id and treats an empty result as unset, and it does that before the mutual-exclusion check. So session_id: "" together with exclude_session_id does not error — it runs as a plain exclude search. Something like session_id=current or "" therefore scopes differently from what you meant, with a 200 and no warning. Pass None / undefined when you have no conversation, never "".

limit

How many results to return. Memsy may return fewer if not enough pass the threshold. Valid range: 1–100.

threshold

A floor on the similarity score — results scoring below it are dropped. The SDK default is 0.0 (no filter), so out of the box you get the top limit results ranked by relevance.

Raise the threshold to cut noise. The right value depends on tier:

  • Free (no reranking): raw retrieval scores cluster in 0.0–0.1 even for strong matches. Stay near 0.0–0.05.
  • Pro and above (with Cohere reranking): scores are normalised to 0.0–1.0. A threshold around 0.3–0.5 becomes a meaningful "good match" floor; 0.8+ returns only near-exact matches.

Scores are not comparable across queries — 0.6 for query A is not the same thing as 0.6 for query B. Treat threshold as a relative tuning knob, not an absolute quality bar.

include_source_events / includeSourceEvents

When true, each result additionally includes the source events the memory was extracted from. Useful for showing provenance in a UI.

The result shape

results.results  # list[SearchResult]

r.id                # stable memory ID
r.content           # natural-language memory, e.g. "User prefers dark mode"
r.score             # float; higher = closer match
r.metadata          # dict | None — server-attached fields (type, kind, …)
r.strength          # float — reinforcement strength, bounded 0.0–5.0
r.session_id        # str | None — conversation it came from; None if it belongs to none
r.source_metadata   # list[dict] — user metadata from the originating event(s)

strength is a reinforcement signal bounded 0.0–5.0 by the platform's policy ceiling; it rises by a small amount each time a memory is returned by search() and decays slowly when unused. Not a probability — don't normalise it into the [0, 1] range.

session_id is the conversation the memory came from. It is absent when the memory belongs to no conversation — which is itself the signal that it's general knowledge rather than something said in one chat. That's what makes searching across conversations readable: send neither filter and you get everything, with each result saying where it came from, so you can tell "you mentioned this earlier today" from "you said this last week".

source_metadata carries the JSON metadata you passed at ingest, attached to the originating event(s) the memory was derived from. Up to 5 entries; each entry is {event_id, metadata: {...}} for valid JSON-object payloads or {event_id, raw: "..."} otherwise. Use it for tagging, filtering, or correlating back to your application's own records.

Reading usage and rate-limit info

Every response carries observability metadata:

results = client.search("preferences", actor_id="user_42")

if results.usage:
    print(f"Plan: {results.usage.plan}")
    print(f"Search queries: {results.usage.search_queries}/{results.usage.search_queries_limit}")

if results.rate_limit:
    print(f"Remaining this window: {results.rate_limit.remaining}")

See Usage and Rate Limits for every field.

Common patterns

Personalisation at prompt time

memories = client.search(
    query=user_message,
    actor_id=user_id,
    limit=5,
    threshold=0.4,
).results

system_preamble = "Relevant context:\n" + "\n".join(f"- {m.content}" for m in memories)

Cross-actor search (admin tools)

Leave actor_id/actorId unset and you'll search every actor in the org. Good for analytics dashboards and support tooling; almost never what you want in an end-user-facing agent loop.

Empty results

search() always returns a response object. If nothing passed the threshold, results.results is []. Handle that case in your UI.

Limitations

Search only returns memories that have finished processing. Very recent events may still be in the queue — see Async Processing.

session_id and exclude_session_id need a Memsy server that supports conversation-scoped search. Against an older server they are silently ignored — unknown fields are dropped, so you get a 200 and an unfiltered result set with no indication the scope wasn't applied.

Next

  • Run the same thing from asyncio (Python) → Async client.