这几天在思考如何对项目做出扩展,当一个网站访问量上升之后随之而来的便是用户的大量交流,根据现在主流的交流方式来看,一般都是一个用户先进行发帖,然后其他用户在下面对之评论,评论系统暂且搁置一边不谈,现在有一个问题就是当帖子数量越来越多,如何快速找到与关键词相对应的帖子,使用关键字在数据库中遍历不太现实,但其实仔细想想搜索引擎不就是做这个的吗,于是就想着实现一个简单的搜索引擎。
要实现一个简单的搜索引擎,必要的相关知识一定是必不可少的,简单来说,第一步先要对已有的资源进行整理,根据整理出来的结果构建索引,搜索模块根据给定关键词查找索引给出排序结果
对已有资源进行整理
搜索引擎使用的前提就是有大量的html文档,需要搜索引擎来做出某种处理来给出正确的结果,所以搜索引擎内部就需要对web杂乱的资源进行管理,以此来完成后面的工作,我们现在假设某一个html目录内有几千条html页面,我们需要对这些页面依次遍历进行统计,将页面中的标题,正文以及html对应在web中的url以某种格式保存下来,这里是保存在一个文件中。
C++提供的库中还不足以让我们拥有遍历整个目录这个功能,所以这里需要借助boost库。
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <boost/filesystem/path.hpp>
#include <boost/filesystem/operations.hpp>
const std::string g_input_path = "../data/input"; // 待解析的html文档都在这个目录下
const std::string g_output_path = "../data/output/pase_file"; //将解析结果放在这个文件中
struct docInfo {
std::string title;
std::string url;
std::string content;
};
bool enumFile(const std::string& input_path, std::vector<std::string>& file_list) {
namespace fs = boost::filesystem;
fs::path root_path(input_path);
if(!fs::exists(root_path)) {
std::cout << "input err " << __FILE__ << ":" << __LINE__ << std::endl;
return false;
}
// boost递归遍历目录,借助迭代器
fs::recursive_directory_iterator end_iter;
for(fs::recursive_directory_iterator iter(root_path); iter != end_iter; ++iter) {
if(!fs::is_regular_file(*iter)) {
continue; // 过滤掉目录
}
if(iter->path().extension() != ".html") {
continue;
}
file_list.push_back(iter->path().string());
}
return true;
}
int main()
{
std::vector<std::string> file_list; // 将所有出现过的页面路径都放在这个vector中
if(enumFile(g_input_path, file_list)) {
std::cout << "enmu file err " << __FILE__ << ":" << __LINE__ << std::endl;
return 1;
}
std::ofstream out_file(g_output_path.c_str());
if(!out_file.is_open()) {
std::cout << "out file err " << __FILE__ << ":" << __LINE__ << std::endl;
return -1;
}
// 依次处理每个html页面
for(const auto& file_path : file_list) {
struct docInfo info;
if(parseFile(file_path, info)) {
std::cout << "pase file err" << std::endl;
continue;
}
writeOutPut(info, out_file);
}
out_file.close();
return 0;
}
解析完毕之后在最终生成的配置文件中每一行就对应一个html文档
构建索引
-
认识倒排索引
- 在普通情况下的索引都是会根据文章的标题或者序号给出文章正文的内容,这种通常情况下的索引我们叫做正排索引
- 倒排索引就是在给出关键词的情况下找出关键词所出现的文章标题或者序号。
-
搜索引擎的工作流程
- 先将用户输入词或句进行拆分(这里使用结巴分词)
- 根据拆分词结合权重在倒排索引中查找,根据返回id找到正排索引
- 根据正排索引将搜索结果进行界面包装进行返回
-
构建索引
struct docInfo {
uint64_t doc_id;
std::string title;
std::string content;
std::string url;
};
struct Weight {
uint64_t doc_id;
int weight; // 权重 为排序做准备
std::string key;
};
std::vector<docInfo> forward_index; //正排索引容器
std::unordered_map<std::string, std::vector<Weight>> inverted_index; // 倒排容器
bool buildIndex(std::string& file_path) {
// ... 判断文件相关
// 读取配置文件 遍历每个解析过的文档
std::string line;
while(std::getline(file, line)) {
// 构造正排索引
const docInfo *doc_info = buildForward(line);
// 更新倒排索引
buildInverted(*doc_info);
}
}
const docInfo* buildForward(std::string html) {
std::vector<std::string> tokens;
// ...使用boost对读取的配置文件进行切分
docInfo doc_info;
doc_info.doc_id = forward_index.size();
doc_info.title = tokens[0];
doc_info.content = tokens[1];
doc_info.url = tokens[2];
forward_index.push_back(doc_info);
return &forward_index.back();
}
void buildInverted(const docInfo& doc_info) {
// 对doc_info中的标题和正文进行分词
std::vector<std::string> title_tokens;
cutWord(doc_info.title, title_tokens);
std::vector<std::string> content_tokens;
curWord(doc_info.content, content_tokens);
// 对分词结果进行词频统计
struct wordCount {
int title_count;
int content_count;
};
std::unordered_map<std::string, wordCount> word_count;
for(std::string word : title_tokens) {
boost::to_lower(word); // 全部转换成小写
++word_count[word].title_count;
}
for(std::string word : content_tokens) {
boost::to_lower(word);
++word_count[word].content_count;
}
// 遍历结果 更新倒排索引
for(const auto& word_pair : word_count) {
Weight weight;
weight.doc_id = doc_info.doc_id;
weight.weight = 10 * word_pair.second.title_count + word_pair.second.content_count;
weight.key = word_pair.first;
auto& inverted_list = inverted_index[word_pair.first];
inverted_index.push_back(weight);
}
}
- 索引已经正常构造完毕,剩下的就是将用户的输入拆分进行搜索
bool Search(const std::string& input_query, std::string& result) {
// 分词 对查询词进行分词
std::vector<std::string> tokens;
curWord(input_query, tokens);
// 触发 对分词结果进行查询
std::vector<Weight> all_tokens_result;
for(std::string word : tokens) {
boost::to_lower(word);
auto *inverted_list = index->getInvertedList(word); // 此处返回的是指向倒排链表的指针
if(inverted_list == nullptr) {
continue;
}
all_tokens_result.insert(all_tokens_result.end(),\
inverted_list.begin(), inverted_list.end());
}
// 排序 对结果进行一定的排序
std::sort(all_tokens_result.begin(), all_tokens_result.end(),
[](const Weight& w1, const Weight& w2){
return w1.weight > w2.weight;
});
// 构造结果
// ...
}
至此全部结构都已经完成, 点我获取源码