Recommender
The vector store read the other way round: hand it an item instead of a query
Showcase — Recommender
The vector store read the other way round: hand it an item instead of a query and ask for nearest neighbors. Describe an app you like — or paste a catalog line — and get the closest matches by meaning, the seed itself excluded. Content-based recommendation with no ratings and no user history, just embeddings.
Run
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Open http://localhost:3000. Run against Gemini with
PROVIDER=gemini docker compose up --build.
What's where
backend/store.py— the sharedVectorStore;search()takes anexclude_textso an item isn't recommended to itself.backend/ai_openai.py/backend/ai_gemini.py— the catalog and the nearest-neighbor lookup.frontend/app/page.tsx— description box + recommendations.
Stop
docker compose down
Run locally
Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.
unzip recommender.zip
cd recommender
bash bootstrap-secrets.sh # one-time: pulls API keys into ./secrets
docker compose up --build # default provider: openai
# or: PROVIDER=gemini docker compose up --build
Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.
The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under README.
backend/ai_openai.py
"""Showcase 3 (OpenAI): a "more like this" recommender.
The same store, read the other way round: instead of a text query, hand it an
item and ask for its nearest neighbors. Describe an app you like — or paste one
from the catalog — and get the closest matches by meaning, itself excluded.
Content-based recommendation with no ratings, no user history, just embeddings.
"""
from openai import OpenAI
from store import VectorStore
_client = OpenAI()
_MODEL = "text-embedding-3-small"
_CATALOG = [
{"id": "1", "text": "Notion — an all-in-one workspace for notes, docs, and databases."},
{"id": "2", "text": "Obsidian — a local-first markdown notes app with backlinks and graph view."},
{"id": "3", "text": "Todoist — a fast task manager with natural-language due dates and projects."},
{"id": "4", "text": "Things — a polished personal to-do app for Apple devices."},
{"id": "5", "text": "Figma — collaborative interface design in the browser."},
{"id": "6", "text": "Excalidraw — a virtual whiteboard for hand-drawn-style diagrams."},
{"id": "7", "text": "Linear — issue tracking and project management built for speed."},
{"id": "8", "text": "Slack — team chat organized into channels."},
{"id": "9", "text": "Zoom — video meetings and screen sharing."},
{"id": "10", "text": "Raycast — a keyboard launcher that automates Mac workflows."},
]
_store: VectorStore | None = None
def _embed(texts: list[str]) -> list[list[float]]:
return [item.embedding for item in _client.embeddings.create(model=_MODEL, input=texts).data]
def _get_store() -> VectorStore:
global _store
if _store is None:
_store = VectorStore(_embed)
_store.add(_CATALOG)
return _store
def run(text: str) -> str:
query = text.strip()
# Exclude the seed item itself if the user pasted an exact catalog line.
hits = _get_store().search(query, k=5, exclude_text=query)
rows = [f"{score:.3f} {item['text']}" for score, item in hits]
return "More like that:\n\n" + "\n".join(rows)
backend/ai_gemini.py
"""Showcase 3 (Gemini): a "more like this" recommender.
Same catalog, same nearest-neighbor lookup, Gemini's embedding model.
"""
import os
from google import genai
from store import VectorStore
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
_MODEL = "gemini-embedding-001"
_CATALOG = [
{"id": "1", "text": "Notion — an all-in-one workspace for notes, docs, and databases."},
{"id": "2", "text": "Obsidian — a local-first markdown notes app with backlinks and graph view."},
{"id": "3", "text": "Todoist — a fast task manager with natural-language due dates and projects."},
{"id": "4", "text": "Things — a polished personal to-do app for Apple devices."},
{"id": "5", "text": "Figma — collaborative interface design in the browser."},
{"id": "6", "text": "Excalidraw — a virtual whiteboard for hand-drawn-style diagrams."},
{"id": "7", "text": "Linear — issue tracking and project management built for speed."},
{"id": "8", "text": "Slack — team chat organized into channels."},
{"id": "9", "text": "Zoom — video meetings and screen sharing."},
{"id": "10", "text": "Raycast — a keyboard launcher that automates Mac workflows."},
]
_store: VectorStore | None = None
def _embed(texts: list[str]) -> list[list[float]]:
return [e.values for e in _client.models.embed_content(model=_MODEL, contents=texts).embeddings]
def _get_store() -> VectorStore:
global _store
if _store is None:
_store = VectorStore(_embed)
_store.add(_CATALOG)
return _store
def run(text: str) -> str:
query = text.strip()
hits = _get_store().search(query, k=5, exclude_text=query)
rows = [f"{score:.3f} {item['text']}" for score, item in hits]
return "More like that:\n\n" + "\n".join(rows)
Project files
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbackend/store.pybootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json