布尔检索——短语检索,含位置索引与双词索引


前言

此专栏记录信息检索课程的学习。
部分代码框架来自温柔的助教小哥哥。

Talk is cheap.


一、对文本进行分词

使用了NLTK工具

def get_words(text):
    text = text.lower()  # 全部字符转为小写
    words = nltk.word_tokenize(text)  # 分词
    return words

二、获取文本文件

给定文本文件目录,获取目录下所有符合要求的文件列表

def get_files(dir, file_type='.txt'):
    file_list = []
    for home, dirs, files in os.walk(dir):
        for filename in files:
            if file_type in filename:
                file_list.append(os.path.join(home, filename))
    return file_list

三、词法分析

通过正则表达式对查询进行词法分析

# 构造每种类型词的正则表达式,()代表分组,?P<NAME>为组命名
token_or = r'(?P<OR>\|\|)'
token_not = r'(?P<NOT>\!)'
token_word = r'(?P<WORD>[a-zA-Z]+)'
token_and = r'(?P<AND>&&)'
token_lp = r'(?P<LP>\()'
token_rp = r'(?P<RP>\))'
token_dp = r'(?P<DP>\")'
lexer = re.compile('|'.join([token_or, token_not, token_word,token_and, token_lp, token_rp, token_dp]))  
# 编译正则表达式 

# 用编译好的正则表达式进行词法分析
def get_tokens(query):
    tokens = []  # tokens中的元素类型为(token, token类型)
    for token in re.finditer(lexer, query):
        tokens.append((token.group(), token.lastgroup))
    return tokens

四、布尔检索类

加入了位置索引与双词索引

