Course ES
← back to chapter

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 shared VectorStore; search() takes an exclude_text so 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.

Download recommender.zip

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

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • backend/store.py
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json