8

J'ai JSONB assez complexe stocké dans une colonne jsonb.Comment mettre en œuvre la recherche en texte intégral sur JSONB complexe imbriqué dans Postgresql

Table DB ressemble:

CREATE TABLE sites (
    id text NOT NULL, 
    doc jsonb, 
    PRIMARY KEY (id) 
) 

données nous stocke dans la colonne doc est une donnée JSONB imbriquée complexe:

{ 
     "_id": "123", 
     "type": "Site", 
     "identification": "Custom ID", 
     "title": "SITE 1", 
     "address": "UK, London, Mr Tom's street, 2", 
     "buildings": [ 
      { 
       "uuid": "12312", 
       "identification": "Custom ID", 
       "name": "BUILDING 1", 
       "deposits": [ 
        { 
         "uuid": "12312", 
         "identification": "Custom ID",    
         "audits": [ 
          { 
          "uuid": "12312",   
           "sample_id": "SAMPLE ID"     
          } 
         ] 
        } 
       ] 
      } 
     ] 
    } 

la structure donc de mon JSONB ressemble:

SITE 
    -> ARRAY OF BUILDINGS 
    -> ARRAY OF DEPOSITS 
     -> ARRAY OF AUDITS 

Nous devons implémenter la recherche plein texte valeurs me dans chaque type d'entrée:

SITE (identification, title, address) 
BUILDING (identification, name) 
DEPOSIT (identification) 
AUDIT (sample_id) 

La requête SQL doit exécuter une recherche de texte intégral uniquement dans ces valeurs de champ.

Je suppose qu'il faut utiliser les index GIN et quelque chose comme tsvector, mais pas assez d'arrière-plan Postgresql. Donc, ma question est-il possible d'indexer puis d'interroger des structures JSONB imbriquées?

+1

Le premier coup serait de "dénormaliser" le stockage: sacrifiez un peu d'espace de stockage par souci de concision. Extraire les données nécessaires dans des champs séparés: site, construction, dépôt, audit, contenant la contanténation pure des champs nécessaires, c'est-à-dire 'building.identification || ';' || building.title || ';' || building.address' etc (cela peut être fait en utilisant les fonctions de postgres comme valeurs par défaut, ou basé sur un trigger, si vos données sont modifiées). Ensuite, créez des index GIN sur ces champs -> puis construisez vos requêtes de texte intégral correspondantes sur ces champs –

+0

Merci @IlyaDyoshin. J'aime ton idée - j'essaierai de l'expérimenter. – rusllonrails

+0

ou vous pouvez attendre jusqu'à la version 10.0 - où json/jsonb FTS sera citoyen de première classe https://www.postgresql.org/docs/10/static/release-10.html –

Répondre

3

Ajoutons une nouvelle colonne de type tsvector:

alter table sites add column tsvector tsvector; 

