按JSON数组中的匹配数查询和排序

aar*_*ell 12 postgresql activerecord ruby-on-rails ruby-on-rails-4 postgresql-9.4

jsonb在Postgres 9.4和Rails 的列中使用JSON数组,我可以设置一个范围,返回包含传递给范围方法的数组中的任何元素的所有行- 如下所示:

scope :tagged, ->(tags) {
  where(["data->'tags' ?| ARRAY[:tags]", { tags: tags }])
}
Run Code Online (Sandbox Code Playgroud)

我还想根据数组中匹配元素的数量来排序结果.

我很欣赏我可能需要超出ActiveRecord的范围才能做到这一点,所以一个vanilla Postgres SQL的答案也很有帮助,但如果它可以包含在ActiveRecord中,那么它可以是一个可链的范围.

根据要求,这是一个示例表.(实际架构要复杂得多,但这就是我所关心的.)

 id |               data                
----+-----------------------------------
  1 | {"tags": ["foo", "bar", "baz"]}
  2 | {"tags": ["bish", "bash", "baz"]}
  3 |
  4 | {"tags": ["foo", "foo", "foo"]}
Run Code Online (Sandbox Code Playgroud)

用例是基于标签查找相关内容.更多匹配标签更相关,因此结果应按匹配数量排序.在Ruby中,我有一个这样的简单方法:

Page.tagged(['foo', 'bish', 'bash', 'baz']).all
Run Code Online (Sandbox Code Playgroud)

哪个应按以下顺序返回页面:2, 1, 4.

Erw*_*ter 6

您的数组只包含原始值,嵌套文档会更复杂.

询问

jsonb_array_elements_text()LATERAL连接和计数匹配中找到找到的行的JSON数组:

SELECT *
FROM  (
   SELECT *
   FROM   tbl
   WHERE  data->'tags' ?| ARRAY['foo', 'bar']
   ) t
, LATERAL (
   SELECT count(*) AS ct
   FROM   jsonb_array_elements_text(t.data->'tags') a(elem)
   WHERE  elem = ANY (ARRAY['foo', 'bar'])  -- same array parameter
   ) ct
ORDER  BY ct.ct DESC;  -- more expressions to break ties?
Run Code Online (Sandbox Code Playgroud)

替代INSTERSECT.这是我们可以使用这个基本SQL功能的极少数情况之一:

SELECT *
FROM  (
   SELECT *
   FROM   tbl
   WHERE  data->'tags' ?| '{foo, bar}'::text[]  -- alt. syntax w. array
   ) t
, LATERAL (
   SELECT count(*) AS ct
   FROM  (
      SELECT * FROM jsonb_array_elements_text(t.data->'tags')
      INTERSECT ALL
      SELECT * FROM unnest('{foo, bar}'::text[])  -- same array literal
      ) i
   ) ct
ORDER  BY ct.ct DESC;
Run Code Online (Sandbox Code Playgroud)

注意一个微妙的区别:这在匹配时消耗每个元素,因此它data->'tags'不像第一个变体那样计算不匹配的重复项.详情请见下面的演示.

还演示了传递数组参数的另一种方法:as array literal : '{foo, bar}'. 对于某些客户来说,这可能更容易处理:

或者您可以使用VARIADIC参数创建服务器端搜索功能并传递可变数量的普通text值:

有关:

指数

一定要有一个功能GIN索引来支持jsonb存在运算符?|:

CREATE INDEX tbl_dat_gin ON tbl USING gin (data->'tags');
Run Code Online (Sandbox Code Playgroud)

有重复的细微差别

根据评论中的要求进行澄清.比如,我们有一个带有两个重复标签的JSON数组(共4个):

jsonb '{"tags": ["foo", "bar", "foo", "bar"]}'
Run Code Online (Sandbox Code Playgroud)

并使用包含两个标记的SQL数组参数进行搜索,其中一个重复(总共3个):

'{foo, bar, foo}'::text[]
Run Code Online (Sandbox Code Playgroud)

考虑一下这个演示的结果:

SELECT *
FROM  (SELECT jsonb '{"tags":["foo", "bar", "foo", "bar"]}') t(data)

, LATERAL (
   SELECT count(*) AS ct
   FROM   jsonb_array_elements_text(t.data->'tags') e
   WHERE  e = ANY ('{foo, bar, foo}'::text[])
   ) ct

, LATERAL (
   SELECT count(*) AS ct_intsct_all
   FROM  (
      SELECT * FROM jsonb_array_elements_text(t.data->'tags')
      INTERSECT ALL
      SELECT * FROM unnest('{foo, bar, foo}'::text[])
      ) i
   ) ct_intsct_all

, LATERAL (
   SELECT count(DISTINCT e) AS ct_dist
   FROM   jsonb_array_elements_text(t.data->'tags') e
   WHERE  e = ANY ('{foo, bar, foo}'::text[])
   ) ct_dist

, LATERAL (
   SELECT count(*) AS ct_intsct
   FROM  (
      SELECT * FROM jsonb_array_elements_text(t.data->'tags')
      INTERSECT
      SELECT * FROM unnest('{foo, bar, foo}'::text[])
      ) i
   ) ct_intsct;
Run Code Online (Sandbox Code Playgroud)

结果:

data                                     | ct | ct_intsct_all | ct_dist | ct_intsct
-----------------------------------------+----+---------------+---------+----------
'{"tags": ["foo", "bar", "foo", "bar"]}' | 4  | 3             | 2       | 2
Run Code Online (Sandbox Code Playgroud)

将JSON数组中的元素与数组参数中的元素进行比较:

  • 4个标签符合任何搜索要素:ct.
  • 3个中的标记相交(可以匹配元件到元件): ct_intsct_all.
  • 可以识别2个不同的匹配标签:ct_distct_intsct.

如果您没有欺骗或者您不想排除它们,请使用前两种技术之一.另外两个有点慢(除了不同的结果),因为他们必须检查欺骗.


归档时间:

查看次数:

1343 次

最近记录:

7 年,5 月 前