Un cambio de una línea que rompe el índice entero
Sale un modelo de embeddings mejor. Más barato, mejor recall, dimensión más cómoda. Cambias la constante en el cliente, despliegas y el buscador empieza a devolver basura.
No es un bug. Es aritmética. Un embedding solo tiene sentido dentro del espacio vectorial en el que se generó. Si el índice guarda vectores del modelo viejo y la consulta llega vectorizada con el nuevo, estás midiendo distancias entre cosas que no viven en el mismo sitio. El resultado no es un error: es un ranking plausible y equivocado, que es bastante peor.
La conclusión práctica: cambiar de modelo de embeddings no es un cambio de configuración, es una migración de datos. Y como toda migración de datos con tráfico encima, se planifica.
Lo que realmente cambia
Tres cosas se mueven a la vez cuando cambias de modelo, y conviene separarlas porque tienen soluciones distintas.
El espacio. Los vectores antiguos y los nuevos no son comparables. Esto obliga a regenerar el 100 % del corpus, no un delta.
La dimensión. Si pasas de 1.536 a 1.024 dimensiones, el esquema de la
colección cambia. En la mayoría de motores vectoriales eso significa colección
nueva, no ALTER.
El comportamiento. Los umbrales de similitud que tenías calibrados dejan de valer. Un 0,82 de coseno en un modelo no significa lo mismo que en otro. Todo filtro con número mágico hay que recalibrarlo.
Tres formas de hacerlo, y cuándo vale cada una
| Estrategia | Cómo funciona | Cuándo tiene sentido | Coste |
|---|---|---|---|
| Recrear en frío | Se para la ingesta, se borra el índice, se reindexa y se vuelve a levantar | Corpus pequeño, uso interno, ventana de mantenimiento aceptable | Mínimo en ingeniería, máximo en indisponibilidad |
| Índice paralelo con alias | Se construye una colección nueva en segundo plano y se conmuta el alias al terminar | El caso general en producción | Doble almacenamiento durante la migración |
| Doble escritura y backfill | Cada documento nuevo se escribe en ambos índices mientras un proceso rellena el histórico | Corpus grande con escrituras constantes | El más complejo; hay que gestionar la ventana de inconsistencia |
En la práctica casi siempre acabo en la segunda, y le añado doble escritura solo si la ingesta no puede pararse ni unos minutos. El patrón es viejo y conocido: es un despliegue azul-verde, pero de vectores.
El backfill: reanudable o no sirve
Reindexar un corpus serio son horas. Cualquier proceso que tarde horas se va a caer por en medio, casi siempre por un límite de tasa del proveedor de embeddings. Si al caerse hay que empezar de cero, la migración no termina nunca.
Dos reglas: checkpoint persistente y escritura idempotente. Con eso, un reintento cuesta minutos en lugar de horas.
# Backfill del índice nuevo. Reanudable y con control de tasa.
BATCH = 128 # lotes grandes amortizan la latencia de red
CHECKPOINT = "backfill.cursor"
def ultimo_cursor() -> str | None:
try:
return open(CHECKPOINT).read().strip() or None
except FileNotFoundError:
return None
def backfill(origen, destino, embebedor):
cursor = ultimo_cursor()
for lote in origen.iterar_chunks(desde=cursor, tam=BATCH):
# El id del vector se deriva del chunk, no es autogenerado:
# reescribir el mismo lote dos veces no duplica nada.
vectores = embebedor.embed([c.texto for c in lote])
destino.upsert(
ids=[c.id for c in lote],
vectores=vectores,
metadatos=[c.meta for c in lote],
)
# El checkpoint se guarda DESPUÉS del upsert confirmado.
# Al revés perderías silenciosamente el último lote.
with open(CHECKPOINT, "w") as f:
f.write(lote[-1].id)
Dos detalles que parecen menores y no lo son. El identificador del vector se deriva del contenido del chunk, así que un lote repetido sobrescribe en vez de duplicar. Y el checkpoint se escribe después de confirmar la escritura, nunca antes: si lo haces al revés, un fallo entre ambas operaciones deja un agujero en el índice que no vas a detectar hasta que un usuario pregunte por ese documento.
No conmutes a ciegas
Aquí es donde la mayoría de migraciones se tuercen. El índice nuevo está completo, el modelo es “mejor” según su ficha técnica y se conmuta el alias confiando en el marketing del proveedor. Los benchmarks públicos se miden sobre corpus que no son el tuyo.
Antes de conmutar, lanza el mismo conjunto de evaluación contra los dos índices y compara. Con 30 o 40 consultas reales bien elegidas basta para ver la diferencia. Mira tres cosas:
- Recall de los documentos correctos en el top-k. Es la métrica que decide.
- Solapamiento entre rankings. Un solapamiento muy bajo no es malo por sí mismo, pero avisa de que el comportamiento cambia mucho y toca revisar umbrales.
- Latencia y coste por consulta con la dimensión nueva.
Si el índice nuevo pierde en alguna consulta que antes funcionaba, esa consulta es ahora un caso de regresión permanente en tu conjunto de evaluación. La migración es una oportunidad gratis para engordarlo.
La conmutación y la vuelta atrás
Conmutar el alias es instantáneo. Lo importante es que volver atrás también lo sea: no borres el índice viejo el mismo día. Déjalo vivo una semana, pagando almacenamiento, con la ingesta escribiendo en ambos si puedes permitírtelo. El almacenamiento vectorial de un corpus mediano cuesta bastante menos que una tarde en caliente reindexando hacia atrás con usuarios delante.
Y deja monitorizado lo que importa durante esos días: proporción de consultas sin resultado por encima del umbral, y tasa de respuestas del asistente que no citan ninguna fuente. Un fallo de recuperación rara vez se manifiesta como un error; se manifiesta como un modelo que responde de forma más vaga que ayer.
Lo que queda montado para la próxima
La migración se hace una vez, pero el corpus se reindexa muchas: cambias la estrategia de chunking, añades metadatos, pruebas otro proveedor. Si el backfill queda como un script reanudable con parámetros y la conmutación como un cambio de alias, la siguiente vez esto deja de ser un proyecto y pasa a ser una tarea de una tarde.
Tres cosas que merece la pena dejar hechas hoy:
- Guardar la versión del modelo de embeddings en los metadatos de cada vector. Es un campo de texto, y es lo que te permite auditar qué hay dentro del índice sin adivinarlo.
- Referenciar el índice siempre por alias en el código, nunca por el nombre de la colección. Sin alias no hay conmutación limpia.
- Versionar el conjunto de evaluación junto al código, para que comparar dos índices sea un comando y no una tarde de trabajo manual.
El modelo de embeddings vas a cambiarlo otra vez. La pregunta no es si, sino si la próxima vez te va a costar una tarde o un fin de semana.