目录
[编写数据去标签与数据清洗的模块 Parser](#编写数据去标签与数据清洗的模块 Parser)
[构建 Parser 模块](#构建 Parser 模块)
[递归式获取 HTML 文件的带文件名称路径](#递归式获取 HTML 文件的带文件名称路径)
[对 HTML 文件内容进行解析](#对 HTML 文件内容进行解析)
[依次读取 HTML 文件内容](#依次读取 HTML 文件内容)
[解析 HTML 文件的 title](#解析 HTML 文件的 title)
[解析 HTML 文件的 content](#解析 HTML 文件的 content)
[解析 HTML 文件的 url](#解析 HTML 文件的 url)
[对 HTML 文件解析整体代码如下](#对 HTML 文件解析整体代码如下)
[将解析之后的 HTML 文件内容拼接并写入对应的文本文件中](#将解析之后的 HTML 文件内容拼接并写入对应的文本文件中)
[Parser 模块整体代码](#Parser 模块整体代码)
[编写建立索引的模块 Index](#编写建立索引的模块 Index)
[通过文档 id 获取对应文档的正排索引对象](#通过文档 id 获取对应文档的正排索引对象)
[编写搜索引擎模块 Searcher](#编写搜索引擎模块 Searcher)
[创建 index 对象,构建索引](#创建 index 对象,构建索引)
[获取 content 的摘要](#获取 content 的摘要)
[通过关键词进行查询,最终返回 json 串](#通过关键词进行查询,最终返回 json 串)
[debug 本地测试](#debug 本地测试)
[编写 http_sever 模块](#编写 http_sever 模块)
[cpp-httplib 测试代码](#cpp-httplib 测试代码)
[编写 http_sever 模块](#编写 http_sever 模块)
[将项目部署到 LINUX 服务器](#将项目部署到 LINUX 服务器)
搜索引擎项目背景
搜索引擎是一个大家众所周知的一个搜索工具,常见的搜索引擎有百度搜索,搜狗搜索,360搜索等等,我们以百度搜索为例。
百度搜索的主页面如下。

我们可以在搜索框中输入我们想要搜索的内容,点击搜索,就会出现如下界面。

点击搜索之后跳转的主页会展现大量的相关关键字的网页信息。 我们对其中一个网页信息进行分析。

网页信息就包含了网页的标题,网页内容的摘要和网页资源对应的url。
我们自己可以实现这样一个大的搜索引擎吗?对与个人而言,实现这样一个大的搜索引擎,代价太大,将全网的数据整合就是一个巨大的难题,所以我们实现这样一个进行全网搜索的搜索引擎是明显不现实的,但是有不少的网页具有站内搜索的引擎,我们以常见的boost库为例。图示如下。

boost库官网中就存在这样一个站内的搜索引擎,可以搜索boost库中的相关知识。
基于以上的背景,此项目旨在设计开发一款如boost库官网界面站内搜索引擎的boost搜索引擎,实现与之类似的站内搜索功能。
搜索引擎的宏观原理
那么像百度搜索,360搜索等等这些当今互联网上应用较为广泛的搜索引擎,它们搜索的宏观原理是什么呢?我们通过一个图示为大家大概的讲解。

区别于之前学习的网络间通信,之前的client和sever指的都是进程,而上图中的client和sever指的其实不是进程,而对应的是客户端的主机和服务器端的主机。
在客户端主机和服务器端主机内部分别有客户端search进程和服务器端search进程,我们可以认为,几乎所有的类似的软件交互,实际上都是客户端进程和服务器端进程的交互。
我们实现的boost搜索引擎实际上只涉及了蓝方框内的宏观原理,相应的html我们不是通过爬虫获取的,而是直接在官网上下载下来的html文件。
搜索引擎技术栈和项目环境
- 技术栈:C/C++,C++11,STL,准标准库Boost,Jsoncpp(数据交换),cppjieba(搜索关键词的分词),cpp-httplib(构建http服务器),html5,css,jQuery,js,Ajax。
- 项目环境:centos7云服务器,vim,(gcc/g++),makefile,vscode。
搜索引擎具体原理(正排索引和倒排索引)
在搜索引擎中,我们在通过关键字进行查询时,往往会使用到倒排索引和正排索引。那么倒排索引和正排索引是什么呢?
正排索引
比如现在有两个文档,两个文档的文档id分别为1和2,两个文档的内容分别为雷布斯发布了小米手机和雷布斯发布了小米su7
|----------|-------------|
| 文档id | 文档内容 |
| 1 | 雷布斯发布了小米手机 |
| 2 | 雷布斯发布了小米su7 |
所谓正排索引,很好理解,就是通过文档id查询文档内容。
倒排索引
倒排索引其实就是通过文档的内容和文档的关键字查询文档的文档id。
那么怎么样获取文档的关键字呢?此时我们就要对文档进行分词。
- 文档1分词(雷布斯发布了小米手机):雷布斯/发布/小米/手机/小米手机
- 文档2分词(雷布斯坐的小米su7):雷布斯/坐/小米/su7/小米su7
不难发现我们在进行分词的时候,将了/的这两个关键字给省略掉了,这是因为在搜索引擎中,我们有了停止词的概念,停止词就是在多个文档中都会出现的共性词,如中文中的 的/了/是等等,英文中的 a/the 等等,如果将这些字作为了关键字,将来查询到的文档就非常多,可以理解为就是查询所有的文档,所以会降低查询的效率,所以我们在关键字拆分的时候,不将停止词作为关键字。
需要注意的是,多个文档的重复关键字我们最终只保留唯一的一份,也就意味着关键字也必须和文档id一样是唯一的。
|---------|-------------|
| 关键字 | 文档id/权值 |
| 雷布斯 | 文档1/文档2 |
| 发布 | 文档1 |
| 小米 | 文档1/文档2 |
| 手机 | 文档1 |
| 小米手机 | 文档1 |
| 坐 | 文档2 |
| su7 | 文档2 |
| 小米su7 | 文档2 |
其实在大家使用关键字在百度等搜索引擎上进行搜索时,查找出来多个同种类型的多个去标签的网页内容会在一个页面先后展示,为什么会先后展示,这是因为,每个网页的权值是不一样的,权值高的会优先展示。所以我们也会为每个文档进行权值的设定,文档id就可以用来表示权值,这个之后我们会再次讲到。
所以搜索引擎的具体查询原理就是sever端先用关键字进行倒排索引,查找到文档的id,然后再通过文档id查询到文档内容,再对查询到的文档内容进行去标签操作得到title,desc和url,最终对多个文档的tile,desc和url进行组合,然后通过文档的权值进行排序,最终将拼装好的页面返回给client端展示。
编写数据去标签与数据清洗的模块 Parser
从boost官网导入HTML网页数据
boost官网主界面如图所示。

我们的网页不是通过爬虫获取的,而是直接下载了boost官网中的对应的html网页。

下载之后使用 rz -E 指令将下载下来的含有 html 网页的 boost文件导入我们自己创建的目录中。

使用 tar xzf 指令对对应的文件进行解包解压,解压之后的目录如图所示。

boost_1_87_0 目录中的文件就是我们在boost官网上看到的所有的内容。
在boost_searcher 下创建一个与boost_1_87_0 同级的目录data ,在 data 里面创建一个input 目录用于存放 boost_1_87_0/doc/html 目录下的所有 html 文件和目录,类似于爬虫获取的大量 html网页数据源。

可以看到此时 input目录里大约有 8761 个网页数据源。
去标签
何为标签?
html
<td align="center"><a href="../../libs/libraries.htm">Libraries</a></td>
上述html代码中,符号 <> 以及 符号 <> 内的内容组合起来,我们称之为一个标签。以 <> 为开始标签,</> 为结束标签。
何为去标签?
所谓去标签,其实就是不用去关心标签内的数据,只关心标签外的数据,比如上述标签我们只关心 Libraries。

在与 input 同级的目录下创建一个 raw_html 目录,目录里创建对应的文件用于存放每一个 html 文件去标签之后的数据在 raw_html 内的文件中存放,且每个 html 文件对应的去标签之后的数据应该以**'\3'** 进行分隔,因为 '\3' 是不可显字符。
构建 Parser 模块
paser模块的构建主要分3步。
- 递归式的获取 input 目录里的所有 html 文件的带文件名称的路径名,并将每个 html 文件的带文件名称的路径名保存在一个vector容器中。
- 根据第一步获取的 html 文件的带文件名的路径,依次打开每个文件,依次读取每个文件的内容,对读取出来的内容进行解析,将解析每个 html 文件的 title,content和url,保存在一个vector容器中。
- 对第2步获取的每个文件的 title,content,url 内容进行拼接,写入 raw.txt 文件中 。
递归式获取 HTML 文件的带文件名称路径
递归式获取文件的带文件名称路径的方法,我们采用的是 boost官网的 Filesystem Library 库,要使用该库,必须先安装 boost 库,安装指令如下。
bash
sudo yum install -y boost-devel
如下图,我们安装的是 boost 库的1.53版本。

代码如下。
cpp
bool EnumFile(const std::string& src_path,std::vector<std::string>*file_list)
{
namespace fs=boost::filesystem;
fs::path root_path(src_path);
//判断路径是否存在,如果不存在直接返回false
if(!fs::exists(root_path))
{
std::cerr<<src_path<<"not exists"<<std::endl;
return false;
}
//定义一个空迭代器,用来判断递归是否结束
fs::recursive_directory_iterator end;
for(fs::recursive_directory_iterator iter(root_path);iter!=end;iter++)
{
//判断当前路径是否对应的是普通文件,目录就不是普通文件
if(!fs::is_regular_file(*iter))
{
continue;
}
//是普通文件,判断是否是html文件
if(iter->path().extension()!=".html")
{
continue;
}
//当前路径对应的是html文件,将该路径保存到file_list中
file_list->push_back(iter->path().string());
}
return true;
}
对 HTML 文件内容进行解析
对 html 文件内容分析主要分为四步。
- 从 file_list 中依次读取每个 html 文件的的内容。
- 从 html 文件内容中解析文件 title。
- 从 html 文件内容中解析文件 content。
- 从 html 文件内容中解析文件 url,最终将解析的 title,content,url全部保存进一个 DocInfo 结构体对象中,最终保存进 vector 中。
基于此我们要先创建一个DocInfo结构体,用于保存每个 html 文件内容的title,content,url。
cpp
//创建一个结构体,用于保存,一个html网页文件分析之后的 title,content,url
typedef struct DocInfo{
std::string title; //网页文件的标题
std::string content; //网页文件的内容
std::string url; //网页文件的url
}DocInfo_t;
依次读取 HTML 文件内容
cpp
static bool ReadFile(const std::string & file_path,std::string *results)
{
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))
{
*results+=line;
}
in.close();
return true;
}
解析 HTML 文件的 title
cpp
bool ParseTitle(const std::string& result,std::string * title)
{
std::size_t begin=result.find("<title>");
if(begin==std::string::npos)
{
return false;
}
std::size_t end=result.find("</title>");
//截取字符串,但是截取的字符串应该是左闭右开区间
begin+=std::string("<title>").size();
if(begin>end)
{
return false;
}
*title= result.substr(begin,end-begin);
return true;
}
解析 HTML 文件的 content
cpp
bool ParseContent(const std::string& result,std::string* content)
{
//去标签,基于一个简单的状态机
enum status{
LABLE,//表示标签
CONTENT//表示正文内容
};
enum status s=LABLE;
for(char c:result)
{
switch(s)
{
case LABLE:
if(c=='>') s=CONTENT;
break;
case CONTENT:
if(c=='<') s=LABLE;
else{
//去掉content中的'\n' 最终我们要使用'\n'作为每个html文件的解析之后的数据之间的分隔符
if(c=='\n') c=' ';
content->push_back(c);
}
break;
default:
break;
}
}
return true;
解析 HTML 文件的 url
cpp
bool ParseUrl(const std::string& filepath,std::string* url)
{
std::string url_head="https://www.boost.org/doc/libs/1_87_0/doc/html";
std::string url_tail=filepath.substr(src_path.size());
*url=url_head+url_tail;
return true;
}
对 HTML 文件解析整体代码如下
cpp
bool ParseHtml(const std::vector<std::string>&file_list,std::vector<DocInfo_t>* results)
{
for(const std::string& filepath:file_list)
{
//1.依次读取file_list中的文件到result中
std::string result;
if(!ns_util::FileUtil::ReadFile(filepath,&result))
{
continue;
}
DocInfo_t doc;
//2.读取文件成功,开始进文件内容分析,提取文件title
if(!ParseTitle(result,&doc.title))
{
continue;
}
//3.提取title成功,提取content
if(!ParseContent(result,&doc.content))
{
continue;
}
//4.提取content成功,提取url
if(!ParseUrl(filepath,&doc.url))
{
continue;
}
//将doc转为右值之后插入,可以减少拷贝
results->push_back(std::move(doc));
// ShowDoc(doc);
// break;
}
return true;
}
将解析之后的 HTML 文件内容拼接并写入对应的文本文件中
cpp
bool SaveHtml(const std::vector<DocInfo_t>& results,const std::string& output)
{
#define SEP '\3'
//按照二进制方式写入
std::ofstream out(output,std::ios::out | std::ios::binary);
if(!out.is_open())
{
std::cerr<<"open"<<output<<"failed!"<<std::endl;
return false;
}
//打开了文件,开始进行数据的写入
for(auto &item:results)
{
std::string out_string;
out_string += item.title;
out_string += SEP;
out_string +=item.content;
out_string+=SEP;
out_string+=item.url;
out_string += '\n';
out.write(out_string.c_str(),out_string.size());
}
return true;
}
我们以 '\3' 区分每个html文件的title,content,url。以 '\n' 区分每个文件的解析之后的内容。
Parser 模块整体代码
cpp
int main()
{
//1:递归式的获取input目录下的所有html网页文件的文件名称和路径,保存到file_list中
std::vector< std::string> file_list;
if(!EnumFile(src_path,&file_list))
{
std::cerr<<"enum file name error!"<<std::endl;
return 1;
}
//2.读取file_list中对应的文件的内容,进行分析,将分析之后的数据放入 results中
std::vector<DocInfo_t> results;
if(!ParseHtml(file_list,&results))
{
std::cerr<<"parse html error!"<<std::endl;
return 2;
}
//3.将results中保存的每个html页面文件分析之后的数据写入raw.txt中
if(!SaveHtml(results,output))
{
std::cerr<<"save html error!"<<std::endl;
return 3;
}
return 0;
}
编写建立索引的模块 Index
编写 index 主要分为两步。
- 编写正排索引模块,即文档 id 和文档内容的关系。
- 编写倒排索引模块,即关键词和文档 id 的关系。
在此之前,我们已经将所有的 html 文件进行了解析,将解析之后所有 html 文件的title,content,url全部保存在了 raw.txt 文本文件中,并且,每个文件之间的解析之后的数据以 '\n'作为分隔符,所以哦将来可以使用使用 getline 一次获取 raw.txt 的一行数据,因为一行的数据刚好是一个文档解析之后的数据,所以我们可以以这行数据建立该行数据所对应的文档的正排索引结构体和倒排索引结构体。
正排索引结构体如下。
cpp
struct DocInfo{
std::string title; //文档的标题
std::string content; //文档的去标签之后的内容
std::string url; //文档的url
uint64_t doc_id; //文档的id
};
正派索引结构体是对于文档而言的,表示当前文档对应的title,content,url和doc_id(文档id),一个 html 文档对应一个正排索引结构体对象。因为文档 id 和 html 文档是一一对应的关系。
倒排索结构体如下。
cpp
//倒排索引结构体,一个关键词对应多个倒排索引结构体
struct InvertedElem{
uint64_t doc_id;
std::string word;
int weight;
InvertedElem():weight(0){}
};
倒排索引结构体是对于关键词而言的,一个关键词可能对应多个倒排索引结构体对象。因为一个关键词可能出现在多个文档中。当关键词存在多个倒排索引结构体对象时,该关键词对应的一个倒排索引对象就是该关键词在一个文档中的关联关系,而该关键词会与多个文档产生关联关系。
正排索引为一个vector容器,该容器的每个元素为一个正排索引结构体对象。
cpp
//正排索引的数组容器,用于存放,每个文档对应的正派索引结构体,数组的下标为结构体中的文档id
std::vector<DocInfo> forward_index;
倒排索引为一个unordered_map容器,该容器的每个元素的 first 对应一个关键词,每个元素的 second 表示该元素对应的倒排拉链,保存 first 对应的关键词的所有倒排索引的结构体对象。
cpp
//倒排索引的容器,用于存放每个关键词,以及这个关键词对应的倒排拉链,倒排拉链一个数组,数组的每个元素是一个 InvertedElem 关键词对应的结构体对象
//一个关键词在一个文档中出现就有一个对应结构体对象,在多个文档中出现就有多个对应的结构体对像,所以倒排拉链中可能有一个结构体对象也可能有多个
//结构体对象
std::unordered_map<std::string,InvertedList> inverted_index;
编写正排索引模块
正排索引的编码主要分为两步。
- 对从 raw.txt 中读取的一行数据,因为我们之前已经将每个 html 文件解析之后的内容通过 '\n' 进行分隔,所以从 raw.txt 中读取的一行数据就是一个文档解析之后的数据。
- 对读取的一行数据进行切分,得到这行数据对应的文档的title,content,url。创建一个正排索引结构体对象,将切分之后获取的 title,content,url 分别设置进这个正派索引结构体对象中,正派索引的 doc_id 成员我们用正排索引的vector下标表示,可以通过建立的正排索引的vector容器的size进行设设置。
按行读取文档
cpp
//根据input路径打开路径下的raw.txt文件
std::ifstream in(input,std::ios::in|std::ios::binary);
if(!in.is_open())
{
std::cerr<<"open"<<input<<"failed"<<std::endl;
return false;
}
//打开文件成功,开始进行文件的读取
std::string line;
while(std::getline(in,line))
{
//因为raw.txt文档中,一行就对应一个html文件解析之后的数据,所以读取完一行之后,就给这一行对应的html文件建立正排索引和倒排索引
DocInfo * doc=BuildForwardIndex(line);
if(nullptr==doc)
{
std::cerr<<"build"<<line<<"failed"<<std::endl;
continue;
}
切分行数据并进行行数据对应的文档的正排索引结构体对象构建
在对读取的行数据进行切分时,我们使用boost库中的split函数进行切分。
cpp
//建立正排索引
DocInfo* BuildForwardIndex(const std::string &line)
{
//1.对line中的数据进行切分,获取到对应html文档的title,content,url
//使用boost库中的split函数进行切分
std::vector<std::string> results;
const std::string sep="\3";
ns_util::StringUtil::Split(line,&results,sep);
if(results.size()!=3)
{
return nullptr;
}
//2.进行字符串填充
DocInfo doc;
doc.title=results[0];
doc.content=results[1];
doc.url=results[2];
doc.doc_id=forward_index.size();
//3.插入到正派索引对应的vector中
forward_index.push_back(std::move(doc));
return &forward_index.back();
}
编写倒排索引模块
倒排索引主要分为两步。
- 根据创建的正排索引结构体对象的title和content进行分词,分词之后,通过一个unordered_map 对象依次统计关键词在title和content中出现的次数。
- 根据 unordered_map 对象中统计出的关键词在title和content中出现的次数,建立关键词的倒排索引结构体对象,并将该结构体对象插入关键词对应的倒排拉链中。
cpp
bool BuildInvertedIndex(const DocInfo &doc)
{
//1.创建一个结构体,用于表示关键词在title和content中出现的次数
struct word_cnt
{
int title_cnt;
int content_cnt;
word_cnt():title_cnt(0),content_cnt(0){}
};
//2.创建一个map容器,key用于保存关键词,value用于保存关键词在一个文档的title和content中分别出现的次数
std::unordered_map<std::string,word_cnt>word_map;
//3.对title进行分词,vector中用于存储title分词之后的多个关键词
std::vector<std::string>title_words;
//4.对title进行分词
ns_util::JiebaUtil::CutString(doc.title,&title_words);
//5.统计title中对应关键词出现的次数,因为将来在进行关键词搜索时,不区分大小写,所以我们在分词之后,必须对大小写进行统一
for(auto title_word:title_words)
{
boost::to_lower(title_word);
word_map[title_word].title_cnt++;
}
//6.对content进行分词,vector中用于存储content分词之后的多个关键词
std::vector<std::string>content_words;
ns_util::JiebaUtil::CutString(doc.content,&content_words);
//7.统计content中对应关键词出现的次数
for(auto content_word:content_words)
{
boost::to_lower(content_word);
word_map[content_word].content_cnt++;
}
//8.构建关键词对应的倒排索引结构体
#define X 10
#define Y 1
for(auto &word:word_map){
InvertedElem em;
em.doc_id=doc.doc_id;
em.word=word.first;
em.weight=word.second.title_cnt*X+word.second.content_cnt*Y;
//9.将关键词对应的倒排索引的结构体插入关键词对应的倒排拉链中
inverted_index[word.first].push_back(std::move(em));
}
return true;
}
对 doc 中的 title 和 content 进行分词时,我们使用cppjieba库进行分词。分词代码如下。
cpp
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::vector<std::string>*out)
{
jieba.CutForSearch(src,*out);
}
};
cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
通过文档 id 获取对应文档的正排索引对象
cpp
//根据doc_id找到文档对应的DocInfo对象
DocInfo* GetForwardIndex(uint64_t doc_id)
{
if(doc_id>=forward_index.size())
{
std::cerr<<"doc_id out range,error!"<<std::endl;
return nullptr;
}
return &forward_index[doc_id];
}
根据关键词获取关键词对应的倒排拉链
cpp
//根据关键词string,找到关键词对应的倒排拉链
InvertedList* GetInvertedList(const string& word)
{
auto iter= inverted_index.find(word);
if(iter==inverted_index.end())
{
std::cerr<<word<<"have no InvertedList"<<std::endl;
return nullptr;
}
return &(iter->second);
}
编写搜索引擎模块 Searcher
当我们在搜索引擎的搜索框中输入关键词之后进行查询时,返回的网页中一定是含有当前的关键词吗?我们以百度搜索引擎为例。

不难发现,搜索出来的网页中既含有我们搜索框中的关键词,也含有搜索框中关键词的一部分。所以也就说明,当我们在使用关键词搜索时,要先对关键词进行分词,分词之后形成的多个关键词才是我们最终在倒排索引中查找的关键词。
Searcher 模块的编写主要分为三步。
- 创建 index 对象,并进行索引的构建。
- 获取文档 content 的摘要 desc。
- 通过关键词在服务器中进行倒排索引查找,然后通过倒排索引进行正排索引,找到关键词对应的文档的 title,content 和 url,并将这三个内容转为 json 串返回到浏览器。
创建 index 对象,构建索引
cpp
//1.初始化Searcher模块,并通过 raw.txt 构建索引。
void InitSearcher(const std::string &input)
{
index=ns_index::Index::GetInstance();
std::cout<<"获取index单例对象成功"<<std::endl;
index->BuildIndex(input);
std::cout<<"建立正排索引和倒排索引成功"<<std::endl;
}
获取 content 的摘要
此时我们要注意一个点,就是我们最终在浏览器上显示对应的 html 模块时,显示的是文档标题 title,文档内容描述 desc,文档的 url。 所以此时我们要获取的不是 文档的 content,而是文档的 content 进行处理之后的 描述 desc。所以此时我们就要使用 GetDesc 函数,获取 content 的 desc。
GetDesc 的实现逻辑就是,在文档 content 中查找关键词 word 的位置,如果 word 可以通过倒排索引和正排索引获取到一个文档,那么这个文档的 content 中一定是含有关键词 word 的。因为生成关键词的步骤,就是对文档的 title 先进行分词,然后对文档 content 分词之后获得的关键词,而且 content 中是包含 title 的内容的,所以可以说 html 文档所有的关键词 word 产生于文档的 content 中。
cpp
std::string GetDesc(const std::string& content,const std::string& word)
{
//在DocInfo对象中找到InvertedElem中word首次出现的位置
std::size_t pos= content.find(word);
if(pos==std::string::npos)
{
return "none1";
}
std::size_t begin=0;
std::size_t end=content.size()-1;
std::size_t step_front =50;
std::size_t step_back=100;
if(pos-step_front>begin) begin=pos-step_front;
if(pos+step_back<end) end=pos+step_back;
if(begin<end)
{
return content.substr(begin,end-begin);
}
return "none2";
}
通过关键词进行查询,最终返回 json 串
cpp
//2.通过搜索关键词进行查询,最终返回json串。
void Search(const std::string& query_words,std::string*json_string)
{
//1.对搜索关键词进行分词
std::vector<std::string> words;
ns_util::JiebaUtil::CutString(query_words,&words);
//list用于存储所有关键词对应的倒排拉链中的所有的倒排索引结构体对象
std::vector<ns_index::InvertedElem> list;
//2.对分词之后的关键词,查询各个关键词的倒排拉链
for( std::string word:words)
{
//对查询的关键词分词之后的关键词在进行查询时也是不区分大小写的
boost::to_lower(word);
std::vector<ns_index::InvertedElem>* lt= index->GetInvertedList(word);
if(nullptr==lt)
{
std::cout<<"get inverted list error"<<std::endl;
continue;
}
//成功获取到了关键词对应的倒排拉链
//将获取的倒排拉链的每个元素插入到list中
list.insert(list.end(),lt->begin(),lt->end());
//对list中的倒排索引结构体对象进行排序,以weight大小的方式进行降序排序
std::sort(list.begin(),list.end(),[](const ns_index::InvertedElem& em1,const ns_index::InvertedElem& em2){ return em1.weight>em2.weight; });
//依次根据排序之后的倒排索引对象获取该对象内部的id对应的正排索引对象
Json::Value root;
for(auto& em:list)
{
ns_index::DocInfo* doc = index->GetForwardIndex(em.doc_id);
//获取到对应的正派索引结构体对象之后,将对象中的 title,content,url 转成json串并返回
if(nullptr==doc)
{
std::cerr<<"get forward index error"<<std::endl;
continue;
}
Json::Value elem;
elem["title"]=doc->title;
elem["desc"]=GetDesc(doc->content,em.word);
elem["url"]=doc->url;
root.append(elem);
}
//使用writer对象对root对象中的结构化数据进行序列化
Json::StyledWriter writer;
*json_string=writer.write(root);
}
}
项目中期测试
debug 本地测试
cpp
#include"searcher.hpp"
const std::string input="./data/raw_html/raw.txt";
int main()
{
ns_searcher::Searcher searcher;
searcher.InitSearcher(input);
std::string query;
std::string json_string;
while(true)
{
std::cout<<"#请输入关键词"<<std::endl;
std::cin>>query;
searcher.Search(query,&json_string);
std::cout<<json_string<<std::endl;
}
}
bug1
在通过 filesystem 关键词进行查找时,我们发现 filesystem 关键词对应的文档的 content 的 desc 字段变成了 none 1。

这就意味着,我们在构建 content 的 desc 时,没有在 content 中找到我们当前查询的关键词信息。

可是我们在官方文档下进行查找时,我们在对应文档中查找到了对应的关键词呀,可是为什么在运行结果中,没有在对应的文档中找到关键词呢?
经过多次排查最终发现,这是因为 string 的 find 函数其实本质上是区分大小写的,我们的关键词在进行分词之后,是全部转成了小写的,所以在使用 find 函数中使用小写关键词肯定是不能找到大写的关键词的,就导致了最终的 content 的 desc 描述成为了 none1。所以我们不能使用 string 类的 find 函数进行查找,可以使用 C++ 库中的 search 函数进行查找。
对 GetDesc 函数进行第一次调整。
cpp
std::string GetDesc(const std::string& content,const std::string& word)
{
//在DocInfo对象的content中找到InvertedElem中word首次出现的位置
auto iter= std::search(content.begin(),content.end(),word.begin(),word.end(),[](const char &x,const char &y){return std::tolower(x)==std::tolower(y);});
if(iter==content.end())
{
return "none1";
}
std::size_t pos =iter-content.begin();
std::size_t begin=0;
std::size_t end=content.size()-1;
std::size_t step_front =50;
std::size_t step_back=100;
if(pos-step_front>begin) begin=pos-step_front;
if(pos+step_back<end) end=pos+step_back;
if(begin<end)
{
return content.substr(begin,end-begin);
}
return "none2";
}
调整后,再次查看对应文档的 content 的 desc 描述。

不难发现,此时 desc 字段已经具有了数据,此 bug 修复成功。
bug2
在通过关键词 split进行查找时,我们发现 split 对应的文档的 content 的 desc 字段出现了 none2 。
这是为什么呢?
经过多次排查最终发现这是因为在 GetDesc 函数中,我们使用了 size_t 类型的变量进行比较运算,但是在比较的语句中,会有对应的减运算,两个 size_t 类型的变量进行相减操作,最终就会变成一个负数,但是负数对于 size_t 类型而言其实就对应了一个很大的整数,所以在本来条件不满足的时候,条件满足了,最终导致了 begin 和 end 位置的变化,最终就有可能导致 begin 的值比 end 的值大,所以最终就会导致 content 的 desc 描述为 none2 的情况出现。
对 GetDesc 函数进行第二次调整。
cpp
std::string GetDesc(const std::string& content,const std::string& word)
{
//在DocInfo对象的content中找到InvertedElem中word首次出现的位置
auto iter= std::search(content.begin(),content.end(),word.begin(),word.end(),[](const char &x,const char &y){return std::tolower(x)==std::tolower(y);});
if(iter==content.end())
{
return "none1";
}
int pos =iter-content.begin();
int begin=0;
int end=content.size()-1;
int step_front =50;
int step_back=100;
if(pos-step_front>begin) begin=pos-step_front;
if(pos+step_back<end) end=pos+step_back;
if(begin<end)
{
return content.substr(begin,end-begin);
}
return "none2";
}
调整之后,再次查看 对应文档的 content 的 desc 描述。

此 bug 修复成功。
检验查询出来的 html 文档是否是按照关键词的权值进行显示的。
cpp
elem["id"]=(int)doc->doc_id;
elem["weight"]=em.weight;

不难发现,关键词对应的文档最终确实是由权值进行降序排序的。
编写 http_sever 模块
http_sever 本质上就是一个 sever 服务器网络服务,即一个网络进程,可以让其他客户端进程跨网络访问。如果我们使用之前学习的 socket 编程代码自己实现一个 sever 服务器也不是不可以,但是代价太大,我们选择使用 现成的第三方库 cpp-httplib 库(推荐下载v.0.7.15),下载压缩包,然后使用 rz 指令上传至项目目录下,使用 unzip 指令压缩即可获得 cpp-httplib 目录,我们主要使用 cpp-httplib 目录下的 httplib.h 头文件。

同时,在下载好 cpp-httplib 库之后,应该使用较新的 gcc 编译器,centos7下默认为 gcc 4.8.5 版本,大家可以自行查找升级,同时升级之后,使用对应的指令启动最新的 gcc 编译器。
cpp-httplib 测试代码
cpp
#include"cpp-httplib/httplib.h"
const std::string root_path="./wwwroot";
int main()
{
httplib::Server s;
s.set_base_dir(root_path.c_str());
s.Get("/s",[](const httplib::Request& req,httplib::Response& res)
{
res.set_content("hello yjd","text/plain;charset=utf8");
});
s.listen("0.0.0.0",8081);
return 0;
}
其中 root_path 为 sever 服务器返回的网络主页面所对应的目录。

运行 http_sever,使用 client 浏览器客户端连接,访问服务器主页面。

加载对应的资源界面。

编写 http_sever 模块
cpp
#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 searcher;
searcher.InitSearcher(input);
httplib::Server s;
s.set_base_dir(root_path.c_str());
s.Get("/s",[&searcher](const httplib::Request& req,httplib::Response& res)
{
if(!req.has_param("word")){
res.set_content(" 必须要有关键字 ","text/plain; charset=utf-8");
return;
}
std::string word=req.get_param_value("word");
std::cout<<"用户搜索的关键词为: "<<word<<std::endl;
std::string json_string;
searcher.Search(word,&json_string);
res.set_content(json_string,"application/json");
});
s.listen("0.0.0.0",8081);
return 0;
}
在搜索框中通过给 word 字段传入关键词,后端获取到关键词请求之后,在后端进行查询,将查询到的 序列化 json 串返回到浏览器客户端。

编写前端模块
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">
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<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: #6f6e6e;
font-size: 18px;
}
/* 先选中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 {
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;
font-size: 14px;
color: green;
}
</style>
</head>
<body>
<div class="container">
<div class="search">
<input type="text" value="请输入搜索关键字">
<button onclick="Search()">搜索一下</button>
</div>
<div class="result">
<!-- 动态生成网页内容 -->
<!-- <div class="item">
<a href="#">这是标题</a>
<p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
<i>https://www.baidu.com</i>
</div> -->
</div>
</div>
<script>
function Search(){
// 是浏览器的一个弹出框
// alert("hello js!");
// 1. 提取数据, $可以理解成就是JQuery的别称
let query = $(".container .search input").val();
console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据
//2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的
$.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 = $("<i>", {
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>
前端页面以 html+css 技术为基础,使用传统的 javascript 技术进行前后端数据交互太过繁琐,所以我们会使用第三方库 jquery库。通过 jquery 库中的 ajax 函数向后端服务器发送 http 请求,并获取后端服务器返回的响应,获取到响应之后,调用回调函数,最终由 jquery 动态构建前端页面。
为项目添加日志信息
在没有添加日期之前,我们是以标准输出和标准错误的形式去反映代码的执行结果。有了日志信息之后,可以更进一步详细的知道代码的执行情况,以及代码执行到了那里,在哪里出现了错误,迅速进行错误的定位。
添加日志代码如下。
cpp
#pragma once
#include<iostream>
#include<string>
#include<ctime>
#define NORMAL 1
#define WARNING 2
#define DEBUG 3
#define FATAL 4
#define LOG(LEVEL,MESSAGE) log(#LEVEL ,MESSAGE,__FILE__,__LINE__)
void log(std::string level,std::string message,std::string file,int line)
{
std::cout<<"["<<level<<"]"<<"["<<time(nullptr)<<"]"<<"["<<message<<"]"<<"["<<file<<"]"<<"["<<line<<"]"<<std::endl;
}
将项目部署到 LINUX 服务器
将项目部署到 linux 服务器,其实就是让我们当前的服务器端进程在服务器的后台运行,使用的指令如下。
bash
nohup ./http_server > log/log.txt 2>&1 &
nohup 可以将进程输出的日志信息保存在一个自动生成的 nohub.out 文件中,这里将 nuhub 指令将进程输出的日志信息全部重定向输出到了 log/log.txt 中。

此时其实我们关闭了 Xshell ,在浏览器端仍然可以访问 http_server 服务。
项目展示
前端展示:

后端展示:

项目总结

以上便是boost搜索引擎项目的所有内容。