LangChain Chroma from_documents Metadata: Attach, Reload, Filter
LangChain Chroma from_documents metadata works by reading each Document's .metadata dict, so set it before calling from_documents. Values must be str, int, float or bool, and an empty reload is almost always a collection_name or path mismatch.
- Set metadata on Document objects (or update doc.metadata after the loader) before calling Chroma.from_documents; it reads doc.metadata directly.
- Chroma metadata values must be str, int, float or bool, so convert lists to a comma string or one boolean key per item.
- An empty reload is nearly always a collection_name or persist_directory mismatch, because Chroma silently creates a new empty collection.
- Reload with the same persist_directory, collection_name and embedding function, then verify with vectordb._collection.count().
- Filter with filter={'species': 'dog'} or operators such as $in, $and and $or; $contains does not work on list metadata.
Does Chroma.from_documents support metadata?
Yes. Chroma.from_documents(documents, embedding, ...) takes each Document's page_content for embedding and its metadata dict for storage. Nothing extra is needed: put the keys on the Document and they are stored next to the vector.
Loaders such as TextLoader and DirectoryLoader already set metadata['source'] to the file path. You add custom keys by looping over the loaded documents and updating doc.metadata, or by building Document(page_content=..., metadata={...}) yourself.
Text splitters copy the parent document's metadata onto every chunk, so adding metadata before splitting propagates it to all chunks. Each chunk still gets its own ID.
How do I attach custom metadata and persist the store? (full example)
This end-to-end script loads text files, attaches per-file metadata from a dict, splits, stores and persists. Run it once to build the store.
from pathlib import Path
from langchain_community.document_loaders import DirectoryLoader, TextLoader
from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain_openai import OpenAIEmbeddings
from langchain_chroma import Chroma
PERSIST_DIR = str(Path("./chroma_db").resolve()) # absolute path
COLLECTION = "animal_notes"
meta_by_file = {
"dogs.txt": {"category": "mammal", "species": "dog"},
"cats.txt": {"category": "mammal", "species": "cat"},
}
docs = DirectoryLoader("./data", glob="*.txt", loader_cls=TextLoader).load()
for d in docs:
name = Path(d.metadata["source"]).name
d.metadata.update(meta_by_file.get(name, {"category": "unknown"}))
chunks = RecursiveCharacterTextSplitter(
chunk_size=800, chunk_overlap=100
).split_documents(docs)
embeddings = OpenAIEmbeddings(model="text-embedding-3-small")
vectordb = Chroma.from_documents(
documents=chunks,
embedding=embeddings,
collection_name=COLLECTION,
persist_directory=PERSIST_DIR,
)
print(vectordb._collection.count())Why does my persisted Chroma reload return zero records?
Chroma uses get-or-create semantics. If you open a persist directory with a collection name that does not exist, it quietly creates a new empty collection and you see 0 records. Nothing errors, so the mismatch is easy to miss.
from_documents uses the collection name 'langchain' unless you pass collection_name. If you built the store with one name and reload with the default (or the reverse), you get an empty store. The same happens with a relative path run from a different working directory, or a raw chromadb client that wrote to a different path or name.
With chromadb 0.4 and later, persistence is automatic when persist_directory is set. .persist() is deprecated or removed, so you do not need it, and calling it is not the fix.
| Symptom | Likely cause | Fix |
|---|---|---|
| count() is 0 after reload | collection_name differs (default is 'langchain') | Pass the same collection_name used at build time |
| count() is 0, no chroma.sqlite3 where expected | Relative path resolved from another working directory | Use an absolute persist_directory |
| Collection exists but results are nonsense | Different embedding model on reload | Use the identical embedding model |
| Dimension error on query | Embedding size differs from stored vectors | Match the model, or rebuild into a new collection |
| get() result has no embeddings | Embeddings are omitted by default; only documents and metadatas are returned | Pass include=['embeddings'] if you need the vectors |
How do I reload the collection and verify it has data?
Reload by constructing Chroma directly, with the same three settings as the build. First list what actually exists on disk, which tells you the real collection names.
- Confirm the directory contains chroma.sqlite3 and run the list_collections check below.
- Create Chroma(...) with the exact collection_name, persist_directory and embedding function.
- Call vectordb._collection.count() and expect the number of chunks you stored.
- Call vectordb.get(include=['metadatas','documents'], limit=3) to inspect contents.
import chromadb
from langchain_chroma import Chroma
client = chromadb.PersistentClient(path=PERSIST_DIR)
print(client.list_collections()) # names (or Collection objects, by version)
vectordb = Chroma(
collection_name="animal_notes",
persist_directory=PERSIST_DIR,
embedding_function=embeddings, # same model as at build time
)
print(vectordb._collection.count())
print(vectordb.get(include=["metadatas", "documents"], limit=3))Why does Chroma reject a list as a metadata value?
Chroma metadata values must be str, int, float or bool. A value such as {'species': ['dog', 'cat']} raises a ValueError (expected metadata value to be a str, int, float or bool) or fails on insert. Chroma has no array type to index, so there is no list filter.
Pick one workaround based on how you filter.
| Approach | Example | Filter | Trade-off |
|---|---|---|---|
| Comma-joined string | {'species': 'dog,cat'} | Exact match only; substring needs post-filtering | Simple, weak filtering |
| One boolean key per item | {'species_dog': True, 'species_cat': True} | filter={'species_dog': True} | Best for fixed vocabularies |
| Separate chunks per item | One Document per species | filter={'species': 'dog'} | More storage, duplicated text |
| Strip unsupported values | filter_complex_metadata(docs) | None for dropped keys | Loses the list data silently |
from langchain_community.vectorstores.utils import filter_complex_metadata
for d in docs:
sp = d.metadata.pop("species_list", [])
d.metadata["species"] = ",".join(sp) # for display
for s in sp:
d.metadata[f"species_{s}"] = True # for filtering
docs = filter_complex_metadata(docs) # safety net How do I filter by metadata and update it later?
Pass a filter dict to similarity_search or to the retriever. Multiple keys need an explicit $and. Operators include $eq, $ne, $in, $nin, $gt, $gte, $lt, $lte, $and and $or.
$contains belongs to where_document (full text search on the content), not to metadata, so it will not match inside a list value.
To change metadata on stored items, fetch their IDs and call the collection's update, or use the vector store's update_document. Supplying ids= at insert time makes later updates and re-runs predictable. Alternatives to from_documents are Chroma.from_texts(texts, embedding, metadatas=[...]) and vectordb.add_texts or add_documents.
hits = vectordb.similarity_search("diet", k=4, filter={"species": "dog"})
hits = vectordb.similarity_search(
"diet", k=4,
filter={"$and": [{"category": "mammal"},
{"species": {"$in": ["dog", "cat"]}}]},
)
retriever = vectordb.as_retriever(
search_kwargs={"k": 4, "filter": {"species_dog": True}}
)
# update metadata in place
res = vectordb.get(where={"source": "data/dogs.txt"})
vectordb._collection.update(
ids=res["ids"],
metadatas=[{**m, "reviewed": True} for m in res["metadatas"]],
) Common questions
Can I use Chroma.from_texts instead of from_documents?
Yes. Chroma.from_texts(texts, embedding, metadatas=[{...}, ...]) takes a list of metadata dicts the same length as texts. It is useful when you already have raw strings and no Document objects.
Do I still need to call persist()?
No. With chromadb 0.4 and later, data is written to disk as it is added when persist_directory is set. persist() is deprecated or removed depending on the version, so calling it is unnecessary.
Can I add documents to an existing collection?
Yes. Reload with the same collection_name, persist_directory and embedding function, then call vectordb.add_documents(new_chunks, ids=new_ids). Use deterministic IDs so repeated runs do not create duplicates.
Why do I get a dimension error when querying a reloaded store?
The embedding model used at query time produces vectors of a different size than those stored. Reload with the original model, or rebuild the data into a new collection with the new model.
Does filtering happen before or after the vector search?
Chroma applies the metadata where clause during the query, so k results come from documents that match the filter. If fewer than k documents match, you get fewer results rather than unrelated ones.