boost搜索引擎
- 1. 项目背景
- 1.1 搜索引擎基本原理
- 1.2 Boost库
- 1.3 项目的目标
- 2. Boost搜索引擎宏观流程
- 3. 技术栈与环境
- 3.1 技术栈
- 3.2 环境
- 4. 认识什么是索引
- 4.1 正排索引
- 4.2 倒排索引
- 4.3 我们如何分词?
- 4.4 模拟查找过程
- 5. 数据处理
- 5.1 下载boost库到本地
- 5.2 认识标签
- 5.3 清除标签的整体框架
- 5.4 EnumFile函数的实现
- 5.5 ParseHtml函数的实现
- 5.5.1 实现读取文件内容的ReadFile函数
- 5.5.2 实现提取titile的函数ParseTitle
- 5.5.3 实现提取content的函数ParseContent
- 5.5.4 实现提取url函数ParseUrl
- 5.6 SaveHtml函数的实现
- 6. 建立索引
- 6.1 jieba的安装与使用
- 6.2 索引框架
- 6.3 BuildIndex函数的实现
- 6.3.1 建立正排索引函数BuildForwardIndex
- 6.3.2 建立倒排索引函数BuildInveredIndex
- 6.4 GetForwardIndex函数
- 6.5 GetInvertedList函数
- 6.6 将index设置成单例
- 7. 搜索引擎模块
- 7.1 InitSearcher函数
- 7.2 Search函数
- 7.3 jsoncpp安装与使用
- 7.4 搜索功能的测试
- 7.5 获取内容摘要
- 8. 搜索服务端
- 8.1 升级gcc版本
- 8.2 引入cpp-httplib库
- 8.3 测试cpp-httplib
- 8.4 设置根目录
- 8.5 编写搜索服务端
- 9. 前端代码
- 9.1 网页结构
- 9.2 网页样式
- 9.3 前后端交互
- 10. 项目补充
- 10.1 取重完善
- 10.2 添加日志
- 11. 项目拓展
- 11.1 摘要完善
- 11.2 后台部署服务
- 11.3 其他拓展
1. 项目背景
什么是搜索引擎呢?其实我们平常使用的百度就是搜索引擎,我们把自己想要搜索的内容输入进去,百度就会给我们返回相关的内容,百度一般给我们返回哪些内容呢?如下我们先来看一下。
1.1 搜索引擎基本原理
简单的了解一下搜索引擎的基本原理:
- 我们给服务器发起请求,例如搜索关键字”boost,服务器拿到请求之后,此时检索自己的资源,然后把结果构成响应发送给我们。
1.2 Boost库
boost库是一个经过千锤百炼、可移植、提供源代码的 C++ 库,作为标准库的后备。他的供能很强大,但是有一个小小的缺陷,它不支持搜索,例如我们想要搜索一个函数,cplusplus库是支持的。
boost库是不支持,但是不知道后面会不会支持。
1.3 项目的目标
下面我们就要说一下我们的项目的目标是什么了,很简单,我们给boost添加一个搜索的功能,这里要说一下,我们服务器上面说了,我们需要搜索资源,可以通过两个方式:
- 搜索其他的网页资源:这里需要使用爬虫,有一定的技术要求。
- 把boost下载下来,我们在本地搜索资源。
这里我们使用第二个方式,下载一下boost库。
2. Boost搜索引擎宏观流程
(1)整理数据
- 把boost库下载下来,此时我们想要把所有的后缀是html的文件进行处理,也就是整理数据(去标签)。如下先看一个简单的html文件,我们需要把其中的title(标题)、content(内容)、url(链接)内容进行保存。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"> Chapter 30. Boost.Process "stylesheet" href="../../doc/src/boostbook.css" type="text/css"> <meta name="generator" content="DocBook XSL Stylesheets V1.79.1"> "home" href="index.html" title="The Boost C++ Libraries BoostBook Documentation Subset"> "up" href="libraries.html" title="Part I. The Boost C++ Libraries (BoostBook Subset)"> "prev" href="poly_collection/acknowledgments.html" title="Acknowledgments"> "next" href="boost_process/concepts.html" title="Concepts">比特就业课 <body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"> <table cellpadding="2" width="100%"> <td valign="top"> 3. 技术栈与环境
3.1 技术栈
后端:C/C++、C++11、STL、boost标准库、Jsoncpp、cppjieba、cpp-httplib。
前端:html5、css、js、jQuery、Ajax。
3.2 环境
- Centos7远端服务器、vim、gcc(g++)、Makefile、Vscode。
4. 认识什么是索引
下面我们就要了解什么是索引?这里很简单,我们给文档上编号,我们可以根据编号找到唯一确定的文,这就是索引的基本的原理,不过这里的索引分为正排索引和倒排索引。
- 正排索引:根据编号找到文件,这里的结果是唯一的。
- 倒排索引:根据关键字,找到文件id,结果不是唯一的。
可以通过一个例子来理解,这里有两个文件:
4.1 正排索引
我们对每一个文件进行编:
文档ID 文档名称 文档内容 1 文档A 我的手机牌子是华为的 2 文档B 我的手机牌子是小米的
这里的正派索引很简单,我们根据文档编号,直接就可以找到文档的内容。
4.2 倒排索引
我们把每一个文档都进行分词,拿出来不重复的词,对于每一个不重复的词,下面都挂着我们的文档的编号。
关键字(具有唯一性) 文档ID 我的 1,2 手机 1,2 牌子 1,2 华为 1 小米 1
倒排索引就是根据关键字拿到我们的文档I。
4.3 我们如何分词?
上面我们说了把文档进行分词,那么我们为何要分词?分词是为了提高查找的效率,那么我们该如何分词呢?我们可以自己手动词,但是已经有大佬给我们编写好了一个库,我们直接使用就可以了,但是如果我们手动分?这里该如何分,很简单.
- 我的手机牌子是华为的:我的/手机/牌子/是/华为/的。
- 我的手机牌子是小米的:我的/手机/牌子/是/小米/的。
注意:上面的分词我们随意分的,不一定就是这样的,不过这里我们要谈一下我们一个提高效率的方法。我们发现一个文旦里面的了" 、“从” 、 “吗” , “the” 、 “a” 有的时候意义不是很大,那么我们在分词的时候就可以直接忽略,可以提高我们的效率,像这一种词,我们称为停止词。
4.4 模拟查找过程
下面我们模拟一下查找的流程的:
用户输入:我的 -> 倒排索引中查找 -> 提取出文档ID(1,2) -> 根据正排索引 -> 找到文档的内容 ->title+conent(desc)+url 文档结果进行摘要->构建响应结果。
5. 数据处理
5.1 下载boost库到本地
(1)我们先下载一下boost库,直接使用最新版本的,我这里是1.84.0.我们下载到桌面,然后在centos7下使用指令rz传入远端服务器当中,然后解压一下就可以了。
[xiaomaker@VM-28-13-centos data]$ rz -E[xiaomaker@VM-28-13-centos data]$ lltotal 141756-rw-r--r-- 1 xiaomaker xiaomaker 145151722 Feb 13 17:54 boost_1_84_0.tar.gz[xiaomaker@VM-28-13-centos data]$ tar xzf boost_1_84_0.tar.gz [xiaomaker@VM-28-13-centos data]$ lltotal 141760drwxr-xr-x 8 xiaomaker xiaomaker4096 Dec7 05:37 boost_1_84_0-rw-r--r-- 1 xiaomaker xiaomaker 145151722 Feb 13 17:54 boost_1_84_0.tar.gz[xiaomaker@VM-28-13-centos data]$
(2)查看一下boost库的内容:
这里面就是我们boost库的全部内容,为了我们的项目简单一些,这里我们使用boost里面的doc里面的html目录下的的html文件。如果我们想要搭建所有的html文件,后面可以慢慢的完善。
boost_1_84_0/doc/html
[xiaomaker@VM-28-13-centos doc]$ cd html[xiaomaker@VM-28-13-centos html]$ lltotal 3080-rw-r--r--1 xiaomaker xiaomaker 3476 Dec7 05:22 about.htmldrwxr-xr-x2 xiaomaker xiaomaker 4096 Dec7 05:23 accumulators-rw-r--r--1 xiaomaker xiaomaker 5858 Dec7 05:23 accumulators.htmldrwxr-xr-x2 xiaomaker xiaomaker 4096 Dec7 05:23 align-rw-r--r--1 xiaomaker xiaomaker 4440 Dec7 05:23 align.htmldrwxr-xr-x2 xiaomaker xiaomaker 4096 Dec7 05:23 any-rw-r--r--1 xiaomaker xiaomaker 9102 Dec7 05:23 any.htmldrwxr-xr-x3 xiaomaker xiaomaker 4096 Dec7 05:23 array-rw-r--r--1 xiaomaker xiaomaker 8377 Dec7 05:23 array.html-rw-r--r--1 xiaomaker xiaomaker36597 Dec7 05:27 array_types.html-rw-r--r--1 xiaomaker xiaomaker 288197 Dec7 05:26 asio_HTML.manifest-rw-r--r--1 xiaomaker xiaomaker 6685 Dec7 05:32 Assignable.html-rw-r--r--1 xiaomaker xiaomaker700 Dec7 05:02 atomic.html-rw-r--r--1 xiaomaker xiaomaker20627 Dec7 05:27 auxiliary.htmldrwxr-xr-x2 xiaomaker xiaomaker 4096 Dec7 05:02 bbv2-rw-r--r--1 xiaomaker xiaomaker640 Dec7 05:02 bbv2.html-rw-r--r--1 xiaomaker xiaomaker 9635 Dec7 05:32 BidirectionalIterator.htmldrwxr-xr-x 39 xiaomaker xiaomaker 4096 Dec7 05:32 boost...
(3)下面我们要做的就是就是把boost_1_84_0/doc/html里面的所有内容保存到data/input文件中。
[xiaomaker@VM-28-13-centos boost_searcher]$ mkdir data/input -p[xiaomaker@VM-28-13-centos boost_searcher]$ cp -rf ../../data/boost_1_84_0/doc/html/* data/input/
这样就成功将boost_1_84_0/doc/html的内容拷贝到了data/input当中。
接下来就可以去标签了,创建一个.cpp文件编写parser。
[xiaomaker@VM-28-13-centos boost_searcher]$ touch parser.cpp
5.2 认识标签
(1)在谈去标签之前,我们需要先认识一下标签,我们随便打开的一个html文件。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"><meta http-equiv="Content-Type" content="text/html; charset=UTF-8">Chapter 45. Boost.YAP "stylesheet" href="../../doc/src/boostbook.css" type="text/css"><meta name="generator" content="DocBook XSL Stylesheets V1.79.1">"home" href="index.html" title="The Boost C++ Libraries BoostBook Documentation Subset">"up" href="libraries.html" title="Part I. The Boost C++ Libraries (BoostBook Subset)">"prev" href="xpressive/appendices.html" title="Appendices">"next" href="boost_yap/manual.html" title="Manual"><meta name="viewport" content="width=device-width, initial-scale=1"><body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF"><table cellpadding="2" width="100%"><td valign="top"> (3)这里我们可以对每一个源html都创建一个文件,但是这样文件就非常多了,不如我们把所有的文档去标签之后结果放在一个文件中,文件与文件之间使用’\3’隔开。就像下面的格式:
XXXXXXXXXXXXXXXXX\3YYYYYYYYYYYYYYYYYYYYY\3ZZZZZZZZZZZZZZZZZZZZZZZZZ\3
解释一下为什么使用’\3’为分隔符:因为在ASCII表中,控制字符是不可显示字符,即无法打印。在我们获取的文档内容(即data/input中的html网页文件)中,里面基本上都是可打印字符,基本上不会有不可显示的控制字符。如此以来也就不会污染我们的文档内容啦。
不过我们不使用上面的格式,这里我们想办法把一个文档的’\n’全部去掉。然后我们使用这样的格式:
类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3url
(4)我们创建一个文件来保存我们去标签之后的内容。
[xiaomaker@VM-28-13-centos data]$ cd raw_html/[xiaomaker@VM-28-13-centos raw_html]$ touch raw.txt[xiaomaker@VM-28-13-centos raw_html]$ lltotal 0-rw-rw-r--. 1 xiaomaker xiaomaker 0 Feb 14 20:17 raw.txt
5.3 清除标签的整体框架
(1)parser.cpp的简单框架:
#include #include #include #include const std::string src_path = "data/input"; //是一个目录,里面放的是所有的html网页const std::string output = "data/raw_html/raw.txt";//将上面的网页去标签后存放在该目录下typedef struct DocInfo{std::string title;//文档的标题std::string content;//文档内容std::string url;//读文档在官网中的url}DocInfo_t;//函数声明bool EnumFile(const std::string& src_path, std::vector<std::string>& file_list);bool ParseHtml(const std::vector<std::string>& file_list, std::vector<DocInfo_t>& results);bool SaveHtml(const std::vector<DocInfo_t>& results, const std::string& output);int main(){std::vector<std::string> file_list;//第一步:递归式的把每个html文件名带路径保存到file_list当中,方便后期进行一个一个文件进行读取if(!EnumFile(src_path, file_list)){std::cerr << "enum file name error" << std::endl;return 1;}//第二步:按照file_list读取每个文件的内容,并进行解析std::vector<DocInfo_t> results;if(!ParseHtml(file_list, results)){std::cerr << "parse html error" << std::endl;return 2;}//第三步:把解析完毕的文件写入到output,按照\3作为每个文档的分隔符if(!SaveHtml(results, output)){std::cerr << "save html error" << std::endl;return 3;}return 0;}
基本思路:
- 拿到我们所有的源html文件名,然后把这些文件名保存在一个数组中。
- 依次遍历数组,把文件进行去标签,然后把去掉的内容整理成一个DocInfo_t结构体,里面保存title、content、url的结果并存放到一个数组中。
- 遍历结构体数组,然后把内容写入到我们的目的文件中,按照一定的格式。
(2)实现如上三个函数我们可以安装boost库,使用boost当中的接口实现:
[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install -y boost-devel[sudo] password for xiaomaker:
简单认识一下boost库,如下是使用手册:
5.4 EnumFile函数的实现
EnumFil函数的功能是把我们给定src_path目录下的所有后缀是html的文件名字给保存下了,存在在一个file_list数组当中。
bool EnumFile(const std::string& src_path, std::vector<std::string>& file_list);
(1)具体的实现:
bool EnumFile(const std::string& src_path, std::vector<std::string>& file_list)//第一步{namespace fs = boost::filesystem;fs::path root_path(src_path);if(!fs::exists(root_path))//判断路径是否存在{ std::cerr << src_path << "not exists" << std::endl;return false;}//定义一个空的迭代器,用来进行判断递归结束fs::recursive_directory_iterator end;for(fs::recursive_directory_iterator iter(root_path); iter != end; ++iter){if(!fs::is_regular_file(*iter))//判断文件是否是普通文件.html文件都是普通文件{continue;}if(iter->path().extension() != ".html") //判断是否是以.html结尾{continue;}//std::cout << "debug: " <path().string() << std::endl;//当前路径是合法的,以.html结束的普通文件file_list.push_back(iter->path().string());//将所有带路径的html保存到file_list。方便后续进行文本分析}return true;}
(2)代码解析:
(3)运行一下看看结果是否正确:
[xiaomaker@VM-28-13-centos boost_searcher]$ ./parser debug: data/input/about.htmldebug: data/input/accumulators/user_s_guide.htmldebug: data/input/accumulators/acknowledgements.htmldebug: data/input/accumulators/reference.htmldebug: data/input/accumulators.html...
如上就说明成功了。
5.5 ParseHtml函数的实现
我们要开始解析我们的每一个html目录。
bool ParseHtml(const std::vector<std::string>& file_list, std::vector<DocInfo_t>& results);
(1)如下是ParseHtml函数的框架:
bool ParseHtml(const std::vector<std::string>& file_list, std::vector<DocInfo_t>& results)//第二步{for(auto& file : file_list){//1. 读取文件. Read();std::string result;if(!ns_util::FileUtil::ReadFile(file, result)){continue;}DocInfo_t doc;//2. 解析指定的文件, 提取titleif(!ParseTitle(result, &doc.title)){continue;}//3. 解析指定的文件, 提取contentif(!ParseContent(result, &doc.content)){continue;}//4. 解析指定的文件, 构建urlif(!ParseUrl(file, &doc.url)){continue;}//完成了解析任务,当前文档的相关结果都保存在了doc当中results.push_back(std::move(doc));//效率可能会很低,加上move就是移动构造,减少拷贝//ShowDoc(doc);//break;}return true;}
(2)ParseHtml函数的大致流程:
- 对于每一个文件,我们把它读取到一个字符串中。
- 根据字符串拿到title。
- 根据字符串拿到content。
- 根据字符串拿到url。
下面我们分别实现这些函数的功能。
5.5.1 实现读取文件内容的ReadFile函数
(1)对于该函数,我们可以把它放在一个工具集(util.hpp)当中,因为后面可能其它函数会使用到。
#pragma once#include #include #include #include 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()){LOG(FATAL, "open file" + file_path + "error");//std::cerr << "open file" << file_path << "error" << std::endl;return false;}std::string line;while(std::getline(in, line))//如何判断一个文件读取结束呢??getline的返回值是&,但是whlie(bool),那是因为重载了强转{out += line;}in.close();return true;}};}
(2)代码解析:
5.5.2 实现提取titile的函数ParseTitle
(1)看看html文件的内容,title是在一个标签里面。
(2)根据字符串来进行提取title。
bool ParseTitle(const std::string& file, std::string* title)//解析指定的文件, 提取title{size_t begin = file.find("" );if(begin == std::string::npos){LOG(FATAL, "获取字符串失败" );return false;}size_t end = file.find("");if(end == std::string::npos){LOG(FATAL, "获取字符串失败");return false;}begin += std::string("" ).size();if(begin > end){LOG(FATAL, "获取的下标不正确");return false;}*title = file.substr(begin, end - begin);return true;}
(3)代码解析:
5.5.3 实现提取content的函数ParseContent
我们获取content,不是把所有的内容都拿出来,而是要去标签,这里需要借助一个状态。
(1)我们知道标签是这样的表示。那么我们这里使用一个状态。我们默认第一个字符是<。
static bool ParseContent(const std::string& file, std::string* content)//解析指定的文件, 提取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;}
(2)代码解析:
5.5.4 实现提取url函数ParseUrl
(1)boost库的官方文档,和我们下载下来的文档,是有路径的对应关系的。如下:
官网url:https://www.boost.org/doc/libs/1_84_0/doc/html/accumulators.html
我们下载下来的url:boost_1_84_0/doc/html/accumulators.html
我们拷贝到我们项目中的样例:data/input/accumulators.html
我们把下载下来的boost库当中的 doc/html/* 拷贝到了data/input/
url_head = “https://www.boost.org/doc/libs/1_84_0/doc/html”;这是固定的
url_tail = data/input /accumulators.html 转换成 url_tail = /accumulators.html
url = url_head + url_tail就相当于形成了一个官网链接。
(2)具体实现:
static bool ParseUrl(const std::string& file_path, std::string* url)//解析指定的文件, 构建url{std::string url_head = "https://www.boost.org/doc/libs/1_84_0/doc/html";std::string url_tail = file_path.substr(src_path.size());*url = url_head + url_tail;return true;}
(3)代码解析:
(4)使用如下函数检测是否构建成功:
void ShowDoc(DocInfo_t& doc){std::cout << "title: " << doc.title << std::endl;std::cout << "content: " << doc.content << std::endl;std::cout << "url: " << doc.url << std::endl;}
(5)测试结果:
title: Struct template result<This(InputIterator, InputIterator)>content: Struct template result<This(InputIterator, InputIterator)>HomeLibrariesPeopleFAQMoreStruct template result<This(InputIterator, InputIterator)>boost::proto::functional::distance::result<This(InputIterator, InputIterator)>Synopsis// In header: <boost/proto/functional/std/iterator.hpp>template<typename This, typename InputIterator> struct result<This(InputIterator, InputIterator)> {// typestypedef typename std::iterator_traits<typename boost::remove_const<typename boost::remove_reference<InputIterator>::type>::type>::difference_type type;};Copyright © 2008 Eric NieblerDistributed under the Boost Software License, Version 1.0. (See accompanyingfile LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)url: https://www.boost.org/doc/libs/1_84_0/doc/html/boost/proto/functional/distance/resu_1_3_32_5_26_2_1_1_2_4.html
(6)我们可以将拿到的这个url去官网上看看是不是正确的:
5.6 SaveHtml函数的实现
bool SaveHtml(const std::vector<DocInfo_t>& results, const std::string& output);
(1)我们已经得到每一个文件的结构体了,现在开始保存文件到要求的文件当中:
bool SaveHtml(const std::vector<DocInfo_t>& results, const std::string& output)//第三步{#define SEP '\3'//按照二进制写入std::ofstream out(output, std::ios::out | std::ios::binary);if(!out.is_open()){LOG(FATAL, "open: " + output + "failed");//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;}
(2)代码解析:
(3)验证是否保存成功:
(4)验证是否保存完全:
[xiaomaker@VM-28-13-centos boost_searcher]$ ls ./data/input/ -Rl | grep -E "*.html" | wc -l8586[xiaomaker@VM-28-13-centos boost_searcher]$ cat ./data/raw_html/raw.txt | wc -l8586[xiaomaker@VM-28-13-centos boost_searcher]$
6. 建立索引
下面我们就要建立索引了,建立索引实际上就是构建存储+搜索的数据结构,来加快我们对于关键字->文档ID->文档内容的搜索过程。根据上面了解,我们建立正派索引和倒排索引。
在建立索引之前我们需要安装jieba这个分词工具来帮助我们分词。
6.1 jieba的安装与使用
(1)对于分词,我们可以直接使用cppjieba分词工具即可。我们执行下面的命令将github上面的jieba库下载到本地。
[xiaomaker@VM-28-13-centos jieba]$ git clone git clone https://gitcode.net/qq_55172408/cppjieba.git
如下是cppjieba的具体内容:
[xiaomaker@VM-28-13-centos jieba]$ tree cppjieba/cppjieba/├── appveyor.yml├── ChangeLog.md├── CMakeLists.txt├── deps│ ├── CMakeLists.txt│ ├── gtest│ │ ├── CMakeLists.txt│ │ ├── include│ │ │ └── gtest│ │ │ ├── gtest-death-test.h│ │ │ ├── gtest.h│ │ │ ├── gtest-message.h│ │ │ ├── gtest-param-test.h│ │ │ ├── gtest-param-test.h.pump│ │ │ ├── gtest_pred_impl.h│ │ │ ├── gtest-printers.h│ │ │ ├── gtest_prod.h│ │ │ ├── gtest-spi.h│ │ │ ├── gtest-test-part.h│ │ │ ├── gtest-typed-test.h│ │ │ └── internal│ │ │ ├── gtest-death-test-internal.h│ │ │ ├── gtest-filepath.h│ │ │ ├── gtest-internal.h│ │ │ ├── gtest-linked_ptr.h│ │ │ ├── gtest-param-util-generated.h│ │ │ ├── gtest-param-util-generated.h.pump│ │ │ ├── gtest-param-util.h│ │ │ ├── gtest-port.h│ │ │ ├── gtest-string.h│ │ │ ├── gtest-tuple.h│ │ │ ├── gtest-tuple.h.pump│ │ │ ├── gtest-type-util.h│ │ │ └── gtest-type-util.h.pump│ │ └── src│ │ ├── gtest-all.cc│ │ ├── gtest.cc│ │ ├── gtest-death-test.cc│ │ ├── gtest-filepath.cc│ │ ├── gtest-internal-inl.h│ │ ├── gtest_main.cc│ │ ├── gtest-port.cc│ │ ├── gtest-printers.cc│ │ ├── gtest-test-part.cc│ │ └── gtest-typed-test.cc│ └── limonp│ ├── ArgvContext.hpp│ ├── BlockingQueue.hpp│ ├── BoundedBlockingQueue.hpp│ ├── BoundedQueue.hpp│ ├── Closure.hpp│ ├── Colors.hpp│ ├── Condition.hpp│ ├── Config.hpp│ ├── FileLock.hpp│ ├── ForcePublic.hpp│ ├── LocalVector.hpp│ ├── Logging.hpp│ ├── Md5.hpp│ ├── MutexLock.hpp│ ├── NonCopyable.hpp│ ├── StdExtension.hpp│ ├── StringUtil.hpp│ ├── Thread.hpp│ └── ThreadPool.hpp├── dict│ ├── hmm_model.utf8│ ├── idf.utf8│ ├── jieba.dict.utf8│ ├── pos_dict│ │ ├── char_state_tab.utf8│ │ ├── prob_emit.utf8│ │ ├── prob_start.utf8│ │ └── prob_trans.utf8│ ├── README.md│ ├── stop_words.utf8│ └── user.dict.utf8├── include│ └── cppjieba│ ├── DictTrie.hpp│ ├── FullSegment.hpp│ ├── HMMModel.hpp│ ├── HMMSegment.hpp│ ├── Jieba.hpp│ ├── KeywordExtractor.hpp│ ├── limonp│ │ ├── ArgvContext.hpp│ │ ├── BlockingQueue.hpp│ │ ├── BoundedBlockingQueue.hpp│ │ ├── BoundedQueue.hpp│ │ ├── Closure.hpp│ │ ├── Colors.hpp│ │ ├── Condition.hpp│ │ ├── Config.hpp│ │ ├── FileLock.hpp│ │ ├── ForcePublic.hpp│ │ ├── LocalVector.hpp│ │ ├── Logging.hpp│ │ ├── Md5.hpp│ │ ├── MutexLock.hpp│ │ ├── NonCopyable.hpp│ │ ├── StdExtension.hpp│ │ ├── StringUtil.hpp│ │ ├── Thread.hpp│ │ └── ThreadPool.hpp│ ├── MixSegment.hpp│ ├── MPSegment.hpp│ ├── PosTagger.hpp│ ├── PreFilter.hpp│ ├── QuerySegment.hpp│ ├── SegmentBase.hpp│ ├── SegmentTagged.hpp│ ├── TextRankExtractor.hpp│ ├── Trie.hpp│ └── Unicode.hpp├── README_EN.md├── README.md└── test├── CMakeLists.txt├── demo.cpp├── load_test.cpp├── testdata│ ├── curl.res│ ├── extra_dict│ │ └── jieba.dict.small.utf8│ ├── gbk_dict│ │ ├── hmm_model.gbk│ │ └── jieba.dict.gbk│ ├── jieba.dict.0.1.utf8│ ├── jieba.dict.0.utf8│ ├── jieba.dict.1.utf8│ ├── jieba.dict.2.utf8│ ├── load_test.urls│ ├── review.100│ ├── review.100.res│ ├── server.conf│ ├── testlines.gbk│ ├── testlines.utf8│ ├── userdict.2.utf8│ ├── userdict.english│ ├── userdict.utf8│ └── weicheng.utf8└── unittest├── CMakeLists.txt├── gtest_main.cpp├── jieba_test.cpp├── keyword_extractor_test.cpp├── pos_tagger_test.cpp├── pre_filter_test.cpp├── segments_test.cpp├── textrank_test.cpp├── trie_test.cpp└── unicode_test.cpp17 directories, 136 files[xiaomaker@VM-28-13-centos jieba]$
这里我们只需要关注的是两个文件:
- cppjieba/include:头文件。
- cppjiba/dict:字典。
(2)下面我们了解jieba分词的使用,里面存在一个demo.cpp文件供我们测试。
[xiaomaker@VM-28-13-centos test]$ pwd/home/xiaomaker/code_cpp/jieba/cppjieba/test[xiaomaker@VM-28-13-centos test]$ lltotal 20-rw-rw-r-- 1 xiaomaker xiaomaker148 Feb 14 14:02 CMakeLists.txt-rw-rw-r-- 1 xiaomaker xiaomaker 2797 Feb 14 14:02 demo.cpp-rw-rw-r-- 1 xiaomaker xiaomaker 1532 Feb 14 14:02 load_test.cppdrwxrwxr-x 4 xiaomaker xiaomaker 4096 Feb 14 14:02 testdatadrwxrwxr-x 2 xiaomaker xiaomaker 4096 Feb 14 14:02 unittest[xiaomaker@VM-28-13-centos test]$
①我们不能直接编译,它会报错。
[xiaomaker@VM-28-13-centos test]$ g++ demo.cpp demo.cpp:1:30: fatal error: cppjieba/Jieba.hpp: No such file or directory #include "cppjieba/Jieba.hpp"^~~~~~~~~~~~~~~~~~~~ ^compilation terminated.[xiaomaker@VM-28-13-centos test]$
这是因为我们这里的库和头文件的路径是不对的,这里添加软链接即可。链接的路径是自己下载jieba的路径。
[xiaomaker@VM-28-13-centos test]$ ln -s ~/code_cpp/jieba/cppjieba/include/ include[xiaomaker@VM-28-13-centos test]$ ln -s ~/code_cpp/jieba/cppjieba/dict/ dict[xiaomaker@VM-28-13-centos test]$ lltotal 20-rw-rw-r-- 1 xiaomaker xiaomaker148 Feb 14 14:02 CMakeLists.txt-rw-rw-r-- 1 xiaomaker xiaomaker 2853 Feb 24 12:16 demo.cpplrwxrwxrwx 1 xiaomaker xiaomaker 45 Feb 24 12:18 dict -> /home/xiaomaker/code_cpp/jieba/cppjieba/dict/lrwxrwxrwx 1 xiaomaker xiaomaker 48 Feb 24 12:17 include -> /home/xiaomaker/code_cpp/jieba/cppjieba/include/-rw-rw-r-- 1 xiaomaker xiaomaker 1532 Feb 14 14:02 load_test.cppdrwxrwxr-x 4 xiaomaker xiaomaker 4096 Feb 14 14:02 testdatadrwxrwxr-x 2 xiaomaker xiaomaker 4096 Feb 14 14:02 unittest[xiaomaker@VM-28-13-centos test]$
②接下来就需要修改demo.cpp的头文件。
③我们继续编译,我们发现还是出现错误。
[xiaomaker@VM-28-13-centos test]$ g++ demo.cpp In file included from include/cppjieba/Jieba.hpp:4, from demo.cpp:1:include/cppjieba/QuerySegment.hpp:7:10: fatal error: limonp/Logging.hpp: No such file or directory #include "limonp/Logging.hpp"^~~~~~~~~~~~~~~~~~~~compilation terminated.
这是因为找不到limonp/Logging.hpp文件。这时候我们只需要将deps/limonp目录拷贝到include/cppjieba当中即可。
[xiaomaker@VM-28-13-centos cppjieba]$ cp deps/limonp/ include/cppjieba/ -rf
④这样我们就可以编译通过了:
[xiaomaker@VM-28-13-centos test]$ g++ demo.cpp[xiaomaker@VM-28-13-centos test]$ lltotal 460-rwxrwxr-x 1 xiaomaker xiaomaker 447008 Feb 24 12:46 a.out-rw-rw-r-- 1 xiaomaker xiaomaker148 Feb 14 14:02 CMakeLists.txt-rw-rw-r-- 1 xiaomaker xiaomaker 2861 Feb 24 12:20 demo.cpplrwxrwxrwx 1 xiaomaker xiaomaker 45 Feb 24 12:18 dict -> /home/xiaomaker/code_cpp/jieba/cppjieba/dict/lrwxrwxrwx 1 xiaomaker xiaomaker 48 Feb 24 12:17 include -> /home/xiaomaker/code_cpp/jieba/cppjieba/include/-rw-rw-r-- 1 xiaomaker xiaomaker 1532 Feb 14 14:02 load_test.cppdrwxrwxr-x 4 xiaomaker xiaomaker 4096 Feb 14 14:02 testdatadrwxrwxr-x 2 xiaomaker xiaomaker 4096 Feb 14 14:02 unittest[xiaomaker@VM-28-13-centos test]$ ./a.out 他来到了网易杭研大厦[demo] Cut With HMM他/来到/了/网易/杭研/大厦[demo] Cut Without HMM 他/来到/了/网易/杭/研/大厦我来到北京清华大学[demo] CutAll我/来到/北京/清华/清华大学/华大/大学小明硕士毕业于中国科学院计算所,后在日本京都大学深造[demo] CutForSearch小明/硕士/毕业/于/中国/科学/学院/科学院/中国科学院/计算/计算所/,/后/在/日本/京都/大学/日本京都大学/深造[xiaomaker@VM-28-13-centos test]$
6.2 索引框架
(1)创建index.hpp文件后,我们要建立正排和倒排索引,并且我们还要提供查找的接口。index.hpp的整体框架如下:
#pragma once#include #include #include #include #include #include "util.hpp"namespace ns_index{struct DocInfo{std::string title; // 文档的标题std::string content; // 文档内容std::string url; // 读文档在官网中的urluint64_t doc_id;//文档id};struct InvertedElem{int doc_id;std::string word;int weight;};typedef std::vector<InvertedElem> InvertedList_t; //倒排拉链class Index{public:Index(){}DocInfo* GetForwardIndex(uint64_t doc_id)//正排索引:根据doc_id找到文档内容{return nullptr;}InvertedList_t* GetInveredList(std::string& word)//倒排索引:根据关键字获取倒排拉链{return nullptr;}//根据去标签,格式化之后的文档,构建正排索引和倒排索引 bool BuildIndex(const std::string& input)//parse处理完毕的数据交给我{return true;}~Index(){}private:DocInfo* BuildForwardIndex(std::string& line){return nullptr;}bool BuildInveredIndex(const DocInfo& doc){return true;}//正排索引使用数组,数组的下标就是文档idstd::vector<DocInfo> forward_index;//正排索引//倒排索引一定是一个关键字和一组InvertedElem对应std::unordered_map<std::string, InvertedList_t> inverted_index;//倒排索引};}
下面我们依次实现index.hpp里面的函数。
6.3 BuildIndex函数的实现
bool BuildIndex(const std::string& input);
(1)根据我们已经处理好的数据,通过它来构建索引。
bool BuildIndex(const std::string& input)//parse处理完毕的数据交给我{std::fstream in(input, std::ios::in | std::ios::binary);if (!in.is_open()){LOG(FATAL, "sorry" + input + "error");//std::cerr << "sorry" << input << "error" << std::endl;return false;}std::string line;int count = 0;while (std::getline(in, line)){DocInfo* doc = BuildForwardIndex(line);if (doc == nullptr){LOG(WARNING, "build" + line + "error");//std::cerr << "build" << line << "error" << std::endl;continue;}BuildInveredIndex(*doc);count++;if (count % 50 == 0){LOG(NORMAL, "当前已经建立索引文档: " + std::to_string(count));//std::cout << "当前已经建立索引文档:" << count << std::endl;}}return true;}
(2)代码解析:
6.3.1 建立正排索引函数BuildForwardIndex
(1)这个非常容易实现,因为我们数组下标天然是我们的文档ID,只需要把处理后每一个文档的内容处理成结构体,然后添加到数组中就可以了。
DocInfo* BuildForwardIndex(std::string& line){//1. 解析line, 字符串切分std::vector<std::string> results;std::string sep = "\3";ns_util::StringUtil::Cutstring(line, results, sep);if (results.size() != 3)//当切分的字符串不是3说明切割出现错误{LOG(FATAL, "build error");return nullptr;}//2. 字符串进行填充到DocInfoDocInfo doc;doc.title = results[0];//titledoc.content = results[1];//contentdoc.url = results[2];//urldoc.doc_id = forward_index.size(); //文档id//3. 插入到vector当中forward_index.push_back(std::move(doc));return &forward_index.back();}
(2)将Cutstring函数写到工具集(util.hpp)当中。split是boost库当中的接口。
namespace ns_util{class StringUtil{public:static void Cutstring(const std::string& target, std::vector<std::string>& out, std::string sep){boost::split(out, target, boost::is_any_of(sep), boost::token_compress_on);}};}
6.3.2 建立倒排索引函数BuildInveredIndex
(1)我们开始根据最新的结构体建立倒排索引,这里就需要我进行分词了。也就需要引入jieba库帮助我们分词。使用软连接将下载好的jieba路径链接到项目当中:
[xiaomaker@VM-28-13-centos boost_searcher]$ ln -s /home/xiaomaker/code_cpp/jieba/cppjieba/include/cppjieb cppjieba[xiaomaker@VM-28-13-centos boost_searcher]$ ln -s /home/xiaomaker/code_cpp/jieba/cppjieba/dict/ cppjieba[xiaomaker@VM-28-13-centos boost_searcher]$ lltotal 24lrwxrwxrwx 1 xiaomaker xiaomaker 57 Feb 14 14:04 cppjieba -> /home/xiaomaker/code_cpp/jieba/cppjieba/include/cppjieba/drwxrwxr-x 4 xiaomaker xiaomaker 4096 Feb 13 18:10 datalrwxrwxrwx 1 xiaomaker xiaomaker 45 Feb 14 14:08 dict -> /home/xiaomaker/code_cpp/jieba/cppjieba/dict/-rw-rw-r-- 1 xiaomaker xiaomaker 6101 Feb 15 18:24 index.hpp-rw-rw-r-- 1 xiaomaker xiaomaker379 Feb 15 14:21 Makefile-rw-rw-r-- 1 xiaomaker xiaomaker 6963 Feb 15 18:32 parser.cpp-rw-rw-r-- 1 xiaomaker xiaomaker 1842 Feb 15 18:38 util.hpp[xiaomaker@VM-28-13-centos boost_searcher]$
(2)这样我们就可以编写我们的切词工具了。
namespace ns_util{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 Split(const std::string& src, std::vector<std::string>& out){jieba.CutForSearch(src, out); //搜索分词,还有其它种类的分词,这里只要搜索分词}};cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);}
(3)具体实现:
bool BuildInveredIndex(const DocInfo& doc){struct word_cnt//统计词汇出现的次数{int title_cnt = 0; //标题出现的次数int content_cnt = 0; //内容出现的次数};//分词---标题std::unordered_map<std::string, word_cnt> word_map;//统计暂存词频率的映射表std::vector<std::string> title_word;ns_util::JiebaUtil::Split(doc.title, title_word);//对标题词频进行统计for (auto& s : title_word){boost::to_lower(s);//统一转换成小写word_map[s].title_cnt++;}//分词---内容std::vector<std::string> content_word;ns_util::JiebaUtil::Split(doc.content, content_word);//对内容词频进行统计for (auto& s : content_word){boost::to_lower(s);//统一转换成小写word_map[s].content_cnt++;}for (auto& word_pair : word_map){InvertedElem item;item.doc_id = doc.doc_id;item.word = word_pair.first;item.weight = word_pair.second.title_cnt * X + word_pair.second.content_cnt * Y;InvertedList_t& inverted_list = inverted_index[word_pair.first];inverted_list.push_back(std::move(item));}return true;}
(4)代码解析:
(5)权重计算:
什么是权重:对于搜索频率高的单词,我们认为它的权重高,同时对一个文档,如果关键字出现的次数越多,权重越大,这里我么权重结算简单些。
#define X 10 //标题出现的次数乘10#define Y 1//内容出现的次数乘1
那么权重有什么作用呢?当我们搜索的时,一个关键字可以对应多个文档,那么此时我们可以把权重高的放在前面。
现在我们的结构是这样的:
6.4 GetForwardIndex函数
(1)根据文档的id找到文档的内:
DocInfo* GetForwardIndex(uint64_t doc_id)//正排索引:根据doc_id找到文档内容{if (doc_id >= forward_index.size()){LOG(FATAL, "doc_id out range. error");//std::cerr << "doc_id out range. error" << std::endl;return nullptr;}return &forward_index[doc_id];}
6.5 GetInvertedList函数
(1)根据关键字拿到倒排拉链:
InvertedList_t* GetInveredList(std::string& word)//倒排索引:根据关键字获取倒排拉链{auto iter = inverted_index.find(word);if (iter == inverted_index.end()){LOG(FATAL, "no have InvertedList_t");//std::cerr << word << "no have InvertedList_t" << std::endl;return nullptr;}return &(iter->second);}
完上述所有工作后可以将index设置为单例模式。
6.6 将index设置成单例
下面我们把index设置成单例模式,这是因为在boost搜索引擎项目当中,事实上不需要建立多个Index索引对象,只需要建立一个索引对象就可以完成查找工作了。而且我们建立一个索引对象的成本事实上是极高的,因为我们需要将所有的网页信息分词、统计、填充、插入、效率上会受极大损失。
index整体代码(单例模式):
#pragma once#include #include #include #include #include #include #include "util.hpp"#include "log.hpp"#define X 10#define Y 1namespace ns_index{struct DocInfo{std::string title; // 文档的标题std::string content; // 文档内容std::string url; // 读文档在官网中的urluint64_t doc_id;//文档id};struct InvertedElem{int doc_id;std::string word;int weight;};typedef std::vector<InvertedElem> InvertedList_t; //倒排拉链class Index{private:Index(){}Index(const Index&) = delete;Index& operator=(const Index&) = delete;~Index(){}static Index* instance;static std::mutex mtx;public:static Index* GetInstance(){if(instance == nullptr){mtx.lock();//多线程安全if(instance == nullptr){instance = new Index();}mtx.unlock();}return instance;}DocInfo* GetForwardIndex(uint64_t doc_id)//正排索引:根据doc_id找到文档内容{if(doc_id >= forward_index.size()){LOG(FATAL, "doc_id out range. error");//std::cerr << "doc_id out range. error" << std::endl;return nullptr;}return &forward_index[doc_id];}InvertedList_t* GetInveredList(std::string& word)//倒排索引:根据关键字获取倒排拉链{auto iter = inverted_index.find(word);if(iter == inverted_index.end()){LOG(FATAL, "no have InvertedList_t");//std::cerr << word << "no have InvertedList_t" << std::endl;return nullptr;}return &(iter->second);}//根据去标签,格式化之后的文档,构建正排索引和倒排索引 bool BuildIndex(const std::string& input)//parse处理完毕的数据交给我{std::fstream in(input, std::ios::in | std::ios::binary);if(!in.is_open()){LOG(FATAL, "sorry" + input + "error");//std::cerr << "sorry" << input << "error" << std::endl;return false;}std::string line;int count = 0;while(std::getline(in, line)){DocInfo* doc = BuildForwardIndex(line);if(doc == nullptr){LOG(WARNING, "build" + line + "error");//std::cerr << "build" << line << "error" << std::endl;continue;}BuildInveredIndex(*doc);count++;if(count % 50 == 0){LOG(NORMAL, "当前已经建立索引文档: " + std::to_string(count));//std::cout << "当前已经建立索引文档:" << count << std::endl;}}return true;}private:DocInfo* BuildForwardIndex(std::string& line){//1. 解析line, 字符串切分std::vector<std::string> results;std::string sep = "\3";ns_util::StringUtil::Cutstring(line, results, sep);if(results.size() != 3){LOG(FATAL, "build error");return nullptr;}//2. 字符串进行填充到DocInfoDocInfo doc;doc.title = results[0];//titledoc.content = results[1];//contentdoc.url = results[2];//urldoc.doc_id = forward_index.size(); //文档id//3. 插入到vector当中forward_index.push_back(std::move(doc));return &forward_index.back();}bool BuildInveredIndex(const DocInfo& doc){struct word_cnt//统计词汇出现的次数{int title_cnt = 0; //标题出现的次数int content_cnt = 0; //内容出现的次数};//分词---标题std::unordered_map<std::string, word_cnt> word_map;//统计暂存词频率的映射表std::vector<std::string> title_word;ns_util::JiebaUtil::Split(doc.title, title_word);//对标题词频进行统计for(auto& s : title_word){boost::to_lower(s);//统一转换成小写word_map[s].title_cnt++;}//分词---内容std::vector<std::string> content_word;ns_util::JiebaUtil::Split(doc.content, content_word);//对内容词频进行统计for(auto& s : content_word){boost::to_lower(s);//统一转换成小写word_map[s].content_cnt++;}for(auto& word_pair : word_map){InvertedElem item;item.doc_id = doc.doc_id;item.word = word_pair.first;item.weight = word_pair.second.title_cnt * X + word_pair.second.content_cnt * Y;InvertedList_t& inverted_list = inverted_index[word_pair.first];inverted_list.push_back(std::move(item));}return true;}//正排索引使用数组,数组的下标就是文档idstd::vector<DocInfo> forward_index;//正排索引//倒排索引一定是一个关键字和一组InvertedElem对应std::unordered_map<std::string, InvertedList_t> inverted_index;//倒排索引};Index* Index::instance = nullptr; //单例模式的指针初始化std::mutex Index::mtx;}
7. 搜索引擎模块
下面我们开始编写搜索模块,创建一个searcher.hpp 文件。
(1)搜索引擎代码框架:
#pragma once#include "index.hpp"#include "util.hpp"#include #include namespace ns_searcher{struct InvertedElemPrint{uint64_t doc_id = 0;std::vector<std::string> word;int weight = 0;};class Searcher{public:Searcher(){}void InitSearcher(const std::string& input){//1. 获取或者创建index对象//2. 根据index对象建立索引}//query:搜索的关键字//json_string:返回个用户浏览器的数据搜索结果void Search(const std::string& query, std::string& json_string){//1. 对query即关键字进行分词//2. 触发:就是根据分词的各个词,进行index查找//3. 合并排序,汇总结果,按照相关性降序排序//4. 构建:根据查找的结果,构建json串---jsoncpp}~Searcher(){}private:std::string GetDesc(const std::string& html_content, const std::string& word){//找到word在html_content中首次出现,往前找50字节(如果没有从开始找),往后100字节//1. 找到首次出现//2. 获取start, end//3. 截取子串}ns_index::Index* index;//供系统进行查找的索引};}
7.1 InitSearcher函数
(1)这个是我们初始化的工作,一共两个内容:
- 拿到index对象
- 根据index建立索引
void InitSearcher(const std::string& input)//input是数据源的地址{//1. 获取或者创建index对象index = ns_index::Index::GetInstance();LOG(NORMAL, "获取单例index成功");//std::cout << "获取单例index成功" << std::endl;//2. 根据index对象建立索引index->BuildIndex(input);LOG(NORMAL, "建立正排和倒排成功");//std::cout << "建立正排和倒排成功" << std::endl;}
7.2 Search函数
(1)这个是我们查找实现的具体流程,我们输入我们想要查找的内容,下面是我们函数的流程:
- 切分输入的内容,统一小写的保存在数组中。
- 根据数组的每一个元素,拿到倒排拉链,然后把所有的倒排拉量的内容保存在一个拉链中。
- 我们以降序的方式排序整个拉链。
- 根据拉链的id找到文档内容,构建json串。
//query:搜索的关键字//json_string:返回个用户浏览器的数据搜索结果void Search(const std::string& query, std::string& json_string){//1. 对query即关键字进行分词std::vector<std::string> words;ns_util::JiebaUtil::Split(query, words);//2. 触发:就是根据分词的各个词,进行index查找//ns_index::InvertedList_t inverted_list_all;std::vector<InvertedElemPrint> inverted_list_all;std::unordered_map<uint64_t, InvertedElemPrint> token_map;for (auto& s : words){boost::to_lower(s);ns_index::InvertedList_t* inverted_list = index->GetInveredList(s);if (inverted_list == nullptr){LOG(WARNING, "通过关键字寻找文章失败");continue;}//不完美//inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());for (auto& elem : *inverted_list){auto& item = token_map[elem.doc_id];item.doc_id = elem.doc_id;item.weight += elem.weight;item.word.push_back(elem.word);}}for (const auto& item : token_map){inverted_list_all.push_back(std::move(item.second));}//3. 合并排序,汇总结果,按照相关性降序排序/*std::sort(inverted_list_all.begin(), inverted_list_all.end(), \[](const ns_index::InvertedElem& e1, const ns_index::InvertedElem& e2){return e1.weight > e2.weight;});*/std::sort(inverted_list_all.begin(), inverted_list_all.end(), \[](const InvertedElemPrint& e1, const InvertedElemPrint& e2){return e1.weight > e2.weight;});//4. 构建:根据查找的结果,构建json串---jsoncppJson::Value root;for (auto& item : inverted_list_all){ns_index::DocInfo* doc = index->GetForwardIndex(item.doc_id);if (doc == nullptr){continue;}Json::Value elem;elem["title"] = doc->title;elem["desc"] = GetDesc(doc->content, item.word[0]);//只需要展示一部分内容就可以elem["url"] = doc->url;root.append(elem);}//Json::StyledWriter w;Json::FastWriter w;json_string = w.write(root);}
(2)上面代码的实现有一个完美的地方,我们知道一个词可以映射到多个文档的id,那么多个关键字映射的文档id,就有可能进行冲突,例如下面的例子:
关键字(具有唯一性) 文档ID 我的 1,2 手机 1,2 牌子 1,2 华为 1 小米 1
我们把"我的、手机、牌子"进行分词,然后得到拉链,放在总拉链里面,这就是[文档1, 文档2,文档1, 文档2]这样就重复了,上面代码解决了该问题(解决问题的解析见本章第十小节)。
(3)代码解析:
7.3 jsoncpp安装与使用
下面我们需要说一下jsoncpp的安装与使用,前面使用了jsoncpp构建了json串,json是序列化和反序列化的。
[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install -y jsoncpp-devel
(1)下面演示一下json的使用:
#include #include #include int main(){Json::Value root;Json::Value item1;item1["key1"] = "value11";item1["key2"] = "value22";Json::Value item2;item2["key1"] = "value1";item2["key2"] = "value2";root.append(item1);root.append(item2);Json::StyledWriter writer;std::string s = writer.write(root);std::cout << s << std::endl;return 0;}
(2)运行结果:
[xiaomaker@VM-28-13-centos boost_searcher]$ g++ test.cpp-ljsoncpp[xiaomaker@VM-28-13-centos boost_searcher]$ ./a.out [ {"key1" : "value11","key2" : "value22" }, {"key1" : "value1","key2" : "value2" }][xiaomaker@VM-28-13-centos boost_searcher]$
7.4 搜索功能的测试
(1)下面是搜索测试代码:
#include "searcher.hpp"const std::string input = "data/raw_html/raw.txt";int main(){ns_searcher::Searcher* search = new ns_searcher::Searcher();search->InitSearcher(input);std::string query;std::string json_string;while (1){std::cout << "请输入关键字# ";//std::cin >> query;std::getline(std::cin, query);//std::cout << query;search->Search(query, &json_string);std::cout << json_string << std::endl;}return 0;}
下面我们测试一下,这是一个html文档的内容,由于内容实在是太多了,我们应该把内容给裁出来一部分,这样比较好。
{"desc" : "Struct template bound_launcherHomeLibrariesPeopleFAQMoreStruct template bound_launcherboost::process::v2::bound_launcher — Utility class to bind initializers to a launcher. Synopsis// In header: <boost/process/v2/bind_launcher.hpp>template<typename Launcher, typename ... Init> struct bound_launcher {// construct/copy/destructtemplate<typename Launcher_, typename ... Init_> bound_launcher(Launcher_ &&, Init_ &&...);// public member functionstemplate<typename ExecutionContext, typename Args, typename ... Inits> auto operator()(ExecutionContext &, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type &, Args &&, Inits &&...);template<typename ExecutionContext, typename Args, typename ... Inits> auto operator()(ExecutionContext &, error_code &, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type &, Args &&, Inits &&...);template<typename Executor, typename Args, typename ... Inits> auto operator()(Executor, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type &, Args &&, Inits &&...);template<typename Executor, typename Args, typename ... Inits> auto operator()(Executor, error_code &, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type &, Args &&, Inits &&...);// private member functionstemplate<std::size_t ... Idx, typename ExecutionContext, typename Args,typename ... Inits> auto invoke(unspecified, ExecutionContext &, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type &, Args &&, Inits &&...);template<std::size_t ... Idx, typename ExecutionContext, typename Args,typename ... Inits> auto invoke(unspecified, ExecutionContext &, error_code &, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type &, Args &&, Inits &&...);template<std::size_t ... Idx, typename Executor, typename Args,typename ... Inits> auto invoke(unspecified, Executor, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type &, Args &&, Inits &&...);template<std::size_t ... Idx, typename Executor, typename Args,typename ... Inits> auto invoke(unspecified, Executor, error_code &, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type &, Args &&, Inits &&...);};DescriptionThis can be used when multiple processes shared some settings, e.g. Template Parameterstypename LauncherThe inner launcher to be used typename ... Initbound_launcher public construct/copy/destructtemplate<typename Launcher_, typename ... Init_> bound_launcher(Launcher_ && l, Init_ &&... init);bound_launcher public member functionstemplate<typename ExecutionContext, typename Args, typename ... Inits> auto operator()(ExecutionContext & context, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);template<typename ExecutionContext, typename Args, typename ... Inits> auto operator()(ExecutionContext & context, error_code & ec, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);template<typename Executor, typename Args, typename ... Inits> auto operator()(Executor exec, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);template<typename Executor, typename Args, typename ... Inits> auto operator()(Executor exec, error_code & ec, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);bound_launcher private member functionstemplate<std::size_t ... Idx, typename ExecutionContext, typename Args,typename ... Inits> auto invoke(unspecified, ExecutionContext & context, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);template<std::size_t ... Idx, typename ExecutionContext, typename Args,typename ... Inits> auto invoke(unspecified, ExecutionContext & context, error_code & ec, const typename std::enable_if< std::is_convertible< ExecutionContext &, boost::asio::execution_context & >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);template<std::size_t ... Idx, typename Executor, typename Args,typename ... Inits> auto invoke(unspecified, Executor exec, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);template<std::size_t ... Idx, typename Executor, typename Args,typename ... Inits> auto invoke(unspecified, Executor exec, error_code & ec, const typename std::enable_if< boost::asio::execution::is_executor< Executor >::value||boost::asio::is_executor< Executor >::value, filesystem::path >::type & executable, Args && args, Inits &&... inits);Copyright © 2006-2012 Julio M. Merino Vidal, Ilya Sokolov,Felipe Tanus, Jeff Flinn, Boris SchaelingCopyright © 2016 Klemens D. MorgensternDistributed under the Boost Software License, Version 1.0. (See accompanyingfile LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)","title" : "Struct template bound_launcher","url" : "https://www.boost.org/doc/libs/1_84_0/doc/html/boost/process/v2/bound_launcher.html"},
7.5 获取内容摘要
当我们搜索出结果的时候并不需要全部展示内容,所以需要获取内容的摘要。
(1)具体实现:
std::string GetDesc(const std::string& html_content, const std::string& word){//找到word在html_content中首次出现,往前找50字节(如果没有从开始找),往后100字节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, endint start = 0;int end = html_content.size() - 1;//如果之前有50个就更新if (pos > start + prev_step){start = pos - prev_step;}//如果后面有100个就更新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;}
(2)测试结果:
请输入关键字# filesystem[ {"desc" : "boost::asio::execution_context & >::value, filesystem::path >::type &, Args &&, Inits &&...);templ...."...,"title" : "Struct template bound_launcher","url" : "https://www.boost.org/doc/libs/1_84_0/doc/html/boost/process/v2/bound_launcher.html" }, .....]
8. 搜索服务端
接下来开始编写网络版本的服务端,创建http_server.cpp文件。
#include "searcher.hpp"int mian(){return 0;}
这里通信我们可以自己写,后面我们会升级,不过这里我们使用cpp-httplib库,这个库很简单,但是cpp-httplib有点问题,我们需要较新版本的编译器,否则就是编译不通过,或者是运行出现错误。
8.1 升级gcc版本
下面是更新新的gcc版本:
//安装scl[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install centos-release-scl scl-utils-build//安装新版本gcc[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++//启动: 细节,命令行启动只能在本会话有效[xiaomaker@VM-28-13-centos boost_searcher]$ scl enable devtoolset-7 bash
8.2 引入cpp-httplib库
这里我们选择下载0.7.15版本,这是因为较新版本的可能运行时会报错。
这里我们选择下载到桌面,然后拖拽到轻量级服务器上,这些方法都试一遍.
(1)cpp-httplib库链接:https://gitee.com/linzhipong/cpp-httplib/tree/v0.7.15
[xiaomaker@VM-28-13-centos http]$ rz -E [xiaomaker@VM-28-13-centos http]$ lltotal 4drwxr-xr-x 6 root root 4096 Nov 192020 cpp-httplib-v0.7.15[xiaomaker@VM-28-13-centos http]$
(2)这时候可以创建软链接到我们的项目中:
[xiaomaker@VM-28-13-centos boost_searcher]$ ln -s /home/xiaomaker/code_cpp/http/cpp-httplib-v0.7.15/ cpp-httplib
8.3 测试cpp-httplib
在测试一下httplib库之前,我们不仅需要链接cpp-httplib库,还需要链接pthread库,因为该库用到了多线程,所以需要链接多线程库。
(1)测试代码:
#include "cpp-httplib/httplib.h"int main(){httplib::Server svr;svr.Get("hi", [](const httplib::Request& req, httplib::Response& rsp) {rsp.set_content("hello word!", "text/plain; charset=utf-8");});svr.listen("0.0.0.0", 8081);return 0;}
(2)运行之后查看服务器:
[xiaomaker@VM-28-13-centos boost_searcher]$ netstat -ntlp(Not all processes could be identified, non-owned process info will not be shown, you would have to be root to see it all.)Active Internet connections (only servers)Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program nametcp00 127.0.0.1:44227 0.0.0.0:* LISTEN1903/node tcp00 0.0.0.0:111 0.0.0.0:* LISTEN- tcp00 0.0.0.0:80810.0.0.0:* LISTEN4191/./http_servertcp00 192.168.122.1:530.0.0.0:* LISTEN- tcp00 0.0.0.0:220.0.0.0:* LISTEN- tcp00 127.0.0.1:631 0.0.0.0:* LISTEN- tcp00 127.0.0.1:250.0.0.0:* LISTEN- tcp6 00 :::111:::*LISTEN- tcp6 00 :::22 :::*LISTEN- tcp6 00 ::1:631 :::*LISTEN- tcp6 00 ::1:25:::*LISTEN- [xiaomaker@VM-28-13-centos boost_searcher]$
运行之前自己的服务器需要开放端口号才可以。
(3)运行结果:
8.4 设置根目录
一般而言我们需要有一个根目录,这样就可以实现前端,创建wwwroot目录。
(1)在服务器上面设置跟目录:
#include "cpp-httplib/httplib.h"const std::string root_path = "./wwwroot";int main(){httplib::Server svr;// 设置跟目录svr.set_base_dir(root_path.c_str());svr.Get("hi", [](const httplib::Request& req, httplib::Response& rsp) {rsp.set_content("hello word!", "text/plain; charset=utf-8");});svr.listen("0.0.0.0", 8080);return 0;}
(2)测试结果:
(3)这是因为我们的根目录下面什么都没有,我们在wwwroot目录下创建index.html文件,在这里设置一下:
(4)再次测试:
8.5 编写搜索服务端
(1)下面编写我们的服务端了:
#include "searcher.hpp"#include "cpp-httplib/httplib.h"#include #include const std::string input = "data/raw_html/raw.txt"; const std::string root_path = "./wwwroot";int main(){ns_searcher::Searcher search;search.InitSearcher(input);httplib::Server svr;svr.set_base_dir(root_path.c_str());svr.Get("/s", [&search](const httplib::Request& req, httplib::Response& rsp){if(!req.has_param("word")){rsp.set_content("必须要有搜索关键字", "text/plain: charset=utf-8");return;}std::string word = req.get_param_value("word");LOG(NORMAL, "用户在搜索" + word);//std::cout << "用户在搜索" << word << std::endl;std::string json_string;search.Search(word, json_string);rsp.set_content(json_string, "application/json");//rsp.set_content("hello world", "text/plain: charset=utf-8");});LOG(NORMAL, "服务器启动成功");svr.listen("0.0.0.0", 8080);return 0;}
(2)代码解析:
9. 前端代码
前端部分我们可以选学,这里我们也不谈,如果想学,可以去下面的网站:
- HTML:编写网页结构,网页的骨骼。
- CSS:网页样式,网页的皮肉。
- Js:前后端交互,网页的灵魂。
前端学习网站推荐:http://www.w3school.com.cn
9.1 网页结构
(1)设置的网页结构是这样的:
(2)按照上面的内容,我们的html代码:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>boost 搜索引擎</title></head><body><div class="container"><div class="search"><input type="text" value="输入搜索关键字..."><button>搜索一下</button></div><div class="result"><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/" /></i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div></div></div></body></html>
(3)运行结果:
9.2 网页样式
(1)上面的网有点丑,所以这里我们要给他美颜一下:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>boost 搜索引擎</title><style>/* 去掉网页中的所有的默认内外边距,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: 52px;}/* 先选中input标签, 直接设置标签的属性,先要选中, input:标签选择器*//* input在进行高度设置的时候,没有考虑边框的问题 */.container .search input {/* 设置left浮动 */float: left;width: 600px;height: 50px;/* 设置边框属性:边框的宽度,样式,颜色 */border: 1px solid black;/* 去掉input输入框的有边框 */border-right: none;/* 设置内边距,默认文字不要和左侧边框紧挨着 */padding-left: 10px;/* 设置input内部的字体的颜色和样式 */color: #CCC;font-size: 15px;}/* 先选中button标签, 直接设置标签的属性,先要选中, button:标签选择器*/.container .search button {/* 设置left浮动 */float: left;width: 150px;height: 52px;/* 设置button的背景颜色,#4e6ef2 */background-color: #4e6ef2;/* 设置button中的字体颜色 */color: #FFF;/* 设置字体的大小 */font-size: 19px;font-family: Georgia, 'Times New Roman', Times, serif;}.container .result {width: 100%;}.container .result .item {margin-top: 15px;}.container .result .item a {/* 设置为块级元素,单独站一行 */display: block;/* a标签的下划线去掉 */text-decoration: none;/* 设置a标签中的文字的字体大小 */font-size: 20px;/* 设置字体的颜色 */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 SansUnicode', Geneva, Verdana, sans-serif;}.container .result .item i {/* 设置为块级元素,单独站一行 */display: block;/* 取消斜体风格 */font-style: normal;color: green;}</style></head><body><div class="container"><div class="search"><input type="text" value="输入搜索关键字..."><button>搜索一下</button></div><div class="result"><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/" /></i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div></div></div></body></html>
(2)运行结果:
9.3 前后端交互
(1)下面我们继续使用前后端交互,也是直接贴代码:
<!DOCTYPE html><html lang="en"><head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script src="http://code.jquery.com/jquery-2.1.1.min.js"></script><title>boost 搜索引擎</title><style>* {/* 设置外边距 */margin: 0;/* 设置内边距 */padding: 0;}html,body {height: 100%;}/* 居中显式以点开头的我们称之类选择器 */.container {/* 这是最大框架 */width: 800px;margin: 0px auto;margin-top: 15px;}/* 复合选择器 */.container .search {width: 100%;/* 为何是52我们后面解释 */height: 52px;}.container .search input {/* 加上浮动 */float: left;width: 600px;height: 50px;/* 设置边框 */border: 1px solid black;/* 去掉右边距 */border-right: none;padding-left: 10px;color: #ccc;font-size: 15px;}.container .search button {/* 加上浮动 */float: left;width: 120px;height: 52px;/* 设置背景颜色 */background-color: #4e6ef2;/* 设置字体颜色 */color: #fff;/* 设置字体大小 */font-size: 19px;/* 设置字体样式 */font-family: 'Times New Roman', Times, serif;}.container .result {width: 100%;}.container .result .item {margin-top: 15px;}.container .result .item a {display: block;/* 去掉下划线 */text-decoration: none;font-size: 20px;color: #4e6ef2;}.container .result .item a:hover {text-decoration: underline;}.container .result .item p {margin: 5px;font-size: 16px;font-family: 'Times New Roman', Times, serif;}.container .result .item i {display: block;/* 取消斜体 */font-style: normal;color: green;}</style></head><body><div class="container"><div class="search"><input type="text" value="输入搜索关键字..."><button onclick="Search()">搜索一下</button></div><div class="result"><!-- 这是标题这是摘要这是摘要,这是摘要这是摘要,这是摘要这是摘要,这是摘要这是摘要
https://www.bilibili.com/这是标题这是摘要这是摘要
https://www.bilibili.com/这是标题这是摘要这是摘要
https://www.bilibili.com/这是标题这是摘要这是摘要
https://www.bilibili.com/这是标题这是摘要这是摘要
https://www.bilibili.com/这是标题这是摘要这是摘要
https://www.bilibili.com/这是标题这是摘要这是摘要
https://www.bilibili.com/ --></div></div><script>function Search() {// alert("hello js");// 1. 提取数据 jquerylet query = $(".container .search input").val();if(query == '' || query == null){return;}console.log("query = " + query);// 2. 发起http 请求$.ajax({type: "GET",url: "/s" /> + query,success: function (data) {console.log(data);// 构建新网页-- 动态的BuildHtml(data);}});}function BuildHtml(data) {if(date == '' || data == null){document.write("搜索的内容没有");return;}let result_lable = $(".container .result");result_lable.empty();for (let elem of data) {// console.log(elem.title);// console.log(elem.url);let a_lable = $("", {text: elem.title,href: elem.url,target: "_blank"});let p_lable = $(""
, {text: elem.desc});let i_lable = $("", {text: elem.url});let div_lable = $("", {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>
(2)最后整体运行结果:
10. 项目补充
现在完善一下系统的一些小细节。
10.1 取重完善
我们在搜索服务那里说过,对于我们关键词的搜索结果,在多个关键字之间,我们的文档id可能会重复,这个时候我们需要进行去重分为两步:
- 找到在重复的id
- 把id里面的权重尽心相加
- 重新构造,让后进行查找构建json串
下面是我们的遇到的情况:
10.2 添加日志
我们可以添加日志,创建一个Log.hpp文件。
(1)日志实现:
#pragma once#include #include #include #define NORMAL 1#define WARNING 2#define DEBUG 3#define FATAL 4#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)void log(std::string level, std::string message, std::string file, int line){std::cout << "[" << level << "]"<< "[" << time(nullptr) << "]"<< "[" << message << "]"<< "[" << file << " : " << line << "]" << std::endl;}
可以在各个需要打印的地方加入日志,区分日志的等级即可(本篇文章附带的代码基本上都已经加入了日志)。
11. 项目拓展
这里我们可以扩展一下项目内容。
11.1 摘要完善
我们知道分词的时候是可以去掉暂停词的,上面的我们都没有这么做,这是因为我们的如果加上去掉暂停词,此时对资源的要求非常大,那么这里可以作为一个扩展.jieba里面也有暂停词的集合,我们可以使用一下:
class JiebaUtil{public:static void CutString(const std::string& src, std::vector<std::string>* out){assert(out);ns_util::JiebaUtil::get_instance()->CutStringHelper(src, out);}private:/// @brief 这里是分词/// @param src/// @param outvoid CutStringHelper(const std::string& src, std::vector<std::string>* out){jieba.CutForSearch(src, *out);for (auto iter = out->begin(); iter != out->end();){auto it = stop_words.find(*iter);if (it != stop_words.end()){// 此时是暂停词 删除//避免迭代器失效// std::cout << *iter << std::endl;iter = out->erase(iter);}else{iter++;}}}static JiebaUtil* get_instance(){static std::mutex mtx;if (nullptr == instance){mtx.lock();if (nullptr == instance){instance = new JiebaUtil;instance->InitJiebaUtil();}mtx.unlock();}return instance;}// 这是我们的切分词void InitJiebaUtil(){std::ifstream in(STOP_WORD_PATH);if (in.is_open() == false){LOG(FATAL, "加载暂停词错误");return;}std::string line;while (std::getline(in, line)){stop_words.insert(std::make_pair(line, true));}in.close();}private:static JiebaUtil* instance;cppjieba::Jieba jieba;std::unordered_map<std::string, bool> stop_words;JiebaUtil() : jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH) {}// 拷贝构造等 delte};JiebaUtil* JiebaUtil::instance = nullptr;
11.2 后台部署服务
我们可以把它设置为精灵进程.
(1)nohup指令
nohup指令:将服务进程以守护进程的方式执行,使关闭XShell之后仍可以访问该服务
例如:nohup ./http_server
如果让程序在后台执行,可以在末尾加上 &,程序就会隐身,不会显示在终端
例如:nohup ./http_server &
(2)nohup形成的文件:
执行完上述的nohup指令之后,将会形成一个nohup.out存储日志信息文件,可以cat查看该文件。
(3)setsid
我们也是可以使用下面的方式进行守护进程化:
#pragma once#include #include #include #include #include #include #include void daemonize(){int fd = 0;// 1. 忽略SIGPIPEsignal(SIGPIPE, SIG_IGN);// 2. 更改进程的工作目录// chdir();// 3. 让自己不要成为进程组组长if (fork() > 0)exit(0);// 4. 设置自己是一个独立的会话setsid();// 5. 重定向0,1,2if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3{dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 6. 关闭掉不需要的fdif (fd > STDERR_FILENO) close(fd);// 6. close(0,1,2)// 严重不推荐}}
11.3 其他拓展
- 我们在搜索引擎中,对于权重的设置先后显示顺序,我们其实可以叠加一些算法,比如可以设置竞价排,热点统计,额外增加某些文档的权重。
- 我们可以利用数据库,设置用户登录注册,引入对MySQL的使用。