《信息检索》一个简单的古诗词检索系统(双词索引)

前排放效果图(UI很难看啦,但是tkinter好偷懒简洁的嘛,几行就写完了)

整个数据集共311828首古诗词构建索引共花费8分09秒,打开构建好的索引需要30秒。不过好像有个bug,年代久远忘的差不多了,就不再考证了

0 要求

要求是这样的:

支持短语查询,支持与、或、非操作,构建复合索引(双词+位置)

原数据格式:都删掉了懒得再去下载了,总之数据是很干净的漂漂亮亮的json格式的字典,nice!

1 思路

从当时写的作业文档里直接搬运了,代码在这里٩(˃̶͈̀௰˂̶͈́)و

1.1 索引

索引结构

建立索引前先确定索引结构。这里建立两个索引,一个用于存字的位置,一个用于存双词

双词索引的存储格式如下,先用词典存单字,再用词典存以这个字为前缀的词,最后用列表存这个词出现的古诗编号

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"每": {
"每勞書": [
308302
]
},
"毒": {
"毒匪": [
875
],
"毒萬蜂": [
1545
],
"毒發": [
2548,
65517
]
}
}

位置索引的存储格式如下,先用词典存单个的汉字,再用词典存包含这个汉字的古诗的编号,最后存这个字在诗中出现的位置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{
"上": {
"1": [
27
],
"3": [
38
],
"8": [
42
],
"10": [
33
],
"12": [
64
],
"15": [
1,
4
]
}
}

然后就是一些为了便于输出结果存的索引了,不展开说明

构建索引

接下来就是构建索引。先利用结巴分词对诗句进行划分,如果得到的结果是一个字就跳过,如否则就把这个词加入到第一个字对应的字典中,并记录古诗编号。这样每个字后面都会存有所有以这个字为前缀的词

在构建位置索引时,一个字一个字读取诗句字符串,并保存某个字在某首诗中出现的位置。最后对两个索引按照发音顺序排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
def create_index():
path = file_path
filenames = os.listdir(path)
shuangci_index = {}
weizhi_index = {}
msg_poet = []
ji = 0
for filename in filenames:
# print(filename)
name = os.path.split(filename)[1].split(".")
if name[0] == 'authors':
continue
else:
poet_json = get_poet(filename)
for poet in poet_json:
author = poet['author']
paragragh = poet['paragraphs']
title = poet['title']
# 储存古诗词信息
poet_dict = {}
poet_dict['title'] = title
poet_dict['author'] = author
poet_dict['paragragh'] = paragragh
msg_poet.append(poet_dict)
# 建立双词索引
hang = len(paragragh)
paragraphs = ''
for j in range(hang):
paragraphs = paragraphs + paragragh[j]
results = list(jieba.cut_for_search(paragraphs))
# 去除停用词
final = []
for word in results:
if word not in STOPWORDS:
final.append(word)
results = final
for word in results:
if len(word) <= 1:
continue
if word[0] not in shuangci_index:
poet_list = []
poet_list.append(ji)
shuangci_index[word[0]] = {}
shuangci_index[word[0]][word] = poet_list
else:
if word not in shuangci_index[word[0]]:
poet_list = []
poet_list.append(ji)
shuangci_index[word[0]][word] = poet_list
else:
shuangci_index[word[0]][word].append(ji)

# 建立位置索引
for i in range(len(paragraphs)):
if paragraphs[i] not in STOPWORDS:
if paragraphs[i] not in weizhi_index:
weizhi_list = []
weizhi_list.append(i)
weizhi_index[paragraphs[i]] = {}
weizhi_index[paragraphs[i]][ji] = weizhi_list
else:
if ji not in weizhi_index[paragraphs[i]]:
weizhi_list = []
weizhi_list.append(i)
weizhi_index[paragraphs[i]][ji] = weizhi_list
else:
weizhi_index[paragraphs[i]][ji].append(i)
ji += 1
shuangci_index = sort_list(shuangci_index)
write_to_file(shuangci_index, project_path + '/shuangci.json')
weizhi_index = sort_list(weizhi_index)
write_to_file(weizhi_index, project_path + '/weizhi.json')
write_to_file(msg_poet, project_path + '/poet_msg.json')

1.2 搜索

简繁体转换

由于输入一般都是简体,而该古诗集中的数据都是繁体,因此需要把输入的简体转化为繁体再进行搜索,利用zhconv库的convert函数实现。

1
text = zhconv.convert(text, 'zh-tw')

单个短语查询

先写一个可以查询单个字或短语的函数,作用是返回包含这个词或短语的古诗编号列表

一开始先判断输入的词是否为双词索引中存在的长的词,如“明月”,若双词索引中包含“明”,而下一级的字典中又包含“明月”,那么可以快速的直接返回包含“明月”的古诗编号集合

如果输入的字或词在双词索引中不存在,那么就需要通过位置索引来寻找。位置索引中的键为单个字,实现搜索存在某字的古诗是容易的

而如果是短语的话,就需要利用到位置信息。如果搜索“花间一壶酒”,那么如果“花”在编号为i的古诗中的位置是x,那么需要满足存在“间”在编号为i的古诗中的位置是i+1,“一”在编号为i的古诗中的位置是x+2,……,以此类推

因此首先对于每个字返回的列表集合,编写函数JiaoList实现两两合并,获得一个同时出现了所有字的文档集合

1
2
3
4
5
6
def JiaoList(list1, list2):
res = []
for item in list1:
if item in list2:
res.append(item)
return res

对于初步筛选出来的古诗集合,循环古诗i,在每次循环中初始集合为第一个字在文档i中出现的位置,然后利用下面的CheckList函数对位置进行判断,如果第二个字在文档i出现的某个位置恰好比第一个中的某个大1,说明这两个字在文档1中是连在一起的。以此类推返回所有位置列表

