位置索引介绍

在本系列的前面一篇文章中, 我们介绍了中文分词. 因为"中文"这门语言主要是"词语"表达的语言, 一个句子中间没有空格停顿, 所以我们在分析处理一段话, 或者一篇文章时, 总是把每一个句中根据最合适的语境拆分成一系列的短语, 其实这个过程就是我们所说的中文分词. 但是在有些语言中我们并不能总是把一句话很好的分拆成一个个的词组, 比如中文"中国梦是我们每个人的梦"可以分成这些词组: 中国梦, 我们, 每个(每个人). 但是如果这样一句英语: nothing is impossible. 我们直接是把各个单词直接做为词项索引起来, 但是以后在用户输入"nothing is impossible"这个关键字搜索时, 我们更些把那些含有"nothing is impossible"的句子搜出来, 而不只是简单的把一篇文章中包含了这三个单词的文档全部反馈出来. 所以问题就来了: 如果处理英文中的短语搜索?

一个简单的办法是二元索引, 比如对"friends romans countrymen"会形成如下词项: friends romans, romans countryment. 对于这种方式, 把每个句子都紧凑的两两组对形成词项, 确实可以起到针对二元词检索的作用, 而且可以扩展到更长的短语索引, 但是这样会使词汇表增长过快. 所以业界更普遍的做法是"位置信息索引(positional index)".

 假如用户输入"boy friend"进行搜索, 如果只要出现了"boy" 或者 "friend"的文档都搜索出来, 那么下面三篇文档都满足要求:

  1. the boy and the girl are good friends
  2. you are my boy friend
  3. the boy has many friends.

现在用户应该只想搜出文档 2 出来. 所以简单的用前面介绍的布尔搜索方式: [boy] AND [friend]. 会使搜索结果返回率高, 但正确性低. 而基于"位置信息索引"方式, 我们可以做到这一点. 相比普通的倒排索引方式, 基于位置信息索引的倒排表结构如下:

从上图我们看到, 除了保存文档id外, 还保存在该词项在每个文档中出现的位置. 比如词项boy在文档1中出现在第2个位置, 在文档2中出现在第4个位置, 在文档3中出现在第2个位置. 有了这个索引存储结构, 要找出不同的短语就比较容易了, 比如用户想搜索"boy friend", 只要找出在文档中, boy出现的位置刚好在friend前一个位置的所有文档. 所以文档2满足我们的要求被搜索出来. 下面用python简单实现下这个算法:

# p1, p2是两个上述结构的倒排记录表, k是两个词项的位置在k以内
def positional_interset(p1, p2, k):
  result = [] # 最终的搜索结果, 以(文档id, 词项1的位置, 词项2的位置)
    while p1 is not None and p2 is not None: # 当p1, 和 p2 都没有达到最尾部时
        if p1.docId == p2.docId: # 如果两个词项出现在同一个文档中
            l = [] # 临时变量, 用来存储计算过程中满足位置距离的位置对信息
            pp1 = p1.position
            pp2 = p2.position
            while pp1 is not None: # 先固定pp1的位置, 循环移动pp2的位置进行检查
                while pp2 is not None:
                    if abs(pp1.pos - pp2.pos) <= k: # 如果pp1和pp2的距离小于k, 则满足要求
                        l.append(pp2.pos) # 添加到临时变量
                        pp2 = pp2.next # pp2向后移一个位置
                    elif pp2.pos > pp1.pos: # 如果pp2当前的位置相对pp1已经超过给定的范围(构不成短语要求), 则停止移动pp2, 后续后把pp1再往前移动一个位置
                        break
                while not l and abs(l[0] - pp1.pos) > k: # 当每次移动一次pp1时, l里面会存储上一次计算所得的pp2的一些位置, 这里要过滤那些相对于当前pp1最新位置, 那些不再满足要求的pp2的位置
                    del l[0]

                for p in l:
                    result.append[(p1.docId, pp1.pos, p)] # 把最终的结果加入到结果集中

                pp1 = pp1.next # pp1向前移动一个位置, 重复上次逻辑计算

            p1 = p1.next
            p2 = p2.next
        elif p1.docId < p2.docId:
            p1 = p1.next
        else:
            p2 = p2.next

上面的算法的复杂度跟文档个数, 及词项在文档中出现的次数有关. 即词项出现的频率越多, 计算量越大.

------------------------------------------EOF

posted @ 2014-10-18 01:02  海鸟  阅读(2501)  评论(0编辑  收藏  举报