class BoolRetrieval:
    """
    布尔检索类
    dic = {'hello':{1:[1,3,5], 3:[5,6]}, 'world':{4:[2,4]}}
    """
    def __init__(self, index_path=''):
        if index_path == '':
            self.index = defaultdict(dict)
        # 已有构建好的索引文件
        else:
            data = np.load(index_path, allow_pickle=True)
            self.files = data['files'][()]
            self.index = data['index'][()]
        self.query_tokens = []

    def build_index(self, text_dir):
        self.files = get_files(text_dir)  # 获取所有文件名
        for num in range(0, len(self.files)):
            f = open(self.files[num])
            text = f.read()
            words = get_words(text)  # 分词
            print(words)
            # 构建倒排索引
            # for word in words:
            #     self.index[word].append(num)
            for pos_num in range(0, len(words)):
                word = words[pos_num]
                if num not in (self.index[word]).keys():
                    self.index[word][num] = []
                self.index[word][num].append(pos_num)
        # print(self.files, self.index)
        self.add_phrase('phrase.txt')                                 #插入双词索引
        np.savez('index_2.npz', files=self.files, index=self.index)

    #  在构建索引之后, 加入短语的索引
    def add_phrase(self, phrase_path):
        f = open(phrase_path)
        text = f.read()
        phrases = text.split('\n')
        for i in phrases:
            self.query_tokens = get_tokens("\""+ i + "\"")  # 获取查询的tokens, [(a,word), (b, word)]
            for num in self.evaluate(0, len(self.query_tokens) - 1):
                self.index[i][num] = []   # 短语的索引 位置记录为空

    def search(self, query):
        self.query_tokens = get_tokens(query)  # 获取查询的tokens, [(a,word), (||, or), (b, word)]
        print(self.query_tokens)
        result = []
        # 将查询得到的文件ID转换成文件名
        for num in self.evaluate(0, len(self.query_tokens) - 1):
            result.append(self.files[num])
        return result

    # 递归解析布尔表达式,p、q为子表达式左右边界的下标
    def evaluate(self, p, q):
        # 解析错误
        if p > q:
            return []
        # 单个token,一定为查询词
        elif p == q:
            return self.index[self.query_tokens[p][0]].keys()
        # 去掉外层括号
        elif self.check_parentheses(p, q):
            return self.evaluate(p + 1, q - 1)
        #被双引号包围的短语
        elif self.check_quotation (p, q):
            return self.phrase_search(p + 1, q - 1)
        else:
            op = self.find_operator(p, q)
            if op == -1:
                return []
            # files1为运算符左边得到的结果,files2为右边
            if self.query_tokens[op][1] == 'NOT':
                files1 = []
            else:
                files1 = self.evaluate(p, op - 1)
            files2 = self.evaluate(op + 1, q)
            return self.merge(files1, files2, self.query_tokens[op][1])

    # 判断表达式是否为 (expr)
    def check_parentheses(self, p, q):
        """
        判断表达式是否为 (expr)
        整个表达式的左右括号必须匹配才为合法的表达式
        返回True或False
        """
        
        if self.query_tokens[p][1] == 'LP' and self.query_tokens[q][1] == 'RP':
            rp = 0       # 记录括号
            for i in range(p+1, q):
                if self.query_tokens[i][1] == 'LP':
                    rp += 1
                elif self.query_tokens[i][1] == 'RP':
                    rp -= 1
                    if rp < 0:
                        return False
            if rp == 0:
                return True  
        return False

    # 被双引号包围的短语,内部不能出现引号及运算符
    def check_quotation(self, p, q):
        if self.query_tokens[p][1] == 'DP' and self.query_tokens[q][1] == 'DP':
            for i in range(p+1, q):
                if self.query_tokens[i][1] in ["DP", "&&", "||", "!"]:
                    return False
            return True 
        return False

    #短语查询:  先找出files 包含短语里的每个词, 然后遍历files 及 每个词
    #双词索引查找结果 与 位置索引查找结果 比较
    def phrase_search(self, p, q):
        #  双词索引查找
        result = []
        phrase = self.query_tokens[p][0]
        for i in range(p+1 , q+1):
            phrase += " "
            phrase += self.query_tokens[i][0]
        if phrase in self.index.keys():
            result = list(self.index[phrase].keys())
            return result                                  #若短语存在索引中,直接返回
        
        #位置索引查找
        files = self.evaluate(p, p)   #短语可能存在的文件
        for i in range(p+1, q+1):
            files = self.merge(files, self.evaluate(i,i), 'AND')
        for i in files:
            pos_list = self.index[self.query_tokens[p][0]][i]        # p 词在文件 i 中的位置
            for j in range(p+1, q+1):
                pos_list = [pos+1 for pos in pos_list]               # 如果是短语,后面的词的位置应该 +1
                true_pos_list = self.index[self.query_tokens[j][0]][i]     # 真实的位置
                pos_list = self.merge(pos_list, true_pos_list, 'AND')                    # 取交集
            if not pos_list == []:
                result.append(i)
        return result

    # 寻找表达式的dominant的运算符(优先级最低)
    def find_operator(self, p, q):
        rp = 0 # 记录括号
        index = -1 #记录位置,若没有运算符,返回-1
        i = p 
        tem = 0 # 记录op
        op_dic = {'!':1, '&&':2, '||':3} #比较优先级
        while i <= q:
            if self.query_tokens[i][1] == 'RP':
                rp += 1
            elif self.query_tokens[i][1] == 'LP':
                rp -= 1
            elif rp == 0 and self.query_tokens[i][1] in ['AND', "OR", 'NOT'] :
                if tem <= op_dic[self.query_tokens[i][0]]:
                    tem = op_dic[self.query_tokens[i][0]]
                    index = i
            i += 1
        # print(index)
        return index
    
    def merge(self, files1, files2, op_type):       
        #根据运算符对进行相应的操作
        '''
        此处set方法十分便利,
        但也可以使用遍历的优化方法。
        '''
        
        test = []
        if op_type == 'AND':
            test = list(set(files1) & set(files2))
        elif op_type == "OR":
            test = list(set(files1) | set(files2))
        elif op_type == "NOT":
            test = list(set(range(0, len(self.files))) - set(files2))
        # print(test)

        # result = []
        # i = 0
        # j = 0
        # if op_type == 'AND':          
        #     while i < len(files1) and j < len(files2):
        #         if files1[i] == files2[j]:
        #             result.append(files1[i])
        #             i += 1
        #             j += 1
        #         elif files1[i] < files2[j]:
        #             i += 1
        #         elif files1[i] > files2[j]:
        #             j += 1
        # elif op_type == "OR":            
        #     while i < len(files1) and j < len(files2):
        #         if files1[i] == files2[j]:
        #             result.append(files1[i])
        #             i += 1
        #             j += 1
        #         elif files1[i] < files2[j]:
        #             result.append(files1[i])
        #             i += 1
        #         elif files1[i] > files2[j]:
        #             result.append(files2[j])
        #             j += 1
        #     if i < len(files1):
        #         for t in range(i,len(files1)):
        #             result.append(files1[t])
        #     elif j < len(files2):
        #         for t in range(j, len(files2)):
        #             result.append(files2[t])
        # elif op_type == "NOT":
        #     while i < len(self.files) and j < len(files2):
        #         if i == files2[j]:
        #             i += 1
        #             j += 1
        #         elif i < files2[j]:
        #             result.append(i)
        #             i += 1
        #     for t in range(i, len(self.files)):
        #         result.append(t)
        return test

五、调用

首次调用,生成.npz文件。再次调用时,直接读取该文件中构建好的索引即可。

#首次调用,构建索引
br = BoolRetrieval()
br.build_index(r'要构建索引的文档路径')
#查询调用
while True:
    query = input("请输入与查询(与||,或&&,非!):")
    print(br.search(query))

总结

若有不解或疏漏之处,欢迎留言。

  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值