基于boost库的搜索引擎项目

文章目录

  • 一、项目背景
  • 二、什么样的搜索引擎
  • 三、搜索引擎的宏观图原理
  • 四、Parse模块
    • 4.1下载boost库源代码
    • 4.2提取boost库中以.html为结尾的文件
      • 4.2.1 boost库的简单使用
    • 4.3数据清洗(去标签化)
      • 4.3.1数据清洗的具体实现
    • 4.4将清洗后的数据写入到raw.txt文件中
  • 五、正排索引 vs 倒排索引——搜索引擎具体原理
  • 六、建立索引
    • 6.1整体框架
    • 6.2建立索引
      • 6.2.1建立正排索引
      • 6.2.2建立倒排索引
    • 6.3根据文档id获得正排
    • 6.4根据关键字获取倒排
    • 6.5单例实现
  • 七、搜索引擎searcher模块
    • 7.1初始化
    • 7.2查找
    • 7.3优化
  • 八、http_server模块
  • 九、日志
  • 十、部署服务到 linux 上

一、项目背景

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.判断文件后缀是否是.htmlif(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.解析指定的文件,提取titleif(!ParseTitle(result, &doc.title)){continue;}//3.解析指定的文件,提取contentif(!ParseContent(result, &doc.content)){continue;}//4.解析指定的文件路径,构建URLif(!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;     // 该文档在官网中的urluint64_t doc_id;     // 文档的ID};struct InvertedElem
{uint64_t doc_id;std::string word;int weight;
};// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;class Index
{
private:// 正派索引的数据结构用数组,数组的下标天然是文档的IDstd::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:// 正派索引的数据结构用数组,数组的下标天然是文档的IDstd::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 splitboost::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, urlstd::vector<std::string> results;std::string sep = "\3";ns_util::StringUtil::Split(line, &results, sep);if (results.size() != 3){return nullptr;}// 2.字符串进行填充到DocInfoDocInfo doc;doc.title = results[0];doc.content = results[1];doc.url = results[2];doc.doc_id = forward_index.size(); // 先进行保存id,再插入,对应的id就是当前doc在vector中的下标// 3.插入到正排索引的vectorforward_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 1for (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; // 内部InverterElemfor (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串--jsoncppJson::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 -> stringroot.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,endint start = 0;int end = html_content.size() - 1;//如果之前有50+字符,就更新开始位置if(pos - prev_step > start) start = pos - prev_step;//否则start值仍然为0if(pos + next_step < end) end = pos + next_step;//3.截取子串,returnif(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';
elsestd::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; // 内部InverterElemstd::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串--jsoncppJson::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 -> stringroot.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 文件中
最后一个&解释:表示后台运行

部署成功后,如果需要关闭服务,则使用kill -9 命令,根据进程pid结束进程即可。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/538505.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

阿里云服务器租用费用一个月多少钱?5元1个月

阿里云服务器一个月多少钱&#xff1f;最便宜5元1个月。阿里云轻量应用服务器2核2G3M配置61元一年&#xff0c;折合5元一个月&#xff0c;2核4G服务器30元3个月&#xff0c;2核2G3M带宽服务器99元12个月&#xff0c;轻量应用服务器2核4G4M带宽165元12个月&#xff0c;4核16G服务…

了解关键字

关键字 定义&#xff1a;被Java语言赋予了特殊含义&#xff0c;用做专门用途的字符串&#xff08;或单词&#xff09; HelloWorld案例中&#xff0c;出现的关键字有 class、public 、 static 、 void 等&#xff0c;这些单词已经被Java定义好了。 特点&#xff1a;全部关键字都…

运放的基础知识

运算放大器&#xff08;Operational Amplifier&#xff0c;简称运放&#xff09;是一种直流耦合、差模&#xff08;差动模式&#xff09;输入的高增益电压放大器&#xff0c;通常具有单端输出。它能产生一个相对于输入端电势差大数十万倍的输出电势&#xff08;对地而言&#x…

JVM 相关知识点记录

文章目录 前言哪些内存需要回收方法区的垃圾回收垃圾收集算法垃圾收集器年轻代进入老年代条件内存担保机制FullGC 触发时机GC日志解析日志参数 前言 JVM包含内容&#xff1a; 类装载子系统(Class Load SubSystem)运行时数据区(Run-Time Data Areas) 堆栈 局部变量表操作数栈动…

面向对象(精髓)变继承关系为组和关系(_Decorator模式)

在软件开发中&#xff0c;设计模式是解决常见问题的可重用解决方案。在面向对象编程中&#xff0c;继承和组合是两种常用的代码复用方式。然而&#xff0c;随着软件需求的不断变化&#xff0c;我们需要更灵活的设计方式来应对不断变化的需求。在本文中&#xff0c;我们将讨论从…

如何选购自助咖啡机?这三点是关键!

在竞争激烈的咖啡市场中&#xff0c;既有知名咖啡连锁品牌&#xff0c;也有众多咖啡馆和小型连锁店&#xff0c;无人智能饮品机要想在激烈的竞争中脱颖而出&#xff0c;发挥自身优势&#xff1a;快速便捷是关键&#xff0c;同时好的口感才能留客&#xff0c;因此饮品机的选购成…

K8s — PVC|PV Terminating State

在本文中&#xff0c;我们将讨论PV和PVC一直Terminating的状态。 何时会Terminting? 在以下情况下&#xff0c;资源将处于Terminating状态。 在删除Bounded 状态的PVC之前&#xff0c;删除了对应的PV&#xff0c;PV在删除后是Terminting状态。删除PVC时&#xff0c;仍有引用…

考研数学——高数:高斯公式

助记: 关于积分时什么时候可以将变量整体代入积分式的问题&#xff1a;在积分过程中&#xff0c;如果某一整体恒为常量&#xff0c;则可以直接替换为定值&#xff0c;常见于对线或面的积分。 而在这题&#xff0c;用高斯公式之前是面积分&#xff0c;如果有这个整体出现的话是…

C++中的STL-string类

文章目录 一、为什么学习string类&#xff1f;1.1 C语言中的字符串 二、准库中的string类2.2 string类2.3 string类的常用接口说明2.4 string类对象的容量操作2.5 string类对象的访问及遍历操作2.5 string类对象的修改操作2.7 string类非成员函数2.8 模拟实现string 一、为什么…

【UE5】非持枪状态蹲姿移动的动画混合空间

项目资源文末百度网盘自取 在BlendSpace文件夹中单击右键选择动画(Animation)中的混合空间(Blend Space) &#xff0c;选择SK_Female_Skeleton&#xff0c;命名为BS_NormalCrouch 打开BS_NormalCrouch 水平轴表示角色的方向&#xff0c;命名为Direction&#xff0c;方向的最…

2024Python二级

1. 2. 前序遍历首先访问根节点再访问左子树和右子树 3. 4. sub不属于保留字 5. 6. 7. 8. continue是再重新开始进行循环&#xff0c;不是题目中所规定字母的话就对它进行输出 9. Python没有主函数的说法 10. 未转化为数据所要求的形式&#xff0c;应首先考虑eval 11. l…

Jmeter —— jmeter中元件的执行顺序作用域

元件的执行顺序&#xff1a; 配置元件-->前置处理程序-->定时器-->取样器-->后置处理程序&#xff08;除非Sampler 得 到的返回结果为空&#xff09;-->断言&#xff08;除非Sampler 得到的返回结果为空&#xff09;-->监听器 &#xff08;除非Sampler 得到…