一.项目需求
针对boost网站没有搜索导航功能,为boost网站文档的查找提供搜索功能
二.正排索引和倒排索引
正排索引类似于书的目录,我们可以根据页数查找到对应的内容 倒排索引和正排索引是相反的概念,我们可以根据文档内容查询到这部分内容在哪些文件中出现,从而找到对应的文件
三.项目模块
1.预处理模块
将离线的所有的html文档组织成为一个行文本文件 具体流程: 利用boost提供的filesystem枚举出所有html文档的路径,方便后续打开; 读取每个文档的内容,解析得到标题、url、正文,将每个文档的标题、url、正文组织为一行数据,将所有文档解析后得到的数据都写入文件,方便后续进行处理。
2.索引模块
解析预处理模块得到的行文件,构建正排索引和倒排索引,并提供查正排和查倒排接口供外界使用 具体流程: 1.设计正排索引和倒排索引对应的数据结构 正排索引通过数组存储,下标对应文档id,数组中存储的是文档信息结构体DocInfo,这个文档信息结构体的内容有文档的id、标题、在线的url、内容; 倒排索引通过哈希表存储,key是文档中的词,value是这个词出现的所有文档集合,这个文档集合我们称之为倒排拉链,倒排拉链用一个数组表示,这个数组中存储着文档中的词对应的文档信息结构体Weight,Weight结构体中包含文档id,单词对应的权重、单词的内容三部分.
struct DocInfo{
int64_t docId;
string title;
string url;
string content;
};
struct Weight{
int64_t docId;
int weight;
string word;
};
typedef vector<Weight> InvertedList;
vector<DocInfo> forwardIndex;
unordered_map<string, InvertedList> invertedIndex;
2.按行读取预处理得到的行文本文件,将每行数据进行解析构建正排索引个倒排索引
- 构建正排索引
对每行数据按照不可见字符’\3’进行分割,得到文档的标题、url、正文,组织一个DocInfo对象,将DocInfo对象尾插到正排索引数组中,建立数组下标和DocInfo信息的一种类似于哈希的映射关系。
DocInfo* Index::buildForward(const string& line){
vector<string> tokens;
common::Util::split(line,"\3",&tokens);
if(tokens.size() != 3){
return nullptr;
}
DocInfo docInfo;
docInfo.docId=forwardIndex.size();
docInfo.title=tokens[0];
docInfo.url=tokens[1];
docInfo.content=tokens[2];
forwardIndex.push_back(std::move(docInfo));
return &forwardIndex.back();
}
- 构建倒排索引
对文档信息中的标题进行分词,使用哈希表统计分词后的结果在标题中出现的次数; 对文档信息中的正文进行分词,使用哈希表统计分词后的结果在正文中出现的次数; 分词后的单词在哈希表中对应的频次信息采用一个结构体表示,这个结构体有两个成员:标题中出现的次数和正文中出现的次数。 遍历存储频次信息的哈希表,拿到每一个单词对应的频次信息,计算对应的权重,权重等于单词在标题中出现的次数*10+在正文中出现的次数,组织一个Weight结构体;获取倒排索引这个哈希表中该单词对应的value的引用,将也就是倒排拉链,将Weight结构体添加到该单词出现的文档信息集合(倒排拉链)中;当哈希表遍历完毕之后,倒排索引构建完成。
void Index::buildInverted(const DocInfo& docInfo){
struct WordCnt{
int titleCnt;
int contentCnt;
WordCnt() : titleCnt(0), contentCnt(0){}
};
unordered_map<string,WordCnt> wordCntMap;
vector<string> titleTokens;
cutWord(docInfo.title,&titleTokens);
for(string word:titleTokens){
boost::to_lower(word);
++wordCntMap[word].titleCnt;
}
vector<string> contentTokens;
cutWord(docInfo.content,&contentTokens);
for(string word:contentTokens){
boost::to_lower(word);
++wordCntMap[word].contentCnt;
}
for(auto wordPair : wordCntMap){
Weight weight;
weight.docId=docInfo.docId;
weight.weight=wordPair.second.titleCnt * 10 + wordPair.second.contentCnt;
weight.word=wordPair.first;
InvertedList& invertedList = invertedIndex[wordPair.first];
invertedList.push_back(weight);
}
}
3.实现查正排和查倒排接口 查正排的内部逻辑是根据下标访问vector容器,文档id就是vector的下标; 查倒排的内部逻辑是通过哈希表的key获取value,根据单词获取对应的倒排拉链
3.搜索模块
根据查询词查询倒排索引和正排索引,组织一个查询结果 具体流程是: 对查询词进行分词; 对分词结果查询倒排,将所有的倒排拉链合并到一个大的数组中,这个数组中存放查询词出现的所有文档集合; 对文档集合中的所有元素按照权重降序排序; 对排序后的集合查正排,将查询正排得到的结果组织成一个JSON对象,这个对象中有三个成员:标题、url、摘要,将集合中所有元素查正排后的结果整体组织成一个JSON格式的结果;
bool Searcher::search(const std::string& query, std::string* output){
vector<string> tokens;
index->cutWord(query,&tokens);
vector<Weight> allTokens;
for(string word : tokens){
boost::to_lower(word);
const auto* invertedList = index->getInverted(word);
if(invertedList == nullptr){
continue;
}
allTokens.insert(allTokens.end(),invertedList->begin(),invertedList->end());
}
std::sort(allTokens.begin(),allTokens.end(),
[](const Weight& w1, const Weight& w2){
return w1.weight > w2.weight;
});
Json::Value results;
for(const auto& weight : allTokens){
const auto* docInfo = index->getDocInfo(weight.docId);
Json::Value result;
result["title"]=docInfo->title;
result["url"]=docInfo->url;
result["desc"]=generateDesc(docInfo->content,weight.word);
results.append(result);
}
Json::FastWriter writer;
*output=writer.write(results);
return true;
}
4.服务器模块
搭建http服务器处理来自浏览器的查询请求,调用搜索模块的代码得到查询结果,将查询结果组织成为一个静态网页,这个网页允许用户可以点击跳转到相关的网页,并显示网页的标题和网站内容的相关摘要。
四.技术点
正排索引和倒排索引; C++11移动语义; IO多路复用; 线程池;
五.项目源码
https://gitee.com/xigongxiaoche/project/tree/master/boostSearch
|