Course EN
← back to chapter

Recomendador

El vector store leído al revés: entrégale un item en lugar de un query y pídele

Showcase — Recomendador

El vector store leído al revés: entrégale un item en lugar de un query y pídele los vecinos más cercanos. Describe una app que te gusta — o pega una línea de catálogo — y obtén las coincidencias más cercanas por significado, con la semilla misma excluida. Recomendación basada en contenido sin ratings y sin historial de usuario, solo embeddings.

Córrelo

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Abre http://localhost:3000. Córrelo contra Gemini con PROVIDER=gemini docker compose up --build.

Qué hay aquí

  • backend/store.py — el VectorStore compartido; search() toma un exclude_text para que un item no se recomiende a sí mismo.
  • backend/ai_openai.py / backend/ai_gemini.py — el catálogo y la búsqueda de vecinos más cercanos.
  • frontend/app/page.tsx — caja de descripción + recomendaciones.

Detenlo

docker compose down

Ejecútalo en tu máquina

Descarga el proyecto como ZIP y córrelo con Docker. Levanta un backend FastAPI y un frontend Next.js en localhost:3000.

Descargar 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

Escribe algo, elige un proveedor y ejecuta el mismo código de Código contra la API real. Requiere iniciar sesión.


  

Los mismos módulos que ejecuta el botón Run. El proyecto completo (frontend, Dockerfile, compose) está en el ZIP, pestaña 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)

Archivos del proyecto

  • .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