实验目的:掌握倒排索引(reverted index)的建立过程;掌握倒排记录表(postings lists)的合并算法;了解Lucene开源信息检索库(library)的简单调用。
实验要求:
(1). 阅读教材《Introduction to Information Retrieval》第一章(特别是第1页),谈谈你对“Information Retrieval”定义或含义的理解,特别是信息检索的主要特点。请对信息检索的主要特点逐个加以详细阐述(至少包含4个特点,每个特点写一段话,突出重点)。(20分)
(2). 阅读教材《Introduction to Information Retrieval》第8页Figure 1.4中所描述的倒排索引(reverted index)建立的详细过程,使用附件“HW1.txt”中的60个文档(每行表示一个document),用Java语言或其他常用语言实现倒排索引建立的详细过程。请在报告中附上代码截图(不要复制源代码,请用截图的方式)、运行结果截图和详细的文字说明。程序要有详细的注释。(40分)
(3). 阅读教材《Introduction to Information Retrieval》第11页Figure 1.6中所描述的倒排记录表(postings lists)的合并算法,使用第(2)题中的倒排索引,用Java语言或其他常用语言实现以下布尔检索:
(a) transfer AND learning
(b) transfer AND learning AND filtering
(c) recommendation AND filtering
(d) recommendation OR filtering
(d) transfer AND NOT (recommendation OR filtering)
(1). 阅读教材《Introduction to Information Retrieval》第一章(特别是第1页),谈谈你对“Information Retrieval”定义或含义的理解,特别是信息检索的主要特点。请对信息检索的主要特点逐个加以详细阐述(至少包含4个特点,每个特点写一段话,突出重点)。(20分)
答:Information Retrieval,即“信息检索”,定义为:信息检索是从大规模非结构化或半结构化数据集合中找出可以满足用户需求的信息资料的过程。
从大规模文本数据中提取信息,对文档进行浏览、过滤或后处理,对信息数据进行分类等等行为,均是通常意义下“信息检索”这一概念的外延。
信息检索的主要特点为:
- 含义广泛:信息检索的含义非常广泛,很多物理行为都能被认为是信息检索。比如使用Web搜索引擎并浏览,查找邮件,查找长文档中的特定此举内容等。信息检索包含很多日常生活中的常见行为
- 规模较大:无论是以Web搜索为代表的大规模级别,还是个人信息检索的小规模级别,信息检索所处理的数据量和信息丰富程度往往大大超过人脑所能处理的极限。Web搜索需要处理存储在数百万台计算机上的数以亿计的文档,而即使只是处理一本书的数据,数据量也是人脑要花费几个小时乃至几天才能处理完的。因此,信息检索面对的数据规模较大。
- 使用高频:无论是生活学习还是办公生产,人们会很高频率地进行信息检索行为。因此我们接触信息检索的时间频率也是非常高的。
- 响应要求迅速:人们在进行信息检索时往往希望快速回复。无论是搜索个人信息还是使用搜索引擎,我们都希望快速得到响应。
- 算法要求高:综合以上四个特点,信息检索要在很多场合经常进行,并且要在很短时间内处理较大规模的数据。因此信息检索对算法要求较高,一般的算法难以同时满足以上四个特点的要求。需要工程师和科学家不断提出新的算法满足用户的信息检索需要。
(2). 阅读教材《Introduction to Information Retrieval》第8页Figure 1.4中所描述的倒排索引(reverted index)建立的详细过程,使用附件“HW1.txt”中的60个文档(每行表示一个document),用Java语言或其他常用语言实现倒排索引建立的详细过程。请在报告中附上代码截图(不要复制源代码,请用截图的方式)、运行结果截图和详细的文字说明。程序要有详细的注释。(30分)
答:
本实验我使用C++语言实现倒排检索。首先打开文件,然后逐行读取document。将每行字符串进行格式化,统一为小写,清除其他符号。然后将每行字符串转化为流,实现通过空格拆分单词。
将单词拆分好之后对拆出来的一个个单词进行统计。如果词典中有这个单词,就在倒排文件的对应倒排列表末尾加上一个文档ID的记录。我采用vector容器来实现链表效果。如果词典中没有这个单词,则扩充词典。
具体的算法内容由注释给出。
fstream HWfile( "e:\\学习\\HW1.txt", ios::in|ios::out );//打开目标文件
if(HWfile)
cout<<"目标文件打开成功"<<endl;
else
cout<<"目标文件打开失败"<<endl;
string line;
int row=0;//行,表示document的ID
int len=0;//表示单词词典长度
vector <string> Lexicon;//单词词典
vector <vector<int>> Invertedfile(150);//倒排文件,内部容器存储单词词典对应单词的倒排列表。注意vector嵌套必须初始化外层容器长度
string word;//当前单词
while(getline(HWfile,line)){//整行读入一行(一个文件)的全部内容
for(int i=0;i<line.size();i++){//格式化文本文档
if(line[i]>='A'&&line[i]<='Z')//大小写转换
line[i]=line[i]+'a'-'A';
if((line[i]<'a'||line[i]>'z')&&line[i]!='-')
line[i]=' ';//清理其他符号并保留连字符
}
stringstream linestream(line);//将读入的每行文本转化为串流,用于切分单词
while(linestream>>word){//从第row行中读入一个单词,判断它是否在词典中
int pos=findword(Lexicon,word);//pos为这个单词在单词词典中的序号
if(pos!=-1)//词典中有这个单词
Invertedfile[pos].push_back(row+1);//在这个单词的倒排文件中添加当前document的序号
else{//词典中没有这个单词
Lexicon.push_back(word);//把单词添加到词典,这个单词在词典中的下标为len
Invertedfile[len].push_back(row+1);//在这个单词的倒排文件中添加当前document的序号
len++;//词典长度扩充
}
}
row++;//文件编号从1开始到60,因此文件编号为row+1
}
for(int i=0;i<len;i++){//输出词典及倒排文件
cout<<Lexicon[i]<<" ";
int len0=Invertedfile[i].size();//获取当前单词倒排列表长度
for(int j=0;j<len0;j++)
cout<<Invertedfile[i][j]<<" ";
cout<<endl;
}
cout<<"词典长度:"<<len<<endl;
以下为创建倒排索引文件的实现结果:
可见词典长度为145,每个词项及其倒排列表全部输出。
为了使倒排文件看上去更为有条理,我们可以将倒排文件的储存顺序进行排序。这里使用折半插入算法对倒排文件进行排序。使得输出的倒排文件是以字典中单词字母顺序排列的。
可见,单词按照字典序排列,看上去更为整齐美观。
同样的,我们可以用另一种思路对倒排文件进行排序。我们可以基于词项的文档频率进行排序,让出现最频繁的词项靠前显示。同样使用折半插入排序算法。
(3). 阅读教材《Introduction to Information Retrieval》第11页Figure 1.6中所描述的倒排记录表(postings lists)的合并算法,使用第(2)题中的倒排索引,用Java语言或其他常用语言实现以下布尔检索:
(a) transfer AND learning
(b) transfer AND learning AND filtering
(c) recommendation AND filtering
(d) recommendation OR filtering
(e) transfer AND NOT (recommendation OR filtering)
请在报告中附上代码截图(不要复制源代码,请用截图的方式)、运行结果截图和详细的文字说明。程序要有详细的注释。(30分)
(a) transfer AND learning
利用教材提供的以下算法思路设计算法进行布尔检索。
如果两个指针指向的文档ID相同,则将相同的文档ID写入答案容器。如果指向的ID不同,则将指向小ID的指针后移一位。注意,每个倒排列表都是天然升序的。因为创建倒排列表时就是从小文档ID开始写入的。
输出检索结果:
可见,利用教材思路编写的程序很好地完成了任务。
(b) transfer AND learning AND filtering
我们可以修改a问题的程序,让它能够处理b问题,修改后代码如下:
三个指针指向的文档ID相同时,则将文档ID写入答案容器。若不同,则把指向较小ID的一个或两个指针后移。
输出检索结果:
可以注意到,用于处理三个词项的“与”查找的代码已经开始显得繁琐。本题与接下来的第五题需要计算并保存临时表达式。
使用保存临时表达式的方法提出另一种实现代码:
输出检索结果:
可见,检索结果一致且正确。
(c) recommendation AND filtering
完成c题的代码仅需修改a题代码即可
输出检索结果:
(d) recommendation OR filtering
处理“或”运算的思路与“与”类似。需要注意的是,“或”运算仍要判断两个指针指向的值是否一致,为了避免重复。此外也可以通过合并优先队列再去除重复的方法,或者直接使用STL提供的取并集方法来完成“或”运算。
输出检索结果:
(e) transfer AND NOT (recommendation OR filtering)
保存临时表达式,先后计算recommendation OR filtering,NOT (recommendation OR filtering)和最终要检索的内容。顺序进行“或”运算,取反运算和“与”运算。
输出检索结果:
全部代码如下:
#include <bits/stdc++.h>
using namespace std;
int findword(vector <string> Lexicon,string word){//找到返回序号,没找到返回-1
int len=Lexicon.size();
int i;
for(i=0;i<len;i++)
if(Lexicon[i]==word)
return i;
return -1;
}
/*
注:本代码文件包含a~e五题分段代码和两段排序。这七段代码均已注释。需要测试哪段代码只需取消注释即可。
*/
int main(){
fstream HWfile( "e:\\学习\\HW1.txt", ios::in|ios::out );//打开目标文件
if(HWfile)
cout<<"目标文件打开成功"<<endl;
else
cout<<"目标文件打开失败"<<endl;
string line;
int row=0;//行,表示document的ID
int len=0;//表示单词词典长度
vector <string> Lexicon;//单词词典
vector <vector<int>> Invertedfile(150);//倒排文件,内部容器存储单词词典对应单词的倒排列表。注意vector嵌套必须初始化外层容器长度
string word;//当前单词
while(getline(HWfile,line)){//整行读入一行(一个文件)的全部内容
for(int i=0;i<line.size();i++){//格式化文本文档
if(line[i]>='A'&&line[i]<='Z')//大小写转换
line[i]=line[i]+'a'-'A';
if((line[i]<'a'||line[i]>'z')&&line[i]!='-')
line[i]=' ';//清理其他符号并保留连字符
}
stringstream linestream(line);//将读入的每行文本转化为串流,用于切分单词
while(linestream>>word){//从第row行中读入一个单词,判断它是否在词典中
int pos=findword(Lexicon,word);//pos为这个单词在单词词典中的序号
if(pos!=-1)//词典中有这个单词
Invertedfile[pos].push_back(row+1);//在这个单词的倒排文件中添加当前document的序号
else{//词典中没有这个单词
Lexicon.push_back(word);//把单词添加到词典,这个单词在词典中的下标为len
Invertedfile[len].push_back(row+1);//在这个单词的倒排文件中添加当前document的序号
len++;//词典长度扩充
}
}
row++;//文件编号从1开始到60,因此文件编号为row+1
}
//使用折半插入排序使倒排文件以词典字母顺序输出
/*
string temp;
for(int i=1; i<len; i++) {
temp=Lexicon[i];
vector <int> tempvec(Invertedfile[i]);//暂存倒排列表
int left=0;
int right=i-1;
while(right>=left) {
int mid=(left+right)/2;
if(Lexicon[mid]>temp)
right=mid-1;
else
left=mid+1;
}
for(int j=i-1; j>right; j--){
Lexicon[j+1]=Lexicon[j];
Invertedfile[j+1]=Invertedfile[j];
}
Lexicon[right+1]=temp;
Invertedfile[right+1]=tempvec;
}
*/
//使用折半插入排序使倒排文件以倒排列表长度顺序输出
/*
string temp;
for(int i=1; i<len; i++) {
temp=Lexicon[i];
vector <int> tempvec(Invertedfile[i]);//暂存倒排列表
int left=0;
int right=i-1;
while(right>=left) {
int mid=(left+right)/2;
if(Invertedfile[mid].size()<Invertedfile[i].size())
right=mid-1;
else
left=mid+1;
}
for(int j=i-1; j>right; j--){
Lexicon[j+1]=Lexicon[j];
Invertedfile[j+1]=Invertedfile[j];
}
Lexicon[right+1]=temp;
Invertedfile[right+1]=tempvec;
}
*/
for(int i=0;i<len;i++){//输出词典及倒排文件
cout<<Lexicon[i]<<" ";
int len0=Invertedfile[i].size();//获取当前单词倒排列表长度
for(int j=0;j<len0;j++)
cout<<Invertedfile[i][j]<<" ";
cout<<endl;
}
cout<<"词典长度:"<<len<<endl;
//↑实现倒排索引
//↓布尔检索
//a~e题分别对应一段(b题为两段)代码,测试某题时取消整段注释即可
//transfer AND learning ******************************************
int pos1=0,pos2=0;//两个指针
int loc1,loc2;//transfer和learning在词典中的位置
vector <int> answer;//存储符合条件的文档ID
loc1=findword(Lexicon,"transfer");
loc2=findword(Lexicon,"learning");
cout<<"检索transfer AND learning:"<<endl;
while(pos1<=Invertedfile[loc1].size()||pos2<=Invertedfile[loc2].size()){//两个指针均未到达倒排文件尾
if(Invertedfile[loc1][pos1]==Invertedfile[loc2][pos2]){//如果找到满足条件的文档ID
answer.push_back(Invertedfile[loc1][pos1]);
pos1++,pos2++;
}
else{
if(Invertedfile[loc1][pos1]<Invertedfile[loc2][pos2])
pos1++;
else
pos2++;
}
}
cout<<"检索结果为:";
for(int i=0;i<answer.size();i++)
cout<<answer[i]<<" ";
cout<<endl;
//transfer AND learning AND filtering ******************************************
/*
int pos1=0,pos2=0,pos3=0;//三个指针
int loc1,loc2,loc3;//transfer,learning和filtering在词典中的位置
vector <int> answer;//存储符合条件的文档ID
loc1=findword(Lexicon,"transfer");
loc2=findword(Lexicon,"learning");
loc3=findword(Lexicon,"filtering");
cout<<"检索transfer AND learning AND filtering:"<<endl;
while(pos1<Invertedfile[loc1].size()-1||pos2<Invertedfile[loc2].size()-1||pos3<Invertedfile[loc3].size()-1){//三个指针均未到达倒排文件尾
if(Invertedfile[loc1][pos1]==Invertedfile[loc2][pos2]&&Invertedfile[loc1][pos1]==Invertedfile[loc3][pos3]){//如果找到满足条件的文档ID
answer.push_back(Invertedfile[loc1][pos1]);
if(pos1<Invertedfile[loc1].size()-1)
pos1++;
if(pos2<Invertedfile[loc2].size()-1)
pos2++;
if(pos3<Invertedfile[loc3].size()-1)
pos3++;
}
else{
if(Invertedfile[loc1][pos1]>=Invertedfile[loc2][pos2]&&Invertedfile[loc1][pos1]>=Invertedfile[loc3][pos3]){//如果指针1对应的文档ID是最大的
if(pos2<Invertedfile[loc2].size()-1&&Invertedfile[loc1][pos1]>Invertedfile[loc2][pos2])
pos2++;
if(pos3<Invertedfile[loc3].size()-1&&Invertedfile[loc1][pos1]>Invertedfile[loc3][pos3])
pos3++;
}
else if(Invertedfile[loc2][pos2]>=Invertedfile[loc1][pos1]&&Invertedfile[loc2][pos2]>=Invertedfile[loc3][pos3]){//如果指针2对应的文档ID是最大的
if(pos1<Invertedfile[loc1].size()-1&&Invertedfile[loc2][pos2]>Invertedfile[loc1][pos1])
pos1++;
if(pos3<Invertedfile[loc3].size()-1&&Invertedfile[loc2][pos2]>Invertedfile[loc3][pos3])
pos3++;
}
else {
if(pos1<Invertedfile[loc1].size()-1&&Invertedfile[loc3][pos3]>Invertedfile[loc1][pos1])
pos1++;
if(pos2<Invertedfile[loc2].size()-1&&Invertedfile[loc3][pos3]>Invertedfile[loc2][pos2])
pos2++;
}
}
}
cout<<"检索结果为:";
for(int i=0;i<answer.size();i++)
cout<<answer[i]<<" ";
cout<<endl;
*/
//transfer AND learning AND filtering实现方法2 ******************************************
/*
int pos1=0,pos2=0;//两个指针
int loc1,loc2;//transfer和learning在词典中的位置
vector <int> temp;//存储符合transfer AND learning的文档ID
loc1=findword(Lexicon,"transfer");
loc2=findword(Lexicon,"learning");
cout<<"检索transfer AND learning AND filtering:"<<endl;
while(pos1<=Invertedfile[loc1].size()||pos2<=Invertedfile[loc2].size()){//两个指针均未到达倒排文件尾
if(Invertedfile[loc1][pos1]==Invertedfile[loc2][pos2]){//如果找到满足条件的文档ID
temp.push_back(Invertedfile[loc1][pos1]);
pos1++,pos2++;
}
else{
if(Invertedfile[loc1][pos1]<Invertedfile[loc2][pos2])
pos1++;
else
pos2++;
}
}
pos1=0,pos2=0;//初始化
loc1=findword(Lexicon,"filtering");
vector <int> answer;//存储最终结果
while(pos1<=Invertedfile[loc1].size()||pos2<=temp.size()){//两个指针均未到达倒排文件尾
if(Invertedfile[loc1][pos1]==temp[pos2]){//如果找到满足条件的文档ID
answer.push_back(Invertedfile[loc1][pos1]);
pos1++,pos2++;
}
else{
if(Invertedfile[loc1][pos1]<temp[pos2])
pos1++;
else
pos2++;
}
}
cout<<"检索结果为:";
for(int i=0;i<answer.size();i++)
cout<<answer[i]<<" ";
cout<<endl;
*/
//recommendation AND filtering ******************************************
/*
int pos1=0,pos2=0;//两个指针
int loc1,loc2;//recommendation和filtering在词典中的位置
vector <int> answer;//存储符合条件的文档ID
loc1=findword(Lexicon,"recommendation");
loc2=findword(Lexicon,"filtering");
cout<<"检索recommendation AND filtering:"<<endl;
while(pos1<=Invertedfile[loc1].size()||pos2<=Invertedfile[loc2].size()){//两个指针均未到达倒排文件尾
if(Invertedfile[loc1][pos1]==Invertedfile[loc2][pos2]){//如果找到满足条件的文档ID
answer.push_back(Invertedfile[loc1][pos1]);
pos1++,pos2++;
}
else{
if(Invertedfile[loc1][pos1]<Invertedfile[loc2][pos2])
pos1++;
else
pos2++;
}
}
cout<<"检索结果为:";
for(int i=0;i<answer.size();i++)
cout<<answer[i]<<" ";
cout<<endl;
*/
//recommendation OR filtering ******************************************
/*
int pos1=0,pos2=0;//两个指针
int loc1,loc2;//recommendation和filtering在词典中的位置
vector <int> answer;//存储符合条件的文档ID
loc1=findword(Lexicon,"recommendation");
loc2=findword(Lexicon,"filtering");
cout<<"检索recommendation OR filtering:"<<endl;
while(pos1<Invertedfile[loc1].size()||pos2<Invertedfile[loc2].size()){//两个指针均未到达倒排文件尾
if(Invertedfile[loc1][pos1]==Invertedfile[loc2][pos2]){//如果找到重合的文档ID
answer.push_back(Invertedfile[loc1][pos1]);
pos1++,pos2++;
}
else{
if(Invertedfile[loc1][pos1]<Invertedfile[loc2][pos2]){
answer.push_back(Invertedfile[loc1][pos1]);
pos1++;
}
else{
answer.push_back(Invertedfile[loc2][pos2]);
pos2++;
}
}
}
cout<<"检索结果为:";
for(int i=0;i<answer.size();i++)
cout<<answer[i]<<" ";
cout<<endl;
*/
//transfer AND NOT (recommendation OR filtering) ******************************************
/*
int pos1=0,pos2=0;//两个指针
int loc1,loc2;//recommendation和filtering在词典中的位置
vector <int> temp;//存储符合括号内表达式的文档ID
vector <int> temp2;
loc1=findword(Lexicon,"recommendation");
loc2=findword(Lexicon,"filtering");
cout<<"transfer AND NOT (recommendation OR filtering):"<<endl;
while(pos1<Invertedfile[loc1].size()||pos2<Invertedfile[loc2].size()){//两个指针均未到达倒排文件尾
if(Invertedfile[loc1][pos1]==Invertedfile[loc2][pos2]){//如果找到重合的文档ID
temp.push_back(Invertedfile[loc1][pos1]);
pos1++,pos2++;
}
else{
if(Invertedfile[loc1][pos1]<Invertedfile[loc2][pos2]){
temp.push_back(Invertedfile[loc1][pos1]);
pos1++;
}
else{
temp.push_back(Invertedfile[loc2][pos2]);
pos2++;
}
}
}
int pos=0;
for(int i=1;i<=60;i++)//执行NOT操作
if(temp[pos]!=i)
temp2.push_back(i);
else
pos++;
pos1=0,pos2=0;//指针初始化
loc1=findword(Lexicon,"transfer");
vector <int> answer;//存储最终结果
while(pos1<Invertedfile[loc1].size()||pos2<temp2.size()){//将transfer与NOT (recommendation OR filtering)执行与操作
if(Invertedfile[loc1][pos1]==temp2[pos2]){//如果找到满足条件的文档ID
answer.push_back(Invertedfile[loc1][pos1]);
pos1++,pos2++;
}
else{
if(Invertedfile[loc1][pos1]<temp2[pos2])
pos1++;
else
pos2++;
}
}
cout<<"检索结果为:";
for(int i=0;i<answer.size();i++)
cout<<answer[i]<<" ";
cout<<endl;
*/
}