(一)实现内容:

        1.参照课堂所讲示例,利用构建的词项文档关联矩阵进行布尔检索,要求分别针对AND,OR和NOT进行检索,并分别给出实际检索案例。课堂示例如下所示:

  • 包含 Brutus 和 Caesar 但不包含Calpurnia?
  • 分别取出 Brutus、Caesar 及Calpurnia 对应的行向量,并对 Calpurnia 对应的向量求反,然后进行基于位的与操作,得到:110100 AND 110111 AND 101111 = 100100 .
  • 向量中的第 1 和第 4 个元素为 1,这表明该查询对应的剧本是 Antony and Cleopatra 和 Hamlet (如图1所示)。

信息检索模型实现_python

 图1

        2.参照课堂所讲案例,利用构建的倒排索引进行布尔检索,支持AND操作,并给出实际检索案例。示例如下所示:

  • 布尔查询Brutus AND Calpurnia
  • 查询步骤:

(1) 在词典中定位 Brutus;

(2) 返回其倒排记录表;

(3) 在词典中定位 Calpurnia;

(4) 返回其倒排记录表;

(5) 对两个倒排记录表求交集(如图2所示)。

信息检索模型实现_倒排索引_02

图2


(二)实现步骤:

首先我们要明确,实现上述的功能需要有以下四个基本的步骤:

A.倒排索引(inverted index)的建立过程

B.考虑布尔查询表达式的实现

C.倒排索引之查找包含某个单词的文件(返回其倒排记录表)

D.合并两个倒排记录表


1.倒排索引(inverted index)的建立过程

首先,明确词项-文档关联矩阵实际上具有高度的稀疏性。所以为了节省存储空间,引入倒排记录表(inverted index)。称为词项词典(dictionary,简称词典,有时也称为vocabulary或者lexicon。而vocabulary则指词汇表)。

每个词项都有一个记录出现该词项的所有文档的列表,该表中的每个元素记录的是词项在某文档中的一次出现信息(在后面的讨论中,该信息中往往还包括词项在文档中出现的位置),这个表中的每个元素通常称为倒排记录(posting)。

根据上讲内容,首先,可以把邮件转换为txt文件,将文档读取到一个一维数据之中

然后,将每篇文档转换成一个个token的列表

步骤:

a. 将文本全部转换成小写 b. 根据“非字符”对文本使用正则表达式进行切割(注:当出现两个连续非字符,会切割出现空串,需要手工删除);

最后,构建倒排索引

步骤:

a. 建立如下数据结构:

建立一个哈希表,key值为字符串,value值为列表。

其中key值中存储所有单词,并作为哈希表的索引;value值中第1位记录倒排索引长度,第2位开始记录每个单词出现文章的序号。

信息检索模型实现_字符串_03

b. 遍历token列表:
如果单词出现过,就将文章序号添加到列表尾部,并且长度加一。
单词第一次出现时,将单词加入哈希表。

#sdnu 202011000106

# 打开文件
f = open('所需txt文档')
# 读取文章,并删除每行结尾的换行符
doc = pd.Series(f.read().splitlines())
# 转换为小写,并使用正则表达式进行切割
doc = doc.apply(lambda x: re.split('[^a-zA-Z]', x.lower()))
# 删除空串
for list in doc:
    while '' in list:
        list.remove('')
hashtable = {}
for index, list in enumerate(doc):
    for word in list:
        if word in hashtable:
            hashtable[word].append(index+1)
            hashtable[word][0] += 1
        else:
            hashtable[word] = [1, index+1]
hashtable = dict(
    sorted(hashtable.items(), key=lambda kv: (kv[1][0], kv[0]), reverse=True))

2.考虑布尔查询表达式的实现

(例如,Brutus AND Caesar)

应该在词典中定位 Brutus和Caesar,并返回两个词项的倒排表。

信息检索模型实现_信息检索模型实现_04

然后为每个倒排表定义一个定位指针,两个指针同时从前往后扫描,每次比较当前指针对应的倒排记录,然后再向后移动指向文档ID较小的那个指针或在文档ID相等时同时两个指针,直到某一个倒排表被检索完毕,

这样就能轻易找出符合Brutus AND Caesar的文档,有:文档a、文档b和文档c。

OR和NOT的同理类似,只是对倒排表的操作不同,详细的说明在后面展开。

注意:NOT操作不能简单理解为某一词项的补集,因为补集可能会很大,必须是两个倒排表的减集。

 AND
思路:
参数为两个单词对应的倒排索引列表,返回值为完成AND操作后的结果列表。
需要完成的操作是将同时出现在list1,list2的index筛选出来。因为原先两个列表都是从小到大排序,因此,只需要不断地将指向较小数的指针不断向后移,遇到相同的index时,将index加入结果列表,直到一个指针走到底。

# sdnu 202011000106

def And(list1, list2):
    i, j = 0, 0
    res = []
    while i < len(list1) and j < len(list2):
        # 同时出现,加入结果列表
        if list1[i] == list2[j]:
            res.append(list1[i])
            i += 1
            j += 1
        # 指向较小数的指针后移
        elif list1[i] < list2[j]:
            i += 1
        else:
            j += 1
    return res

OR
思路:
参数为两个单词对应的倒排索引列表,返回值为完成OR操作后的结果列表。
需要完成的操作是将在list1,list2中出现的所有index合并筛选出来。思路与AND的解法大致类似,原先两个列表都是从小到大排序,因此,同样只需要不断地将指向较小数的指针不断向后移,区别是在index大小不相同时仍然需要将index加入结果列表,直到一个指针走到底。
因为OR操作是将两个列表合并,还需要将两个列表中剩余未遍历到的index加入结果列表之中。

