LIKE no escala
Si alguna vez has buscado texto en SQLite, seguro que has tirado de LIKE '%palabra%'. Funciona bien con tablas pequeñas y se cae a pedazos en cuanto crecen. No hay ningún índice que pueda echar una mano: SQLite tiene que recorrer fila por fila, pasarla a minúsculas y comprobar si contiene la subcadena. Los límites entre palabras, el ranking, las consultas con varias palabras y la búsqueda por prefijo corren por tu cuenta.
Aquí es donde entra FTS5, la respuesta que ya viene incluida. Es un tipo de tabla virtual que mantiene un índice invertido sobre tus columnas de texto, entiende un pequeño lenguaje de consulta y ordena los resultados con BM25. Viene de serie con SQLite, así que no hay nada que instalar.
Cómo crear una tabla virtual FTS5 en SQLite
Las tablas FTS5 se crean con CREATE VIRTUAL TABLE ... USING fts5(...), indicando las columnas de texto que quieres indexar:
Tres detalles que vale la pena destacar. Las columnas no llevan tipo: FTS5 trata todo como texto. El operador MATCH se aplica contra el nombre de la tabla (posts MATCH ...), no contra una columna. Y la consulta no distingue mayúsculas y se tokeniza, así que 'sqlite' encuentra SQLite en cualquiera de las filas.
El lenguaje de consultas del operador MATCH
MATCH admite mucho más que una sola palabra. La cadena de búsqueda tiene su propia mini-gramática:
Qué hace cada uno:
'fts5 AND prefix'— ambos términos deben aparecer (en cualquier orden, en cualquier parte de la fila).'"keep fts"'— frase exacta, en ese orden.'trig*'— búsqueda por prefijo, encuentratrigger,triggers,trigonometry...'index NOT trigger'— contieneindex, pero notrigger.
También puedes apuntar a una sola columna con columna:término, por ejemplo 'title:sqlite'. La gramática completa admite paréntesis para agrupar y OR para alternativas — la misma forma que esperarías de cualquier motor de búsqueda.
Ranking con BM25 en SQLite
Por defecto, FTS5 añade una columna oculta rank a cada fila. Es la puntuación de relevancia BM25 — cuanto más bajo el número, mejor el match. Ordena por esa columna para obtener primero los resultados más relevantes:
¿Quieres dar más peso a unas columnas que a otras? Pásale los pesos a bm25() —uno por cada columna, en el mismo orden en que las declaraste—:
El primer post gana porque sqlite aparece en title (con peso 10×) en lugar de solo en body (peso 1×). Elige los pesos según cómo quiera rankear realmente tu aplicación.
Mantener el índice sincronizado
La tabla FTS5 más sencilla guarda su propia copia del texto. Eso está bien para datos tipo log en los que solo haces inserciones, pero la mayoría de las aplicaciones ya tienen una tabla real y lo que quieren es que el FTS la siga. El patrón limpio para esto es una tabla FTS de tipo external content acompañada de tres triggers.
content='articles' le indica a FTS5 que no guarde el texto otra vez: lo recuperará de la tabla articles cuando haga falta. Los triggers se encargan de replicar las escrituras en el índice FTS. Así, articles pasa a ser la fuente de verdad y articles_fts queda como una estructura de búsqueda paralela.
La sentencia INSERT INTO articles_fts(articles_fts, ...) VALUES ('delete', ...), que se ve algo rara a primera vista, es la sintaxis de comandos propia de FTS5 para pedirle al índice que elimine una fila.
Snippets y resaltado de coincidencias en SQLite FTS5
En los resultados de búsqueda casi siempre queremos mostrar una vista previa con los términos coincidentes destacados. Para eso, FTS5 trae dos funciones:
highlight(tabla, indice_columna, apertura, cierre)devuelve el texto completo de la columna con los tokens coincidentes envueltos.snippet(tabla, indice_columna, apertura, cierre, puntos_suspensivos, num_tokens)devuelve un fragmento corto centrado en la coincidencia.
Los índices de columna empiezan en cero, en el orden en que las declaraste. Estas son las piezas básicas para conseguir ese efecto de "términos coincidentes resaltados en amarillo" que toda interfaz de búsqueda necesita.
Tropiezos que conviene tener presentes
Hay un par de detalles que suelen pillar a la gente desprevenida:
MATCHsolo funciona sobre tablas FTS. No puedes hacerMATCHcontra una columna normal. Si necesitas búsqueda sobre una tabla ya existente, usa el patrón de external content que vimos antes.- No olvides ordenar por
rank. Sin eso, FTS5 te devuelve las filas en el orden en que están almacenadas, que no tiene nada que ver con la relevancia. - El tokenizer importa, y mucho. El tokenizer por defecto (
unicode61) parte por los límites de palabra Unicode y pasa todo a minúsculas. Si quieres stemming (queruncoincida conrunning), usa el tokenizerporter:USING fts5(body, tokenize='porter'). - FTS5 no tolera erratas. Hace coincidencia por prefijo, no búsqueda difusa. Si quieres un "¿quizás quisiste decir...?", eso es una capa por encima de FTS5.
- Las tablas sin contenido (
content='') ocupan menos pero pierden información. Puedes buscar en ellas, pero no recuperar el texto original — solo el rowid. Útil cuando guardas el texto en otro sitio.
Lo que viene: funciones de ventana
Con FTS5 ya tienes cubierta la búsqueda de texto. La siguiente página entra en otro tipo de consulta avanzada: las funciones de ventana, que te permiten calcular totales acumulados, rankings y analíticas por grupo sin colapsar las filas en agregados.
Preguntas frecuentes
¿Qué es FTS5 en SQLite?
FTS5 es la extensión de búsqueda full-text que viene incluida en SQLite. La idea es crear una tabla virtual especial con CREATE VIRTUAL TABLE ... USING fts5(...) y consultarla con el operador MATCH. Al insertar, FTS5 tokeniza el texto, guarda un índice invertido y, por defecto, ordena los resultados con BM25.
¿En qué se diferencian MATCH y LIKE en SQLite?
LIKE hace un escaneo lineal buscando subcadenas y no entiende de palabras. MATCH, en cambio, se apoya en el índice invertido de FTS5, así que es muy rápido en tablas grandes y reconoce tokens, búsquedas por prefijo (term*), operadores booleanos (AND, OR, NOT) y frases exactas ("frase exacta"). Eso sí, MATCH solo funciona sobre tablas virtuales FTS.
¿Cómo mantengo el índice FTS5 sincronizado con la tabla original?
Tienes dos caminos: usar una tabla FTS5 contentless o de external-content que apunte a tu tabla real, o crear triggers AFTER INSERT, AFTER UPDATE y AFTER DELETE que reflejen los cambios en la tabla FTS. La opción de external-content (content='posts') tiene la ventaja de que no duplica el texto.
¿Cómo ordeno los resultados de una búsqueda full-text en SQLite?
FTS5 expone una columna oculta rank con la puntuación BM25 (cuanto más bajo, mejor). Lo más directo es ORDER BY rank. También puedes llamar a bm25(tabla) para obtener la puntuación de forma explícita, o pasarle pesos por columna como bm25(posts, 10.0, 1.0) si quieres que el título pese más que el cuerpo.