信息检索 -- 词项词典及倒排记录表

倒排索引

倒排索引分为两个部分,分别是词项词典和全体倒排记录表,每个词项都有一个记录出现该词项的所有文档的列表,该表中的每个元素记录的是词项在某文档中的以此出现信息,这个表中的每个元素通常称为倒排记录。
词典部分往往放在内存中,而指针指向的每个倒排记录表则往往存放在磁盘上。
另一方面由于采用连续的内存存储,可以充分利用现代计算机的缓存技术来提高访问速度。

构建倒排索引的步骤

  1. 收集需要建立索引的文档。
  2. 将每篇文档转换成一个个词条的列表,这个过程通常称为词条化。
  3. 进行语言学预处理,产生归一化的词条来作为词项。
  4. 对所有文档按照其中出现的词项来建立倒排索引,索引中包括一部词典和一个全体倒排记录表。
    给定一个文档集,我们假定每篇文档都由一个唯一的标识符即编号。在索引构建过程中,给每篇新出现的文档赋一个连续的整数编号。对每篇文档建立索引时的输入就是一个归一化的词条表,也可以看成二元组(词项,文档ID)的一个列表。

词项集合的确定

词条化

在构建倒排索引的第二步,我们需要将每篇文档转换成一个个词条的列表,称为词条化。
词条化的主要任务就是确定哪些才是正确的词条。在英文中,如何确定连字符连接的应该被分为两个单词还是一个是一个难点,还有多个单词组成的特有名词如何分词也是难点。

去除停用词

一个常用的生成停用词表的方法就是将词项按照为文档集频率从高到低排列,然后手工选择哪些语义内容与文档主题关系不大的高频词作为停用词。不对停用词建立索引一般不会对系统造成太大的影响,但对短语查询来说会造成较大的影响。

词项归一化

词条归一化是指将看起来不完全一致的多个词条归纳成一个等价类,以便在它们之间进行匹配的过程。最常规的做法是隐式地建立等价类,每类可以用其中的某个元素来命名。
另一种建立等价类地方法是维护多个非归一化词条之间的关联关系。该方法可以进一步扩展成同义词词表的手工构建。常用地方式是采用非归一化地词条进行索引,并为某个查询词项维护一张由多个词组成的查询扩展此表。当输入一个查询词项时,则根据扩展词表进行扩展并将扩展后得到地多个词所对应的倒排记录表合在一起。另一种方式是在索引构建时就对词进行扩展。比如,对于包含automobile的文档,我们同时也用car来索引。上述两种方法相对于隐式建立等价类的方法来说效率较低。
大小写问题。 大小写转换问题的一个一般处理策略是将所有的字幕都转换成大小写。这种做法通常效果不错。但是这种全部进行小写的处理方法可能会将那些应该区分的词语等同化。

词干还原和词性归并

词干还原和词性归并的目的都是为了减少曲折变化的形式,比如

am,is,are ->be
car,cars,car's,cars->car

词干还原通常指的是一个很粗糙的去除单词两端词缀的启发式过程,这个过程也常常包括取出派生词缀。而词形归并指的是利用词汇表和词形分析来取出屈折词缀,从而返回词的原型或词典中的词的过程。

布尔查询的处理

Brutus AND Calpurnia为例,在倒排索引中执行以下操作:

  1. 在词典中定位Brutus,返回其倒排记录表
  2. 在词典中定位Calpurnia,返回某倒排记录表
  3. 对两个倒排记录表求交集

合并算法

一种是类似于两个有序数组的合并算法,维持对有序数组的两个指针,比较大小进行后移操作。

INTERSECT(p1,p2)
	answer = []
	while (p1 && p2) {
		if(*p1 == *p2) {
			answer.add(*p1)
			p1 = p1->next
			p2 = p2->next
		}
		else if(*p1 < *p2) {
			p1 = p1->next
		}
		else {
			p2 = p2->next
		}
	}
	return answer

时间复杂度O(m+n)。

基于跳表的倒排记录表快速合并算法

在构建索引的同时在倒排记录表上建立跳表。
image
跳表指针只对AND类型的查询有用,而对OR类型的查询不起作用。

INTERSECTWithSKips(p1,p2)
	answer = []
	while(p1 && p2)
		if(*p1 == *p2) {
			answer.add(*p1)
			p1 = p1.next
			p2 = p2.next
		}
		else if(*p1 < *p2) {
			if(hasSkip(p1) && (*skip(p1) <= *p2)) {
				while(hasSkip(p1) && (*skip(p1) <= *p2)) {
					p1 = skip(p1)
				}
			}
			else {
				p1 = p1.next;
			}
		}
		else {
			if(hasSkip(p2) && (*skip(p2) <= *p1)) {
				while(hasSkip(p2) && (*skip(p2) <= *p1)) {
					p2 = skip(p2);
				}
			}
			else {
				p2 = p2.next;
			}
		}

含有位置信息的倒排记录表及短语查询

二元词索引

处理短语查询的一个方法就是将文档中每个连续此堪称一个短语。这种方法将每个连续词对看成词项,这样马上就能处理两个词构成的短语查询。更长的查询可以分成多个短查询来处理:
stanford university palo alto -> "stanford university" AND "university palo" AND "palo alto"

位置信息索引

