MongoDB 实现中文全文搜索

Prerequisite

倒排索引是所有支持全文搜索的数据库的基础。比如 i am iron mani will be soon back,欲查找 be,先查第一句,再查第二局,这是正排;将每个单词提取出来形成一个排序,i {1, 2}am {1}be {2} 形成一个排序,再要搜索 be 的时候,立刻就搜索到了,并且知道对应第二句,这就是倒排。

MongoDB 内置的默认分词器,是按空格分切即可,这是因为英文默认的,那么倒排的最小单位就是单词。如果文档中存在中文,句子直接没有空格,那么倒排的最小单位就是一个字了,解决这个问题也很简单,就是找到最合适的中文分词。

一元分词和二元分词

所谓一元分词:就是一个字一个字地切分,把字当成词(我是团子 ->
所谓二元分词:就是按两字两个分词(我是团子 -> 我是是团团子

很正常的想到,如果将二元分词,作为中文的倒排最小单位,那么就可以全文检索中文了,那么来写实现吧 ~

编写索引程序

先来写二元分词生成器,这个很简单

def bigram_tokenize(word):
    return ' '.join(word[i:i + 2] for i in range(len(word)) if i + 2 <= len(word))

print(bigram_tokenize('我是团子')) # 我是 是团 团子

再对 MongoDB 建立二元分词索引,采用的方法是:创建 _t 字段,用于储存一个句子的全部二元分词即可
我选择使用 pymongo 实现

"""
# 实际文档
{
  _id: ObjectId("630d708529bd493430410366"),
  desc: '切换至聚合模式',
  method: 'put',
  uri: '/api/v2/mail_setting',
  body: '{\n\t"general": {\n\t\t"browse_mode": 1\n\t}\n}'
}
"""
# 编写二元分词索引程序(用 pymongo 实现)

from pymongo import MongoClient
client = MongoClient('localhost', 27017)
db = client["邮件"]
collection = db['邮件多选']

def build_fts():
    # 在 _t 字段建立全文索引
    collection.create_index([('_t', 'text')])
    # 在 _t 字段储存 desc 的全部二元分词
    for item in collection.find():
        collection.update_one({'_id': item['_id']}, {'$set': {'_t': bigram_tokenize(item['desc'])}})

if __name__ == "__main__":
   build_fts()

二元分词索引

那么再来看一下,如何实现检索的,不妨在 MongoDB 数据库中查看

# 获取全部索引
# db.邮件多选.getIndexes()
# 在 _t 字段建立全文索引
# db.邮件多选.ensureIndex({_t:"text"})

# 获取集合下全部文档
db.邮件多选.find().pretty()

[
  {
    _id: ObjectId("630d708529bd493430410366"),
    desc: '切换至聚合模式',
    method: 'put',
    uri: '/api/v2/mail_setting',
    body: '{\n\t"general": {\n\t\t"browse_mode": 1\n\t}\n}',
    _t: '切换 换至 至聚 聚合 合模 模式'
  },
  {
    _id: ObjectId("630d708529bd493430410367"),
    desc: '切换至非聚合模式',
    method: 'put',
    uri: '/api/v2/mail_setting',
    body: '{\n\t"general": {\n\t\t"browse_mode": 2\n\t}\n}',
    _t: '切换 换至 至非 非聚 聚合 合模 模式'
  },
  {
    _id: ObjectId("630d74dac79d6354805b86c3"),
    desc: 'all',
    method: 'put',
    uri: '/api/v2/mail_setting',
    body: '{\n\t"general": {\n\t\t"browse_mode": 1\n\t}\n}',
    params_dict: { mail_id: '1438', mbox: 'INBOX' },
    extracts_dict: { result: '$.result' },
    validations_dict: { '$.result': 'ok' },
    _t: 'al ll'
  }
]

# 检索目标分词
db.邮件多选.find({$text:{$search:"聚合"}})

[
  {
    _id: ObjectId("630d708529bd493430410366"),
    desc: '切换至聚合模式',
    method: 'put',
    uri: '/api/v2/mail_setting',
    body: '{\n\t"general": {\n\t\t"browse_mode": 1\n\t}\n}',
    _t: '切换 换至 至聚 聚合 合模 模式'
  },
  {
    _id: ObjectId("630d708529bd493430410367"),
    desc: '切换至非聚合模式',
    method: 'put',
    uri: '/api/v2/mail_setting',
    body: '{\n\t"general": {\n\t\t"browse_mode": 2\n\t}\n}',
    _t: '切换 换至 至非 非聚 聚合 合模 模式'
  }
]

再用 pymongo 来实现

from pymongo import MongoClient
client = MongoClient('localhost', 27017)
db = client["邮件"]
collection = db['邮件多选']

def bigram_tokenize(word):
    return ' '.join(word[i:i + 2] for i in range(len(word)) if i + 2 <= len(word))

# 全文检索
for post in collection.find({'$text': {'$search': f'"{bigram_tokenize("聚合模式")}"'}}):
    pprint.pprint(post)

{'_id': ObjectId('630d708529bd493430410366'),
 '_t': '切换 换至 至聚 聚合 合模 模式',
 'body': '{\n\t"general": {\n\t\t"browse_mode": 1\n\t}\n}',
 'desc': '切换至聚合模式',
 'method': 'put',
 'uri': '/api/v2/mail_setting'}
{'_id': ObjectId('630d708529bd493430410367'),
 '_t': '切换 换至 至非 非聚 聚合 合模 模式',
 'body': '{\n\t"general": {\n\t\t"browse_mode": 2\n\t}\n}',
 'desc': '切换至非聚合模式',
 'method': 'put',
 'uri': '/api/v2/mail_setting'}

重点就在于,只用检索其中的文字(如 “切换至聚合模式” 中的 “聚合模式”),就可以检索出来
本质就在于,二元分词之间有空格隔开,所以可以像英语那样搜索得到

初步结果,可以实现中文全文检索,但效率不高,因为一个句子的二元分词占用太多,如果可以缩减分词数量就好了

优化

结巴中文分词优化,最流行的Python中文分词组件,它有一种搜索引擎模式,在精确模式的基础上,对长词再次切分,提高召回率,适合用于搜索引擎分词

import jieba
import paddle
import pprint

paddle.enable_static()

test = "批量星标(前两封邮件打星标)"

jieba.enable_paddle()
strs=[test]
for str in strs:
    seg_list = jieba.cut(str,use_paddle=True) # 使用 paddle 模式
    print("Paddle Mode: " + '/'.join(list(seg_list)))

seg_list = jieba.cut(test, cut_all=True)
print("Full Mode: " + "/ ".join(seg_list))  # 全模式

seg_list = jieba.cut(test, cut_all=False)
print("Default Mode: " + "/ ".join(seg_list))  # 精确模式

seg_list = jieba.cut(test)  # 默认是精确模式
print(", ".join(seg_list))

seg_list = jieba.cut_for_search(test)  # 搜索引擎模式
print(", ".join(seg_list))

Paddle Mode: 批量/星标/(/前/两封/邮件/打/星标/)
Full Mode: 批量/ 星/ 标/ (/ 前/ 两封/ 邮件/ 打/ 星/ 标/ )
Default Mode: 批量/ 星标/ (/ 前/ 两封/ 邮件/ 打星标/ )
批量, 星标, (, 前, 两封, 邮件, 打星标, )
批量, 星标, (, 前, 两封, 邮件, 打星标, )

其他优化

  • 组合全文索引(Compound textIndex)
  • 用户体验优化
  • 实时性优化

References

posted @ 2022-09-01 10:29  筱团  阅读(2201)  评论(0编辑  收藏  举报