How to Power RAG Search With a Vector Database
RAG retrieval has two steps: embed every document once at ingest time, then embed each query and find the closest matches by vector similarity. Here's how to wire that loop with ChromaDB and a local embeddings model.
The two-step retrieval loop
RAG (Retrieval-Augmented Generation) retrieval works the same way regardless of which vector store you use:
- Index - call an embeddings model for each document and store the resulting vector alongside the document text.
- Query - embed the incoming query with the same model, then ask the vector store for the top-k documents whose vectors are closest (by cosine similarity) to the query vector.
The LLM that generates the answer is downstream of this retrieval step - retrieval quality is what makes or breaks the feature.
Indexing documents into ChromaDB
import json, urllib.request, chromadb
EMBED_URL = "http://ollama:11434/api/embeddings"
EMBED_MODEL = "nomic-embed-text"
_client = chromadb.EphemeralClient()
_collection = _client.get_or_create_collection("docs")
def _embed(text: str) -> list[float]:
req = urllib.request.Request(
EMBED_URL,
data=json.dumps({"model": EMBED_MODEL, "prompt": text}).encode(),
headers={"Content-Type": "application/json"},
)
with urllib.request.urlopen(req, timeout=60) as r:
return json.loads(r.read())["embedding"]
def index_docs(docs: list[str]) -> None:
for i, doc in enumerate(docs):
vec = _embed(doc)
_collection.add(ids=[str(i)], embeddings=[vec], documents=[doc])
chromadb.EphemeralClient() keeps the index in memory - swap it for
chromadb.PersistentClient(path="/data/chroma") when you need the index to survive
a restart. For production scale, the same pattern works with OpenSearch, pgvector, or
Pinecone - only the client and add/query calls change.
Querying for the top-k results
def search(query: str, k: int = 3) -> list[str]:
qv = _embed(query)
res = _collection.query(query_embeddings=[qv], n_results=k)
return res["documents"][0] # list of doc strings, ranked by similarity
collection.query returns results ranked by cosine distance - the closest vectors
first. res["documents"][0] is the ranked list for the first (and only) query vector.
Putting it together
DOCS = [
"Kubernetes orchestrates containers across a cluster of nodes.",
"PostgreSQL is a powerful open-source relational database engine.",
"Redis is an in-memory key-value store often used for caching.",
]
index_docs(DOCS)
for q in ("container orchestration", "in-memory caching"):
print(f"Q: {q}")
for doc in search(q):
print(f" -> {doc}")
The query "in-memory caching" surfaces the Redis doc first, even though the word
"caching" doesn't appear in every candidate - that's semantic similarity at work.
What to tune in production
- Chunk size - long documents should be split (512-1024 tokens per chunk) so retrieved passages are actually relevant to the query.
- Hybrid search - combine vector search with BM25 keyword search and merge the ranked lists (reciprocal rank fusion) to catch exact-match terms that embeddings miss.
- Re-ranking - pass the top-20 candidates through a cross-encoder to reorder them before sending the top-3 to the LLM.
- Eval - measure hit rate at k and mean reciprocal rank against a labeled query set so retrieval regressions are caught before they affect answer quality.
Want to try it hands-on? HeyDevJob gives you this exact setup in a live cloud workspace in your browser - edit it, run it, and see it work. Free, nothing to install.
Try it in a workspace →What you'll practice
- Embedding documents with nomic-embed-text and indexing them into ChromaDB
- Embedding a query and retrieving top-k results by cosine similarity
- Structuring the index/search loop so the same model handles both sides
FAQ
How does RAG vector database search work?
You embed every document with an embeddings model and store the vectors in a vector database. At query time you embed the question with the same model and retrieve the top-k documents whose vectors are nearest by cosine similarity. Those documents are injected into the LLM prompt as context.
How do I use ChromaDB for RAG retrieval in Python?
Create an EphemeralClient collection, call collection.add(ids=[...], embeddings=[...], documents=[...]) at ingest time, then call collection.query(query_embeddings=[qv], n_results=k) to retrieve the top-k results. Use the same embeddings model for both indexing and querying.
Which embeddings model should I use for RAG?
nomic-embed-text is a strong open-source choice for English text - it runs locally via Ollama and produces 768-dimensional vectors. For production you can also use OpenAI text-embedding-3-small or text-embedding-3-large. The key rule: always use the same model for indexing and querying or the similarity scores become meaningless.
Keep learning
Learn it by doing. Open this in a live cloud workspace, make the change yourself, and keep a record of the work you can share.
Open the workspace →