Buscador de duplicados
Pega una lista — tickets de soporte, respuestas de encuestas, nombres de
Showcase — Buscador de duplicados
Pega una lista — tickets de soporte, respuestas de encuestas, nombres de producto — y encuentra los pares que significan lo mismo aunque estén redactados distinto. Cada línea se embebe una vez, todos los pares se comparan por cosine similarity y los más cercanos aparecen hasta arriba. El dedup por coincidencia exacta o por fuzzy-string no puede ver más allá de las palabras; los embeddings comparan significado.
Córrelo
bash bootstrap-secrets.sh # reads ../../../../.env, writes secrets/
docker compose up --build # default: PROVIDER=openai
Abre http://localhost:3000.
Para correrlo contra Gemini en su lugar:
PROVIDER=gemini docker compose up --build
Qué hay aquí
backend/ai_openai.py/backend/ai_gemini.py— embebe cada línea, compara todos los pares por cosine similarity, rankea los más cercanos.backend/main.py— loader idéntico de FastAPI; lee PROVIDER y despacha.frontend/app/page.tsx— una caja de texto (un item por línea) + pares rankeados.
La comparación de todos los pares es O(n²) — bien para una caja de texto, no para un millón de filas. Ese es el problema que resuelve una base de datos vectorial, el próximo capítulo.
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 dedup-finder.zip
cd dedup-finder
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): near-duplicate detection with embeddings.
Paste a list of lines — support tickets, survey answers, product names — and
find the pairs that MEAN the same thing even when they're worded differently.
Embed every line once, compare all pairs by cosine similarity, and surface the
closest ones. Exact-match dedup can't do this; embeddings can.
"""
import math
from openai import OpenAI
_client = OpenAI()
_MODEL = "text-embedding-3-small"
def _embed(texts: list[str]) -> list[list[float]]:
return [item.embedding for item in _client.embeddings.create(model=_MODEL, input=texts).data]
def _cosine(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb) if na and nb else 0.0
def run(text: str) -> str:
items = [line.strip() for line in text.splitlines() if line.strip()]
if len(items) < 2:
return "Enter at least two lines (one item per line)."
vecs = _embed(items)
pairs = []
for i in range(len(items)):
for j in range(i + 1, len(items)):
pairs.append((_cosine(vecs[i], vecs[j]), items[i], items[j]))
pairs.sort(reverse=True)
rows = [f"{score:.3f} [{a}] ≈ [{b}]" for score, a, b in pairs[:8]]
return "Most similar pairs (likely duplicates at the top):\n\n" + "\n".join(rows)
backend/ai_gemini.py
"""Showcase 3 (Gemini): near-duplicate detection with embeddings.
Same all-pairs cosine comparison, Gemini's embedding model.
"""
import math
import os
from google import genai
_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
_MODEL = "gemini-embedding-001"
def _embed(texts: list[str]) -> list[list[float]]:
return [e.values for e in _client.models.embed_content(model=_MODEL, contents=texts).embeddings]
def _cosine(a: list[float], b: list[float]) -> float:
dot = sum(x * y for x, y in zip(a, b))
na = math.sqrt(sum(x * x for x in a))
nb = math.sqrt(sum(y * y for y in b))
return dot / (na * nb) if na and nb else 0.0
def run(text: str) -> str:
items = [line.strip() for line in text.splitlines() if line.strip()]
if len(items) < 2:
return "Enter at least two lines (one item per line)."
vecs = _embed(items)
pairs = []
for i in range(len(items)):
for j in range(i + 1, len(items)):
pairs.append((_cosine(vecs[i], vecs[j]), items[i], items[j]))
pairs.sort(reverse=True)
rows = [f"{score:.3f} [{a}] ≈ [{b}]" for score, a, b in pairs[:8]]
return "Most similar pairs (likely duplicates at the top):\n\n" + "\n".join(rows)
Archivos del proyecto
.gitignoreREADME.es.mdREADME.mdbackend/Dockerfilebackend/ai_gemini.pybackend/ai_openai.pybackend/main.pybackend/requirements.txtbootstrap-secrets.shdocker-compose.ymlfrontend/Dockerfilefrontend/app/layout.tsxfrontend/app/page.tsxfrontend/next.config.tsfrontend/package.jsonfrontend/tsconfig.json