Maintenant, nous allons créer un déclencheur qui permettra de recueillir lexèmes, les organiser et de mettre à notre tsvector. Nous allons utiliser 4 groupes (A, B, C, D) - ceci est une fonction spéciale de tsvector qui vous permet de distinguer les lexèmes plus tard, au moment de la recherche (voir les exemples dans le manuel https://www.postgresql.org/docs/current/static/textsearch-controls.html) malheureusement cette fonction ne prend en charge que 4 groupes développeurs réservé seulement 2 bits pour cela, mais nous avons la chance ici, nous avons besoin de seulement 4 groupes):

create or replace function t_sites_tsvector() returns trigger as $$ 
declare 
    dic regconfig; 
    part_a text; 
    part_b text; 
    part_c text; 
    part_d text; 
begin 
    dic := 'simple'; -- change if you need more advanced word processing (stemming, etc) 

    part_a := coalesce(new.doc->>'identification', '') || ' ' || coalesce(new.doc->>'title', '') || ' ' || coalesce(new.doc->>'address', ''); 

    select into part_b string_agg(coalesce(a, ''), ' ') || ' ' || string_agg(coalesce(b, ''), ' ') 
    from (
    select 
     jsonb_array_elements((new.doc->'buildings'))->>'identification', 
     jsonb_array_elements((new.doc->'buildings'))->>'name' 
) _(a, b); 

    select into part_c string_agg(coalesce(c, ''), ' ') 
    from (
    select jsonb_array_elements(b)->>'identification' from (
     select jsonb_array_elements((new.doc->'buildings'))->'deposits' 
    ) _(b) 
) __(c); 

    select into part_d string_agg(coalesce(d, ''), ' ') 
    from (
    select jsonb_array_elements(c)->>'sample_id' 
    from (
     select jsonb_array_elements(b)->'audits' from (
     select jsonb_array_elements((new.doc->'buildings'))->'deposits' 
    ) _(b) 
    ) __(c) 
) ___(d); 

    new.tsvector := setweight(to_tsvector(dic, part_a), 'A') 
    || setweight(to_tsvector(dic, part_b), 'B') 
    || setweight(to_tsvector(dic, part_c), 'C') 
    || setweight(to_tsvector(dic, part_d), 'D') 
    ; 
    return new; 
end; 
$$ language plpgsql immutable; 

create trigger t_sites_tsvector 
    before insert or update on sites for each row execute procedure t_sites_tsvector(); 

^^ - faire défiler, cet extrait est plus grand qu'il n'y paraît (en particulier vous avez Mac OS w/o ...) scrollbars

Créons index GIN pour speedup requêtes de recherche (logique si vous avez plusieurs lignes - disons, plus de centaines ou des milliers):

create index i_sites_fulltext on sites using gin(tsvector); 

Et maintenant, nous insérer quelque chose à vérifier:

insert into sites select 1, '{ 
     "_id": "123", 
     "type": "Site", 
     "identification": "Custom ID", 
     "title": "SITE 1", 
     "address": "UK, London, Mr Tom''s street, 2", 
     "buildings": [ 
      { 
       "uuid": "12312", 
       "identification": "Custom ID", 
       "name": "BUILDING 1", 
       "deposits": [ 
        { 
         "uuid": "12312", 
         "identification": "Custom ID", 
         "audits": [ 
          { 
          "uuid": "12312", 
           "sample_id": "SAMPLE ID" 
          } 
         ] 
        } 
       ] 
      } 
     ] 
    }'::jsonb; 

Vérifiez avec select * from sites; - vous devez voir que tsvector colonne est remplie avec des données.

Maintenant, nous allons l'interroger:

select * from sites where tsvector @@ to_tsquery('simple', 'sample'); 

- il doit retourner notre dossier. Dans ce cas, nous cherchons 'sample' mot et nous ne nous soucions pas du groupe dans lequel il sera trouvé.

Changeons et essayer de ne rechercher que dans le groupe A (« SITE (identification, titre, adresse) » comme vous l'avez décrit):

select * from sites where tsvector @@ to_tsquery('simple', 'sample:A'); 

- cela doit retourner rien parce que mot 'sample' se trouve seulement dans le groupe D ("AUDIT (sample_id)"). En effet:

select * from sites where tsvector @@ to_tsquery('simple', 'sample:D'); 

- nous reviendrons à nouveau notre record. Notez que vous devez utiliser to_tsquery(..), pas plainto_tsquery(..) pour pouvoir adresser 4 groupes. Vous devez donc nettoyer vous-même votre entrée (évitez d'utiliser ou de supprimer des caractères spéciaux tels que & et | car ils ont une signification particulière dans les valeurs tsquery).

Et les bonnes nouvelles est que vous pouvez combiner différents groupes dans une seule requête, comme ceci:

select * from sites where tsvector @@ to_tsquery('simple', 'sample:D & london:A'); 

L'autre chemin à parcourir (par exemple si vous devez travailler avec plus de 4 groupes) est d'avoir plusieurs tsvectors, chacun assis dans une colonne séparée, les construisent en utilisant une seule requête, créent un index (vous pouvez créer un index unique sur plusieurs colonnes tsvector) et interrogent des colonnes séparées. C'est similaire à ce que j'ai expliqué ci-dessus, mais peut-être moins efficace.

Espérons que cela aide.

+0

Merci beaucoup @Nick. Jetez un coup d'oeil dans votre suggestion bientôt. – rusllonrails

+1

Bien sûr. Faites-moi savoir si ça ne va pas. – Nick

+1

Hey @Nick j'ai oublié de vous dire grand merci) Je vous ai testé approche et ça marche génial! Merci beaucoup tant d'ami – rusllonrails