Recherche lente de texte intégral pour les termes à occurrence élevée

8

J'ai un tableau qui contient des données extraites de documents texte. Les données sont stockées dans une colonne appelée "CONTENT"pour laquelle j'ai créé cet index à l'aide de GIN:

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

J'utilise la requête suivante pour effectuer une recherche en texte intégral sur la table:

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 table File contient 250 000 lignes et chaque "CONTENT"entrée se compose d'un mot aléatoire et d'une chaîne de texte identique pour toutes les lignes.

Maintenant, lorsque je recherche un mot aléatoire (1 hit dans tout le tableau), la requête s'exécute très rapidement (<100 ms). Cependant, lorsque je recherche un mot qui est présent dans toutes les lignes, la requête s'exécute extrêmement lentement (10 minutes ou plus).

EXPLAIN ANALYZEmontre que pour la recherche en une touche, un scan d'index bitmap suivi d'un scan de tas bitmap est effectué. Pour la recherche lente, une analyse Seq est effectuée à la place, ce qui prend si longtemps.

Certes, il n'est pas réaliste d'avoir les mêmes données dans toutes les lignes. Mais comme je ne peux pas contrôler les documents texte téléchargés par les utilisateurs, ni les recherches qu'ils effectuent, il est possible qu'un scénario similaire se produise (recherche sur des termes avec une occurrence très élevée dans la base de données). Comment puis-je augmenter les performances de ma requête de recherche pour un tel scénario?

Exécution de PostgreSQL 9.3.4

Plans de requête de EXPLAIN ANALYZE:

