Meridian
Personal project · 2025 – 2026
Meridian transforms 75,173 real Yelp places into a searchable travel atlas. It combines semantic and keyword retrieval, behavioral recommendations, and LLM-generated itineraries in a production-deployed, observable platform.
75,173
live places indexed
143%
Yelp NDCG@10 improvement
10,000
retrieval evaluation queries
4
retrieval modes
6
containerized application images
154
automated tests
100%
OpenAPI-derived frontend typing
Problem
Travel discovery tools usually force a trade-off: keyword search misses intent, pure semantic search ignores hard constraints like price and distance, and itinerary generators treat destinations as interchangeable tokens instead of ranked, explainable recommendations grounded in real place data.
Meridian is more than a travel UI: it is a measured retrieval system, recommendation engine, LLM application, and production infrastructure project presented as one cohesive product — 75,173 real Yelp places, four retrieval modes, personalized ranking, and Gemini-powered itineraries deployed on Kubernetes with full observability.
Architecture
Next.js frontend
RSC · celestial UI · ⌘K command palette
Better Auth
traveler profiles · account history
Query understanding
rewrite · multi-query · constraints · HyDE
PostgreSQL + pgvector
dense embeddings · full-text search · RRF fusion
ONNX cross-encoder
reranking · circuit breaker
Redis personalization
For You feed · explainable recs
Gemini itineraries
latency/cost budgets · deterministic fallbacks
k3s deployment
6 images · KEDA autoscaling · isolated workers
Technical Decisions
Hybrid search with reciprocal-rank fusion
Dense embeddings, PostgreSQL full-text search, reciprocal-rank fusion, and ONNX cross-encoder reranking run as a single retrieval stack — semantic recall for paraphrased intent, keyword precision for named places and categories, and a learned reranker to order the fused candidate set.
Natural-language discovery with hard filters
Queries accept natural language while enforcing location, category, price, rating, and distance constraints — so users can describe what they want without sacrificing the structured filters that make results actionable.
Redis-cached preference vectors
Personalized ranking uses Redis-cached preference vectors and behavioral signals so repeat visitors see results tuned to their history without recomputing embeddings on every request.
A behavioral “For You” feed
The feed synthesizes searches, listing views, itinerary activity, and recommendation clicks into a single surface — not a static popularity list, but a live reflection of how someone actually explores.
Explainable recommendations
Each suggestion identifies which previously viewed place influenced it, so recommendations are auditable instead of opaque — users can see why a place surfaced, not just that it did.
Gemini itineraries from ranked destinations
Day-by-day itinerary generation uses the highest-ranked destinations from the retrieval pipeline as input, keeping LLM output grounded in places the system has already validated and scored.
Explicit LLM latency and cost budgets
LLM requests carry explicit latency and cost budgets, with deterministic itinerary fallbacks when generation fails — the product degrades gracefully instead of hanging or burning quota.
Four query-understanding strategies
Query rewriting, multi-query retrieval, constraint extraction, and HyDE run as complementary strategies so vague, constraint-heavy, and exploratory queries each get an appropriate interpretation path.
Faceted browse and similar-place discovery
Faceted browsing, embedding-based similar places, account history, and live system-health pages round out the product surface beyond search — discovery works whether you know what you want or are still exploring.
Better Auth traveler profiles
Better Auth accounts connect to traveler profiles so personalization, history, and recommendations persist across sessions without bolting identity on as an afterthought.
Global ⌘K command palette
A global Ctrl/⌘ + K command palette provides debounced live search and full keyboard navigation — fast retrieval for power users without sacrificing the visual browse experience.
Challenges
Measuring retrieval quality at scale
Improved Yelp NDCG@10 from 0.079 to 0.191 on a fixed 10,000-query evaluation — a 143% gain that validated each retrieval mode and fusion strategy against a reproducible benchmark instead of subjective spot checks.
Contract safety between FastAPI and Next.js
TypeScript clients are generated directly from FastAPI's OpenAPI schemas, and CI rejects backend/frontend contract drift — payload shapes stay synchronized across the stack without manual type maintenance.
Circuit breakers on critical dependencies
Circuit breakers protect reranking, LLM generation, and ingestion dependencies so a slow reranker or unavailable model doesn't cascade into full request failures — each dependency fails independently with a defined fallback.
Queue depth autoscaling with KEDA
Redis queues with retry policies, dead-letter handling, and queue-depth autoscaling via KEDA keep ingestion and background work responsive under load without over-provisioning idle workers.
Distributed tracing across services
OpenTelemetry and Jaeger instrument traces across Next.js, FastAPI, and the reranker so latency spikes can be attributed to a specific hop instead of guessed from aggregate response times.
Production metrics and dashboards
Prometheus metrics and Grafana dashboards cover latency, throughput, fallbacks, and queue health — the operational view matches the retrieval benchmark rigor applied during development.
Six-image Kubernetes deployment
Six application images ship through GitHub Actions, GHCR, Helm, and k3s with automatic HTTPS — internal workers and the unauthenticated reranker stay isolated from public ingress.
Celestial-cartography interface with RSC
A custom celestial-cartography interface built with React Server Components and minimal client-side state — deterministic, category-colored constellation artwork uniquely generated for every listing, without shipping a heavy client bundle.
Shareable URL-based filters
Filters serialize into shareable URLs without a client state library — deep links work on first load, which matters for a discovery product where people share places and searches.
Responsive and accessible by default
Layouts span mobile, tablet, and desktop with reduced-motion support, semantic meters, live regions, keyboard navigation, and accessible dialog behavior — accessibility is part of the design system, not a post-launch patch.
Graceful degradation everywhere
Loading, empty, degraded-service, and cold-start states are handled throughout the application — the UI communicates system health honestly instead of showing broken shells during partial outages.