【项目设计】—— 基于Boost库的搜索引擎
目录
前言
一、项目的相关背景
1. 什么是Boost库
2. 什么是搜索引擎
3. 为什么要做Boost搜索引擎
二、搜索引擎的宏观原理
三、搜索引擎技术栈和项目环境
四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理
1. 正排索引(forword index)
2. 倒排索引(inverted index)
五、编写数据去标签和数据清洗模块 Parser
1. 数据准备
2. 编写parser模块
1. 基本结构设计
2. 细节实现
六、编写建立索引的模块 Index
1. 节点设计
2. 基本结构设计
2. 获取正排索引(GetForwardIndex)
2. 获取倒排索引(GetInvertedList)
3. 建立索引(BuildIndex)
5. 构建正排索引(BuildForwardIndex)
6. 倒排索引的原理介绍
7. cppjieba分词工具的安装和使用介绍
8. 引入cppjieba到项目中
9. 构建倒排索引(BuildInvertedIndex)
七、编写搜索引擎模块 Searcher
1. 基本结构
2. 初始化服务(InitSearcher)
1. Index模块的单例设计
2. 编写InitSearcher
3. 提供服务(Search)
1. 对用户关键字进行分词
2. 触发分词,进行索引查找
3. 按文档权重进行降序排序
4. 根据排序结果构建json串
八、编写http_server模块
1. 引入cpp-httplib到项目中
2. cpp-httplib的使用介绍
3. 正式编写http_server
九、添加日志到项目中
十、编写前端模块
十一、项目总结
前言
首先,这个基于Boost库的搜索引擎项目,博主之前就已经做过了,但是由于各方面的原因没有记录这个博客,拖了很久。这次我想把这个项目好好的总结出来,希望看到这个项目博客的小伙伴可以点点关注和收藏。
一、项目的相关背景
1. 什么是Boost库
Boost库是C++的准标准库, 它提供了很多C++没有的功能,可以称之为是C++的后备力量。早期的开发者多为C++标准委员会的成员,一些Boost库也被纳入了C++11中(如:哈希、智能指针);这里大家可以去百度百科上搜索,一看便知。下面是boost的官网:
2. 什么是搜索引擎
对于搜索引擎,相信大家一定不陌生,如:百度、360、搜狗等,都是我们常用的搜索引擎。但是你想自己实现出一个和百度、360、搜狗一模一样哪怕是类似的搜索引擎,是非常非常困难的。我们可以看一下这些搜索引擎在搜索关键字的时候,给我们展示了哪些信息:
我们可以看到,基本上搜索引擎根据我们所给的关键字,搜出来的结果展示都是以,网页标题、网页内容摘要和跳转的网址组成的。但是它可能还有相应的照片、视频、广告,这些我们在设计基于Boost库的搜索引擎项目的时候,不考虑这些,它们属于扩展内容;
3. 为什么要做Boost搜索引擎
刚刚我们看到了boost的官网界面,我们可以对比一下cplusplus官网,看看有什么区别
可以看到,boost库是没有站内搜索框的,如果我们可以对boost库做一个站内搜索,向cplusplus一样,搜索一个关键字,就能够跳转到指定的网页,并显示出来。那么这个项目还是具有一定意义的。这也就是项目的背景。
其次,站内搜索的数据更加垂直,数据量其实更小。
二、搜索引擎的宏观原理
刚刚我们介绍完了基于Boost库的搜索引擎的项目背景后,相信大家有了一定的了解,大致上知道了这个项目是什么意思。但是我们还需要了解一下搜索引擎的宏观原理。接下来以下面的图为例,介绍一下其宏观原理。
原理图分析:
我们要实现出boost库的站内搜索引擎,红色虚线框内就是我们要实现的内容,总的分为客户端和服务器,详细分析如下:
- 我们从客户端想要获取到大学生的相关信息(呈现在网页上的样子就是:网页的标题+摘要+网址),首先我们构建的服务器就要有对应的数据存在,这些数据从何而来,我们可以进行全网的一个爬虫,将数据爬到我们的服务器的磁盘上,但是我们这个项目是不涉及任何爬虫程序的,我们可以直接将boost库对应版本的数据直接解压到我们对应文件里。
- 现在数据已经被我们放到了磁盘中了,接下来客户端要访问服务器,那么服务器首先要运行起来,服务器一旦运行起来,它首先要做的工作就是对磁盘中的这些数据进行去标签和数据清洗的动作,因为我们从boost库拿的数据其实就是对应文档html网页,但是我们需要的只是每个网页的标题+网页内容摘要+跳转的网址,所以才有了去标签和数据清洗(只拿我们想要的)。这样就可以直接跳过网址跳转到boost库相应文档的位置。
- 服务器完成了去标签和数据清洗之后,就需要对这些清洗后的数据建立索引(方便客户端快速查找);
- 当服务器所以的工作都完成之后,客户端就发起http请求,通过GET方法,上传搜索关键,服务器收到了会进行解析,通过客户端发来的关键字去检索已经构建好的索引,找到了相关的html后,就会将逐个的将每个网页的标题、摘要和网址拼接起来,构建出一个新的网页,响应给客户端;至此,客户就看到了相应的内容,点击网址就可以跳转到boost库相应的文档位置。
三、搜索引擎技术栈和项目环境
基于Boost库的搜索引擎项目所涉及的技术栈和项目环境如下:
技术栈:
- C/C++/C++11、STL、boost库、Jsoncpp、cppjieba、cpp-httplib
- html5、css、js、jQuery、Ajax
项目环境:
- Centos 7 云服务器
- vim/gcc/g++/Makefile
- vs2019 or vscode
技术栈和项目环境,有些你可能不了解,没关系,下面的代码编写中会有介绍,但是基本的技术栈:C/C++/C++11/STL 你是要熟悉的;项目环境 :云服务器、vim、vs这些你也是需要知道的。
四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理
通过下面两个文档来解释一下正排索引和倒排索引
文档ID | 文档内容 |
---|---|
1 | 雷军买了四斤小米 |
2 | 雷军发布了小米手机 |
1. 正排索引(forword index)
正排索引:就是从文档ID找到文档内容(文档内的关键字)
正排索引是创建倒排索引的基础,有了正排索引之后,如何构建倒排索引呢?
我们要对目标文档进行分词,以上面的文档1/2为例,我们来进行分词演示:
- 文档1:雷军、买、四斤、小米、四斤小米
- 文档2:雷军、发布、小米、手机、小米手机
进行分词之后,就能够方便的建立倒排索引和查找。
我们可以看到,在文档1/2中,其中的 “了” 子被我们省去了,这是因为像:了,呢,吗 ,a,the等都是属于停止词,一般我们在分词的时候可以不考虑。那么什么是停止词呢?
停止词: 它是搜索引擎分词的一项技术,停止词就是没有意义的词。如:在一篇文章中,你可以发现有很多类似于了,呢,吗 ,a,the等(中文或英文中)都是停止词,因为频繁出现,如果我们在进行分词操作的时候,如果把这些停止词也算上,不仅会建立索引麻烦,而且会增加精确搜索的难度。
2. 倒排索引(inverted index)
刚刚我们说正排索引是创建倒排索引的基础,首先是要对文档进行分词操作;
倒排索引:就是根据文档内容的分词,整理不重复的各个关键字,对应联系到文档ID的方案
关键词(具有唯一性) | 文档ID |
---|---|
雷军 | 文档1,文档2 |
买 | 文档1 |
四斤 | 文档1 |
小米 | 文档1,文档2 |
四斤小米 | 文档1 |
发布 | 文档2 |
手机 | 文档2 |
小米手机 | 文档2 |
模拟一次查找的过程:
用户输入:小米 ---> 去倒排索引中查找关键字“小米” ---> 提取出文档ID【1,2】---> 去正排索引中,根据文档ID【1,2】找到文档内容 ---> 通过 [ 标题 + 内容摘要 + 网址 ] 的形式,构建响应结果 ---> 返回给用户小米相关的网页信息。
五、编写数据去标签和数据清洗模块 Parser
在编写Parser模块的之前,我们先将数据准备好,去boost官网下载最新版本的库,解压到Linux下,操作方法如下:
1. 数据准备
boost官网:https://www.boost.org/
进入官网后,是如下界面:
进入之后,你可以选择最新版本的下载,我之前下载的是1.78.0版本,这里我就不下载最新版本了,都是可以用的。
点击Download之后,我们选择这个进行下载:
下载好之后,我们先在linux下,创建一个名为Boost_Searcher目录,以后将会在这个目录下进行各种代码模块的编写以及存放各种数据,下面是创建过程:
接下来我们将下载好的1.78.0版本的boost库解压到Linux下,使用 rz 命令(用于文件传输),输入rz -E 命令后直接回车,找到boost,点击打开即可,你也可以直接将压缩包拖拽到命令行中;
效果如下:
此时,我们使用 tar xzf boost_1_78_0.tar.gz 进行解压,解压好后,我们进行查看
可以看到,解压好的boost,里面有这么所文件,但这么多文件并不是我们都需要的,我们需要的就是boost_1_78_0/doc/html目录下的html。为什么呢?结合下面的图:
上面的图就是boost库的操作方法,我们可以看到右下角的两个网页的网址,他们都是在doc/html目录下的文件,都是 .html。我们只要这个就可以了。后期通过地址进行拼接,达到跳转,就能来到这个网页。
---------------------------------------------------------------------------------------------------------------------------------
我们进入到Linux下的doc/html目录,看看里面有哪些东西:
可以看到,里面处理html为后缀的文件外,还有一些目录,但是我们只需要html文件,所以我们要进行数据清洗。只拿html文件。
对数据清洗之后,拿到的全都是html文件,此时还需要对html文件进行去标签处理,我们这里随便看一个html文件:
<> : html的标签,这个标签对我们进行搜索是没有价值的,需要去掉这些标签,一般标签都是成对出现的!但是也有单独出现的,我们也是不需要的。
我们的目标:把每个文档都去标签,然后写入到同一个文件中!每个文档内容不需要任何\n!文档和文档之间用 \3 进行区分。
类似:XXXXXXXXXXXXXXX\3YYYYYYYYYYYYYYYYYY\3ZZZZZZZZZZZZZZZZZZZZ\3
采用下面的方案:
- 写入文件中,一定要考虑下一次在读取的时候,也要方便操作!
- 类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...
- 方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3url
我们了解了大概的情况之后,我们来将我们所需要的数据源拷贝到data目录下的intput目录下:
最后,我们在data目录下的raw_html目录下创建有一个raw.txt文件,用来存储干净的数据文档
2. 编写parser模块
1. 基本结构设计
这里我是在vim下进行代码编写的,你可以选择vscode,但是需要连接一下云服务器,与Linux进行同步。
基本框架主要完成的工作如下:
- 将data/input/所有后缀为html的文件筛选出来
- 然后对筛选好的html文件进行解析(去标签),拆分出标题、内容、网址
- 最后将去标签后的所有html文件的标题、内容、网址按照 \3 作为分割符,每个文件再按照 \n 进行区分。写入到data/raw_html/raw.txt下
#include <iostream>
#include <vector>
#include <string>//将数据源的路径 和 清理后干净文档的路径 径定义好
const std::string src_path = "data/input"; //数据源的路径
const std::string output = "data/raw_html/raw.txt"; //清理后干净文档的路径//DocInfo --- 文件信息结构体
typedef struct DocInfo
{std::string title; //文档的标题std::string content; //文档的内容std::string url; //该文档在官网当中的url
}DocInfo_t;// const & ---> 输入
// * ---> 输出
// & ---> 输入输出//把每个html文件名带路径,保存到files_list中
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list);//按照files_list读取每个文件的内容,并进行解析
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results);//把解析完毕的各个文件的内容写入到output
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);int main()
{std::vector<std::string> files_list;// 第一步:递归式的把每个html文件名带路径,保存到files_list中,方便后期进行一个一个的文件读取if(!EnumFile(src_path, &files_list)) //EnumFile--枚举文件{std::cerr << "enum file name error! " << std::endl;return 1;}// 第二步:按照files_list读取每个文件的内容,并进行解析std::vector<DocInfo_t> results;if(!ParseHtml(files_list, &results))//ParseHtml--解析html{std::cerr << "parse html error! " << std::endl;return 2;}// 第三部:把解析完毕的各个文件的内容写入到output,按照 \3 作为每个文档的分隔符if(!SaveHtml(results, output))//SaveHtml--保存html{std::cerr << "save html error! " << std::endl;return 3;}return 0;
}
2. 细节实现
主要实现:枚举文件、解析html文件、保存html文件三个工作。
这三个工作完成是需要我们使用boost库当中的方法的,我们需要安装一下boost的开发库:
命令:sudo yum install -y boost-devel
下图就是我们接下来编写代码需要用到的boost库当中的filesystem方法。
枚举文件
//在原有的基础上添加这个头文件
#include <boost/filesystem.hpp>bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{namespace fs = boost::filesystem;fs::path root_path(src_path); // 定义一个path对象,枚举文件就从这个路径下开始// 判断路径是否存在if(!fs::exists(root_path)){std::cerr << src_path << " not exists" << std::endl;return false;}// 对文件进行递归遍历fs::recursive_directory_iterator end; // 定义了一个空的迭代器,用来进行判断递归结束for(fs::recursive_directory_iterator iter(root_path); iter != end; iter++){// 判断指定路径是不是常规文件,如果指定路径是目录或图片直接跳过if(!fs::is_regular_file(*iter)){continue;}// 如果满足了是普通文件,还需满足是.html结尾的// 如果不满足也是需要跳过的// ---通过iter这个迭代器(理解为指针)的一个path方法(提取出这个路径)// ---然后通过extension()函数获取到路径的后缀if(iter->path().extension() != ".html"){continue;}//std::cout << "debug: " << iter->path().string() << std::endl; // 测试代码// 走到这里一定是一个合法的路径,以.html结尾的普通网页文件files_list->push_back(iter->path().string()); // 将所有带路径的html保存在files_list中,方便后续进行文本分析}return true;
}
代码编写到这里我们就可以进行测试了,使用上述代码中注释掉的代码进行测试,首先编写Makefile:
cc=g++
parser:parser.cc $(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
.PHONY:clean
clean:rm -f parser
接下来就可以make,然后运行了:
解析html文件
- 读取刚刚枚举好的文件
- 解析html文件中的title
- 解析html文件中的content
- 解析html文件中的路径,构建url
这里我们将这读取操作写到一个工具类中,包括后续有什么方法也可以写到这个里面,方便调用。创建一个util.hpp
util.hpp如下:
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <vector>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 << " error" << std::endl;return false;}std::string line;while(std::getline(in, line)) //如何理解getline读取到文件结束呢??getline的返回值是一个&,while(bool), 本质是因为重载了强制类型转化{*out += line;}in.close();return true;}};
}
解析html文件:
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{for(const std::string &file : files_list){// 1.读取文件,Read()std::string result;if(!ns_util::FileUtil::ReadFile(file, &result)){continue;}// 2.解析指定的文件,提取titleDocInfo_t doc;if(!ParseTitle(result, &doc.title)){continue;}// 3.解析指定的文件,提取contentif(!ParseContent(result, &doc.content)){continue;}// 4.解析指定的文件路径,构建urlif(!ParseUrl(file, &doc.url)){continue; }// 到这里,一定是完成了解析任务,当前文档的相关结果都保存在了doc里面results->push_back(std::move(doc)); // 本质会发生拷贝,效率肯能会比较低,这里我们使用move后的左值变成了右值,去调用push_back的右值引用版本}return true;
}
解析html的title:
在进行提取title的时候,我们可以看看html的代码。它显示标题的时候,是以<title>标题</title>构成的,我们只需要find(<title>)就能找到这个标签的左尖括号的位置,然后加上<title>的长度,此时就指向了标题的起始位置,同理,再去找到</title>的左尖括号,最后截取子串;
static 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);return true; }
解析html的content
解析内容的时候,我们采用一个简易的状态机来完成,状态机包括两种状态:LABLE(标签)和CONTENT(内容);
html的代码中标签都是这样的<>;起始肯定是标签,我们追个字符进行遍历判断,如果遇到“>”,表明下一个即将是内容了,我们将状态机置为CONTENT,接着将内容保存起来,如果此时遇到了“<”,表明到了标签了,我们再将状态机置为LABLE;不断的循环,知道遍历结束;
static 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; }
解析html的url
在编写解析html的url的时候,我们需要注意,我们自己路径下的html的路径和官网上的路径是有对应关系的:
- 官网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
url_tail = [data/input(删除)] /accumulators.html -> url_tail = /accumulators.html;//将我们项目的路径data/input删除后得到/accumulators.html; 将url_head + url_tail 得到 官网的urlstatic bool ParseUrl(const std::string &file_path, std::string *url) { std::string url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html"; std::string url_tail = file_path.substr(src_path.size());//将data/input截取掉 *url = url_head + url_tail;//拼接return true; }
我们不是已经定义好了两个路径嘛!源数据路径和清理后干净文档的路径;url_head这个比较简单,直接复制官网的。url_tail,我们可以将传过来的文件路径使用一个substr把data/input截取掉,保留剩下的,然后和url_head拼接起来。
保存html文件
说明一下,分隔符为什么使用‘\3’ :
\3在ASSCII码表中是不可以显示的字符,我们将title、content、url用\3进行区分,不会污染我们的文档,当然你也可以使用\4等
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());}out.close();return true;
}
接下来我们做一下测试:运行代码后,查看我们data/raw_html/raw.txt文件,就如下图:
至此,我们的parser(去标签+数据清)模块就完成了,为了大家能够更好的理解,下面是一张关系图:
六、编写建立索引的模块 Index
1. 节点设计
在构建索引模块时,我们要构建出正排索引和倒排索引,正排索引是构建倒排索引的基础;通过给到的关键字,去倒排索引里查找出文档ID,再根据文档ID,找到对应的文档内容;所以在这个index模块中,就一定要包含两个节点结构,一个是文档信息的节点,一个是倒排对应的节点;
namespace ns_index
{struct DocInfo //文档信息节点{std::string title; //文档的标题std::string content; //文档对应的去标签后的内容std::string url; //官网文档的urluint64_t doc_id; //文档的ID};struct InvertedElem //倒排对应的节点{uint64_t doc_id; //文档IDstd::string word; //关键字(通过关键字可以找到对应的ID)int weight; //权重---根据权重对文档进行排序展示};
}
说明一下:
在倒排对应的节点之中, 有doc_id、word和weight;我们可以通过word关键字找到对应的文档ID,并且我们有文档的信息节点,通过倒排找到的文档ID,就能够在文档信息节点中找到对应的文档所有内容;这两个节点都有doc_id,就像MySQL中外键,相当于两张表产生了关联;
2. 基本结构设计
1. Index类的基本框架
我们创建一个Index类:主要用来构建索引模块,但是内部的细节还是比较多的,暂时不多赘述;索引模块最大的两个部分当然是构建正排索引和构建倒排索引,其主要接口如下:
namespace ns_index
{struct DocInfo //文档信息节点{std::string title; //文档的标题std::string content; //文档对应的去标签后的内容std::string url; //官网文档的urluint64_t doc_id; //文档的ID};struct InvertedElem //倒排对应的节点{uint64_t doc_id; //文档IDstd::string word; //关键字(通过关键字可以找到对应的ID)int weight; //权重---根据权重对文档进行排序展示};typedef std::vector<InvertedElem> InvertedList;class Index{private://正排索引的数据结构采用数组,数组下标就是天然的文档IDstd::vector<DocInfo> forward_index; //正排索引//倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]std::unordered_map<std::string, InvertedList> inverted_index;public:Index(){} ~Index(){}public://根据doc_id找到正排索引对应doc_id的文档内容DocInfo* GetForwardIndex(uint64_t doc_id){//...}//根据倒排索引的关键字word,获得倒排拉链InvertedList* GetInvertedList(const std::string &word){//...}//根据去标签,格式化后的文档,构建正排和倒排索引 //将数据源的路径:data/raw_html/raw.txt传给input即可,这个函数用来构建索引bool BuildIndex(const std::string &input){}
}
2. 获取正排索引(GetForwardIndex)
GetForwardIndex函数:根据正排索引的doc_id找到文档内容
//根据doc_id找到正排索引对应doc_id的文档内容
DocInfo* GetForwardIndex(uint64_t doc_id)
{//如果这个doc_id已经大于正排索引的元素个数,则索引失败if(doc_id >= forward_index.size()){ std::cout << "doc_id out range, error!" << std::endl;return nullptr;}return &forward_index[doc_id];//否则返回相应doc_id的文档内容
}
2. 获取倒排索引(GetInvertedList)
GetInvertedList函数:根据倒排索引的关键字word,获得倒排拉链(和上面类似)
//根据倒排索引的关键字word,获得倒排拉链
InvertedList* GetInvertedList(const std::string &word)
{auto iter = inverted_index.find(word);if(iter == inverted_index.end()){std::cerr << " have no InvertedList" << std::endl;return nullptr;}return &(iter->second);
}
3. 建立索引(BuildIndex)
BuildIndex函数:根据去标签,格式化后的文档,构建正排和倒排索引
在编写这部分代码时,稍微复杂一些,我们要构建索引,那我们应该是先把处理干净的文档读取上来,是按行读取,这样就能读到每个html文档;按行读上来每个html文档后,我们就可以开始构建正排索引和倒排索引,此时就要提供两个函数,分别为BuildForwardIndex(构建正排索引)和 BuildInvertedIndex(构建倒排索引),基本的代码如下:
//根据去标签,格式化后的文档,构建正排和倒排索引
//将数据源的路径:data/raw_html/raw.txt传给input即可,这个函数用来构建索引
bool BuildIndex(const std::string &input)
{//在上面SaveHtml函数中,我们是以二进制的方式进行保存的,那么读取的时候也要按照二进制的方式读取,读取失败给出提示std::ifstream in(input, std::ios::in | std::ios::binary);if(!in.is_open()){std::cerr << "sory, " << input << " open error" << std::endl;return false;}std::string line;int count = 0;while(std::getline(in, line)){DocInfo* doc = BuildForwardIndex(line);//构建正排索引if(nullptr == doc){std::cerr << "build " << line << " error" << std::endl;continue;}BuildInvertedIndex(*doc);//有了正排索引才能构建倒排索引count++; if(count % 50 == 0) { std::cout << "当前已经建立的索引文档:" << count << "个" << std::endl; }}return true;
}
5. 构建正排索引(BuildForwardIndex)
BuildForwardIndex(构建正排索引):
在编写构建正排索引的代码前,我们要知道,在构建索引的函数中,我们是按行读取了每个html文件的(每个文件都是这种格式:title\3content\3url...)构建正排索引,就是将DocInfo结构体内的字段进行填充,这里我们就需要给一个字符串切分的函数,我们写到util.hpp中,这里我们又要引入一个新的方法——boost库当中的切分字符串函数split;代码如下:
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <boost/algorithm/string.hpp>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 << " error" << std::endl;return false;}std::string line;while(std::getline(in, line)) //如何理解getline读取到文件结束呢??getline的返回值是一个&,while(bool), 本质是因为重载了强制类型转化{*out += line;}in.close();return true;}};class StringUtil{public://切分字符串static void Splist(const std::string &target, std::vector<std::string> *out, const std::string &sep){//boost库中的split函数boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);//第一个参数:表示你要将切分的字符串放到哪里//第二个参数:表示你要切分的字符串//第三个参数:表示分割符是什么,不管是多个还是一个//第四个参数:它是默认可以不传,即切分的时候不压缩,不压缩就是保留空格//如:字符串为aaaa\3\3bbbb\3\3cccc\3\3d//如果不传第四个参数 结果为aaaa bbbb cccc d//如果传第四个参数为boost::token_compress_on 结果为aaaabbbbccccd//如果传第四个参数为boost::token_compress_off 结果为aaaa bbbb cccc d}};
}
构建正排索引的编写:
//构建正排索引
DocInfo* BuildForwardIndex(const std::string &line)
{// 1.解析line,字符串切分// 将line中的内容且分为3段:原始为title\3content\3url\3// 切分后:title content urlstd::vector<std::string> results;std::string sep = "\3"; //行内分隔符ns_util::StringUtil::Splist(line, &results, sep);//字符串切分 if(results.size() != 3) { return nullptr; } // 2.字符串进行填充到DocInfo 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)); //使用move可以减少拷贝带来的效率降低return &forward_index.back();
}
6. 倒排索引的原理介绍
建立倒排的原理,我们之前只是单纯的说了一下,没有详细的说明如何实现,接下来我通过x张图来解释建立倒排索引的原理:
总的思路:
- 对title和content进行分词(使用cppjieba)
- 在分词的时候,必然会有某些词在title和content中出现过;我们这里还需要做一个处理,就是对每个词进行词频统计(你可想一下,你在搜索某个关键字的时候,为什么有些文档排在前面,而有些文档排在最后)这主要是词和文档的相关性(我们这里认为关键字出现在标题中的相关性高一些,出现在内容中的低一些,当然,关于相关性其实是比较复杂的,我们这里只考虑这些)
- 自定义相关性:我们有了词和文档的相关性的认识后,就要来自己设计这个相关性;我们把出现在title中的词,其权重更高,在content中,其权重低一些(如:让出现在title中的词的词频x10,出现在content中的词的词频x1,两者相加的结果称之为该词在整个文档中的权重)根据这个权重,我们就可以对所有文档进行权重排序,进行展示,权重高的排在前面展示,权重低的排在后面展示
伪代码操作演示:
如下是我们之前的基本结构代码
//倒排拉链节点 struct InvertedElem{uint64_t doc_id; //文档的IDstd::string word; //关键词int weight; //权重 };//倒排拉链 typedef std::vector<InvertedElem> InvertedList;//倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系] std::unordered_map<std::string, InvertedList> inverted_index;//文档信息节点 struct DocInfo{std::string title; //文档的标题std::string content; //文档对应的去标签之后的内容std::string url; //官网文档urluint64_t doc_id; //文档的ID };
1. 需要对 title && content都要先分词 -- 使用jieba分词
- title: 吃/葡萄/吃葡萄(title_word)
- content:吃/葡萄/不吐/葡萄皮(content_word)
2. 词频统计统计词频,它是包含标题和内容的,我们就需要有一个结构体,来存储每一篇文档中每个词出现在title和content中的次数,伪代码如下://词频统计的结点 struct word_cnt{title_cnt; //词在标题中出现的次数content_cnt;//词在内容中出现的次数 }
统计这些次数之后,我们还需要将词频和关键词进行关联,文档中的每个词都要对应一个词频结构体,这样我们通过关键字就能找到其对应的词频结构体,通过这个结构体就能知道该关键字在文档中的title和content中分别出现了多少次,下一步就可以进行权重的计算。这里我们就可以使用数据结构unordered_map来进行存储。伪代码如下:
//关键字和词频结构体的映射 unordered_map<std::string, word_cnt> word_map;//范围for进行遍历,对title中的词进行词频统计 for(auto& word : title_word){word_map[word].title_cnt++; //吃(1)/葡萄(1)/吃葡萄(1) } //范围for进行遍历,对content中的词进行词频统计 for(auto& word : content_word){word_map[word].content_cnt++; //吃(1)/葡萄(1)/不吐(1)/葡萄皮(1) }
3. 自定义相关性知道了在文档中,标题和内容每个词出现的次数,接下来就需要我们自己来设计相关性了,伪代码如下://遍历刚才那个unordered_map<std::string, word_cnt> word_map; for(auto& word : word_map){struct InvertedElem elem;//定义一个倒排拉链,然后填写相应的字段elem.doc_id = 123;elem.word = word.first;elem.weight = 10*word.second.title_cnt + word.second.content_cnt ;//权重计算inverted_index[word.first].push_back(elem);//最后保存到倒排索引的数据结构中 }//倒排索引结构如下: //std::unordered_map<std::string, InvertedList> inverted_index;
至此就是倒排索引比较完善的原理介绍和代码思路。
7. cppjieba分词工具的安装和使用介绍
获取链接:https://github.com/yanyiwu/cppjieba 里面有详细的教程
我这里是在GitHub上下载的解压包,然后自己解压的(可以使用git clone,主要因为太慢了,就直接下的压缩包),就解压好后基本环境的搭建,如下:
创建一个test目录用来解压好cppjieba,效果如下:
查看cppjieba-master目录,里面包含如下:
我们待会儿需要用到的分词工具是在include/cppjieba/jieba.hpp
首先,这是别人的写好的一个开源项目,里面会有这个测试代码,通常是在test目录下:
我们来做个分词演示,先将这个demo.cpp拷贝到我们的test目录下:
打开之后,就是一堆错误,主要原因是路径不对:
首先,从上图可以看到头文件的路径就不对,我们先来修改一下头文件的路径,它本身是要使用cppjieba/Jieba.hpp的,我们看一下这个头文件的具体路径:
路径是cppjieba-master/include/cppjieba/Jieba.hpp
我们要在test目录下执行这个demo.cpp,要引入这个头文件,我们不能直接引入,需要使用软连接:
软连接建立好后并修改demo.hpp的相应路径,再将该包的头文件包起来,再来查看demo.hpp是否还有错误:
我们编译后发现,limonp/Logging.hpp这个头文件没有:
此时,我们还是需要对这个头文件进行软连接,我们通过查找,发现有这么一个路径:
但是里面什么东西都没有,这是我在联系项目中出现的问题,经过我去GitHub查找一番后,发现它在另外一个压缩包里:
解压好如下:
此时,我们找一下limonp/Logging.hpp:
拷贝之后如下:
此时,再打开demo.hpp既没有任何问题了:(命令行参数的提示,不是错误,不考虑)
7~11行的路径中有个dict目录,在我们的test目录下是没有的,我还需要软连接:
我们将路径都完善之后,接下来,我们编译运行一下demo.hpp,看下效果:
可以看出,分词效果还是很不错的。那么接下来就要在我们的项目路径中,加入cppjieba下的Jieba.hpp,操作和上面的类似,这里我就不在操作了。直接看结果:
上面的操作做完之后,就可以在我们的项目中引入头文件,来使用cppjieba分词工具啦!!
8. 引入cppjieba到项目中
将软链接建立好之后,我们在util.hpp中编写一个jieba分词的类,主要是为了方便后期其他地方需要使用的时候,可以直接调用。
我们在util.hpp中创建一个 JiebaUtil的分词工具类,首先我们先看一下之前测试过的demo.cpp的代码:
util.hpp代码如下:
#pragma once
#include <iostream>
#include <string>
#include <fstream>
#include <vector>
#include <boost/algorithm/string.hpp>
#include "cppjieba/Jieba.hpp"//引入头文件(确保你建立的没有错误才可以使用)
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 << " error" << std::endl;return false;}std::string line;while(std::getline(in, line)) //如何理解getline读取到文件结束呢??getline的返回值是一个&,while(bool), 本质是因为重载了强制类型转化{*out += line;}in.close();return true;}};class StringUtil{public://切分字符串static void Splist(const std::string &target, std::vector<std::string> *out, const std::string &sep){//boost库中的split函数boost::split(*out, target, boost::is_any_of(sep), boost::token_compress_on);//第一个参数:表示你要将切分的字符串放到哪里//第二个参数:表示你要切分的字符串//第三个参数:表示分割符是什么,不管是多个还是一个//第四个参数:它是默认可以不传,即切分的时候不压缩,不压缩就是保留空格//如:字符串为aaaa\3\3bbbb\3\3cccc\3\3d//如果不传第四个参数 结果为aaaa bbbb cccc d//如果传第四个参数为boost::token_compress_on 结果为aaaabbbbccccd//如果传第四个参数为boost::token_compress_off 结果为aaaa bbbb cccc d}};//下面这5个是分词时所需要的词库路径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) { //调用CutForSearch函数,第一个参数就是你要对谁进行分词,第二个参数就是分词后的结果存放到哪里jieba.CutForSearch(src, *out); } }; //类外初始化,就是将上面的路径传进去,具体和它的构造函数是相关的,具体可以去看一下源代码cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
}
9. 构建倒排索引(BuildInvertedIndex)
BuildInvertedIndex(构建倒排索引):
构建倒排索引相对复杂一些,只要将上面倒排索引的原理和伪代码的思路;理解到位后,下面的代码就比较简单了。
//构建倒排索引
bool BuildInvertedIndex(const DocInfo &doc)
{ //词频统计结构体 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(auto s : title_words) { boost::to_lower(s); // 将我们的分词进行统一转化成为小写的 word_map[s].title_cnt++;//如果存在就获取,不存在就新建 } //对文档内容进行分词 std::vector<std::string> content_words; ns_util::JiebaUtil::CutString(doc.content, &content_words); //对文档内容进行词频统计 for(auto s : content_words) { boost::to_lower(s); // 将我们的分词进行统一转化成为小写的 word_map[s].content_cnt++; } #define X 10
#define Y 1 //最终构建倒排 for(auto &word_pair : word_map) { InvertedElem item; item.doc_id = doc.doc_id; //倒排索引的id即文档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]; inverted_list.push_back(std::move(item)); } return true;
}
七、编写搜索引擎模块 Searcher
1. 基本结构
我们已经完成了数据清洗、去标签和索引相关的工作,接下来就是要编写服务器所提供的服务,我们试想一下,服务器要做哪些工作:首先,我们的数据事先已经经过了数据清洗和去标签的,服务器运行起来之后,应该要先去构建索引,然后通过服务,索引我们在Searcher模块中实现两个函数,分别为InitSearcher()和Search(),代码如下:
#include "index.hpp"
namespace ns_searcher{class Searcher{private:ns_index::Index *index; //供系统进行查找的索引public:Searcher(){}~Searcher(){}public:void InitSearcher(const std::string &input){//...}//query: 搜索关键字//json_string: 返回给用户浏览器的搜索结果void Search(const std::string &query, std::string *json_string){//...}};
}
2. 初始化服务(InitSearcher)
服务器要去构建索引,本质上就是去构建一个Index对象,然后调用其内部的方法,我们知道构建正排索引和倒排索引本质就是将磁盘上的数据加载的内存,其数据量还是比较大的(可能本项目的数据量不是很大)。从这一点可以看出,假设创建了多个Index对象的话,其实是比较占内存的,我们这里就可以将这个Index类设计成为单例模式;关于单例模式是什么及代码框架(懒汉模式和饿汉模式)我这里不做详细介绍,不了解的小伙伴可以去看我
写的这篇博客:https://blog.csdn.net/sjsjnsjnn/article/details/126364511里面有详细的讲解,这里我直接给出Index的全部代码。
1. Index模块的单例设计
#pragma once #include <iostream>
#include <string>
#include <vector>
#include <mutex>
#include <fstream>
#include <unordered_map>
#include "util.hpp"namespace ns_index
{struct DocInfo //文档信息节点{std::string title; //文档的标题std::string content; //文档对应的去标签后的内容std::string url; //官网文档的urluint64_t doc_id; //文档的ID};struct InvertedElem //倒排对应的节点{uint64_t doc_id; //文档IDstd::string word; //关键字(通过关键字可以找到对应的ID)int weight; //权重---根据权重对文档进行排序展示};typedef std::vector<InvertedElem> InvertedList;class Index{private://正排索引的数据结构采用数组,数组下标就是天然的文档IDstd::vector<DocInfo> forward_index; //正排索引//倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]std::unordered_map<std::string, InvertedList> inverted_index;private:Index(){} //这个一定要有函数体,不能deleteIndex(const Index&) = delete;Index& operator = (const Index&) = delete;static Index* instance;static std::mutex mtx;//C++互斥锁,防止多线程获取单例存在的线程安全问题public:~Index(){}public://获取index单例static Index* GetInstance(){if(nullptr == instance)// 双重判定空指针, 降低锁冲突的概率, 提高性能{mtx.lock();//加锁if(nullptr == instance){instance = new Index();//获取单例}mtx.unlock();//解锁}return instance;}//根据doc_id找到正排索引对应doc_id的文档内容DocInfo* GetForwardIndex(uint64_t doc_id){if(doc_id >= forward_index.size()){std::cout << "doc_id out range, error!" << std::endl;return nullptr;}return &forward_index[doc_id];}//根据倒排索引的关键字word,获得倒排拉链InvertedList* GetInvertedList(const std::string &word){auto iter = inverted_index.find(word);if(iter == inverted_index.end()){std::cerr << " have no InvertedList" << std::endl;return nullptr;}return &(iter->second);}//根据去标签,格式化后的文档,构建正排和倒排索引 //将数据源的路径:data/raw_html/raw.txt传给input即可,这个函数用来构建索引bool BuildIndex(const std::string &input){//在上面SaveHtml函数中,我们是以二进制的方式进行保存的,那么读取的时候也要按照二进制的方式读取,读取失败给出提示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 = BuildForwardIndex(line);//构建正排索引if(nullptr == doc){std::cerr << "build " << line << " error" << std::endl;continue;}BuildInvertedIndex(*doc);//有了正排索引才能构建倒排索引}return true;}public://构建正排索引DocInfo* BuildForwardIndex(const std::string &line){// 1.解析line,字符串切分// 将line中的内容且分为3段:原始为title\3content\3url\3// 切分后:title content urlstd::vector<std::string> results;std::string sep = "\3"; //行内分隔符ns_util::StringUtil::Splist(line, &results, sep);//字符串切分 if(results.size() != 3) { return nullptr; } // 2.字符串进行填充到DocInfo 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)); //使用move可以减少拷贝带来的效率降低return &forward_index.back(); }//构建倒排索引bool BuildInvertedIndex(const DocInfo &doc) { //词频统计结构体 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(auto s : title_words) { boost::to_lower(s); // 将我们的分词进行统一转化成为小写的 word_map[s].title_cnt++;//如果存在就获取,不存在就新建 } //对文档内容进行分词 std::vector<std::string> content_words; ns_util::JiebaUtil::CutString(doc.content, &content_words); //对文档内容进行词频统计 for(auto s : content_words) { boost::to_lower(s); // 将我们的分词进行统一转化成为小写的 word_map[s].content_cnt++; } #define X 10 #define Y 1 //最终构建倒排 for(auto &word_pair : word_map) { InvertedElem item; item.doc_id = doc.doc_id; //倒排索引的id即文档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]; inverted_list.push_back(std::move(item)); } return true; }}; Index* Index::instance = nullptr;std::mutex Index::mtx;
}
2. 编写InitSearcher
#include "index.hpp"
namespace ns_searcher{class Searcher{private:ns_index::Index *index; //供系统进行查找的索引public:Searcher(){}~Searcher(){}public://这里的input就是用户传过来的关键字,首先创建单例,然后构建索引void InitSearcher(const std::string &input){//1.获取或者创建index对象 index = ns_index::Index::GetInstance(); //2.根据index对象建立索引 index->BuildIndex(input); }};
}
3. 提供服务(Search)
对于提供服务,我们需要从四个方面入手,达到服务效果:
- 对用户的输入的关键字,我们首先要做的就是分词,只有分成不同的词之后,才能按照不同的词去找文档;
- 分词完毕后,我们就要去触发这些分词,本质就是查找建立好的正排索引和倒排索引;
- 我们的每个文档都是设置了权重字段的,我们就应该在触发分词之后,进行权重的降序排序,达到权重高的文档靠前,权重低的文档靠后;
- 根据排序完的结果,构建json串,用于网络传输。因为结构化的数据不便于网络传输,我们就需要使用一个工具(jsoncpp),它是用来将结构化的数据转为字节序(你可以理解为很长的字符串),jsoncpp可以进行序列化(将结构化的数据转换为字节序列,发生到网络)和反序列化(将网络中的字节序列转化为结构化的数据)
jsoncpp使用的效果如下图:
具体的使用方法会在下面有介绍。
1. 对用户关键字进行分词
为什么我们要对用户输入的关键字进行分词呢?
这也不难理解,虽然我们index模块中的正排索引中已经做了分词操作,这只能说明服务器已经将数据准备好了,按照不同的词和对应的文档分好类了;但是用户输入的关键字,我们依旧是要做分词操作的。设想一下,如果没有做分词,直接按照原始的关键字进行查找,给用户反馈的文档一定没有分词来的效果好,甚至有可能匹配不到文档。影响用户的体验。代码如下:
//query--->搜索关键字
//json_string--->返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{//1.分词---对query按照Searcher的要求进行分词 std::vector<std::string> words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, &words);//分词操作
}
2. 触发分词,进行索引查找
分词完成以后,我们就应该按照分好的每个词(关键字)去获取倒排拉链,我们将获取上来的倒排拉链进行保存到vector当中,这也就是我们根据用户关键字所查找的结果,但是我们还需要考虑一个问题,用户输入的关键字进行分词了以后,有没有可能多个关键字对应的是同一个文档,如下图所示:
根据上面的图,我们首先想到的就是去重。其次,每个倒排拉链的结点都包含:doc_id、关键字和权重。既然显示了重复的文档,我们应该是只显示一个,那么这个最终显示的文档其权重就是几个文档之和,关键字就是几个文档的组合,那么我们可以定义一个新的结构体来保存查找后的倒排拉链,代码如下:
//该结构体是用来对重复文档去重的结点结构
struct InvertedElemPrint
{uint64_t doc_id; //文档IDint weight; //重复文档的权重之和std::vector<std::string> words;//关键字的集合,我们之前的倒排拉链节点只能保存一个关键字InvertedElemPrint():doc_id(0), weight(0){}
};
有了上面的铺垫,我们就可以来编写触发分词的代码了:
//query--->搜索关键字
//json_string--->返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{//1.分词---对query按照Searcher的要求进行分词 std::vector<std::string> words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, &words);//分词操作//2.触发---就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索关键字也需要std::vector<InvertedElemPrint> inverted_list_all; //用vector来保存std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;//用来去重for(std::string word : words)//遍历分词后的每个词{boost::to_lower(word);//忽略大小写ns_index::InvertedList* inverted_list = index->GetInvertedList(word);//获取倒排拉链if(nullptr == inverted_list){continue;}//遍历获取上来的倒排拉链for(const auto &elem : *inverted_list){auto &item = tokens_map[elem.doc_id];//插入到tokens_map中,key值如果相同,这修改value中的值item.doc_id = elem.doc_id;item.weight += elem.weight;//如果是重复文档,key不变,value中的权重累加item.words.push_back(elem.word);//如果树重复文档,关键字会被放到vector中保存}}//遍历tokens_map,将它存放到新的倒排拉链集合中(这部分数据就不存在重复文档了)for(const auto &item : tokens_map) {inverted_list_all.push_back(std::move(item.second));}
}
3. 按文档权重进行降序排序
对于排序,应该不难,我们直接使用C++库当中的sort函数,并搭配lambda表达式使用;当然你也可以自己写一个快排或者归并排序,按权重去排;
//query--->搜索关键字
//json_string--->返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{//1.分词---对query按照Searcher的要求进行分词 std::vector<std::string> words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, &words);//分词操作//2.触发---就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索关键字也需要std::vector<InvertedElemPrint> inverted_list_all; //用vector来保存std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;//用来去重for(std::string word : words)//遍历分词后的每个词{boost::to_lower(word);//忽略大小写ns_index::InvertedList* inverted_list = index->GetInvertedList(word);//获取倒排拉链if(nullptr == inverted_list){continue;}//遍历获取上来的倒排拉链for(const auto &elem : *inverted_list){auto &item = tokens_map[elem.doc_id];//插入到tokens_map中,key值如果相同,这修改value中的值item.doc_id = elem.doc_id;item.weight += elem.weight;//如果是重复文档,key不变,value中的权重累加item.words.push_back(elem.word);//如果树重复文档,关键字会被放到vector中保存}}//遍历tokens_map,将它存放到新的倒排拉链集合中(这部分数据就不存在重复文档了)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 InvertedElemPrint &e1, const InvertedElemPrint &e2){return e1.weight > e2.weight;});
}
4. 根据排序结果构建json串
关于json的使用,我们首先需要在Linux下安装jsoncpp:sudo yum install -y jsoncpp-devel 这里我之前下载过了,已经是最新的版本了,你们只需要输入上面的指令,有这样的提示,就表明安装成功了。
如何使用:
- root对象:你可以理解为json数组;
- item1对象:就是json中value的对象,他可以保存kv值
- item2对象:就是json中value的对象,他可以保存kv值
- 将item1和item2 ,append到root中:你可以理解为将root这个大json数组,保存了两个子json
- 序列化的方式有两种:StyledWriter和FastWriter 两者的区别:1. 呈现的格式不一样;2. 在网络传输中FastWriter更快。
序列化方式1:StyledWriter
序列化方式2:FastWriter
有了基本的了解之后,我们开始编写正式的代码:
//query--->搜索关键字
//json_string--->返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{//1.分词---对query按照Searcher的要求进行分词 std::vector<std::string> words; //用一个数组存储分词的结果 ns_util::JiebaUtil::CutString(query, &words);//分词操作//2.触发---就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索关键字也需要std::vector<InvertedElemPrint> inverted_list_all; //用vector来保存std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;//用来去重for(std::string word : words)//遍历分词后的每个词{boost::to_lower(word);//忽略大小写ns_index::InvertedList* inverted_list = index->GetInvertedList(word);//获取倒排拉链if(nullptr == inverted_list){continue;}//遍历获取上来的倒排拉链for(const auto &elem : *inverted_list){auto &item = tokens_map[elem.doc_id];//插入到tokens_map中,key值如果相同,这修改value中的值item.doc_id = elem.doc_id;item.weight += elem.weight;//如果是重复文档,key不变,value中的权重累加item.words.push_back(elem.word);//如果树重复文档,关键字会被放到vector中保存}}//遍历tokens_map,将它存放到新的倒排拉链集合中(这部分数据就不存在重复文档了)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 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]); //content是文档去标签后的结果,但不是我们想要的,我们要的是一部分 elem["url"] = doc->url; //调式 //elem["id"] = (int)item.doc_id; //elem["weight"] = item.weight; root.append(elem); } //Json::StyledWriter writer; //方便调试 Json::FastWriter writer;//调式没问题后使用这个 *json_string = writer.write(root);
}
在上述的代码中,我们构建出来的json串最后是要返回给用户的,对于内容,我们只需要一部分,而不是全部,所以我们还要实现一个 GetDesc 的函数:
std::string GetDesc(const std::string &html_content, const std::string &word)
{//找到word(关键字)在html_content中首次出现的位置//然后往前找50个字节(如果往前不足50字节,就从begin开始)//往后找100个字节(如果往后不足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位置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.截取子串,然后返回if(start >= end) return "None2";std::string desc = html_content.substr(start,end - start);desc += "...";return desc;
}
最后,我们来测试一下效果,编写debug.cc,这个文件和我们项目文件关联性不大,主要是用来调式(需要将上文代码中备注调式的代码放开):
#include "searcher.hpp"
#include <cstdio>
#include <iostream>
#include <string> const std::string input = "data/raw_html/raw.txt"; int main()
{ ns_searcher::Searcher *search = new ns_searcher::Searcher(); search->InitSearcher(input); //初始化search,创建单例,并构建索引 std::string query; //自定义一个搜索关键字 std::string json_string; //用json串返回给我们 char buffer[1024]; while(true) { std::cout << "Please Enter You Search Query:"; //提示输入 fgets(buffer, sizeof(buffer) - 1, stdin); //读取 buffer[strlen(buffer)-1] = 0; query = buffer; search->Search(query, &json_string); //执行服务,对关键字分词->查找索引->按权重排序->构建json串->保存到json_string->返回给我们 std::cout << json_string << std::endl;//输出打印 } return 0;
}
对应的Makefile:
运行结果如下:
我们输入搜索关键字:split
我们可以看到,效果很明显。我们复制第三个网址,查看一下权重是否一样:
当你再去查看其他网址,然后自己进行权重计算的时候,有时候会多一个或者少一个,我分析的原因就是,在对标题和内容进行分词的时候,产生的一些影响,但是大体上没有太大的问题。
测试完毕之后,那些测试可以删除或屏蔽
八、编写http_server模块
1. 引入cpp-httplib到项目中
安装cpp-httplib,安装的是v0.7.15版本:https://gitcode.net/mirrors/yhirose/cpp-httplib/-/releases?after=eyJpZCI6IjEwMDUwNCIsInJlbGVhc2VkX2F0IjoiMjAyMC0xMi0wMSAyMzozMjo0Mi4wMDAwMDAwMDAgQ1NUIn0https://gitcode.net/mirrors/yhirose/cpp-httplib/-/releases?after=eyJpZCI6IjEwMDUwNCIsInJlbGVhc2VkX2F0IjoiMjAyMC0xMi0wMSAyMzozMjo0Mi4wMDAwMDAwMDAgQ1NUIn0
下载zip,上传到服务器即可,这些操作在上面都演示过了。
cpp-httplib在使用的时候需要使用较新版本的gcc,centos 7下默认gcc 4.8.5,我们是需要升级的,这里我是已经升级过的,接下来我介绍一下升级gcc的方法。
升级gcc
- scl gcc devsettool 升级gcc(使用scl工具集来升级)
- 安装scl:sudo yum install centos-release-scl scl-utils-build
- 安装新版本gcc:sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
启动新版的gcc:scl enable devtoolset-7 bash(注意这只是本次会话有效) 长期使用新版的gcc:修改 ~/.bash_profile 文件只要在这个文件下加上这个命令,启动服务器就会执行,gcc就是新版的了
我们将cpp-httplib放到项目中的test目录下,并解压好;
提示:这里你可以将test目录修改为thirdparty,这样从命名上更加直观,将test目录下的一些软连接删除(使用unlink命令),但是在Boost_Searcher目录下的软连接还要重新修改一下。
cpp-httplib有了之后,我们只需要使用这个目录下的httplib.h文件即可:
建立软连接到我们的项目路径下:
至此,我们就可以在我们的项目中使用了。
2. cpp-httplib的使用介绍
创建一个http_server.cc的文件,编写测试代码:
#include "cpp-httplib/httplib.h" int main()
{ //创建一个Server对象,本质就是搭建服务端httplib::Server svr; // 这里注册用于处理 get 请求的函数,当收到对应的get请求时(请求hi时),程序会执行对应的函数(也就是lambda表达式)svr.Get("/hi", [](const httplib::Request& req, httplib::Response& rsp){ //设置 get "hi" 请求返回的内容 rsp.set_content("hello world!", "text/plain; charset=utf-8"); }); // 绑定端口(8080),启动监听(0.0.0.0表示监听任意端口)svr.listen("0.0.0.0", 8080); return 0;
}
对应的Makefile:
我们直接编译运行http_server
打开浏览器,访问我们这个端口(如43.138.201:8081/hi),结果如下:
但是当我们访问43.138.201:8081时,却找不到对应的网页,
像我们访问百度时,www.baidu.com,百度会给一个首页,所有在我们的项目目录下呢,也需要一个首页。 (在项目路径下创建一个wwwroot目录,目录中包含一个index.html文件)
编写我们的首页,并修改我们的http_server.cc:
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>boost搜索引擎</title>
</head>
<body><h1>欢迎来到我的世界</h1>
</body>
</html>
#include "cpp-httplib/httplib.h"const std::string root_path = "./wwwroot";int main()
{ //创建一个Server对象,本质就是搭建服务端httplib::Server svr; //访问首页svr.set_base_dir(root_path.c_str());// 这里注册用于处理 get 请求的函数,当收到对应的get请求时(请求hi时),程序会执行对应的函数(也就是lambda表达式)svr.Get("/hi", [](const httplib::Request& req, httplib::Response& rsp){ //设置 get "hi" 请求返回的内容 rsp.set_content("hello world!", "text/plain; charset=utf-8"); }); // 绑定端口(8080),启动监听(0.0.0.0表示监听任意端口)svr.listen("0.0.0.0", 8080); return 0;
}
再次通过浏览器进行访问:
3. 正式编写http_server
#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); //创建一个Server对象,本质就是搭建服务端httplib::Server svr; //访问首页svr.set_base_dir(root_path.c_str()); // 这里注册用于处理 get 请求的函数,当收到对应的get请求时(请求s时),程序会执行对应的函数(也就是lambda表达式)svr.Get("/s", [&search](const httplib::Request &req, httplib::Response &rsp){//has_param:这个函数用来检测用户的请求中是否有搜索关键字,参数中的word就是给用户关键字取的名字(类似word=split) if(!req.has_param("word")){ rsp.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8"); return; } //获取用户输入的关键字std::string word = req.get_param_value("word"); std::cout << "用户在搜索:" << word << std::endl; //根据关键字,构建json串std::string json_string; search.Search(word, &json_string);//设置 get "s" 请求返回的内容,返回的是根据关键字,构建json串内容rsp.set_content(json_string, "application/json"); }); std::cout << "服务器启动成功......" << std::endl; // 绑定端口(8080),启动监听(0.0.0.0表示监听任意端口)svr.listen("0.0.0.0", 8080); return 0;
}
此时我们编译运行我们的代码,先执行parser进行数据清洗,然后执行http_server,搭建服务,创建单例,构建索引,发生请求(根据用户输入的关键字,进行查找索引,构建json串),最后响应给用户
此时服务器启动成功,索引也建立完毕
此时,我们在浏览器进行访问(43.138.71.201:8080/s)
此时,我们在浏览器进行访问(43.138.71.201:8080/s?word=split)
最终,在浏览器上就显示出来了,到这里我们的后端内容大致上算是完成了,最后添加一个日志就可以了,如果你对前端不感兴趣,到这里就可以了。可以把日志功能的添加看一看
九、添加日志到项目中
我们创建一个log.hpp的头文件,需要添加日志的地方:index模块,searcher模块、http_server模块。代码如下:
#pragma once
#include <iostream>
#include <string>
#include <ctime> #define NORMAL 1 //正常的
#define WARNING 2 //错误的
#define DEBUG 3 //bug
#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;
}
/*
简单说明: 我们用宏来实现日志功能,其中LEVEL表明的是等级(有四种),这里的#LEVEL的作用是:把一个宏参数变成对应的字符串(直接替换)
C语言中的预定义符号:__FILE__:进行编译的源文件__LINE__:文件的当前行号
补充几个:__DATE__:文件被编译的日期__TIME__:文件被编译的时间__FUNCTION__:进行编译的函数
*/
你可以在你想要的地方进行添加
十、编写前端模块
前端模块,我做详细的解释,代码中都有注释,直接上代码
<!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: 50px;}/* 选中input标签,直接设置标签的属性,先要选中,标签选择器 *//* input在进行高度设置的时候,没有考虑边框的问题 */.container .search input {/* 设置left浮动 */float: left;width: 600px;height: 50px;/* 设置边框属性,依次是边框的宽度、样式、颜色 */border: 2px solid #CCC;/* 去掉input输入框的右边框 */border-right: none;/* 设置内内边距,默认文字不要和左侧边框紧挨着 */padding-left: 10px;/* 设置input内部的字体的颜色和样式 */color: #CCC;color: #CCC;font-size: 17px;}.container .search button {/* 设置left浮动 */float: left;width: 150px;height: 54px;/* 设置button的背景颜色 #4e6ef2*/background-color: #4e6ef2;color: #FFF;/* 设置字体的大小 */font-size: 19px;font-family: Georgia, 'Times New Roman', Times, serif 'Times New Roman', Times, serif;}.container .result {width: 100%;}.container .result .item {margin-top: 15px;}.container .result .item a {/* 设置为块级元素,单独占一行 */display: block;text-decoration: none;/* 设置a标签中的文字字体大小 */font-size: 22px;/* 设置字体的颜色 */color: #4e6ef2;}.container .result .item a:hover {/* 设置鼠标放在a之上的动态效果 */text-decoration: underline;}.container .result .item p {margin-top: 5px;font-size: 16px;font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;}.container .result .item i {/* 设置为块级元素,单独占一行 */display: block;/* 取消斜体风格 */ font-style: normal;color: green;}</style>
</head><body><div class="container"><div class="search"><input type="text" value="输入搜索关键字..."><button onclick="Search()">搜索一下</button></div><div class="result"><!-- <div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://hao.360.com/?hj=llq7a</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://hao.360.com/?hj=llq7a</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://hao.360.com/?hj=llq7a</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://hao.360.com/?hj=llq7a</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://hao.360.com/?hj=llq7a</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://hao.360.com/?hj=llq7a</i></div> --></div></div><script>function Search() {// 是浏览器的一个弹出窗// 1.提取数据,$可以理解为就是JQuery的别称let query = $(".container .search input").val();console.log("query = " + query); //console是浏览器对话框,可以用来进行查看js数据// 2.发起http请求,ajax属于一个和后端进行数据交互的函数$.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 = $("<p>", {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>
最终演示:
将我们的项目部署到Linux上: nohup ./http_server > log/log.txt 2>&1 &
一些日志信息就会保存到log/log.txt中
十一、项目总结
关于项目总结,主要是针对项目的扩展
1. 建立整站搜索
- 我们搜索的内容是在boost库下的doc目录下的html文档,你可以将这个库建立搜索,也可以将所有的版本,但是成本是很高的,对单个版本的整站搜索还是可以完成的,取决于你服务器的配置。
2. 设计一个在线更新的方案,信号,爬虫,完成整个服务器的设计
- 我们在获取数据源的时候,是我们手动下载的,你可以学习一下爬虫,写个简单的爬虫程序。采用信号的方式去定期的爬取。
3. 不使用组件,而是自己设计一下对应的各种方案
- 我们在编写http_server的时候,是使用的组件,你可以自己设计一个简单的;
4. 在我们的搜索引擎中,添加竞价排名
- 我们在给用户反馈是,提供的是json串,显示到网页上,有title、content和url;就可以在构建json串时,你加上你的博客链接(将博客权重变高了,就能够显示在第一个)
5. 热次统计,智能显示搜索关键词(字典树,优先级队列)
6. 设置登陆注册,引入对mysql的使用
相关文章:
【项目设计】—— 基于Boost库的搜索引擎
目录 前言 一、项目的相关背景 1. 什么是Boost库 2. 什么是搜索引擎 3. 为什么要做Boost搜索引擎 二、搜索引擎的宏观原理 三、搜索引擎技术栈和项目环境 四、正排索引 VS 倒排索引 —— 搜索引擎的具体原理 1. 正排索引(forword index) 2. 倒…...
全网详解MyBatis-Plus updateById方法更新不了空字符串或null的解决方法
文章目录1. 文章引言2. 分析问题3. 解决问题3.1 方法1:全局配置方式3.2 方法2:非null字段验证策略3.3 方法3:通过注解的方式4. 总结1. 文章引言 在开发的过程中,我们经常使用MyBatis-Plus的updateById方法更新数据表,…...
联想K14电脑开机全屏变成绿色无法使用怎么U盘重装系统?
联想K14电脑开机全屏变成绿色无法使用怎么U盘重装系统?最近有用户使用联想K14电脑的时候,开机后桌面就变成了绿色的背景显示,无法进行任何的操作。而且通过强制重启之后还是会出现这个问题,那么这个情况如何去进行系统重装呢&…...
Spring Boot HTTP 400排查
背景 前段时间朋友咨询他们公司某个HTTP接口偶现400错误,有没有什么好的分析方法和解决方案,使用的是Spring Cloud体系。最近有时间总结下这个问题的处理过程。 为了分析问题,笔者使用 Spring Boot 3.0.2还原报错场景进行讲解。 问题分析 …...
【手撕源码】vue2.x中keep-alive源码解析
🐱 个人主页:不叫猫先生 🙋♂️ 作者简介:前端领域新星创作者、阿里云专家博主,专注于前端各领域技术,共同学习共同进步,一起加油呀! 💫系列专栏:vue3从入门…...
ROS2机器人编程简述humble-第四章-BASIC DETECTOR .3
书中程序适用于turtlebot、husky等多种机器人,配置相似都可以用的。支持ROS2版本foxy、humble。基础检测效果如下:由于缺¥,所有设备都非常老旧,都是其他实验室淘汰或者拼凑出来的设备。机器人控制笔记本是2010年版本。…...
【图像分类】基于PyTorch搭建LSTM实现MNIST手写数字体识别(双向LSTM,附完整代码和数据集)
写在前面: 首先感谢兄弟们的关注和订阅,让我有创作的动力,在创作过程我会尽最大能力,保证作品的质量,如果有问题,可以私信我,让我们携手共进,共创辉煌。 在https://blog.csdn.net/A…...
【Linux】多线程编程 - 同步/条件变量/信号量
目录 一.线程同步 1.什么是线程同步 2.为什么需要线程同步 3.如何实现线程同步 二.条件变量 1.常见接口以及使用 2.wiat/signal中的第二个参数mutex的意义 3.代码验证 三.POSIX信号量 1.概念 2.常见接口以及使用 四.条件变量vsPOSIX信号量 一.线程同步 1.什么是线…...
ES优化方案
ES优化&联合HBASE: 【Elasticsearch】优秀实践-ESHbase的实现_少加点香菜的博客-CSDN博客_sceshbase ES写入性能优化方案 ElasticSearch 调优笔记_index.refresh_interval_六月飞雪的博客-CSDN博客 es如何提升写入性能_婲落ヽ紅顏誶的博客-CSDN博客_es写入性…...
从数据备份保护到完整生命周期管理平台,爱数全新发布 AnyBackup Family 8
编辑 | 宋慧 出品 | CSDN 云计算 从2003年创业,开始做数据备份技术,爱数已经走过了近20年的时间。现在,数据的价值被越来越多的业界与用户看到,数据分析应用赛道近年一直持续火热。而现在的爱数在做的,已经从数据的备…...
Go 微服务开发框架 DMicro 的设计思路
Go 微服务开发框架 DMicro 的设计思路 DMicro 源码地址: Gitee:dmicro: dmicro是一个高效、可扩展且简单易用的微服务框架。包含drpc,dserver等 背景 DMicro 诞生的背景,是因为我写了 10 来年的 PHP,想在公司内部推广 Go, 公司内部的组件及 rpc 协议都…...
浅谈功能测试
1.功能测试流程 1.1 功能测试流程 # 功能测试大致按照以下流程进行: (1).需求分析与评审(2).测试计划与测试方案(3).测试用例设计(4).测试用例评审(5).执行用例(6).缺陷跟踪及报告产出 1.2 功能测试流程详解 (1).需求分析与评审 功能测试应从需求出发, 功能测试就是尽量覆…...
UDP的详细解析
UDP的详细解析 文章目录UDP的详细解析UDP 概述UDP的首部格式检验和的计算抓包测试参考TCP/IP运输层的两个主要协议都是互联网的正式标准,即:用户数据报协议UDP (User Datagram Protocol)传输控制协议TCP (Transmission Control Protocol) 按照OSI的术语…...
史上最详细JUC教程之Synchronized与锁升级详解
在Java早期版本中,synchronized属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的Mutex Lock来实现的,挂起线程和恢复线程都需要转入内核态去完成,阻塞或唤醒一个Java线程需…...
Vue|初识Vue
Vue是一款用于构建用户界面的JavaScript框架。它基于标准HTML、CSS和JavaScript构建,并提供了一套声明式的、组件化的编程模型,帮助开发者高效地开发用户界面。 初识Vue1. Vue简介2. 开发准备3. 模板语法3.1 差值语法3.2 指令语法4. 数据绑定4.1 单向数据…...
在职阿里6年,一个29岁女软件测试工程师的心声
简单的先说一下,坐标杭州,14届本科毕业,算上年前在阿里巴巴的面试,一共有面试了有6家公司(因为不想请假,因此只是每个晚上去其他公司面试,所以面试的公司比较少)其中成功的有4家&…...
(C语言)自定义类型,枚举与联合
问:1. 结构体在自引用的时候不能怎么样?可以怎么样?2. Solve the problems:自定义一个学生结构体类型,要包含姓名,性别,年龄,六科成绩,家乡(也为结构体&#…...
node.js服务端笔记文档学会写接口,学习分类:path、包、模块化、fs、express、中间件、jwt、开发模式、cors。
node.js 学习笔记 node.js服务端笔记文档学会写接口,path、包、模块化、fs、express、中间件、JWT、开发模式、cors。 gitee:代码接口笔记 1什么是node.js nodejs 是基于ChromeV8,引擎的一个javaScript 运行环境。node.js 无法使用DOM和BO…...
初始C++(三):引用
文章目录一.引用的概念二.引用的使用1.引用作为输出型参数2. 引用作为函数返回值3.const引用三.引用的一些小问题四.引用和指针五.引用和指针的区别一.引用的概念 引用的作用是给一个已经存在的变量取别名,编译器不会为引用变量开空间,引用变量和被他引…...
【前端】参考C站动态发红包界面,高度还原布局和交互
最近有些小伙伴咨询博主说前端布局好难,其实都是熟能生巧! 模仿C站动态发红包界面,cssdiv实现布局,纯javascript实现交互效果 目录 1、界面效果 2、界面分析 2.1、整体结构 2.2、标题 2.3、表单 2.4、按钮 3、代码实现 3.…...
VR全景带你浪漫“狂飙”情人节,见证甜蜜心动
当情人节遇上VR,足以让情侣过一个难忘的情人节。马上情人节就要到了,大家是不是还在绞尽脑汁的想着,如何和另一半过一个浪漫的情人节呢?老套的剧情已经不能吸引人了,让我们看看VR全景给情人节带来了哪些不同的体验吧&a…...
Linux系统安全之iptables防火墙
目录 一.iptables防火墙基本介绍 二.iptables的四表五链 三.iptables的配置 1.iptables的安装 2.iptables防火墙的配置方法 四.添加、查看、删除规则 1.查看(fliter)表中的所有链 iptables -L 2.使用数字形式(fliter)表所有链 查看输出结果 iptables -nL 3.清空表中所…...
【C#基础】C# 变量与常量的使用
序号系列文章1【C#基础】C# 程序通用结构2【C#基础】C# 基础语法解析3【C#基础】C# 数据类型总结文章目录前言一. 变量(variable)1,变量定义及初始化2,变量的类别3,接收输出变量二. 常量(constantÿ…...
[ 常用工具篇 ] CobaltStrike(CS神器)基础(一) -- 安装及设置监听器详解
🍬 博主介绍 👨🎓 博主介绍:大家好,我是 _PowerShell ,很高兴认识大家~ ✨主攻领域:【渗透领域】【数据通信】 【通讯安全】 【web安全】【面试分析】 🎉点赞➕评论➕收藏 养成习…...
Redis集群
Redis集群 本章是基于CentOS7下的Redis集群教程,包括: 单机安装RedisRedis主从Redis分片集群 1.单机安装Redis 首先需要安装Redis所需要的依赖: yum install -y gcc tcl然后将课前资料提供的Redis安装包上传到虚拟机的任意目录ÿ…...
00---C++入门
1. C关键字(C98) C总计63个关键字,C语言32个关键字 2. 命名空间 在C/C中,变量、函数和后面要学到的类都是大量存在的,这些变量、函数和类的名称将都存在于全局作用域中,可能会导致很多冲突。使用命名空间的目的是对标识符的名称进…...
Spring-事务2
文章目录前言一、事务的特性(ACID)二、事务的隔离级别三、spring中的事务平台事务管理器.事务定义ISOLation_XXX:**事务隔离级别.**PROPAGATION_XXX:**事务的传播行为**.事务状态关系:四、使用XML文件配置事务1、 搭建…...
Windows Git Bash 配置
Windows Git Bash 配置 本文参考的文章: 在 Windows 的 Git Bash 中使用包管理器 - iris (ginshio.org)Git bash 安装 pacman & Windows 解压 zst 文件 | 伪斜杠青年 (lckiss.com) 一、Git的安装 Git 的安装应该是都会的,但还是应该说以下&#…...
java代码整合kettle9.3实现读取表中的数据,生成excel文件
java代码整合kettle9.3实现读取表中的数据,生成excel文件 1.简介 本次使用java代码整合kettle9.3版本,数据库使用mysql。 2.jar包导入 项目需要依赖部分kettle中的jar包,请将这部分jar包自行导入maven仓库。 <dependency><groupId…...
分享微信点餐小程序搭建步骤_微信点餐功能怎么做
线下餐饮实体店都开始摸索发展网上订餐服务。最多人选择的是入驻外卖平台,但抽成高,推广还要另买流量等问题,也让不少商家入不敷出。在这种情况下,建立自己的微信订餐小程序,做自己的私域流量是另一种捷径。那么&#…...
做暧暧视频网站免费/优化师是做什么的
▲点击上方 雷锋网 关注华为已经宣布方舟编译器会从 2019 年全面开源。文 | I/O 2019 年 4 月 11 日,在上海的华为新品发布会上,除了可以拍月亮的华为 P30 系列,余承东还亲自抛出了两项软件层面的“重磅炸弹”,分别是方舟编译器和…...
怎么做创意短视频网站/在哪里可以免费自学seo课程
目录1 训练集测试集的划分以及模型评估1.1 测试集是训练集的一部分1.2 训练集和测试集不相交2 评估指标2.1 回归准确率linear accuracy2.2 模型错误指标3 复合回归模型3.1 复合回归模型的例子3.2 复合回归预测连续值3.3 问答1 训练集测试集的划分以及模型评估 训练集和测试集的…...
免备案做网站 可以盈利吗/专业做app软件开发公司
因为每个人二分的风格不同,所以在学习二分的时候总是被他们的风格搞晕。有的人二分风格是左闭右开也就是[L,R),有的人是左开右闭的(L,R]。 二分的最基本条件是,二分的序列需要有单调性。 下面介绍的时候用v来代表我们二分的目标,用…...
大连做网站的网络公司/seo哪个软件好
一、安装Java SE JKD A、下载 http://www.oracle.com/technetwork/java/javase/downloads/index.html?ssSourceSiteIdocomen 选择最新版本,当前最新版本 1.8.0 。下载 jdk-8u45-linux-x64.tar.gz 。 B、解压下载包,添加环境变量。 $ sudo vim ~/.bash…...
公司网站是做的谷歌的/百度关键词搜索工具
这次我想分享一下车的话题,非专业汽车评测人员,完全个人喜好。 这次想分享的主题是关于车——十代思域,想必在我朋友圈的朋友已经看过我晒过我的小黑思了,直接上图: 简直帅爆了有木有,自从去年我拿到驾照之…...
中心网站建设/市场调研报告3000字范文
参考博文: sqlyog安装详细步骤...