Búsqueda lenta de texto completo para términos con alta ocurrencia

8

Tengo una tabla que contiene datos que se extraen de documentos de texto. Los datos se almacenan en una columna llamada "CONTENT"para la cual he creado este índice usando GIN:

CREATE INDEX "File_contentIndex"
  ON "File"
  USING gin
  (setweight(to_tsvector('english'::regconfig
           , COALESCE("CONTENT", ''::character varying)::text), 'C'::"char"));

Utilizo la siguiente consulta para realizar una búsqueda de texto completo en la tabla:

SELECT "ITEMID",
  ts_rank(setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C') , 
  plainto_tsquery('english', 'searchTerm')) AS "RANK"
FROM "File"
WHERE setweight(to_tsvector('english', coalesce("CONTENT",'')), 'C') 
  @@ plainto_tsquery('english', 'searchTerm')
ORDER BY "RANK" DESC
LIMIT 5;

La tabla Archivo contiene 250 000 filas y cada "CONTENT"entrada consta de una palabra aleatoria y una cadena de texto que es igual para todas las filas.

Ahora, cuando busco una palabra aleatoria (1 hit en toda la tabla) la consulta se ejecuta muy rápido (<100 ms). Sin embargo, cuando busco una palabra que está presente en todas las filas, la consulta se ejecuta extremadamente lenta (10 minutos o más).

EXPLAIN ANALYZEmuestra que para la búsqueda de 1 acierto se realiza un Análisis de índice de mapa de bits seguido de un Análisis de montón de mapa de bits . Para la búsqueda lenta, se realiza un Seq Scan , que es lo que lleva tanto tiempo.

Por supuesto, no es realista tener los mismos datos en todas las filas. Pero dado que no puedo controlar los documentos de texto que cargan los usuarios, ni las búsquedas que realizan, es posible que surja un escenario similar (buscar términos con una ocurrencia muy alta en DB). ¿Cómo puedo aumentar el rendimiento de mi consulta de búsqueda para tal escenario?

Ejecutar PostgreSQL 9.3.4

Consultar planes de EXPLAIN ANALYZE:

Búsqueda rápida (1 acierto en DB)

"Limit  (cost=2802.89..2802.90 rows=5 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
"  ->  Sort  (cost=2802.89..2806.15 rows=1305 width=26) (actual time=0.037..0.037 rows=1 loops=1)"
"        Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''wfecg'''::tsquery))"
"        Sort Method: quicksort  Memory: 25kB"
"        ->  Bitmap Heap Scan on "File"  (cost=38.12..2781.21 rows=1305 width=26) (actual time=0.030..0.031 rows=1 loops=1)"
"              Recheck Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
"              ->  Bitmap Index Scan on "File_contentIndex"  (cost=0.00..37.79 rows=1305 width=0) (actual time=0.012..0.012 rows=1 loops=1)"
"                    Index Cond: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''wfecg'''::tsquery)"
"Total runtime: 0.069 ms"

Búsqueda lenta (250k hits en DB)

"Limit  (cost=14876.82..14876.84 rows=5 width=26) (actual time=519667.404..519667.405 rows=5 loops=1)"
"  ->  Sort  (cost=14876.82..15529.37 rows=261017 width=26) (actual time=519667.402..519667.402 rows=5 loops=1)"
"        Sort Key: (ts_rank(setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char"), '''cyberspace'''::tsquery))"
"        Sort Method: top-N heapsort  Memory: 25kB"
"        ->  Seq Scan on "File"  (cost=0.00..10541.43 rows=261017 width=26) (actual time=2.097..519465.953 rows=261011 loops=1)"
"              Filter: (setweight(to_tsvector('english'::regconfig, (COALESCE("CONTENT", ''::character varying))::text), 'C'::"char") @@ '''cyberspace'''::tsquery)"
"              Rows Removed by Filter: 6"
"Total runtime: 519667.429 ms"
danjo
fuente
1
Desde lo alto de mi cabeza: los índices GIN han recibido mejoras importantes en Postgres 9.4 (y algunos más en los próximos 9.5). Sin duda pagará para actualizar a la actual 9.4. Y también investigaría el rendimiento de GiST en lugar del índice GIN. El culpable en su consulta es ORDER BY "RANK" DESC. Investigaría pg_trgmcon el índice GiST y los operadores de similitud / distancia como alternativa. Considere: dba.stackexchange.com/questions/56224/… . Incluso podría producir "mejores" resultados (además de ser más rápido).
Erwin Brandstetter
¿En qué sistema operativo está ejecutando su instancia de PostgreSQL?
Kassandry
¿Puedes repetir esto con explain (analyze, buffers), preferiblemente con track_io_timing establecido en ON? No hay forma de que se tarde 520 segundos en explorar esa tabla, a menos que la tenga almacenada en un RAID de disquetes. Algo es definitivamente patológico allí. Además, ¿cuál es su configuración random_page_costy los otros parámetros de costo?
jjanes
@danjo Estoy enfrentando los mismos problemas incluso cuando no uso el pedido. ¿Puedes decirme cómo lo arreglaste?
Sahil Bahl

Respuestas:

11

Caso de uso cuestionable

... cada entrada de CONTENIDO consiste en una palabra aleatoria y una cadena de texto que es igual para todas las filas.

Una cadena de texto que es igual para todas las filas es solo carga muerta. Elimínelo y concatenelo en una vista si necesita mostrarlo.

Obviamente, eres consciente de eso:

De acuerdo, no es realista ... Pero como no puedo controlar el texto ...

Actualiza tu versión de Postgres

Ejecutar PostgreSQL 9.3.4

Mientras todavía esté en Postgres 9.3, al menos debería actualizar a la última versión de punto (actualmente 9.3.9). La recomendación oficial del proyecto:

