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— elVectorStorecompartido;search()toma unexclude_textpara 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.
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
.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