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.