目录
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 )。这意味着,在我们的倒排索引中,用于查找的关键词(即倒排表的左侧)都是小写形式。当用户在索引中进行搜索时,我们也会自动将输入的搜索词转换为小写进行检索。这样做可以确保搜索的准确性和一致性,无论用户输入的是大写、小写还是混合大小写。
以上就是我们建立索引部分的过程。
索引就是一个存储 + 查找的数据结构(其实就是填充两张表)
我们思考一下索引类,我们需要建立多个索引对象吗?
- 我们实际上并不需要创建多个索引对象来完成查找任务。一个索引对象足以应对所有的搜索需求,因为它包含了所有必要的正排和倒排表,从而支持对网页信息的快速检索。
- 创建一个索引对象的成本是相当高的。这涉及到对大量网页信息的