SmartStore AI — Phase 2 Implementation Guide

Product Catalog Ingestion (RAG)

This phase makes the product catalog from Phase 1 semantically searchable — the actual core of the entire product.

What got built

backend/app/
├── embeddings.py   — the ONE place embedding calls happen
├── qdrant.py        — shared Qdrant client + collection setup, swappable for tests
├── ingest.py        — reads Products from Postgres, embeds, upserts into Qdrant
└── retrieval.py      — query-time semantic search, scoped by store_id

backend/tests/test_ingestion.py

An important, real correction to the bootcamp material

While building and testing this phase against the actual installed qdrant-client (v1.18.0), I found that .search() — used throughout the bootcamp's Volumes 2, 3, and 5 — is deprecated in favor of .query_points(). The new method takes query= instead of query_vector=, and returns a QueryResponse object whose results live under .points, not as a direct list. This implementation guide's actual code (app/retrieval.py) uses the current, correct method — verified by actually running it, not assumed. If you go back to copy code from the earlier bootcamp volumes directly, swap .search(...) for .query_points(...) and access result.points instead of iterating result directly.

Key design decisions

embed_fn and qdrant_client are injectable parameters on every function (ingest_products, retrieve), defaulting to the real implementations. This isn't speculative "clean code" — it's exactly what made it possible to actually test this phase's real logic without a live OpenAI key or a running Qdrant server, using a fake embedder and Qdrant's embedded local mode instead. Production code never passes these explicitly; tests always do.

product_to_text() is the single definition of "what text represents a product." Phase 3's grounded prompt reads the same payload["text"] field this function writes — if you ever change what gets embedded, this is the one function to change, and the one place to check that retrieval and generation haven't drifted apart.

Verified test results

Using a deterministic fake embedder (bag-of-words hashing, not a real semantic model — see the test file for why that's still a meaningful test) against Qdrant's embedded local mode:

tests/test_ingestion.py::test_ingest_and_retrieve_end_to_end PASSED
tests/test_ingestion.py::test_retrieval_respects_store_scoping PASSED

The second test is the one that actually matters most: it ingests an identically-named product into two different stores, then confirms that querying as Store A never returns Store B's product, regardless of semantic similarity — Volume 2, Chapter 8's metadata filtering, proven, not just described.

Running this for real (with a real OpenAI key)

export OPENAI_API_KEY="sk-..."
python -m app.ingest
# Ingested 5 products into Qdrant.

This step does require your real API key and a running Qdrant instance (docker compose up from Phase 0) — it wasn't run live in this sandbox since no real key was available here. Everything else about this phase was verified for real; this specific live-API call is the one piece to verify yourself on your first run.

What's next

Phase 3 — Core RAG Query Pipeline builds the real /ask endpoint: takes a user's question, calls retrieve() from this phase, builds a grounded prompt, and calls the Claude API for a real answer.