5

我有一个大型的 postgres 位置表(商店、地标等),用户可以通过各种方式进行搜索。当用户想要搜索一个地点的名称时,系统当前会这样做(假设搜索是在咖啡馆上):

lower(location_name) LIKE '%cafe%'

作为查询的一部分。这是非常低效的。禁止如此。我必须让它更快。我试过索引表

gin(to_tsvector('simple', location_name))

并搜索

(to_tsvector('simple',location_name) @@ to_tsquery('simple','cafe'))

它工作得很好,并且将搜索时间减少了几个数量级。

但是,位置名称可以是任何语言,包括没有空格分隔的中文等语言。这个新系统找不到任何中文位置,除非我搜索确切的名称,而旧系统可以找到部分名称的匹配项。

所以,我的问题是:我可以让它同时适用于所有语言,还是我走错了路?

4

3 回答 3

4

如果要优化任意子字符串匹配,一种选择是使用pg_tgrm模块。添加索引:

CREATE INDEX table_location_name_trigrams_key ON table
  USING gin (location_name gin_trgm_ops);

这会将“Simple Cafe”分解为“sim”、“imp”、“mpl”等,并为每行中的每个 trigam 添加一个条目。然后,查询计划器可以自动将此索引用于子字符串模式匹配,包括:

SELECT * FROM table WHERE location_name ILIKE '%cafe%';

此查询将在索引中查找“caf”和“afe”,找到交集,获取这些行,然后根据您的模式检查每一行。(最后的检查是必要的,因为“caf”和“afe”的交集匹配“simple cafe”和“unsafe scaffolding”,而“%cafe%”应该只匹配一个)。随着输入模式变得更长,索引变得更有效,因为它可以排除更多行,但它仍然不如索引整个单词那么有效,所以不要期望性能改进超过to_tsvector.

抓住的是,三元组对于三个字符以下的模式根本不起作用。这可能会或可能不会破坏您的应用程序。


编辑:我最初将此添加为评论。

昨晚我大半睡着的时候,我有了另一个想法。创建一个cjk_chars函数,该函数接受输入字符串、regexp_matches整个 CJK Unicode 范围,并返回任何此类字符的数组,NULL如果没有,则返回。在 上添加 GIN 索引cjk_chars(location_name)。然后查询:

WHERE CASE
  WHEN cjk_chars('query') IS NOT NULL THEN
    cjk_chars(location_name) @> cjk_chars('query')
    AND location_name LIKE '%query%'
  ELSE
    <tsvector/trigrams>
  END

达达,一元!

于 2012-10-11T03:21:26.660 回答
2

对于多语言环境中的全文搜索,您需要将每个数据所在的语言与文本本身一起存储。然后,您可以使用 tsearch 函数的语言指定风格来获得正确的词干提取等。

例如给定:

CREATE TABLE location(
    location_name text, 
    location_name_language text
);

...加上任何适当的约束,您可能会写:

CREATE INDEX location_name_ts_idx
USING gin(to_tsvector(location_name_language, location_name));

和搜索:

SELECT to_tsvector(location_name_language,location_name) @@ to_tsquery('english','cafe');

无论您做什么,跨语言搜索都会有问题。在实践中,我会使用多种匹配策略:我会将搜索词与配置中的tsvectorof文本的存储语言进行比较。我可能还会使用 willglynn 建议的基于三元组的方法,然后我会统一显示结果,寻找常用术语。location_namesimple

您可能会发现 Pg 的全文搜索过于有限,在这种情况下,您可能想查看类似Lucerne / Solr的内容。

请参阅: *控制全文搜索。* tsearch 字典

于 2012-10-11T04:12:14.540 回答
1

类似于@willglynn 已经发布的内容,我会考虑pg_trgm模块。但最好使用GiST索引:

CREATE INDEX tbl_location_name_trgm_idx
USING gist(location_name gist_trgm_ops);

gist_trgm_ops运算符类通常忽略大小写,并且ILIKELIKE. 引用源代码:

注意: IGNORECASE 宏意味着三元组不区分大小写。

COLLATE "C"在这里使用 - 这实际上不是特殊的排序规则(而是字节顺序),因为您的列中显然混合了各种排序规则。排序规则与排序或范围相关,对于基本的相似性搜索,您可以不用它。我会考虑COLLATE "C"为您的专栏设置开始。

该索引将为您的第一个简单形式的查询提供支持:

SELECT * FROM tbl WHERE location_name ILIKE '%cafe%';
  • 非常快。
  • 保留查找部分匹配项的能力。
  • 添加模糊搜索功能。
    检查%运算符set_limit()
  • LIMIT nGiST 索引对于选择 n 个“最佳”匹配的查询也非常快。您可以添加到上面的查询:

ORDER BY location_name <-> 'cafe'
LIMIT 20

<-> 在此处的手册中阅读有关“距离”运算符的更多信息。

甚至:

SELECT *
FROM   tbl
WHERE  location_name ILIKE '%cafe%'        -- exact partial match
OR     location_name %     'cafe'          -- fuzzy match
ORDER  BY 
       (location_name ILIKE 'cafe%') DESC  -- exact beginning first
      ,(location_name ILIKE '%cafe%') DESC -- exact partial match next
      ,(location_name <->   'cafe')        -- then "best" matches
      ,location_name                       -- break remaining ties (collation!)
LIMIT  20;

我在几个应用程序中使用类似的东西以获得(对我而言)令人满意的结果。当然,结合应用多个功能,它会变得有点慢。找到你的甜蜜点...

您可以更进一步,为每种语言创建一个单独的部分索引,并为每种语言使用匹配的排序规则:

CREATE INDEX location_name_trgm_idx
USING gist(location_name COLLATE "de_DE" gist_trgm_ops)
WHERE location_name_language = 'German';

-- repeat for each language

如果您只想要每个查询的特定语言的结果并且在这种情况下会非常快,那只会很有用。

于 2012-10-15T18:02:15.360 回答