Team: Prabuddha Tamhane (pat0216), Ojasv Issar (ojasv31)
Repository: https://github.com/UBC-MDS/Amazon_recommender_search_assistant
A retrieval-augmented generation (RAG) system for the Appliances category of the Amazon Reviews 2023 dataset. Given a natural language query (e.g. "energy efficient dishwasher under $500"), the system retrieves relevant products using BM25 keyword search and semantic embedding search, generates grounded answers via an open-source LLM, and presents results through a Streamlit web app.
We use Ollama with llama3.2:3b (Llama 3.2, Meta, 3 billion parameters) as our default LLM for RAG generation.
- Why Llama 3.2 3B: After comparing
qwen2.5:3bandllama3.2:3bon 5 identical queries with the same retrieved context, Llama 3.2 produced better-grounded answers with more citations and direct review quotes. Seeresults/final_discussion.mdfor the full comparison. - Why 3B: Good balance between answer quality and inference speed on laptop hardware (M2 Air, 16 GB RAM). Both 3B models run at ~15-25 tokens/sec locally.
- Why Ollama: Runs fully offline with no API key needed, making it easy for TAs and collaborators to reproduce.
- Alternative: The pipeline also supports HuggingFace Inference API (
Qwen/Qwen3.5-2B) if Ollama is unavailable -- select it in the app sidebar.
We use the Appliances category from the Amazon Reviews 2023 dataset (McAuley Lab, UCSD):
| File | Description |
|---|---|
Appliances.jsonl.gz |
User reviews -- ratings, review text, timestamps, helpful votes |
meta_Appliances.jsonl.gz |
Product metadata -- titles, descriptions, features, price, categories |
Key fields used for retrieval:
- Metadata:
title,description,features,price,average_rating - Reviews:
text,rating,verified_purchase,helpful_vote - Join key:
parent_asin
For EDA we work with a smaller subset; for the retrieval pipeline we download the full Appliances category using DuckDB and store it as parquet in data/processed/. Raw files go in data/raw/ and are not committed.
- Stream review and metadata files from the McAuley Lab servers using DuckDB
- Inspect the first 200 entries in the EDA notebook to understand schema, sample text, and missing values
- Convert full dataset to parquet format locally for fast repeated queries
- Join reviews with metadata on
parent_asin - Drop rows with missing review text or product title
- Aggregate to product-level documents (top 5 reviews per product by helpfulness)
Preprocessing details and justifications are documented in notebooks/milestone1_exploration.ipynb.
Lexical retrieval using rank-bm25 (BM25Okapi) through our retriever layer. Documents and queries are tokenized with lowercasing and basic punctuation removal. Returns top-k results ranked by BM25 score. Main implementation is in src/ranking.py with API helpers in src/bm25.py.
Persistence:
- Tokenized corpus and BM25 index are saved to
data/processed/bm25_index.pkl.
Uses sentence-transformers (all-MiniLM-L6-v2) when available, with a TF-IDF cosine-similarity fallback in this workspace. Main implementation is in src/ranking.py with API helpers in src/semantic.py.
Persistence:
- When sentence-transformers is available, a FAISS index and metadata are saved under
data/processed/semantic_faiss/.
The Step 2 RAG implementation is in src/rag_pipeline.py.
It includes:
- Semantic retrieval with top-k support (
retrieve,retrieve_indices) - Structured context builder (
build_context) - Prompt template variants (
strict,concise,analyst) - End-to-end generation flow (
answer) with open-source LLM backends
flowchart LR
A[User Query] --> B[Semantic Retriever]
B --> C[Top-k Documents k=5]
C --> D[Context Builder]
D --> E[Prompt Template]
E --> F[Open-Source LLM]
F --> G[RAG Answer with Citations]
from src.rag_pipeline import build_default_rag_pipeline
pipeline = build_default_rag_pipeline(
provider="ollama",
model="llama3.2:3b",
default_k=5,
)
result = pipeline.answer(
query="quiet dishwasher for a small apartment",
k=5,
prompt_variant="strict",
)
print(result.retrieved_indices)
print(result.answer)The Step 3 hybrid implementation is in src/hybrid_rag_pipeline.py.
simple-merge: concatenate BM25 and semantic top-k resultsmerge-dedup: merge and remove duplicatesrrf(default): Reciprocal Rank Fusion re-ranking
Default fusion weights are BM25 0.4 and semantic 0.6.
flowchart LR
A[User Query] --> B1[BM25 Retriever]
A --> B2[Semantic Retriever]
B1 --> C[Hybrid Fusion RRF]
B2 --> C
C --> D[Top-k Hybrid Context]
D --> E[Prompt Template]
E --> F[Open-Source LLM]
F --> G[Hybrid RAG Answer]
from src.hybrid_rag_pipeline import FusionConfig, build_default_hybrid_rag_pipeline
pipeline = build_default_hybrid_rag_pipeline(
provider="ollama",
model="llama3.2:3b",
default_k=5,
fusion=FusionConfig(mode="rrf", bm25_weight=0.4, semantic_weight=0.6),
)
result = pipeline.answer(
query="dishwasher that runs quietly at night",
k=5,
prompt_variant="strict",
)
print(result.retrieved_indices)
print(result.answer)The qualitative evaluation workflow lives in src/qualitative_eval.py and writes the step-4 report to results/milestone1_discussion.md.
It covers:
- A 10-query set spanning easy, medium, and complex query types
- Top-5 retrieval outputs for both BM25 and semantic search
- Side-by-side comparison for five selected queries
- A short discussion of strengths, weaknesses, and cases that may need reranking or RAG
Run the report generator after your processed corpus is available (defaults to the first matching parquet under data/processed/):
python -m src.qualitative_evalTo pin a specific file:
python -m src.qualitative_eval --data-path data/processed/appliances_merged.parquetThe Streamlit app is implemented in app/app.py with two tabs:
- Search Only (Milestone 1): keyword / semantic / hybrid retrieval with top-k results, ratings, scores, and feedback buttons
- RAG Mode (Milestone 2): hybrid RAG pipeline that generates an LLM answer above the retrieved source documents
LLM provider and model can be changed in the sidebar (defaults to Ollama llama3.2:3b).
DSCI_575_project_ojasv31_pat0216/
|-- README.md
|-- requirements.txt
|-- .env # API keys (not committed)
|
|-- data/
| |-- raw/ # downloaded .jsonl.gz (gitignored)
| |-- processed/ # cleaned parquet files (gitignored)
|
|-- notebooks/
| |-- milestone1_exploration.ipynb
| |-- milestone2_rag.ipynb
|
|-- src/
| |-- bm25.py
| |-- semantic.py
| |-- hybrid.py
| |-- rag_pipeline.py
| |-- hybrid_rag_pipeline.py
| |-- llm_pipeline.py
| |-- retrieval_metrics.py
| |-- data_io.py
| |-- ranking.py
| |-- feedback.py
| |-- qualitative_eval.py
| |-- download_full.py
| |-- utils.py
|
|-- scripts/
| |-- compare_llms.py # LLM comparison script (final submission)
|
|-- results/
| |-- milestone1_discussion.md
| |-- milestone2_discussion.md
| |-- final_discussion.md
| |-- llm_comparison.json # raw LLM comparison outputs
|
|-- app/
|-- app.py
git clone https://github.com/UBC-MDS/DSCI_575_project_ojasv31_pat0216.git
cd DSCI_575_project_ojasv31_pat0216python -m venv venv
source venv/bin/activate
pip install -r requirements.txtAlternative (conda + environment file):
conda env create -f environment.yml
conda activate dsci575-project# Install Ollama: https://ollama.com/download
ollama pull llama3.2:3bAlternatively, set a HuggingFace token in .env for the HF API fallback:
HF_TOKEN=your_huggingface_token_here
Open and run notebooks/milestone1_exploration.ipynb to explore a subset of the data and understand the schema.
python -m src.download_fullThis streams the full Appliances category (~2.1M reviews, ~94K products) via DuckDB and produces data/processed/appliances_products.parquet with product-level documents.
streamlit run app/app.pypython -m src.build_indexesThis creates:
data/processed/bm25_index.pkldata/processed/semantic_faiss/index.faissdata/processed/semantic_faiss/metadata.json
The app automatically loads these persisted indexes when present.
python -m src.qualitative_eval