目录
🌈前言
这篇文章来实现一个站内搜索引擎!!!
Gitee:【Boost站内搜索引擎源代码】
🌸1. 搜索引擎项目背景
当今的全网搜索引擎技术已经非常成熟了,我们做的项目是站内的搜索引擎
项目背景:
-
现代技术成熟的公司:百度、搜狗、谷歌、微软、雅虎 等等…
-
全网搜索:全网搜索引擎是一个复杂且具有很强技术性的项目,它要爬取全网上的网页数据,并且还要考虑网页的类型,而且网页的分布非常的散,然后在服务器中对爬取的海量数据进行数据清洗,之后还要提取网页标题进行分词等等…
-
站内搜索:搜索的数据量小,更垂直,容易爬取,处理起来简单
-
C++ Boost官网中是没有搜索框的,需要我们自己实现一个!
搜索引擎搜索时呈现的网页内容:
搜索引擎搜索关键字后,会拿到与关键字相关联的网页,并且按权重(关键字相关性)降序呈现出来
-
网页的Title:通过搜索引擎的数据清洗技术获取到的网页标题
-
网页的Content:网页内容的摘要 – 摘要包含搜索关键字
-
网页的Url:公网网址,点击后会跳转到新的的网页
🌺2. 搜索引擎宏观原理
原理
-
进行搜索服务之前,服务器要通过爬虫程序抓取全网的网页数据保存到某个文件路径下,随后对抓取到的网页数据(一堆文件夹和文件)进行数据清洗和去标签,最后建立索引
-
搜索服务启动之后,用户使用网页进行搜索时,是通过HTTP请求的方式(HTTP通过GET方法上传关键字给服务器),给服务器发送搜索的任务
-
服务器拿到关键字后,通过检索索引,得到一批相关联的html,随后根据多个"html中的标题、摘取内容和网页url"的组合来拼接成一个新的网页,返回给用户
-
我们主要编写后端的功能,爬虫和前端的功能不编写 – 主要完成服务器的任务
🍀3. 搜索引擎技术栈和项目环境
技术栈:
-
C/C++, C++11, STL、C++准标准库Boost、jsoncpp、cppjieba(分词库)
-
选学(前端):html5, css,javescript(js)、jQuery、Ajax
项目环境:
-
Centos 7云服务器,vim/gcc(g++)/Makefile , vs2019 或 vscode
-
注意:vscode是云服务器远端连接,gcc(g++)版本最好是7.3.1版本
🍁4. 正排索引 vs 倒排索引 - 搜索引擎具体原理
例如:我们用爬虫程序抓取到二个网页的数据(文档) – 比如文档里面只有网页的标题title(关键字)
文档数据
-
文档1的内容:雷军买了四斤小米
-
文档2的内容:雷军发布了小米手机
正排索引
- 原理:通过文档ID找到文档内容(文档内的关键字)
文档ID | 文档内容 |
---|---|
文档1 | 雷军买了四斤小米 |
文档2 | 雷军发布了小米手机 |
文档内容分词
-
对目标文档进行分词,目的是为了构建倒排索引和查找
-
文档1[雷军买了四斤小米 ]: 雷军/买/四斤/小米/四斤小米
-
文档2[雷军发布了小米手机]:雷军/发布/小米/小米手机
-
注意:停止词:了,的,吗,a,the,一般我们在分词的时候可以不考虑
倒排索引
-
原理:根据文档内容进行分词,整理不重复的各个关键字,对应联系到文档ID的方案
-
权重值:权重值就是网页内容与搜索关键字的相关性,相关性越高的排网页越前
关键字(具有唯一性) | 文档ID,weight(权重) |
---|---|
雷军 | 文档1、文档2 |
买 | 文档1 |
四斤 | 文档1 |
小米 | 文档1、文档2 |
四斤小米 | 文档1 |
发布 | 文档2 |
小米手机 | 文档2 |
模拟查找过程
-
用户输入:雷军 ✂ 倒排索引中查找(已经分词并且建立好映射)✂ 提取出文档(1, 2)
-
✂ 通过正排索引 ✂ 找到文档的内容✂title+conent(desc)+url 文档结果进行摘要 ✂ 最后构建响应结果(构建新的网页呈现搜索结果)
🍂5. 编写数据去标签与数据清洗的模块 Parser
🍨5.1、下载boost官网的数据包并解压
【boost官网】
- 下载Boost官网的网页数据
- 将压缩包拉取到服务器中,并且解压到目录
- 也可以使用Linux的【rz指令】进行本地文件上传到服务器当中
- 使用tar指令解压tar文件
tar -xzvf xxx.tar.gz
🍨5.2、去标签
思路讲解
-
<>:html的标签,这个标签对我们进行搜索是没有价值的,需要去掉这些标签,一般标签都是成对出现的
-
我们只需要保留去掉成对出现的标签后的有效网页数据字段即可,成对出现的标签是html用来制作网页的,与有效内容无关
- 随便打开一个从Boost官网下载好的html文件
去标签之后
-
我们要将全部去标签之后的干净文档放到同一个文件中保存
-
目标:把每个文档都去标签,然后写入到同一个文件中!每个文档内容不需要任何\n,文档和文档之间用\3区分
方案
-
文档只需要保存网页的标题、内容和网址就行了,保存到文件的格式为:title content url
-
version1:title\3content\3url – 使用\3作为每个文档的分隔符
-
version2:title\3content\3url \n title\3content\3url \n – 这里在写入文件中,考虑到了下一次在读取的时候,也方便操作,用回车作为文档之间分隔符
-
采用第二种方案,因为方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3url
🍨5.3、编写Parser模块
思路
- 将网页的内容解析成可以被搜索引擎理解的格式,以便更好的抓取和索引内容
- 首先将网页解析成文本,再进行语义分析,然后提取网页的title、content和构建url
- 最后将提取到每个文档的title、content和url按照特定的分隔符写入到文件当中
前置条件
-
创建一个目录,保存解压好的Boost官网数据包,编写该模块时需要使用
-
创建一个目录,并且创建一个保存数据清洗后的文档内容
-
该模块需要用到Boost库,我们需要引入Boost库的filesystem.hpp头文件
- 代码基本结构
我们要编写Parser模块要分三个小部分
-
first:获取下载好的数据包的全部后缀为.html的文件名,保存到一个数组中
-
second:获取html文件的内容,并且解析成结构化数据(包含网页title、content、url)
-
third:将获取到的结构化数据进行反序列化存放到新建的文本文件或二进制文件当中
#include <boost/filesystem.hpp>
#include "Util.hpp"
// const &: 输入型参数 -- 只读
// *: 输出型参数 -- 填充
// &: 输入输出型参数 -- 可读可写
// 目录的路径,存储着全部C++Boost官网内的html文件
const std::string src_html_path = "data/BoostHtmlSet/";
// 数据清洗完后,data就存储在out_path下的文件内
const std::string out_path = "data/raw_html/raw.txt";
// 网页内显示的数据
typedef struct DocInfo
{
std::string title; // 文档标题
std::string content; // 文档内容
std::string url; // 该文档在官网中的url(网址)
} DockInfo_t;
bool EnumFile(const std::string &src_html_path, std::vector<std::string> *Files_List);
bool ParserHtml(std::vector<std::string> &fl, std::vector<DockInfo_t> *result);
bool SaveHtml(std::vector<DockInfo_t> &result, const std::string &outpath);
int main()
{
std::vector<std::string> Files_List;
// 1.该函数用于获取目录中所有后缀为.html文件,保存到Files_List中,方便后续文件读取
bool IsGet = EnumFile(src_html_path, &Files_List);
if (IsGet == false)
{
std::cerr << "EnumFile error!!!" << std::endl;
return ENUMFILE_ERROR;
}
std::cout << Files_List[0] << std::endl;
// 2.读取Files_List中的文件,获取html文件的内容,并且解析成DocInfo结构化数据(反序列化)
std::vector<DockInfo_t> result;
bool IsAls = ParserHtml(Files_List, &result);
if (IsAls == false)
{
std::cerr << "ParserHtml error!!!" << std::endl;
return PARSERHTML_ERROR;
}
// 3.将解析完毕的内容,写入到out_path路径下的raw.txt文件中,按照'\3'作为每个文档的分隔符,'\n'为结束符
bool IsWrite = SaveHtml(result, out_path);
if (IsWrite == false)
{
std::cerr << "SaveHtml error!!!" << std::endl;
return SAVEHTML_ERROR;
}
return 0;
}
bool ParserTitle(const std::string &htmldata, DockInfo_t *di)
{
return true;
}
bool ParserContent(const std::string &htmldata, DockInfo_t *di)
{
return true;
}
bool ParserUrl(const std::string &filepath, DockInfo_t *di)
{
return true;
}
- 编写获取后缀为.html文件名的小模块
-
这里使用到了Boost库的API,编译链接第三方库时要增加链接的库
(-lboost_system -lboost_filesystem) -
使用Boost库中的filesystem.hpp中的递归迭代器来获取后缀为.html的文件
-
并且使用里面的接口来判断递归的文件是目录还是普通文件,还要是否是后缀为.html的文件
bool EnumFile(const std::string &src_html_path, std::vector<std::string> *Files_List)
{
namespace fs = boost::filesystem; // boost库的命名空间
fs::path root_path(src_html_path);
bool IsPath = fs::exists(root_path); // 判断路径是否存在
if (IsPath == false)
{
std::cerr << "Src html path: " << src_html_path << " non-existent!!!" << std::endl;
return false;
}
// 定义一个空的的迭代器 -- nullptr, 用于判断迭代结束条件
fs::recursive_directory_iterator Iter_end;
for (fs::recursive_directory_iterator Iter(root_path); Iter != Iter_end; ++Iter)
{
// 目录下可能有目录或图片文件等等,需要筛选常规文件(常规文件包含html文件)
if (fs::is_regular_file(*Iter) == false)
continue;
// 判断常规文件的后缀是否为.html
if (Iter->path().extension() != ".html")
continue;
// result: "data/BoostHtmlSet/xxx.html"
Files_List->push_back(Iter->path().string()); // 筛选完成后,尾插到Files_List中
}
return true;
}
- 编写第二个小部分,获取html的内容并且序列化成结构化数据 – 需要提取title和去标签和构建url
-
首先我们要对网页的数据读取到string当中,然后进行去标签和数据清洗,才能提取title和去标签和构建url
-
注意:每次读取"一个"文档,不是一次读取完,每次仅处理一个文档
这里读取后缀为.html文件内容的函数,我放到了工具类当中
#pragma once
#include "cjb/Jieba.hpp" // jieba库
#include <boost/algorithm/string.hpp> // boost库
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <unordered_map>
#define ENUMFILE_ERROR 1
#define PARSERHTML_ERROR 2
#define SAVEHTML_ERROR 3
#define OPEN_ERROR 4;
namespace Ns_Util
{
class FileUtil
{
public:
// 读取文件内容到htmldata当中 -- htmldata是输出型参数
static bool ReadHtmlFile(const std::string &file_path, std::string *htmldata)
{
std::ifstream in(file_path.c_str(), std::fstream::in); // 输入流对象,以只读方式打开
if (!in.is_open())
{
std::cerr << "open file: " << file_path << " error!!!" << std::endl;
return false;
}
std::string line;
while (getline(in, line)) // 每次在文件中读取一行到line当中,不保留'\n',不重置文件指针
{
*htmldata += line;
}
in.close();
return true;
}
}
bool ParserHtml(std::vector<std::string> &Files_List, std::vector<DockInfo_t> *result)
{
namespace nu = Ns_Util;
for (const auto &filepath : Files_List)
{
std::string htmldata;
bool IsRead = nu::FileUtil::ReadHtmlFile(filepath, &htmldata); // 读取文件内容
if (IsRead == false)
continue;
DockInfo_t di;
// 1.提取文档的标题title
if (!(ParserTitle(htmldata, &di)))
continue;
// 2.提取文档的内容,去标签<>
if (!(ParserContent(htmldata, &di)))
continue;
// 3.构建网页url
if (!(ParserUrl(filepath, &di)))
continue;
result->push_back(std::move(di)); // 使用move优化(右值资源转换),防止深拷贝
}
return true;
}
- 提取title,只需要判断html文件里面< html>和< /html>出现的位置,然后提取里面的标题即可
bool ParserTitle(const std::string &htmldata, DockInfo_t *di)
{
// "<titie>abcdef123</title>" -- 提取文档标题 -- "abcdef123"
// find成员函数查找到返回子串第一个字符的下标, 没查找到则返回string::npos(无符号-1)
size_t TitleStart = htmldata.find("<title>");
if (TitleStart == std::string::npos)
return false;
size_t TitleEnd = htmldata.find("</title>");
if (TitleEnd == std::string::npos)
return false;
TitleStart += strlen("<title>");
if (TitleStart > TitleEnd)
return false;
// 字符串切割
di->title = htmldata.substr(TitleStart, TitleEnd - TitleStart);
return true;
}
- 对后缀为.html的文件进行去标签操作,这里使用了简单的"状态机"进行枚举
-
原理:枚举二个状态,一个是遇到标签的状态,另一个是遇到标签外数据的状态
-
遇到标签就更新状态标签跳过,遇到网页有效数据就追加数据到字符串里面
-
特殊情况:遇到右尖括号>后就更新状态了,如果下一个是左尖括号<还是要继续更新原来的状态的
bool ParserContent(const std::string &htmldata, DockInfo_t *di)
{
// 使用简单的"状态机"进行标签清洗
enum Status
{
LABLE, // 遇到标签的状态(<>)
CONTENT // 遇到标签外数据的状态
};
Status st = LABLE;
for (auto ch : htmldata)
{
switch (st)
{
case LABLE:
// 遇到标签,跳过,遇到标签结束字符'>',更新状态 -- "<a>xxxxx</a>"
if (ch == '>')
st = CONTENT;
break;
case CONTENT:
// 特殊情况,标签结束字符后继续遇到'<',更新状态 -- "<a></a>"
if (ch == '<')
st = LABLE;
else
{
// 不保留原文件\n, 后续使用\n来做html解析之后的文本分隔符
if (ch == '\n')
ch = ' ';
di->content.push_back(ch);
}
break;
default:
break;
}
}
return true;
}
- 构建Boost官网的url网址
-
构建url网址其实很简单,我们找到自己下载的是哪个版本的网页数据
-
我们下载的是1.81.0版本的文档,只需要添加我框起来上面那个文档即可,后面只需要填充特定的资源路径即可
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yWQK961M-1677866745344)(C:\Users\13614\AppData\Roaming\Typora\typora-user-images\image-20230303233041933.png)]
bool ParserUrl(const std::string &filepath, DockInfo_t *di)
{
// 构建官网url网址
std::string url_head = "https://www.boost.org/doc/libs/1_81_0/doc/html/"; // 官网url,不包含.html文件
// filepath:"data/BoostHtmlSet/xxx.html" -> "xxx.html"
std::string url_tail = filepath.substr(src_html_path.size()); // 将这次处理的html文件名截取出来
di->url = url_head + url_tail;
return true;
}
编写第三个小部分,将提取的网页title、content、url进行反序列化转换成字符串 – 结构体数据转换成字符串
-
反序列化完成每个文档的内容后,末尾加上 ‘\n’ 作为每个文档之间分隔符
-
文档内的内容title、content、url之间的分隔使用 特殊字符 ‘\3’ 进行分割
-
为什么要使用 ‘\3’ 呢?因为它是一个无法“显示”出来的特殊转义字符
// 反序列化 -- 结构化数据 -> 字符串
void Deserialization(const DockInfo_t &di, std::string *diStr)
{
#define SPE '\3'
#define END_OP '\n'
// "title\3content\3url\3\n"
*diStr += di.title;
*diStr += SPE;
*diStr += di.content;
*diStr += SPE;
*diStr += di.url;
*diStr += END_OP;
}
bool SaveHtml(std::vector<DockInfo_t> &result, const std::string &outpath)
{
// 输出流对象,以只写和二进制状态的方式打开文件
std::ofstream out(outpath.c_str(), std::ios::out | std::ios::binary);
if (!out.is_open())
{
std::cerr << "open " << outpath << "error" << std::endl;
return false;
}
for (DockInfo_t &Iter : result)
{
std::string diStr;
// 进行反序列化 -- 格式:"title\3content\3url\n"
Deserialization(Iter, &diStr);
// 写入到outpath路径下的文件中 -- diStr -> outpath路径下的文件
out.write(diStr.c_str(), diStr.size());
}
out.close();
return true;
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-W6YIdPCa-1677866745345)(C:\Users\13614\AppData\Roaming\Typora\typora-user-images\image-20230303234309020.png)]
总结
- Parser模块已经编写完成,除了使用到了新的Boost库方法之外,其他的文件流操作和基本的string操作都是学过的
- 还有就是学到了状态机这个新的筛选说法,总的来说还是有所收获!!!
- 如果自己前面储备的知识不够扎实,做起来会有些困难,还要花时间去学习不懂得知识点
🍃6. 编写建立索引的模块 Index
- 代码结构
索引模块分为正排索引和倒排索引
-
正排索引通过ID索引到文档内容
-
倒排索引通过关键字(也就是对网页title进行分词)找到一个或多个文档ID和权重值(与关键字关联性的值)
-
倒排拉链:它是一个映射表,通过关键字可以找到一个或一组倒排节点(KV结构),上面第四个标题讲的倒排索引就是这样的
编写基本索引模块的代码结构
#pragma once
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <mutex>
#include <unordered_map>
namespace Ns_Index
{
// 文档数据
struct DockInfo
{
std::string title; // 文档标题
std::string content; // 清洗后(去标签)的文档的内容
std::string url; // boost官方文档网址
uint64_t doc_id; // 文档id
};
// 倒排节点
struct InvertedNode
{
std::string keyword; // 倒排索引关键字
int doc_id; // 文档id
int weight; // 权重值,影响文档在新构建的网页中呈现的先后顺序
};
class Index
{
public:
// 倒排拉链 -- 倒排节点数组
typedef std::vector<InvertedNode> InvertedZipper;
public:
~Index() {}
public:
// 正排索引:根据"文档id"找到"文档数据(DockInfo)"
DockInfo *ForwardIndex(uint64_t doc_id)
{
// 判断doc_id是否超出数组范围
if (doc_id >= Forward_Index.size())
{
std::cerr << "The id of index is out of range" << std::endl;
return nullptr;
}
return &(Forward_Index[doc_id]);
}
// 倒排索引:根据"关键字"(分词后的词组)找到"一个或多个文档id(InvertedNode) ->> 倒排拉链"
InvertedZipper *InvertedIndex(const std::string &keyword)
{
// 查找关键字是否存在
auto iter = Inverted_Index.find(keyword);
if (iter == Inverted_Index.end())
{
std::cerr << "The index key does not exist" << std::endl;
return nullptr;
}
// 存在则返回哈希表中关键字(key)映射的倒排拉链(value)
return &(iter->second);
}
private:
// 单例模式 -- 构造、拷贝构造和赋值构造函数私有化
Index() {}
Index(const Index &) = default;
Index &operator=(const Index &) = default;
// Index类指针和互斥锁
static Index *instance;
static std::mutex lock_;
public:
// 单例模式 -- 只提供一个接口,返回一个已经实例化好了的该类的指针
static Index *GetInstance()
{
// 防止不需要加锁的资源申请锁,导致阻塞住
if (instance == nullptr)
{
Index::lock_.lock(); // 加锁
if (instance == nullptr)
return new Index();
Index::lock_.try_lock(); // 解锁
}
return instance;
}
private:
// 正排索引数据结构,文档id天然是数组的下标(快速索引)
std::vector<DockInfo> Forward_Index;
// 倒排索引数据结构,一个关键字可能对应多个文档id(1:n -- n >= 1),需要一个或多个倒排节点
std::unordered_map<std::string, InvertedZipper> Inverted_Index;
};
// 静态成员变量初始化
Index *Index::instance = nullptr;
std::mutex Index::lock_;
}
🍨6.1、构建索引基本框架
- 构建索引
构建索引就是要构建正排索引和构建倒排索引,每次仅构建一个文档的索引
-
构建正排索引:将raw.txt下的内容序列化成结构化数据,并且放到正排数据结构中
-
构建倒排索引:对结构化数据中的网页标题和有效内容进行分词,然后进行词频统计(后面要统计权重值),最后建立一个倒排拉链
public:
// 构建索引,使用数据清洗后的内容,构建正排索引和倒排索引
bool BulidIndex(const std::string &raw_path) // raw_path: "data/raw_html/raw.txt"
{
std::cout << "开始建立文档索引" << std::endl;
// 读取raw_path路径下的raw.txt文件内容
std::ifstream in(raw_path.c_str(), std::ios::in | std::ios::binary); // 只读和二进制状态打开文件
if (!in.is_open())
{
std::cerr << "Fail to open raw.txt file" << std::endl;
return false;
}
std::string raw_data;
while (std::getline(in, raw_data)) // 循环按行读取清洗后的内容, 默认每次丢弃'\n'
{
// 构建正排索引
DockInfo *doc = BulidForwardIndex(raw_data);
if (doc == nullptr)
{
std::cerr << "Failed to build forward index" << std::endl;
return false;
}
// 构建倒排索引
BulidInvertedIndex(*doc);
// For debug
static size_t cnt = 0;
cnt++;
if (cnt % 50 == 0)
{
std::cout << "已经建立好索引的文档数量: " << cnt << std::endl;
}
}
std::cout << "建立文档索引完成!!!" << std::endl;
return true;
}
🍨6.2、构建正排索引
思路
-
首先对raw.txt的内容进行分割,以\3为每个数据的分隔符进行提取,以\n为每个文档之间的分隔符
-
提取到网页title、content、url后,尾插到正排数据结构中(文档数组)
private:
DockInfo *BulidForwardIndex(const std::string &raw_data)
{
// 切割raw_data里面的字符串 -- 格式: "title\3content\3url\3" -> "title content url"
std::vector<std::string> results;
const std::string SPE = "\3";
Ns_Util::StringUtil::CutString(raw_data, &results, SPE);
if (results.size() != 3)
{
std::cerr << "Cutting failed, content missing" << std::endl;
return nullptr;
}
// 填充DockInfo结构体 -- results-> [0]: title [1]: content [2]: url
DockInfo doc;
doc.title = results[0];
doc.content = results[1];
doc.url = results[2];
// 文档id是从0下标让后增长的,一开始Forward_Index为空,大小就是0, 下一次就是1,....
doc.doc_id = Forward_Index.size();
// 尾插到正排索引数据结构中
Forward_Index.push_back(std::move(doc));
return &(Forward_Index.back()); // 返回最新插入文件数据 -- 就是数组最后的数据 -- 后面倒排要用
}
🍨6.3、构建倒排索引
思路
-
首先对文档的title和content进行分词,这里我们需要使用到cppjieba库来进行分词
-
下载链接: https://gitee.com/yanyiwu/cppjieba(如果不行就找博客看)– 最好自己下载压缩包解压,GitHub克隆太慢了…
-
我们需要自己执行: cd cppjieba; cp -rf deps/limonp include/cppjieba/,不然会编译报错,因为作者单独把limonp作为了一个项目方便更新,故需要进行拷贝
上面写的指令就是为了把cppjieba库中的limonp目录拷贝到 /cppjieba/include/cppjieba/ 目录下, 否则会编译错误,缺少头文件
这里的分词函数,我也放到了工具类当中
- jieba分词还要引入词库,才能进行分词,我们使用软连接建立词库的快捷方式 – 不使用快捷方式也可以,就是要使用绝对路径
#pragma once
#include "cjb/Jieba.hpp" // jieba库
#include <boost/algorithm/string.hpp> // boost库
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
#include <unordered_map>
#define ENUMFILE_ERROR 1
#define PARSERHTML_ERROR 2
#define SAVEHTML_ERROR 3
#define OPEN_ERROR 4;
namespace Ns_Util
{
// 词库路径 -- demo.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";
// jieba分词接口
class JiebaUtil
{
public:
// str: 待分词字符串,result: 分词之后保存到这里
static void StringParticiple(const std::string& str, std::vector<std::string>* result)
{
jieba.CutForSearch(str, *result); // 调用jieba分词api
}
private:
static cppjieba::Jieba jieba; // jieba对象
};
cppjieba::Jieba JiebaUtil::jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,
IDF_PATH, STOP_WORD_PATH);
}
- 分词完成后,我们要统计词频出现的个数,最后建立倒排拉链,详细看代码逻辑
关于权重值
-
权重值关联着文档再网页中的排序,对文档的title和content进行分词后,出现的最多与搜索的关键字向匹配的词组,权重值就越高
-
我这里写的是title中每出现一次关键字算15点权重值,content出现一次关键字算2点权重值
// 词频统计
struct word_cnt
{
uint64_t title_cnt; // 标题分词后词频统计
uint64_t content_cnt; // 文档内容词频统计
};
private:
bool BulidInvertedIndex(const DockInfo &doc)
{
// 对doc的title标题进行分词
std::vector<std::string> title_participle;
std::unordered_map<std::string, word_cnt> wordfrequency_cnt; // 建立词组和词频的映射表
// 对title进行分词,词组放到title_participle
Ns_Util::JiebaUtil::StringParticiple(doc.title, &title_participle);
// title词频统计 -- 统计关键字出现的次数
for (auto str : title_participle)
{
// 搜索关键字不区分大小写,统一转换成小写
boost::to_lower(str); // 统一转换小写
wordfrequency_cnt[str].title_cnt++;
}
// 对doc的content(文档内容)进行分词
std::vector<std::string> content_participle;
Ns_Util::JiebaUtil::StringParticiple(doc.content, &content_participle); // 对content进行分词
// 词频统计
for (auto str : content_participle)
{
boost::to_lower(str);
wordfrequency_cnt[str].content_cnt++;
}
// 建立倒排拉链(InvertedZipper) -- 建立一个或多个InvertedNode倒排节点
for (auto &wfc : wordfrequency_cnt)
{
// 填充倒排节点
InvertedNode In;
In.doc_id = doc.doc_id; // 文档id
In.keyword = wfc.first; // 倒排索引关键字
// 权重值,影响网页呈现文档的位置
In.weight = wfc.second.title_cnt * TITLE_WEIGHT + wfc.second.content_cnt * CONTENT_WEIGHT;
// 构建倒排拉链 -- 哈希表[]运算符的返回值是第二模板参数的引用 -- 也就是倒排拉链的引用
InvertedZipper &Iz = Inverted_Index[wfc.first]; // key值不存在就会自动创建
Iz.push_back(std::move(In));
}
return true;
}
🍌7. 编写搜索引擎模块 Searcher
- 基本结构
思路:
- 首先,获取Index索引模块中Index类的指针 – 初始化Index指针,然后构建索引
- 处理浏览器用HTTP中GET方法发送过来的搜索关键字,对他进行分词,得到一组词组,然后根据词组进行索引
- 汇总查找结果,按照权重值(InvertedNode.weight)进行降序排序 – 权重值越大就排在网页中越前面的地方
- 根据查找出来的倒排拉链,构建json串 – 通过正排索引找到文档内容,随后通过json库进行序列化操作,转换成字符串
注意:josn序列化结构体中的content时,要对网页内容进行摘要,不能把全部网页内容全部呈现出来
#pragma once
#include "index.hpp"
#include "Util.hpp"
#include "Jsoncpp/json.h"
#include <algorithm>
namespace Ns_Searcher
{
class Searcher
{
public:
// 获取索引对象的指针
void InitSearcher(const std::string &raw_path)
{}
// keyword: 搜索关键字,result: 返回给浏览器的搜索结果
void Search(std::string &keyword, std::string *json_string)
{
// 1.对搜索关键字进行分词
// 2.根据各个"词组"(phrase), 进行index查找
// 3.汇总查找结果,按照权重值(InvertedNode.weight)进行降序排序 -- 网页呈现的是高权重到低权重的文档
// 4.根据查找出来的倒排拉链,构建json串 -- 通过jsoncpp库来完成序列化和反序列化
}
// 获取文档的摘要 -- 摘要部分要包含搜索关键字 -- html_content: 文档内容, keyword: 搜索关键字
std::string GetDesc(const std::string &html_content, const std::string &keyword)
{
return "";
}
private:
Ns_Index::Index *pIndex; // Index类·对象指针
};
}
🍨7.1、构建索引
- Index模块中将类实现成了单例模式,通过空间指定符调用GetInstance()函数获取Index类对象指针
- 随后使用空间指定符调用Index里面的构建索引方法(BulidIndex())
public:
// 获取索引对象的指针
void InitSearcher(const std::string &raw_path)
{
// 获取index对象
pIndex = Ns_Index::Index::GetInstance();
// 根据index对象构建索引
pIndex->BulidIndex(raw_path);
}
}
🍨7.2、编写搜索引擎
思路:
- 对搜索关键字进行分词
- 根据各个"词组"(phrase), 进行index查找 – 首先进行倒排索引,因为进行分词了,拿到文档id,然后通过正排索引找到文档内容
- 汇总查找结果,按照权重值(InvertedNode.weight)进行降序排序 – 网页呈现的是高权重到低权重的文档
- 根据查找出来的倒排拉链,构建json串 – 通过jsoncpp库来完成序列化操作
- 安装jsoncpp库
- 教程:https://www.bbsmax.com/A/1O5EZqD3d7/
- 安装好后,需要自己添加json.h头文件 – 我这里使用软连接建立快捷方式
- 链接第三方库时,需要指定链接位置 – -ljsoncpp
public:
// keyword: 搜索关键字,result: 返回给浏览器的搜索结果
void Search(std::string &keyword, std::string *json_string)
{
// 1.对搜索关键字进行分词
std::vector<std::string> phrase; // 保存分词后的"词组"
Ns_Index::Index::InvertedZipper Index_result;
Ns_Util::JiebaUtil::StringParticiple(keyword, &phrase); // 对搜索关键字进行分词
// 2.根据各个"词组"(phrase), 进行index查找
for (auto &ps : phrase)
{
// 词组也要进行统一小写转换,因为倒排里面也进行了统一
boost::to_lower(ps);
// "首先进行倒排搜索(通过"词组找到文档id"),获取到倒排拉链"
Ns_Index::Index::InvertedZipper *itz = pIndex->InvertedIndex(ps);
if (itz == nullptr)
{
continue; // 没有索引成功就进行下一个 -- 没有文档id就找不到文档数据
}
// InvertedZipper -> vector<InvertedNode>
Index_result.insert(Index_result.end(), itz->begin(), itz->end()); // 统计倒排拉链
// 3.汇总查找结果,按照权重值(InvertedNode.weight)进行降序排序 -- 网页呈现的是高权重到低权重的文档
std::sort(Index_result.begin(), Index_result.end(),
[](const Ns_Index::InvertedNode &x, const Ns_Index::InvertedNode &y)
{
return x.weight > y.weight;
});
// 4.根据查找出来的倒排拉链,构建json串 -- 通过jsoncpp库来完成序列化和反序列化
Json::Value root;
for (const auto &Item : Index_result)
{
// 通过正排索引找到文档内容
Ns_Index::DockInfo *doc = pIndex->ForwardIndex(Item.doc_id);
if (doc == nullptr)
{
continue;
}
// 进行序列化 -- 结构化数据 ->> 字符串
Json::Value elem;
elem["title"] = doc->title;
elem["desc"] = GetDesc(doc->content, Item.keyword); // 这里提取的是清洗后的全部数据,而不是摘要
elem["url"] = doc->url;
// 追加到root中
root.append(elem);
}
Json::StyledWriter sw;
*json_string = sw.write(root); // 获取json序列化后的字符串
}
}
// 获取文档的摘要 -- 摘要部分要包含搜索关键字 -- html_content: 文档内容, keyword: 搜索关键字
std::string GetDesc(const std::string &html_content, const std::string &keyword)
{
// 找到第一次出现的keyword, 向前找80个字节(如果没有就从起始位置开始start), 往后找100个字节(没有就直接end)
int prev_stop = 80;
int next_stop = 100;
// 因为在原始清洗过后的文档拿出的数据是没有进行大小写转换的,这里要统一小写查找
auto it = std::search(html_content.begin(), html_content.end(), keyword.begin(),keyword.end(),
[](int x, int y)
{
// 统一小写进行查找
return std::tolower(x) == std::tolower(y);
}); // 从html_content中查找子串keyword第一次出现的位置,忽略大小写查找
if (it == html_content.end())
{
return "None";
}
int pos = std::distance(html_content.begin(), it); // 返回二个迭代器之间的距离
// 获取start,end
int start = 0;
int end = html_content.size() - 1;
// 更新start,end
if (pos - prev_stop > start)
start = pos - prev_stop;
if (pos + next_stop < end)
end = pos + next_stop;
// 返回摘要
if (start >= end) return "None";
return html_content.substr(start, end - start);
}
🍎8. 编写 Http_Server 模块
思路:
这里我们就不使用套接字手搓一个网络通信了,我们使用cpp-httplib库直接进行操作
- cpp-httplib库:https://gitcode.net/mirrors/yhirose/cpp-httplib/-/tree/v0.7.15 下载ZIP压缩包然后sz到shell,最后解压就可以了
- 里面有demo代码,可以自己测试一下,这个库要链接线程库,里面实现了线程池
- 注意:cpp-httplib在使用的时候需要使用较新版本的gcc,centos 7下默认gcc 4.8.5
[lyhsky@iZwz908i6voykw3uqjr3maZ SearchEngineItem]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-7/root/usr/libexec/gcc/x86_64-redhat-linux/7/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/opt/rh/devtoolset-7/root/usr --mandir=/opt/rh/devtoolset-7/root/usr/share/man --infodir=/opt/rh/devtoolset-7/root/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --enable-plugin --with-linker-hash-style=gnu --enable-initfini-array --with-default-libstdcxx-abi=gcc4-compatible --with-isl=/builddir/build/BUILD/gcc-7.3.1-20180303/obj-x86_64-redhat-linux/isl-install --enable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch_32=i686 --build=x86_64-redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)
更新gcc/g++编译器的版本
- 教程:https://blog.csdn.net/attemptendeavor/article/details/128705148
//可选:如果想每次登陆的时候,都是较新的gcc
[lyhsky@iZwz908i6voykw3uqjr3maZ SearchEngineItem]$ sudo vim ~/.bash_profile
# .bash_profile
# Get the aliases and functions
if [ -f ~/.bashrc ]; then
. ~/.bashrc
fi
# User specific environment and startup programs
PATH=$PATH:$HOME/.local/bin:$HOME/bin
export PATH
#每次启动的时候,都会执行这个scl命令
sc1 enable devtoo1set-7 bash
- 编写http_server
- httpserver教程:https://gitcode.net/mirrors/yhirose/cpp-httplib?utm_source=csdn_github_accelerator – 里面有例子
#include "index.hpp"
#include "searcher.hpp"
#include <cstdio>
const std::string out_path = "data/raw_html/raw.txt";
int main()
{
Ns_Searcher::Searcher* search = new Ns_Searcher::Searcher;
search->InitSearcher(out_path);
std::string keyword;
std::string result;
while (true)
{
std::printf("Please enter the keyword you want to search# ");
fflush(stdout);
std::getline(std::cin , keyword);
search->Search(keyword, &result);
std::printf("result:\n\t%s\n", result.c_str());
}
return 0;
}