A multi-stage recommendation engine that takes natural language queries (e.g., "quiet cabin in the woods with a hot tub under $200/night") and returns the best-matching Airbnb listings, ranked using a pipeline that combines semantic search, graph algorithms, review analysis, and multi-criteria decision making.
Built on real data: over 10,000 listings and 500K+ reviews from Inside Airbnb.
User Query: "quiet cabin with hot tub under $200"
│
▼
┌─────────────────────────────────────────────────┐
│ STAGE 1: VECTOR RETRIEVAL │
│ Embed query + listings with MiniLM │
│ Cosine similarity → Top 50 candidates │
├─────────────────────────────────────────────────┤
│ STAGE 2: GRAPH + PERSONALIZED PAGERANK │
│ Item-item graph (Jaccard on amenity features) │
│ + User-item bipartite graph (from reviews) │
│ → Authority scores for candidates │
├─────────────────────────────────────────────────┤
│ STAGE 3: REVIEW RELEVANCE (Cross-Encoder) │
│ Score candidate reviews against user query │
│ → "Do past guests describe what user wants?" │
├─────────────────────────────────────────────────┤
│ STAGE 4: TOPSIS MULTI-CRITERIA RANKING │
│ Combine: cosine_sim, pagerank, wilson_score, │
│ review_relevance, price → closeness score [0,1] │
│ → Final top-K recommendations │
└─────────────────────────────────────────────────┘
| Algorithm | Purpose |
|---|---|
| Cosine Similarity (on sentence embeddings) | Semantic matching — "quiet" ≈ "peaceful" |
| Jaccard Similarity (on binary amenity features) | Graph edge construction — which listings offer similar amenities |
| Personalized PageRank | Authority — boost listings connected to query-relevant candidates |
| Wilson Score CI | Rating confidence — penalizes low-review-count listings |
| Cross-Encoder (ms-marco-MiniLM) | Review relevance — do reviews match what the user wants? |
| TOPSIS | Final ranking — optimal compromise across all criteria |
src/
├── data/
│ ├── loader.py # Raw CSV → clean parquet
│ └── features.py # Amenity parsing (25 binary features) + Wilson score
├── retrieval/
│ ├── embeddings.py # Generate text embeddings (all-MiniLM-L6-v2)
│ └── vector_search.py # Cosine similarity → top 50 candidates
├── graph/
│ ├── build_graph.py # Item-item (Jaccard) + user-item (reviews) graph
│ └── pagerank.py # Personalized PageRank scoring
├── scoring/
│ ├── cross_encoder.py # Query vs review text relevance
│ └── wilson.py # Rating confidence interval
├── ranking/
│ └── topsis.py # Multi-criteria final ranking
└── pipeline.py # End-to-end orchestrator
- Listings: 4,199 properties with 90 columns → cleaned to 3,635 rows with 25 engineered amenity features
- Reviews: 506,132 guest reviews with free-text comments
- Source: Inside Airbnb (Portland, OR)
pip install -r requirements.txtpython -m src.data.loader
python -m src.retrieval.embeddings
python -m src.graph.build_graphpython -m src.pipeline "quiet cabin in the woods with a hot tub"| Component | Tool |
|---|---|
| Embeddings | sentence-transformers (all-MiniLM-L6-v2) |
| Cross-Encoder | ms-marco-MiniLM-L-6-v2 |
| Graph | NetworkX |
| Language | Python |
| Data Processing | pandas, numpy |
- Jaccard over cosine for amenity similarity: Amenity features are binary (yes/no). Jaccard is designed for set comparison and ignores shared absences.
- Personalized PageRank over global: Global PageRank just finds popular listings. PPR seeds on query-relevant candidates so authority scores are query-aware.
- Why Cross-encoder: Cross-encoder answers "do reviews describe what the user actually wants?" — much more useful signal.
- TOPSIS over weighted sum: TOPSIS penalizes being terrible on any single dimension. A listing that's consistently good across all criteria beats one that's amazing on one but bad on others.
- Wilson score over raw ratings: A 5.0★ from 2 reviews is less trustworthy than 4.7★ from 200 reviews. Wilson gives a confidence-adjusted lower bound.