Summary#
using Memory; turns on semantic search for an app: it adds a searchable content store, and the
Memory.Search("…") call returns the content most relevant to a natural-language query as a ranked
List<SearchHit>. The query is embedded for you — you write text, never vectors — and results come back ordered
by relevance. Without the using, the store and Memory.Search don't exist in the app; opting in is what makes
them available.
Signature#
using Osyrin.Memory;
List<SearchHit> Memory.Search(
string query, // natural-language text (embedded for you)
Entity[] about = null, // null/empty = the whole corpus; else memory ABOUT these entities
int limit = 20, // maximum hits
decimal threshold = 0.5, // maximum distance (closer = smaller)
string keyword = null, // optional keyword for hybrid ranking
Type[] of = null, // restrict to these entity TYPES
Entity[] by = null, // restrict to what these principals AUTHORED
MemoryKind[] kinds = null, // restrict to these kinds of memory
MemoryOrigin origin = null, // Derived (follows the data) or Authored (somebody chose it)
int related = 0) // also search what `about` is LINKED to, this many hops outDescription#
Semantic search finds content by meaning, not exact words: a search for "how do I get a refund" surfaces a
passage about returns and money back even with no shared keywords. You opt an app in with a single declaration:
using Osyrin.Memory;That brings three things into the app:
- a searchable content store — the app now holds a store of embeddable content. Opted-in apps may query it
directly like any other data, but
Memory.Searchis the ergonomic default and the one you'll reach for. Memory.Search(...)— the search call itself, described below.SearchHit— the result type each hit comes back as (see SearchHit).
None of this exists in an app that doesn't opt in — there is no store to write, nothing to search. The using is
the switch.
What fills the corpus#
Memory.Search searches a shared, cross-entity corpus — you don't write to it directly. Two authoring surfaces
contribute content to it:
[Searchable(Memory)]fields — aStringorMarkdownproperty marked for the corpus is chunked in and kept in sync as rows change (see [Searchable]). This is the usual way to make an entity's text findable.- the
semantic =>entity card — a composed template that embeds a rendered summary of a whole row into the corpus. Memory.Remember(file, about: row)— a stored file's text, filed against the record it is about. Unlike the two above it is not automatic: uploading a file indexes nothing until somebody asks for it (Memory.Remember / Memory.Forget).
For entity-local search over a single field — ranking an entity's own rows and controlling the ranking yourself —
use the field primitives in field search (Matches / TextScore / Similarity) instead; Memory.Search is the turnkey call across the whole
corpus.
The call#
Memory.Search(query, …) embeds query, ranks the stored content against it, and returns up to limit hits
best-first. Every argument past query is optional and tunes the search:
query(required) — the natural-language text to match. You never construct or pass a vector; the engine embeds the text.about— zero or more entities the memory should be about. Leave it out (or passnull/an empty list) to search the whole accessible corpus; pass one or more entities to restrict hits to their content. The list may mix entity types. This narrows the search itself, not the result — solimitapplies to the entities you named, and a hit about them is never crowded out by the rest of the corpus.limit— the maximum number of hits to return (default20).threshold— the maximum distance a hit may have; smaller is closer, so a lower threshold is stricter (default0.5).keyword— an optional keyword. When supplied, ranking fuses semantic similarity with a keyword match (hybrid search) so an exact term still pulls its content up. A row matching more of the keyword's words ranks higher; matching them all is not required.Pass a distinctive term, not the query.
keywordis for the word you know appears verbatim — a part code, a clause name,"warranty". Handing it the whole question makes retrieval worse, not better: a question is mostly ordinary words that appear everywhere, so the keyword half stops discriminating and dilutes the semantic half, which was already going to find the answer. If you have no particular term in mind, leavekeywordout.of— zero or more entity types, when you want (say) only deals and contracts.aboutnames instances;ofnames types, and the two combine.by— the principals whose memory to return. Only authored memory has an author — a conversation turn, a stated link, a file someone chose to remember — so this excludes derived content by construction.kinds— which kinds of memory to return: a chunk of a property, a section of a document, a conversation turn. Use it when a hit's kind changes what you would do with it.origin—MemoryOrigin.Derived(it follows the data — the chunks of a[Searchable]field, a document's sections) orMemoryOrigin.Authored(somebody chose it: a conversation turn, a stated link, a file passed to Memory.Remember / Memory.Forget).related— how many stated links to follow OUT fromabout, so the search also covers what those entities are linked to (0, the default, follows none; at most 3). It needsabout— following links out of "the whole corpus" would reach everything linked to anything, so asking for it without a starting point is a compile error. Each hit reached this way carries the link's words in SearchHit'sVia, and a direct hit leavesViaempty — see Memory.Link / Memory.Unlink for how those words get stated in the first place.
A related hit is marked, never re-ranked. Its Score means exactly what every other hit's Score means; the
fact that a link led you to it is a different claim about relevance, and blending the two into one number would
leave that number meaning neither. So you can show "found because it is similar" and "found because your deal
supersedes it" as the different things they are.
Results are always ranked — the list comes back ordered by relevance, most-relevant first — and always secured: a hit you aren't allowed to read never appears, exactly as with an ordinary query.
Memory.Search reads content and embeds text, so it is an effect (like a data read): its result is held
across a durable suspend/resume without re-running the search.
The result#
Each hit is a SearchHit carrying the matched text and how relevant it was — Content, Score
(higher = more relevant), SourceEntityId and SourceEntityType (which row, and which type, the content came
from), Kind (what sort of memory matched), Distance, and Via (the link that led here, when related did).
See SearchHit for the full shape.
Examples#
using Osyrin.Memory;
List<SearchHit> FindArticles(string q) {
return Memory.Search(q, limit: 5);
}using Osyrin.Memory;
entity Product { string Name; }
List<SearchHit> RelatedTo(Product p, string q) {
// restrict hits to this product's content, and let an exact keyword pull matches up
return Memory.Search(q, about: [p], limit: 10, threshold: 0.4, keyword: "warranty");
}using Osyrin.Memory;
entity Deal { [MaxLength(120)] string Title; [Searchable(Memory)] string Notes; }
List<SearchHit> AroundThisDeal(Deal d, string q) {
// this deal's own memory, plus memory about whatever it is linked to, one hop out.
// Anything reached that way comes back with `Via` filled in; anything found directly does not.
// The LEAD hit is still about this deal — a linked record widens what you get to read, it does not
// take over the answer. See [How retrieval works](/reference/memory/how-retrieval-works/) for what that costs and what it buys.
return Memory.Search(q, about: [d], related: 1, limit: 10);
}See also#
- Memory.Remember / Memory.Forget — putting a file's text into the corpus, and taking it back out
- Memory.Link / Memory.Unlink — stating the links
relatedfollows, and what their words mean at each end - SearchHit — the
SearchHitresult type each hit comes back as - [Searchable] — the
[Searchable]attribute;[Searchable(Memory)]feeds this corpus - field search (Matches / TextScore / Similarity) — entity-local field search (
Matches/TextScore/Similarity) when you want to rank one entity's own rows - Dynamic IN (list.Contains in a query) — filtering an ordinary query by a runtime set