How should you design a schema for a memory system?

Short answer: Define properties and collections around how memory will be filtered and searched, not only what you can store.

Auto-inferred schemas are fine for prototypes but risky in production when typos create silent new fields. Put together properties that are queried together; split collections when types and access patterns diverge. Formal cross-collection links cost resolution time, so denormalizing a name or id is often enough. Over-rigid required fields can break real memory variance. Engram’s schema support lets teams shape structure to real search use.

Knowing what objects, collections, and schemas are, as the previous chapter covered, doesn’t automatically tell a team how to actually use them well. Schema design is the practical discipline of turning that structural vocabulary into decisions: which properties a collection should actually carry, how many separate collections a system genuinely needs, and how much rigidity to impose upfront without making the whole system brittle later.

Why Does Explicitly Defining a Schema Beat Letting It Be Inferred Automatically?

Many underlying data platforms, Weaviate included, can infer a schema automatically from whatever data happens to arrive, creating properties on the fly as new fields show up. This is genuinely convenient for early prototyping, but it carries a real risk once a system moves toward production: a small typo in a property name, or an unexpectedly formatted value, can quietly create an unintended new property instead of raising an error someone would actually notice. An explicitly defined schema catches exactly this kind of mistake immediately, rejecting malformed data at the point of ingestion rather than letting it silently corrupt how memory is organized until something downstream eventually breaks.

How Should a Team Decide Which Properties Actually Belong on a Given Collection?

A property belongs on a collection when it’s something a search or a filter genuinely needs to act on directly, a category, an identifier, a timestamp, anything a caller would reasonably want to narrow results by. Content that exists purely to be searched semantically, the actual substance of a memory, belongs in whatever property gets vectorized, while structured, filterable details belong in their own separate properties rather than being buried inside that same block of free text where a filter could never reliably reach them. Getting this split right up front saves considerable rework later, since a property that should have existed from the start but didn’t often means retroactively reprocessing everything already stored to backfill it.

Choosing the right data type for each property matters just as much as choosing which properties to include at all. A property meant for exact matching, an identifier or a code, behaves differently under search than a property meant for natural, searchable language, and picking the wrong type can quietly break the kind of filtering or matching a system was actually counting on.

How Many Separate Collections Does a Memory System Actually Need?

Content that genuinely needs to be searched together, compared against the same query in one pass, belongs in one shared collection, since collections don’t share a vector space and can never be directly compared against each other in a single similarity search. Content serving a fundamentally different purpose, with a genuinely different property structure, is a reasonable candidate for its own separate collection instead. But collections aren’t free to multiply without cost, each one carries its own indexing and configuration overhead, and creating far too many small, narrowly scoped collections can actually hurt performance and memory usage rather than helping organize things more cleanly.

Should Relationships Between Different Kinds of Memory Be Modeled as Formal Links, or Handled More Simply?

Formal cross-references between collections are available, letting one object point directly at another, related object elsewhere in the system. But resolving a cross-reference at query time carries a real performance cost, since looking up the reference is roughly as expensive as looking up both connected objects individually, and that cost compounds when a relationship might point toward many different objects rather than just one or two. A simpler alternative, denormalizing the relevant detail directly onto the object that needs it, storing a name or an identifier redundantly rather than following a formal link, often serves a system’s actual needs just as well while avoiding that ongoing resolution cost entirely.

Is a Rigid Schema Ever Actually the Wrong Choice for a Memory System?

A schema that’s too rigid, demanding every single property be populated for every object regardless of whether it actually applies, forces awkward, meaningless placeholder values into memories that genuinely don’t have anything relevant to put there. The right degree of rigidity keeps the properties that matter uniformly across every object in a collection mandatory, while letting properties that only sometimes apply remain optional, populated only when a specific memory actually has something meaningful to say about them. This balance matters considerably for memory specifically, since real, accumulated knowledge is naturally uneven, rarely fitting a single, identically-shaped template across every single thing worth remembering.

How Does Weaviate Engram’s Underlying Schema Support Let a Team Apply These Design Principles Directly?

Weaviate Engram runs on top of Weaviate’s explicit schema and property configuration, letting a team define exactly which properties matter for their specific memory content rather than accepting a one-size-fits-all structure. Consider a regional theater company’s production-archive assistant, tracking notes across many different shows, each with its own cast, crew, and rehearsal history:

from engram import EngramClient

client = EngramClient(api_key=os.environ["ENGRAM_API_KEY"])

client.memories.add(
    "During tech week for the spring production, the fly system's counterweight rigging needed a last-minute adjustment after the backdrop's weight was recalculated, delaying the cue-to-cue rehearsal by roughly ninety minutes.",
    properties={"production_id": "spring-production-2026", "department": "technical"},
)

Because the department property was deliberately included in the schema as a distinct, filterable field rather than left buried inside the note’s free text, a stage manager reviewing only technical department history can scope a search precisely, without technical notes getting mixed in with costume, casting, or front-of-house memories that happen to reference the same production:

results = client.memories.search(
    query="Were there any rigging or fly system delays during tech week?",
    properties={"production_id": "spring-production-2026", "department": "technical"},
)

A schema that instead buried the department distinction inside unstructured text, rather than as its own dedicated property, would leave this kind of precise, department-scoped filtering unreliable, forcing every search to hope the semantic match happened to land on the right department rather than guaranteeing it through an actual filter. This is exactly the value deliberate schema design delivers: a structure shaped around how the theater company’s staff actually needs to search their own accumulated production history, rather than a generic structure that happens to technically hold the data without actually serving how it gets used.

Schema design is about shaping memory’s structure to fit how it actually needs to be searched and filtered. A related but distinct question sits just beneath it: whether a given piece of memory content should be captured as tightly structured, well-defined properties in the first place, or left as loosely organized free text that a search has to interpret more directly. Our next chapter, What is the difference between structured and free-text memories?, takes up exactly that distinction.