Short answer: Treat a collection as a contract for properties, vectors, inverted indexes, and optional multi-tenancy—and let Engram groups and topics act as the product-facing schema.
Collections and schema design decide how memory objects are shaped, indexed, and isolated. A collection is not just a table name; get the contract wrong and you fight missing filters, bloated vectors, or schema changes that cannot rewrite history. Agent memory looks simple from the outside—store text, embed, search later—but every object still needs typed properties and searchable fields. This chapter covers which property and index choices matter for recall, how Engram groups and topics sit above that schema (user-scoped topics demand user_id; bounded topics keep one memory per scope; unbounded topics accumulate facts), and how schemas should evolve without breaking recall. Test like isolation: cross-user emptiness, property filters narrowing correctly, hybrid quality on real operator phrases including codes BM25 should catch.
Collections and schema design decide how memory objects are shaped, indexed, and isolated inside Weaviate. A collection is not just a table name. It is a contract for properties, vector settings, inverted indexes, and optional multi-tenancy. Get that contract right and recall stays fast as tenants grow. Get it wrong and you fight missing filters, bloated vectors, or schema changes that cannot rewrite history. This chapter explains how to think about memory collections, which property and index choices matter for agent recall, how Engram’s groups and topics sit above that schema, and what a careful write-and-search loop looks like when you treat schema as product design rather than an afterthought.
Why Does Memory Need an Explicit Collection Contract?
Agent memory looks simple from the outside. Store text. Embed it. Search later. Underneath, every object still needs typed properties, searchable fields, and a vector configuration. Weaviate stores those rules on the collection definition. Property names and data types are mostly fixed after create. You can add new properties later. You cannot casually reshape existing ones. That immutability is why memory teams design the schema before the first production write.
For memory workloads the collection usually holds short factual texts, topic labels, timestamps, and scope keys. Some fields should enter the embedding. Others should only filter or keyword-match. Skip vectorization on noisy ids so they do not pull semantic space off course. Keep content text searchable for BM25 and hybrid. Enable multi-tenancy when many users share one collection but must never share shards. The collection definition is where those decisions become enforceable instead of tribal knowledge in application code.
If the collection is the container, the next design question is which indexes each property should pay for.
Which Property and Index Choices Matter for Recall?
Weaviate maintains vector indexes for similarity and inverted indexes for filters and keyword search. On text properties, indexSearchable feeds BM25 and hybrid. indexFilterable feeds efficient equality-style filters. Tokenization choices change how codes and phrases split. A job number may want a stricter tokenization than free-form notes. Range filters on dates or numbers help when you prune by recency or score thresholds, but only if you enable them on those properties.
Vector index type should match tenant shape. Small tenants often do well with a flat index. Large tenants need HNSW. Multi-tenant memory with uneven sizes is a classic fit for a dynamic index that starts flat and promotes to HNSW past a threshold. Compression such as rotational or binary quantization reduces memory for large HNSW graphs when cost becomes the constraint. None of these knobs replace a clear property plan. They amplify the plan you already made for content, filters, and tenancy.
A practical rule is to index what you query. If operators never filter on a field, do not pay for every secondary index by default on huge estates. If hybrid recall depends on exact tokens in content, keep that field searchable. Memory schemas fail most often when everything is vectorized and nothing is filterable, or the reverse.
Application teams still need a higher-level vocabulary than raw collection JSON. Engram provides that layer for agent memory products.
How Do Engram Groups and Topics Act as the Memory Schema?
Engram does not ask every product engineer to hand-tune shard configs on day one. You configure groups and topics. A group bundles topics with a pipeline for one use case. Topics name the kinds of facts to extract and the scopes they require. User-scoped topics demand a user_id. Property-scoped topics add keys such as a job or conversation id. Bounded topics keep at most one memory per scope, which fits profiles and running summaries. Unbounded topics accumulate many facts over time.
That configuration is the product-facing schema. Underneath, Engram stores memories in Weaviate with multi-tenancy isolating users and groups. Your job is to choose topic descriptions that route facts cleanly, scopes that match real isolation needs, and groups that separate use cases that should not share pipelines. When you later search with topics and properties, you are using that schema as a retrieval contract, not as decorative metadata.
Seeing the contract in code makes the design choices concrete. Here is a sail loft scenario that depends on group, user, and property scope together.
What Does a Schema-Aware Engram Write Look Like in Practice?
A coastal rigging shop keeps loft notes per bench and per sail job. The Engram group is the use case. The bench is the hard user boundary. The job id is a soft property scope for summaries and job-specific facts.
import os
from engram import EngramClient, HybridRetrieval, Topic
client = EngramClient(api_key=os.environ["ENGRAM_API_KEY"])
bench = "sail-loft-2"
group = "rigging_shop"
job = "mainsail-reef-points-17"
run = client.memories.add(
"Bench 2 finished hand-sewing three reef points on mainsail job 17. "
"Use the heavier waxed twine on the middle point. "
"Customer wants the cringle offset 12mm toward the luff versus the old pattern.",
user_id=bench,
group=group,
properties={"job_id": job},
)
client.runs.wait(run.run_id)
# Narrow to this job's memories inside the loft bench tenant
job_hits = client.memories.search(
query="reef point twine and cringle offset on the mainsail",
user_id=bench,
group=group,
properties={"job_id": job},
topics=["UserKnowledge"],
retrieval=HybridRetrieval(alpha=0.45),
)
# Same bench, clear the job filter to search across all jobs on this loft
all_jobs = client.memories.search(
query="reef point twine and cringle offset on the mainsail",
user_id=bench,
group=group,
topics=[Topic(name="UserKnowledge", properties={"job_id": None})],
retrieval=HybridRetrieval(alpha=0.45),
)
assert any("cringle" in m.content.lower() for m in job_hits)
assert len(all_jobs) >= len(job_hits)
The call never invents a Weaviate class name. Engram’s group and scopes are the schema the agent sees. Hybrid retrieval still benefits from the underlying inverted and vector indexes Weaviate maintains on memory content. Property filters behave like the soft cuts you planned when you designed topics. Hard isolation still rides on user_id.
Schema work is not finished at create time. Memory products evolve, and you need a path that does not strand old objects.
How Should Memory Schemas Evolve Without Breaking Recall?
Prefer additive change. Add a property when a new filter becomes real. Add a topic or group when a new use case needs different extraction rules. Avoid renaming fields that already hold production memories. If a vectorizer or index choice was wrong, plan a new collection or named vector path and dual-read during migration instead of rewriting history in place. Document which fields are semantic, which are filter-only, and which are scope keys that must never be model-supplied.
Test the schema the way you test isolation. Write two benches. Confirm cross-user search stays empty. Write two jobs under one bench. Confirm property filters narrow correctly when present and broaden when omitted. Measure hybrid quality on real operator phrases, including codes like job numbers that BM25 should catch. Schema design for memory is successful when those tests stay boring as volume grows.
Our next chapter, What is Weaviate’s Model Context Protocol server?, shifts from how memory is shaped in the database to how agents discover and call Weaviate capabilities through MCP, so tools and schema stay aligned in the same runtime.