- La búsqueda de texto completo de PostgreSQL es un enfoque para construir funciones de búsqueda sin un motor de búsqueda separado, combinando
tsvector,tsquery,@@,ts_ranke índices GIN - Normaliza las consultas y los documentos a nivel de lexeme y permite expresar consultas con operadores como AND, OR, NOT y FOLLOWED BY, con lo que se puede implementar gran parte de la sintaxis típica de búsqueda
- En el entorno de ejemplo, el índice GIN redujo el tiempo de búsqueda de más de 200 ms a alrededor de 4 ms, pero cuando aumentan los resultados, el costo de ranking y ordenamiento de
ts_rankpuede convertirse en un cuello de botella - El ajuste de relevancia se implementa agregando señales como peso del título, número de votos, calificación, género y actualidad a la expresión de ordenamiento, o asignando pesos por columna con
setweight - También son posibles la tolerancia a errores tipográficos, la búsqueda facetada, el autocompletado, la búsqueda exacta de frases y la búsqueda híbrida, pero en PostgreSQL hay que ensamblar los componentes manualmente y verificar los límites de rendimiento en conjuntos de datos grandes
Enfoque de la búsqueda de texto completo en PostgreSQL
- PostgreSQL ofrece componentes de bajo nivel para búsqueda de texto completo, que se pueden combinar para crear funciones de motor de búsqueda
- Este enfoque es flexible, pero requiere más trabajo de implementación que Elasticsearch, Typesense o Meilisearch, que están pensados principalmente para búsqueda de texto completo
- La consulta de ejemplo usa el dataset Wikipedia Movie Plots de Kaggle
- Incluye 34,000 títulos de películas
- El tamaño en formato CSV es de aproximadamente 81 MB
Componentes clave
- La búsqueda de texto completo de PostgreSQL gira en torno a los siguientes elementos
tsvector: almacena el texto objetivo de búsqueda como una lista de lexemes normalizadostsquery: representa una consulta de búsqueda normalizada@@: operador de coincidencia que verifica si untsquerycoincide con untsvectorts_rank,ts_rank_cd: calculan la puntuación de relevancia de los resultados- Índice GIN: índice invertido para consultar
tsvectorde forma eficiente
tsvector y configuración de búsqueda
tsvectoralmacena una lista ordenada de lexemes- Un lexeme es parecido a un token, pero es una cadena normalizada para que distintas formas de una misma palabra se reduzcan a una sola
- En la configuración para inglés, la normalización convierte mayúsculas a minúsculas y elimina sufijos
- Al analizar una frase en inglés con
to_tsvector, se eliminan stop words como “I”, “to” y “an”- “refuse” y “Refusing” se convierten ambas en
refus - Se ignora la puntuación
- También se registran la posición de las palabras y sus pesos en el texto original
- “refuse” y “Refusing” se convierten ambas en
- Si se usa la configuración
simpleen lugar deenglish, las palabras se incluyen tal como aparecen en el texto- “refuse” y “refusing” permanecen como lexemes distintos
- La configuración
simplees especialmente útil para columnas con etiquetas o tags
- PostgreSQL ofrece configuraciones de búsqueda integradas para varios idiomas, pero no incluye configuración para CJK (chino, japonés y coreano)
- Para idiomas no soportados, la configuración
simplepuede funcionar razonablemente bien en la práctica - Aun así, no está claro si será suficiente para CJK
- Para idiomas no soportados, la configuración
tsquery y expresión de consultas
tsqueryes un tipo de dato que representa consultas de búsqueda normalizadas- Los términos de búsqueda ya deben estar normalizados como lexemes
- Se pueden combinar varios términos con operadores AND, OR, NOT y FOLLOWED BY
to_tsquery,plainto_tsqueryywebsearch_to_tsqueryayudan a convertir el texto ingresado por el usuario en untsqueryadecuado- Su función principal es normalizar las palabras incluidas en el texto de entrada
- Con
websearch_to_tsqueryse pueden crear consultas más parecidas a una caja de búsqueda comúndarth vaderse procesa como un AND lógico que exige que ambas palabras estén en el documento- También permite búsqueda OR y exclusión de palabras
- La búsqueda de frases expresa que las palabras aparezcan seguidas y en orden
- En la configuración para inglés, se eliminan stop words como “the”, por lo que en algunas búsquedas de frases casi toda la expresión puede desaparecer
- En esos casos, usar la configuración
simplepuede dar el resultado esperado
- En esos casos, usar la configuración
- El operador
@@se usa para verificar si untsquerycoincide con untsvector
Índice GIN y rendimiento de búsqueda
- GIN significa Generalized Inverted Index y es un tipo de índice diseñado para consultas que buscan valores de elementos contenidos dentro de valores compuestos
- GIN no solo sirve para búsqueda de texto, también puede usarse en consultas JSON
- Se puede crear una columna
tsvectorque combine varias columnas buscables y luego crear sobre ella un índice GIN - En el entorno de ejemplo, el índice GIN redujo el tiempo de búsqueda de más de 200 ms a alrededor de 4 ms
Ranking y cálculo de relevancia
- Para una buena experiencia de búsqueda, los resultados deben ordenarse por relevancia
- PostgreSQL ofrece dos funciones de ranking predefinidas:
ts_rankyts_rank_cd- Ambas consideran la frecuencia de aparición de los términos de búsqueda
ts_rank_cdtambién refleja la proximidad entre los lexemes coincidentes
- La relevancia depende en gran medida de la naturaleza de la aplicación
- Las funciones de ranking base son más bien un punto de partida; según sea necesario, se puede crear una función propia o combinarlas con otros factores
ts_ranknecesita acceder a la columnasearchde cada resultado- Si muchas filas coinciden con la condición
WHERE, PostgreSQL debe visitar todas esas filas para calcular el ranking y ordenarlas - En el entorno de ejemplo, una consulta devolvía resultados en 5~7 ms, pero una consulta como
darth OR vader, que debía rankear más de 1,000 resultados, tardó alrededor de 80 ms
- Si muchas filas coinciden con la condición
Ajuste de relevancia
- La relevancia basada en frecuencia de palabras es un buen valor por defecto, pero los datos pueden tener señales más importantes que la frecuencia
- En el dataset de películas, se pueden reflejar las siguientes señales en la relevancia
- Tratar como más importantes las coincidencias en el título que las coincidencias en la descripción o la trama
- Promover películas más populares según calificación o número de votos
- Si el usuario prefiere comedia, ubicar más arriba las películas de comedia
- Considerar más relevantes los títulos recientes que los antiguos
- Los motores de búsqueda dedicados ofrecen funciones para configurar cómo influyen diferentes columnas o campos en el ranking
- Como documentación relacionada, pueden verse Elastic, Typesense y Meilisearch
Boosting con números, fechas y valores exactos
- PostgreSQL no ofrece boosting basado en otras columnas de forma directa, pero como el ranking al final es una expresión de ordenamiento, se pueden sumar señales propias
- Para reflejar el número de votos, puede implementarse agregando al score de ranking un boost basado en esa cantidad
- En el ejemplo se usa un logaritmo para suavizar el impacto
- El coeficiente
0.01ajusta el booster a una escala similar a la del score de ranking
- También se pueden crear funciones más complejas que impulsen la calificación solo cuando el número de votos supera cierto nivel
- Si se quiere promover un género específico, se puede usar una función como
valueBoosterque devuelva un coeficiente solo cuando el valor coincida con cierto valor de columna
Pesos por columna
- A los lexemes de
tsvectorse les puede asignar peso - PostgreSQL admite cuatro pesos: A, B, C y D
- A es el peso más alto
- D es el más bajo y el valor por defecto
- La función
setweightpermite controlar esos pesos al construir una columnatsvector - Si se da más peso a la columna del título, las películas cuyo término de búsqueda aparece en el título subirán en los resultados y también aumentará su score de ranking
- El hecho de que solo existan cuatro clases de peso es una limitación, y los pesos deben aplicarse al calcular el
tsvector
Tolerancia a errores tipográficos y búsqueda difusa
- PostgreSQL no ofrece de forma nativa búsqueda difusa ni tolerancia a errores tipográficos cuando se usan
tsvectorytsquery - Suponiendo que el error tipográfico está en la consulta, se puede implementar así
- Indexar todos los lexemes del contenido en una tabla separada
- Para cada palabra de la consulta, buscar palabras candidatas por similitud o distancia de Levenshtein
- Modificar la consulta para incluir las palabras encontradas
- Ejecutar la búsqueda con la consulta modificada
- En el ejemplo se usa Levenshtein distance porque es el método que usan los motores de búsqueda para búsqueda difusa
- Después de obtener la lista de palabras candidatas, la consulta debe ajustarse para incluirlas todas
Búsqueda facetada
- La búsqueda facetada se usa mucho, especialmente en sitios de comercio electrónico, para que el usuario reduzca iterativamente el alcance de una búsqueda
- En PostgreSQL, se puede implementar definiendo categorías manualmente y agregándolas como condición
WHEREde la búsqueda - También es posible generar categorías algorítmicamente a partir de los datos existentes
- En el ejemplo se crea una faceta “Decade” basada en el año de la película
- También se puede calcular la cantidad de coincidencias por década y mostrarla entre paréntesis
- Para obtener varias facetas en una sola consulta, se pueden combinar CTE
- Este enfoque puede funcionar bien con datasets pequeños o medianos, pero puede volverse lento en datasets muy grandes
Alcance y límites del motor de búsqueda con PostgreSQL
- Combinando los componentes de búsqueda de texto completo de PostgreSQL, se puede construir un motor de búsqueda bastante avanzado
- La búsqueda basada en PostgreSQL también admite joins y transacciones ACID
- Son capacidades que otros motores de búsqueda normalmente no tienen
- Otros temas avanzados de búsqueda que vale la pena tratar incluyen
- Sugerencias y autocompletado
- Coincidencia exacta de frases
- Búsqueda híbrida combinada con
pg-vector
- Estas funciones son posibles con PostgreSQL, pero hay que ensamblar los componentes manualmente
- En algunos casos, el rendimiento puede degradarse con datasets muy grandes
- La publicación de seguimiento, part 2, compara implementar búsqueda en PostgreSQL frente a agregar Elasticsearch a la infraestructura y sincronizar los datos
1 comentarios
Comentarios en Hacker News
No deberías intentar hacer esto imitando funcionalidades de Elasticsearch
En los 2000 hice un motor de búsqueda con MySQL 3.x que parseaba datos EXIF de imágenes e indexaba una taxonomía de 3 niveles con conteos; era algo que varios proveedores caros tampoco lograban hacer bien, y Autonomy ni siquiera podía con la clasificación de nivel superior
Después de 6 semanas escribiendo SQL frágil donde hasta cambiar el orden de las columnas en un SELECT afectaba el rendimiento, al final salió, pero no quisiera volver a hacerlo jamás. Aunque en esencia las bases de datos siguen siendo bastante parecidas, los motores de búsqueda han avanzado mucho más
Se puede hacer por curiosidad intelectual, pero la búsqueda no es solo tokenizar y listo. Muy pronto llegan requisitos como navegación por categorías, soporte multilingüe, sinónimos automáticos, sugerencias ortográficas tipo "Did you mean" y rendimiento a gran escala, y terminas atrapado en un callejón sin salida. Por salud mental, es mejor usar un motor de búsqueda, y para sincronizar PG con ES también hay herramientas como ZomboDB o PGSync
En cambio, si un analista de datos o un desarrollador quiere buscar en columnas de texto de una base de datos grande con más flexibilidad que
LIKE/ILIKE, puede ser más fácil y rápido llegar al 90% creando un índice/tabla de búsqueda de texto completo dentro de la misma base de datosTengo curiosidad por la segunda parte comparando Postgres vs Elasticsearch. Una aplicación de la empresa usa PG para CRUD de objetos y Elastic para búsqueda, y subestimamos por completo el esfuerzo de mantener sincronizados ambos almacenes; de hecho, estamos considerando quitar Elasticsearch
Cada 5 minutos un cron buscaba en la DB los objetos a indexar con
last_modified_at > last_indexing_started_timestamp, los indexaba en Elasticsearch y luego actualizabalast_indexing_started_timestampcon la hora de inicio de la sincronización. Así, cualquier objeto modificado entre el inicio y el fin de la ejecución se recoge en la siguiente corridaSi hay que reconstruir Elasticsearch, basta con vaciar la última marca de indexación y volver a sincronizar desde cero; eso permite autorecuperación y evita que la sincronización se descuadre
Como siempre con trabajos asíncronos, hace falta monitoreo y reintentos, pero ES era estable y rápido, así que rara vez había problemas. Eso sí, nuestros requisitos de consistencia eran relajados: bastaba con que el estado más reciente de PG llegara a ES en un tiempo razonable; con otros requisitos podría ser distinto
Después migramos a búsqueda de texto completo en Postgres con ponderaciones en índices y consultas, y con solo triggers de actualización y consultas de búsqueda muy rápidas pudimos resolver todo lo que necesitábamos dentro de Postgres
La mayoría quedó parecida y solo una pequeña parte salió mejor
En general fue un éxito y redujo bastante la carga operativa, así que recuperamos de sobra el tiempo de ingeniería invertido, pero no es algo para empezar a la ligera
Dependiendo de la necesidad, una vista materializada, una vista normal o triggers podrían ser mejores. Puede que la búsqueda de texto integrada no encaje con tu caso de uso, y construir una alternativa no necesariamente tiene que ser tan difícil
Sin latencias p50/p99 de consulta bajo carga realista, es difícil considerarlo demostrado. Un motor de búsqueda que devuelve resultados en 1 minuto no es "avanzado", y una base de datos relacional como Postgres también puede hacerlo, al menos sobre el papel
En general estoy de acuerdo, y a gran escala, por ejemplo con varios millones de registros o más, probablemente no usaría este enfoque. Mi interés principal era ver hasta qué punto se podían replicar las funcionalidades
Para casos de búsqueda pequeños, tiene ventajas como menos infraestructura que mantener, consistencia fuerte y joins. En Xata también estamos pensando en una transición suave: usar Postgres a pequeña escala y luego pasar a Elasticsearch con el mínimo de cambios disruptivos
Desde esa perspectiva, que los resultados salgan rápido no sorprende tanto. Puedes seguir refrescando resultados cacheados en segundo plano y entregarlos tal cual cuando llegue una solicitud. Este tema de caché y tiempos de respuesta parece separado de la velocidad real de cálculo de resultados de búsqueda
Hay gente con conexiones lentas, y ciertos términos como terremoto pueden dispararse solo en regiones específicas
Si las consultas son demasiado aleatorias, la caché no tendrá resultados y el desempeño se verá peor de lo real; si no son lo bastante aleatorias, se verá mejor de lo real
Por lo tanto, la ventaja de ES y similares está en el escalado horizontal entre varios nodos o en las funcionalidades adicionales que se montan sobre el índice principal
ts_rankpor sí solo está bien, pero normalmente terminas queriendo ajustar el orden con otras métricas de relevancia. Si resuelves que esa métrica no puede ser el criterio principal de ranking, los resultados son tan rápidos como consultar una tabla normal de DB con índiceCuando era adolescente intenté construir desde cero tanto un motor de búsqueda como una base de datos, sin entender muy bien ninguna de las dos cosas. Quería ver hasta dónde podía llegar y qué tan rápido y qué tan relevantes podían ser los resultados que devolviera
Construir rápidamente una base de datos y un motor de búsqueda básicos probablemente sea bastante fácil incluso para un programador amateur. Si entiendes los algoritmos básicos de ciencias de la computación y cómo aprovechar el sistema operativo y el hardware, puedes hacerlo en uno o dos meses
Incluso con un lenguaje de alto nivel, la velocidad no era mala, y en una laptop de 2003 daba alrededor de 250 mil QPS. Con sharding, la escalabilidad tampoco es un gran problema. Indexar, bloquear y mantener consistencia es más complejo que almacenar y consultar
El verdadero gran problema es la subjetividad de la búsqueda. Cuando aparecen preguntas como qué es lo que realmente quieres encontrar, cómo buscar cuando ni siquiera sabes bien qué buscas, cómo impedir que la gente abuse del sistema, y cómo manejar consultas y datasets complejos, la dificultad sube varios órdenes de magnitud
Me da curiosidad saber si de verdad usaste una base de datos de propósito general y si ese número es correcto. Quisiera saber qué técnicas usaste para superar a esos motores
Cuando crece más allá de eso, hacen falta soluciones creativas, y a partir de ahí empiezan a aparecer varios trade-offs
Uno de los varios problemas al buscar dentro de Postgres es que la búsqueda es una tarea intensiva en CPU, y cada vez más también se está yendo hacia usar GPU. Idealmente, quieres reservar la CPU de la base de datos para las actualizaciones transaccionales del modelo de datos principal
He visto muchos casos donde clústeres de ES y Solr, durante el reindexado, corren al 100% en más de 10 nodos, o incluso en operación normal usan 30~50% en más de 10 nodos. La base de datos correspondiente podría ser, por ejemplo, una instancia AWS L/XL con 50~100GB de datos y uso de CPU de alrededor de 30%
Si mueves toda la CPU de búsqueda a la BD principal, entonces ahora también tienes que hacer sharding de la BD. Aun así, para side projects me gustan las extensiones de PG para búsqueda, joins recursivos, vectores, etc., porque son divertidas y simples
Ya había hecho esto en la práctica hace tiempo, y lo construí bastante rápido
https://austingwalters.com/fast-full-text-search-in-postgres...
Actualmente el sitio web es https://askhn.ai
[1]: https://en.wikipedia.org/wiki/Kerning
Si combinas este tipo de técnicas con pgvector, también puedes encontrar contenido relacionado mediante embeddings. Se sintió bastante mágico
Por cierto, no es "Dark" Vader sino Darth Vader. Yo también pensaba que era "Dark" cuando era niño
Es un artículo muy bueno y claro. SQLite también ofrece funciones avanzadas de indexación y stemming como plugin estándar
Para inglés, SQLite también funciona bien
https://github.com/daitangio/knowledge
Vale la pena probarlo. Es bastante potente
Buen artículo, pero la parte donde dice que PostgreSQL no soporta búsqueda difusa es parcialmente incorrecta. La extensión
pg_trgmy los índices GIN de trigramas sí soportan casos de uso de búsqueda difusa como los ejemplos del artículohttps://www.postgresonline.com/article_pfriendly/169.html
Las consultas pueden volverse mucho más rápidas, pero eso viene con el costo de mayor uso de memoria y más trabajo de actualización