一、项目背景
boost库是指一些为C++标准库提供扩展的程序库总称,但是boost网站中并没有为我们提供站内搜索功能,因此我们要想找到某一个类的用法还要一个个去找,因此我们这次的目的就是实现一个搜索引擎功能,提高我们获取知识的效率
二、什么样的搜索引擎
比如百度,谷歌,360等,这些都是大型的搜索引擎仅靠我自己一个人肯定是无法完成的,因此我打算实现一个小型的。先来看大多数搜索引擎的共同点
我们可以发现搜索出来的内容大致由三部分组成,分别是标题,内容和URL链接,因此我们基于boost库实现的搜索引擎的搜索结果也以这三部分构成
三、搜索引擎的宏观图原理
我们只是去写一个服务器,让用户在我们的服务器上通过关键词搜索获得相关资源,这些资源可以放在我们服务器本地,也可以是在其他网站上的资源,我们本次就从boost官网上将boost库下载到我们服务器为例
四、Parse模块
将下载到我们服务器本地boost库进行数据清洗,我们这里仅把boost库中以.html为结尾的文件放到我们服务器本地中,因为数据量太大会导致我们服务器运行变慢,那么为什么要进行数据清洗和,也就是去标签等操作,因为在文件中包含大量的双标签和单标签,与我们用户搜索关键词无关,因此我们只把有用的信息进行保存即可。
1.递将下载好的boost库放到input文件夹中
2. 读取 html中的对数据进行清洗(去标签化)和提取标题,正文和URL
3.将数据清洗后的每个文档我们以’\3’作为分隔符,写入到我们创建的data/raw_html/raw.txt文件中。
为什么要用’\3’作为文档分隔符?因为它是不可显示的控制字符,使用其它的不可显控制字符也可以
// 源文件目录,下面存放的是所有的HTML网页
const std::string src_path = "data/input/";
// 处理完成的数据的存放位置
const std::string output = "data/raw_html/raw.txt";
typedef struct DocInfo{
std::string title; //文档的标题
std::string content; //文档内容
std::string url; //该文档在官网中的url
}DocInfo_t;
//我们自己定的命名规则
//const &:输入
//*: 输出
//&: 输入输出
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list);
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results);
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);
int main()
{
// 1.递归式的把每个html文件名带路径,到存到files_list容器中,方便后期进行一个一个的文件进行读取
std::vector<std::string> files_list;
if (!EnumFile(src_path, &files_list))
{
std::cerr << "enum file name error!" << std::endl;
return 1;
}
// 2.对files_list中文件的内容读取与分析
std::vector<DocInfo_t> results;
if (!ParseHtml(files_list, &results))
{
std::cerr << "parse file name error!" << std::endl;
return 2;
}
// 3.把解析完毕的各个文件内容,写入到output,按照\3作为每个文档的分隔符
if (!SaveHtml(results, output))
{
std::cerr << "Save file name error!" << std::endl;
return 3;
}
return 0;
}
上述三步一句话总结为:把boost库下载到input文件夹下,然后递归式的把以.html结尾的文件进行数据清洗,最后将清洗后的数据写入到/data/raw.txt文件中作为数据源
下面我们来依次实现上述内容
4.1下载boost库源代码
rz -E放到Linux中,然后解压,将解压后的文件放到/data/input/文件下
4.2提取boost库中以.html为结尾的文件
递归式的把每个后缀为.html的文件,以路径+文件名的方式保存到vector容器files_list中,方便我们后续直接从容器中获取文件路径+文件名,从而读取文件内容
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{
namespace fs = boost::filesystem;
fs::path root_path(src_path);
//判断路径是否存在,如果不存在就没有必要向后走了
if(!fs::exists(root_path))
{
std::cerr << src_path << "not exists" << std::endl;
return false;
}
//定义一个空的迭代器,用来进行判断递归结束
fs::recursive_directory_iterator end;
for(fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
//1.判断是否是普通文件
if(!fs::is_regular_file(*iter))
{
continue;//不是break
}
//2.判断文件后缀是否是.html
if(iter->path().extension() != ".html")
{
continue;
}
//std::cout << "debug: " << iter->path().string() << std::endl;
//当前的路径一定是一个合法的,以.html结束的普通网页文件
files_list->push_back(iter->path().string());//将符合要求的文件放到files_list容器中
}
return true;
}
4.2.1 boost库的简单使用
boost库的文件操作函数全在filesystem命名空间下,因此我们赋给一个命名空间fs,方便我们使用,以后我们直接使用fs就行了
namespace fs = boost::filesystem;
4.3数据清洗(去标签化)
遍历files_list容器中的每一个文档,提取该文档的标题,正文,和URL,并将这些数据作为成员保存到一个对象中,然后再将这些依次对象放到保存当前对象的vector容器results中。我们提取后的标题,正文和URL中没有标签存在的
typedef struct DocInfo{
std::string title; //文档的标题
std::string content; //文档内容
std::string url; //该文档在官网中的url
}DocInfo_t;
ParseHtml整体框架
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{
//int cnt = 3;
for(const std::string &file : files_list)
{
//1.读取文件,Read()
std::string result;
if(!ns_util::FileUtil::ReadFile(file, &result))
{
continue;
}
DocInfo_t doc;
//2.解析指定的文件,提取title
if(!ParseTitle(result, &doc.title))
{
continue;
}
//3.解析指定的文件,提取content
if(!ParseContent(result, &doc.content))
{
continue;
}
//4.解析指定的文件路径,构建URL
if(!ParseUrl(file, &doc.url))
{
continue;
}
//完成解析任务,当前文档的相关结果都保存在了doc里面
//results->push_back(doc);//这样写会发生拷贝,效率比较低
results->push_back(std::move(doc));
//ShowDoc(doc);
// cnt--;
// if(cnt == 0)
// {
// break;
// }
}
return true;
}
4.3.1数据清洗的具体实现
##############获取标题
static bool ParseTitle(const std::string &file, std::string *title)
{
std::size_t begin = file.find("<title>");
if(begin == std::string::npos)
{
return false;
}
std::size_t end = file.find("</title>");
if(end == std::string::npos)
{
return false;
}
begin += std::string("<title>").size();
if(begin > end)
{
return false;
}
*title = file.substr(begin, end - begin);
return true;
}
##############获取正文
static bool ParseContent(const std::string &file, std::string *content)
{
//去标签,基于一个简易的状态机
enum status
{
LABLE,
CONTENT
};
enum status s = LABLE;
for(char c : file)
{
switch (s)
{
case LABLE:
if(c == '>') s = CONTENT;
break;
case CONTENT:
if(c == '<') s = LABLE;
else{
//我么不想保留原始文件中的\n,因为我们想用\n作为html解析之后的文本分割符
if(c == '\n') c = ' ';
content->push_back(c);
}
default:
break;
}
}
return true;
}
################获取URL
static bool ParseUrl(const std::string &file_path, std::string *url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html/";
std::string url_tail = file_path.substr(src_path.size());
*url = url_head + url_tail;
return true;
}
4.4将清洗后的数据写入到raw.txt文件中
清洗后的文件内容已经以对象的方式被我们保存到results容器中了,因此此时每个文档去标签后的内容,就为容器中其中一个对象的各成员相加之后得到的字符串
我们将文档中的标题,正文,URL以’\3’作为分隔符进行分隔,文档与文档之间以’\n’作为分隔符进行分隔
将每个文件以二进制的方式写入到raw.txt文件中
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP '\3'
//按照二进制方式进行写入
std::ofstream out(output, std::ios::out | std::ios::binary);
if(!out.is_open())
{
std::cerr << "open " << output << " failed!" <<std::endl;
return false;
}
//进行文件内容的写入
for(auto &item : results)
{
std::string out_string;
out_string = item.title;
out_string += SEP;
out_string += item.content;
out_string += SEP;
out_string += item.url;
out_string += '\n';
out.write(out_string.c_str(), out_string.size());
}
return true;
}
五、正排索引 vs 倒排索引——搜索引擎具体原理
正排索引: 根据文档ID映射文档内容,只要文档ID正确,就一定能找到对应的文档
对目标文档进行分词,目的是为了方便建立倒排索引和查找
重庆今年特别热:重庆/今年/特别/热/特别热/不愧/中国/火炉
重庆是中国的四大火炉之首:重庆/中国/四大火炉/火炉/之首
像"的",“呀"之类的词,几乎在所有的文章都有大量出现,不应该作为搜索的关键词,这类词称之为**“暂停词”**,类似的还有"了”、"吗"等等,对此,不给予考虑。
倒排索引: 根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
模拟一次查找的过程: 输入关键词火炉,通过倒排索引提取对应的文档ID,在正排索引中,通过文档ID获取文档内容,对文档内容中的title、conent(desc)、url文档结果进行摘要,最后构成响应结果
此时有两个细节:
细节一:需要用到权值(weight)对搜索到的文档结果进行排序,而权值根据关键词在标题中是否出现以及在内容中出现的次数决定
细节二:搜索的内容可能不是一个关键词,而是一条语句,语句经过分词处理,被处理成多个关键词,多个关键词可能对应着同一个文档,此时需要进行去重处理
六、建立索引
为什么要建立索引?
因为我们的目标是文档既可以通过文档中关键字进行搜索到,也可以通过文档的文件ID被搜索到,
因此我们要分别建立正排索引和倒排索引
6.1整体框架
自定义类型
struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档内容
std::string url; // 该文档在官网中的url
uint64_t doc_id; // 文档的ID
};
struct InvertedElem
{
uint64_t doc_id;
std::string word;
int weight;
};
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
class Index
{
private:
// 正派索引的数据结构用数组,数组的下标天然是文档的ID
std::vector<DocInfo> forward_index; // 正排索引
// 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string, InvertedList> inverted_index;
}
为什么要定义出上述两种类型?
正排索引是要通过文档id获取对应的文档内容,一个id只会对应一个文档,因此我们可以利用vector数组来存放文档对象DocInfo,以下标作为文档id。
而倒排索引是要通过关键字来获取对应文档,与正排索引不同的是,通过关键字得到的文档可能是多个,因为不同的文档可能会具有相同的关键字,因此我们选择用map容器,通过实体的std::string关键字去映射一个vector数组,数组中存放的是多个文档
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
std::unordered_map<std::string, InvertedList> inverted_index;
并倒排拉链对象中还要保存一个叫权重的值,因为不同的文件包含该关键字的数量不同,包含的多我们认为该文件占有的权重大,权重我们以关键字在标题中出现的次数*10 + 在正文中出现的次数
struct InvertedElem
{
uint64_t doc_id;
std::string word;
int weight;
};
class Index
{
private:
// 正派索引的数据结构用数组,数组的下标天然是文档的ID
std::vector<DocInfo> forward_index; // 正排索引
// 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string, InvertedList> inverted_index;
public:
//获取单例
static Index *GetInstance(){}
// 根据去标签,格式化之后的文档,构建正排和倒排索引 --建立索引
bool BuildIndex(const std::string &input) {}// parse处理完毕的数据交给我
// 根据doc_id找到文档内容 --获得正排
DocInfo *GetForwordIndex(uint16_t doc_id) {}
// 根据关键字string,获得倒排拉链 --获得倒排
InvertedList *GetInvertedList(const std::string &word){}
}
在Index类中我们对外部提供的接口有四个(不包含析构),分别是
1.建立索引(建立正排索引和倒排索引)
2.根据文档关键字获取倒排索引
3.根据文档倒排索引id获取正排索引中的文件
4.获取单例
6.2建立索引
以输入模式或二进制的方式打开数据清理后的文件
bool BuildIndex(const std::string &input) // parse处理完毕的数据交给我
{
std::ifstream in(input, std::ios::in | std::ios::binary);
if (!in.is_open())
{
std::cerr << "sorry, " << input << "open error" << std::endl;
return false;
}
std::string line;
int count = 0;
while (std::getline(in, line))
{
DocInfo *doc = BuildForwordIndex(line);
if (doc == nullptr)
{
std::cerr << "build " << line << " error" << std::endl;
continue;
}
BuildInvertedIndex(*doc);
count++;
if(count % 50 == 0)
{
//std::cout << "当前已经建立的索引文档:" << count <<std::endl;
LOG(NORMAL, "当前的已经建立的索引文档: " + std::to_string(count));
}
}
return true;
}
std::getline(in, line)每次读取一行,也就是读到以\n,而遇到一个\n分隔符就意味着一个文件的结束,因此我们是每读取一个文件就去对它建立正排和倒排索引
6.2.1建立正排索引
namespace ns_util{
class StringUtil
{
public:
static void Split(const std::string &target, std::vector<std::string> *out, const std::string &sep)
{
// boost split
boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);
}
};
}
boost::token_compress_on :意味将连续的分隔符视为一个分隔符
private:
DocInfo *BuildForwordIndex(const std::string &line)
{
// 1.解析line,字符串切分
// line -> 3 string, title, content, url
std::vector<std::string> results;
std::string sep = "\3";
ns_util::StringUtil::Split(line, &results, sep);
if (results.size() != 3)
{
return nullptr;
}
// 2.字符串进行填充到DocInfo
DocInfo doc;
doc.title = results[0];
doc.content = results[1];
doc.url = results[2];
doc.doc_id = forward_index.size(); // 先进行保存id,再插入,对应的id就是当前doc在vector中的下标
// 3.插入到正排索引的vector
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
DocInfo *BuildForwordIndex(const std::string &line)参数line是读取的一行,也就是一个文件
先通过boost库中的split函数将读取的字符串(这个文件)进行拆分,拆分后的字符串自然的就放到了一个vector容器中,然后vector容器中的每个元素依次填充到DocInfo对象中,最后将DocInfo对象放到正排索引的vector容器中
6.2.2建立倒排索引
private:
bool BuildInvertedIndex(const DocInfo &doc)
{
// DocInfo{title, content, url, doc_id}
// word -> 倒排拉链
struct word_cnt
{
int title_cnt;
int content_cnt;
word_cnt() : title_cnt(0), content_cnt(0) {}
};
std::unordered_map<std::string, word_cnt> word_map; // 用来暂存词频的映射表
// 对标题进行分词
std::vector<std::string> title_words;
ns_util::JiebaUtil::CutString(doc.title, &title_words);
// 对标题进行词频统计
for (auto &s : title_words)
{
boost::to_lower(s); //需要统一转化成为小写
word_map[s].title_cnt++; // 如果存在就获取,如果不存在就新建
}
// 对文档内容进行分词
std::vector<std::string> content_words;
ns_util::JiebaUtil::CutString(doc.content, &content_words);
// 对内容进行词频统计
for (auto &s : content_words)
{
boost::to_lower(s);
word_map[s].content_cnt++;
}
#define X 10
#define Y 1
for (auto &word_pair : word_map)
{
InvertedElem item;
item.doc_id = doc.doc_id;
item.word = word_pair.first;
item.weight = X * word_pair.second.title_cnt + Y * word_pair.second.content_cnt; // 相关性
InvertedList &inverted_list = inverted_index[word_pair.first];
inverted_list.push_back(std::move(item));
}
return true;
}
};
建立倒排索引是在基于建立完成正排索引之后的,因为在建立完正排索引之后我们就可以直接使用正排索引的文档id作为倒排索引的文档id,即提高了效率,又保证了同一个文档对应的文档id的唯一性
在词频统计的时候不区分大小写,以便我们在搜索时也可以不区分大小写
1.我们每次读取的是正排索引中的一个文档对象,然后对这个对象的标题和正文进行分词和词频统计,再将词和词频映射到一个临时map容器中
2.遍历这个临时map容器,构建当前文档的文档id,关键词和权重放到倒排拉链元素对象InvertedElem 中
根据关键词找到它在倒排索引inverted_index中的映射值,这个映射值为一个倒排拉链(vector容器),然后我们再把当前关键字对应的元素对象InvertedElem插入到倒排拉链中
注意:索引是索引,拉链是拉链,倒排索引中有拉链,因为它不是的元素一一映射的关系,一个关键字可能对应多个值(多个文档),而在正排索引中,一个文档id只会对应一个文档
我们在对文档进行分词,从而获取关键字的时候,使用了第三方库cppjieba分词
#include "cppjieba/Jieba.hpp"
class JiebaUtil
{
private:
static cppjieba::Jieba jieba;
public:
static void CutString(const std::string &src, std::vector<std::string> *out)
{
jieba.CutForSearch(src, *out);
}
};
const char *const DICT_PATH = "./dict/jieba.dict.utf8";
const char *const HMM_PATH = "./dict/hmm_model.utf8";
const char *const USER_DICT_PATH = "./dict/user.dict.utf8";
const char *const IDF_PATH = "./dict/idf.utf8";
const char *const STOP_WORD_PATH = "./dict/stop_words.utf8";
//静态成员类外初始化
cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
6.3根据文档id获得正排
// 根据doc_id找到文档内容 1.获得正排
DocInfo *GetForwordIndex(uint16_t doc_id)
{
if (doc_id >= forward_index.size())
{
std::cerr << "doc_id out range, error!" << std::endl;
return nullptr;
}
return &forward_index[doc_id];
}
6.4根据关键字获取倒排
// 根据关键字string,获得倒排拉链 2.获得倒排
InvertedList *GetInvertedList(const std::string &word)
{
auto iter = inverted_index.find(word);
if (iter == inverted_index.end())
{
std::cerr << word << " have no InvertedList" << std::endl;
return nullptr;
}
return &(iter->second);
}
在倒排索引中,找是否存在该关键字如果存在,就把他所映射的vector容器返回,vector容器包含所有具有该关键字的文件
6.5单例实现
private:
Index()
{}
Index(const Index &) = delete;
Index &operator=(const Index &) = delete;
static Index *instance;
static std::mutex mtx;
public:
~Index()
{}
static Index *GetInstance()
{
if (instance == nullptr)
{
mtx.lock();
if (instance == nullptr)
{
instance = new Index();
}
mtx.unlock();
}
return instance;
}
因为我们只需服务器上保存一份已经被清洗过且建立完索引的数据就可以,如果每访问一次都要去重新创建一次,就会导致服务器数据冗余,包含多份相同的数据
七、搜索引擎searcher模块
我们想要的搜索结果是关键词不区分大小写,权重越大(与关键字相关性越强)的文件越靠前,显示的结果没有重复文档,且只需要显示该文档中的部分信息即可。
我们在搜索的时候用户搜索关键字,通过关键字获取倒排索引,再通过倒排索引中的对象id获取正排索引中保存的文件,注意:在搜索栏搜索的是关键字信息或一个语句,不是文件索引id号
7.1初始化
namespace ns_searcher
{
class Searcher
{
private:
ns_index::Index *index; // 供系统进行查找的索引
public:
Searcher() {}
~Searcher() {}
public:
void InitSearcher(const std::string &input)
{
// 1.获取或者建立index对象
index = ns_index::Index::GetInstance();
//std::cout << "获取index单例成功..." << std::endl;
LOG(NORMAL, "获取index单例成功...");
// 2.根据index对象建立索引
index->BuildIndex(input);
//std::cout << "建立正排和倒排索引成功..." << std::endl;
LOG(NORMAL, "建立正排和倒排索引成功...");
}
}
7.2查找
查找过程分为四个模块
1.对搜索关键词或关键句进行分词
2.将分完的关键词在倒排索引中进行搜索
3.根据搜索结果的权重,将搜出来的文件进行排序
4.再通过排序后的文件id,在正排索引中获取对应的文件内容,返回给用户
// query:搜索关键字
// json_string:返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{
// 1.[分词]:对我们的query进行按照searcher的要求进行分词
std::vector<std::string> words;
ns_util::JiebaUtil::CutString(query, &words);
// 2.[触发]:就是根据分词的各个"词", 进行index查找
ns_index::InvertedList inverted_list_all; // 内部InverterElem
for (std::string word : words)
{
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if (inverted_list == nullptr)
{
continue;
}
inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
}
// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
std::sort(inverted_list_all.begin(), inverted_list_all.end(),
[](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
{
return e1.weight > e2.weight;
});
// 4.[构建]:根据查找出来的结果,构建json串--jsoncpp
Json::Value root;
for (auto &item : inverted_list_all)
{
ns_index::DocInfo *doc = index->GetForwordIndex(item.doc_id);
if (doc == nullptr)
{
continue;
}
Json::Value elem;
elem["title"] = doc->title;
elem["desc"] = GetDesc(doc->content, item.word);
elem["url"] = doc->url;
elem["id"] = (int)item.doc_id;
elem["weight"] = item.weight;//int -> string
root.append(elem);
}
Json::StyledWriter writer;
//Json::FastWriter writer;
*json_string = writer.write(root);
}
inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
将所有关键词对应的倒多个排拉链拉链都汇总插入到同一个倒排拉链中
std::sort(inverted_list_all.begin(), inverted_list_all.end(),
[](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
{
return e1.weight > e2.weight;
});
对汇总后的倒排拉链,按照权重将文件进行排序
7.3优化
1.呈现给用户的信息不是把该文件的所有内容都呈现,而是只呈现部分关键信息
因此我们选择在找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin考试),往后找100字节(如果没有,到end就可以),最后截取出这部分内容
//获取摘要
std::string GetDesc(const std::string &html_content, const std::string &word)
{
//找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin考试),往后找100字节(如果没有,到end就可以的)
//截取出这部分内容
const int prev_step = 50;
const int next_step = 100;
//1.找到首次出现
//find去找是有坑的
//std::size_t pos = html_content.find(word);
// if(pos == std::string::npos)
// {
// return "None";//这种情况是不可能存在的,因为一定会找到
// }
//使用忽略大小写查找
auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(), [](int x, int y){
return std::tolower(x) == std::tolower(y);
});
if(iter == html_content.end())
{
return "None1";
}
int pos = std::distance(html_content.begin(), iter);//获取html_content.begin()和iter之间的距离
//2.获取start,end
int start = 0;
int end = html_content.size() - 1;
//如果之前有50+字符,就更新开始位置
if(pos - prev_step > start) start = pos - prev_step;//否则start值仍然为0
if(pos + next_step < end) end = pos + next_step;
//3.截取子串,return
if(start >= end) return "None2";
return html_content.substr(start, end - start);
}
标准库中的search函数
查找条件
// using predicate comparison:
bool mypredicate (int i, int j) {
return (i==j);
int needle2[] = {20,30,50};
it = std::search (haystack.begin(), haystack.end(), needle2, needle2+3, mypredicate);
if (it!=haystack.end())
std::cout << "needle2 found at position " << (it-haystack.begin()) << '\n';
else
std::cout << "needle2 not found\n";
}
标准库中的distance函数
计算两个迭代器之间的距离
2.因为同一个关键句中可能包含多个关键词信息,而这多个关键词就会映射多个倒排索引,那么在这些索引对应的倒排拉链中的文件很有可能有一些是相同的,因此我们还要对搜索出来的文件进行去重操作
通过map容器去重
struct InvertedElemPrint{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint():doc_id(0), weight(0){}
};
// query:搜索关键字
// json_string:返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{
// 1.[分词]:对我们的query进行按照searcher的要求进行分词
std::vector<std::string> words;
ns_util::JiebaUtil::CutString(query, &words);
// 2.[触发]:就是根据分词的各个"词", 进行index查找
//ns_index::InvertedList inverted_list_all; // 内部InverterElem
std::vector<InvertedElemPrint> inverted_list_all;
std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;
for (std::string word : words)
{
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if (inverted_list == nullptr)
{
continue;
}
//inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
for(const auto &elem : *inverted_list){
auto &item = tokens_map[elem.doc_id]; //[]:如果存在直接获取,如果不存在新建
//item一定是doc_id相同的print节点
item.doc_id = elem.doc_id;
item.weight += elem.weight;
item.words.push_back(elem.word);
}
}
for(const auto &item : tokens_map){
inverted_list_all.push_back(std::move(item.second));
}
// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
// std::sort(inverted_list_all.begin(), inverted_list_all.end(),
// [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
// {
// return e1.weight > e2.weight;
// });
std::sort(inverted_list_all.begin(), inverted_list_all.end(),
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2){
return e1.weight > e2.weight;
});
// 4.[构建]:根据查找出来的结果,构建json串--jsoncpp
Json::Value root;
for (auto &item : inverted_list_all)
{
ns_index::DocInfo *doc = index->GetForwordIndex(item.doc_id);
if (doc == nullptr)
{
continue;
}
Json::Value elem;
elem["title"] = doc->title;
elem["desc"] = GetDesc(doc->content, item.words[0]);
elem["url"] = doc->url;
elem["id"] = (int)item.doc_id;
elem["weight"] = item.weight;//int -> string
root.append(elem);
}
Json::StyledWriter writer;
//Json::FastWriter writer;
*json_string = writer.write(root);
}
八、http_server模块
利用第三方网络库
#include "searcher.hpp"
#include "cpp-httplib/httplib.h"
const std::string input = "data/raw_html/raw.txt";
const std::string root_path = "./wwwroot";
int main()
{
ns_searcher::Searcher search;
search.InitSearcher(input);
httplib::Server svr;
svr.set_base_dir(root_path.c_str());
svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &rsp){
if(!req.has_param("word"))
{
rsp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
return;
}
std::string word = req.get_param_value("word");
//std::cout << "用户正在搜索:" << word <<std::endl;
LOG(NORMAL, "用户搜索的: " + word);
std::string json_string;
search.Search(word, &json_string);
rsp.set_content(json_string, "application/json");
});
LOG(NORMAL, "服务器启动成功...");
svr.listen("0.0.0.0",8080 );
return 0;
}
九、日志
#pragma once
#include <iostream>
#include <string>
#include <ctime>
#define NORMAL 1
#define WARNING 2
#define DEBUG 3
#define FATAL 4
#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)
void log(std::string level, std::string message, std::string file, int line)
{
std::cout << "[" << level << "]" << "[" << time(nullptr) << "]" << "[" << message << "]" << "[" << file << " : " << line << "]" << std::endl;
}
十、部署服务到 linux 上
nohup命令 ,英文全称no hang up (不挂起),用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行。nohup命令,在默认情况下(非重定向时),会输出一个名叫nohup.out的文件到当前目录下,如果当前目录的nohup out文件不可写,输出重定向到$HOME/nohup.out文件中。
使用命令:nohup ./http_server > log/log.txt 2>&1 &将服务部署到linux上
2>&1 解释:将标准错误2重定向到标准输出1,标准输出1再重定向输入到 log/log.txt 文件中
最后一个&解释:表示后台运行文章来源:https://www.toymoban.com/news/detail-839908.html
部署成功后,如果需要关闭服务,则使用kill -9 命令,根据进程pid结束进程即可。文章来源地址https://www.toymoban.com/news/detail-839908.html
到了这里,关于基于boost库的搜索引擎项目的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!