【发布时间】:2020-03-01 00:19:19
【问题描述】:
我有一个数据库,我们经常需要对字符串进行模糊/距离匹配。在此示例中,目标 citext 字段名为 analytic_scan.inv_name。但是相同类型的代码可能对任意数量的其他text 和citext 字段有用。表结构的其余部分不会在此查询中发挥作用。
从 Tom Lane 的 tip about K-NN text searches 开始,我有一个 trigram GIST 索引,它实现了 <-> 距离运算符。使用提示,对字符串最近的 10 个邻居的查询如下所示:
select distinct on (inv_name <-> 'Pack CT - 002') inv_name,
inv_name <-> 'Pack CT - 002' AS distance
from analytic_scan
order by 2 -- order by the distance column...using the column position saves retyping the formula here.
limit 10;
这很好用,虽然有 7M+ 行,但需要一些时间。我的目标是计算并存储一组用于快速查找的值,如下所示:
在目标字段
analytic_scan.inv_name中查找不同的术语。对于每个词,计算词的频率和频率百分位数。
对于每个术语,找出 10 个(或 100 个等)最近的邻居及其距离。
从那里,我想为每个术语添加distance_min、distance_max 和distance_width,我认为我可以使用正确的窗口函数魔法来做到这一点。 (我不在这里尝试那部分。)
上面是K-NN搜索,频率计数搜索很简单:
select distinct inv_name,
count(*) as frequency,
ntile(100) OVER(ORDER BY count(*)) as frequency_percentile
from analytic_scan
group by inv_name
order by 1,2;
把这两个查询结合起来让我很困惑。感觉就像LATERAL JOIN,但我很可能是错的。我已经尝试了一些,但这并没有给我来自 KNN 子查询的列中的任何值,它们都是 NULL。另外,我每学期只有一行,而不是 10 行。所以,很明显,我错了。
明确地说,我确实得到了预期的列:
inv_name
frequency
frequency_percentile
neighbor_name
distance
...但是未填充基于 KNN 的字段,我只得到一行输出,而不是 KNN 搜索的 LIMIT 10 子句中的 10 行。我知道我想要将“查找 10 个邻居”代码应用于我的示例中的每个项目,但不知道如何正确地做到这一点。我累了LATERAL,但如果有更好的方法,我全力以赴。
-- Final results I'm after, with one row per *neighbor*.
-- So, 10x the distinct terms, in this case.
select frequency_table.inv_name,
frequency_table.frequency,
frequency_table.frequency_percentile,
knn.neighbor_name,
knn.distance
-- Calculate the distinct terms and their frequencies. There are 6,958 distinct terms in my sample table.
from (
select inv_name,
count(*) as frequency,
ntile(100) OVER(ORDER BY count(*)) as frequency_percentile
from analytic_scan
group by inv_name
) frequency_table
-- I'm wanting to "multiply" the terms above with the 10 neighbors below. LEFT JOIN is obviously wrong.
left join lateral -- CROSS JOIN LATERAL gives me 70 rows on 6,958 distinct terms. ¯\_(ツ)_/¯
-- Find the 10 nearest neighbors.
(select distinct on (analytic_scan.inv_name <-> frequency_table.inv_name) analytic_scan.inv_name AS neighbor_name,
analytic_scan.inv_name <-> frequency_table.inv_name AS distance
from analytic_scan
where frequency_table.inv_name = analytic_scan.inv_name and
frequency_table.frequency_percentile = 1
limit 10
) knn ON TRUE
order by frequency_table.inv_name,
knn.distance
如果有人能指出我正确的方向,那就太好了。我显然已经在这里滑雪了。
注意:我最终可能会使用数组或 jsonb 与相邻数据存储每个术语的一行。目前,数据将被客户端应用程序使用,他们只需要一个 JSON 数组。通常,我对拥挤的田野过敏,但在这种情况下可能是有道理的。我没有在这里尝试合并,因为我认为正确获取基本查询是有意义的。但是,如果有人有一个最终创建 JSON 聚合而不是我的每个邻居一行的解决方案,那也没关系。这是我想象的那种表:
CREATE TABLE IF NOT EXISTS analytics.inv_name_frequency (
id uuid NOT NULL DEFAULT extensions.gen_random_uuid(), -- What the boss likes.
inv_name citext NOT NULL DEFAULT 0,
frequency integer NOT NULL DEFAULT 0,
frequency_range int4range NOT NULL DEFAULT '(0,0)'::int4range -- For min, max distances.
frequency_width integer NOT NULL DEFAULT 0, -- Stores min-max value, can use a calculated column in PG12.
neighbors jsonb NOT NULL DEFAULT '{}'::jsonb) -- JSON array with {"term","foo","distance":0.3} for each neighbor.
后续详情
jjanes,我从 SO 档案中大量使用了他的 cmets 和代码,花时间回复。我的回答不适合评论,所以我在这里添加它。这可能有助于澄清是的,我正在查看的数据是混乱的。我们做的第一件事是帮助人们从非标准和不一致的名称转变为一组高度标准化的名称。这需要一堆软件,以及同样多的人类技能和努力。老实说,我们应该聘请人类学家,因为大部分工作是提取当地知识。标准化过程的起点是多年积累的具有各种不一致的真实数据。这就是我在这里查看的数据。
我们已经通过大量模糊字符串比较攻击了自动匹配,我真的很喜欢三元组的 Postgres 实现。当我阅读“K-NN”搜索提示时,它看起来是一种在历史数据的“汤”表中查找模式的非常有趣的方法。它的速度相当快,因为它正在做什么......即使我已经编写了代码。通过快速捕获和/或存储大量近用词以供检索,您就有了一个非常好的起点,可以使用 Levenshtein 等进行更昂贵的相似度评分。
因此,作为一个实验,我想在历史数据上建立一个术语表和近邻表。我可以用客户端语言轻松做到这一点,甚至是 PL/PgSQL
-
select distinct使用 Postgres - 遍历每个结果,获取邻居,存储结果。
但这似乎在 Postgres 中的直接 SQL 中应该是可能的,我想弄清楚如何。 很多次我想对文本进行扩展频率分析。正如我的代码中的 cmets 所示,很明显,我对查询期间发生的事情的心理地图是……相当空白。我想更好地理解如何使用lateral join 或子查询等来解决这个 SQL 问题。
【问题讨论】:
标签: postgresql join knn fuzzy-comparison