Skip to content
Forward Deployed

Forward Deployed
Lessons and contents
Vector Stores & Index OperationsLesson 3 of 7
7 lessons
Learn

Embedded stores: Chroma and FAISS

Source
From the RAG & Agents Engineering course
By
Sourav Banerjee
Last updated
9 Nov 2026

Report an issue

Chroma or FAISS

Both run inside your Python process with no server. They differ in what they do beyond the search:

Chroma FAISS (LangChain wrapper)
Index HNSW, l2 space by default Exact IndexFlatL2 from from_documents; FAISS itself also has HNSW and IVF indexes
Persistence Writes to persist_directory as it goes save_local / load_local, metadata in a pickle
Metadata filter Runs inside the store, before ranking; returns every match up to k Runs in Python on fetch_k=20 candidates after ranking
Filter syntax One key per dict; combine with $and / $or. {"dept": "HR", "year": 2026} raises ValueError A dict of values or operators ($eq, $gt, $in, …), or a callable on the metadata
Read by ID get(ids=...) Through the docstore only
Good for Local development and small production indexes you update Static, read-heavy corpora where search speed matters

Table 2: Chroma and FAISS compared.

The filter difference shows up as missing results. In a check run for this page, a FAISS store held 25 chunks tagged t="no" that sat close to the query and 3 tagged t="yes" further away. similarity_search(q, k=3, filter={"t": "yes"}) returned 0 results, because none of the 3 was among the 20 nearest candidates; with fetch_k=28 it returned all 3. Chroma, asked for k=5 with a filter only 2 entries match, returned those 2. We call the FAISS case a starved post-filter: the filter runs on too few candidates. The same check in table form:

Store and query Matching entries Returned
FAISS, filter t="yes", k=3, default fetch_k=20 3, none among the 20 nearest 0
FAISS, same filter, fetch_k=28 3 3
Chroma, a filter matching 2 entries, k=5 2 2

Table 3: A planted selective filter against both stores, from a check run for this page.

The notebooks' guidance: start with Chroma for anything you might keep, choose FAISS for a static corpus you search often, and move to a hosted store (lesson 3 of T5.5) when you need concurrent writers or more than one machine.

In practice: Larkspur's embedded store

For the handbook on one machine, the choices follow from Tables 1 to 3:

  1. Use Chroma for the handbook index, because it is updated nightly and filters inside the store.
  2. Measure recall@k of the HNSW index against an exact search on the golden set once the corpus grows, and raise ef_search until the misses stop mattering.
  3. Raise fetch_k, or filter elsewhere, for any selective filter on a FAISS archive.
  4. Move to a hosted store only when a second writer or machine arrives (lesson 3 of T5.5).

Next: lesson 3 of T5.5 moves the index off your machine, to managed stores, and looks at what decides which entries one query can see.

Run the notebooks

The code for this lesson is in these notebooks. Colab runs one in your browser: install the packages its first cell imports and set the API keys it reads. To run every notebook as written, clone the repository and use its uv environment.

Notebook GitHub Colab
Chromadb Open on GitHub Open in Colab
Faiss Open on GitHub Open in Colab
Vector stores in practice (optional) Open on GitHub Open in Colab

Key Takeaways

  • ANN indexes such as HNSW and IVF trade the guaranteed top k for speed, and a miss raises no error. Measure recall@k against exact search and raise ef_search until misses stop mattering.
  • Chroma filters inside the store before ranking; the FAISS wrapper filters after ranking fetch_k=20 candidates, so a selective filter can return nothing.
  • Start with Chroma for anything you will update, use FAISS for a static read-heavy corpus, and move to a hosted store for concurrent writers or several machines.

Check yourself

1. Why can an approximate nearest-neighbour index miss the right chunk without anyone noticing?

An ANN index visits only a small part of the collection and gives up the guarantee of the true top k. A miss raises no error: the index returns the next-best chunk with a normal-looking score, so a recall drop shows up as worse answers. Measure recall@k against an exact search on your own queries, and raise ef_search until the misses stop mattering for the golden set.

2. A FAISS store holds 25 chunks tagged no near the query and 3 tagged yes further away. A search filtered to yes with k of 3 returns nothing. Why?

The FAISS wrapper filters in Python after ranking, on the fetch_k=20 nearest candidates, and none of the 3 matching chunks was among them. With fetch_k=28 it returns all 3. Chroma filters inside the store before ranking, so it returns every match up to k.

3. Your index needs several writers at once across more than one machine. Is Chroma or FAISS the right choice?

Neither. Both run inside your Python process with no server, so the notebooks' guidance is to move to a hosted store (lesson 3 of T5.5) when you need concurrent writers or more than one machine. On one machine, choose Chroma for anything you might keep and update, and FAISS for a static corpus you search often.

Rate your confidence and keep notes on the module page.