1
2
3
4
5
6
7
def CheckList(list1, list2):
res = []
for item1 in list1:
for item2 in list2:
if item2 == item1 + 1:
res.append(item2)
return res

如果短语中的所有字遍历完后位置列表不为空,那么说明存在这个短语,就在结果列表中加入古诗i,最终根据一个词或短语word返回包含该词或短语的所有古诗编号集合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
def SearchWord(word):
if (len(word) == 0):
return
print(word)
if (len(word) > 1):
if word[0] in INDEX_SHUANGCI:
if word in INDEX_SHUANGCI[word[0]]:
return INDEX_SHUANGCI[word[0]][word]
if word[0] not in INDEX_WEIZHI:
return []
# poeta存储包含每个字的文档集
poeta = INDEX_WEIZHI[word[0]].keys()
for i in range(1, len(word)):
zi = word[i]
if zi not in INDEX_WEIZHI:
return []
poetb = INDEX_WEIZHI[zi].keys()
poeta = JiaoList(poeta, poetb)
if len(poeta) == 0:
return []
res = []
for poetid in poeta:
weizhia = INDEX_WEIZHI[word[0]][poetid]
for i in range(1, len(word)):
weizhib = INDEX_WEIZHI[word[i]][poetid]
weizhia = CheckList(weizhia, weizhib)
if len(weizhia) != 0:
res.append(poetid)
# if len(res) == 0:
# print("没有找到关于\"" + word + "\"的信息")
return res

与或非查询

“或搜索”是同时输入很多字或短语,返回包含其中任一的所有古诗集合。“或”的实现思路是对于每个字或短语得到的结果列表,因为只要包含其中一个就可以,所以相加即可,然后利用set实现去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def SearchOr():
text_or = e1.get()
if (text_or == ""):
print("输入为空!")
return
text_or = zhconv.convert(text_or, 'zh-tw')
search_list = list(text_or.split(" "))
print("您输入的词语集合为:", end="")
print(search_list)
res = []
for word in search_list:
poetlist = SearchWord(word)
res = res + poetlist
res = set(res)
if len(res) == 0:
print("没有找到关于\"" + text_or + "\"的信息")
return res

“与搜索”是同时输入很多字或短语,返回同时包含所有词的古诗集合。与的实现思路是,对于每个字或短语返回的结果集,因为要包含所有,所以求交集即可,利用set去重

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def SearchAnd():
text_and = e2.get()
if (text_and == ""):
print("输入为空!")
return
text_and = zhconv.convert(text_and, 'zh-tw')
search_list = list(text_and.split(" "))
print("您输入的词语集合为:", end="")
print(search_list)
res = []
for word in search_list:
poetlist = SearchWord(word)
if len(res) != 0:
res = JiaoList(res, poetlist)
else:
res = poetlist
res = set(res)
if len(res) == 0:
print("没有找到关于\"" + text_and + "\"的信息")
return res

“非搜索”是同时输入很多字或短语,返回所有包含其中任一的古诗集合。因为这样筛选出的古诗会很多,因此非搜索只支持在“与搜索”或者“或搜索”的基础上减去“非搜索”的结果

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
def SearchNot():
text_not = e3.get()
if (text_not == ""):
print("输入为空!")
return
text_not = zhconv.convert(text_not, 'zh-tw')
search_list = list(text_not.split(" "))
print("您输入的词语集合为:", end="")
print(search_list)
res = []
for word in search_list:
poetlist = SearchWord(word)
if len(res) != 0:
res = JiaoList(res, poetlist)
else:
res = poetlist
res = set(res)
return res

“复合搜索”,在“非搜索”的描述中已经实现了初步的复合搜索,“非搜索”只能与“与搜索”或“或搜索”同时进行。将“或与非”分开输入,分别保存在e1,e2,e3中,假设返回结果保存在t1,t2,t3中,那么最终复合搜索的结果为:+t1+t2-t3。

3 加了个好玩的

上面效果图的对诗系统参考自github上的seq2seq-couplet项目。这是一个使用seq2seq模型对对联的项目,用Tensorflow编写,数据集为70万个对联,原作者在Nivida GTX-1080 GPU上训练了模型约4天。

原作者并未公开训练好的模型,算力有限因此使用了github上别人训练好的模型,模型来源


杂谈

大三上的信息检索是个有点刺激的课

没有别人做不出来的,只有我做不出来的
没有一届做的差的,只有一届比一届做的更好的
所以没有很水的作业,只有一届比一届难的作业

要不是为了那两个学分!

(开玩笑的,课是好课)

第一个作业是「安然邮件系统检索」,那个原数据格式太乱了,我讨厌洗数据!所以那个作业我做的非常囫囵吞枣,

第二个作业就是本博客讲的这个,这可能是我完成的最完整的一个了QAQ所以记录下来

第三个作业是「nku教师检索系统」,我还是觉得Django好难用啊ORZ,主要不是它难用,是我不会用,就各种报错唉,做的也十分凹糟

第四个作业是「智能小开问答系统」,很有意思的作业。数据来源是自己爬的页面和google文件搜索,然后学了一下word2vec计算词向量和句向量,检索就是基于向量相似度的比较,用抽样和多线程加了个速(从别人github里学的真的好厉害)。在一个大佬的安利下还试了试chatterbot,但是训练出来的是什么玩意儿,我猜想这个肯定需要自己构造许多问句才可以,不然只能获得各种文不对题的回答

就是这样啦~

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2018-2020 LeFlacon

奶茶一杯 快乐起飞

支付宝
微信