Unofficial, self-initiated prototype. This is a candidate demo I built on my own initiative for HomeRun (an Indian construction-materials Shopify store). It is not affiliated with, endorsed by, or commissioned by HomeRun. All catalog data was scraped from the public storefront; no private data was used.
Describe a project in plain language — "stop my bathroom leaking," "tile a 2BHK" — and HomeRun returns the specific SKUs that project actually needs, with quantities, and checks you out on the store's real cart.
▶ Try the live demo · the store it plugs into: home-run.co
The demo backend runs on a free Hugging Face Space that sleeps when idle — the first request may take ~30–60s to wake it, then it's fast.
The bet behind it: retrieval, not the LLM, is the product. A plain-language request is parsed into structured intent, answered from a governed catalog of 326 real scraped products / 1,363 variants, shaped into either a homeowner's quantity calculator or a contractor's staged bill-of-materials, and routed to HomeRun's actual cart via Shopify deep-links — so the demo ends in a real, buyable basket, not a mockup.
The LLM never writes a price or a SKU. Every retrieval change is gated by a standing eval suite. Retrieval wins have to be proven on blind, freshly-authored held-out cases before they ship — the same discipline a real search team lives by.
HomeRun sells cement, tiles, waterproofing, wiring, plywood, and sanitaryware. Its buyer's real problem isn't browsing — it's translation:
- A homeowner knows "my bathroom is leaking." They do not know the fix is a surface primer, an acrylic waterproof coating at the right coverage, and a membrane.
- A contractor tiling a 2BHK doesn't want a search box. They want a full bill-of-materials — adhesive, spacers, grout, tools — with bulk-priced bags and real quantities.
A keyword search box serves neither. This assistant closes the gap between how people describe a job and the SKUs the job requires.
The whole commerce loop — search → recommend → cart → checkout — has zero LLM dependency. The AI narration is additive; if the free model rate-limits, the loop still works.
| Entry point | What happens |
|---|---|
| Chat a project | Describe the job in plain language. At most one clarifying question. Then grounded SKUs with a short "why." |
| Photo / paste a list | Snap or paste a materials list. Vision OCR reads it, matches each line to a SKU, buckets as confident / ambiguous / no-match — never silently substituted. |
Both paths converge on the same variant-keyed cart and the same Shopify deep-link checkout.
The segment toggle drives both product ranking and tone over the same catalog:
-
Homeowner — plain-language-to-trade translation, a friendlier voice, gentler hand-holding, and a quantity calculator that writes its result back into the cart.
Plain language in, a staged kit out — "I need to tile my 2 bathroom apartment" becomes an adhesive → spacers → grout → tools kit, sized for the room and priced from the real catalog. The AI's one-line reasoning streams in on top; the product grid never waits for it. -
Contractor — trade language, a staged bill-of-materials that owns the screen, bulk pricing, and a per-line swap drawer with A-vs-B spec comparison.
Quantity-aware and honest about it — the kit totals only what's checked ("Add 3 items · ₹3,848"), blocks on a required choice ("grout not added — choose one"), and labels the sizing an estimate to confirm with your engineer rather than pretending to be exact. (The per-line swap drawer and A-vs-B spec compare live one tap deeper on each line.)
Every pick can explain itself — "Why this?" expands a grounded rationale ("the notched trowel and lay-tools to spread adhesive evenly and set the tiles level"), pulled from the catalog, never written by the LLM.
For items the store genuinely doesn't stock (curtain rods, wallpaper, AAC blocks), the system returns an honest "no results" instead of confidently recommending the wrong thing.
HomeRun's cart is cookie-bound and can't be written to directly. So the app keeps its own accurate cart and hands off through Shopify's native multi-line deep-link (home-run.co/cart/<variant_id>:<qty>,...), landing the exact items and quantities in the store's real cart.
The other way in — "I have a list" takes a photographed or pasted materials list (a contractor's scribbled BOM), with a plain-language note that a photo is read to match products but never stored or used for training.
Each line matched with its confidence shown — "Birla cement 50kg" resolves to a real SKU (₹440, 6% off) and is pre-checked; lower-confidence lines are left unchecked with "choose the right one," and anything it can't find is skipped, never silently substituted.
Plain-language query
│
▼
┌─────────────────────────────────────────────────────────┐
│ RETRIEVAL_V3 (default-on · one-flag rollback RETRIEVAL_V3=0) │
│ │
│ ontology spine + compositional query parse │
│ │ (deterministic, 429-proof, load-bearing) │
│ ▼ │
│ structured-hybrid retrieval │
│ governed-field filter + dense bge embeddings │
│ + BM25 lexical ──fused by Reciprocal Rank Fusion──▶ │
│ │ │
│ ▼ │
│ cross-encoder re-ranker (ms-marco-MiniLM-L-6-v2, CPU) │
│ │ │
│ ▼ │
│ calibrated answerability gate ──▶ state:"no_results" │
│ │ (abstains when catalog lacks head noun) │
│ ▼ │
│ optional free-model LLM slot-extractor tail │
│ (fires only on low spine-confidence / cache hit) │
└─────────────────────────────────────────────────────────┘
The two-call flow the UI depends on:
POST /api/search→ renders the segment-ranked product grid instantly from retrieval. Never blocks on the LLM.POST /api/answer→ swaps in the LLM one-liner asynchronously, with a templated fallback so the experience survives an LLM 429 / outage.
Deterministic-spine-first design. The ontology spine is fully load-bearing. The LLM only ever adds, never gates, and silently falls back to the spine on error or timeout. A safety-net floor makes the structured filter non-destructive, so a mis-parse can never empty the grid.
10 selection archetypes classify all 326 SKUs — BAGGED_DRY, LIQUID_CAN, COIL_RUN, PANEL_SHEET, MODULAR_FITMENT, CUT_LENGTH_SIZE, DISCRETE_UNIT, SPEC_DEVICE, ASSEMBLY_KIT, FINISH_VARIANT — so a 50 kg cement bag, a 100 m wire coil, a 20 L paint can, and a 4-pole MCB each get the right selection card and quantity model, instead of one generic card + one area×coverage rule.
React · Vite · Flask · Python · ChromaDB · RAG · Hybrid search (RRF) · Cross-encoder reranker · bge-small-en-v1.5 · BM25 · OpenRouter · Vision OCR · Shopify deep-link checkout · Express · Hugging Face Spaces · Vercel
Embeddings are local and free (BAAI/bge-small-en-v1.5, 384-dim) at index and query time. Chat/vision synthesis runs on OpenRouter free-tier models (Llama-3.3-70B primary + Qwen/Gemma fallback chain). The only secret is OPENROUTER_API_KEY, and the system degrades gracefully without it. See artifacts/architecture.md.
I refused to let the AI feature ship on vibes. "Good" is a data-driven eval suite, and retrieval gains have to be earned.
| Metric | Value | Source |
|---|---|---|
| Standing relevance eval | 14/14 passing (6 contractor / 8 homeowner), LLM-off deterministic path — on both V3 default and v2 rollback | evals/results.json |
| Blind fresh held-out | RETRIEVAL_V3 23/29 (79.3%) beats v2 baseline 19/29 (65.5%), 0 floor violations | docs/research/HELDOUT_EVAL_V3_FRESH.md |
| Confident-wrong on not-stocked items | 57% → 43% (V3 answerability gate vs v2) | docs/research/HELDOUT_EVAL_V3_FRESH.md |
| Held-out latency | 28 of 29 cases ~0.1s on the deterministic spine (LLM path engaged once, 19–31s) | docs/research/HELDOUT_EVAL_V3_FRESH.md |
| Catalog validation | 106 → 0 HARD violations (13 soft warnings remain) | data/catalog_health.json |
| Catalog scrape | 326 products / 1,363 variants / 147 collections | data/catalog_meta.json |
| Variant ID + image coverage | 100% each (326/326) | data/catalog_meta.json |
| Deep-links resolved live | 20/20 sampled (0 failures) | docs/PRODUCT.md |
| Bulk-price tiers | 25 real tiers (all BAGGED_DRY) after a fabricated pricing path was removed (246 → 25) |
docs/ROADMAP.md |
| Flow audit | 29 findings (5 critical / 7 high / 10 medium / 7 low); Tier A + B shipped | docs/ROADMAP.md |
The three disciplines that make those numbers trustworthy:
- Governed catalog as a hard gate.
catalog_schema.py+validate_catalog.pyschema-validatedata/products.jsonand abort classification on any HARD violation — bad data can't silently reach retrieval. - The LLM is walled off from ground truth. A permanent, code-verified no-regression rule guarantees the model never populates a product field. Every price, SKU, and title comes only from the catalog, so synthesis can't hallucinate commerce data.
- Blind held-out discipline. Before declaring a retrieval win, fresh cases are authored without seeing the fix — colloquial phrasings, misspellings (
geyzer,wall puty), Hindi trade terms (kabza,saria), and honest not-stocked probes. This caught RETRIEVAL_V3's first overfit and gated the flag-flip behind the safety-net floor.
Full breakdown in artifacts/eval-scorecard.md.
This is the case study for the parts of a product-org job you can't fake:
- Domain translation — I did competitive/UX recon on a live store and turned "my bathroom is leaking" into the right governed SKUs for two very different buyers.
- Eval-first rigor — I defined "good" as a data-driven suite, insisted retrieval gains be proven on blind held-out cases, caught my own V3 overfit, and gated the release behind a floor that guarantees the new pipeline is never worse than the old.
- Honesty as a product rule — for genuinely un-stocked items the assistant abstains ("no results") rather than confidently recommend the wrong thing, and I measured the residual confident-wrong rate (57% → 43%) and tracked it as open work.
- Shippable scoping — I shipped the commerce loop first with zero LLM dependency, and deferred GST-invoice export and telemetry rather than gold-plating.
- Running the team — I wrote a canonical
PRODUCT.mdspec with a frozen API contract and a permanent no-regression gate, then ran a team of specialist sub-agents (PM, backend, frontend, designer, QA, researcher, doc-syncer) against it.
There's also a packaged deliverable: a 13-slide recommendation deck plus a measurement workbook framing this explicitly as a candidate recommendation to HomeRun.
See artifacts/product-brief.md and artifacts/positioning.md.
Intellectual honesty is the point, so the open questions stay on the record:
- Post-deep-link conversion is unattributed. The demo lands a real basket, but once the handoff crosses into HomeRun's own cart, I have no visibility into whether it converts. Conversion attribution is unbuilt.
- The confident-wrong rate is down, not zero. 43% of genuinely not-stocked items still draw a confident answer instead of an abstention. That's tracked open work, not a solved problem.
- Catalog gaps are merchandising decisions, not bugs. Some "misses" are the store simply not stocking a thing; the assistant can't invent inventory.
- Eval scale is deliberately small. 14 standing golden cases + 29 blind held-out cases is enough to catch regressions and an overfit — it is not a production-scale relevance benchmark.
- 13 soft catalog warnings remain. HARD violations are at zero; the soft warnings are known and un-fixed.
- Single-store, single-domain. Nothing here claims to generalize beyond HomeRun's catalog and category.
README.md
artifacts/
product-brief.md — the problem, users, scope, what shipped vs deferred
positioning.md — who it's for, the wedge, the "retrieval is the product" bet
eval-scorecard.md — every real number, methodology, held-out design, open work
architecture.md — ADR-style: stack, the two-call flow, V3 pipeline, decisions
assets/
homeowner-results.png clarifier.png why-this-tellmore.png
contractor-bom.png bom-swap-drawer.png cart-checkout.png photo-list.png