择要
MongoDB在2.4版中引入全文索引后几经迭代更新已经比较完美地支持以空格分隔的西语,但一贯不支持中日韩等措辞,社区版用户不得不通过挂接ElasticSearch等支持中文全文搜索的数据库来实现业务需求,由此引入了许多业务限定、安全问题、性能问题和技能繁芜性。作者独辟路子,基于纯MongoDB社区版(v4.x和v5.0)实现中文全文搜索,在靠近四千万个记录的商品表搜索商品名,检索韶光在200ms以内,并利用Change Streams技能同步数据变革,知足了业务须要和用户体验需求。
本文首先描述碰着的业务需求和困难,先容了MongoDB和Atlas Search对全文搜索的支持现状,然后从全文搜索事理讲起,结合MongoDB全文搜索实现,挂接中文分词程序,达到纯MongoDB社区版实现中文全文搜索的目标;针对性能需求,从分词、组合文本索引、用户体验、实时性等多方面给出了优化实践,使全体方案达到商业级的实用性。

业务需求和困难
电商易是作者公司的电商大数据工具品牌,旗下多个产品都有搜索商品的业务需求。早期的时候,我们的搜索是直接用$regex去匹配的,在数据量比较大的时候,须要耗时十几秒乃至几分钟,以是用户总是反馈说搜不出东西来。实在不是搜不出来,而是搜的韶光太长,做事器掐断连接了。加上我们普遍利用极简风格的首页,像搜索引擎那样,有个框,右侧是一个“一键剖析”的按钮,用户点击后显示干系的商品的数据。搜索成为用户最常用的功能,搜索性能的问题也就变得更加突出了,优化搜索成为了迫不及待的任务。
MongoDB在2.4版中引入文本索引(Text Index)实现了全文搜索(Full Text Search,下文简称FTS),虽然后来在2.6和3.2版本中两经改版优化,但一贯不支持中日韩等措辞。MongoDB官网推出做事Atlas Search,也是通过外挂Lucene的办法支持的,这个做事须要付费,而且未在中国大陆地区运营,与我们无缘,以是还是要探求自己的办理之道。
那么能否仅仅基于MongoDB社区版实现中文全文搜索呢?带着这个问题,作者深入到MongoDB文本索引的文档、代码中去,创造了些许端倪,并逐步实现和优化了纯MongoDB实现中文全文搜索的方案,下文将从全文搜索的事理讲起,详细描述这个方案。
过程
全文搜索事理
倒排索引是搜索引警的根本。倒排是与正排相对的,假设有一个 ID 为 1 的文档,内容为“ My name is LaiYonghao.“,那么通过 ID 1 总能找到这个文档所有的词。通过文档 ID 找包含的词,称为正排;反过来通过词找到包括该词的文档 ID,称为倒排,词与文档ID的对应关系称为倒排索引。下面直接引用一下维基百科上的例子。
0 "it is what it is"1 "what is it"2 "it is a banana"
上面 3 个文档的倒排索引大概如下:
"a": {2}"banana": {2}"is": {0, 1, 2}"it": {0, 1, 2}"what": {0, 1}
这时如果要搜索banana的话,利用倒排索引可以立时查找到包括这个词的文档是ID为2的文档。而正排的话,只能一个一个文档找过去,找完3个文档才能找到(也便是$regex的办法),这种情形下的耗时大部分是无法接管的。
倒排索引是所有支持全文搜索的数据库的根本,无论是PostgreSQL还是MySQL都是用它来实现全文搜索的,MongoDB也不例外,这也是我们终极办理问题的根本底座。大略来说,倒排索引类似MongoDB里的多键索引(Multikey Index),能够通过内容元素找到对应的文档。文本索引可以大略类比为对字符串分割(即分词)转换为由词组成的数组,并建立多键索引。虽然文本索引还是停滞词、同义词、大小写、权重和位置等信息须要处理,但大致如此理解是可以的。
西文的分词较为大略,基本上是按空格分切即可,这便是MongoDB内置的默认分词器:当建立文本索引时,默认分词器将按空格分切句子。而CJK措辞并不该用空格切分,而且最小单位是字,以是没有办法直策应用MongoDB的全文搜索。那么如果我们预先将中文句子进行分词,并用空格分隔重新组装为“句子”,不就可以利用上MongoDB的全文搜索功能了吗?通过这一个打破点进行深挖,实验证明,这是可行的,由此我们的问题就转化为了分词问题。
一元分词和二元分词从上文可知,数据库的全文搜索是基于空格切分的词作为最小单位实现的。中文分词的方法有很多,最根本的是一元分词和二元分词。
所谓一元分词:便是一个字一个字地切分,把字当成词。如我爱北京天安门,可以切分为我爱北京天安门,这是最大略的分词方法。这种方法带来的问题便是文档过于集中,常用汉字只有几千个,姑且算作一万个,如果有一千万个文档,每一个字会对应到10000000/10000avg_len(doc)个。以文档内容是电商平台的商品名字为例,均匀长度约为 60 个汉字,那每一个男人对应 6 万个文档,用北京两字搜索的话,哀求两个长度为6万的凑集的交集,就会要良久的韶光。以是大家更常利用二元分词法。
所谓二元分词:便是按两字两个分词。如我爱北京天安门,分词结果是我爱爱北北京京每天安安门。可见两个字的组合数量多了很多,相对地一个词对应的文档也少了许多,当搜索两个字的时候,如北京不用再求交集,可以直接得到结果。而搜索三个字以上的话,如天安门也是由天安和安门两个不太常见的词对应的文档凑集求交集,数量少,运算量也小,速率就很快。下面是纯中文的二元分词Python代码,实际事情中须要考虑多措辞稠浊的处理,在此仅作示例:
def bigram_tokenize(word): return' '.join( word[i:i+2]for i inrange(len(word))if i+2<=len(word) )print(bigram_tokenize('我爱北京天安门'))# 输出结果:我爱 爱北 北京 京天 天安 安门
Lucene自带一元分词和二元分词,它的中文全文搜索也是基于二元分词和倒排索引实现的。接下来只须要预先把句子进行二元分词再存入MongoDB,就可以借助它已有的西语全文搜索功能实现对中文的搜索。
编写索引程序编写一个分词程序,它将全表遍历须要实现全文搜索的凑集(Collection),并将指定的文本字段内容进行分词,存入指定的全文索引字段。
以对products表的name字段建立全文索引为例,代码大概如下:
def build_products_name_fts(): # 在 _t 字段建立全文索引 db.products.create_index([('_t', 'TEXT')]) # 遍历凑集 for prod in db.products.find({}): db.products.update_one( {'_id': prod['_id']}, { '$set': { '_t': bigram_tokenize(prod['name']) # 写入二元分词结果 } } )if__name__=="__main__": build_products_name_fts()
只须要10来行代码就行了,它在首次运行的时候会做一次全表更新,完成后即可用以全文搜索。MongoDB的高等用户也可以用带更新的聚合管道完成这个功能,只须要写针对二元分词实现一个javascript函数(利用$function操作符)放到数据库中实行即可。
查询词预处理由于我们针对二元分词的结果做搜索,以是无法直接搜索。以牛仔裤为例,二元分词的全文索引里根本没有三个字的词,是搜索不出来结果的,必须转换成短语"牛仔仔裤"这样才能匹配上,以是要对查询词作预处理:进行二元分词,并用双引号约束位置,这样才能精确查询。
products = db.products.find( { '$text': { '$search': f'"{bigram_tokenize(kw)}"', } })
如果有多个查询词或带有反向查询词,则须要作相应的处理,在此仅以独词查询示例,详细不用细述。
MongoDB不仅支持在find中利用全文搜索,也可在aggregate中利用,在find中利用是差不多的,不过要留神的是只能在第一阶段利用带$text的$match。
初步结果首先值得肯定的是做了大略的二元分词处理之后,纯MongoDB就能够实现中文全文搜索,搜索结果是精准的,没有错搜或漏搜的情形。
不过在性能上比较差强人意,在约4000万文档的products凑集中,搜索牛仔裤须要10秒钟以上。而且在项目的利用场景中,我们创造用户实际查询的词很长,每每是直接在电商平台复制商品名的一部分,乃至全部,这种极度情形须要几分钟才能得到查询结果。
在产品层面,可以对用户查询的词长度进行限定,比如最多3个词(即2个空格)且总长度不要超过10个汉字(或20个字母,每汉字按两个字母打算),这样可以掌握相对快一点。但这样的规则不随意马虎让用户明白,用户体验受损,须要想办法优化性能。
优化
结巴中文分词
结巴中文分词是最盛行的Python中文分词组件,它有一种搜索引擎模式,在精确模式的根本上,对长词再次切分,提高召回率,适宜用于搜索引擎分词。下面是引用自它项目主页的示例:
seg_list = jieba.cut_for_search("小明硕士毕业于中国科学院打算所,后在日本京都大学深造") # 搜索引擎模式print(", ".join(seg_list))# 结果:【搜索引擎模式】:小明, 硕士, 毕业, 于, 中国, 科学, 学院, 科学院, 中国科学院, 打算, 打算所, 后, 在, 日本, 京都, 大学, 日本京都大学, 深造
可见它的分词数量比二元分词少了很多,对应地索引产寸也小了。利用二元分词时,4000万文档的products表索引超过40GB,而利用结巴分词后,减少到约26GB。
由上例也可看出,结巴分词的结果丢失了位置信息,以是查询词预处理过程也可以省略加入双引号,这样MongoDB在全文搜索时打算量也大大少,搜索速率加速了数十倍。以牛仔裤为例,利用结巴分词后查询韶光由10秒以上降到约400ms,而直接复制商品名进行长词查询,也基本上能够在5秒钟之内完成查询,可用性和用户体验都得到了巨大提升。
结巴分词的毛病是须要行业词典进行分词。比如电商平台的商品名都有长度限定,都是针对搜索引擎优化过的,日常用语“男装牛仔裤”在电商平台上被优化成了“牛仔裤男”,这显然不是一个常日意义上的词。在没有行业词典的情形下,结巴分词的结果是牛仔裤男,用户搜索时,将打算“牛仔裤”和“男”的结果交集;如果利用自定义词典,将优化为牛仔裤牛仔裤男,则无需打算,搜索速率更快,但增加了掩护自定义词典的本钱。
组合全文索引(Compound textIndex)组合全文索引是MongoDB的一个特色功能,是指带有全文索引的组合索引。下面引用一个官方文档的例子:
db.inventory.createIndex( { dept:1, description:"text" })// 查询db.inventory.find( { dept:"kitchen",$text: { $search:"green" } } )
通过这种办法,当查询部门(dept)字段的描述中是否有某些词时,由于先过滤掉了大量的非同dept的文档,可以大大减少全文搜索的韶光,从而实现性能优化。
只管组合全文索引有许多限定,如查询时必须指定前缀字段,且前缀字段只支持等值条件匹配等,但实际运用中还是有很多适用场景的,比如商品凑集中有分类字段,天然便是等值条件匹配的,在此情形根据前缀字段的分散程度,基本上可以得到同等比例的性能提升,一样平常都在10倍以上。
用户体验优化MongoDB的全文搜索实在是很快的,但当须要根据其它字段进行排序的时候,就会显著变慢。比如在我们的场景中,当搜索牛仔裤并按销量排序时,速率显著变慢。以是在产品设计时,应将搜索功能独立,只办理“快速找出最想要的产品”这一个问题,想在一个功能里办理多个问题,一定须要付出性能代价。
另一个有助于提升提升用户体验的技能手段是一次搜索,大量缓存。便是一个搜索词第一次被查询时,直接返回前面多少条结果,缓存起来(比如放到Redis),当用户翻页或其他用户查询此词时,直接从缓存中读取即可,速率大幅提升。
实时性优化前文提到编写索引程序对全文索引字段进行更新,但如果后面持续增加或修正数据时,也须要及时更新,否则实时性没有保障。在此可以引入Change Streams,它许可运用程序访问实时数据变动,而不必担心跟踪 oplog 的繁芜性和风险。运用程序可以利用Change Streams来订阅单个凑集、数据库或全体支配中的所有数据变动,并立即对它们作出反应。由于Change Streams利用聚合框架,运用程序还可以根据须要筛选特定的变动或转换关照。Change Streams也是MongoDB Atlas Search同步数据变革的方法,以是它是非常可靠的。利用Change Streams非常大略,我们的代码片断类似于这样:
try: # 订阅 products 凑集的新增和修正Change Streams with db.products.watch( [{'$match': {'operationType': {'$in':['insert', 'update']}}}]) as stream: for insert_change in stream: check_name_changed_then_update(insert_change)exceptpymongo.errors.PyMongoError: logging.error('...')
在check_name_changed_then_update()函数中我们检讨可搜索字段是否产生了变革(更新或删除),如果是则对该文档更新_t字段,从而实时数据更新。
优化
本文描述了作者实现纯MongoDB实现中文全文搜索的过程,终极方案在生产环境中稳定运营了一年多韶光,并为多个产品采纳,经受住了业务和韶光的磨练,证明了方案的可行性和稳定性。在性能上在靠近四千万个记录的商品表搜索商品名,检索韶光在200ms以内,并利用Change Streams技能同步数据变革,知足了业务须要和用户体验需求。
作者在完成对中文全文搜索的探索过程中,经由对MongoDB源代码的剖析,创造mongo/src/mongo/db/fts目录包含了对不同措辞的分词框架,在未来,作者将考试测验在MongoDB中实现中文分词,期待用上内建中文全文搜索支持的那一天。
关于作者:赖勇浩
广州天勤数据有限公司
2005年至2012年在网易(广州)、广州银汉等公司从事网络游戏开拓和技能管理事情。2013年至2014年在广东彩惠带领团队从事彩票行业数字化研发和履行。2015年至今,创办广州齐昌网络科技有限公司,后并入广东天勤科技有限公司,任职CTO,并且担当广州天勤数据有限公司联合创始人&CEO,现带领团队卖力电商大数据剖析软件的研发事情,形成由看店宝等十余个数据工具组成的产品矩阵,覆盖剖析淘宝、天猫、拼多多和抖音等多个电商平台数据,做事全国各地200多万电商从业职员。热爱分享,于2009年联合创办程序员社区TechParty(原珠三角技能沙龙)并担当两届组委主席,于2021年创办中小团队技能管理者和技能专家社区小红花俱乐部,均深受目标群体的喜好。
精通Python、C++、Java等编程措辞和Linux操作系统,熟习大规模多人在线系统的设计与实现,在大数据方面,对数据网络、洗濯、存储、管理、剖析等方面有丰富履历,设计和实现了准PB级别的基于MongoDB的电商数据湖系统,对冷热数据分级处理、系统本钱掌握和数据产品设计研发有一定心得。
曾在《打算机工程》等期刊揭橥多篇论文,于2014年出版《编写高质量代码:改进Python程序的91个建议》一书。