在这种索引中,对每个词项,以如下方式存储倒排记录:
文档ID:(位置1,位置2,...)
为处理短语查询,仍然需要访问各个词项的倒排记录表。这里可以采用最小文档频率有限的策略,从而可以限制后续合并的候选词项的数目。
位置索引的大小。采用位置索引会大大增加倒排记录表的存储空间。事实上,采用位置索引会加深倒排记录表合并操作的渐进复杂性,这是因为要检查的项的个数不再受限于文档数目而是文档集中出现的所有的词条的个数T。

k近邻搜索两个倒排记录表的合并算法

image

混合索引机制

一个混合策略是:对某些查询使用短语索引或只使用二元词索引,而对其他短语则采用位置索引。

代码实现

实现构建含有位置的倒排索引表

点击查看代码
import os
from nltk.tokenize import word_tokenize
from nltk import pos_tag
from nltk.corpus import stopwords
from nltk.stem import WordNetLemmatizer
from collections import defaultdict
from nltk.corpus import wordnet as wn
import json


def make_index(dir):
    # 从文件夹中获取文档文件
    files = []
    for file in os.listdir(dir):
        files.append(file)
    # 倒排索引
    invert_index_mul = {}
    file_id = 0
    for file in files:
        with open(dir+'/'+file,'r',encoding='utf-8') as f:
            s = f.read()
            # 预处理
            words = word_preprocessing(s)
            # 为单个文件建立倒排索引
            single_inverted = single_invert( words )
            merge_index(invert_index_mul, file_id, single_inverted)
        file_id += 1
    return invert_index_mul


tag_map = defaultdict( lambda: wn.NOUN )
tag_map['J'] = wn.ADJ
tag_map['V'] = wn.VERB
tag_map['R'] = wn.ADV
word_lemmatized = WordNetLemmatizer()


# 预处理:词条化、归一化
def word_preprocessing(s):
    # 转小写,并分词
    words = word_tokenize(s.lower())
    final_words = []
    # 去停用词
    index = 0
    for word, tag in pos_tag( words ):
        if word not in stopwords.words( 'english' ) and word.isalpha():
            # 词干还原
            word_final = word_lemmatized.lemmatize( word, tag_map[tag[0]] )
            final_words.append((index,word_final))
        index += 1
    return final_words


# 为单个文件处理倒排索引
def single_invert(words):
    invert = {}
    for location, word in words:
        locations = invert.setdefault(word,[])
        locations.append(location)
    return invert


# 将单个文件的倒排索引合并到倒排索引中
def merge_index(invert_index_mul, file_id, single_inverted):
    for word, locations in single_inverted.items():
        indices = invert_index_mul.setdefault(word,{})
        indices[file_id] = locations
    return invert_index_mul


def save_index(index_mul):
    with open('invert_index.json', 'w') as fp:
        json.dump(index_mul,ensure_ascii=False)


if __name__ == '__main__':
    index_mul = make_index("./docs")
    for word,index in index_mul.items():
        print(word,index)

k近邻搜索

点击查看代码
import json
import os


# 从json文件中读取倒排索引
def get_index():
    with open( "./invert_index.json", 'r' ) as fp:
        index_mul = json.load( fp )
    return index_mul


def intersect(p1, p2, k):
    answer = []
    k1, k2 = [key for key in p1], [key for key in p2]
    i, j = 0, 0
    while i < len( p1 ) and j < len( p2 ):
        if k1[i] == k2[j]:
            l = []
            pp1, pp2 = p1[k1[i]], p2[k2[j]]
            i1, j1 = 0, 0
            while i1 < len( pp1 ):
                while j1 < len( pp2 ):
                    if abs( pp1[i1] - pp2[j1] ) <= k:
                        l.append( pp2[j1] )
                    elif pp2[j1] > pp1[i1]:
                        break
                    j1 = j1 + 1
                while l != [] and abs( l[0] - pp1[i1] > k ):
                    del (l[0])
                for n in range( 0, len( l ) ):
                    answer.append( [k1[i], pp1[i1], l[n]] )
                i1 = i1 + 1
            i = i + 1
            j = j + 1
        elif k1[i] > k2[j]:
            j = j + 1
        else:
            i = i + 1
    return answer


if __name__ == '__main__':
    index_mul = get_index()
    answer = intersect( index_mul["li"], index_mul["wang"], 3 )
    for doc_id, w1, w2 in answer:
        filename = "./docs/" + os.listdir( "./docs" )[int( doc_id )]
        with open( filename, "r", encoding='utf-8' ) as fp:
            article = fp.read()
            print( article )

PERSONAL INTERSECT(p1,p2,k)
answer = []
while p1!= NULL and p2!=NULL
do if docID(p1) = docID(p2)
	then I=()
		pp1 = position(p1)
		pp2 = position(p2)
		while pp1 != NULL && pp2 != NULL
			do if Abs(pos[pp1]-pos[pp2]) <= k
				then ADD(l,pos(pp2))
				else if pos(pp2) > pos(pp1)
					then break
			pp2 = next(pp2)
			while l != NULL and Abs(l[0]-pos(pp1) > k)
			do DELETE(l[0])
			for each ps ∈ l
			do ADD(answer,(docID,pos(pp1),ps))
			pp1 = next(pp1)
		p1 = next(p1)
		p2 = next(p2)
	else if docID(p1) < docID(p2)
		then p1 = next(p1)
		else p2 = next(p2)
return answer
	
posted @ 2023-03-10 15:09  三文咸鱼  阅读(267)  评论(0)    收藏  举报