问题描述

最近在给甲方做项目的时候,30W商品信息的检索需要5-10s,数据库是MongoDB。这让我有点大跌眼镜,MongoDB的速度再慢,也不能慢到这个量级,然后这个事情就非常奇怪,经过我几天的折腾,总算把时间量级下降到500ms左右,达到了一个能接收的范围。

背景

我们需要做的是在商品库中按照商品名称和tag进行商品的模糊检索,一开始我们写出了这样的搜索语句db.product.find({"$or":{"name":"/keyword/", "tag":"/keyword/"}),有经验的一看就知道是正则的问题,我之前吧搜索语句写的正则忘了,结果导致了一大堆的弯路,直到重新Review代码才看到了这个坑。

解决过程

一开始毫无头绪,就像掉入了一个大坑中。尝试着通过创建索引的方式来加快搜索,结果并没有太多的改善。同时在创建索引的时候发现了MongoDB的一个限制:在创建多键值索引的时候,之多只能包含一个Array。我们正好索引有两个,这样的话就非常的不方便。 之前考虑过是使用mgo中某些库导致的速度下降,去研究了一下mgo这个库的源码,否定了这个结论。

即使优化了之后,速度还是没有大的改善。于是借助mongo的Explain来分析语句执行时间。通过学习了MongoDB Explain的含义,正好也看到这篇介绍(数据库原理)[http://blog.jobbole.com/100349/]的文章,结合起来看,研究和分析执行时间。

MongoDB Explain

explain 在2.6和3.2版本下显示的结果不同,我主要使用的是3.2版本。默认有三种模式,queryPlanner/executionStats/allPlansExecution,默认是queryPlanner。在queryPlanner模式下,explain运行查询优化器,返回通过计算之后得出的最优的执行plan(不知道翻译做什么比较好)。而executionStats则是在这基础之上,执行最优的plan,并且返回描述信息。allPlansExecution 则是上述两个的综合,执行查询优化器选择最优的plan并执行。 而在本例中,我们比较关心的是executionStats。其结果解释见官方文档。 在返回结果中,在扫描nametag时,Stage字段是比较重要的,这个字段有四种可能:

  • COLLSCAN 全表扫描,这是我们要避免的
  • IXSCAN 扫描已经索引的键值
  • FETCH 获取Documents
  • SHARED_MERGE 从shards中获取结果

然而我们的执行结果在扫描nametag时已经是IXSCAN,结果依然不理想。挫败感袭来,所以只好去Review代码,在看代码的时候发现代码在搜索的时候使用了$or的逻辑,更重要的是使用正则表达式,这个就极大拖慢了系统的速度。通过对比正则表达式和精确查找对比,发现这个速度简直差距太大,精确查找只要100ms以内解决的问题,正则需要5-10s。

解决方法

MongoDB有一个全文检索的功能,现在在3.2版本上已经支持。这个版本只能在企业版本上用,所以社区版本基本上不用考虑了。 我们提出的解决方案是分词,将商品名称分词。将分词之后的结果 append上tag作为索引字段进行搜索。这样的话能够大幅度提高搜索时间,但是同时也会造成一些错误的分词啊什么的,我想还是可以接受的。 具体说来,我们使用jieba分词作为分词工具,同时按照空格和’/‘再次进行分词。jieba在新词,特别是品牌名称上识别还是有一些不准。 写的时候还要注意下大小写转换的问题,是时候学习一波python3了。。python2处理中文太痛苦了。

总结

这次很有意思,想了想过程。再次看代码很重要,不能上来就蛮干,一定要使用正确的工具。不能猜。。。毕竟是计算机,不是别的,靠猜是不行的,还需要其他。