全文检索的背后

620 阅读4分钟

写在前面

我在Django REST framework 搜索 以及 MySQL的全文检索 这两篇文章在多次提到全文检索;全文索引背后的算法为 倒排索引 所以今天就聊一聊全文检索背后的实现以及自己实现一套搜索。

项目准备

  • 数据准备 依旧用到几篇喜欢的现代诗
  • 分词工具 大名鼎鼎的jieba分词
  • 语言 python3.6

分词统计

关于结巴分词不同 API的详细介绍(来自jieba官网)

  • jieba.cut 方法接受四个输入参数: 需要分词的字符串;cut_all 参数用来控制是否采用全模式;HMM 参数用来控制是否使用 HMM 模型;use_paddle 参数用来控制是否使用paddle模式下的分词模式,paddle模式采用延迟加载方式,通过enable_paddle接口安装paddlepaddle-tiny,并且import相关代码;
  • jieba.cut_for_search 方法接受两个参数:需要分词的字符串;是否使用 HMM 模型。该方法适合用于搜索引擎构建倒排索引的分词,粒度比较细
  • 待分词的字符串可以是 unicode 或 UTF-8 字符串、GBK 字符串。注意:不建议直接输入 GBK 字符串,可能无法预料地错误解码成 UTF-8
  • jieba.cut 以及 jieba.cut_for_search 返回的结构都是一个可迭代的 generator,可以使用 for 循环来获得分词后得到的每一个词语(unicode),或者用
  • jieba.lcut 以及 jieba.lcut_for_search 直接返回 list
  • jieba.Tokenizer(dictionary=DEFAULT_DICT) 新建自定义分词器,可用于同时使用不同词典。jieba.dt 为默认分词器,所有全局分词相关函数都是该分词器的映射。
import jieba
text = '今生今世 永不再将你想起除了除了在有些个因落泪而湿润的夜里 如果如果你愿意'

collections.Counter(jieba.lcut_for_search(text))

"""
Counter({'今生': 1,
         '今世': 1,
         '今生今世': 1,
         ' ': 2,
         '永不': 1,
         '再': 1,
         '将': 1,
         '你': 2,
         '想起': 1,
         '除了': 2,
         '在': 1,
         '有些': 1,
         '个': 1,
         '因': 1,
         '落泪': 1,
         '而': 1,
         '湿润': 1,
         '的': 1,
         '夜里': 1,
         '如果': 2,
         '愿意': 1})
"""

倒排索引

也常被称为反向索引、置入档案或反向档案,是一种索引方法,被用来存储在全文搜索下某个单词在一个文档或者一组文档中的存储位置的映射。它是文档检索系统中最常用的数据结构。

举个例子 来自 Elastic 倒排索引

1. The quick brown fox jumped over the lazy dog
2. Quick brown foxes leap over lazy dogs in summer

所以分词处理后的倒排索引为

TermDoc_1Doc_2
QuickX
TheX
brownXX
dogX
dogsX
foxX
foxesX
inX
jumpedX
lazyXX
leapX
overXX
quickX
summerX
theX

这样的好处就是可以通过关键词搜索对应的文章

完整的例子

image.png

下面为python实现的完整的demo,主要步骤如下

  1. 分词建立索引
  2. 查找文档
  3. 文档评分 (这里只用单词的频次代替)
import jieba
import collections

articles = (
    (1, '今生今世 永不再将你想起 除了 除了在有些个 因落泪而湿润的夜里 如果 如果你愿意'),
    (2, '有一天路标迁了希望你能从容有一天桥墩断了希望你能渡越有一天栋梁倒了希望你能坚强有一天期待蔫了希望你能理解'),
    (3, '你 一会看我一会看云我觉得你看我时很远你看云时很近'),
    (4, '你站在桥上看风景 看风景人在楼上看你 明月装饰了你的窗子 你装饰了别人的梦'),
    (5, '我向你倾吐思念 你如石像 沉默不应 如果沉默是你的悲抑 你知道这悲抑最伤我心')
)

text_index = {}

# 构造倒排索引
def gen_index():
    for article in articles:
        index, text = article
        words = jieba.lcut_for_search(text)
        # 这里只对单词长度大于一的词语做分词,顺便剔除空格符
        words = [wd for wd in words if len(wd) > 1]

        # print(collections.Counter(words))

        for word, count in collections.Counter(words).items():
            index_count = text_index.get(word)
            if index_count is None:
                text_index[word] = [0] * len(articles)
                text_index[word][index-1] = count
            else:
                index_count[index-1] = count


# 文章搜索
def search(word: str):
    text_index_get = text_index.get(word)
    if text_index_get is None:
        print('没有文章满足')
    else:
        [print("文章为:" + dict(articles).get(index+1) + "  得分:" + str(count))
         for index, count in enumerate(text_index_get) if count > 0]


def main():
    gen_index()
    search("如果")


if __name__ == "__main__":
    main()

"""
结果为:
Building prefix dict from the default dictionary ...
Loading model from cache /var/folders/st/b16fyn3s57x_5vszjl599njw0000gn/T/jieba.cache
Loading model cost 0.819 seconds.
Prefix dict has been built successfully.
文章为:今生今世 永不再将你想起 除了 除了在有些个 因落泪而湿润的夜里 如果 如果你愿意  得分:2
文章为:我向你倾吐思念 你如石像 沉默不应 如果沉默是你的悲抑 你知道这悲抑最伤我心  得分:1
"""

参考资料

jieba 分词