FTS5 en Cloudflare D1: de 220ms a 22ms (y qué casi se rompe)
Pasar de búsqueda con LIKE a FTS5 en Cloudflare D1 redujo la latencia del typeahead coreano de 220ms a 22ms, según el reporte técnico de un dev que corre 12 Workers en producción (publicado el 24 de junio de 2026). El detalle: casi se rompe dos veces, por tokenización del coreano y por triggers que nadie había puesto.
FTS5 (Full Text Search 5) es el motor de búsqueda de texto completo integrado en SQLite, y por extensión en Cloudflare D1. Crea un índice invertido sobre tus columnas de texto para resolver consultas de palabras clave sin escanear toda la tabla. Viene activado por defecto en D1, sin opt-in, y reemplaza los LIKE '%texto%' que no usan índice.
En 30 segundos
- 10x más rápido: el caso real pasó de 180-220ms a 22ms en un índice de ~400K keywords.
- Ya viene activado: FTS5 está habilitado en D1 sin configuración previa, no hay que prenderlo.
- Trampa coreano: el tokenizer por defecto rompe por espacios, y el coreano no separa morfemas, así que el recall cae de 91% a 88% al crecer el índice.
- Trampa de sincronía: con tablas externas (
content='') el índice no se actualiza solo; sin triggers se desincroniza en silencio. - El fix de recall: partir la búsqueda del usuario y unir tokens con
ORrecupera casi toda la precisión sin tocar el tokenizer.
¿Cuánta velocidad ganás al pasar a FTS5 en D1?
Arranquemos por el número que importa. El autor del reporte tenía un dashboard de ad-ops que se vuelve a renderizar con cada tecla que apreta el usuario. Búsqueda incremental, typeahead, lo que quieras llamarle. Con escaneos tradicionales en D1, cada consulta tardaba entre 180 y 220ms.
Para un autocompletado, eso es inviable. Vos escribís tres letras y ya tenés tres consultas encimadas, cada una arrastrando 200ms. La experiencia se siente rota.
Con FTS5 la misma búsqueda cayó a 22ms. Diez veces más rápido (que no es poco cuando tu interfaz depende de eso). La diferencia es estructural: en vez de recorrer fila por fila buscando coincidencias, FTS5 consulta un índice invertido que ya sabe en qué documentos aparece cada término. Es la misma lógica que usa un buscador grande, metida adentro de tu base SQLite.
¿Cuándo conviene migrar de búsqueda tradicional a FTS5?
El caso concreto da una pista clara de timing. El sistema indexa cerca de 400.000 keywords publicitarias: marcas, slugs de productos, términos mezclados en coreano y alfabeto latino. Eso alimenta un dashboard que corre sobre 12 Workers en producción para operaciones de ads D2C. Cubrimos ese tema en detalle en gestión de variables en Workers.
¿La señal para migrar? Cuando la latencia de búsqueda choca contra la interactividad. Estos son los disparadores:
- Latencia arriba de 100ms en typeahead: el caso medía 180-220ms, y para autocompletado eso ya es un “no” rotundo.
- Volumen de filas que crece: los escaneos lineales empeoran a medida que la tabla engorda; el índice invertido se mantiene casi plano.
- Búsqueda por palabras dentro de texto largo: si usás
LIKE '%termino%', ningún índice clásico te ayuda y FTS5 sí.
El armado básico es directo: creás una tabla virtual FTS5, la poblás con tu contenido y empezás a consultar con la sintaxis MATCH. La migración parecía un trámite. No lo fue.
¿Por qué FTS5 falla con coreano y otros idiomas sin espacios?
Acá viene la primera trampa de verdad. El tokenizer por defecto de FTS5 divide el texto por espacios en blanco y puntuación. Funciona perfecto para inglés o español, donde las palabras vienen separadas. El coreano no juega con esas reglas.
En coreano una palabra compuesta puede contener, pegados, lo que un usuario tipea como dos términos separados. El tokenizer guarda el compuesto entero como un solo token. Después llega el usuario, busca uno de los morfemas sueltos, y FTS5 no encuentra nada porque en su índice ese fragmento no existe como unidad independiente.
El impacto se mide en recall, o sea cuántos resultados relevantes recuperás del total que existen. Sobre un golden set de 500 consultas verificadas a mano, el recall pasó de 91% con 40.000 filas a 88% con 180.000 filas. No es una catástrofe, pero la dirección preocupa: cuanto más crece el índice con términos long-tail más ruidosos, más se degrada. Sobre eso hablamos en almacenamiento sin costo de egreso.
¿Y por qué empeora con el tamaño? Porque los términos largos y poco frecuentes son justo los que más sufren la mala segmentación. Ojo con esto si tu base está en japonés, chino o tailandés: el problema es el mismo en cualquier idioma que no separe palabras con espacios.
¿Cómo recuperar el recall con query-side splitting y OR?
La solución que terminó funcionando no toca el tokenizer. Se resuelve del lado de la consulta. Partís la entrada del usuario en sus posibles fragmentos y los unís con el operador OR de FTS5. Así, aunque el índice tenga el compuesto entero, la búsqueda lo alcanza por cualquiera de sus partes.
La lógica en SQLite queda más o menos así:
-- En vez de buscar el término crudo:
SELECT * FROM keywords_fts WHERE keywords_fts MATCH '검색어';
-- Partís la entrada y unís con OR:
SELECT * FROM keywords_fts WHERE keywords_fts MATCH '검색 OR 어' ORDER BY rank;Una salvedad sobre el orden de resultados: el rank de relevancia en FTS5 devuelve un float, y valores más bajos significan más relevante. El autor pasó tres días viendo basura arriba de todo hasta darse cuenta de que estaba ordenando al revés. Tomá nota: ordenás ascendente, no descendente.
El trade-off es claro. Ganás recall sin penalidad grande en performance, pero tu lógica de consulta se vuelve más compleja y menos portable, porque depende de cómo partís los tokens. Para un caso multilingüe pesado, vale la pena. Para una base en español, probablemente ni lo necesites.
¿Por qué el índice FTS5 se desincroniza sin triggers?
La segunda trampa es más silenciosa, y por eso más peligrosa. Cuando creás una tabla FTS5 con content='' (tabla externa), FTS5 guarda solo el índice, no el texto original. Ahorrás espacio, buenísimo. El problema aparece después. En automatización de deployments en CI/CD profundizamos sobre esto.
Ponele que insertás una keyword nueva en tu tabla de contenido. El índice FTS5 no se entera. Borrás una fila, el índice sigue apuntando a algo que ya no existe. Actualizás un valor, y la búsqueda devuelve el dato viejo. Las operaciones INSERT, UPDATE y DELETE sobre la tabla fuente no se propagan solas al índice.
El resultado es la peor clase de bug: nada falla, no salta ningún error, pero el índice va quedando cada vez más viejo respecto de los datos reales. Las búsquedas devuelven resultados incompletos o fantasma, y vos ni te enterás hasta que un usuario reclama. Esa deriva entre fuente e índice es lo que hay que cortar de raíz.
| Aspecto | Tabla externa (content=”) | Tabla interna (FTS5 normal) |
|---|---|---|
| Almacenamiento | Solo el índice | Índice + copia del texto |
| Espacio en disco | Menor | Mayor |
| Sincronización | Manual, con triggers | Automática |
| Riesgo de drift | Alto si te olvidás triggers | Bajo |
| Ideal para | Bases grandes con espacio ajustado | Simplicidad y seguridad |

