28 puntos por xguru 2024-08-26 | 2 comentarios | Compartir por WhatsApp
  • Se puede construir un motor de búsqueda híbrido dentro de Postgres que combine búsqueda semántica, de texto completo y difusa
  • La búsqueda es una parte importante de muchas apps, pero no es fácil implementarla bien. En especial en los pipelines de RAG, la calidad de la búsqueda puede determinar el éxito o el fracaso de todo el proceso
  • Aunque la búsqueda semántica está de moda, la búsqueda tradicional basada en léxico sigue siendo la columna vertebral de la búsqueda
  • Las técnicas semánticas pueden mejorar los resultados, pero funcionan mejor sobre una base sólida de búsqueda basada en texto

Implementar un motor de búsqueda con Postgres

  • Se combinan tres técnicas:
    • búsqueda de texto completo con tsvector
    • búsqueda semántica con pgvector
    • coincidencia difusa con pg_trgm
  • Puede que este enfoque no sea el mejor absoluto en todos los casos, pero es una excelente alternativa a construir un servicio de búsqueda separado
  • Es un punto de partida sólido que puede implementarse y escalarse dentro de una base de datos Postgres existente
  • Por qué usar Postgres para todo: Simplemente usa Postgres para todo, PostgreSQL es suficiente, Solo usa Postgres

Implementar FTS y búsqueda semántica

  • Supabase tiene una excelente documentación sobre cómo implementar búsqueda híbrida, así que se tomará como punto de partida
  • Siguiendo la guía, se implementa FTS con índices GIN y búsqueda semántica con pgvector, también conocida como bi-encoder dense retrieval
  • Según la experiencia personal, elegir embeddings de 1536 dimensiones puede dar resultados mucho mejores
  • Las funciones de Supabase se reemplazan por CTE y consultas, y se agrega $ delante de los parámetros
  • Aquí se usa RRF (Reciprocal Ranked Fusion) para fusionar los resultados
  • Este método garantiza que los elementos con una posición alta en varias listas reciban una posición alta en la lista final
  • También garantiza que los elementos con posición alta en algunas listas pero baja en otras no reciban una posición demasiado alta en la lista final
  • Calcular la puntuación poniendo el ranking en el denominador puede penalizar los registros con posiciones bajas
  • Puntos a destacar
    • $rrf_k: para evitar que la puntuación del elemento en primer lugar se vuelva extremadamente alta, algo común al dividir por el ranking, a menudo se agrega una constante k al denominador para suavizar la puntuación
    • $_weight: se puede asignar un peso a cada método. Esto es muy útil al ajustar los resultados

Implementar búsqueda difusa

  • Los métodos anteriores pueden resolver gran parte del problema, pero si hay errores tipográficos en entidades con nombre, pueden aparecer problemas inmediatos
  • La búsqueda semántica captura similitudes y reduce algunos de esos problemas, pero tiene dificultades con nombres, abreviaturas y otros textos que no son semánticamente similares
  • Para mitigar esto, se introduce la extensión pg_trgm para permitir búsqueda difusa
    • Funciona con trigramas. Los trigramas descomponen una palabra en secuencias de 3 caracteres, por lo que son útiles para búsqueda difusa
    • Esto permite encontrar coincidencias con palabras similares incluso si contienen errores tipográficos o ligeras variaciones
    • Por ejemplo, "hello" y "helo" comparten muchos trigramas, por lo que pueden coincidir más fácilmente en una búsqueda difusa
  • Se crea un nuevo índice sobre la columna deseada y luego se agrega a la consulta general de búsqueda
  • La extensión pg_trgm expone el operador %, que filtra textos cuya similitud es mayor que pg_trgm.similarity_threshold (valor predeterminado: 0.3)
  • También hay varios otros operadores útiles

Ajustar la búsqueda de texto completo

  • Ajustar los pesos de tsvector: los documentos reales incluyen no solo el título, sino también el contenido
  • Aunque haya varias columnas, se mantiene una sola columna de embeddings
  • Personalmente, se ha observado que mantener title y body en el mismo embedding no produce una gran diferencia de rendimiento frente a mantener varios embeddings
  • Al final, title debería ser una representación breve del cuerpo. Conviene experimentar según sea necesario
  • Se espera que el título sea corto y rico en palabras clave, mientras que el cuerpo será más largo y contendrá más detalles
  • Por eso hay que ajustar cómo se ponderan entre sí las columnas de búsqueda de texto completo
  • Se puede priorizar según la ubicación de las palabras en el documento o según su importancia
    • A-weight: lo más importante (por ejemplo, título, encabezados). Valor predeterminado 1.0
    • B-weight: importante (por ejemplo, inicio del documento, resumen). Valor predeterminado 0.4
    • C-weight: importancia estándar (por ejemplo, texto principal). Valor predeterminado 0.2
    • D-weight: lo menos importante (por ejemplo, notas al pie, anotaciones). Valor predeterminado 0.1
  • Ajustando estos pesos según la estructura del documento y las necesidades de la app, se puede afinar la relevancia
  • Por qué darle más peso al título
    • Porque normalmente el título expresa de forma concisa el tema principal del documento
    • Los usuarios tienden a revisar primero los títulos al buscar, así que una coincidencia de palabras clave en el título suele estar más relacionada con la intención del usuario que una coincidencia en el cuerpo del texto

