市建设与管理局网站,中国互联网电视app下载安装,做分销商城网站的,集团网银目录
前言
一、项目的相关背景
1. 什么是Boost库
2. 什么是搜索引擎
3. 为什么要做Boost搜索引擎
二、搜索引擎的宏观原理
三、搜索引擎技术栈和项目环境
四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理
1. 正排索引#xff08;forword index#xff09;
2. 倒…
目录
前言
一、项目的相关背景
1. 什么是Boost库
2. 什么是搜索引擎
3. 为什么要做Boost搜索引擎
二、搜索引擎的宏观原理
三、搜索引擎技术栈和项目环境
四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理
1. 正排索引forword index
2. 倒排索引inverted index
五、编写数据去标签和数据清洗模块 Parser
1. 数据准备
2. 编写parser模块
1. 基本结构设计
2. 细节实现
六、编写建立索引的模块 Index
1. 节点设计
2. 基本结构设计
2. 获取正排索引GetForwardIndex
2. 获取倒排索引GetInvertedList
3. 建立索引BuildIndex
5. 构建正排索引BuildForwardIndex
6. 倒排索引的原理介绍
7. cppjieba分词工具的安装和使用介绍
8. 引入cppjieba到项目中
9. 构建倒排索引BuildInvertedIndex
七、编写搜索引擎模块 Searcher
1. 基本结构
2. 初始化服务InitSearcher
1. Index模块的单例设计
2. 编写InitSearcher
3. 提供服务Search
1. 对用户关键字进行分词
2. 触发分词进行索引查找
3. 按文档权重进行降序排序
4. 根据排序结果构建json串
八、编写http_server模块
1. 引入cpp-httplib到项目中
2. cpp-httplib的使用介绍
3. 正式编写http_server
九、添加日志到项目中
十、编写前端模块
十一、项目总结 前言 首先这个基于Boost库的搜索引擎项目博主之前就已经做过了但是由于各方面的原因没有记录这个博客拖了很久。这次我想把这个项目好好的总结出来希望看到这个项目博客的小伙伴可以点点关注和收藏。 一、项目的相关背景
1. 什么是Boost库 Boost库是C的准标准库 它提供了很多C没有的功能可以称之为是C的后备力量。早期的开发者多为C标准委员会的成员一些Boost库也被纳入了C11中如哈希、智能指针这里大家可以去百度百科上搜索一看便知。下面是boost的官网 2. 什么是搜索引擎 对于搜索引擎相信大家一定不陌生如百度、360、搜狗等都是我们常用的搜索引擎。但是你想自己实现出一个和百度、360、搜狗一模一样哪怕是类似的搜索引擎是非常非常困难的。我们可以看一下这些搜索引擎在搜索关键字的时候给我们展示了哪些信息 我们可以看到基本上搜索引擎根据我们所给的关键字搜出来的结果展示都是以网页标题、网页内容摘要和跳转的网址组成的。但是它可能还有相应的照片、视频、广告这些我们在设计基于Boost库的搜索引擎项目的时候不考虑这些它们属于扩展内容 3. 为什么要做Boost搜索引擎 刚刚我们看到了boost的官网界面我们可以对比一下cplusplus官网看看有什么区别 可以看到boost库是没有站内搜索框的如果我们可以对boost库做一个站内搜索向cplusplus一样搜索一个关键字就能够跳转到指定的网页并显示出来。那么这个项目还是具有一定意义的。这也就是项目的背景。 其次站内搜索的数据更加垂直数据量其实更小。 二、搜索引擎的宏观原理 刚刚我们介绍完了基于Boost库的搜索引擎的项目背景后相信大家有了一定的了解大致上知道了这个项目是什么意思。但是我们还需要了解一下搜索引擎的宏观原理。接下来以下面的图为例介绍一下其宏观原理。 原理图分析 我们要实现出boost库的站内搜索引擎红色虚线框内就是我们要实现的内容总的分为客户端和服务器详细分析如下 我们从客户端想要获取到大学生的相关信息呈现在网页上的样子就是网页的标题摘要网址首先我们构建的服务器就要有对应的数据存在这些数据从何而来我们可以进行全网的一个爬虫将数据爬到我们的服务器的磁盘上但是我们这个项目是不涉及任何爬虫程序的我们可以直接将boost库对应版本的数据直接解压到我们对应文件里。现在数据已经被我们放到了磁盘中了接下来客户端要访问服务器那么服务器首先要运行起来服务器一旦运行起来它首先要做的工作就是对磁盘中的这些数据进行去标签和数据清洗的动作因为我们从boost库拿的数据其实就是对应文档html网页但是我们需要的只是每个网页的标题网页内容摘要跳转的网址所以才有了去标签和数据清洗只拿我们想要的。这样就可以直接跳过网址跳转到boost库相应文档的位置。服务器完成了去标签和数据清洗之后就需要对这些清洗后的数据建立索引方便客户端快速查找当服务器所以的工作都完成之后客户端就发起http请求通过GET方法上传搜索关键服务器收到了会进行解析通过客户端发来的关键字去检索已经构建好的索引找到了相关的html后就会将逐个的将每个网页的标题、摘要和网址拼接起来构建出一个新的网页响应给客户端至此客户就看到了相应的内容点击网址就可以跳转到boost库相应的文档位置。三、搜索引擎技术栈和项目环境 基于Boost库的搜索引擎项目所涉及的技术栈和项目环境如下 技术栈 C/C/C11、STL、boost库、Jsoncpp、cppjieba、cpp-httplibhtml5、css、js、jQuery、Ajax项目环境 Centos 7 云服务器vim/gcc/g/Makefilevs2019 or vscode 技术栈和项目环境有些你可能不了解没关系下面的代码编写中会有介绍但是基本的技术栈C/C/C11/STL 你是要熟悉的项目环境 云服务器、vim、vs这些你也是需要知道的。 四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理 通过下面两个文档来解释一下正排索引和倒排索引 文档ID文档内容1雷军买了四斤小米2雷军发布了小米手机
1. 正排索引forword index 正排索引就是从文档ID找到文档内容(文档内的关键字) 正排索引是创建倒排索引的基础有了正排索引之后如何构建倒排索引呢 我们要对目标文档进行分词以上面的文档1/2为例我们来进行分词演示 文档1雷军、买、四斤、小米、四斤小米文档2雷军、发布、小米、手机、小米手机进行分词之后就能够方便的建立倒排索引和查找。 我们可以看到在文档1/2中其中的 “了” 子被我们省去了这是因为像了呢吗 athe等都是属于停止词一般我们在分词的时候可以不考虑。那么什么是停止词呢 停止词 它是搜索引擎分词的一项技术停止词就是没有意义的词。如在一篇文章中你可以发现有很多类似于了呢吗 athe等中文或英文中都是停止词因为频繁出现如果我们在进行分词操作的时候如果把这些停止词也算上不仅会建立索引麻烦而且会增加精确搜索的难度。 2. 倒排索引inverted index 刚刚我们说正排索引是创建倒排索引的基础首先是要对文档进行分词操作 倒排索引就是根据文档内容的分词整理不重复的各个关键字对应联系到文档ID的方案 关键词具有唯一性文档ID雷军文档1文档2买文档1四斤文档1小米文档1文档2四斤小米文档1发布文档2手机文档2小米手机文档2模拟一次查找的过程 用户输入小米 --- 去倒排索引中查找关键字“小米” --- 提取出文档ID【1,2】--- 去正排索引中根据文档ID【1,2】找到文档内容 --- 通过 [ 标题 内容摘要 网址 ] 的形式构建响应结果 --- 返回给用户小米相关的网页信息。 五、编写数据去标签和数据清洗模块 Parser 在编写Parser模块的之前我们先将数据准备好去boost官网下载最新版本的库解压到Linux下操作方法如下 1. 数据准备
boost官网https://www.boost.org/ 进入官网后是如下界面 进入之后你可以选择最新版本的下载我之前下载的是1.78.0版本这里我就不下载最新版本了都是可以用的。 点击Download之后我们选择这个进行下载 下载好之后我们先在linux下创建一个名为Boost_Searcher目录以后将会在这个目录下进行各种代码模块的编写以及存放各种数据下面是创建过程 接下来我们将下载好的1.78.0版本的boost库解压到Linux下使用 rz 命令用于文件传输输入rz -E 命令后直接回车找到boost点击打开即可你也可以直接将压缩包拖拽到命令行中 效果如下 此时我们使用 tar xzf boost_1_78_0.tar.gz 进行解压解压好后我们进行查看 可以看到解压好的boost里面有这么所文件但这么多文件并不是我们都需要的我们需要的就是boost_1_78_0/doc/html目录下的html。为什么呢结合下面的图 上面的图就是boost库的操作方法我们可以看到右下角的两个网页的网址他们都是在doc/html目录下的文件都是 .html。我们只要这个就可以了。后期通过地址进行拼接达到跳转就能来到这个网页。 --------------------------------------------------------------------------------------------------------------------------------- 我们进入到Linux下的doc/html目录看看里面有哪些东西 可以看到里面处理html为后缀的文件外还有一些目录但是我们只需要html文件所以我们要进行数据清洗。只拿html文件。 对数据清洗之后拿到的全都是html文件此时还需要对html文件进行去标签处理我们这里随便看一个html文件 : html的标签这个标签对我们进行搜索是没有价值的需要去掉这些标签一般标签都是成对出现的但是也有单独出现的我们也是不需要的。 我们的目标把每个文档都去标签然后写入到同一个文件中每个文档内容不需要任何\n文档和文档之间用 \3 进行区分。 类似XXXXXXXXXXXXXXX\3YYYYYYYYYYYYYYYYYY\3ZZZZZZZZZZZZZZZZZZZZ\3 采用下面的方案 写入文件中一定要考虑下一次在读取的时候也要方便操作! 类似title\3content\3url \n title\3content\3url \n title\3content\3url \n ... 方便我们getline(ifsream, line)直接获取文档的全部内容title\3content\3url 我们了解了大概的情况之后我们来将我们所需要的数据源拷贝到data目录下的intput目录下 最后我们在data目录下的raw_html目录下创建有一个raw.txt文件用来存储干净的数据文档 2. 编写parser模块
1. 基本结构设计 这里我是在vim下进行代码编写的你可以选择vscode但是需要连接一下云服务器与Linux进行同步。 基本框架主要完成的工作如下 将data/input/所有后缀为html的文件筛选出来然后对筛选好的html文件进行解析去标签拆分出标题、内容、网址最后将去标签后的所有html文件的标题、内容、网址按照 \3 作为分割符每个文件再按照 \n 进行区分。写入到data/raw_html/raw.txt下#include iostream
#include vector
#include string//将数据源的路径 和 清理后干净文档的路径 径定义好
const std::string src_path data/input; //数据源的路径
const std::string output data/raw_html/raw.txt; //清理后干净文档的路径//DocInfo --- 文件信息结构体
typedef struct DocInfo
{std::string title; //文档的标题std::string content; //文档的内容std::string url; //该文档在官网当中的url
}DocInfo_t;// const --- 输入
// * --- 输出
// --- 输入输出//把每个html文件名带路径保存到files_list中
bool EnumFile(const std::string src_path, std::vectorstd::string *files_list);//按照files_list读取每个文件的内容并进行解析
bool ParseHtml(const std::vectorstd::string files_list, std::vectorDocInfo_t *results);//把解析完毕的各个文件的内容写入到output
bool SaveHtml(const std::vectorDocInfo_t results, const std::string output);int main()
{std::vectorstd::string files_list;// 第一步递归式的把每个html文件名带路径保存到files_list中方便后期进行一个一个的文件读取if(!EnumFile(src_path, files_list)) //EnumFile--枚举文件{std::cerr enum file name error! std::endl;return 1;}// 第二步按照files_list读取每个文件的内容并进行解析std::vectorDocInfo_t results;if(!ParseHtml(files_list, results))//ParseHtml--解析html{std::cerr parse html error! std::endl;return 2;}// 第三部把解析完毕的各个文件的内容写入到output按照 \3 作为每个文档的分隔符if(!SaveHtml(results, output))//SaveHtml--保存html{std::cerr save html error! std::endl;return 3;}return 0;
}2. 细节实现 主要实现枚举文件、解析html文件、保存html文件三个工作。 这三个工作完成是需要我们使用boost库当中的方法的我们需要安装一下boost的开发库 命令sudo yum install -y boost-devel 下图就是我们接下来编写代码需要用到的boost库当中的filesystem方法。 枚举文件 //在原有的基础上添加这个头文件
#include boost/filesystem.hppbool EnumFile(const std::string src_path, std::vectorstd::string *files_list)
{namespace fs boost::filesystem;fs::path root_path(src_path); // 定义一个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){// 判断指定路径是不是常规文件如果指定路径是目录或图片直接跳过if(!fs::is_regular_file(*iter)){continue;}// 如果满足了是普通文件还需满足是.html结尾的// 如果不满足也是需要跳过的// ---通过iter这个迭代器理解为指针的一个path方法提取出这个路径// ---然后通过extension()函数获取到路径的后缀if(iter-path().extension() ! .html){continue;}//std::cout debug: iter-path().string() std::endl; // 测试代码// 走到这里一定是一个合法的路径以.html结尾的普通网页文件files_list-push_back(iter-path().string()); // 将所有带路径的html保存在files_list中方便后续进行文本分析}return true;
}
代码编写到这里我们就可以进行测试了使用上述代码中注释掉的代码进行测试首先编写Makefile ccg
parser:parser.cc $(cc) -o $ $^ -lboost_system -lboost_filesystem -stdc11
.PHONY:clean
clean:rm -f parser
接下来就可以make然后运行了: 解析html文件 读取刚刚枚举好的文件解析html文件中的title解析html文件中的content解析html文件中的路径构建url这里我们将这读取操作写到一个工具类中包括后续有什么方法也可以写到这个里面方便调用。创建一个util.hpp util.hpp如下
#pragma once
#include iostream
#include string
#include fstream
#include vectornamespace ns_util
{class FileUtil{ public:static bool ReadFile(const std::string file_path, std::string *out){std::ifstream in(file_path, std::ios::in);if(!in.is_open()){std::cerr open file file_path error std::endl;return false;}std::string line;while(std::getline(in, line)) //如何理解getline读取到文件结束呢getline的返回值是一个while(bool), 本质是因为重载了强制类型转化{*out line;}in.close();return true;}};
}解析html文件
bool ParseHtml(const std::vectorstd::string files_list, std::vectorDocInfo_t *results)
{for(const std::string file : files_list){// 1.读取文件Read()std::string result;if(!ns_util::FileUtil::ReadFile(file, result)){continue;}// 2.解析指定的文件提取titleDocInfo_t doc;if(!ParseTitle(result, doc.title)){continue;}// 3.解析指定的文件提取contentif(!ParseContent(result, doc.content)){continue;}// 4.解析指定的文件路径构建urlif(!ParseUrl(file, doc.url)){continue; }// 到这里一定是完成了解析任务当前文档的相关结果都保存在了doc里面results-push_back(std::move(doc)); // 本质会发生拷贝效率肯能会比较低这里我们使用move后的左值变成了右值去调用push_back的右值引用版本}return true;
} 解析html的title 在进行提取title的时候我们可以看看html的代码。它显示标题的时候是以title标题/title构成的我们只需要findtitle就能找到这个标签的左尖括号的位置然后加上title的长度此时就指向了标题的起始位置同理再去找到/title的左尖括号最后截取子串 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;
} 解析html的content 解析内容的时候我们采用一个简易的状态机来完成状态机包括两种状态LABLE(标签)和CONTENT(内容) html的代码中标签都是这样的起始肯定是标签我们追个字符进行遍历判断如果遇到“”表明下一个即将是内容了我们将状态机置为CONTENT接着将内容保存起来如果此时遇到了“”表明到了标签了我们再将状态机置为LABLE不断的循环知道遍历结束 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);}break;default:break;}}return true;
}解析html的url 在编写解析html的url的时候我们需要注意我们自己路径下的html的路径和官网上的路径是有对应关系的 官网URL样例 https://www.boost.org/doc/libs/1_78_0/doc/html/accumulators.html我们下载下来的url样例boost_1_78_0/doc/html/accumulators.html 我们拷贝到我们项目中的样例data/input/accumulators.html //我们把下载下来的boost库 doc/html/* copy data/input/ 此时我们想要从我们的项目中得到和官网一样的网址我们可以这样做 url_head https://www.boost.org/doc/libs/1_78_0/doc/html;//拿官网的部分网址作为头部的url url_tail [data/input(删除)] /accumulators.html - url_tail /accumulators.html;//将我们项目的路径data/input删除后得到/accumulators.html 将url_head url_tail 得到 官网的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());//将data/input截取掉 *url url_head url_tail;//拼接return true;
} 我们不是已经定义好了两个路径嘛源数据路径和清理后干净文档的路径url_head这个比较简单直接复制官网的。url_tail我们可以将传过来的文件路径使用一个substr把data/input截取掉保留剩下的然后和url_head拼接起来。 保存html文件 说明一下分隔符为什么使用‘\3’ : \3在ASSCII码表中是不可以显示的字符我们将title、content、url用\3进行区分不会污染我们的文档当然你也可以使用\4等 bool SaveHtml(const std::vectorDocInfo_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());}out.close();return true;
} 接下来我们做一下测试运行代码后查看我们data/raw_html/raw.txt文件就如下图 至此我们的parser去标签数据清模块就完成了为了大家能够更好的理解下面是一张关系图 六、编写建立索引的模块 Index
1. 节点设计 在构建索引模块时我们要构建出正排索引和倒排索引正排索引是构建倒排索引的基础通过给到的关键字去倒排索引里查找出文档ID再根据文档ID找到对应的文档内容所以在这个index模块中就一定要包含两个节点结构一个是文档信息的节点一个是倒排对应的节点 namespace ns_index
{struct DocInfo //文档信息节点{std::string title; //文档的标题std::string content; //文档对应的去标签后的内容std::string url; //官网文档的urluint64_t doc_id; //文档的ID};struct InvertedElem //倒排对应的节点{uint64_t doc_id; //文档IDstd::string word; //关键字通过关键字可以找到对应的IDint weight; //权重---根据权重对文档进行排序展示};
} 说明一下 在倒排对应的节点之中 有doc_id、word和weight我们可以通过word关键字找到对应的文档ID并且我们有文档的信息节点通过倒排找到的文档ID就能够在文档信息节点中找到对应的文档所有内容这两个节点都有doc_id就像MySQL中外键相当于两张表产生了关联 2. 基本结构设计
1. Index类的基本框架 我们创建一个Index类主要用来构建索引模块但是内部的细节还是比较多的暂时不多赘述索引模块最大的两个部分当然是构建正排索引和构建倒排索引其主要接口如下 namespace ns_index
{struct DocInfo //文档信息节点{std::string title; //文档的标题std::string content; //文档对应的去标签后的内容std::string url; //官网文档的urluint64_t doc_id; //文档的ID};struct InvertedElem //倒排对应的节点{uint64_t doc_id; //文档IDstd::string word; //关键字通过关键字可以找到对应的IDint weight; //权重---根据权重对文档进行排序展示};typedef std::vectorInvertedElem InvertedList;class Index{private://正排索引的数据结构采用数组数组下标就是天然的文档IDstd::vectorDocInfo forward_index; //正排索引//倒排索引一定是一个关键字和一组个InvertedElem对应[关键字和倒排拉链的映射关系]std::unordered_mapstd::string, InvertedList inverted_index;public:Index(){} ~Index(){}public://根据doc_id找到正排索引对应doc_id的文档内容DocInfo* GetForwardIndex(uint64_t doc_id){//...}//根据倒排索引的关键字word获得倒排拉链InvertedList* GetInvertedList(const std::string word){//...}//根据去标签格式化后的文档构建正排和倒排索引 //将数据源的路径data/raw_html/raw.txt传给input即可这个函数用来构建索引bool BuildIndex(const std::string input){}
}
2. 获取正排索引GetForwardIndex GetForwardIndex函数根据正排索引的doc_id找到文档内容 //根据doc_id找到正排索引对应doc_id的文档内容
DocInfo* GetForwardIndex(uint64_t doc_id)
{//如果这个doc_id已经大于正排索引的元素个数则索引失败if(doc_id forward_index.size()){ std::cout doc_id out range, error! std::endl;return nullptr;}return forward_index[doc_id];//否则返回相应doc_id的文档内容
}2. 获取倒排索引GetInvertedList GetInvertedList函数根据倒排索引的关键字word获得倒排拉链和上面类似 //根据倒排索引的关键字word获得倒排拉链
InvertedList* GetInvertedList(const std::string word)
{auto iter inverted_index.find(word);if(iter inverted_index.end()){std::cerr have no InvertedList std::endl;return nullptr;}return (iter-second);
}3. 建立索引BuildIndex BuildIndex函数根据去标签格式化后的文档构建正排和倒排索引 在编写这部分代码时稍微复杂一些我们要构建索引那我们应该是先把处理干净的文档读取上来是按行读取这样就能读到每个html文档按行读上来每个html文档后我们就可以开始构建正排索引和倒排索引此时就要提供两个函数分别为BuildForwardIndex构建正排索引和 BuildInvertedIndex构建倒排索引基本的代码如下 //根据去标签格式化后的文档构建正排和倒排索引
//将数据源的路径data/raw_html/raw.txt传给input即可这个函数用来构建索引
bool BuildIndex(const std::string input)
{//在上面SaveHtml函数中我们是以二进制的方式进行保存的那么读取的时候也要按照二进制的方式读取读取失败给出提示std::ifstream in(input, std::ios::in | std::ios::binary);if(!in.is_open()){std::cerr sory, input open error std::endl;return false;}std::string line;int count 0;while(std::getline(in, line)){DocInfo* doc BuildForwardIndex(line);//构建正排索引if(nullptr doc){std::cerr build line error std::endl;continue;}BuildInvertedIndex(*doc);//有了正排索引才能构建倒排索引count; if(count % 50 0) { std::cout 当前已经建立的索引文档 count 个 std::endl; }}return true;
}5. 构建正排索引BuildForwardIndex BuildForwardIndex构建正排索引 在编写构建正排索引的代码前我们要知道在构建索引的函数中我们是按行读取了每个html文件的每个文件都是这种格式title\3content\3url...构建正排索引就是将DocInfo结构体内的字段进行填充这里我们就需要给一个字符串切分的函数我们写到util.hpp中这里我们又要引入一个新的方法——boost库当中的切分字符串函数split代码如下 #pragma once
#include iostream
#include string
#include fstream
#include vector
#include boost/algorithm/string.hppnamespace ns_util
{class FileUtil{public:static bool ReadFile(const std::string file_path, std::string *out){std::ifstream in(file_path, std::ios::in);if(!in.is_open()){std::cerr open file file_path error std::endl;return false;}std::string line;while(std::getline(in, line)) //如何理解getline读取到文件结束呢getline的返回值是一个while(bool), 本质是因为重载了强制类型转化{*out line;}in.close();return true;}};class StringUtil{public://切分字符串static void Splist(const std::string target, std::vectorstd::string *out, const std::string sep){//boost库中的split函数boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);//第一个参数表示你要将切分的字符串放到哪里//第二个参数表示你要切分的字符串//第三个参数表示分割符是什么不管是多个还是一个//第四个参数它是默认可以不传即切分的时候不压缩不压缩就是保留空格//如字符串为aaaa\3\3bbbb\3\3cccc\3\3d//如果不传第四个参数 结果为aaaa bbbb cccc d//如果传第四个参数为boost::token_compress_on 结果为aaaabbbbccccd//如果传第四个参数为boost::token_compress_off 结果为aaaa bbbb cccc d}};
}构建正排索引的编写 //构建正排索引
DocInfo* BuildForwardIndex(const std::string line)
{// 1.解析line字符串切分// 将line中的内容且分为3段原始为title\3content\3url\3// 切分后title content urlstd::vectorstd::string results;std::string sep \3; //行内分隔符ns_util::StringUtil::Splist(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)); //使用move可以减少拷贝带来的效率降低return forward_index.back();
}
6. 倒排索引的原理介绍 建立倒排的原理我们之前只是单纯的说了一下没有详细的说明如何实现接下来我通过x张图来解释建立倒排索引的原理 总的思路 对title和content进行分词使用cppjieba在分词的时候必然会有某些词在title和content中出现过我们这里还需要做一个处理就是对每个词进行词频统计你可想一下你在搜索某个关键字的时候为什么有些文档排在前面而有些文档排在最后这主要是词和文档的相关性我们这里认为关键字出现在标题中的相关性高一些出现在内容中的低一些当然关于相关性其实是比较复杂的我们这里只考虑这些自定义相关性我们有了词和文档的相关性的认识后就要来自己设计这个相关性我们把出现在title中的词其权重更高在content中其权重低一些如让出现在title中的词的词频x10出现在content中的词的词频x1两者相加的结果称之为该词在整个文档中的权重根据这个权重我们就可以对所有文档进行权重排序进行展示权重高的排在前面展示权重低的排在后面展示伪代码操作演示 如下是我们之前的基本结构代码 //倒排拉链节点
struct InvertedElem{uint64_t doc_id; //文档的IDstd::string word; //关键词int weight; //权重
};//倒排拉链
typedef std::vectorInvertedElem InvertedList;//倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_mapstd::string, InvertedList inverted_index;//文档信息节点
struct DocInfo{std::string title; //文档的标题std::string content; //文档对应的去标签之后的内容std::string url; //官网文档urluint64_t doc_id; //文档的ID
}; 1. 需要对 title content都要先分词 -- 使用jieba分词 title: 吃/葡萄/吃葡萄(title_word) content吃/葡萄/不吐/葡萄皮(content_word)2. 词频统计 统计词频它是包含标题和内容的我们就需要有一个结构体来存储每一篇文档中每个词出现在title和content中的次数伪代码如下 //词频统计的结点
struct word_cnt{title_cnt; //词在标题中出现的次数content_cnt;//词在内容中出现的次数
} 统计这些次数之后我们还需要将词频和关键词进行关联文档中的每个词都要对应一个词频结构体这样我们通过关键字就能找到其对应的词频结构体通过这个结构体就能知道该关键字在文档中的title和content中分别出现了多少次下一步就可以进行权重的计算。这里我们就可以使用数据结构unordered_map来进行存储。伪代码如下 //关键字和词频结构体的映射
unordered_mapstd::string, word_cnt word_map;//范围for进行遍历对title中的词进行词频统计
for(auto word : title_word){word_map[word].title_cnt; //吃1/葡萄1/吃葡萄1
}
//范围for进行遍历对content中的词进行词频统计
for(auto word : content_word){word_map[word].content_cnt; //吃1/葡萄1/不吐1/葡萄皮1
} 3. 自定义相关性 知道了在文档中标题和内容每个词出现的次数接下来就需要我们自己来设计相关性了伪代码如下 //遍历刚才那个unordered_mapstd::string, word_cnt word_map;
for(auto word : word_map){struct InvertedElem elem;//定义一个倒排拉链然后填写相应的字段elem.doc_id 123;elem.word word.first;elem.weight 10*word.second.title_cnt word.second.content_cnt ;//权重计算inverted_index[word.first].push_back(elem);//最后保存到倒排索引的数据结构中
}//倒排索引结构如下
//std::unordered_mapstd::string, InvertedList inverted_index; 至此就是倒排索引比较完善的原理介绍和代码思路。 7. cppjieba分词工具的安装和使用介绍 获取链接https://github.com/yanyiwu/cppjieba 里面有详细的教程 我这里是在GitHub上下载的解压包然后自己解压的可以使用git clone主要因为太慢了就直接下的压缩包就解压好后基本环境的搭建如下 创建一个test目录用来解压好cppjieba效果如下 查看cppjieba-master目录里面包含如下 我们待会儿需要用到的分词工具是在include/cppjieba/jieba.hpp 首先这是别人的写好的一个开源项目里面会有这个测试代码通常是在test目录下 我们来做个分词演示先将这个demo.cpp拷贝到我们的test目录下 打开之后就是一堆错误主要原因是路径不对 首先从上图可以看到头文件的路径就不对我们先来修改一下头文件的路径它本身是要使用cppjieba/Jieba.hpp的我们看一下这个头文件的具体路径
路径是cppjieba-master/include/cppjieba/Jieba.hpp 我们要在test目录下执行这个demo.cpp要引入这个头文件我们不能直接引入需要使用软连接 软连接建立好后并修改demo.hpp的相应路径再将该包的头文件包起来再来查看demo.hpp是否还有错误 我们编译后发现limonp/Logging.hpp这个头文件没有 此时我们还是需要对这个头文件进行软连接我们通过查找发现有这么一个路径 但是里面什么东西都没有这是我在联系项目中出现的问题经过我去GitHub查找一番后发现它在另外一个压缩包里 解压好如下 此时我们找一下limonp/Logging.hpp 拷贝之后如下 此时再打开demo.hpp既没有任何问题了命令行参数的提示不是错误不考虑 7~11行的路径中有个dict目录在我们的test目录下是没有的我还需要软连接 我们将路径都完善之后接下来我们编译运行一下demo.hpp看下效果 可以看出分词效果还是很不错的。那么接下来就要在我们的项目路径中加入cppjieba下的Jieba.hpp操作和上面的类似这里我就不在操作了。直接看结果 上面的操作做完之后就可以在我们的项目中引入头文件来使用cppjieba分词工具啦
8. 引入cppjieba到项目中 将软链接建立好之后我们在util.hpp中编写一个jieba分词的类主要是为了方便后期其他地方需要使用的时候可以直接调用。 我们在util.hpp中创建一个 JiebaUtil的分词工具类首先我们先看一下之前测试过的demo.cpp的代码 util.hpp代码如下 #pragma once
#include iostream
#include string
#include fstream
#include vector
#include boost/algorithm/string.hpp
#include cppjieba/Jieba.hpp//引入头文件确保你建立的没有错误才可以使用
namespace ns_util
{class FileUtil{public:static bool ReadFile(const std::string file_path, std::string *out){std::ifstream in(file_path, std::ios::in);if(!in.is_open()){std::cerr open file file_path error std::endl;return false;}std::string line;while(std::getline(in, line)) //如何理解getline读取到文件结束呢getline的返回值是一个while(bool), 本质是因为重载了强制类型转化{*out line;}in.close();return true;}};class StringUtil{public://切分字符串static void Splist(const std::string target, std::vectorstd::string *out, const std::string sep){//boost库中的split函数boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);//第一个参数表示你要将切分的字符串放到哪里//第二个参数表示你要切分的字符串//第三个参数表示分割符是什么不管是多个还是一个//第四个参数它是默认可以不传即切分的时候不压缩不压缩就是保留空格//如字符串为aaaa\3\3bbbb\3\3cccc\3\3d//如果不传第四个参数 结果为aaaa bbbb cccc d//如果传第四个参数为boost::token_compress_on 结果为aaaabbbbccccd//如果传第四个参数为boost::token_compress_off 结果为aaaa bbbb cccc d}};//下面这5个是分词时所需要的词库路径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; class JiebaUtil { private: static cppjieba::Jieba jieba; //定义静态的成员变量需要在类外初始化 public: static void CutString(const std::string src, std::vectorstd::string *out) { //调用CutForSearch函数第一个参数就是你要对谁进行分词第二个参数就是分词后的结果存放到哪里jieba.CutForSearch(src, *out); } }; //类外初始化就是将上面的路径传进去具体和它的构造函数是相关的具体可以去看一下源代码cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
}9. 构建倒排索引BuildInvertedIndex BuildInvertedIndex构建倒排索引 构建倒排索引相对复杂一些只要将上面倒排索引的原理和伪代码的思路理解到位后下面的代码就比较简单了。 //构建倒排索引
bool BuildInvertedIndex(const DocInfo doc)
{ //词频统计结构体 struct word_cnt { int title_cnt; int content_cnt; word_cnt():title_cnt(0), content_cnt(0){} }; std::unordered_mapstd::string, word_cnt word_map; //用来暂存词频的映射表 //对标题进行分词 std::vectorstd::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::vectorstd::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; //倒排索引的id即文档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;
}
七、编写搜索引擎模块 Searcher
1. 基本结构 我们已经完成了数据清洗、去标签和索引相关的工作接下来就是要编写服务器所提供的服务我们试想一下服务器要做哪些工作首先我们的数据事先已经经过了数据清洗和去标签的服务器运行起来之后应该要先去构建索引然后通过服务索引我们在Searcher模块中实现两个函数分别为InitSearcher()和Search()代码如下 #include index.hpp
namespace ns_searcher{class Searcher{private:ns_index::Index *index; //供系统进行查找的索引public:Searcher(){}~Searcher(){}public:void InitSearcher(const std::string input){//...}//query: 搜索关键字//json_string: 返回给用户浏览器的搜索结果void Search(const std::string query, std::string *json_string){//...}};
}
2. 初始化服务InitSearcher 服务器要去构建索引本质上就是去构建一个Index对象然后调用其内部的方法我们知道构建正排索引和倒排索引本质就是将磁盘上的数据加载的内存其数据量还是比较大的可能本项目的数据量不是很大。从这一点可以看出假设创建了多个Index对象的话其实是比较占内存的我们这里就可以将这个Index类设计成为单例模式关于单例模式是什么及代码框架懒汉模式和饿汉模式我这里不做详细介绍不了解的小伙伴可以去看我 写的这篇博客https://blog.csdn.net/sjsjnsjnn/article/details/126364511里面有详细的讲解这里我直接给出Index的全部代码。 1. Index模块的单例设计
#pragma once #include iostream
#include string
#include vector
#include mutex
#include fstream
#include unordered_map
#include util.hppnamespace ns_index
{struct DocInfo //文档信息节点{std::string title; //文档的标题std::string content; //文档对应的去标签后的内容std::string url; //官网文档的urluint64_t doc_id; //文档的ID};struct InvertedElem //倒排对应的节点{uint64_t doc_id; //文档IDstd::string word; //关键字通过关键字可以找到对应的IDint weight; //权重---根据权重对文档进行排序展示};typedef std::vectorInvertedElem InvertedList;class Index{private://正排索引的数据结构采用数组数组下标就是天然的文档IDstd::vectorDocInfo forward_index; //正排索引//倒排索引一定是一个关键字和一组个InvertedElem对应[关键字和倒排拉链的映射关系]std::unordered_mapstd::string, InvertedList inverted_index;private:Index(){} //这个一定要有函数体不能deleteIndex(const Index) delete;Index operator (const Index) delete;static Index* instance;static std::mutex mtx;//C互斥锁防止多线程获取单例存在的线程安全问题public:~Index(){}public://获取index单例static Index* GetInstance(){if(nullptr instance)// 双重判定空指针, 降低锁冲突的概率, 提高性能{mtx.lock();//加锁if(nullptr instance){instance new Index();//获取单例}mtx.unlock();//解锁}return instance;}//根据doc_id找到正排索引对应doc_id的文档内容DocInfo* GetForwardIndex(uint64_t doc_id){if(doc_id forward_index.size()){std::cout doc_id out range, error! std::endl;return nullptr;}return forward_index[doc_id];}//根据倒排索引的关键字word获得倒排拉链InvertedList* GetInvertedList(const std::string word){auto iter inverted_index.find(word);if(iter inverted_index.end()){std::cerr have no InvertedList std::endl;return nullptr;}return (iter-second);}//根据去标签格式化后的文档构建正排和倒排索引 //将数据源的路径data/raw_html/raw.txt传给input即可这个函数用来构建索引bool BuildIndex(const std::string input){//在上面SaveHtml函数中我们是以二进制的方式进行保存的那么读取的时候也要按照二进制的方式读取读取失败给出提示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;while(std::getline(in, line)){DocInfo* doc BuildForwardIndex(line);//构建正排索引if(nullptr doc){std::cerr build line error std::endl;continue;}BuildInvertedIndex(*doc);//有了正排索引才能构建倒排索引}return true;}public://构建正排索引DocInfo* BuildForwardIndex(const std::string line){// 1.解析line字符串切分// 将line中的内容且分为3段原始为title\3content\3url\3// 切分后title content urlstd::vectorstd::string results;std::string sep \3; //行内分隔符ns_util::StringUtil::Splist(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)); //使用move可以减少拷贝带来的效率降低return forward_index.back(); }//构建倒排索引bool BuildInvertedIndex(const DocInfo doc) { //词频统计结构体 struct word_cnt { int title_cnt; int content_cnt; word_cnt():title_cnt(0), content_cnt(0){} }; std::unordered_mapstd::string, word_cnt word_map; //用来暂存词频的映射表 //对标题进行分词 std::vectorstd::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::vectorstd::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; //倒排索引的id即文档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; }}; Index* Index::instance nullptr;std::mutex Index::mtx;
}
2. 编写InitSearcher #include index.hpp
namespace ns_searcher{class Searcher{private:ns_index::Index *index; //供系统进行查找的索引public:Searcher(){}~Searcher(){}public://这里的input就是用户传过来的关键字首先创建单例然后构建索引void InitSearcher(const std::string input){//1.获取或者创建index对象 index ns_index::Index::GetInstance(); //2.根据index对象建立索引 index-BuildIndex(input); }};
}
3. 提供服务Search 对于提供服务我们需要从四个方面入手达到服务效果 对用户的输入的关键字我们首先要做的就是分词只有分成不同的词之后才能按照不同的词去找文档分词完毕后我们就要去触发这些分词本质就是查找建立好的正排索引和倒排索引我们的每个文档都是设置了权重字段的我们就应该在触发分词之后进行权重的降序排序达到权重高的文档靠前权重低的文档靠后根据排序完的结果构建json串用于网络传输。因为结构化的数据不便于网络传输我们就需要使用一个工具jsoncpp它是用来将结构化的数据转为字节序你可以理解为很长的字符串jsoncpp可以进行序列化将结构化的数据转换为字节序列发生到网络和反序列化将网络中的字节序列转化为结构化的数据jsoncpp使用的效果如下图 具体的使用方法会在下面有介绍。 1. 对用户关键字进行分词 为什么我们要对用户输入的关键字进行分词呢 这也不难理解虽然我们index模块中的正排索引中已经做了分词操作这只能说明服务器已经将数据准备好了按照不同的词和对应的文档分好类了但是用户输入的关键字我们依旧是要做分词操作的。设想一下如果没有做分词直接按照原始的关键字进行查找给用户反馈的文档一定没有分词来的效果好甚至有可能匹配不到文档。影响用户的体验。代码如下 //query---搜索关键字
//json_string---返回给用户浏览器的搜索结果
void Search(const std::string query, std::string *json_string)
{//1.分词---对query按照Searcher的要求进行分词 std::vectorstd::string words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, words);//分词操作
}
2. 触发分词进行索引查找 分词完成以后我们就应该按照分好的每个词关键字去获取倒排拉链我们将获取上来的倒排拉链进行保存到vector当中这也就是我们根据用户关键字所查找的结果但是我们还需要考虑一个问题用户输入的关键字进行分词了以后有没有可能多个关键字对应的是同一个文档如下图所示 根据上面的图我们首先想到的就是去重。其次每个倒排拉链的结点都包含doc_id、关键字和权重。既然显示了重复的文档我们应该是只显示一个那么这个最终显示的文档其权重就是几个文档之和关键字就是几个文档的组合那么我们可以定义一个新的结构体来保存查找后的倒排拉链代码如下 //该结构体是用来对重复文档去重的结点结构
struct InvertedElemPrint
{uint64_t doc_id; //文档IDint weight; //重复文档的权重之和std::vectorstd::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::vectorstd::string words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, words);//分词操作//2.触发---就是根据分词的各个词进行index查找建立index是忽略大小写所以搜索关键字也需要std::vectorInvertedElemPrint inverted_list_all; //用vector来保存std::unordered_mapuint64_t, InvertedElemPrint tokens_map;//用来去重for(std::string word : words)//遍历分词后的每个词{boost::to_lower(word);//忽略大小写ns_index::InvertedList* inverted_list index-GetInvertedList(word);//获取倒排拉链if(nullptr inverted_list){continue;}//遍历获取上来的倒排拉链for(const auto elem : *inverted_list){auto item tokens_map[elem.doc_id];//插入到tokens_map中key值如果相同这修改value中的值item.doc_id elem.doc_id;item.weight elem.weight;//如果是重复文档key不变value中的权重累加item.words.push_back(elem.word);//如果树重复文档关键字会被放到vector中保存}}//遍历tokens_map将它存放到新的倒排拉链集合中这部分数据就不存在重复文档了for(const auto item : tokens_map) {inverted_list_all.push_back(std::move(item.second));}
}
3. 按文档权重进行降序排序 对于排序应该不难我们直接使用C库当中的sort函数并搭配lambda表达式使用当然你也可以自己写一个快排或者归并排序按权重去排 //query---搜索关键字
//json_string---返回给用户浏览器的搜索结果
void Search(const std::string query, std::string *json_string)
{//1.分词---对query按照Searcher的要求进行分词 std::vectorstd::string words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, words);//分词操作//2.触发---就是根据分词的各个词进行index查找建立index是忽略大小写所以搜索关键字也需要std::vectorInvertedElemPrint inverted_list_all; //用vector来保存std::unordered_mapuint64_t, InvertedElemPrint tokens_map;//用来去重for(std::string word : words)//遍历分词后的每个词{boost::to_lower(word);//忽略大小写ns_index::InvertedList* inverted_list index-GetInvertedList(word);//获取倒排拉链if(nullptr inverted_list){continue;}//遍历获取上来的倒排拉链for(const auto elem : *inverted_list){auto item tokens_map[elem.doc_id];//插入到tokens_map中key值如果相同这修改value中的值item.doc_id elem.doc_id;item.weight elem.weight;//如果是重复文档key不变value中的权重累加item.words.push_back(elem.word);//如果树重复文档关键字会被放到vector中保存}}//遍历tokens_map将它存放到新的倒排拉链集合中这部分数据就不存在重复文档了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 InvertedElemPrint e1, const InvertedElemPrint e2){return e1.weight e2.weight;});
}
4. 根据排序结果构建json串 关于json的使用我们首先需要在Linux下安装jsoncppsudo yum install -y jsoncpp-devel 这里我之前下载过了已经是最新的版本了你们只需要输入上面的指令有这样的提示就表明安装成功了。 如何使用 root对象你可以理解为json数组item1对象就是json中value的对象他可以保存kv值 item2对象就是json中value的对象他可以保存kv值 将item1和item2 append到root中你可以理解为将root这个大json数组保存了两个子json序列化的方式有两种StyledWriter和FastWriter 两者的区别1. 呈现的格式不一样2. 在网络传输中FastWriter更快。序列化方式1StyledWriter 序列化方式2FastWriter 有了基本的了解之后我们开始编写正式的代码 //query---搜索关键字
//json_string---返回给用户浏览器的搜索结果
void Search(const std::string query, std::string *json_string)
{//1.分词---对query按照Searcher的要求进行分词 std::vectorstd::string words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, words);//分词操作//2.触发---就是根据分词的各个词进行index查找建立index是忽略大小写所以搜索关键字也需要std::vectorInvertedElemPrint inverted_list_all; //用vector来保存std::unordered_mapuint64_t, InvertedElemPrint tokens_map;//用来去重for(std::string word : words)//遍历分词后的每个词{boost::to_lower(word);//忽略大小写ns_index::InvertedList* inverted_list index-GetInvertedList(word);//获取倒排拉链if(nullptr inverted_list){continue;}//遍历获取上来的倒排拉链for(const auto elem : *inverted_list){auto item tokens_map[elem.doc_id];//插入到tokens_map中key值如果相同这修改value中的值item.doc_id elem.doc_id;item.weight elem.weight;//如果是重复文档key不变value中的权重累加item.words.push_back(elem.word);//如果树重复文档关键字会被放到vector中保存}}//遍历tokens_map将它存放到新的倒排拉链集合中这部分数据就不存在重复文档了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 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-GetForwardIndex(item.doc_id); if(nullptr doc) { continue; } Json::Value elem; elem[title] doc-title; elem[desc] GetDesc(doc-content, item.words[0]); //content是文档去标签后的结果但不是我们想要的我们要的是一部分 elem[url] doc-url; //调式 //elem[id] (int)item.doc_id; //elem[weight] item.weight; root.append(elem); } //Json::StyledWriter writer; //方便调试 Json::FastWriter writer;//调式没问题后使用这个 *json_string writer.write(root);
} 在上述的代码中我们构建出来的json串最后是要返回给用户的对于内容我们只需要一部分而不是全部所以我们还要实现一个 GetDesc 的函数 std::string GetDesc(const std::string html_content, const std::string word)
{//找到word(关键字)在html_content中首次出现的位置//然后往前找50个字节(如果往前不足50字节就从begin开始)//往后找100个字节(如果往后不足100字节就找到end即可)//截取出这部分内容const int prev_step 50;const int next_step 100;//1.找到首次出现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);//2.获取start和end位置int start 0;int end html_content.size() - 1;//如果之前有50个字符就更新开始位置if(pos start prev_step) start pos - prev_step;if(pos end - next_step) end pos next_step;//3.截取子串然后返回if(start end) return None2;std::string desc html_content.substr(start,end - start);desc ...;return desc;
} 最后我们来测试一下效果编写debug.cc这个文件和我们项目文件关联性不大主要是用来调式需要将上文代码中备注调式的代码放开 #include searcher.hpp
#include cstdio
#include iostream
#include string const std::string input data/raw_html/raw.txt; int main()
{ ns_searcher::Searcher *search new ns_searcher::Searcher(); search-InitSearcher(input); //初始化search创建单例并构建索引 std::string query; //自定义一个搜索关键字 std::string json_string; //用json串返回给我们 char buffer[1024]; while(true) { std::cout Please Enter You Search Query; //提示输入 fgets(buffer, sizeof(buffer) - 1, stdin); //读取 buffer[strlen(buffer)-1] 0; query buffer; search-Search(query, json_string); //执行服务对关键字分词-查找索引-按权重排序-构建json串-保存到json_string-返回给我们 std::cout json_string std::endl;//输出打印 } return 0;
} 对应的Makefile 运行结果如下 我们输入搜索关键字split 我们可以看到效果很明显。我们复制第三个网址查看一下权重是否一样 当你再去查看其他网址然后自己进行权重计算的时候有时候会多一个或者少一个我分析的原因就是在对标题和内容进行分词的时候产生的一些影响但是大体上没有太大的问题。 测试完毕之后那些测试可以删除或屏蔽
八、编写http_server模块
1. 引入cpp-httplib到项目中 安装cpp-httplib安装的是v0.7.15版本https://gitcode.net/mirrors/yhirose/cpp-httplib/-/releases?aftereyJpZCI6IjEwMDUwNCIsInJlbGVhc2VkX2F0IjoiMjAyMC0xMi0wMSAyMzozMjo0Mi4wMDAwMDAwMDAgQ1NUIn0https://gitcode.net/mirrors/yhirose/cpp-httplib/-/releases?aftereyJpZCI6IjEwMDUwNCIsInJlbGVhc2VkX2F0IjoiMjAyMC0xMi0wMSAyMzozMjo0Mi4wMDAwMDAwMDAgQ1NUIn0 下载zip上传到服务器即可这些操作在上面都演示过了。 cpp-httplib在使用的时候需要使用较新版本的gcccentos 7下默认gcc 4.8.5我们是需要升级的这里我是已经升级过的接下来我介绍一下升级gcc的方法。 升级gcc scl gcc devsettool 升级gcc使用scl工具集来升级安装sclsudo yum install centos-release-scl scl-utils-build 安装新版本gccsudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c 启动新版的gccscl enable devtoolset-7 bash(注意这只是本次会话有效) 长期使用新版的gcc修改 ~/.bash_profile 文件 只要在这个文件下加上这个命令启动服务器就会执行gcc就是新版的了 我们将cpp-httplib放到项目中的test目录下并解压好 提示这里你可以将test目录修改为thirdparty这样从命名上更加直观将test目录下的一些软连接删除使用unlink命令但是在Boost_Searcher目录下的软连接还要重新修改一下。 cpp-httplib有了之后我们只需要使用这个目录下的httplib.h文件即可 建立软连接到我们的项目路径下 至此我们就可以在我们的项目中使用了。 2. cpp-httplib的使用介绍 创建一个http_server.cc的文件编写测试代码 #include cpp-httplib/httplib.h int main()
{ //创建一个Server对象本质就是搭建服务端httplib::Server svr; // 这里注册用于处理 get 请求的函数当收到对应的get请求时请求hi时程序会执行对应的函数也就是lambda表达式svr.Get(/hi, [](const httplib::Request req, httplib::Response rsp){ //设置 get hi 请求返回的内容 rsp.set_content(hello world, text/plain; charsetutf-8); }); // 绑定端口8080启动监听0.0.0.0表示监听任意端口svr.listen(0.0.0.0, 8080); return 0;
} 对应的Makefile 我们直接编译运行http_server 打开浏览器访问我们这个端口如43.138.201:8081/hi,结果如下 但是当我们访问43.138.201:8081时却找不到对应的网页 像我们访问百度时www.baidu.com百度会给一个首页所有在我们的项目目录下呢也需要一个首页。 在项目路径下创建一个wwwroot目录目录中包含一个index.html文件 编写我们的首页并修改我们的http_server.cc !DOCTYPE html
html langen
headmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0titleboost搜索引擎/title
/head
bodyh1欢迎来到我的世界/h1
/body
/html
#include cpp-httplib/httplib.hconst std::string root_path ./wwwroot;int main()
{ //创建一个Server对象本质就是搭建服务端httplib::Server svr; //访问首页svr.set_base_dir(root_path.c_str());// 这里注册用于处理 get 请求的函数当收到对应的get请求时请求hi时程序会执行对应的函数也就是lambda表达式svr.Get(/hi, [](const httplib::Request req, httplib::Response rsp){ //设置 get hi 请求返回的内容 rsp.set_content(hello world, text/plain; charsetutf-8); }); // 绑定端口8080启动监听0.0.0.0表示监听任意端口svr.listen(0.0.0.0, 8080); return 0;
} 再次通过浏览器进行访问 3. 正式编写http_server
#include cpp-httplib/httplib.h
#include searcher.hpp const std::string input data/raw_html/raw.txt;
const std::string root_path ./wwwroot; int main()
{ ns_searcher::Searcher search; search.InitSearcher(input); //创建一个Server对象本质就是搭建服务端httplib::Server svr; //访问首页svr.set_base_dir(root_path.c_str()); // 这里注册用于处理 get 请求的函数当收到对应的get请求时请求s时程序会执行对应的函数也就是lambda表达式svr.Get(/s, [search](const httplib::Request req, httplib::Response rsp){//has_param这个函数用来检测用户的请求中是否有搜索关键字参数中的word就是给用户关键字取的名字类似wordsplit if(!req.has_param(word)){ rsp.set_content(必须要有搜索关键字!, text/plain; charsetutf-8); return; } //获取用户输入的关键字std::string word req.get_param_value(word); std::cout 用户在搜索 word std::endl; //根据关键字构建json串std::string json_string; search.Search(word, json_string);//设置 get s 请求返回的内容返回的是根据关键字构建json串内容rsp.set_content(json_string, application/json); }); std::cout 服务器启动成功...... std::endl; // 绑定端口8080启动监听0.0.0.0表示监听任意端口svr.listen(0.0.0.0, 8080); return 0;
} 此时我们编译运行我们的代码先执行parser进行数据清洗然后执行http_server搭建服务创建单例构建索引发生请求根据用户输入的关键字进行查找索引构建json串最后响应给用户 此时服务器启动成功索引也建立完毕 此时我们在浏览器进行访问43.138.71.201:8080/s 此时我们在浏览器进行访问43.138.71.201:8080/s?wordsplit 最终在浏览器上就显示出来了到这里我们的后端内容大致上算是完成了最后添加一个日志就可以了如果你对前端不感兴趣到这里就可以了。可以把日志功能的添加看一看 九、添加日志到项目中 我们创建一个log.hpp的头文件需要添加日志的地方index模块searcher模块、http_server模块。代码如下 #pragma once
#include iostream
#include string
#include ctime #define NORMAL 1 //正常的
#define WARNING 2 //错误的
#define DEBUG 3 //bug
#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;
}
/*
简单说明 我们用宏来实现日志功能其中LEVEL表明的是等级有四种这里的#LEVEL的作用是把一个宏参数变成对应的字符串直接替换
C语言中的预定义符号__FILE__进行编译的源文件__LINE__文件的当前行号
补充几个__DATE__文件被编译的日期__TIME__文件被编译的时间__FUNCTION__进行编译的函数
*/ 你可以在你想要的地方进行添加 十、编写前端模块 前端模块我做详细的解释代码中都有注释直接上代码 !DOCTYPE html
html langenheadmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0script srchttp://code.jquery.com/jquery-2.1.1.min.js/scripttitleboost 搜索引擎/titlestyle/*去掉网页中的所有内外边距可以了解html的盒子模型*/* {margin: 0;/* 设置外边距 */padding: 0;/* 设置内边距 */}/* 将我们的body内的内容100%和html的呈现吻合 */html,body {height: 100%;}/* 以点开头的称为类选择器.container */.container {/* 设置div的宽度 */width: 800px;/* 通过设置外边距达到居中对其的目的 */margin: 0px auto;/* 设置外边距的上边距保持元素和网页的上部距离 */margin-top: 15px;}/* 复合选择器选中container下的search */.container .search {/* 宽度与父标签保持一致 */width: 100%;/* 高度设置52px */height: 50px;}/* 选中input标签,直接设置标签的属性先要选中标签选择器 *//* input在进行高度设置的时候没有考虑边框的问题 */.container .search input {/* 设置left浮动 */float: left;width: 600px;height: 50px;/* 设置边框属性依次是边框的宽度、样式、颜色 */border: 2px solid #CCC;/* 去掉input输入框的右边框 */border-right: none;/* 设置内内边距默认文字不要和左侧边框紧挨着 */padding-left: 10px;/* 设置input内部的字体的颜色和样式 */color: #CCC;color: #CCC;font-size: 17px;}.container .search button {/* 设置left浮动 */float: left;width: 150px;height: 54px;/* 设置button的背景颜色 #4e6ef2*/background-color: #4e6ef2;color: #FFF;/* 设置字体的大小 */font-size: 19px;font-family: Georgia, Times New Roman, Times, serif Times New Roman, Times, serif;}.container .result {width: 100%;}.container .result .item {margin-top: 15px;}.container .result .item a {/* 设置为块级元素单独占一行 */display: block;text-decoration: none;/* 设置a标签中的文字字体大小 */font-size: 22px;/* 设置字体的颜色 */color: #4e6ef2;}.container .result .item a:hover {/* 设置鼠标放在a之上的动态效果 */text-decoration: underline;}.container .result .item p {margin-top: 5px;font-size: 16px;font-family: Lucida Sans, Lucida Sans Regular, Lucida Grande, Lucida Sans Unicode, Geneva, Verdana, sans-serif;}.container .result .item i {/* 设置为块级元素单独占一行 */display: block;/* 取消斜体风格 */ font-style: normal;color: green;}/style
/headbodydiv classcontainerdiv classsearchinput typetext value输入搜索关键字...button onclickSearch()搜索一下/button/divdiv classresult!-- div classitema href#这是标题/ap这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要/pihttps://hao.360.com/?hjllq7a/i/divdiv classitema href#这是标题/ap这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要/pihttps://hao.360.com/?hjllq7a/i/divdiv classitema href#这是标题/ap这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要/pihttps://hao.360.com/?hjllq7a/i/divdiv classitema href#这是标题/ap这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要/pihttps://hao.360.com/?hjllq7a/i/divdiv classitema href#这是标题/ap这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要/pihttps://hao.360.com/?hjllq7a/i/divdiv classitema href#这是标题/ap这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要/pihttps://hao.360.com/?hjllq7a/i/div --/div/divscriptfunction Search() {// 是浏览器的一个弹出窗// 1.提取数据,$可以理解为就是JQuery的别称let query $(.container .search input).val();console.log(query query); //console是浏览器对话框可以用来进行查看js数据// 2.发起http请求ajax属于一个和后端进行数据交互的函数$.ajax({type: GET,url: /s?word query,success: function (data) {console.log(data);BuildHtml(data);}});}function BuildHtml(data) {// 获取html中的result标签let result_lable $(.container .result);// 清空历史搜索结果result_lable.empty();for (let elem of data) {console.log(elem.title);console.log(elem.url);let a_lable $(a, {text: elem.title,href: elem.url,// 跳转到新的页面target: _blank});let p_lable $(p, {text: elem.desc});let i_lable $(p, {text: elem.url});let div_lable $(div, {class: item});a_lable.appendTo(div_lable);p_lable.appendTo(div_lable);i_lable.appendTo(div_lable);div_lable.appendTo(result_lable);}} /script
/body/html 最终演示 将我们的项目部署到Linux上 nohup ./http_server log/log.txt 21 一些日志信息就会保存到log/log.txt中 十一、项目总结 关于项目总结主要是针对项目的扩展 1. 建立整站搜索 我们搜索的内容是在boost库下的doc目录下的html文档你可以将这个库建立搜索也可以将所有的版本但是成本是很高的对单个版本的整站搜索还是可以完成的取决于你服务器的配置。2. 设计一个在线更新的方案信号爬虫完成整个服务器的设计 我们在获取数据源的时候是我们手动下载的你可以学习一下爬虫写个简单的爬虫程序。采用信号的方式去定期的爬取。3. 不使用组件而是自己设计一下对应的各种方案 我们在编写http_server的时候是使用的组件你可以自己设计一个简单的4. 在我们的搜索引擎中添加竞价排名 我们在给用户反馈是提供的是json串显示到网页上有title、content和url就可以在构建json串时你加上你的博客链接将博客权重变高了就能够显示在第一个5. 热次统计智能显示搜索关键词字典树优先级队列 6. 设置登陆注册引入对mysql的使用