Find things you only partially remember. Describe a fragment โ "twelve people voting in one room" โ and MemoryLens returns the most likely real titles, grounded in a real catalog, with confidence scores and explanations.
AI course final project, built as a production-grade SaaS.
๐ฌ Movies ยท ๐บ TV Series ยท ๐ต Songs ยท ๐ Books ยท ๐ฎ Games ยท ๐ค Actors
You pick a category first; it becomes a hard retrieval filter. The AI ranks and explains real candidates โ it never invents titles. When a query clearly belongs to another category, the UI offers a soft "switch?" suggestion. When no catalog item is a confident match, the AI names the real title from world knowledge instead (marked "AI knowledge").
| Layer | Tech |
|---|---|
| Backend | FastAPI (Python 3.12) |
| Frontend | React + Vite + TypeScript + Tailwind v4 + framer-motion |
| Store + Vectors | Postgres 16 + pgvector |
| Cache / rate-limit | Redis |
| Embeddings / rerank | sentence-transformers (local, bge + cross-encoder) |
| LLM reasoning | OpenAI gpt-4.1-nano (primary) โ OpenRouter (fallback) |
| Infra | Docker Compose |
React SPA โ FastAPI (routers โ services โ AI pipeline)
โโ Postgres + pgvector (catalog, vectors, keyword)
โโ Redis (cache, rate limit)
โโ OpenAI / OpenRouter (LLM reasoning only)
local models: embeddings + cross-encoder reranking
Search pipeline: validate โ clean โ understand โ hybrid retrieve (vector + keyword + weighted RRF) โ rerank โ LLM reasoning โ confidence โ free-form fallback โ response.
See ARCHITECTURE.md for the design, and docs/ for the full documentation set โ overview, pipeline, API, data model, features, catalog, configuration, development, security, and changelog.
One command, everything starts. Make sure Docker Desktop is running first.
cp .env.example .env # then fill OPENAI_API_KEY (see below)
docker compose up --buildThat starts all four services: postgres, redis, api, frontend. The frontend installs its own dependencies on start, so newly added packages always work โ no manual npm install needed.
| What | URL |
|---|---|
| App (frontend) | http://localhost:5173 |
| API docs (Swagger) | http://localhost:8000/docs |
| API health | http://localhost:8000/api/v1/health |
Port note: in Docker the app is always on 5173 (fixed via
--strictPort). If you ever ran the frontend natively withnpm run devwhile 5173 was already busy, Vite silently jumped to 5174 โ that was the source of the 5173/5174 confusion. Inside Docker it never drifts.
.env needs an OpenAI key for LLM reasoning:
OPENAI_API_KEY=sk-...
Cost is tiny (gpt-4.1-nano โ $0.0003/search). OpenAI is prepaid โ set a low hard limit in your OpenAI billing dashboard. If OpenAI fails, it falls back to the free OpenRouter model (OPENROUTER_API_KEY, optional but slower).
First run only โ load the real catalog data:
docker compose exec api python -m scripts.ingest --all --source fixtureThen search at http://localhost:5173.
docker compose up -d # start in background
docker compose down # stop everything
docker compose logs -f frontend # watch frontend logs
docker compose logs -f api # watch API logs
docker compose ps # see what's running + portsWe benchmark on a fixed dataset of 48 fuzzy-memory queries (8 per category,
easy/medium/hard), each with a known correct answer that exists in the catalog
(backend/eval/dataset.json). Two numbers matter:
- recall@1 โ how often the correct answer is the #1 result.
- MRR โ on average, how close to the top the correct answer lands (1.0 = always first).
| Configuration | recall@1 | recall@5 | MRR |
|---|---|---|---|
| Keyword search only | 54% | 71% | 0.625 |
| Vector search only | 54% | 77% | 0.644 |
| Hybrid (vector + keyword, no rerank) | 58% | 77% | 0.653 |
| Hybrid + cross-encoder rerank | 54% | 79% | 0.648 |
| Hybrid + rerank + HyDE | 56% | 79% | 0.662 |
| Full pipeline (+ LLM reasoning + free-form fallback) | 92% | 96% | 0.938 |
What the table says, in plain words:
- Retrieval alone finds the right item in its top-5 about 8 times out of 10; hybrid fusion and reranking mostly improve depth (recall@5), not the #1 spot.
- The big jump comes from the LLM layer: reasoning re-orders the shortlist and the free-form fallback rescues queries the small catalog simply can't answer (e.g. weak actor bios) โ top-1 accuracy goes 54% โ 92%.
- Hard queries (no title words at all) score 82% top-1 on the full pipeline vs 35% on retrieval alone.
Reproduce it yourself (results are saved to backend/eval/results/):
docker compose exec api python -m scripts.run_eval # retrieval baseline, free, ~40s
docker compose exec api python -m scripts.run_eval --no-rerank --label no-rerank
docker compose exec api python -m scripts.run_eval --leg vector --label vector-only
docker compose exec api python -m scripts.run_eval --full --label full # whole pipeline, uses the LLM (~$0.03)Full run details and per-category breakdowns: backend/eval/RESULTS.md.
The site won't open at http://localhost:5173
- Check the frontend container is actually up:
You should see
docker compose ps
memorylens-frontend-1withUpstatus. If it's missing orExited, it crashed on start. - Read why:
docker compose logs frontend --tail 30
Cannot find package '@tailwindcss/vite'(or any missing package): the container'snode_modulesvolume is stale after a dependency was added. Rebuild and renew the volume:(Thedocker compose up -d --build --force-recreate --renew-anon-volumes frontend
command:indocker-compose.ymlnow runsnpm installon every start, so this should be self-healing going forward.)
API returns 404 on /health โ the health endpoint is /api/v1/health, not /health. Swagger is at /docs.
Nothing responds โ confirm Docker Desktop itself is running, then docker compose up -d.
docker-compose.yml is the development stack (Vite dev server, --reload,
Postgres/Redis published to the host). For a public deployment use
docker-compose.prod.yml, which serves a built static frontend via nginx, runs the
API without reload, and keeps the database and cache on the internal network only.
-
Create
.env.prodfrom.env.exampleand set strong secrets:JWT_SECRET=$(openssl rand -hex 32) # required โ the API won't start otherwise POSTGRES_USER=memorylens_prod POSTGRES_PASSWORD=$(openssl rand -base64 32) REDIS_PASSWORD=$(openssl rand -base64 32) REDIS_URL=redis://:<that-redis-password>@redis:6379/0 METRICS_TOKEN=$(openssl rand -hex 16) # required to scrape /metrics in prod ENV=production CORS_ORIGINS=https://your-domain
Also set the real
OPENAI_API_KEYand ingestion keys. Rotate any key that has ever sat in a shared.env. -
Launch:
docker compose -f docker-compose.prod.yml --env-file .env.prod up -d --build
-
Front it with TLS. Put nginx/Caddy/Traefik in front of the
frontendservice (port 8080) to terminate HTTPS; the app emits HSTS whenENV=production. The proxy must setX-Forwarded-Forto the real client IP โ the per-user/per-IP rate limiter reads it (the bundled nginx already does this).
What ENV=production changes: requires a real JWT_SECRET (fail-fast on
startup), hides /docs /redoc /openapi.json, gates /metrics behind
X-Metrics-Token, and emits HSTS.
Cost / abuse controls: search requires login and is capped per user by
SEARCH_RATE_PER_MIN (burst) and SEARCH_DAILY_QUOTA (hard daily ceiling), so one
account can't drain the LLM budget. Security headers (X-Content-Type-Options,
X-Frame-Options, CSP, Referrer-Policy) are set on every response.
โ Backend (grounded RAG pipeline, hybrid retrieval, auth, history), frontend SPA (Aurora + glass UI, Tailwind v4, framer-motion), 6 live categories with real catalog data, observability, Docker, CI. Verified end-to-end.