Siempre recomendamos que todos los usuarios ejecuten la última versión menor disponible para cualquier versión principal que esté en uso.

Mejor aún, actualice a 9.4, que ha recibido importantes mejoras para los índices GIN .

Problema principal 1: estimaciones de costos

El costo de algunas funciones de búsqueda de texto se ha subestimado seriamente hasta la versión 9.4 incluida. Ese costo es elevado por el factor 100 en la próxima versión 9.5 como @jjanes describe en su respuesta reciente:

Aquí está el hilo respectivo donde se discutió esto y el mensaje de confirmación de Tom Lane.

Como puede ver en el mensaje de confirmación, se to_tsvector()encuentra entre esas funciones. Puede aplicar el cambio inmediatamente (como superusuario):

ALTER FUNCTION to_tsvector (regconfig, text) COST 100;

lo que debería hacer que sea mucho más probable que se use su índice funcional.

Problema principal 2: KNN

El problema central es que Postgres tiene que calcular un rango con ts_rank()260k filas ( rows=261011) antes de que pueda ordenar y elegir los 5 mejores. Esto será costoso , incluso después de que haya solucionado otros problemas como se discutió. Es un problema de K-vecino más cercano (KNN) por naturaleza y hay soluciones para casos relacionados. Pero no puedo pensar en una solución general para su caso, ya que el cálculo del rango en sí depende de la entrada del usuario. Intentaría eliminar la mayor parte de las partidas de bajo rango temprano para que el cálculo completo solo tenga que hacerse para unos pocos buenos candidatos.

Una forma en la que puedo pensar es combinar su búsqueda de texto completo con la búsqueda de similitud de trigrama , que ofrece una implementación funcional para el problema KNN. De esta manera, puede preseleccionar las "mejores" coincidencias con el LIKEpredicado como candidatos (en una subconsulta con, LIMIT 50por ejemplo) y luego seleccionar las 5 filas de mayor clasificación según su cálculo de clasificación en la consulta principal.

O aplique ambos predicados en la misma consulta y elija las coincidencias más cercanas de acuerdo con la similitud de trigrama (que produciría resultados diferentes) como en esta respuesta relacionada:

Investigué un poco más y no eres el primero en encontrarte con este problema. Publicaciones relacionadas en pgsql-general:

Se está trabajando para implementar eventualmente un tsvector <-> tsqueryoperador.

Oleg Bartunov y Alexander Korotkov incluso presentaron un prototipo funcional (utilizando ><como operador en lugar de en <->aquel entonces) pero es muy complejo de integrar en Postgres, toda la infraestructura para los índices de GIN tiene que ser modificada (la mayoría de los cuales ya está hecha).

Problema principal 3: pesos e índice

E identifiqué un factor más que aumenta la lentitud de la consulta. Por documentación:

Los índices GIN no son perdidosos para consultas estándar, pero su rendimiento depende logarítmicamente de la cantidad de palabras únicas. ( Sin embargo, los índices GIN almacenan solo las palabras (lexemas) de los tsvectorvalores, y no sus etiquetas de peso. Por lo tanto, es necesario volver a comprobar la fila de la tabla cuando se usa una consulta que involucra pesos).

El énfasis en negrita es mío. Tan pronto como interviene el peso, cada fila debe recuperarse del montón (no solo un control de visibilidad barato) y los valores largos deben eliminarse, lo que aumenta el costo. Pero parece haber una solución para eso:

Definición de índice

Mirando su índice nuevamente, no parece tener sentido para comenzar. Asigne un peso a una sola columna, lo que no tiene sentido siempre que no concatene otras columnas con un peso diferente .

COALESCE() tampoco tiene sentido siempre que no concatene más columnas.

Simplifica tu índice:

CREATE INDEX "File_contentIndex" ON "File" USING gin
(to_tsvector('english', "CONTENT");

Y tu consulta:

SELECT "ITEMID", ts_rank(to_tsvector('english', "CONTENT")
                       , plainto_tsquery('english', 'searchTerm')) AS rank
FROM   "File"
WHERE  to_tsvector('english', "CONTENT")
       @@ plainto_tsquery('english', 'searchTerm')
ORDER  BY rank DESC
LIMIT  5;

Todavía es costoso para un término de búsqueda que coincide con cada fila, pero probablemente mucho menos.

Aparte

Todos estos problemas combinados, el costo loco de 520 segundos para su segunda consulta está comenzando a tener sentido. Pero aún puede haber más problemas. ¿Configuraste tu servidor?
Se aplican todos los consejos habituales para la optimización del rendimiento.

Te hace la vida más fácil si no trabajas con identificadores de mayúsculas y minúsculas CaMeL:

Erwin Brandstetter
fuente
Me estoy encontrando con esto también. Con Postgresql 9.6, usamos la reescritura de consultas para sinónimos, así que no creo que usar la búsqueda de similitud de trigrama para limitar el número de documentos funcione bien.
pholly
¡Asombroso! USING gin (to_tsvector('english', "CONTENT")
K-Gun
1

Tuve un problema similar Me ocupé de ello precalculando el ts_rank de cada término de consulta de texto popular en un campo: tupla de tabla y almacenándolo en una tabla de búsqueda. Esto me ahorró mucho tiempo (factor de 40X) durante la búsqueda de palabras populares en el corpus pesado de texto.

  1. Obtenga palabras populares en el corpus escaneando el documento y contando sus ocurrencias.
  2. Ordenar por palabra más popular.
  3. precalcule ts_rank de las palabras populares y guárdelo en una tabla.

Consulta: busque esta tabla y obtenga los identificadores de documentos ordenados por su rango respectivo. si no está allí, hágalo a la antigua usanza.

Pari Rajaram
fuente