¿Cómo sincronizar el índice con triggers INSERT, UPDATE y DELETE?
El fix son triggers en la tabla de contenido que replican cada cambio al índice. Tres triggers, uno por operación:
CREATE TRIGGER kw_ai AFTER INSERT ON keywords BEGIN
INSERT INTO keywords_fts(rowid, term) VALUES (new.id, new.term);
END;
CREATE TRIGGER kw_ad AFTER DELETE ON keywords BEGIN
INSERT INTO keywords_fts(keywords_fts, rowid, term) VALUES('delete', old.id, old.term);
END;
CREATE TRIGGER kw_au AFTER UPDATE ON keywords BEGIN
INSERT INTO keywords_fts(keywords_fts, rowid, term) VALUES('delete', old.id, old.term);
INSERT INTO keywords_fts(rowid, term) VALUES (new.id, new.term);
END;Con esto, cada escritura en keywords se refleja al instante en keywords_fts. Eso sí: tiene costo. Los triggers agregan sobrecarga en cada operación de escritura, así que bajo concurrencia alta vas a sentir la diferencia. Si tu carga es de muchas escrituras simultáneas, conviene evaluar updates en lote o tareas de limpieza programadas en vez de propagar todo en tiempo real.
Para validar que el índice no derivó, corré una comprobación periódica que compare la cantidad de filas de la tabla fuente contra el índice. Si los números no cierran, algo se te escapó de los triggers. Más contexto en deploys automatizados con GitHub Actions.
Qué significa para equipos que usan D1 en Latinoamérica
Si estás armando algo sobre Cloudflare Workers y D1 desde la región, la buena noticia es que para español la trampa del tokenizer ni te roza: nuestro idioma separa palabras con espacios, así que el comportamiento por defecto te sirve. La trampa de los triggers, en cambio, te aplica igual que a cualquiera.
Y si el proyecto necesita hosting o infraestructura adicional para el front o las APIs que consumen ese D1, en donweb.com tenés opciones pensadas para proyectos argentinos. El edge de Cloudflare resuelve la búsqueda; el resto de tu stack lo seguís manejando donde te quede más cómodo.
Errores comunes al implementar FTS5 en D1
- Ordenar el rank al revés: el float de relevancia es ascendente, valores bajos primero. Si ordenás descendente, te sale la basura arriba. Tres días perdió el autor con esto.
- Olvidar los triggers con tablas externas: sin ellos el índice se desincroniza en silencio. No hay error, solo resultados que van quedando viejos.
- Esperar 100% de recall: en idiomas sin espacios la tokenización nunca es perfecta. Medí con un golden set de al menos 500 consultas verificadas a mano antes de prometer cobertura total.
- No revalidar al crecer: el recall cae con el volumen. Lo que medías al 91% con 40K filas baja al 88% con 180K. Medí en cada etapa de crecimiento, no una sola vez.
Preguntas Frecuentes
¿Qué es FTS5 en Cloudflare D1?
FTS5 es el motor de búsqueda de texto completo de SQLite, integrado y activado por defecto en Cloudflare D1. Crea un índice invertido sobre columnas de texto para resolver búsquedas por palabra clave sin escanear toda la tabla, lo que reduce la latencia de forma drástica frente a consultas con LIKE.
¿Cuánto mejora la performance FTS5?
En el caso documentado, la búsqueda pasó de 180-220ms a 22ms sobre un índice de unas 400.000 keywords, una mejora cercana a 10x. La ganancia depende del volumen de datos y del tipo de consulta, pero el salto es mayor cuanto más grande es la tabla.
¿Por qué FTS5 tiene problemas con el coreano?
El tokenizer por defecto divide el texto por espacios y puntuación, pero el coreano no separa los morfemas con espacios. Así, palabras compuestas se guardan como un solo token y las búsquedas de fragmentos sueltos no las encuentran, lo que bajó el recall de 91% a 88% al crecer el índice.
¿Necesito triggers para usar FTS5?
Solo si usás tablas externas con content='', donde FTS5 guarda únicamente el índice. En ese modo las operaciones INSERT, UPDATE y DELETE no se propagan solas, así que necesitás triggers para mantener el índice sincronizado. Con tablas FTS5 normales la sincronización es automática.
¿FTS5 funciona bien con español?
Sí. El español separa palabras con espacios, así que el tokenizer por defecto segmenta bien y no sufre el problema de recall del coreano. Para texto en español no hace falta el query-side splitting, lo que simplifica la implementación.
Conclusión
FTS5 en D1 te da una mejora de velocidad seria casi gratis, porque ya viene activado. El salto de 220ms a 22ms es real y reproducible. Pero el reporte deja dos lecciones que no salen en la documentación oficial: la tokenización por defecto rompe con idiomas sin espacios, y las tablas externas se desincronizan en silencio si no ponés triggers.
Si trabajás en español, la primera trampa no te toca y arrancás con ventaja. La segunda te aplica siempre. Antes de mandar a producción, armá un golden set de consultas, medí el recall en cada etapa de crecimiento y verificá que tus triggers cubran las tres operaciones. El motor es rápido; lo que casi se rompe siempre está en los detalles que nadie documentó.






