Howwise RSS
AI & Automation5 min read

LangChain Chroma from_documents Metadata: Attach, Reload, Filter

Short answer

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.

Key takeaways
  • 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.

Tip: Use the langchain_chroma package (pip install langchain-chroma) for new code. Older tutorials import from langchain.vectorstores or langchain_community.vectorstores; those still exist in many versions but are deprecated and lag behind chromadb changes.

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())
Warning: Re-running this script adds the chunks again and creates duplicates, because IDs are random UUIDs by default. Pass ids=[...] with deterministic values (for example f"{source}-{i}") or delete the directory first. Deleting a directory is irreversible.

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.

SymptomLikely causeFix
count() is 0 after reloadcollection_name differs (default is 'langchain')Pass the same collection_name used at build time
count() is 0, no chroma.sqlite3 where expectedRelative path resolved from another working directoryUse an absolute persist_directory
Collection exists but results are nonsenseDifferent embedding model on reloadUse the identical embedding model
Dimension error on queryEmbedding size differs from stored vectorsMatch the model, or rebuild into a new collection
get() result has no embeddingsEmbeddings are omitted by default; only documents and metadatas are returnedPass 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.

  1. Confirm the directory contains chroma.sqlite3 and run the list_collections check below.
  2. Create Chroma(...) with the exact collection_name, persist_directory and embedding function.
  3. Call vectordb._collection.count() and expect the number of chunks you stored.
  4. 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))
Tip: get() returns documents and metadatas by default, so you do not have to request them. Embeddings are the only field omitted; they come back only when you pass include=['embeddings']. If the text looks missing, check count() and your collection_name first.

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.

ApproachExampleFilterTrade-off
Comma-joined string{'species': 'dog,cat'}Exact match only; substring needs post-filteringSimple, weak filtering
One boolean key per item{'species_dog': True, 'species_cat': True}filter={'species_dog': True}Best for fixed vocabularies
Separate chunks per itemOne Document per speciesfilter={'species': 'dog'}More storage, duplicated text
Strip unsupported valuesfilter_complex_metadata(docs)None for dropped keysLoses 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.

Related topics:langchainchromadbragvector-storemetadata