如何在 Postgresql 中实现对复杂嵌套 JSONB 的全文搜索

Posted

技术标签:

【中文标题】如何在 Postgresql 中实现对复杂嵌套 JSONB 的全文搜索【英文标题】:How to implement full text search on complex nested JSONB in Postgresql 【发布时间】:2018-01-22 16:06:21 【问题描述】:

我将相当复杂的 JSONB 存储在一个 jsonb 列中。

数据库表如下:

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

我们存储在doc 列中的数据是一个复杂的嵌套JSONB 数据:

   
      "_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 的结构如下:

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

我们需要通过每种类型的条目中的一些值来实现全文搜索:

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

SQL 查询应仅在这些字段值中运行全文搜索。

我猜需要使用GIN 索引和tsvector 之类的东西,但没有足够的Postgresql 背景。

那么,我的问题是可以索引然后查询这种嵌套的JSONB 结构吗?

【问题讨论】:

第一个镜头是“非规范化”存储:为了简洁而牺牲一些存储空间。在单独的字段中提取所需的数据:站点、建筑物、存款、审计,包含所需字段的纯字符串连接,即building.identification ||';'||building.title||';'||building.address 等(这可以使用 postgres 的函数作为默认值或基于触发器来完成,如果您的数据已修改)。然后在这些字段上创建 GIN 索引 -> 然后在这些字段上构建相应的全文查询 谢谢@IlyaDyoshin。我喜欢你的想法 - 将尝试尝试它。 或者你可以等到 10.0 版本发布 - json/jsonb FTS 将成为一等公民postgresql.org/docs/10/static/release-10.html 哇,感谢@IlyaDyoshin 提供的信息:啤酒: 伙计们,请参阅下面的@Nick 方法。我测试了它 - 效果很好 + 1 【参考方案1】:

让我们添加tsvector类型的新列:

alter table sites add column tsvector tsvector;

现在让我们创建一个触发器来收集词法,组织它们并放入我们的 tsvector。我们将使用 4 个组(A、B、C、D)——这是 tsvector 的一个特殊功能,允许您稍后在搜索时区分词法(参见手册中的示例https://www.postgresql.org/docs/current/static/textsearch-controls.html;不幸的是,此功能最多支持 4组,因为开发人员只为此保留了 2 位,但我们很幸运,我们只需要 4 个组):

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();

^^ -- 滚动它,这个 sn-p 比它看起来要大(尤其是你有没有滚动条的 MacOS...)

现在让我们创建 GIN 索引来加速搜索查询(如果您有很多行 - 例如,超过数百或数千行,这很有意义):

create index i_sites_fulltext on sites using gin(tsvector);

现在我们插入一些东西来检查:

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;

检查select * from sites; - 您必须看到tsvector 列填充了一些数据。

现在让我们查询它:

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

-- 它必须返回我们的记录。在这种情况下,我们搜索 'sample' 单词,我们不关心它会在哪个组中找到。

让我们更改它并尝试仅在 A 组中搜索(“SITE(标识、标题、地址)”,如您所描述的):

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

-- 这必须不返回任何内容,因为单词 'sample' 仅位于 D 组(“AUDIT (sample_id)”)中。确实:

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

-- 将再次返回我们的记录。

注意,您需要使用 to_tsquery(..),而不是 plainto_tsquery(..) 才能寻址 4 个组。所以你需要自己清理你的输入(避免使用或删除像&|这样的特殊字符,因为它们在tsquery值中有特殊含义)。

好消息是您可以在一个查询中组合不同的组,如下所示:

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

另一种方法(例如,如果您必须使用 4 个以上的组)是拥有多个 tsvector,每个都位于单独的列中,使用单个查询构建它们,创建索引(您可以在多个tsvector 列)和查询寻址单独的列。它与我上面解释的类似,但效率可能较低。

希望这会有所帮助。

【讨论】:

非常感谢@Nick。很快就会考虑您的建议。 嘿@Nick 我忘了说你非常感谢)我测试了你的方法,效果很好!非常感谢你的朋友【参考方案2】:

在 Postgres 10 中,事情似乎更简单了,因为 to_tsvector 函数支持 json。例如,这很好用:

UPDATE dataset SET search_vector = to_tsvector('english',
'
  "abstract":"Abstract goes here",
  "useConstraints":"None",
  "dataQuality":"Good",
  "Keyword":"historic",
  "topicCategory":"Environment",
  "responsibleOrganisation":"HES"
'::json)
where dataset_id = 4;

请注意,我没有在深度嵌套的结构上尝试过这个,但不明白为什么它不起作用

【讨论】:

以上是关于如何在 Postgresql 中实现对复杂嵌套 JSONB 的全文搜索的主要内容,如果未能解决你的问题,请参考以下文章

如何在mvc中实现对图像的点击绑定

如何在富编辑控件中实现对 URL 的鼠标单击

如何在 R Shiny 中实现对数据表的内联编辑

如何在delphi中实现对文件进行base64编码

如何在服务器端的 Relay Mutations 中实现对另一个模型的引用?

如何在 RecyclerView 的 CardView 中实现对 Item Click 的操作以显示结果