* { color: #3e3e3e }
body { font-family: "Helvetica Neue", Helvetica, "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif; font-size: 15px }
p { line-height: 25.6px; text-align: justify; margin: 23.7px 0 }
blockquote { border-left: 2px solid rgba(128,128,128,0.075); background-color: rgba(128,128,128,0.05); padding: 10px -1px; margin: 0px }
blockquote p { color: #898989; margin: 0px }
strong { font-weight: 700; color: #3e3e3e }
pre { background-color: #f8f8f8; overflow: scroll; padding: 12px 13px; font-size: 13px; color: #898989 }
h1,h2,h3,h4,h5,h6 { text-align: left; font-weight: bold }
hr { border: 1px solid #ddd }
h1 { font-size: 170% }
h1:first-child { margin-top: 0; padding-top: .25em; border-top: none }
table { padding: 0; border-collapse: collapse }
table tr { border-top: 1px solid #cccccc; background-color: white; margin: 0; padding: 0 }
table tr:nth-child(2n) { background-color: #f8f8f8 }
table tr th { font-weight: bold; border: 1px solid #cccccc; margin: 0; padding: 6px 13px }
table tr td { border: 1px solid #cccccc; margin: 0; padding: 6px 13px }
table tr th :first-child,table tr td :first-child { margin-top: 0 }
table tr th :last-child,table tr td :last-child { margin-bottom: 0 }
a { color: #4183C4; text-decoration: none }
ul,ol { padding-left: 30px }
li { line-height: 24px }
hr { height: 2px; padding: 0; margin: 16px 0; background-color: #e7e7e7; border: 0 none; overflow: hidden; border-bottom: 1px solid #ddd }
pre { background: #f2f2f2; padding: 12px 13px }
code { padding: 2px 4px; color: #c7254e; background: #f9f2f4 }
code[class*="language-"],pre[class*="language-"] { color: black; background: none; font-family: Consolas, Monaco, "Andale Mono", "Ubuntu Mono", monospace; text-align: left; white-space: pre; word-spacing: normal; line-height: 1.5 }
pre[class*="language-"] { position: relative; margin: .5em 0; border-left: 10px solid #358ccb; background-color: #fdfdfd; background-image: linear-gradient(transparent 50%, rgba(69, 142, 209, 0.04) 50%); overflow: visible; padding: 0 }
code[class*="language"] { max-height: inherit; height: 100%; padding: 0 1em; display: block; overflow: auto }
:not(pre)>code[class*="language-"],pre[class*="language-"] { background-color: #fdfdfd; margin-bottom: 1em }
:not(pre)>code[class*="language-"] { position: relative; padding: .2em; color: #c92c2c; border: 1px solid rgba(0, 0, 0, 0.1); display: inline; white-space: normal }
pre[class*="language-"]::before,pre[class*="language-"]::after { content: ""; z-index: -2; display: block; position: absolute; bottom: 0.75em; left: 0.18em; width: 40%; height: 20%; max-height: 13em }
:not(pre)>code[class*="language-"]::after,pre[class*="language-"]::after { right: 0.75em; left: auto }
.token.comment,.token.block-comment,.token.prolog,.token.doctype,.token.cdata { color: #7D8B99 }
.token.punctuation { color: #5F6364 }
.token.property,.token.tag,.token.boolean,.token.number,.token.function-name,.token.constant,.token.symbol,.token.deleted { color: #c92c2c }
.token.selector,.token.attr-name,.token.string,.token.char,.token.function,.token.builtin,.token.inserted { color: #2f9c0a }
.token.operator,.token.entity,.token.url,.token.variable { color: #a67f59; background: rgba(255, 255, 255, 0.5) }
.token.atrule,.token.attr-value,.token.keyword,.token.class-name { color: #1990b8 }
.token.regex,.token.important { color: #e90 }
.language-css .token.string,.style .token.string { color: #a67f59; background: rgba(255, 255, 255, 0.5) }
.token.important { font-weight: normal }
.token.bold { font-weight: bold }
.token.italic { font-style: italic }
.token.entity { cursor: help }
.namespace { opacity: .7 }
.token.tab:not(:empty)::before,.token.cr::before,.token.lf::before { color: #e0d7d1 }
pre[class*="language-"].line-numbers { padding-left: 0 }
pre[class*="language-"].line-numbers code { padding-left: 3.8em }
pre[class*="language-"].line-numbers .line-numbers-rows { left: 0 }
pre[class*="language-"][data-line] { padding-top: 0; padding-bottom: 0; padding-left: 0 }
pre[data-line] code { position: relative; padding-left: 4em }
pre .line-highlight { margin-top: 0 }
pre.line-numbers { position: relative; padding-left: 3.8em; counter-reset: linenumber }
pre.line-numbers>code { position: relative }
.line-numbers .line-numbers-rows { position: absolute; top: 0; font-size: 100%; left: -3.8em; width: 3em; letter-spacing: -1px; border-right: 1px solid #999 }
.line-numbers-rows>span { display: block; counter-increment: linenumber }
.line-numbers-rows>span::before { content: counter(linenumber); color: #999; display: block; padding-right: 0.8em; text-align: right }

前言

上文 使用PostgreSQL进行中文全文检索 中我使用 PostgreSQL 搭建完成了一套中文全文检索系统,对数据库配置和分词都进行了优化,基本的查询完全可以支持,但是在使用过程中还是发现了一些很恼人的问题,包括查询效果和查询效率,万幸都一一解决掉了。

其中过程自认为还是很有借鉴意义的,今天来总结分享一下。

博客欢迎转载,请带上来源:http://www.cnblogs.com/zhenbianshu/p/8253131.html


使用B树索引优化查询效果

分词问题

一开始是分词效果的问题:

  • 中文博大精深,乒乓球拍卖啦、南京市长江大桥 这种歧义句的分词,还没有一个分词插件能够达到 100% 的准确率,当然包括我们正在使用的 scws 分词库;
  • 我们的搜索内容是 Poi 地点名,而很多地点名都缺失语义性,产生歧义词的概率更大;

scws 支持更为灵活的分词等级,为了能分出较多的词来尽量包含目标结果,我们将 scws 的分词等级调为了 7(不了解的可以看上文),但同时也引入了更奇葩的问题: 搜索天安门 查不到 天安门广场。。。

原因也很另人无语:

  • 天安门广场 的分词结果向量 tsv 是 '天安':2 '天安门广场':1 '广场':4 '门广':3;
  • 查询向量 to_tsquery('parser', '天安门') tsq 的结果是 '天安门' & '天安' & '安门';
  • 查询语句是 SELECT * FROM table WHERE tsv @@ tsq, 由于 tsv 里没有 tsq 里的 安门向量,匹配失败。

B树索引

一个常识:大家想搜一个地点时大多会先输入其名称前面的部分,基于此考虑,我向表内引入 B树索引支持前缀查询,配合原来分词的 GIN 索引,解决了此问题。

如Mysql一样,PostgreSQL 也支持通过 like '关键词%' 语句来使用 B树索引。在 name 列上添加了 B树索引,再修改查询语句变为 SELECT * FROM table WHERE tsv @@ tsq OR name LIKE 'keyword%',这样结果就完全 OK 啦。


使用子查询优化查询效率

GIN索引效率问题

紧接着又发现了新的问题:

PostgreSQL 的 GIN 索引(Generalized Inverted Index 通用倒排索引)存储的是 (key, posting list)对, 这里的 posting list 是一组出现键的行ID。如 数据:

行ID 分词向量
1 测试 分词
2 分词 结果

则索引的内容就是 测试=>1 分词=>1,2 结果=>2,在我们要查询分词向量内包含 分词 的数据时就可以快速查找到第1,2列。

但这种设计也带来了另一个问题,当某一个 key 对应的 posting list 过大时,数据操作会很慢,如我们的数据中地点名带有 饭店 的数据就很多,有几十万,而我们的需求有一项就是要对查询结果按照 评分 一列倒序排序,这么几十万数据,数据库响应超时会达到 3000 ms。

我们期望的响应时间是 90% 50ms 以内,虽然统计结果显示,确实 90% 的请求已经符合要求,但另外的 10% 完全不能用也是不可能接受的。

接下来的优化就是针对这些 bad case。

缓存

对于这种响应超时的问题,大家肯定会想到万能的缓存:把响应超时的查询结果放到缓存,查询时先检查缓存。

可是超时的毕竟只有很少一部分,缓存的命中率堪忧。虽然这一小部分查询可用了,但是所有查询语句都会多出一次取缓存的操作。

为了能提高缓存命中率,我还特意统计了关键字各长度的搜索数量占比和超时率占比,发现以下情况:

  • 1字节(1个字母)、3字节(单字)关键词的超时率最高,可是也不超过 30%;
  • 1字节、3字节关键词的搜索量占比有30%左右;
  • 其他长度关键词的超时率10%左右,非常尴尬。

这种情况打消了我只针对某些长度的关键词设置缓存的想法。

不仅是命中率问题,缓存过期时间和缓存更新等更是大坑,基于以上考虑,缓存方案彻底被放弃。

分表

一个方法不行,那就换一个方向,既然某些关键词的结果集太大,那么我们就将它变小一些,我们一开始采用的策略是分表。

由于 Poi 地点都有区域属性,我们以区域 ID 将这些数据分成了多个数据表,原来最大的关键词结果集有几十万,拆分到多个表后,每个表中最大的关键词结果集也就几万,此时的排序性能提高了,基本在 100~200ms 之间。

查询时我们先通过位置将用户定位到区域,根据区域 ID 确定要查询的表,再从对应表内查询结果。

这个方案的缺点也非常多:

  • 对定位很依赖,且定位计算区域也会有耗时;
  • 区域边缘点的搜索很蛋疼,明明离得很近,如果被划分到跟用户不同区域了就搜索不到。
  • 多个表非常不好维护。

子查询

终于灵活考虑了业务需求,引入子查询提出了一种颇为完美的方案:

用户在搜索框键入了 饭店、宾馆 等无意义关键词,不同于搜索 海底捞,此时用户也不知道他自己需要什么,对搜索结果是没有明确期待的。

这时候,我们也并不需要很愣地把全国名字中带有饭店、宾馆的地点都拿出来排序,这样的排序结果用户也不一定满意。 我们可以只取一部分 Poi 地点给用户,如果结果用户不满意,会再完善关键词,而关键词稍有完善,结果集就会极大地减小。

子查询用来实现结果集过滤非常有效,如我们可以在极大页码查询分页时使用子查询先过滤掉一大批无用数据。

本例中,我们在子查询语句中使用 limit 语句限制取的结果集条数,从而大大减小排序压力,查询语句类似 SELECT id FROM (SELECT * FROM table WHERE tsv @@ tsq OR name LIKE 'keyword%' LIMIT 10000) AS tmp ORDER BY score DESC

这样优化过后,查询语句的最差性能也可以稳定在 170ms 以下了。


替换B树索引消灭慢查询

多索引效率问题

本以为优化到此为止了呢,可是有次在试着查询 中关村 两个关键词时,我明确感觉到了响应时间的差异, 100ms 左右的时间差还是很明显的。

子查询语句才是这条 SQL 语句的效率关键,于是我开始分析 这个关键词的 子查询SQL 语句,首先我试着调整语句中 limit 的限制值,发现即使只取 1000条,响应时间也在 100ms 以上。

接着我又尝试改变 SQL 语句的 WHERE 条件,去除 OR name LIKE 'keyword%' 后, 总条数并没有太大的变动,结果集由 13w 减小到了 11w, 但 添加 limit 后的效率却急剧提升:

SQL 结果条数 响应时间 添加 limit 后 SQL 响应时间
WHERE tsv @@ tsq OR name LIKE 'keyword%' 13W 2400ms   WHERE tsv @@ tsq OR name LIKE 'keyword%' LIMIT 10000 170ms
WHERE tsv @@ tsq 11W 1900ms   WHERE tsv @@ tsq limit 10000 25ms

这样对比起来就很明显了, 分词查询的 GIN 索引和前缀词查询的 B树索引之间配合并不完美。

想想也是,如果在一个索引上取 1w 条数据,直接取就行了,而如果在两个索引上取 1w 数据,那么还得考虑每个索引上各取多少,取完后还要排重。

替换B树索引

问题分析完,那么就得根据问题寻找解决方案了,怎么能把两个索引并到同一索引上呢?把分词 GIN 索引并到 B树索引显然是不可能的,只能试着使用分词来替代 B树索引。

当时有三种方案:

  • 修改开源分词库 scws,添加一个分前缀词的功能。不过我担心改出 Bug,而且还要改 PostgreSQL 的分词插件 zhparser 以适应 scws 的参数变动。
  • 使用 PostgreSQL 的数组类型(text[])存储分词结果,后续往此字段内灵活添加前缀词。但填充数组字段需要调用 SELECT to_tsvector('parser', 'nane') 查询后使用脚本处理结果后再写入数组,比较麻烦。
  • 修改 tsvector 分词向量字段,手动向此字段添加前缀词的分词向量。但分词向量不同于文本,不能直接拼接。

最好的方案当然是最后一种,改动最小,于是我就查询了一下 PostgreSQL 向量拼接,还是找到了向量拼接的方法,使用 ::tsvector 将字符串强转成向量,再使用 || 拼接到原来的分词向量上,SQL 语句类似 SELECT to_tsvector('parser', 'keyword') || 'prefix'::tsvector

在查询时,就可以直接使用 WHERE tsv @@ to_tsquery('parser', 'keyword') 查询前缀了。这样,子查询语句的响应时间就可以大大降低了,在 50ms 左右,而且还可以通过减小 LIMIT 值来加快响应。

此后,B树索引就可以退休啦~


小结

以上就是我对 PostgreSQL 关键词查询从效果到效率优化的全过程了,效果和效率已经完全达标了。当然,还可以对用户体验进行再优化,比如添加错别字识别、拼音首字母智能识别等,打磨好一款产品当然是非常不容易的,还需要继续努力。

顺便吐槽几句周边同事对 PostgreSQL 的态度,理由竟然是认为它是一个开源产品,可能会有各种埋得深的坑,所以不信任。

比较想不到比较前沿的互联网公司也会有人对开源抱如此看法,不可否认很多开源产品或工具都有各种各样的坑,但为此因噎废食大可不必,我们一直在用的 Linux/Git 还是开源产品呢,可有多少人离不开它们?而且闭源产品就不会出现问题么?也不可否认 PostgreSQL 小众,但它也有自己的特色,而且近年来它的占有率一率攀升,未来什么样,还未可知。

关于本文有什么问题可以在下面留言交流,如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我,博客一直在更新,欢迎 关注

见招拆招-PostgreSQL中文全文索引效率优化的更多相关文章

  1. Java正则速成秘籍(三)之见招拆招篇

    导读 正则表达式是什么?有什么用? 正则表达式(Regular Expression)是一种文本规则,可以用来校验.查找.替换与规则匹配的文本. 又爱又恨的正则 正则表达式是一个强大的文本匹配工具,但 ...

  2. 知物由学 | 见招拆招,Android应用破解及防护秘籍

    本文来自网易云社区. “知物由学”是网易云易盾打造的一个品牌栏目,词语出自汉·王充<论衡·实知>.人,能力有高下之分,学习才知道事物的道理,而后才有智慧,不去求问就不会知道.“知物由学”希 ...

  3. 见招拆招:绕过WAF继续SQL注入常用方法

    Web Hacker总是生存在与WAF的不断抗争之中的,厂商不断过滤,Hacker不断绕过.WAF bypass是一个永恒的话题,不少基友也总结了很多奇技怪招.那今天我在这里做个小小的扫盲吧.先来说说 ...

  4. MySQL中文全文索引插件 mysqlcft 1.0.0 安装使用文档[原创]

    [文章+程序 作者:张宴 本文版本:v1.0 最后修改:2008.07.01 转载请注明原文链接:http://blog.zyan.cc/post/356/] MySQL在高并发连接.数据库记录数较多 ...

  5. [转]PostgreSQL 中文资料汇总

    原文链接:http://francs3.blog.163.com/blog/static/405767272014017341219/ --1 中文社区网站  PostgreSQL 中文社区官网: h ...

  6. PostgreSQL LIKE 查询效率提升实验<转>

    一.未做索引的查询效率 作为对比,先对未索引的查询做测试 EXPLAIN ANALYZE select * from gallery_map where author = '曹志耘'; QUERY P ...

  7. C#效率优化(1)-- 使用泛型时避免装箱

    本想接着上一篇详解泛型接着写一篇使用泛型时需要注意的一个性能问题,但是后来想着不如将之前的详解XX系列更正为现在的效率优化XX系列,记录在工作时遇到的一些性能优化的经验和技巧,如果有什么不足,还请大家 ...

  8. php效率优化

    php效率优化 最近在公司一边自学一边写PHP程序,由于公司对程序的运行效率要求很高,而自己又是个新手,一开始就注意程序的效率很重要,这里就结合网上的一些资料,总结下php程序效率优化的一些策略:1. ...

  9. QRowTable表格控件(三)-效率优化之-合理使用QStandardItem

    目录 一.开心一刻 二.概述 三.效果展示 四.QStandardItem 1.QStandardItem是什么鬼 2.性能分析 3.QStandardItem使用上的坑 五.相关文章 原文链接:QR ...

随机推荐

  1. 使用Jenkins自动部署博客

    title: 使用Jenkins自动部署博客 toc: true comment: true date: 2017-12-17 19:48:10 tags: ['Jenkins', 'CI'] cat ...

  2. NGUI_Input

    九.输入框Input 1.凡是用户可以输入文本的地方,几乎都用输入框,有登录账号和密码.输入角色名称.输入聊天内容 2.手动拼接输入框,拖动预制体的就不再说了 (1).创建一个Sprite作为输入框的 ...

  3. always中的敏感变量

    always语句下如果有判断语句if,那么if语句中的条件必须有always中的敏感变量. 否则错误提示为:Error (10200): Verilog HDL Conditional Stateme ...

  4. vue项目的骨架及常用组件介绍

    vue项目基础结构 一个vue的项目,我觉得最小的子集其实就是{vue,vue-router,component},vue作为基础库,为我们提供双向绑定等功能.vue-router连接不同的" ...

  5. XML解析之SAX

    今天在敲代码的时候,想要实现地址选择功能,就是那个能够选择省.市.县的一个,用到的一个开源框架Android-PickerView,当然他这个里面尽管实现了能够选择的城市列表.可是他这是自己创建的,可 ...

  6. poj - 1185 炮兵阵地 状压DP 解题报告

    炮兵阵地 Time Limit: 2000MS   Memory Limit: 65536K Total Submissions: 21553   Accepted: 8363 Description ...

  7. Python 爬虫 爬校花网!!

    爬虫:是一种按照一定的规则,自动地抓取万维网信息的程序或者脚本 1.福利来了  校花网 ,首先说为什么要爬这个网站呢,第一这个网站简单爬起来容易不会受到打击,第二呢 你懂得... 1.第一步,需要下载 ...

  8. springboot+CXF开发webservice对外提供接口(转)

    文章来源:http://www.leftso.com/blog/144.html 1.项目要对外提供接口,用webservcie的方式实现 2.添加的jar包 maven: <dependenc ...

  9. redis基础二

    前面已经学习了redis的基本的命令行操作和数据类型,下面开始redis一些有趣的功能. 订阅和发布机制 定义:发布者相当于电台,订阅者相当于客户端,客户端发到频道的消息,将会被推送到所有订阅此频道的 ...

  10. grep 、find 、tree 新发现

    [root@localhost tftpboot]# ip address | grep -A 1 " eno16777736"2: eno16777736: <BROADC ...