Recherche rapide (1 hit dans 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"

Recherche lente (250k hits dans 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
la source
1
Du haut de ma tête: les index GIN ont reçu des améliorations majeures dans Postgres 9.4 (et quelques autres dans la prochaine 9.5). Il sera certainement avantageux de passer à la version 9.4 actuelle. Et j'examinerais également les performances de GiST au lieu de l'indice GIN. Le coupable de votre requête est ORDER BY "RANK" DESC. J'enquête pg_trgmavec l' index GiST et les opérateurs similarité / distance , comme alternative. Considérez: dba.stackexchange.com/questions/56224/… . Peut même produire de "meilleurs" résultats (en plus d'être plus rapide).
Erwin Brandstetter
Sur quel système d'exploitation exécutez-vous votre instance PostgreSQL?
Kassandry
Pouvez-vous les répéter avec explain (analyze, buffers), de préférence avec track_io_timing défini sur ON? Il n'y a aucun moyen que cela prenne 520 secondes pour seq analyser cette table, sauf si vous l'avez stockée sur un RAID de disquettes. Quelque chose y est définitivement pathologique. En outre, quel est votre réglage random_page_costet les autres paramètres de coût?
jjanes
@danjo Je suis confronté aux mêmes problèmes même lorsque je n'utilise pas la commande. Pouvez-vous me dire comment vous l'avez résolu?
Sahil Bahl

Réponses:

11

Cas d'utilisation douteux

... chaque entrée de CONTENU se compose d'un mot aléatoire et d'une chaîne de texte identique pour toutes les lignes.

Une chaîne de texte identique pour toutes les lignes n'est que du fret mort. Retirez-le et concaténez-le dans une vue si vous avez besoin de le montrer.

De toute évidence, vous en êtes conscient:

Certes, ce n'est pas réaliste ... Mais comme je ne peux pas contrôler le texte ...

Mettez à niveau votre version Postgres

Exécution de PostgreSQL 9.3.4

Alors que vous êtes toujours sur Postgres 9.3, vous devriez au moins mettre à niveau vers la dernière version ponctuelle (actuellement 9.3.9). La recommandation officielle du projet:

Nous recommandons toujours à tous les utilisateurs d'exécuter la dernière version mineure disponible quelle que soit la version principale utilisée.

Mieux encore, passez à la version 9.4 qui a reçu des améliorations majeures pour les index GIN .

Problème majeur 1: Estimation des coûts

Le coût de certaines fonctions de recherche de texte a été sérieusement sous-estimé jusqu'à la version 9.4 incluse. Ce coût est augmenté du facteur 100 dans la prochaine version 9.5, comme @jjanes le décrit dans sa récente réponse:

Voici le fil respectif où cela a été discuté et le message de validation de Tom Lane.

Comme vous pouvez le voir dans le message de validation, to_tsvector()figure parmi ces fonctions. Vous pouvez appliquer la modification immédiatement (en tant que superutilisateur):

ALTER FUNCTION to_tsvector (regconfig, text) COST 100;

ce qui devrait beaucoup plus probable que votre index fonctionnel est utilisé.

Problème majeur 2: KNN

Le problème principal est que Postgres doit calculer un classement avec ts_rank()260k lignes ( rows=261011) avant de pouvoir passer commande et sélectionner le top 5. Cela va coûter cher , même après avoir résolu d'autres problèmes comme discuté. C'est un problème K-le plus proche voisin (KNN) par nature et il existe des solutions pour les cas connexes. Mais je ne peux pas penser à une solution générale pour votre cas, car le calcul du classement lui-même dépend de l'entrée de l'utilisateur. J'essaierais d'éliminer le plus grand nombre de matchs de bas niveau tôt afin que le calcul complet ne soit effectué que pour quelques bons candidats.

Une façon dont je peux penser est de combiner votre recherche de texte intégral avec la recherche de similitude de trigramme - qui offre une implémentation fonctionnelle pour le problème KNN. De cette façon, vous pouvez présélectionner les «meilleures» correspondances avec le LIKEprédicat en tant que candidats (dans une sous-requête avec LIMIT 50par exemple), puis sélectionner les 5 premières lignes en fonction de votre classement dans la requête principale.

Ou appliquez les deux prédicats dans la même requête et choisissez les correspondances les plus proches en fonction de la similitude des trigrammes (ce qui produirait des résultats différents) comme dans cette réponse connexe:

J'ai fait quelques recherches supplémentaires et vous n'êtes pas le premier à rencontrer ce problème. Related posts sur pgsql-general:

Des travaux sont en cours pour éventuellement mettre en place un tsvector <-> tsqueryopérateur.

Oleg Bartunov et Alexander Korotkov ont même présenté un prototype fonctionnel (utilisé ><comme opérateur au lieu de l' <->époque), mais son intégration dans Postgres est très complexe, toute l'infrastructure des index GIN doit être retravaillée (la plupart se fait maintenant).

Problème majeur 3: poids et indice

Et j'ai identifié un facteur supplémentaire s'ajoutant à la lenteur de la requête. Par documentation:

Les index GIN ne sont pas à perte pour les requêtes standard, mais leurs performances dépendent logarithmiquement du nombre de mots uniques. ( Cependant, les index GIN stockent uniquement les mots (lexèmes) des tsvectorvaleurs, et non leurs étiquettes de poids. Par conséquent, une nouvelle vérification de la ligne du tableau est nécessaire lors de l'utilisation d'une requête impliquant des pondérations.)

Accentuation mienne. Dès que le poids est impliqué, chaque ligne doit être récupérée à partir du tas (pas seulement un contrôle de visibilité bon marché) et les valeurs longues doivent être grillées, ce qui augmente le coût. Mais il semble y avoir une solution à cela:

Définition d'index

En examinant à nouveau votre index, il ne semble pas logique de commencer. Vous attribuez un poids à une seule colonne, ce qui n'a aucun sens tant que vous ne concaténez pas d'autres colonnes avec un poids différent .

COALESCE() n'a également aucun sens tant que vous ne concaténez pas plus de colonnes.

Simplifiez votre index:

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

Et votre requête:

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;

Toujours cher pour un terme de recherche qui correspond à chaque ligne, mais probablement beaucoup moins.

À part

Tous ces problèmes combinés, le coût insensé de 520 secondes pour votre deuxième requête commence à faire sens. Mais il peut encore y avoir plus de problèmes. Avez-vous configuré votre serveur?
Tous les conseils habituels pour l'optimisation des performances s'appliquent.

Cela vous simplifie la vie si vous ne travaillez pas avec des identificateurs de casse CaMeL entre guillemets doubles:

Erwin Brandstetter
la source
Je rencontre aussi cela. Avec Postgresql 9.6, nous utilisons la réécriture de requêtes pour les synonymes, donc je ne pense pas que l'utilisation de la recherche de similitude de trigramme pour limiter le nombre de documents fonctionnera bien.
pholly
Incroyable! USING gin (to_tsvector('english', "CONTENT")
K-Gun
1

J'ai eu un problème similaire. J'ai pris soin de cela en précalculant le ts_rank de chaque terme de requête de texte populaire par rapport à un champ: table tuple et en le stockant dans une table de recherche. Cela m'a fait gagner beaucoup de temps (facteur 40X) lors de la recherche de mots populaires dans le corpus lourd de texte.

  1. Obtenez des mots populaires dans le corpus en scannant le document et en comptant ses occurrences.
  2. Trier par le mot le plus populaire.
  3. précalculer ts_rank des mots populaires et les stocker dans une table.

Requête: recherchez ce tableau et obtenez les identifiants des documents triés par leur rang respectif. sinon, faites-le à l'ancienne.

Pari Rajaram
la source