Skip to main content

retriever.py - Vector Search

The retriever is the heart of our RAG pipeline. It searches ChromaDB for semantically similar dishes based on user queries.

Complete Source

backend/app/rag/retriever.py
import chromadb
from chromadb.config import Settings as ChromaSettings
from app.core.config import settings


def get_chroma_client():
return chromadb.HttpClient(
host=settings.chroma_host,
port=settings.chroma_port,
settings=ChromaSettings(anonymized_telemetry=False),
)


def get_collection():
client = get_chroma_client()
return client.get_or_create_collection(
name=settings.chroma_collection,
metadata={"hnsw:space": "cosine"}
)


def search_foods(
query: str,
allergies: list[str] | None = None,
dietary_type: str | None = None,
cuisines: list[str] | None = None,
spice_level: str | None = None,
meal_type: str | None = None,
n_results: int = 5,
) -> list[dict]:
collection = get_collection()

where_filters = []

if allergies:
for allergen in allergies:
where_filters.append({"$not": {"allergens": {"$contains": allergen}}})

if dietary_type:
where_filters.append({"tags": {"$contains": dietary_type}})

if spice_level:
where_filters.append({"spice_level": spice_level})

if meal_type:
where_filters.append({"meal_type": meal_type})

where = None
if where_filters:
if len(where_filters) == 1:
where = where_filters[0]
else:
where = {"$and": where_filters}

try:
results = collection.query(
query_texts=[query],
n_results=n_results,
where=where,
include=["documents", "metadatas", "distances"],
)

foods = []
if results["documents"] and results["documents"][0]:
for i, doc in enumerate(results["documents"][0]):
metadata = results["metadatas"][0][i] if results["metadatas"] else {}
foods.append({
"id": results["ids"][0][i],
"content": doc,
"metadata": metadata,
"score": 1 - results["distances"][0][i] if results["distances"] else 0,
})

return foods
except Exception as e:
print(f"ChromaDB search error: {e}")
return []

Architecture Overview

ChromaDB Connection

def get_chroma_client():
return chromadb.HttpClient(
host=settings.chroma_host, # "localhost" or "chroma" in Docker
port=settings.chroma_port, # 8000
settings=ChromaSettings(anonymized_telemetry=False),
)

Connection types:

  • HttpClient: Connects to ChromaDB server (our approach)
  • PersistentClient: Local persistence to disk
  • Client(): In-memory (ephemeral)

Collection Configuration

def get_collection():
client = get_chroma_client()
return client.get_or_create_collection(
name=settings.chroma_collection, # "foods"
metadata={"hnsw:space": "cosine"}
)

The hnsw:space setting controls distance calculation:

Filter Building

where_filters = []

# Exclude allergens
if allergies:
for allergen in allergies:
where_filters.append({"$not": {"allergens": {"$contains": allergen}}})

# Include dietary type in tags
if dietary_type:
where_filters.append({"tags": {"$contains": dietary_type}})

# Exact match for spice level
if spice_level:
where_filters.append({"spice_level": spice_level})

Filter Examples

# User has dairy allergy
{"$not": {"allergens": {"$contains": "dairy"}}}

# User wants medium spice
{"spice_level": "medium"}

# Combined filters
{
"$and": [
{"$not": {"allergens": {"$contains": "dairy"}}},
{"spice_level": "medium"},
{"meal_type": "breakfast"}
]
}

Query Execution

results = collection.query(
query_texts=[query], # ["spicy breakfast"]
n_results=n_results, # 5
where=where, # Metadata filters
include=["documents", "metadatas", "distances"],
)

What happens internally:

Result Processing

foods = []
if results["documents"] and results["documents"][0]:
for i, doc in enumerate(results["documents"][0]):
metadata = results["metadatas"][0][i] if results["metadatas"] else {}
foods.append({
"id": results["ids"][0][i],
"content": doc,
"metadata": metadata,
"score": 1 - results["distances"][0][i] if results["distances"] else 0,
})

Why 1 - distance?

  • ChromaDB returns cosine distance (lower = more similar)
  • We want similarity score (higher = more similar)
  • score = 1 - distance converts it

Result Structure

Raw ChromaDB result:

{
"ids": [["food_1", "food_2", "food_3"]],
"documents": [["Masala Dosa is...", "Upma is...", "Poha is..."]],
"metadatas": [[{"name": "Masala Dosa"}, {"name": "Upma"}, {"name": "Poha"}]],
"distances": [[0.15, 0.23, 0.31]]
}

Formatted result:

[
{"id": "food_1", "content": "Masala Dosa is...", "metadata": {"name": "Masala Dosa"}, "score": 0.85},
{"id": "food_2", "content": "Upma is...", "metadata": {"name": "Upma"}, "score": 0.77},
{"id": "food_3", "content": "Poha is...", "metadata": {"name": "Poha"}, "score": 0.69},
]

Next, let's look at the OpenAI client.