A full-stack product search and recommendation system for a real furniture catalog. You search the catalog, select one of six recommendation strategies, and see how each strategy changes the results.
Open the live demo. The demo shows captured results for all six strategies side by side. It is a static page. To run the full system, see HOWTO.md.
| What it is | A search and recommendation system: React UI, GraphQL gateway, three Java microservices, a vector database, and a trained ranking model. |
| Data | The WANDS catalog from Wayfair: 42,994 real products, 480 queries, and 233,448 human relevance labels. |
| Scope | 6 recommendation strategies, an offline evaluation harness, and a training pipeline for the ranking model. |
| Engineering | Hexagonal architecture, gRPC between services, more than 100 Java tests, and a CI pipeline with 6 quality and security jobs. |
| Run it | One docker compose stack on a laptop. |
| You are a... | Read this | Time |
|---|---|---|
| Recruiter | Summary, What this project shows, and the live demo | 2 minutes |
| Hiring manager | What this project shows, Results, then docs/STRATEGIES.md | 10 minutes |
| Engineering manager | docs/ENGINEERING_PRACTICES.md and docs/DESIGN_DECISIONS.md | 15 minutes |
| Code reviewer | docs/ARCHITECTURE.md, which gives a reading order through the code | 30 minutes |
- System design. Each service uses a hexagonal (ports and adapters) structure. The business logic has no framework imports, so you can test it without Spring, a network, or Docker. The services communicate only through gRPC. A single GraphQL gateway is the only entry point for the frontend. See docs/ARCHITECTURE.md.
- Applied machine learning. A pairwise learning-to-rank model (XGBoost) learns from clickstream data and runs in Java through ONNX Runtime. A test makes sure that the Java features and the Python training features stay identical. See training/TRAINING.md.
- Honest measurement. An offline harness scores each strategy against two independent ground truths. It also calculates an off-policy (IPS) reward estimate. The results include the negative findings. See docs/EVALUATION.md.
- Production practices. CI runs tests, dependency and container vulnerability scans, static analysis, secret scanning, and Dockerfile lint on each change. Dependabot proposes dependency updates. See docs/ENGINEERING_PRACTICES.md.
- Research-based design. Each strategy comes from a published paper or an engineering article from a company such as Netflix, Spotify, Etsy, or Pinterest. See docs/REFERENCES.md.
flowchart LR
ui[React UI] -- GraphQL over HTTP --> gw[graphql-gateway]
gw -- gRPC --> ss[search-service]
gw -- gRPC --> rs[recommender-service]
ss -- embed + vector query --> pc[(Pinecone Local)]
rs -- similar-product query --> pc
rs -- loads at startup --> cs[(clickstream + catalog)]
rs -- scores candidates --> onnx[(ranking model, ONNX)]
- The user enters a query and selects a strategy in the React UI.
graphql-gatewaysends the query tosearch-service.search-servicefinds candidates with hybrid search. It combines vector search (Pinecone) and keyword search (BM25) with Reciprocal Rank Fusion.- The gateway sends the candidates to
recommender-service. The selected strategy re-ranks them and can add products. - The gateway returns the final list to the UI.
| Strategy | What it does |
|---|---|
| None | Returns the search results without change. This is the baseline. |
| Popularity | Combines search relevance with product popularity from the clickstream. |
| Collaborative filtering | Promotes products that other users used together with products that this user used. |
| Bandit exploration | Uses Thompson sampling across product categories to show more varied results. |
| Neural ranking | Scores each candidate with the trained XGBoost ranking model. |
| Diverse popularity | Applies Popularity, then spreads the results across categories (MMR re-ranking). |
For the algorithm, the design choices, and the sources for each strategy, see docs/STRATEGIES.md.
The evaluation replays 2,568 held-out user sessions through each strategy.
- On the clickstream metric, no strategy is better than the baseline. The baseline gets nDCG@5 0.505. The data set causes this result: its original result order already follows the human relevance grades.
- On the human relevance labels, Popularity is a little better than the baseline (nDCG@5 0.972 against 0.967). Collaborative filtering is also a little better.
- The ranking model has a known weakness. It has no feature that measures how well a product matches the query. Thus it has the lowest clickstream score.
These results are reported as measured. docs/EVALUATION.md explains the method and how to read the numbers. RESULTS.md has the full tables.
| Area | Technology |
|---|---|
| Frontend | React 19, TypeScript 7, Vite, Apollo Client |
| API | GraphQL (Spring GraphQL) |
| Services | Java 26, Spring Boot 4, gRPC, Protocol Buffers |
| Search | Pinecone Local (vector search), in-memory BM25, bge-small-en-v1.5 embeddings with ONNX Runtime |
| Machine learning | Python, XGBoost, ONNX |
| Build and run | Maven, npm, Docker Compose |
| CI and security | GitHub Actions, Trivy, Semgrep, Gitleaks, Hadolint, Dependabot |
You need JDK 26, Maven, Docker, Node.js, and Python 3.
./scripts/download-data.sh # 1. Get the catalog and clickstream data.
./scripts/download-models.sh # 2. Get the embedding model.
(cd training && pip install -r requirements.txt && python train_neural_ranker.py) # 3. Train the ranking model.
./scripts/start-services.sh # 4. Start all services.
./scripts/ingest-catalog.sh # 5. Load the catalog into Pinecone Local.Then open http://localhost:5173. HOWTO.md gives each step in detail, with a check for each step.
| Folder | Contents |
|---|---|
search-service/ |
gRPC service: hybrid vector and keyword search |
recommender-service/ |
gRPC service: the six strategies and the offline evaluation harness |
graphql-gateway/ |
The GraphQL API for the frontend, and a gRPC client of the two services |
rec-support/ |
Shared infrastructure: catalog file reader, embedding model, Pinecone client |
web/ |
React frontend |
proto/ |
gRPC contracts between the services |
training/ |
Python training pipeline for the ranking model |
scripts/ |
Scripts to download data, start services, and run the evaluation |
docs/ |
Project documentation and the static demo page |
| Document | Contents |
|---|---|
| docs/ARCHITECTURE.md | A tour of the code for reviewers: reading order, layers, and tests |
| docs/DESIGN_DECISIONS.md | The main technical decisions, the alternatives, and the trade-offs |
| docs/STRATEGIES.md | How each recommendation strategy works |
| docs/EVALUATION.md | The evaluation method and a summary of the results |
| RESULTS.md | The full evaluation tables, generated by the harness |
| training/TRAINING.md | How the ranking model is trained and evaluated |
| docs/ENGINEERING_PRACTICES.md | Tests, CI, security scans, and dependency management |
| docs/DATA.md | The data sets, their sizes, and their limits |
| docs/REFERENCES.md | The papers and articles behind each strategy |
| docs/PROJECT_STATE.md | Operational facts and known limitations |
| HOWTO.md | Step-by-step instructions to run the system |
- The clickstream data is synthetic. It is not real user behavior. See docs/DATA.md.
- Pinecone Local stores data only in memory and holds a maximum of 100,000 vectors. It is for development and demonstration only.
- The system has more service boundaries than a catalog of this size needs. This is intentional, to show service design. docs/DESIGN_DECISIONS.md explains this trade-off.
For the full list, see docs/PROJECT_STATE.md.