目录
1. 项目的相关背景
2. 搜索引擎的相关宏观原理
3. 搜索引擎技术栈和项目环境
4. 正排索引 vs 倒排索引 - 搜索引擎具体原理
4.1 正排索引
4.2 目标文档进行分词
4.3 倒排索引
4.4 模拟一次查找的过程:
5. 编写数据去标签与数据清洗的模块 Parser
5.1获取相关boost资源
5.2 去标签化思路
5.3 编写parser解析文档
5.2.1整体架构
5.2.2 EnumFile接口实现
5.2.3 ParseHtml接口实现
5.2.3.1框架
5.2.3.2 ns_util::FileUtil::ReadFile();
5.2.3.3 ParseTitle();
5.2.3.4 ParseContent();
5.2.3.5 ParseUrl();
5.2.3.6 测试
5.2.4 SaveHtml()接口实现
5.4 解决小bug
5.5 结果
6.编写建立索引的模块 Index
6.1 建立索引的基本代码框架
6.2 索引查找接口的实现GetforwardIndex();和Getinverted_index();
6.3 构建索引代码结构的实现BulidIndex();
6.3.1 正排索引函数BulidForWardIndex();
6.3.1.1 正排索引代码基本结构
6.3.1.2 切分字符串-boost库split函数使用
6.3.1.3 ns_util::StringUtil::Split
6.3.2 建立倒排索引 BuildInvertedIndex();
6.3.2.1 倒排索引的思路结构
编辑6.3.2.2 分词工具 cppjieba 介绍
6.3.2.3 编写倒排索引的代码
6.4 将索引类设计成为单例模式
6.4.1 私有化构造函数和delete拷贝构造
6.4.2 编写单例接口
7. 编写搜索引擎模块 Searcher
7.1 基本代码结构
7.2 对InitSearcher接口的实现
7.3 对Search接口的实现
7.3.1安装josncpp及使用示例
7.3.2 Search()编写
7.3.3实现获取内容摘要接口GetDesc();
8. 编写http_server模块
8.1测试版debug.cc
8.2网络版http_server.cc
8.2.1升级gcc
8.2.2 引入cpp-httplib库
8.2.3 使用cpp-httplib库
8.2.4 编写搜索服务端http_server.cc
9. 编写前端模块
9.1 了解 vscode
编辑
9.2 了解前端
9.3 Html网页结构书写
9.3.1boost搜索引擎网页基本结构
编辑9.3.2 HTML网页代码基本实现
9.4 CSS网页样式设置
9.4.1 设置网页总体样式
9.4.2 设置搜索框样式
编辑9.4.3 设置搜索结果样式
9.5 编写前后端交互JS代码
9.5.1 增加搜索功能
9.5.2 引入JQuery库
9.5.3 网页Search()搜索函数书写
10. 解决搜到重复内容的问题
编辑
11.添加日志
11.1日志代码
11.2进行日志部署
11.2.1在index.hpp中进行日志部署
11.2.2在searcher.hpp中进行日志部署
11.2.3在http_server.cc中进行日志部署
11.2.4日志打印测试
11.3 部署服务到 linux 上
11.3.1 nohup指令
11.3.2 使用nohup指令进行服务和日志部署
12. 结项与项目扩展方向
备注
1. 项目的相关背景
在如今的信息时代下,检索信息成为几乎人人的"必需品",在此大背景下,出现了百度、搜狗、360搜索、头条新闻客户端等大型的搜索引擎。
站内搜索:搜索的数据更垂直,数据量其实更小
类似于cplusplus.com的搜索
我们可以通过做一个微型的搜索引擎,明晰搜索引擎的运行原理。
boost作为C++的准标准库,在C++代码编写中使用频率很高,但是在官方的网站中,却没有站内搜索,并不便于用户的快速查找。
2. 搜索引擎的相关宏观原理
步骤概览
1.数据收集:
使用合法的爬虫程序,在全网中抓取相关的html网页信息。并保存这些页面的HTML内容到服务器端的磁盘中。(由于爬虫程序,涉及法律,技术等因素限制,所以我们暂时只爬取一个boost库官方网站,且通过正规渠道下载boost库的相关文件。)
2. 数据预处理:对收集到的HTML文件进行处理,去除HTML标签,提取出网页的标题(title)、内容(content)和链接(url)。
3.建立索引:将处理后的数据建立索引,以便快速检索。索引可能是一个数据库(如Elasticsearch、MySQL等),其中包含了所有提取出来的title、content和url信息,并且这些数据是以一种能够高效执行搜索查询的方式存储的。
4. 搜索服务:实现一个搜索接口,该接口能够接受来自客户端(例如,用户的浏览器)的HTTP请求,并在索引中查找与请求中的关键词匹配的内容。(搜索服务应提供一种机制来评分和排序搜索结果,确保最相关和最准确的结果首先被返回。)
5.结果展示:一旦搜索服务找到匹配的结果,它会生成一个新的HTML页面,该页面包含了搜索结果的相关信息(title、content的摘要和url)。然后,这个新页面会被发送回客户端的浏览器,供用户查看。
3. 搜索引擎技术栈和项目环境
- 后端:C/C++ C++11, STL, 准标准库Boost,Jsoncpp,cppjieba,cpp-httplib
- 前端: html5,css,js、jQuery、Ajax
- 项目环境: Centos 7云服务器,vim/gcc(g++)/Makefile , vs2019 or vs code
4. 正排索引 vs 倒排索引 - 搜索引擎具体原理
- 文档1: 雷军买了四斤小米
- 文档2: 雷军发布了小米手机
4.1 正排索引
正排索引:就是从文档ID找到文档内容(文档内的关键字)
正排索引类似于书的目录,我们可以根据页数查找到对应的内容
文档ID | 文档内容 |
1 | 雷军买了四斤小米 |
2 | 雷军发布了小米手机 |
4.2 目标文档进行分词
文档分词目的:方便建立倒排索引和查找:
- 文档1[雷军买了四斤小米 ]: 雷军/买/四斤/小米/四斤小米
- 文档2[雷军发布了小米手机]:雷军/发布/小米/小米手机
注意:停止词:了,的,吗,a,the,一般我们在分词的时候可以不考虑
4.3 倒排索引
倒排索引:根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
倒排索引和正排索引是相反的概念,我们可以根据文档内容查询到这部分内容在哪些文件中出现,从而找到对应的文件。
关键字(具有唯一性) | 文档ID, weight(权重) |
雷军 | 文档1, 文档2 |
买 | 文档1 |
四斤 | 文档1 |
小米 | 文档1, 文档2 |
四斤小米 | 文档1 |
发布 | 文档2 |
小米手机 | 文档2 |
注意:后续我们倒排索引找到文档ID后,在网页中,需要按照权重对不同文档进行先后的排序显示,所以我们在倒排索引这里需要增加权重weight信息项。
4.4 模拟一次查找的过程:
用户输入:小米 -> 倒排索引中查找 -> 提取出文档ID(1,2) -> 根据正排索引 -> 找到文档的内容 ->
title+conent(desc)+url 文档结果进行摘要->构建响应结果
大致过程就是先使用倒排索引,通过关键字找到文档ID。再使用正排索引,通过文档ID找到文档内容。
5. 编写数据去标签与数据清洗的模块 Parser
5.1获取相关boost资源
(1)进入官网 https://www.boost.org 进行相应资源下载(我们以boost1.84为例)
目前只需要boost_1_84_0/doc/html目录下的html文件,用它来进行建立索引
(2)下载后载入云服务器中
PS:这里需要使用lrzsz进行云服务的文件上传 , 如果没有安装可以执行 sudo yum install lrzsz 指令进行安装。
然后我们使用以下指令进行解包:
tar xzf boost_1_84_0.tar.gz
我们只需要boost_1_84_0/doc/html/* 内的所有网址即可:
cp -rf boost_1_84_0/doc/html/* data/input/
然后删除boost_1_84_0即可
我们只需要:boost_1_79_0/doc/html 目录下的html文件,用它来进行建立索引
5.2 去标签化思路
我们随便打开一个.html原生文件,观察其内容
我们的目的是提取出一个网页文件的 title + content + url,得到每一个网页的去标签化内容。
所以就需要过滤掉<...>等并不需要的标签内容 ,
最终将每个网页文件的主体三件套 title + content + url 进行保存。
查看一共多少html文件
目标:就是把每个文档内容都进行解析去标签化后,写入raw_html文件夹中的同一个文件当中。每个文档内容不需要任何\n!文档和文档之间用 \3 区分
version1:
类似:XXXXXXXXXXXXXXXXX\3YYYYYYYYYYYYYYYYYYYYY\3ZZZZZZZZZZZZZZZZZZZZZZZZZ\3写入文件中,一定要考虑下一次在读取的时候,也要方便操作!
因此我们采用下面的方案:
version2:
类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...
方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3url
- 当我们需要保存网页数据时,选择使用特定的分隔符是非常重要的。在这里,我们选择了'\3'作为分隔符,原因在于'\3'在ASCII编码中是一个控制字符,它不会在文档中显示出来。这意味着,当我们使用'\3'来分隔网页数据时,它不会干扰或污染实际的网页内容,因为控制字符在大多数应用中都是不可见的。
- 由于我们处理的网页内容(如data/input目录下的HTML文件)主要包含可打印字符,很少包含不可见的控制字符,因此使用'\3'作为分隔符可以确保数据的清晰和准确。
这样,我们可以确信在解析或处理这些数据时,分隔符不会与实际的网页内容混淆,从而提高了数据处理的可靠性。
5.3 编写parser解析文档
5.2.1整体架构
#include <iostream>
#include <string>
#include <vector>
//目录,下面放的是所有的html网页
const std::string src_path = "data/input/";
const std::string output = "data/raw_html/raw.txt";
typedef struct DocInfo{
std::string title;//文档标题
std::string content;//文档内容
std::string url;//该文档在官网中的url
}DocInfo_t;
//格式规范:
//const &: 输入
//*: 输出
//&:输入输出
// 运用boost库进行读取
bool EnumFile(const std::string &src_path,std::vector<std::string> *file_list);
//解析标签
bool PraseHtml(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文件名带路径,保存到files_list中,方便后期进行一个一个的文件进行读取
if(!EnumFile(src_path,&file_list))
{
std::cerr << "enum file name error!" << std::endl;
return 1;
}
//第二步: 按照files_list读取每个文件的内容,并进行解析
std::vector<DocInfo_t> results;
if(!PraseHtml(file_list,&results))
{
std::cerr << "prase html error!" << std::endl;
return 2;
}
//第三步: 把解析完毕的各个文件内容,写入到output,按照\3作为每个文档的分割符
if(!Savehtml(results,output))
{
std::cerr << "sava html error" << std::endl;
return 3;
}
return 0;
}
bool EnumFile(const std::string &src_path,std::vector<std::string> *file_list)
{
return true;
}
bool PraseHtml(const std::vector<std::string> &file_list,std::vector<DocInfo_t> *results)
{
return true;
}
bool Savehtml(const std::vector<DocInfo_t> &results,const std::string& output)
{
return true;
}
5.2.2 EnumFile接口实现
要实现EnumFile接口,就是要在/data/input/文件夹下 , 提取每个html网页文件的路径名称。
这时候就需要借助boost库中的接口来完成这一任务。
我们需要使用filesystem函数(boost库中的)
boost库安装
sudo yum install -y boost-devel
//devel是boost开发库
这里做一个区分,我们做站内搜索的版本是1.84,我们写代码要使用的boost库是1.53版本,这两个不冲突。
在boost官方中可以找到相应接口的使用手册: (如下是进入方法)
可以先通过Tutorial 粗略了解各个接口的作用,然后到Rererence做具体了解。
下面开始我们的编写EnumFile接口的代码部分:
首先包含头文件:#include<boost/filesystem.hpp>
#include<boost/filesystem.hpp>
bool EnumFile(const std::string &src_path,std::vector<std::string> *file_list)
{
namespace fs = boost::filesystem;
fs::path rootpath(src_path);
//判断路径是否存在,不存在,就没有必要再往后走了
if(!fs::exists(rootpath))
{
std::cerr<< src_path << std::endl;
return false;
}
//定义一个空的迭代器,用来进行判断递归结束
fs::recursive_directory_iterator end;
for(fs::recursive_directory_iterator iter(rootpath); iter != end; iter++)
{
//判断文件是否是普通文件,html都是普通文件
if(!fs::is_regular_file(*iter)){
continue;
}
if(iter->path().extension() != ".html"){
//判断文件路径名的后缀是否符合要求
continue;
}
std::cout << "debug: " << iter->path().string() << std::endl;
//当前的路径一定是一个合法的,以.html结束的普通网页文件
file_list->push_back(iter->path().string());//将所有带路径的html保存在files_list,方便后续进行文本分析
}
return true;
}
我们进行测试一下:
makefile文件:
parser:parser.cc
g++ -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
.PHONY:clean
clean:
rm -f parser
注意:这里要带上-lboost_system以及-lboost_filesystem选项
可以看到EnumFile已经把data/input/目录下的所有.html文件名路径,读取到了file_list中,
下一步就是根据所有的文件名路径,打开每一个.html文件,读取+解析文件(数据清洗,去标签化)
只提取出每个文件的title content url,再将一个个提取内容保存到result中。
5.2.3 ParseHtml接口实现
5.2.3.1框架
bool PraseHtml(const std::vector<std::string> &file_list,std::vector<DocInfo_t> *results)
{
for(const std::string &file:file_list)
{
//1.读取文件,Read();
std::string result;
if(!ns_util::FileUtil::ReadFile(file, &result)){
continue;
}
DocInfo_t doc;
//2.解析指定的文件,提取title
if(!ParseTitle(result,&doc.title)){
continue;
}
//3. 解析指定的文件,提取content,就是去标签
if(!ParseContent(result,&doc.content)){
continue;
}
//4. 解析指定的文件路径,构建url
if(!ParseUrl(file,&doc.url)){
continue;
}
//5. 完成了解析任务,当前文档的相关结果都保存在了doc里面
results->push_back(doc);//bug:todo;细节,push_back本质会发生拷贝,效率可能会比较低
}
return true;
}
5.2.3.2 ns_util::FileUtil::ReadFile();
读取文件函数
namespace ns_util{
class FileUtil{
public:
static bool ReadFile(const std::string &file_path, std::string* out)
{
std::ifstream in(file_path,std::ios::in);
if(!in.is_open())
{
std::cerr << "open file" << file_path << std::endl;
return false;
}
std::string line;
while (std::getline(in,line))//如何理解getline读取到文件结束呢??getline的返回值是一个&,while(bool), 本质是因为重载了强制类型转化
{
*out += line;
}
in.close();
return true;
}
};
}
如何理解getline读取到文件结束呢?
getline返回的是一个&,本质是因为重载了强制类型转化
5.2.3.3 ParseTitle();
提取标签函数
bool ParseTitle(const std::string& file, std::string* title)
{
std::size_t begin = file.find("<title>");
if(begin == std::string::npos)
return false;
std::size_t end = file.find("</title>");
if(end == std::string::npos)
return false;
begin += std::string("<title>").size();
if(begin > end)
return false;
*title = file.substr(begin,end - begin);//从begin开始向后截取end - begin个字符
return true;
}
5.2.3.4 ParseContent();
获取文档的Content(解析去标签)
bool ParseContent(const std::string& file, std::string* content)
{
//去标签,基于一个简易的状态机
enum status
{
LABLE,
CONTENT
};
enum status s = LABLE;
for(char c:file)
{
switch (s)
{
case LABLE:
if(c == '>') s = CONTENT;
break;
case CONTENT:
if(c == '<') s = LABLE;
else
{
//我们不想保留原始文件中的\n,因为我们想用\n作为html解析之后文本的分隔符
if(c == '\n') c = ' ';
content->push_back(c);
}
break;
default:
break;
}
}
return true;
}
5.2.3.5 ParseUrl();
构建URL
boost库的官方文档,和我们下载下来的文档,是有路径对应关系的
官网URL样例:https://www.boost.org/doc/libs/1_78_0/doc/html/accumulators.html
我们下载下来的url样例:boost_1_78_0/doc/html/accumulators.html
我们拷贝到我们项目中的样例:data/input/accumulators.html //我们把下载下来的boost库 doc/html/* copy data/input/
url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html";
url_tail = [data/input](删除) /accumulators.html -> url_tail = /accumulators.html
url = url_head + url_tail ; 相当于形成了一个官网链接
const std::string src_path = "data/input";
bool ParseUrl(const std::string &filepath, std::string* url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_84_0/doc/html";
std::string url_tail = filepath.substr(src_path.size());
*url = url_head + url_tail;
return true;
}
5.2.3.6 测试
//for debug
static void ShowDoc(const DocInfo_t &doc)
{
std::cout << "title: " << doc.title << std::endl;
std::cout << "content: " << doc.content << std::endl;
std::cout << "url: " << doc.url << std::endl;
}
bool PraseHtml(const std::vector<std::string> &file_list,std::vector<DocInfo_t> *results)
{
for(const std::string &file:file_list)
{
//1.读取文件,Read();
std::string result;
if(!ns_util::FileUtil::ReadFile(file, &result)){
continue;
}
DocInfo_t doc;
//2.解析指定的文件,提取title
if(!ParseTitle(result,&doc.title)){
continue;
}
//3. 解析指定的文件,提取content,就是去标签
if(!ParseContent(result,&doc.content)){
continue;
}
//4. 解析指定的文件路径,构建url
if(!ParseUrl(file,&doc.url)){
continue;
}
//5. 完成了解析任务,当前文档的相关结果都保存在了doc里面
results->push_back(doc);//bug,push_back本质会发生拷贝,效率可能会比较低
//for debug
ShowDoc(doc);
break;//打印一份测试就行
}
return true;
}
5.2.4 SaveHtml()接口实现
SaveHtml()接口实现将解析结果写入到文件当中
目标:将results,写入到output中
写入文件中,一定要考虑下一次在读取的时候,也要方便操作!
因此我们采用下面的方案:
version2:
类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...
方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3url
bool Savehtml(const std::vector<DocInfo_t> &results,const std::string& output)
{
#define SEP '\3'
//我们要将结果写入到output路径下的文件,首先打开output路径下的文件
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 outstring;
outstring += item.title;
outstring += SEP;
outstring += item.content;
outstring += SEP;
outstring += item.url;
outstring += '\n';
out.write(outstring.c_str(),outstring.size());
}
out.close();
return true;
}
5.4 解决小bug
因为ParseHtml接口中,解析数据后放入results中,results是一个vector容器,存放的是DocInfo_t类型而不是其指针类型。每次循环之后,我们都要push_back(doc),其本质是拷贝,效率低下,影响性能。
因此我们使用move函数减少拷贝
move:作为右值移动
将我们所要拷贝的对象,在地址空间层面上,让我们对象直接和容器当中的成员相关联,也就是说不会发生太多的拷贝
bool PraseHtml(const std::vector<std::string> &file_list,std::vector<DocInfo_t> *results)
{
for(const std::string &file:file_list)
{
//1.读取文件,Read();
std::string result;
if(!ns_util::FileUtil::ReadFile(file, &result)){
continue;
}
DocInfo_t doc;
//2.解析指定的文件,提取title
if(!ParseTitle(result,&doc.title)){
continue;
}
//3. 解析指定的文件,提取content,就是去标签
if(!ParseContent(result,&doc.content)){
continue;
}
//4. 解析指定的文件路径,构建url
if(!ParseUrl(file,&doc.url)){
continue;
}
//5. 完成了解析任务,当前文档的相关结果都保存在了doc里面
// results->push_back(doc); //push_back()本质是拷贝,效率低
results->push_back(std::move(doc));//bug:由于results容器存的是DocInfo_t类型而不是其指针类型,所以push_back本质会发生拷贝,效率可能会比较低,我们使用move函数减少拷贝
//for debug
// ShowDoc(doc);//可以试试使用move后打印结果的影响
// break;//打印一份测试就行
}
return true;
}
5.5 结果
综上,我们就有了去标签化的各个网页文件的主体内容(title+content+url)了(如下图)。
这里的^c就是\3
完成以上的解析去标签化工作,就可以给建立索引提供基本的数据源了。
6.编写建立索引的模块 Index
建立索引,本质上是为了创建一种既能存储信息又能高效检索的数据结构。这种结构可以极大地提升我们根据关键字快速定位到相关文档ID,进而获取文档内容的效率。正如本文第四部分“正排索引与倒排索引——搜索引擎基本原理”中所阐释的,我们需要同时构建正排索引和倒排索引,以实现这一目的。
简单来说,正排索引能够帮助我们根据文档ID迅速找到对应的文档内容,而倒排索引则是根据文档中的关键字反向映射到包含这些关键字的文档ID,从而支持快速的关键字搜索。这两种索引方式相辅相成,共同构成了现代搜索引擎的基石。
6.1 建立索引的基本代码框架
index.hpp基本框架
我们使用vector作为构建正排索引的容器,则vector的下标就是天然的文档ID
使用unordered_map作为构建倒排索引的容器:通过关键word,从而快速查找到倒排文档信息拉链。
对索引Index对象,我们有三个核心接口,分别是在正排索引的查找,在倒排索引的查找,构建索引(包括构建正排和倒排索引结构)。
#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
namespace ns_index
{
//文档
struct DocInfo
{
std::string title;
std::string content;
std::string url;
uint64_t doc_id;//文档id //
};
//倒排对应的节点
struct InvertedElem
{
std::string word;//关键字
uint64_t doc_id;//文档id
int weight;//权重
};
//倒排拉链
typedef std::vector<InvertedElem> InvertedList;
class Index
{
private:
//正排索引的数据结构用数组,数组的下标天然是文档的ID
std::vector<DocInfo> forward_index;//正排索引
//倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string,InvertedList> inverted_index;//关键字与文档id = 1:n
public:
Index(/* args */);
~Index();
//doc_id找到文档内容
DocInfo* GetforwardIndex(const uint64_t doc_id)
{
return nullptr;
}
//关键字查找倒排拉链
InvertedList* GetInvertedList(const std::string &word)
{
return nullptr;
}
//根据去标签,格式化之后的文档,构建正排和倒排索引
//data/raw_html/raw.txt
bool BulidIndex(const std::string& input)
{
return true;
}
};
}
6.2 索引查找接口的实现GetforwardIndex();和Getinverted_index();
//doc_id找到文档内容
DocInfo* GetforwardIndex(const uint64_t doc_id)
{
if(doc_id > forward_index.size()){
std::cerr << "doc_id is out range,error!" << std::endl;
return nullptr;
}
return &forward_index[doc_id];
}
//关键字查找倒排拉链
InvertedList* GetInvertedList(const std::string &word)
{
auto iter = inverted_index.find(word);
if(iter == inverted_index.end()){
std::cerr << word << "have no InvertedList" << std::endl;
return nullptr;
}
// return &inverted_index[word];
return &(iter->second);
}
6.3 构建索引代码结构的实现BulidIndex();
//根据去标签,格式化之后的文档,构建正排和倒排索引
//data/raw_html/raw.txt
bool BulidIndex(const std::string& input)//parse处理完毕的数据交给我
{
std::ifstream in(input,std::ios::in | std::ios::binary);//
if(!in.is_open()){
std::cerr << "sorry, " << input << " open error" << std::endl;
return false;
}
std::string line;
while (std::getline(in,line))
{
DocInfo* doc = BulidForWardIndex(line);
if(doc == nullptr){
std::cerr << "build" << line << "error!" << std::endl;
continue;
}
}
BuildInvertedIndex(*doc);
return true;
}
};
private:
DocInfo* BulidForWardIndex(const std::string &line)
{
return nullptr;
}
bool BuildInvertedIndex(const DocInfo& doc)
{
return false;
}
6.3.1 正排索引函数BulidForWardIndex();
6.3.1.1 正排索引代码基本结构
DocInfo* BulidForWardIndex(const std::string &line)
{
//1. 解析line,字符串切分
//line -> 3 string, title, content, url
std::vector<std::string> results;
const std::string sep = "\3";
ns_util::StringUtil::Split(line, &results, sep);
if(results.size() != 3){
return nullptr;
}
//2. 字符串进行填充到DocIinfo
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));
return &forward_index.back();
}
上面用到ns_util::StringUtil::Split函数,其实是boost库split函数,下面我们来进行讲解
6.3.1.2 切分字符串-boost库split函数使用
我们先举例使用boost::split分词
我们首先需要引头文件#include<boost//algorithm/boost.hpp>
#include <iostream>
#include <vector>
#include <string>
#include <boost/algorithm/string.hpp>
const std::string line = "###############";
int main() {
std::string input = "apple,banana,orange,,,grape";
std::vector<std::string> result1,result2,result3;
// 使用boost::split()函数将input字符串按照逗号分隔符分割,并将结果存储到result容器中
boost::split(result1, input, boost::is_any_of(","));
// 输出分割后的结果
for (const auto& fruit1 : result1) {
std::cout << fruit1 << std::endl;
}
std::cout << line << std::endl;
boost::split(result2, input, boost::is_any_of(","),boost::token_compress_on);
// 输出分割后的结果
for (const auto& fruit2 : result2) {
std::cout << fruit2 << std::endl;
}
std::cout << line << std::endl;
boost::split(result3, input, boost::is_any_of(","),boost::token_compress_off);
// 输出分割后的结果
for (const auto& fruit3 : result3) {
std::cout << fruit3 << std::endl;
}
std::cout << line << std::endl;
return 0;
}
运行结果:
可以看到我们可以通过第四个参数来确定是否要忽略重复分隔符的情况。
6.3.1.3 ns_util::StringUtil::Split
学习了-boost库split函数使用,下面我们来编写util.hpp的切分字符串函数
首先需要引头文件#include<boost//algorithm/boost.hpp>
#include <boost/algorithm/string.hpp>
namespace ns_util{
class StringUtil{
public:
//target:切分目标;out:目的地;sep:分隔符
//这个方法需要被外部直接使用,所以要加static
static void Split(const std::string &target,std::vector<std::string>* out, const std::string& sep)
{
//boost/split:切分字符串
//out目的地;target:切分目标;boost::is_any_of("分隔符");boost::token_compress_on:切分是否间隔
boost::split(*out,target,boost::is_any_of(sep),boost::token_compress_on);//第四个参数设置为on是遇到多个sep整合成一个
}
};
}
6.3.2 建立倒排索引 BuildInvertedIndex();
6.3.2.1 倒排索引的思路结构
我们对于每一个获取到的文档信息title+content+url,建立倒排索引。
1.首先我们对一个文档的title+content进行分词(借助于jieba分词工具)
2.我们对分词后得到的多个词段,进行词频统计,得到每一个词段的在标题/内容的出现次数
3.知道了在文档中,标题和内容每个词出现的次数,就可以根据<词key_word,频次word_cnt> map表,自定义相关性。
4. 填充单个倒排词段信息,插入到倒排索引。
注意:一个文档中的一个key_word关键词的权重weight信息的计算:
就是根据这个文档中该关键词在标题title 内容content中出现的次数,
再自定义相关性计算出该关键词key_word的分量weight,再填充词段信息weight。
6.3.2.2 分词工具 cppjieba 介绍
#1.Jieba库的安装和使用
我们进入GitHub来获取cppjieba分词工具资源:
GitCode - 开发者的代码家园
我们看一下cppjieba工具包的具体构造
解决jiaba库的链接使用问题
其中我们需要连接使用的是如下三个:
cppjieba/include (需要建立对/include目录下文件的链接)
cppjieba/dict(词库 ) (需要建立对/dict目录下文件的链接,从而获取jieba分词时的字典根据)
cppjibea/deps/limonp (这是cppjiaba库的坑,需要将此目录拷贝到cppjieba/include/cppjieba)
下面我们通过举例测试使用cppjieba工具,了解cppjiba库的基本使用
cppjieba的使用方法路径:我们使用jieba库中的demo.cpp进行测试,当然我们也要首先解决jiba库的链接问题。
我们拷贝demo.cpp到和cppjieba一个目录下进行测试:
解决jieba库的链接问题
- 链接问题实际上是要解决在一个.cpp源文件中要找到库的链接路径
- 我们软连接ln -s 库路径,实际上就是在为源文件.cpp寻找到库路径提供便利
下面我们对修改源文件demo.cpp的对库的链接获取路径:
这里我们发现了一个问题:我们直接下载的cppjieba/deps/limonp实际上是空文件夹,
这就需要我们重新专门下载limonp文件夹。
git clone https://github.com/yanyiwu/limonp.git
得到limonp文件夹 按照如下操作就可以了
然后我们将再下载的limonp/include/limonp文件夹直接复制更新到cppjieba当中。
开始测试:
可以看到解决了库资源链接问题以及limonp更新问题,就可以编译成功jieba库示例的demo.cpp了!
#2.引入Jieba库到项目
我们先将完整的,更新limonp的jieba库 , 拷贝到专门的资源仓库文件夹中。
建立对jieba库的软连接
根据demo.cpp的对字符串的分词示例,设计util.hpp中Jieba分词接口
#3.工具集util.hpp中对于Jieba分词接口的实现
首先在util.hpp中#include"cppjieba/Jieba.hpp"(注意路径位置)
#include "cppjieba/Jieba.hpp"
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;//jieba对象定义成静态的,这样就不用每次使用都创建一个jieba对象
public:
static void Splict(const std::string& src, std::vector<std::string>* out)//要使用静态的jieba对象,函数也必须是静态的
{
jieba.CutForSearch(src, *out);
}
};
//类静态成员函数,要在类外初始化
//类型 成员 初始化
cppjieba::Jieba jiebaUtil::jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
}
6.3.2.3 编写倒排索引的代码
namespace ns_index
{
....
class Index
{
....
bool BuildInvertedIndex(const DocInfo& doc)
{
//DocInfo{title, content, url, doc_id}
//word -> 倒排拉链
struct word_cnt{
int title_cnt;
int content_cnt;
word_cnt():title_cnt(0),content_cnt(0){}
};
std::unordered_map<std::string, word_cnt> word_map;//用来暂存词频的映射表
//对标题进行分词
std::vector<std::string> title_words;
ns_util::jiebaUtil::CutString(doc.title, &title_words);
//对标题进行词频统计
for(std::string s: title_words)
{
boost::to_lower(s);//需要统一转化成为小写
word_map[s].title_cnt++;//如果存在就获取,如果不存在就新建并从0开始计数
}
//对文档内容进行分词
std::vector<std::string> content_words;
ns_util::jiebaUtil::CutString(doc.content, &content_words);
//对内容进行词频统计
for(std::string s: content_words)
{
boost::to_lower(s);//需要统一转化成为小写
word_map[s].content_cnt++;//如果存在就获取,如果不存在就新建并从0开始计数
}
#define X 10
#define Y 1
//Hello,hello,HELLO
for(auto &word_pair:word_map)
{
//一个关键词对应的其中一个文档id+weight信息
InvertedElem item;
item.doc_id = doc.doc_id;
item.word = word_pair.first;
item.weight = X*word_pair.second.title_cnt + Y*word_pair.second.content_cnt;//相关性
//<字符串,数组>,,,,插入数组元素
InvertedList &inverted_list = inverted_index[word_pair.first];//获取关键词为word_pair.first对应的倒排拉链的引用,没有的话新建
inverted_list.push_back(std::move(item));//增加word_pair.first对应的倒排拉链的数量
}
return true;
}
};
}
说明:在进行搜索时,我们并不需要关注关键词的大小写,因此,在统计关键词时,我们习惯上将所有的分词统一转换为小写(使用 boost::to_lower )。这意味着,在我们的倒排索引中,用于查找的关键词(即倒排表的左侧)都是小写形式。当用户在索引中进行搜索时,我们也会自动将输入的搜索词转换为小写进行检索。这样做可以确保搜索的准确性和一致性,无论用户输入的是大写、小写还是混合大小写。
以上就是我们建立索引部分的过程。
索引就是一个存储 + 查找的数据结构(其实就是填充两张表)
我们思考一下索引类,我们需要建立多个索引对象吗?
- 我们实际上并不需要创建多个索引对象来完成查找任务。一个索引对象足以应对所有的搜索需求,因为它包含了所有必要的正排和倒排表,从而支持对网页信息的快速检索。
- 创建一个索引对象的成本是相当高的。这涉及到对大量网页信息的分词、统计、填充和插入等操作,这些步骤在效率和资源消耗上都是不小的挑战。
为了优化性能和资源利用,我们将索引对象设计为单例模式。这意味着在整个应用程序运行期间,索引对象只会被创建一次,并在需要时被重复使用。这样不仅减少了创建和销毁索引对象的开销,还确保了数据的一致性和完整性。通过采用单例模式,我们能够有效地管理索引对象,提高搜索效率,同时减少资源消耗。
6.4 将索引类设计成为单例模式
6.4.1 私有化构造函数和delete拷贝构造
6.4.2 编写单例接口
注意:这里使用锁要#include <mutex>
类外初始化静态成员
7. 编写搜索引擎模块 Searcher
7.1 基本代码结构
#pragma once
#include "index.hpp"
#include "util.hpp"
namespace ns_searcher
{
class Searcher
{
private:
ns_index::Index *index; // 供系统进行查找的索引
public:
Searcher() {}
~Searcher() {}
public:
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进行按照searcher的要求进行分词
// 2.[触发]:就是根据分词的各个"词",进行index查找
// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
// 4.[构建]:根据查找出来的结果,构建json串 -- jsoncpp
}
};
}
7.2 对InitSearcher接口的实现
void InitSearcher(const std::string &input)
{
// 1. 获取或者创建index对象
index = ns_index::Index::GetInstance();
// 2. 根据index对象建立索引
index->BulidIndex(input);
}
7.3 对Search接口的实现
我们对搜索的关键字在服务端进行分词,然后才能进行查找index。
7.3.1安装josncpp及使用示例
sudo yum install -y jsoncpp-devel
- 安装完成后,项目中加入头文件#include <jsoncpp/json/json.h>
- 编译命令后面加上-ljsoncpp
7.3.2 Search()编写
// query: 搜索关键字
// json_string: 返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{
// 1.[query分词]:对我们的query进行按照searcher的要求进行分词
std::vector<std::string> words;
ns_util::jiebaUtil::CutString(query, &words);
// 2.[触发]:就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索关键字也需要
ns_index::InvertedList inverted_list_all; // 内部InvertedElem
for (std::string word : words)
{
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if (nullptr == inverted_list)
{
continue;
}
inverted_list_all.insert(inverted_list_all.end(),inverted_list->begin(),inverted_list->end());
}
// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
std::sort(inverted_list_all.begin(),inverted_list_all.end(),\
[](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2){//
return e1.weight > e2.weight;//这种情况称之为降序
});
// 4.[构建]:根据查找出来的结果,构建json串 -- 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.word);//doc->content是文档去标签之后的内容,我们要的是截取其中一部分
elem["url"] = doc->url;
root.append(elem);
}
Json::StyledWriter writer;
*json_string = writer.write(root);
}
ns_util::jiebaUtil::CutString:
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;//jieba对象定义成静态的,这样就不用每次使用都创建一个jieba对象
public:
static void CutString(const std::string& src, std::vector<std::string>* out)//要使用静态的jieba对象,函数也必须是静态的
{
jieba.CutForSearch(src, *out);
}
};
//类静态成员函数,要在类外初始化
//类型 成员 初始化
cppjieba::Jieba jiebaUtil::jieba(DICT_PATH,
HMM_PATH,
USER_DICT_PATH,
IDF_PATH,
STOP_WORD_PATH);
}
7.3.3实现获取内容摘要接口GetDesc();
为什么需要摘要?
因为经过我们的测试,搜索出来的数据量很大,不是很美观。
获取内容摘要接口注意点:
(1)我们的建立的索引Index,其中的倒排表实际上是统一按照小写词段进行查找的,也就是说,我们倒排索引表的左侧是小写的词。
我们搜索到的倒排拉链里面的文档ID信息里面存储的也是小写化的搜索关键词word。
(2)而索引Index.其中的正排表中,文档id对应的文档内容,doc_info里面的_content内容,却是不区分大小写的。
所以我们不能直接用小写化的关键词word在content中find搜索,大小写不匹配,这是可能找不到包含该关键词的语句的。
我们应该在content中寻找word时,在检查对比时,统一小写化对比。
这里我们使用C++ <algorithm>中的search接口解决。
获取摘要
std::string GetDesc(const std::string& html_content,const std::string& word)
{
//找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)
//截取出这部分内容
const int prev_step = 50;
const int next_step = 100;
//1.找到首次出现
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 , std::size_t 无符号整数
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. 截取子串,return
if(start >= end) return "None2";
std::string desc = html_content.substr(start,end - start);
desc += "...";
return desc;
}
8. 编写http_server模块
8.1测试版debug.cc
#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;//返回序列化后的结果
char buffer[1024];
while (true)
{
std::cout << "Please Enter Your Search Query@ ";
fgets(buffer, sizeof(buffer), stdin);//用数组来存放query,如果用cin的话输入空格会被分开
buffer[strlen(buffer) - 1] = 0;
query = buffer;
search->Search(query, &json_string);
std::cout << json_string << std::endl;
}
return 0;
}
改写编译规则Makefile
PARSER=parser
DUG=debug
cc=g++
.PHONY:all
all:$(PARSER) $(DUG)
$(PARSER):parser.cc
$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
$(DUG):debug.cc
$(cc) -o $@ $^ -ljsoncpp -std=c++11
.PHONY:clean
clean:
rm -f $(PARSER) $(DUG)
测试:
可以看到,当我们输入关键词split,就成功将对应的文档信息显示出来,并且按照权重进行倒排排序。
8.2网络版http_server.cc
8.2.1升级gcc
cpp_httplib在使用的时候需要使用较新版本的gcc(大约是7.0以上的版本)
centos 7默认版本是4.8.5
搜索:scl gcc devsettool升级gcc
我们更新gcc需要用到三条指令 + 一个配置 (需要用scl来启动gcc 后续的配置是方便使用)
前三步:
//安装scl源
sudo yum install centos-release-scl scl-utils-build
//安装新版本的gcc
sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
//启动新版本
[whb@VM-0-3-centos boost_searcher]$ scl enable devtoolset-7 bash
第四步(可选):如果想每次登陆的时候,都是较新的gcc
我们启动scl更新gcc,实际上只有当前次会话有效而已,即下一次重新启动之后仍然需要启动scl更新gcc。所以才有了我们的第四步配置bash_profile,可以使每次启动的时候,都会自动执行更新gcc的指令:scl enable devtoolset-7 bash 。就不用劳烦大家每次都启动更新一下gcc。
执行前面三步之后,就可以使用gcc的升级版本了
8.2.2 引入cpp-httplib库
最新的cpp-httplib在使用的时候,如果gcc不是特别新的话有可能会有运行时错误的问题
建议:cpp-httplib 0.7.15
下载zip安装包,上传到服务器即可
我们在gitee网站上搜索该资源
将下载好的库文件上传入云服务器中 (依托lrzsz工具 , 没有可以sudo yum install -y lrzsz)
我们就有了对cpp-httplib库资源的链接
PS:这里注意,我们cpp-httplib的实现是运用到了原生线程库的,所以我们在编译的时候,需要链接线程库-lpthread
PARSER=parser
# DUG=debug
HTTP_SERVER=http_server
cc=g++
.PHONY:all
all:$(PARSER) $(DUG)
$(PARSER):parser.cc
$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
# $(DUG):debug.cc
# $(cc) -o $@ $^ -ljsoncpp -std=c++11
$(HTTP_SERVER):http_server.cc
$(cc) -o $@ $^ -ljsoncpp -lpthread -std=c++11
.PHONY:clean
clean:
rm -f $(PARSER) $(DUG)
8.2.3 使用cpp-httplib库
首先: #include "cpp-httplib/httplib.h"
#include "cpp-httplib/httplib.h"
int main()
{
httplib::Server svr;
svr.Get("/hi",[](const httplib::Request &req, httplib::Response &rsp){
rsp.set_content("你好,世界!", "text/plain; charset=utf-8");
});
svr.listen("0.0.0.0", 8888);
return 0;
}
8.2.4 编写搜索服务端http_server.cc
设置服务端的web根目录wwwroot/index.html,web根目录就是直接访问该IP: Port端口时默认显示的网页。web根目录这里我们在后续实现前端的网页HTML时再进行演示。
#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);
httplib::Server svr;
svr.set_base_dir(root_path.c_str());//设置web根目录
//以Get的方式给client发送信息
svr.Get("/s",[&search](const httplib::Request &req, httplib::Response &rsp){
//client客户端浏览器访问时,请求中必须输入 word项-搜索关键词
if(!req.has_param("word"))
{
rsp.set_content("必须要有搜索关键字", "text/plain; charset=utf-8");//
return;
}
//提出用户输入的关键词word,用Search进行搜索,将结果存入jsonstring字符串
std::string word = req.get_param_value("word");
std::cout << "用户在搜索:" << word << std::endl;
std::string jsonstring;
search.Search(word,&jsonstring);
rsp.set_content(jsonstring, "application/json");
//rsp.set_content("你好,世界!", "text/plain; charset=utf-8");
});
svr.listen("0.0.0.0", 8888);
return 0;
}
http_server搜索端测试:
9. 编写前端模块
我们boost搜索引擎的主要代码(后端)已经完成,我们接下来简单介绍一下前端。
9.1 了解 vscode
我们使用Vscode连接云服务器进行前端代码的编写 , 下面我们安装Vscode并进行连接。
#1.下载链接:
官网:https://code.visualstudio.com/
下载慢:https://www.zhihu.com/searchtype=content&q=vscode%20%E4%B8%8B%E8%BD%BD%E6%85%A2
#2.安装插件
Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code
Open in Browser
3.远程链接Linux
安装插件: Remote SSH
示例:
【1】安装好Remote - SSH之后 ,按F1打开输入对话框。
【2】输入remote-ssh
【3】ssh user_name@60.204.169.245
9.2 了解前端
1. !tab
2. 由标签构成:单标签,双标签
了解前端三大件:html , css , javascript(js)
html:是网页的骨骼 --- 负责网页结构
css: 网页的皮肉 --- 负责网页的美观
js:网页的灵魂 --- 负责动态效果,以及前后端交互
前端学习网站教程:http://www.w3school.com.cn
9.3 Html网页结构书写
9.3.1boost搜索引擎网页基本结构
9.3.2 HTML网页代码基本实现
进入wwwroot/index.html进行代码书写
下面我们先介绍一下快捷键:
!+ Tab :会自动设计出html网页代码基本结构
h1 + Tab :会自动设计出h1标签
我们按照9.3.1搜索引擎根目录网页基本结构设计 ,设计如下的HTML文件。
代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<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/?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 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>
wwwroot/index.hmtl 网页测试:
9.4 CSS网页样式设置
设置样式的本质:找到要设置的标签,设置它的属性
1. 选择特定的标签:类选择器,标签选择器,复合选择器
2. 设置指定标签的属性:见代码
9.4.1 设置网页总体样式
测试:
9.4.2 设置搜索框样式
9.4.3 设置搜索结果样式
测试:
9.5 编写前后端交互JS代码
9.5.1 增加搜索功能
点击“搜索一下”按钮,我们可以进行搜索功能。
即我们需要添加点击按钮button时,会发生搜索Search()事件,执行Search()函数
代码结构:
测试代码结构:
9.5.2 引入JQuery库
如果直接使用原生的js成本会比较高(xmlhttprequest),我们推荐使用JQuery.
我们使用JQuery库 (就像C++语言之于C++标准库的关系)
来进行前后端交互,Js代码部分的书写。
JQuery CDN: https://www.jq22.com/cdn/
下面利用JQuery进行前后端交互搜索服务模块的编写。
9.5.3 网页Search()搜索函数书写
#1.获取搜索结果data
将之前的静态显示的内容注释掉:
#2.根据搜索结果,建立搜索结果网页。
搜索功能测试:
10. 解决搜到重复内容的问题
之前在写searcher.hpp时,我们对搜索词query进行jieba分词后,由于分出的是多个词段,不同词段可能在同一份文档中找到,所以在Search();的第二步:根据分“词”进行index查找中,我们会查到重复的文档信息。
以搜索内容“吃葡萄不吐葡萄皮”为例:
分词后可能得到“吃”、“葡萄”、“吐”、“葡萄皮”等词汇。
文档内容:吃葡萄不吐葡萄皮吃葡萄不吐葡萄皮吃葡萄不吐葡萄皮
在根据分词进行查找时:
吃,葡萄,吐,等等很多词语都会指向这个文档内容
从而导致:输出结果会出现很多重复内容
修改代码:
我们新建一个InvertedElemPrint结构体,结构体中words成员变量来存放同一个query中不同词段存在在该文档。然后把inverted_list_all存放的节点换成InvertedElemPrint。
然后我们建立一个tokens_map,key值为文档id,value为InvertedElemPrint。然后再遍历每个关键词段的倒排拉链的时候,我们利用tokens_map,重复出现的文档我们就将词段存入到对应的InvertedElemPrint,然后权重值进行累加。
最后我们再将去重后的InvertedElemPrint节点再存进inverted_list_all
#pragma once
#include "index.hpp"
#include "util.hpp"
#include <algorithm>
#include <vector>
#include <jsoncpp/json/json.h>
namespace ns_searcher
{
//同一个query去重后要打印的节点结构
struct InvertedElemPrint{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint():doc_id(0),weight(0){}
};
const std::string raw_file = "data/raw_html/raw.txt";
class Searcher
{
private:
ns_index::Index *index; // 供系统进行查找的索引
public:
Searcher() {}
~Searcher() {}
public:
void InitSearcher(const std::string &input)
{
// 1. 获取或者创建index对象
index = ns_index::Index::GetInstance();
// 2. 根据index对象建立索引
index->BulidIndex(input);
std::cout << "建立正排和倒排索引成功..." << std::endl;
}
// query: 搜索关键字
// json_string: 返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{
// 1.[query分词]:对我们的query进行按照searcher的要求进行分词
std::vector<std::string> words;
ns_util::jiebaUtil::CutString(query, &words);
// 2.[触发]:就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索关键字也需要
// ns_index::InvertedList inverted_list_all; // 内部InvertedElem
std::vector<InvertedElemPrint> inverted_list_all; // 内部InvertedElem
std::unordered_map<uint64_t, InvertedElemPrint> tokens_map; //文件id作为key,进行去重
for (std::string word : words)
{
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if (nullptr == inverted_list)
{
continue;
}
// inverted_list_all.insert(inverted_list_all.end(),inverted_list->begin(),inverted_list->end());
//去重。不同同一个词段对应同一个文档,要把其权重加起来,以及所有对应该文档的词段都保存起来
for(auto &elem:*inverted_list)
{
auto& item = tokens_map[elem.doc_id];//[]:如果存在直接获取,如果不存在新建
//item一定是doc_id相同的print节点
item.doc_id = elem.doc_id;
item.weight += elem.weight;
item.words.push_back(elem.word);
}
//去重后的InvertedElemPrint节点再存进inverted_list_all
for(const auto& item:tokens_map)
{
inverted_list_all.push_back(std::move(item.second));
}
}
// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序
// std::sort(inverted_list_all.begin(),inverted_list_all.end(),\
// [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2){//
// return e1.weight > e2.weight;//这种情况称之为降序
// });
std::sort(inverted_list_all.begin(),inverted_list_all.end(),\
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2){//
return e1.weight > e2.weight;//这种情况称之为降序
});
// 4.[构建]:根据查找出来的结果,构建json串 -- 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]); //doc->content是文档去标签之后的内容,我们要的是截取其中一部分
elem["url"] = doc->url;
elem["id"] = (int)item.doc_id;
elem["weight"] = (int)item.weight;
root.append(elem);
}
Json::StyledWriter writer;
*json_string = writer.write(root);
}
std::string GetDesc(const std::string& html_content,const std::string& word)
{
//找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)
//截取出这部分内容
const int prev_step = 50;
const int next_step = 100;
//1.找到首次出现
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,防止size_t的有坑
int pos = std::distance(html_content.begin(),iter);
//2. 获取start,end , std::size_t 无符号整数
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. 截取子串,return
if(start >= end) return "None2";
std::string desc = html_content.substr(start,end - start);
desc += "...";
return desc;
}
};
}
修正之前
修正之后
11.添加日志
11.1日志代码
#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;
}
#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__): 这是一个宏定义,用于简化日志记录的调用。当你使用LOG(NORMAL, "This is a normal message")时,它会展开为log("NORMAL", "This is a normal message", __FILE__, __LINE__)。其中,__FILE__和__LINE__是预处理器宏,分别表示当前代码的文件名和行号。
注意:#LEVEL #后面的宏: 对之不进行宏替换,而是自动将其作为一个字符串
举个例子: LOG(WARNING,"这里有xxx警告") ->
在预处理阶段处理成: log( "WARNING" , "这里有xxx警告" , "调用log的文件名" , "调用行数")
最后打印出来的信息: [WARNING][这里有xxx警告][searcher.hpp][50]
11.2进行日志部署
11.2.1在index.hpp中进行日志部署
首先: #include "log.hpp"
11.2.2在searcher.hpp中进行日志部署
11.2.3在http_server.cc中进行日志部署
11.2.4日志打印测试
11.3 部署服务到 linux 上
11.3.1 nohup指令
nohup指令: 将服务进程以守护进程的方式执行 , 使关闭XShell之后仍可以访问该服务。
示例:
nohup ./http_server:这将会启动 http_server 程序,并确保它在后台运行,不受终端关闭的影响。
为了进一步使程序在后台静默运行,不显示在终端上,你可以在命令的末尾加上 & 符号。示例:
nohup ./http_server &:这样,http_server 会在后台启动并运行,而不会显示在终端上。即使你关闭了终端或 SSH 连接,它也会继续运行。nohup形成的文件:
执行完上述的nohup指令之后,将会形成一个nohup.out存储日志信息文件,可以cat查看该文件
11.3.2 使用nohup指令进行服务和日志部署
实现基本的 自动服务+更新日志 的部署
使用nohup指令后,我们的http_server就可以一直在后台执行了
下面验证即使我们关闭了XShell,而且日志信息也会自动同步更新到nohup.out中。
我们将xshell关掉后,同样可以使用搜索服务,并且我们看到了日志信息也会自动同步更新到nohup.out中。
事实上,我们也可以存一个专门的日志文件夹,进行日志信息的存储。
而这之前我们要删除上一条对于日志 服务的部署。
我们先使用以下指令找到相应服务的PID
ps axj | head -1 && ps axj | grep http_server //找到相应服务的PID。
然后使用kill-9 将其进程杀掉
我们下面我们再来 自动服务+更新日志 的部署
可以看到我们完成了自动服务+更新日志 的部署
12. 结项与项目扩展方向
至此,我们就完成了boost搜索引擎项目,我们可以随时访问该服务网址,进行在boost准标准库相应接口的站内搜索。
这里我们再总结一下该项目扩展方向
2.我们在搜索引擎中,对于权重的设置先后显示顺序,我们其实可以叠加一些算法,比如可以设置竞价排名,热点统计,额外增加某些文档的权重。
3.我们可以利用数据库,设置用户登录注册,引入对MySQL的使用。
1. 建立整站搜索。受限于云服务器配置,我们没有做boost库的整站搜索,而只是做了1.8.0版本下的doc/html/*下的文件搜索。
2. 在我们的搜索引擎中,添加竞价排名
3. 热次统计,智能显示搜索关键词(字典树,优先级队列)
4. 设置登陆注册,引入对mysql的使用
备注
项目源代码地址:
GTY/boost_search - 码云 - 开源中国 (gitee.com)