Ajuste según la longitud

  • Si se lee la documentación de ts_rank_cd, se puede ver que existe un parámetro de normalización
    • Ambas funciones de ranking usan una opción entera normalization para indicar cómo debe afectar la longitud del documento al ranking. Como la opción entera controla varios comportamientos, se trata de una máscara de bits: se puede usar | para especificar uno o más comportamientos (por ejemplo, 2|4).

  • Con estas distintas opciones se puede
    • ajustar el sesgo por longitud del documento
    • equilibrar la relevancia en distintos conjuntos de documentos
    • ajustar los resultados del ranking para una representación consistente
  • Da buenos resultados establecer 0 (sin normalización) para el título y 1 (longitud logarítmica del documento) para el cuerpo
  • Como siempre, conviene experimentar con distintas opciones para encontrar la que mejor se adapte al caso de uso

Re-ranking con cross-encoder

  • Muchos sistemas de búsqueda constan de dos etapas
  • Es decir, primero recuperan los N resultados iniciales usando un bi-encoder y luego usan un cross-encoder para compararlos con la consulta de búsqueda y ordenarlos
    • bi-encoder: como es rápido, sirve bien para recuperar una gran cantidad de documentos
    • cross-encoder
      • es más lento, pero ofrece mejor rendimiento, por lo que es bueno para volver a ordenar los resultados recuperados
      • procesa la consulta y el documento juntos, lo que permite una comprensión más matizada de la relación entre ambos
      • esto ofrece mejor precisión en el ranking a costa de tiempo de cómputo y escalabilidad
  • Existen varias herramientas para hacer esto
  • Una de las mejores es Rerank de Cohere
  • Otro método es construirlo por cuenta propia usando el GPT de OpenAI
  • Los cross-encoders pueden aumentar la precisión de los resultados de búsqueda al comprender mejor la relación entre la consulta y el documento
  • Sin embargo, como su costo computacional es alto, tienen limitaciones en términos de escalabilidad
  • Por eso, suele ser efectivo un enfoque en dos etapas: usar un bi-encoder para la búsqueda inicial y aplicar el cross-encoder solo a un pequeño número de documentos recuperados

Cuándo conviene buscar una solución alternativa

  • PostgreSQL es una buena opción para muchos escenarios de búsqueda, pero no está libre de limitaciones
  • La ausencia de algoritmos avanzados como BM25 puede hacerse notar al manejar documentos de longitudes muy variadas
  • La búsqueda de texto completo de PostgreSQL depende de TF-IDF, por lo que puede tener dificultades con documentos muy largos y términos raros en colecciones grandes
  • Antes de buscar una solución alternativa, hay que medirlo. Puede que no valga la pena

Conclusión

  • Este artículo cubre muchos temas, desde búsqueda básica de texto completo hasta técnicas avanzadas como coincidencia difusa, búsqueda semántica y boosting de resultados
  • Aprovechando las potentes capacidades de Postgres, se puede crear un motor de búsqueda potente y flexible adaptado a necesidades específicas
  • Postgres quizá no sea la primera herramienta que viene a la mente para búsqueda, pero realmente puede llegar muy lejos
  • Claves para una gran experiencia de búsqueda
    • iteración y ajuste fino continuos
    • hay que usar las técnicas de depuración comentadas para entender el rendimiento de la búsqueda, y no temer ajustar pesos y parámetros según la retroalimentación y el comportamiento de los usuarios
  • PostgreSQL puede carecer de funciones de búsqueda avanzadas, pero en la mayoría de los casos permite construir un motor de búsqueda lo bastante potente
  • Antes de buscar una solución alternativa, conviene primero aprovechar al máximo las capacidades de Postgres y medir el rendimiento; si aun así no es suficiente, entonces se puede considerar otra solución

2 comentarios

 
eajrezz 2024-08-27

Me pregunto si también funciona bien para búsquedas en coreano.

 
xguru 2024-08-26

El tema del Weekly de hoy también fue Postgres, y aquí otra vez sale Postgres. Definitivamente parece que, en proporción a su popularidad, están saliendo muchos artículos jaja.
Si quieren revisar BM25, vean lo de abajo.

pg_bm25 - Extensión de búsqueda Full-Text para Postgres que ofrece una calidad al nivel de Elastic
ParadeDB - PostgreSQL for Search