#sdnu 202011000106

def Or(list1, list2):
    i, j = 0, 0
    res = []
    while i < len(list1) and j < len(list2):
        # 同时出现,只需要加入一次
        if list1[i] == list2[j]:
            res.append(list1[i])
            i += 1
            j += 1
        # 指向较小数的指针后移,并加入列表
        elif list1[i] < list2[j]:
            res.append(list1[i])
            i += 1
        else:
            res.append(list2[j])
            j += 1
    # 加入未遍历到的index
    res.extend(list1[i:]) if j == len(list2) else res.extend(list2[j:])
    return res

AND NOT
思路:
参数为两个单词对应的倒排索引列表,返回值为完成AND NOT操作后的结果列表。
需要完成的操作是将出现在list1,但是未出现在list2的index筛选出来。原先两个列表都是从小到大排序,因此,同样需要不断地将指向较小数的指针不断向后移,并且当指向list1的index较小时,将index加入结果列表,直到一个指针走到底。
假设list1未遍历完,list2已经结束,那么list1剩余的index一定不会出现在list2中,所以还需要将剩余未遍历到的index加入结果列表之中。

#sdnu 202011000106

def AndNot(list1, list2):
    i, j = 0, 0
    res = []
    while i < len(list1) and j < len(list2):
        # index相等时,同时后移
        if list1[i] == list2[j]:
            i += 1
            j += 1
        # 指向list1的index较小时,加入结果列表
        elif list1[i] < list2[j]:
            res.append(list1[i])
            i += 1
        else:
            j += 1
    # list1 未遍历完,加入剩余index
    if i != len(list1):
        res.extend(list1[i:])
    return res

辅助函数:从哈希表中获取倒排索引列表,并删除第一个元素(用于记录元素个数)

def getList(word):
    return hashtable[word][1:]

3.返回某个词的倒排记录表

find函数用于返回用户所输入词项的倒排记录表用于合并计算。

def find(test, dict1, dict0):
    ft0 = re.split('[()]', test)
    ft = []
    for i in range(len(ft0)):
        ft = ft + ((ft0[i].replace(' ', '')).split("AND"))
    ft = [i for i in ft if i != '']
    p = []
    for j in range(len(ft)):
        p0 = []
        if('OR' in ft[j]):
            ft1 = ft[j].split('OR')
            for k in range(len(ft1)):
                if(ft1[k] in dict1):
                    p0 = p0 + dict1[ft1[k]]
            p0 = list(set(p0))
        elif('NOT' in ft[j]):
            ft[j] = ft[j].replace('NOT', '')
            if(ft[j] in dict1):
                p0 = [y for y in dict0 if y not in dict1[ft[j]]]
            else:
                p0 = dict0
        elif(ft[j] in dict1):
            p0 = dict1[ft[j]]
        p.append(p0)
    return p

4.合并两个倒排记录表

倒排记录表合并算法伪代码如下所示:

信息检索模型实现_倒排索引_05

 步骤:

首先输入两个倒排记录表p1,p2,然后判断两个倒排记录表的指针是否为空,若为空,则结束;

若不为空,判断是否相同,若相同,则p1,p2指针后移;

否则,判断p1指针所指元素是否>p2;

若是,则p2指针后移,否则p1后移。

信息检索模型实现_信息检索模型实现_06


#sdnu 202011000106

def Intersect(p1, p2):
    r = []
    i, j = 0, 0
    lp1, lp2 = len(p1), len(p2)
    while(i < lp1 and j < lp2):
        if(p1[i] == p2[j]):
            r.append(p1[i])
            i, j = i + 1, j + 1
        elif(p1[i] > p2[j]):
            j = j + 1
        else:
            i = i + 1  
    return r

print("输入第一个词项的倒排记录表,文档ID之间用“,”分隔:", end = '')
p1 = input().split(",")
p1 = [int(i) for i in p1]
print("输入第二个词项的倒排记录表,文档ID之间用“,”分隔:", end = '')
p2 = input().split(",")
p2 = [int(i) for i in p2]
#p1 = [1,2,3]
#p2 = [3,4,5]
print("合并结果为:", Intersect(p1, p2))

Intersect函数为倒排记录表计算模块,首先通过len函数获取倒排记录表长度,然后通过下标循环获取记录表元素,通过数值判断合并记录表并存储到新的列表中,最终返回该结果列表。
Input函数获取用户输入字符串,split(“,”)函数对字符串进行切分,[int]方法则将字符串转化为数值列表。

 


(三)实验总结

1.布尔检索的优缺点

优点:构建简单,或许是构建IR系统的一种最简单方式;易被接收,仍是目前最主流的检索方式之一;操作专业化,对于非常清楚想要查什么、能得到什么的用户而言,布尔检索是个强有力的检索工具。

缺点:布尔查询构建复杂,不适合普通用户。如果构建不当, 检索结果就会过多或者过少;没有充分利用词项的频率信息;不能对检索结果进行排序。

2.布尔查询在倒排表上的优化

有两个简单的优化方法:

倒排表的文档ID升序排列:正如在AND操作中演示的那样,文档ID升序排列可以尽量地提前结束对倒排表的操作,而不需要对两个倒排表从头到尾进行检索。

优先处理词频小的词项:在复杂布尔表达式中,例如(tangerine OR trees) AND (marmalade OR skies) AND (kaleidoscope OR eyes),优先合并词频小的词项,生成文档数量少的词项,有利于结合上面的优化方法尽量地提前结束对倒排表的操作。

信息检索模型实现_python_07