JSONPath查询:对另一个字段中的语言值进行全文检索
我在设计一个JSON路径查询时遇到困难,想通过全文检索索引对其进行有效的调优,似乎这是一个并不难的用例。这个用例适用于PostgreSQL 14,但如果能提供任意版本的PostgreSQL的信息也会很有帮助。
给出如下表格
CREATE TABLE T (ID BIGINT NOT NULL, DATA JSONB);
其中 DATA 包含来自以下JSON的 JSONB
{
"text" : [
{ "lang" : "en", "desc" : "English description" },
{ "lang" : "fr", "desc" : "Description en français" },
{ "lang" : "de", "desc" : "Deutsche Beschreibung" }
],
"otherkey" : "doesn't matter"
}
如何创建一个索引并查询,在任意 text 数组元素中包含英文 desc,且与给定的搜索字符串匹配?
我可以像这样创建一个索引:
CREATE INDEX I ON T USING GIN (to_tsvector('english', data->'text'->0->>'desc'));
然后查询
SELECT *
FROM T
WHERE
to_tsvector('english', data->'text'->0->>'desc') @@ websearch_to_tsquery('english', 'whatever')
但这假设英文标题总是放在第一个 text 数组项中,而它也可能出现在任意项中,甚至可能缺失。我希望查询能够在任意 text 数组元素中,找到键名为 lang、值为 "en" 的 desc 条目;如果不可能,至少在第一个这样的数组元素中也行。
我尝试过
-- doesn't work: --
CREATE INDEX I ON T USING GIN (to_tsvector('english', jsonb_path_query_first(data->'text', '$[*].desc ( @ .. .lang = "en" )'));
但Postgres的 JSONPath实现并没有实现能够过滤前驱节点子节点的 .. 前驱段/选择器。
我也尝试过
-- doesn't work either: --
CREATE INDEX I ON T USING GIN (to_tsvector('english', jsonb_path_query(data->'text', '$[*].desc')));
(查询本身没有问题,返回三个规范化结果行)仅将所有 desc 文本作为英文进行索引,但Postgres输出 ERROR: set-returning functions are not allowed in index expressions。
解决方案
- 你其实根本不需要考虑数组中的位置,也不需要对其进行展开。对数组访问命名字段等同于对其中的元素进行访问。
- 过滤器在你放置
?的上下文/层级中起作用,而不会让目标路径进入括号内引用的内容。你可以在.lang上进行过滤,但随后仍然让目标路径在括号之后指向.desc。
jsonb_path_query_first(data,'$.text?(@.lang=="en").desc')
你可以在这个测试的第二行看到一个匹配:[demo at db<>fiddle]
select to_tsvector('english',jsonb_path_query_first(data,'$.text?(@.lang=="en").desc'))
@@ websearch_to_tsquery('english', 'whatever') as is_a_match
,jsonb_path_query_first(data,'$.text?(@.lang=="en").desc')
,*
from t limit 10;
| is_a_match | jsonb_path_query_first | id | data |
|---|---|---|---|
| f | "English description" | 1 | {"text": [{"desc": "English description", "lang": "en"}, {"desc": "Description en français", "lang": "fr"}, {"desc": "Deutsche Beschreibung", "lang": "de"}], "otherkey": "doesn't matter"} |
| t | "Wherever you go, whatever you do." | 2 | {"text": [{"desc": "English description", "lang": "zh"}, {"desc": "Wherever you go, whatever you do.", "lang": "en"}, {"desc": "Deutsche Beschreibung", "lang": "br"}], "otherkey": "doesn't matter"} |
| null | null | 61089 | {"text": [{"desc": "1857929bcf416dfd8ccb68c91bb988c3", "lang": "sk"}, {"desc": "06d09783e675caf60c139163e33ae12f", "lang": "ua"}], "otherkey": "doesn't matter"} |
| f | "dcda20cfad3ced4a288c883fb085f92c" | 5392 | {"text": [{"desc": "316ead8665350bd0d0a5e009f11afb7f", "lang": "zh"}, {"desc": "938c87ba25989685260ac8a4eefd5b97", "lang": "ua"}, {"desc": "e2936fe7fc939df4555a39cdb15cc317", "lang": "de"}, {"desc": "dcda20cfad3ced4a288c883fb085f92c", "lang": "en"}, {"desc": "b80aae0d46db875bee7cfc75e80226d1", "lang": "zh"}, {"desc": "7403e51a34b1e0e78386d738679d14ae", "lang": "ua"}, {"desc": "842e37b47d4aadefdb071fa48e571a2e", "lang": "br"}, {"desc": "734c66a9b735baaf3aba2431f8eea63c", "lang": "ua"}, {"desc": "b7f347b6373011021839d36899dc3084", "lang": "sk"}, {"desc": "feb7cb18aa95aaf0fa9597839a9e7188", "lang": "fr"}, {"desc": "8cbe22babe02557f7d00452f18576512", "lang": "zh"}], "otherkey": "doesn't matter"} |
为完整起见,这里给出索引和查询,并附有一个 explain analyze,以确认它确实在按预期使用了它(构建完表后别忘了 analyze 表)。
create index i on t using gin (
to_tsvector('english', jsonb_path_query_first(data,'$.text?(@.lang=="en").desc')));
vacuum analyze t;
explain analyze verbose
select * from t
where to_tsvector('english', jsonb_path_query_first(data,'$.text?(@.lang=="en").desc'))
@@ websearch_to_tsquery('english', 'whatever')
| QUERY PLAN |
|---|
| Bitmap Heap Scan on public.t (cost=12.00..16.27 rows=1 width=401) (actual time=0.032..0.034 rows=1 loops=1) |
| Output: id, data |
| Recheck Cond: (to_tsvector('english'::regconfig, jsonb_path_query_first(t.data, '$."text"?(@."lang" == "en")."desc"'::jsonpath, '{}'::jsonb, false)) @@ '''whatev'''::tsquery) |
| Heap Blocks: exact=1 |
| -> Bitmap Index Scan on i (cost=0.00..12.00 rows=1 width=0) (actual time=0.014..0.015 rows=1 loops=1) |
| Index Cond: (to_tsvector('english'::regconfig, jsonb_path_query_first(t.data, '$."text"?(@."lang" == "en")."desc"'::jsonpath, '{}'::jsonb, false)) @@ '''whatev'''::tsquery) |
| Planning Time: 0.298 ms |
| Execution Time: 0.060 ms |
如果你记得始终使用一个额外条件,你也可以把它做成一个部分索引:
create index i on t using gin (
to_tsvector('english', jsonb_path_query_first(data,'$.text?(@.lang=="en").desc')))
where data @? '$.text.lang=="en"';
之所以 array.key 的访问起作用,是因为 默认情况下JSONPath表达式使用 lax 模式:
Lax模式在JSON文档与路径表达式不符合预期架构时,便于匹配JSON文档与路径表达式。若一个操作数不符合某一操作的要求,它可以在执行操作前自动被包装成一个SQL/JSON数组,或通过将其元素转换为一个SQL/JSON序列来解包后再执行操作。此外,比较运算符在lax模式下会自动解包它们的操作数,因此你可以开箱即用地比较SQL/JSON数组。