上海人才网官网招聘肖瑶,国内seo公司排名,支付宝网站接口申请,知名室内设计网站目录
一、前言
二、项目的相关背景
⚡什么是Boost库#xff1f;⚡
⚡什么是搜索引擎#xff1f;⚡
⚡为什么要做Boost搜索引擎#xff1f;⚡
二、搜索引擎的宏观原理
三、搜索引擎技术栈和项目环境
四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理
#x…目录
一、前言
二、项目的相关背景
⚡什么是Boost库⚡
⚡什么是搜索引擎⚡
⚡为什么要做Boost搜索引擎⚡
二、搜索引擎的宏观原理
三、搜索引擎技术栈和项目环境
四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理
正排索引forword index
倒排索引inverted index
搜索引擎的具体原理
五、编写数据去除标签和数据清洗模块 Parser
1. 数据准备
2. 编写parser模块
基本结构设计
细节实现
六、编写建立索引的模块 Index
节点设计
基本结构设计
Index类的基本框架
获取正排索引GetForwardIndex
获取倒排拉链GetInvertedList 构建索引BuildIndex
构建正排索引BuildForwardIndex
倒排索引的原理介绍 (重点)
cppjieba分词工具的安装和使用介绍
引入cppjieba到项目中
构建倒排索引BuildInvertedIndex
七、编写搜索引擎模块 Searcher
基本结构
初始化服务InitSearcher
1. Index模块的单例设计
2. 编写InitSearcher
提供服务Search 1. 对用户关键字进行分词
2. 触发分词进行索引查找
3. 按文档权重进行降序排序
4. 根据排序结果构建json串
八、编写http_server模块
1. 引入cpp-httplib到项目中
2. cpp-httplib的使用介绍
3. 正式编写http_server
九、添加日志到项目中
十、编写前端模块
十一、项目总结
项目的总体框架和功能
技术栈和项目环境 gitee代码地址
十二、共勉 一、前言 我们平时在用浏览器搜索时服务器给我们返回的分别是跟搜索关键字相关的一个个网站信息网站信息分别包括网站的标题网站内容的简述和该网站的url。在点击标题后会跳转到对应链接的页面。平时我们用的搜索引擎比如说百度谷歌等等他们都是搜索全网的信息的我们项目做的是一个小范围的搜索引擎一个用 boost库 实现的 boost站内搜索。 二、项目的相关背景
⚡什么是Boost库⚡ Boost库 是C的准标准库 它提供了很多C没有的功能可以称之为是C的后备力量。早期的开发者多为C标准委员会的成员一些Boost库也被纳入了C11中如哈希、智能指针这里大家可以去百度百科上搜索一看便知。下面是boost的官网 -------------------Boost官网链接 ⚡什么是搜索引擎⚡ 对于搜索引擎相信大家一定不陌生如百度、360、搜狗等都是我们常用的搜索引擎。但是你想自己实现出一个和百度、360、搜狗一模一样哪怕是类似的搜索引擎是非常非常困难的。我们可以看一下这些搜索引擎在搜索关键字的时候给我们展示了哪些信息 我们可以看到基本上搜索引擎根据我们所给的关键字搜出来的结果展示都是以网页标题、网页内容摘要和跳转的网址组成的。但是它可能还有相应的照片、视频、广告这些我们在设计基于Boost库的搜索引擎项目的时候不考虑这些它们属于扩展内容 ⚡为什么要做Boost搜索引擎⚡ 刚刚我们看到了Boost的官网界面我们可以对比一下cplusplus官网看看有什么区别 可以看到Boost库是没有站内搜索框的如果我们可以对boost库做一个站内搜索向cplusplus一样搜索一个关键字就能够跳转到指定的网页并显示出来。那么这个项目还是具有一定意义的。这也就是项目的背景。 其次站内搜索的数据更加垂直数据量其实更小。 二、搜索引擎的宏观原理 刚刚我们介绍完了基于Boost库的搜索引擎的项目背景后相信大家有了一定的了解大致上知道了这个项目是什么意思。但是我们还需要了解一下搜索引擎的宏观原理。接下来以下面的图为例介绍一下其宏观原理。 原理图分析 我们要实现出boost库的站内搜索引擎红色虚线框内就是我们要实现的内容总的分为客户端和服务器详细分析如下 1️⃣客户端 想要获取到大学生的相关信息呈现在网页上的样子就是网页的标题摘要网址首先我们构建的服务器就要有对应的数据存在这些数据从何而来我们可以进行全网的一个爬虫将数据爬到我们的服务器的磁盘上但是我们这个项目是不涉及任何爬虫程序的我们可以直接将 boost库 对应版本的数据直接解压到我们对应文件里。
2️⃣现在数据已经被我们放到了磁盘中了接下来客户端要访问服务器那么服务器首先要运行起来服务器一旦运行起来它首先要做的工作就是对磁盘中的这些数据进行去标签和数据清洗的动作因为我们从boost库拿的数据其实就是对应文档html网页但是我们需要的只是每个网页的标题网页内容摘要跳转的网址所以才有了去标签和数据清洗只拿我们想要的。这样就可以直接跳过网址跳转到boost库相应文档的位置。
3️⃣服务器完成了去标签和数据清洗之后就需要对这些清洗后的数据建立索引方便客户端快速查找
4️⃣当服务器所以的工作都完成之后客户端就发起http请求通过GET方法上传搜索关键服务器收到了会进行解析通过客户端发来的关键字去检索已经构建好的索引找到了相关的html后就会将逐个的将每个网页的标题、摘要和网址拼接起来构建出一个新的网页响应给客户端至此客户就看到了相应的内容点击网址就可以跳转到boost库相应的文档位置。 三、搜索引擎技术栈和项目环境 基于Boost库的搜索引擎项目所涉及的 技术栈 和 项目环境 如下 技术栈
C/C/C11、STL、boost库、Jsoncpp、cppjieba、cpp-httplibhtml5、css、js、jQuery、Ajax
项目环境
Ubuntu 20.04 云服务器vim/gcc/g/Makefilevs2022 or vscode
相关库
cppjieba 提供分词的相关接口boost 提供在当前目录下遍历所有子目录文件的迭代器Jsoncpp 提供可以将格式化的数据和 json 字符串相互转换的接口cpp-httplib 提供http相关接口 技术栈和项目环境有些你可能不了解没关系下面的代码编写中会有介绍但是基本的技术栈C/C/C11/STL 你是要熟悉的项目环境 云服务器、vim、vs这些你也是需要知道的。 四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理 通过下面两个文档来解释一下 正排索引 和 倒排索引 文档ID文档内容1雷军买了四斤小米2雷军发布了小米汽车
正排索引forword index 正排索引就是从【文档ID】找到 【文档内容】(文档内的关键字) 正排索引 是创建 倒排索引 的基础有了正排索引之后如何构建倒排索引呢 -- 分词分词的目的方便建立 倒排索引 和 查找 我们要对目标文档进行【分词】以上面的文档1、2为例我们来进行分词演示 文档1雷军、买、四斤、小米、四斤小米文档2雷军、发布、小米、汽车、小米汽车 进行分词之后就能够方便的建立倒排索引和查找。 我们可以看到在文档1、2中其中的 “了” 子被我们省去了这是因为像了呢吗 athe 等都是属于停止词一般我们在分词的时候可以不考虑。那么什么是停止词呢 停止词 它是搜索引擎分词的一项技术停止词就是没有意义的词。如在一篇文章中你可以发现有很多类似于了呢吗 athe等中文或英文中都是停止词因为频繁出现如果我们在进行分词操作的时候如果把这些停止词也算上不仅会建立索引麻烦而且会增加精确搜索的难度。 倒排索引inverted index 刚刚我们说 正排索引 是 创建 【倒排索引】的基础首先是要对文档进行分词操作 倒排索引就是根据文档内容的分词整理不重复的各个关键字对应联系到文档ID的方案 关键词具有唯一性文档ID雷军文档1 文档2买文档1四斤文档1小米文档1 文档2四斤小米文档1发布文档2汽车文档2小米汽车文档2 模拟一次查找的过程 用户输入小米 --- 去倒排索引中查找关键字“小米” --- 提取出文档ID【1,2】--- 去正排索引中根据文档ID【1,2】找到文档内容 --- 通过 [ 标题 内容摘要 网址 ] 的形式构建响应结果 --- 返回给用户小米相关的网页信息。 搜索引擎的具体原理 第一步 我们需要去boost官网下载boost库这个库里面包含boost官网的所有文档的html文件。 第二步 我们写一个解析程序从一个个html文件的源码中提取标题、内容和url将他们保存到硬盘的一个data.txt文件中。 第三步 读取data.txt文件建立正排和倒排索引提供索引的接口来获取正排和倒排数据 第四步 写一个html页面提供给用户一个搜索功能。 一次访问过程 当用户通过浏览器向服务器发送搜索信息时服务器会根据搜索的关键字获取对应倒排数据然后通过倒排数据找到正排ID从而找到正排的文档内容。然后构建出网页的标题简述内容的一部分url通过json字符串响应回去然后在用户的浏览器显示出一个个网页信息。 五、编写数据去除标签和数据清洗模块 Parser 在编写Parser模块的之前我们先将数据准备好去boost官网下载最新版本的库解压到 Linux下操作方法如下 1. 数据准备
boost官网Boost C Libraries 进入官网后是如下界面 进入之后你可以选择最新版本的下载不管是那个版本其实都是可以用的。 点击Download之后我们选择这个进行下载 下载好之后我们先在 linux下创建一个名为 Boost_Searcher 目录以后将会在这个目录下进行各种代码模块的编写以及存放各种数据下面是创建过程 接下来我们将下载好的1.85.0版本的boost库解压到Linux下使用 rz 命令用于文件传输输入rz -E 命令后直接回车找到 boost点击打开即可你也可以直接将压缩包拖拽到命令行中 效果如下 此时我们使用 tar xzf boost_1_85_0.tar.gz 进行解压解压好后我们进行查看 可以看到解压好的boost里面有这么所文件但这么多文件并不是我们都需要的我们需要的就是boost_1_85_0/doc/html目录下的html。为什么呢结合下面的图 上面的图就是boost库的操作方法我们可以看到右下角的两个网页的网址他们都是在doc/html目录下的文件都是 .html。我们只要这个就可以了。后期通过地址进行拼接达到跳转就能来到这个网页。 我们进入到Linux下的 doc/html 目录看看里面有哪些东西 可以看到里面除了 html为后缀的文件外还有一些目录但是我们只需要html文件所以我们要进行数据清洗。只拿html文件。 我们了解了大概的情况之后我们来将我们所需要的数据源 拷贝到 data目录下的intput目录下 2. 编写parser模块
基本结构设计 这里我是在 vscode 下进行代码编写的但是需要连接一下云服务器与 Linux进行同步。 你也可以选择 vim。 基本框架主要完成的工作如下 将 data/input/ 所有后缀为 html 的文件筛选出来 ---- 清洗数据然后对筛选好的html文件进行解析去标签拆分出标题、内容、网址 ---- 去标签最后将去标签后的所有html文件的标题、内容、网址按照 \3 作为分割符每个文件再按照 \n 进行区分。写入到 data/raw_html/raw.txt 下 什么是清洗数据呢 我们进入到Linux下的 doc/html 目录看看里面有哪些东西 可以看到里面除了 html为后缀的文件外还有一些目录但是我们只需要html文件所以我们要进行数据清洗。只拿html文件。 最后我们在 data目录 下的 raw_html目录下 创建有一个 raw.txt文件用来存储干净的数据文档 什么是去标签呢 对数据清洗之后拿到的全都是 html 文件此时还需要对 html 文件进行去标签处理我们这里随便看一个html文件 我们呢进入 input 这个目录下随便打开一个 .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 接下来 我们根据上面分析的 基本结构设计 来写出一个 parser 模块的框架 在 Boost_Searcher 目录下创建 parser.cpp 文件开始编写框架 parser.cpp 框架如下 #include iostream
#include string
#include vector// 首先我们肯定会读取文件所以先将文件的路径名 罗列出来
// 将 数据源的路径 和 清理后干净文档的路径 定义好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 中// 第一步递归式的把每个html文件名带路径保存到files_list中方便后期进行一个一个的文件读取// 从 src_path 这个路径中提取 html文件将提取出来的文件存放在 string 类型的 files_list 中if(!EnumFile(src_path, files_list)) //EnumFile--枚举文件{std::cerr enum file name error! std::endl;return 1;}return 0;// 第二步从 files_list 文件中读取每个.html的内容并进行解析std::vectorDocInfo_t results;// 从 file_list 中进行解析将解析出来的内容存放在 DocInfo 类型的 results 中if(!ParseHtml(files_list, results))//ParseHtml--解析html{std::cerr parse html error! std::endl;return 2;}// 第三部把解析完毕的各个文件的内容写入到output按照 \3 作为每个文档的分隔符// 将 results 解析好的内容直接放入 output 路径下if(!SaveHtml(results, output))//SaveHtml--保存html{std::cerr save html error! std::endl;return 3;}return 0;
}
细节实现 主要实现枚举文件、解析html文件、保存html文件三个工作。 这三个工作完成是需要我们使用boost库当中的方法的我们需要安装一下boost的开发库 命令sudo apt install -y libboost-all-dev 下图就是我们接下来编写代码需要用到的 boost库 当中的 filesystem方法。 枚举文件 //在原有的基础上添加这个头文件
#include boost/filesystem.hpp//把每个html文件名带路径保存到files_list中
bool 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; // 定义了一个空的迭代器用来进行判断递归结束 -- 相当于 NULLfor(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 注意boost库 并不是C语言的标准库我们需要指明我们需要链接那个库 链接 -lboost_system -lboost_filesystem
cppg
parser:parser.cpp$(cpp) -o $ $^ -lboost_system -lboost_filesystem -stdc11
.PHONY:clean
clean:rm -f parser
make 后查看 parser的链接库 接下来就可以 make然后运行: 测试成功后将测试代码注释掉哦运行测试成功继续编写下一个模块 经过上面函数的筛选后我们 files_list中存放的都是 html文件的路径名了可以进行html 的文件解析了 解析html文件 读取刚刚枚举好的文件解析html文件中的title解析html文件中的content解析html文件中的路径构建url ParseHtml()解析函数代码的整体框架如下 函数架构
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;
}
解释该函数主要完成 4件事
①根据路径名依次读取文件内容②提取title③提取content④构建url。 读取文件
遍历 files_list 中存储的文件名从中读取文件内容到 result 中由函数 ReadFile() 完成该功能。该函数定义于头文件 util.hpp 的类 FileUtil中。
#pragma once
#include iostream
#include string
#include fstream
#include vectornamespace ns_util
{class FileUtil{ public://输入文件名将文件内容读取到out中static bool ReadFile(const std::string file_path, std::string *out){// 读取 file_path一个.html文件 中的内容 -- 打开文件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(bool),getline的返回值istream会重载操作符bool读到文件尾eofset被设置并返回false//如何理解getline读取到文件结束呢getline的返回值是一个while(bool), 本质是因为重载了强制类型转化while(std::getline(in, line)) // 每循环一次读取的是文件的一行内容{*out line; // 将文件内容保存在 *out 里面}in.close(); // 关掉文件return true;}};
} 提取title —— ParseTitle()
随意打开一个html文件可以看到我们要提取的title部分是被title标签包围起来的部分。如下所示 上图显示标题的时候是以 title标题/title 构成的我们只需要findtitle就能找到这个标签的左尖括号的位置然后加上title的长度此时就指向了标题的起始位置同理再去找到/title的左尖括号最后截取子串 这里需要依赖函数 —— bool ParseTitle(const std::string file,doc.title)来帮助完成这一工作函数就定义在 parse.cpp 中。
//解析title
static bool ParseTitle(const std::string file,std::string* title)
{// 查找 title 位置std::size_t begin file.find(title);if(begin std::string::npos){return false;}// 查找 /title 位置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;
} 提取content实际上是去除标签 —— ParseContent()
随意打开一个html文件即 把所有尖括号及尖括号包含的部分全部去除只保留 尖括号以外有价值的内容 解析内容的时候我们采用一个简易的状态机来完成状态机包括两种状态LABLE(标签)和CONTENT(内容)html的代码中标签都是这样的起始肯定是标签我们追个字符进行遍历判断如果遇到“”表明下一个即将是内容了我们将状态机置为CONTENT接着将内容保存起来如果此时遇到了“”表明到了标签了我们再将状态机置为LABLE不断的循环知道遍历结束这里需要依赖函数 —— bool ParseContent(const std::string file,doc.content)来帮助完成这一工作函数就定义在parse.cpp中。
//去标签 -- 数据清洗
static bool ParseContent(const std::string file,std::string* content)
{//去标签基于一个简易的状态机enum status // 枚举两种状态{ LABLE, // 标签CONTENT // 内容};enum status s LABLE; // 刚开始肯定会碰到 默认状态为 LABLEfor(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 boost库 在网页上的 url和我们 下载的文档的路径 是 有对应关系的 举个例子
当我们进入官网中查询 Accumulators其官网 url 为 官网URL样例为https://www.boost.org/doc/libs/1_85_0/doc/html/accumulators.html
如果我们在下载的文档中查询该网页文件那么其路径为 我们下载下来的url样例/boost_1_85_0/doc/html/accumulators.html 而我们项目中的所有数据源都拷贝到了 data/input 目录下那么在我们项目中寻找该网页文件的路径为
data/input/accumulators.html此时我们想要从我们的项目中得到和官网一样的网址我们可以这样做 拿 官网的部分网址作为 头部的 url url_head https://www.boost.org/doc/libs/1_85_0/doc/html 将我们项目的路径 data/input 删除后得到 /accumulators.html url_tail [data/input(删除)] /accumulators.html - url_tail /accumulators.html; 将 url_head url_tail 得到 官网的 url
url url_head url_tail //相当于形成了一个官网链接这里需要依赖函数 —— bool ParseUrl(const std::string file_path,std:string* url)来帮助完成这一工作函数就定义在 parser.cpp 中。
//构建官网url :url_head url_tail
static bool ParseUrl(const std::string file_path,std::string* url)
{std::string url_head https://www.boost.org/doc/libs/1_85_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拼接起来。 到这里我们写的 ParseHtml 解析部分就已经写完啦那定是我们要先测试一下它的正确性啦 向源代码中加入了 ShowDoc 测试代码测试完注释掉即可
// for debug
void ShowDoc(const DocInfo_t doc)
{std::couttitle: doc.titlestd::endl;std::coutcontent: doc.contentstd::endl;std::couturl: doc.urlstd::endl;
}//按照files_list读取每个文件的内容并进行解析
bool ParseHtml(const std::vectorstd::string files_list, std::vectorDocInfo_t *results)
{// 首先在解析文件之前肯定需要 遍历 读取文件for(const std::string file : files_list){// 1.读取文件Read() --- 将文件的全部内容全部读出放到 result 中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的右值引用版本// for debug -- 在测试的时候将上面的代码改写为 results-push_back(doc);ShowDoc(doc);break; // 只截取一个文件打印}return true;
} 接下来就可以 make 然后运行 为了进一步验证正确性我们可以将网页复制下来在浏览器中打开看是否成功 测试成功后将测试代码注释掉哦 保存html文件 采用下面的方案 写入文件中一定要考虑下一次在读取的时候也要方便操作!类似title\3content\3url \n title\3content\3url \n title\3content\3url \n ...方便我们getline(ifsream, line)直接获取文档的全部内容title\3content\3url 说明一下分隔符为什么使用‘\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;
} 接下来我们做一下测试我们编译下 parser.cpp得到./parser可执行文件。如果成功那么此时 /data/raw_html目录下的 raw.txt 就会填入所有的处理完的 html 文档。 用 vim 打开 raw.txt 进行观察每个html文档占据一行显然行数与处理之前的html文件数是匹配的。 至此我们的parser去标签数据清模块就完成了为了大家能够更好的理解下面是一张关系图 六、编写建立索引的模块 Index 首先创建 一个 Index.hpp 文件用来编写 索引模块 xasVM-16-15-ubuntu:~/Boost_Searcher$ touch index.hpp
该文件主要负责三件事① 构建索引 、② 正排索引 、③ 倒排索引
构建思路框图 搜索引擎逻辑 用户输入【关键字】搜索 → 【倒排索引】搜出 【倒排拉链】 → 倒排拉链中包含所有关键字有关的文档ID及其权重 → 【正派索引】文档ID得到文档三元素 并按照权重排列呈现给用户 节点设计 在【构建索引】模块时我们要构建出 正排索引 和 倒排索引正排索引是构建倒排索引的基础通过给到的关键字去倒排索引里查找出文档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中外键相当于两张表产生了关联 基本结构设计
Index类的基本框架 我们创建一个 Index类主要用来构建索引模块但是内部的细节还是比较多的暂时不多赘述索引模块最大的两个部分当然是构建正排索引和构建倒排索引其主要接口如下 #pragma once
#include iostream
#include string
#include vector
#include unordered_mapnamespace 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:// 正排索引的数据结构采用数组数组下标就是天然的文档ID// 每一个数组里面存放一个 文档信息std::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){//...return nullptr;}//根据倒排索引的关键字word获得倒排拉链InvertedList* GetInvertedList(const std::string word){//...return nullptr;}//根据去标签格式化后的文档构建正排和倒排索引 //将数据源的路径data/raw_html/raw.txt传给input即可这个函数用来构建索引bool BuildIndex(const std::string input){return true;}};
}获取正排索引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的文档内容
}
获取倒排拉链GetInvertedList GetInvertedList函数根据倒排索引的关键字word获得倒排拉链和上面类似 //根据倒排索引的关键字word获得倒排拉链
InvertedList* GetInvertedList(const std::string word)
{// word关键字不是在 unordered_map 中直接去里面找对应的倒排拉链即可auto iter inverted_index.find(word);if(iter inverted_index.end()) // 判断是否越界{std::cerr have no InvertedList std::endl;return nullptr;}// 返回 unordered_map 中的第二个元素--- 倒排拉链return (iter-second);
} 构建索引BuildIndex 显然这部分的难点就是如何构建索引而构建索引的思路正好和用户使用搜索功能的过程正好相反。 思路一个一个文档遍历为其每个构建先正排索引后构建倒排索引。 BuildIndex函数根据 去标签格式化后的.html 文档构建 正排 和 倒排索引 在编写这部分代码时稍微复杂一些我们要构建索引那我们应该是先把处理干净的文档读取上来是按行读取这样就能读到每个html文档按行读上来每个html文档后我们就可以开始构建正排索引和倒排索引此时就要提供两个函数分别为BuildForwardIndex构建正排索引和 BuildInvertedIndex构建倒排索引基本的代码如下 //根据去标签格式化后的文档构建正排和倒排索引
//将数据源的路径data/raw_html/raw.txt传给input即可这个函数用来构建索引
bool BuildIndex(const std::string input)
{// 要构建索引肯定先把我们之前处理好的 raw.txt 打开按行处理每一行就是一个.html 文件//在上面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;
}
构建正排索引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}};
} 构建正排索引的编写 // 构建正排索引 将拿到的一行html文件传输进来进行解析
// 构建的正排索引就是填充一个 DocInfo这个数据结构 然后将 DocInfo 插入 正排索引的 vector中即可
DocInfo* BuildForwardIndex(const std::string line)
{// 1. 解析 line 字符串的切分 分为 DocInfo 中的结构// 1. line - 3 个 string (title , content , url)std::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();
}
倒排索引的原理介绍 (重点) 建立倒排索引的原理我们之前只是单纯的说了一下没有详细的说明如何实现接下来我通过这张图来解释建立倒排索引的原理 总的思路 对 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; // 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;//倒排索引结构体 -- 一个倒排拉链节点
struct InvertedElem
{uint64_t doc_id; // 文档IDstd::string word; // 文档相关关键字int weight; // 文档权重
};//倒排拉链
typedef std::vectorInvertedElem InvertedList;至此就是倒排索引比较完善的原理介绍和代码思路。 cppjieba分词工具的安装和使用介绍 获取链接cppjieba 下载链接 里面有详细的教程 我们这里可以使用 git clone如下 创建一个test目录将我们 git clone 直接在 test 目录下进行
git clone https://github.com/yanyiwu/cppjieba查看 cppjieba 目录里面包含如下 我们待会儿需要用到的分词工具是在 include/cppjieba/jieba.hpp 首先这是别人的写好的一个开源项目里面会有这个测试代码通常是在test目录下 我们来做个分词演示先将这个demo.cpp拷贝到我们自己的test目录下 打开之后就是一堆错误主要原因是路径和链接不对 首先从上图可以看到头文件的路径就不对我们先来修改一下头文件的路径它本身是要使用cppjieba/Jieba.hpp的我们看一下这个头文件的具体路径 路径是cppjieba/include/cppjieba/Jieba.hpp 我们要在 test 目录下执行这个 demo.cpp就要引入这个头文件我们不能直接引入需要使用软连接 其次我们还要在 test 目录下执行这个 dict词库里面的内容要引入这个头文件我们不能直接引入需要使用软连接 软连接建立好后并修改 demo.hpp 的相应路径再将该包的头文件包起来再来查看demo.hpp是否还有错误 我们编译后发现limonp/Logging.hpp这个头文件没有 此时我们还是需要对这个头文件进行软连接我们通过查找发现有这么一个路径 但是里面什么东西都没有这是我在联系项目中出现的问题经过我去GitHub查找一番后发现它在另外一个压缩包里 解压好如下
unzip limonp-275bf555737689ebc4eee3938250f5ba0e5da0bd.zip 此时我们将它换一下名字并进入它里面看是否有我们需要的内容 此时我们只需要将它 拷贝到 include/cppjieba/ 下即可 我们将路径都完善之后接下来我们编译运行一下demo.hpp看下效果 测试代码
#include inc/cppjieba/Jieba.hpp
#include iostream
#include vector
#include string
using namespace std;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;int main(int argc, char** argv)
{cppjieba::Jieba jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH); vectorstring words; // 接收切分的词string s;s 小明硕士毕业于中国科学院计算所;cout 原句 s endl;jieba.CutForSearch(s, words);cout 分词后;for (auto word : words){cout word | ;}cout endl;
}
运行结果 上面的操作做完之后就可以在我们的项目中引入头文件来使用cppjieba分词工具啦 引入cppjieba到项目中 将软链接建立好之后我们在util.hpp中编写一个jieba分词的类主要是为了方便后期其他地方需要使用的时候可以直接调用。 我们在util.hpp中创建一个 JiebaUtil的分词工具类首先我们先看一下之前测试过的demo.cpp的代码 那么接下来就要在我们的项目路径中加入cppjieba下的Jieba.hpp操作和上面的类似这里我就不在操作了。直接看结果 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://输入文件名将文件内容读取到out中static bool ReadFile(const std::string file_path, std::string *out){// 读取 file_path一个.html文件 中的内容 -- 打开文件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(bool),getline的返回值istream会重载操作符bool读到文件尾eofset被设置并返回false//如何理解getline读取到文件结束呢getline的返回值是一个while(bool), 本质是因为重载了强制类型转化while(std::getline(in, line)) // 每循环一次读取的是文件的一行内容{*out line; // 将文件内容保存在 *out 里面}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);}
构建倒排索引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
基本结构 我们已经完成了 数据清洗、去标签和索引相关的工作接下来就是要编写服务器所提供的服务我们试想一下服务器要做哪些工作首先我们的数据事先已经经过了数据清洗和去标签的服务器运行起来之后应该要先去构建索引然后根据索引去搜索所以我们在Searcher模块中实现两个函数分别为InitSearcher()和Search()代码如下 首先创建一个 searcher.hpp 文件用来编写 搜索模块
touch searcher.hpp
#include index.hppnamespace ns_searcher
{class Searcher{private:ns_index::Index *index; //供系统进行查找的索引public:Searcher(){}~Searcher(){}public:void InitSearcher(const std::string input){//...// 获取或者创建index对象单例// 根据index对象建立索引}//query: 搜索关键字//json_string: 返回给用户浏览器的搜索结果void Search(const std::string query, std::string *json_string){//...//1.[分词]:对搜索关键字query在服务端也要分词然后查找index//2.[触发]:根据分词的各个词进行index查找//3.[合并排序]:汇总查找结果按照相关性权重weight降序排序//4.[构建]:将排好序的结果生成json串 —— jsoncpp}};
}
初始化服务InitSearcher 服务器 要去 构建索引本质上就是去构建一个 Index对象然后调用其内部的方法我们知道构建正排索引和倒排索引本质就是将磁盘上的数据加载的内存其数据量还是比较大的可能本项目的数据量不是很大。从这一点可以看出假设创建了多个Index对象的话其实是比较占内存的我们这里就可以将这个Index类设计成为单例模式关于单例模式是什么及代码框架懒汉模式和饿汉模式我这里不做详细介绍不了解的小伙伴可以去自行搜索也是比较简单的。 1. Index模块的单例设计
#pragma once
#include iostream
#include string
#include vector
#include unordered_map
#include fstream
#include mutex
#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:// 正排索引的数据结构采用数组数组下标就是天然的文档ID// 每一个数组里面存放一个 文档信息std::vectorDocInfo forward_index; //正排索引// 一个【关键字】可能出现在 无数个 【文档】中 我们需要根据权重判断 文档的重要顺序//倒排索引一定是一个关键字和一组或者一个InvertedElem对应[关键字和倒排拉链的映射关系]std::unordered_mapstd::string, InvertedList inverted_index;// 将 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){//如果这个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的文档内容}//根据倒排索引的关键字word获得倒排拉链InvertedList* GetInvertedList(const std::string word){// word关键字不是在 unordered_map 中直接去里面找对应的倒排拉链即可auto iter inverted_index.find(word);if(iter inverted_index.end()) // 判断是否越界{std::cerr have no InvertedList std::endl;return nullptr;}// 返回 unordered_map 中的第二个元素--- 倒排拉链return (iter-second);}//根据去标签格式化后的文档构建正排和倒排索引 //将数据源的路径data/raw_html/raw.txt传给input即可这个函数用来构建索引bool BuildIndex(const std::string input){// 要构建索引肯定先把我们之前处理好的 raw.txt 打开按行处理每一行就是一个.html 文件// 在上面SaveHtml函数中我们是以二进制的方式进行保存的那么读取的时候也要按照二进制的方式读取读取失败给出提示std::ifstream in(input, std::ios::in | std::ios::binary); // 读取input(raw.txt) 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;}private:// 构建正排索引 将拿到的一行html文件传输进来进行解析// 构建的正排索引就是填充一个 DocInfo这个数据结构 然后将 DocInfo 插入 正排索引的 vector中即可 DocInfo* BuildForwardIndex(const std::string line){// 1. 解析 line 字符串的切分 分为 DocInfo 中的结构// 1. line - 3 个 string (title , content , url)std::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){// 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_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; // 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
void InitSearcher(const std::string input)
{// 获取或者创建index对象单例index ns_index::Index::GetInstance(); // 根据index对象建立索引index-BuildIndex(input);
}
提供服务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 apt-get install -y libjsoncpp-dev 这里我之前下载过了已经是最新的版本了你们只需要输入上面的指令有这样的提示就表明安装成功了。 如何使用 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.cpp这个文件和我们项目文件关联性不大主要是用来调式需要将上文代码中备注调式的代码放开 #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版本 下载链接cpp-httplib 下载地址 我们将cpp-httplib放到项目中的test目录下并解压好 建立软连接到我们的项目路径下 注意要使用 cpp-httplib 我们的 gcc 的版本必须时 7 以上哦 至此我们就可以在我们的项目中使用了。 2. cpp-httplib的使用介绍 创建一个http_server.cpp的文件编写测试代码 #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 PARSERparser
DUGdebug
HTTP_SERVERhttp_server
cppg.PHONY:all
all:$(PARSER) $(DUG) $(HTTP_SERVER)$(PARSER):parser.cpp$(cpp) -o $ $^ -lboost_system -lboost_filesystem -stdc11
$(DUG):debug.cpp$(cpp) -o $ $^ -stdc11 -ljsoncpp
$(HTTP_SERVER):http_server.cpp$(cpp) -o $ $^ -stdc11 -ljsoncpp -lpthread
.PHONY:clean
clean:rm -f $(DUG) $(PARSER) $(HTTP_SERVER) 我们直接编译运行 http_server 打开浏览器访问我们这个端口如服务器IP:8080/hi,结果如下 但是当我们访问服务器IP:8080时却找不到对应的网页 像我们访问百度时www.baidu.com百度会给一个首页所以在我们的项目目录下呢也需要一个首页。 在项目路径下创建一个wwwroot目录目录中包含一个index.html文件 编写我们的首页并修改我们的 http_server.cpp !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串最后响应给用户 此时服务器启动成功索引也建立完毕 此时我们在浏览器进行访问服务器IP:8080/s 此时我们在浏览器进行访问服务器IP: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__进行编译的函数
*/ 假设在如下示例代码
int main() {LOG(NORMAL, This is a normal log message);LOG(WARNING, This is a warning log message);LOG(DEBUG, This is a debug log message);LOG(FATAL, This is a fatal log message);return 0;
}编译并运行这段代码后输出会类似于
[NORMAL][1691585012][This is a normal log message][main.cpp : 2]
[WARNING][1691585012][This is a warning log message][main.cpp : 3]
[DEBUG][1691585012][This is a debug log message][main.cpp : 4]
[FATAL][1691585012][This is a fatal log message][main.cpp : 5]所以我们可以将日志添加到有输出入口的地方方便监视我们的代码那里出现了问题。 日志系统的作用 调试和错误追踪记录程序执行过程中的各种状态和错误信息方便定位和修复问题。运行监控监控程序的运行状态了解程序的执行流程和重要事件。审计和分析分析日志记录了解用户行为和系统性能进行数据挖掘和改进。 十、编写前端模块 前端模块我做详细的解释代码中都有注释直接上代码 !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 由于不太美观不太熟悉前端所以用gpt调整了一个代码出来让我们的界面更加美观这个界面都可以使用大家自行选择 !DOCTYPE html
html langenheadmeta charsetUTF-8meta http-equivX-UA-Compatible contentIEedgemeta nameviewport contentwidthdevice-width, initial-scale1.0script srchttps://code.jquery.com/jquery-3.6.0.min.js/scripttitleBoost 库搜索引擎/titlestyle* {margin: 0;padding: 0;box-sizing: border-box;}html,body {height: 100%;background: url(https://images.unsplash.com/photo-1517430816045-df4b7de6d0e6) no-repeat center center fixed;background-size: cover;font-family: Arial, sans-serif;}.container {width: 90%;max-width: 800px;margin: 50px auto;padding: 20px;background-color: rgba(255, 255, 255, 0.9);box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);border-radius: 8px;}h1 {margin-bottom: 20px;font-size: 36px;color: #4e6ef2;text-align: center;}.search {display: flex;justify-content: center;position: relative;}.search input {flex: 1;height: 50px;border: 2px solid #ccc;padding-left: 10px;font-size: 17px;border-radius: 25px 0 0 25px;transition: border-color 0.3s;}.search input:focus {border-color: #4e6ef2;outline: none;}.search button {width: 160px;height: 50px;background-color: #4e6ef2;color: #fff;font-size: 19px;cursor: pointer;transition: background-color 0.3s;border: none;border-radius: 0 25px 25px 0;}.search button:hover {background-color: #3b5ec2;}.clear-btn {position: absolute;right: 170px;top: 50%;transform: translateY(-50%);cursor: pointer;font-size: 18px;display: none;color: #ccc;}.result {width: 100%;margin-top: 20px;}.result .item {margin-top: 15px;padding: 15px;background-color: #fff;border: 1px solid #ddd;border-radius: 5px;transition: box-shadow 0.3s;text-align: left;}.result .item:hover {box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);}.result .item a {display: block;text-decoration: none;font-size: 22px;color: #4e6ef2;margin-bottom: 5px;}.result .item a:hover {text-decoration: underline;}.result .item p {font-size: 16px;color: #333;margin-bottom: 5px;}.result .item i {display: block;font-style: normal;color: green;}.loader {border: 4px solid #f3f3f3;border-top: 4px solid #4e6ef2;border-radius: 50%;width: 40px;height: 40px;animation: spin 2s linear infinite;display: none;margin: 20px auto;}keyframes spin {0% {transform: rotate(0deg);}100% {transform: rotate(360deg);}}.error-message {color: red;text-align: center;margin-top: 20px;}.pagination {margin-top: 20px;display: flex;justify-content: center;}.pagination button {background-color: #4e6ef2;color: #fff;border: none;border-radius: 5px;padding: 10px 15px;margin: 0 5px;cursor: pointer;transition: background-color 0.3s;}.pagination button:hover {background-color: #3b5ec2;}.pagination button:disabled {background-color: #ccc;cursor: not-allowed;}.previous-searches {margin-top: 20px;}.previous-searches h2 {font-size: 20px;color: #4e6ef2;text-align: center;margin-bottom: 10px;}.previous-searches ul {list-style-type: none;text-align: center;}.previous-searches ul li {display: inline-block;margin: 5px 10px;padding: 5px 10px;background-color: #4e6ef2;color: #fff;border-radius: 5px;cursor: pointer;transition: background-color 0.3s;}.previous-searches ul li:hover {background-color: #3b5ec2;}/style
/headbodydiv classcontainerh1Boost库搜索引擎/h1div classsearchinput typetext placeholder输入搜索关键字... idsearchInputspan classclear-btn idclearBtntimes;/spanbutton onclickSearch()搜索一下/button/divdiv classloader idloader/divdiv classresult idresultContainer/divdiv classpagination idpaginationContainer/divdiv classerror-message iderrorMessage/divdiv classprevious-searches idpreviousSearchesh2之前的搜索/h2ul idpreviousSearchList/ul/div/divscriptlet currentPage 1;const resultsPerPage 8;let allResults [];let previousSearches JSON.parse(localStorage.getItem(previousSearches)) || [];$(document).ready(function () {$(#searchInput).on(input, function () {if ($(this).val()) {$(#clearBtn).show();} else {$(#clearBtn).hide();}});$(#clearBtn).on(click, function () {$(#searchInput).val();$(this).hide();});displayPreviousSearches();});function Search() {const query $(#searchInput).val().trim();if (!query) {alert(请输入搜索关键字);return;}if (!previousSearches.includes(query)) {if (previousSearches.length 5) {previousSearches.shift();}previousSearches.push(query);localStorage.setItem(previousSearches, JSON.stringify(previousSearches));}$(#loader).show();$(#errorMessage).text();$.ajax({type: GET,url: /s?word query,success: function (data) {$(#loader).hide();allResults data;currentPage 1;displayResults();},error: function () {$(#loader).hide();$(#errorMessage).text(搜索失败请稍后重试。);}});}function displayResults() {const resultContainer $(#resultContainer);const paginationContainer $(#paginationContainer);resultContainer.empty();paginationContainer.empty();const totalResults allResults.length;const totalPages Math.ceil(totalResults / resultsPerPage);if (totalResults 0) {$(#errorMessage).text(没有搜索到相关的内容。);return;}const start (currentPage - 1) * resultsPerPage;const end Math.min(start resultsPerPage, totalResults);const currentResults allResults.slice(start, end);currentResults.forEach(elem {const item $(div classitema href${elem.url} target_blank${elem.title}/ap${elem.desc}/pi${elem.url}/i/div);resultContainer.append(item);});displayPagination(totalPages);displayPreviousSearches();}function displayPagination(totalPages) {const paginationContainer $(#paginationContainer);if (currentPage 1) {const prevButton $(button上一页/button);prevButton.on(click, function () {currentPage--;displayResults();});paginationContainer.append(prevButton);}let startPage, endPage;if (totalPages 5) {startPage 1;endPage totalPages;} else {if (currentPage 3) {startPage 1;endPage 5;} else if (currentPage 2 totalPages) {startPage totalPages - 4;endPage totalPages;} else {startPage currentPage - 2;endPage currentPage 2;}}for (let i startPage; i endPage; i) {const button $(button${i}/button);if (i currentPage) {button.prop(disabled, true);}button.on(click, function () {currentPage i;displayResults();});paginationContainer.append(button);}if (currentPage totalPages) {const nextButton $(button下一页/button);nextButton.on(click, function () {currentPage;displayResults();});paginationContainer.append(nextButton);}}function displayPreviousSearches() {const previousSearchList $(#previousSearchList);previousSearchList.empty();previousSearches.forEach(search {const item $(li${search}/li);item.on(click, function () {$(#searchInput).val(search);Search();});previousSearchList.append(item);});}/script
/body/html十一、项目总结
项目的总体框架和功能
功能实现boost文档站内搜索引擎通过输入查询内容将与查询内容有关文档的网页按该词的权值降序显示出来包括标题、内容摘要和网页url通过点击标题可直接跳转boost库网页进行文档阅读。
框架 技术栈和项目环境
技术栈 c/c、c11、STL、准标准库boost、Jsoncpp、cppjieba、cpp-httplib 项目环境 Linux Ubuntu云服务器、vim/gcc(g)/Makefile、vscode gitee代码地址 https://gitee.com/xing-aosheng/linux/tree/master/Boost_Searcher 这里针对项目的扩展进行一下叙述 1. 建立整站搜索 我们搜索的内容是在boost库下的doc目录下的html文档你可以将这个库建立搜索也可以将所有的版本但是成本是很高的对单个版本的整站搜索还是可以完成的取决于你服务器的配置。 2. 设计一个在线更新的方案信号爬虫完成整个服务器的设计 我们在获取数据源的时候是我们手动下载的你可以学习一下爬虫写个简单的爬虫程序。采用信号的方式去定期的爬取。 3. 不使用组件而是自己设计一下对应的各种方案 我们在编写http_server的时候是使用的组件你可以自己设计一个简单的 4. 在我们的搜索引擎中添加竞价排名 我们在给用户反馈是提供的是json串显示到网页上有title、content和url就可以在构建json串时你加上你的博客链接将博客权重变高了就能够显示在第一个 5. 热次统计智能显示搜索关键词字典树优先级队列 6. 设置登陆注册引入对mysql的使用 十二、共勉 以下就是我对 【C综合项目】--基于Boost库的搜索引擎的理解如果有不懂和发现问题的小伙伴请在评论区说出来哦同时我还会继续更新对 【C】的综合项目 请持续关注我哦