目录
搜刮引擎项目配景
搜刮引擎的宏观原理
搜刮引擎技术栈和项目环境
搜刮引擎具体原理(正排索引和倒排索引)
正排索引
倒排索引
编写数据去标签与数据清洗的模块 Parser
从boost官网导入HTML网页数据
去标签
构建 Parser 模块
递归式获取 HTML 文件的带文件名称路径
对 HTML 文件内容举行剖析
依次读取 HTML 文件内容
剖析 HTML 文件的 title
剖析 HTML 文件的 content
剖析 HTML 文件的 url
对 HTML 文件剖析整体代码如下
将剖析之后的 HTML 文件内容拼接并写入对应的文本文件中
Parser 模块整体代码
编写建立索引的模块 Index
编写正排索引模块
按行读取文档
切分行数据并举行行数据对应的文档的正排索引结构体对象构建
编写倒排索引模块
通过文档 id 获取对应文档的正排索引对象
根据关键词获取关键词对应的倒排拉链
编写搜刮引擎模块 Searcher
创建 index 对象,构建索引
获取 content 的摘要
通过关键词举行查询,终极返回 json 串
项目中期测试
debug 当地测试
bug1
bug2
编辑
编写 http_sever 模块
cpp-httplib 测试代码
编写 http_sever 模块
编写前端模块
为项目添加日志信息
将项目摆设到 LINUX 服务器
项目展示
项目总结
搜刮引擎项目配景
搜刮引擎是一个大家众所周知的一个搜刮工具,常见的搜刮引擎有百度搜刮,搜狗搜刮,360搜刮等等,我们以百度搜刮为例。
百度搜刮的主页面如下。
我们可以在搜刮框中输入我们想要搜刮的内容,点击搜刮,就会出现如下界面。
点击搜刮之后跳转的主页会显现大量的相关关键字的网页信息。 我们对其中一个网页信息举行分析。
网页信息就包含了网页的标题,网页内容的摘要和网页资源对应的url。
我们自己可以实现这样一个大的搜刮引擎吗?对与个人而言,实现这样一个大的搜刮引擎,代价太大,将全网的数据整合就是一个巨大的困难,所以我们实现这样一个举行全网搜刮的搜刮引擎是明显不实际的,但是有不少的网页具有站内搜刮的引擎,我们以常见的boost库为例。图示如下。
boost库官网中就存在这样一个站内的搜刮引擎,可以搜刮boost库中的相关知识。
基于以上的配景,此项目旨在设计开发一款如boost库官网界面站内搜刮引擎的boost搜刮引擎,实现与之类似的站内搜刮功能。
搜刮引擎的宏观原理
那么像百度搜刮,360搜刮等等这些当今互联网上应用较为广泛的搜刮引擎,它们搜刮的宏观原理是什么呢?我们通过一个图示为大家大概的解说。
区别于之前学习的网络间通信,之前的client和sever指的都是进程,而上图中的client和sever指的其实不是进程,而对应的是客户端的主机和服务器端的主机。
在客户端主机和服务器端主机内部门别有客户端search进程和服务器端search进程,我们可以以为,险些所有的类似的软件交互,实际上都是客户端进程和服务器端进程的交互。
我们实现的boost搜刮引擎实际上只涉及了蓝方框内的宏观原理,相应的html我们不是通过爬虫获取的,而是直接在官网上下载下来的html文件。
搜刮引擎技术栈和项目环境
- 技术栈:C/C++,C++11,STL,准标准库Boost,Jsoncpp(数据互换),cppjieba(搜刮关键词的分词),cpp-httplib(构建http服务器),html5,css,jQuery,js,Ajax。
- 项目环境:centos7云服务器,vim,(gcc/g++),makefile,vscode。
搜刮引擎具体原理(正排索引和倒排索引)
在搜刮引擎中,我们在通过关键字举行查询时,每每会使用到倒排索引和正排索引。那么倒排索引和正排索引是什么呢?
正排索引
好比如今有两个文档,两个文档的文档id分别为1和2,两个文档的内容分别为雷布斯发布了小米手机和雷布斯发布了小米su7
文档id | 文档内容 | 1 | 雷布斯发布了小米手机 | 2 | 雷布斯发布了小米su7 | 所谓正排索引,很好理解,就是通过文档id查询文档内容。
倒排索引
倒排索引其实就是通过文档的内容和文档的关键字查询文档的文档id。
那么怎么样获取文档的关键字呢?此时我们就要对文档举行分词。
- 文档1分词(雷布斯发布了小米手机):雷布斯/发布/小米/手机/小米手机
- 文档2分词(雷布斯坐的小米su7):雷布斯/坐/小米/su7/小米su7
不难发现我们在举行分词的时间,将了/的这两个关键字给省略掉了,这是由于在搜刮引擎中,我们有了制止词的概念,制止词就是在多个文档中都会出现的共性词,如中文中的 的/了/是等等,英文中的 a/the 等等,如果将这些字作为了关键字,将来查询到的文档就非常多,可以理解为就是查询所有的文档,所以会降低查询的效率,所以我们在关键字拆分的时间,不将制止词作为关键字。
需要留意的是,多个文档的重复关键字我们终极只保存唯一的一份,也就意味着关键字也必须和文档id一样是唯一的。
关键字 | 文档id/权值 | 雷布斯 | 文档1/文档2 | 发布 | 文档1 | 小米 | 文档1/文档2 | 手机 | 文档1 | 小米手机 | 文档1 | 坐 | 文档2 | su7 | 文档2 | 小米su7 | 文档2 | 其实在大家使用关键字在百度等搜刮引擎上举行搜刮时,查找出来多个同种类型的多个去标签的网页内容会在一个页面先后展示,为什么会先后展示,这是由于,每个网页的权值是不一样的,权值高的会优先展示。所以我们也会为每个文档举行权值的设定,文档id就可以用来表示权值,这个之后我们会再次讲到。
所以搜刮引擎的具体查询原理就是sever端先用关键字举行倒排索引,查找到文档的id,然后再通过文档id查询到文档内容,再对查询到的文档内容举行去标签利用得到title,desc和url,终极对多个文档的tile,desc和url举行组合,然后通过文档的权值举行排序,终极将拼装好的页面返回给client端展示。
编写数据去标签与数据清洗的模块 Parser
从boost官网导入HTML网页数据
boost官网主界面如图所示。
我们的网页不是通过爬虫获取的,而是直接下载了boost官网中的对应的html网页。
下载之后使用 rz -E 指令将下载下来的含有 html 网页的 boost 文件导入我们自己创建的目录中。
使用 tar xzf 指令对对应的文件举行解包解压,解压之后的目录如图所示。
boost_1_87_0 目录中的文件就是我们在boost官网上看到的所有的内容。
在boost_searcher 下创建一个与 boost_1_87_0 同级的目录 data,在 data 内里创建一个input 目任命于存放 boost_1_87_0/doc/html 目录下的所有 html 文件和目录,类似于爬虫获取的大量 html 网页数据源。
可以看到此时 input 目录里约莫有 8761 个网页数据源。
去标签
何为标签?
- <td align="center"><a href="../../libs/libraries.htm">Libraries</a></td>
复制代码 上述html代码中,符号 <> 以及 符号 <> 内的内容组合起来,我们称之为一个标签。以 <> 为开始标签,</> 为结束标签。
何为去标签?
所谓去标签,其实就是不用去关心标签内的数据,只关心标签外的数据,好比上述标签我们只关心 Libraries。
在与 input 同级的目录下创建一个 raw_html 目录,目录里创建对应的文件用于存放每一个 html 文件去标签之后的数据在 raw_html 内的文件中存放,且每个 html 文件对应的去标签之后的数据应该以 '\3' 举行分隔,由于 '\3' 是不可显字符。
构建 Parser 模块
paser模块的构建主要分3步。
- 递归式的获取 input 目录里的所有 html 文件的带文件名称的路径名,并将每个 html 文件的带文件名称的路径名保存在一个vector容器中。
- 根据第一步获取的 html 文件的带文件名的路径,依次打开每个文件,依次读取每个文件的内容,对读取出来的内容举行剖析,将剖析每个 html 文件的 title,content和url,保存在一个vector容器中。
- 对第2步获取的每个文件的 title,content,url 内容举行拼接,写入 raw.txt 文件中 。
递归式获取 HTML 文件的带文件名称路径
递归式获取文件的带文件名称路径的方法,我们接纳的是 boost官网的 Filesystem Library 库,要使用该库,必须先安装 boost 库,安装指令如下。
- sudo yum install -y boost-devel
复制代码 如下图,我们安装的是 boost 库的1.53版本。
代码如下。
- bool EnumFile(const std::string& src_path,std::vector<std::string>*file_list)
- {
- namespace fs=boost::filesystem;
- fs::path root_path(src_path);
- //判断路径是否存在,如果不存在直接返回false
- if(!fs::exists(root_path))
- {
- std::cerr<<src_path<<"not exists"<<std::endl;
- return false;
- }
- //定义一个空迭代器,用来判断递归是否结束
- fs::recursive_directory_iterator end;
- for(fs::recursive_directory_iterator iter(root_path);iter!=end;iter++)
- {
- //判断当前路径是否对应的是普通文件,目录就不是普通文件
- if(!fs::is_regular_file(*iter))
- {
- continue;
- }
- //是普通文件,判断是否是html文件
- if(iter->path().extension()!=".html")
- {
- continue;
- }
- //当前路径对应的是html文件,将该路径保存到file_list中
- file_list->push_back(iter->path().string());
- }
- return true;
- }
复制代码 对 HTML 文件内容举行剖析
对 html 文件内容分析主要分为四步。
- 从 file_list 中依次读取每个 html 文件的的内容。
- 从 html 文件内容中剖析文件 title。
- 从 html 文件内容中剖析文件 content。
- 从 html 文件内容中剖析文件 url,终极将剖析的 title,content,url全部保存进一个 DocInfo 结构体对象中,终极保存进 vector 中。
基于此我们要先创建一个DocInfo结构体,用于保存每个 html 文件内容的title,content,url。
- //创建一个结构体,用于保存,一个html网页文件分析之后的 title,content,url
- typedef struct DocInfo{
- std::string title; //网页文件的标题
- std::string content; //网页文件的内容
- std::string url; //网页文件的url
- }DocInfo_t;
复制代码 依次读取 HTML 文件内容
- static bool ReadFile(const std::string & file_path,std::string *results)
- {
- std::ifstream in(file_path,std::ios::in);
- if(!in.is_open())
- {
- std::cerr<<"open file "<<file_path<<" error "<<std::endl;
- return false;
- }
- //文件打开成功,读取文件
- std::string line;
- while(std::getline(in,line))
- {
- *results+=line;
- }
- in.close();
- return true;
- }
复制代码 剖析 HTML 文件的 title
- bool ParseTitle(const std::string& result,std::string * title)
- {
- std::size_t begin=result.find("<title>");
- if(begin==std::string::npos)
- {
- return false;
- }
- std::size_t end=result.find("</title>");
- //截取字符串,但是截取的字符串应该是左闭右开区间
- begin+=std::string("<title>").size();
- if(begin>end)
- {
- return false;
- }
- *title= result.substr(begin,end-begin);
- return true;
- }
复制代码 剖析 HTML 文件的 content
- bool ParseContent(const std::string& result,std::string* content)
- {
- //去标签,基于一个简单的状态机
- enum status{
- LABLE,//表示标签
- CONTENT//表示正文内容
- };
-
- enum status s=LABLE;
- for(char c:result)
- {
- switch(s)
- {
- case LABLE:
- if(c=='>') s=CONTENT;
- break;
- case CONTENT:
- if(c=='<') s=LABLE;
- else{
- //去掉content中的'\n' 最终我们要使用'\n'作为每个html文件的解析之后的数据之间的分隔符
- if(c=='\n') c=' ';
- content->push_back(c);
- }
- break;
- default:
- break;
- }
- }
- return true;
复制代码 剖析 HTML 文件的 url
- bool ParseUrl(const std::string& filepath,std::string* url)
- {
- std::string url_head="https://www.boost.org/doc/libs/1_87_0/doc/html";
- std::string url_tail=filepath.substr(src_path.size());
- *url=url_head+url_tail;
- return true;
- }
复制代码 对 HTML 文件剖析整体代码如下
- bool ParseHtml(const std::vector<std::string>&file_list,std::vector<DocInfo_t>* results)
- {
- for(const std::string& filepath:file_list)
- {
- //1.依次读取file_list中的文件到result中
- std::string result;
- if(!ns_util::FileUtil::ReadFile(filepath,&result))
- {
- continue;
- }
- DocInfo_t doc;
- //2.读取文件成功,开始进文件内容分析,提取文件title
- if(!ParseTitle(result,&doc.title))
- {
- continue;
- }
- //3.提取title成功,提取content
- if(!ParseContent(result,&doc.content))
- {
- continue;
- }
- //4.提取content成功,提取url
- if(!ParseUrl(filepath,&doc.url))
- {
- continue;
- }
- //将doc转为右值之后插入,可以减少拷贝
- results->push_back(std::move(doc));
-
- // ShowDoc(doc);
- // break;
- }
- return true;
- }
复制代码 将剖析之后的 HTML 文件内容拼接并写入对应的文本文件中
- bool SaveHtml(const std::vector<DocInfo_t>& results,const std::string& output)
- {
- #define SEP '\3'
- //按照二进制方式写入
- std::ofstream out(output,std::ios::out | std::ios::binary);
- if(!out.is_open())
- {
- std::cerr<<"open"<<output<<"failed!"<<std::endl;
- return false;
- }
- //打开了文件,开始进行数据的写入
- for(auto &item:results)
- {
- std::string out_string;
- out_string += item.title;
- out_string += SEP;
- out_string +=item.content;
- out_string+=SEP;
- out_string+=item.url;
- out_string += '\n';
- out.write(out_string.c_str(),out_string.size());
- }
- return true;
- }
复制代码 我们以 '\3' 区分每个html文件的title,content,url。以 '\n' 区分每个文件的剖析之后的内容。
Parser 模块整体代码
- int main()
- {
- //1:递归式的获取input目录下的所有html网页文件的文件名称和路径,保存到file_list中
- std::vector< std::string> file_list;
- if(!EnumFile(src_path,&file_list))
- {
- std::cerr<<"enum file name error!"<<std::endl;
- return 1;
- }
- //2.读取file_list中对应的文件的内容,进行分析,将分析之后的数据放入 results中
- std::vector<DocInfo_t> results;
- if(!ParseHtml(file_list,&results))
- {
- std::cerr<<"parse html error!"<<std::endl;
- return 2;
- }
- //3.将results中保存的每个html页面文件分析之后的数据写入raw.txt中
- if(!SaveHtml(results,output))
- {
- std::cerr<<"save html error!"<<std::endl;
- return 3;
- }
-
- return 0;
- }
复制代码 编写建立索引的模块 Index
编写 index 主要分为两步。
- 编写正排索引模块,即文档 id 和文档内容的关系。
- 编写倒排索引模块,即关键词和文档 id 的关系。
在此之前,我们已经将所有的 html 文件举行了剖析,将剖析之后所有 html 文件的title,content,url全部保存在了 raw.txt 文本文件中,并且,每个文件之间的剖析之后的数据以 '\n'作为分隔符,所以哦将来可以使用使用 getline 一次获取 raw.txt 的一行数据,由于一行的数据刚好是一个文档剖析之后的数据,所以我们可以以这行数据建立该行数据所对应的文档的正排索引结构体和倒排索引结构体。
正排索引结构体如下。
- struct DocInfo{
- std::string title; //文档的标题
- std::string content; //文档的去标签之后的内容
- std::string url; //文档的url
- uint64_t doc_id; //文档的id
- };
复制代码 正直索引结构体是对于文档而言的,表示当前文档对应的title,content,url和doc_id(文档id),一个 html 文档对应一个正排索引结构体对象。由于文档 id 和 html 文档是一一对应的关系。
倒排索结构体如下。
- //倒排索引结构体,一个关键词对应多个倒排索引结构体
- struct InvertedElem{
- uint64_t doc_id;
- std::string word;
- int weight;
- InvertedElem():weight(0){}
- };
复制代码 倒排索引结构体是对于关键词而言的,一个关键词大概对应多个倒排索引结构体对象。由于一个关键词大概出如今多个文档中。当关键词存在多个倒排索引结构体对象时,该关键词对应的一个倒排索引对象就是该关键词在一个文档中的关联关系,而该关键词会与多个文档产生关联关系。
正排索引为一个vector容器,该容器的每个元素为一个正排索引结构体对象。
- //正排索引的数组容器,用于存放,每个文档对应的正派索引结构体,数组的下标为结构体中的文档id
- std::vector<DocInfo> forward_index;
复制代码 倒排索引为一个unordered_map容器,该容器的每个元素的 first 对应一个关键词,每个元素的 second 表示该元素对应的倒排拉链,保存 first 对应的关键词的所有倒排索引的结构体对象。
- //倒排索引的容器,用于存放每个关键词,以及这个关键词对应的倒排拉链,倒排拉链一个数组,数组的每个元素是一个 InvertedElem 关键词对应的结构体对象
- //一个关键词在一个文档中出现就有一个对应结构体对象,在多个文档中出现就有多个对应的结构体对像,所以倒排拉链中可能有一个结构体对象也可能有多个
- //结构体对象
- std::unordered_map<std::string,InvertedList> inverted_index;
复制代码 编写正排索引模块
正排索引的编码主要分为两步。
- 对从 raw.txt 中读取的一行数据,由于我们之前已经将每个 html 文件剖析之后的内容通过 '\n' 举行分隔,所以从 raw.txt 中读取的一行数据就是一个文档剖析之后的数据。
- 对读取的一行数据举行切分,得到这行数据对应的文档的title,content,url。创建一个正排索引结构体对象,将切分之后获取的 title,content,url 分别设置进这个正直索引结构体对象中,正直索引的 doc_id 成员我们用正排索引的vector下标表示,可以通过建立的正排索引的vector容器的size举行设设置。
按行读取文档
- //根据input路径打开路径下的raw.txt文件
- std::ifstream in(input,std::ios::in|std::ios::binary);
- if(!in.is_open())
- {
- std::cerr<<"open"<<input<<"failed"<<std::endl;
- return false;
- }
- //打开文件成功,开始进行文件的读取
- std::string line;
- while(std::getline(in,line))
- {
- //因为raw.txt文档中,一行就对应一个html文件解析之后的数据,所以读取完一行之后,就给这一行对应的html文件建立正排索引和倒排索引
- DocInfo * doc=BuildForwardIndex(line);
- if(nullptr==doc)
- {
- std::cerr<<"build"<<line<<"failed"<<std::endl;
- continue;
- }
复制代码 切分行数据并举行行数据对应的文档的正排索引结构体对象构建
在对读取的行数据举行切分时,我们使用boost库中的split函数举行切分。
- //建立正排索引
- DocInfo* BuildForwardIndex(const std::string &line)
- {
- //1.对line中的数据进行切分,获取到对应html文档的title,content,url
- //使用boost库中的split函数进行切分
- std::vector<std::string> results;
- const std::string sep="\3";
- ns_util::StringUtil::Split(line,&results,sep);
- if(results.size()!=3)
- {
- return nullptr;
- }
- //2.进行字符串填充
- DocInfo doc;
- doc.title=results[0];
- doc.content=results[1];
- doc.url=results[2];
- doc.doc_id=forward_index.size();
- //3.插入到正派索引对应的vector中
- forward_index.push_back(std::move(doc));
- return &forward_index.back();
- }
复制代码 编写倒排索引模块
倒排索引主要分为两步。
- 根据创建的正排索引结构体对象的title和content举行分词,分词之后,通过一个unordered_map 对象依次统计关键词在title和content中出现的次数。
- 根据 unordered_map 对象中统计出的关键词在title和content中出现的次数,建立关键词的倒排索引结构体对象,并将该结构体对象插入关键词对应的倒排拉链中。
-
- bool BuildInvertedIndex(const DocInfo &doc)
- {
- //1.创建一个结构体,用于表示关键词在title和content中出现的次数
- struct word_cnt
- {
- int title_cnt;
- int content_cnt;
- word_cnt():title_cnt(0),content_cnt(0){}
- };
-
- //2.创建一个map容器,key用于保存关键词,value用于保存关键词在一个文档的title和content中分别出现的次数
- std::unordered_map<std::string,word_cnt>word_map;
-
- //3.对title进行分词,vector中用于存储title分词之后的多个关键词
- std::vector<std::string>title_words;
- //4.对title进行分词
- ns_util::JiebaUtil::CutString(doc.title,&title_words);
- //5.统计title中对应关键词出现的次数,因为将来在进行关键词搜索时,不区分大小写,所以我们在分词之后,必须对大小写进行统一
- for(auto title_word:title_words)
- {
- boost::to_lower(title_word);
- word_map[title_word].title_cnt++;
- }
- //6.对content进行分词,vector中用于存储content分词之后的多个关键词
- std::vector<std::string>content_words;
- ns_util::JiebaUtil::CutString(doc.content,&content_words);
- //7.统计content中对应关键词出现的次数
- for(auto content_word:content_words)
- {
- boost::to_lower(content_word);
- word_map[content_word].content_cnt++;
- }
- //8.构建关键词对应的倒排索引结构体
- #define X 10
- #define Y 1
- for(auto &word:word_map){
- InvertedElem em;
- em.doc_id=doc.doc_id;
- em.word=word.first;
- em.weight=word.second.title_cnt*X+word.second.content_cnt*Y;
- //9.将关键词对应的倒排索引的结构体插入关键词对应的倒排拉链中
- inverted_index[word.first].push_back(std::move(em));
- }
- return true;
- }
复制代码 对 doc 中的 title 和 content 举行分词时,我们使用cppjieba库举行分词。分词代码如下。
- const char* const DICT_PATH = "./dict/jieba.dict.utf8";
- const char* const HMM_PATH = "./dict/hmm_model.utf8";
- const char* const USER_DICT_PATH = "./dict/user.dict.utf8";
- const char* const IDF_PATH = "./dict/idf.utf8";
- const char* const STOP_WORD_PATH = "./dict/stop_words.utf8";
- class JiebaUtil
- {
- private:
- static cppjieba::Jieba jieba;
- public:
- static void CutString(const std::string & src,std::vector<std::string>*out)
- {
- jieba.CutForSearch(src,*out);
-
- }
-
-
- };
- cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
复制代码 通过文档 id 获取对应文档的正排索引对象
- //根据doc_id找到文档对应的DocInfo对象
- DocInfo* GetForwardIndex(uint64_t doc_id)
- {
- if(doc_id>=forward_index.size())
- {
- std::cerr<<"doc_id out range,error!"<<std::endl;
- return nullptr;
- }
- return &forward_index[doc_id];
- }
复制代码 根据关键词获取关键词对应的倒排拉链
- //根据关键词string,找到关键词对应的倒排拉链
- InvertedList* GetInvertedList(const string& word)
- {
- auto iter= inverted_index.find(word);
- if(iter==inverted_index.end())
- {
- std::cerr<<word<<"have no InvertedList"<<std::endl;
- return nullptr;
- }
- return &(iter->second);
- }
复制代码 编写搜刮引擎模块 Searcher
当我们在搜刮引擎的搜刮框中输入关键词之后举行查询时,返回的网页中一定是含有当前的关键词吗?我们以百度搜刮引擎为例。
不难发现,搜刮出来的网页中既含有我们搜刮框中的关键词,也含有搜刮框中关键词的一部门。所以也就说明,当我们在使用关键词搜刮时,要先对关键词举行分词,分词之后形成的多个关键词才是我们终极在倒排索引中查找的关键词。
Searcher 模块的编写主要分为三步。
- 创建 index 对象,并举行索引的构建。
- 获取文档 content 的摘要 desc。
- 通过关键词在服务器中举行倒排索引查找,然后通过倒排索引举行正排索引,找到关键词对应的文档的 title,content 和 url,并将这三个内容转为 json 串返回到欣赏器。
创建 index 对象,构建索引
- //1.初始化Searcher模块,并通过 raw.txt 构建索引。
- void InitSearcher(const std::string &input)
- {
- index=ns_index::Index::GetInstance();
- std::cout<<"获取index单例对象成功"<<std::endl;
- index->BuildIndex(input);
- std::cout<<"建立正排索引和倒排索引成功"<<std::endl;
- }
复制代码 获取 content 的摘要
此时我们要留意一个点,就是我们终极在欣赏器上表现对应的 html 模块时,表现的是文档标题 title,文档内容形貌 desc,文档的 url。 所以此时我们要获取的不是 文档的 content,而是文档的 content 举行处理之后的 形貌 desc。所以此时我们就要使用 GetDesc 函数,获取 content 的 desc。
GetDesc 的实现逻辑就是,在文档 content 中查找关键词 word 的位置,如果 word 可以通过倒排索引和正排索引获取到一个文档,那么这个文档的 content 中一定是含有关键词 word 的。由于生成关键词的步骤,就是对文档的 title 先举行分词,然后对文档 content 分词之后获得的关键词,而且 content 中是包含 title 的内容的,所以可以说 html 文档所有的关键词 word 产生于文档的 content 中。
- std::string GetDesc(const std::string& content,const std::string& word)
- {
- //在DocInfo对象中找到InvertedElem中word首次出现的位置
- std::size_t pos= content.find(word);
- if(pos==std::string::npos)
- {
- return "none1";
- }
- std::size_t begin=0;
- std::size_t end=content.size()-1;
- std::size_t step_front =50;
- std::size_t step_back=100;
- if(pos-step_front>begin) begin=pos-step_front;
- if(pos+step_back<end) end=pos+step_back;
- if(begin<end)
- {
- return content.substr(begin,end-begin);
- }
- return "none2";
- }
复制代码 通过关键词举行查询,终极返回 json 串
- //2.通过搜索关键词进行查询,最终返回json串。
- void Search(const std::string& query_words,std::string*json_string)
- {
- //1.对搜索关键词进行分词
- std::vector<std::string> words;
- ns_util::JiebaUtil::CutString(query_words,&words);
- //list用于存储所有关键词对应的倒排拉链中的所有的倒排索引结构体对象
- std::vector<ns_index::InvertedElem> list;
- //2.对分词之后的关键词,查询各个关键词的倒排拉链
- for( std::string word:words)
- {
- //对查询的关键词分词之后的关键词在进行查询时也是不区分大小写的
- boost::to_lower(word);
- std::vector<ns_index::InvertedElem>* lt= index->GetInvertedList(word);
- if(nullptr==lt)
- {
- std::cout<<"get inverted list error"<<std::endl;
- continue;
- }
- //成功获取到了关键词对应的倒排拉链
- //将获取的倒排拉链的每个元素插入到list中
- list.insert(list.end(),lt->begin(),lt->end());
-
- //对list中的倒排索引结构体对象进行排序,以weight大小的方式进行降序排序
- std::sort(list.begin(),list.end(),[](const ns_index::InvertedElem& em1,const ns_index::InvertedElem& em2){ return em1.weight>em2.weight; });
-
- //依次根据排序之后的倒排索引对象获取该对象内部的id对应的正排索引对象
- Json::Value root;
- for(auto& em:list)
- {
- ns_index::DocInfo* doc = index->GetForwardIndex(em.doc_id);
- //获取到对应的正派索引结构体对象之后,将对象中的 title,content,url 转成json串并返回
- if(nullptr==doc)
- {
- std::cerr<<"get forward index error"<<std::endl;
- continue;
- }
- Json::Value elem;
- elem["title"]=doc->title;
- elem["desc"]=GetDesc(doc->content,em.word);
- elem["url"]=doc->url;
- root.append(elem);
- }
- //使用writer对象对root对象中的结构化数据进行序列化
- Json::StyledWriter writer;
- *json_string=writer.write(root);
- }
- }
复制代码 项目中期测试
debug 当地测试
- #include"searcher.hpp"
- const std::string input="./data/raw_html/raw.txt";
- int main()
- {
- ns_searcher::Searcher searcher;
- searcher.InitSearcher(input);
- std::string query;
- std::string json_string;
- while(true)
- {
- std::cout<<"#请输入关键词"<<std::endl;
- std::cin>>query;
- searcher.Search(query,&json_string);
- std::cout<<json_string<<std::endl;
- }
-
- }
复制代码 bug1
在通过 filesystem 关键词举行查找时,我们发现 filesystem 关键词对应的文档的 content 的 desc 字段酿成了 none 1。
这就意味着,我们在构建 content 的 desc 时,没有在 content 中找到我们当前查询的关键词信息。
但是我们在官方文档下举行查找时,我们在对应文档中查找到了对应的关键词呀,但是为什么在运行结果中,没有在对应的文档中找到关键词呢?
颠末多次排查终极发现,这是由于 string 的 find 函数其实本质上是区分巨细写的,我们的关键词在举行分词之后,是全部转成了小写的,所以在使用 find 函数中使用小写关键词肯定是不能找到大写的关键词的,就导致了终极的 content 的 desc 形貌成为了 none1。所以我们不能使用 string 类的 find 函数举行查找,可以使用 C++ 库中的 search 函数举行查找。
对 GetDesc 函数举行第一次调解。
- std::string GetDesc(const std::string& content,const std::string& word)
- {
- //在DocInfo对象的content中找到InvertedElem中word首次出现的位置
-
- auto iter= std::search(content.begin(),content.end(),word.begin(),word.end(),[](const char &x,const char &y){return std::tolower(x)==std::tolower(y);});
- if(iter==content.end())
- {
- return "none1";
- }
- std::size_t pos =iter-content.begin();
- std::size_t begin=0;
- std::size_t end=content.size()-1;
- std::size_t step_front =50;
- std::size_t step_back=100;
- if(pos-step_front>begin) begin=pos-step_front;
- if(pos+step_back<end) end=pos+step_back;
- if(begin<end)
- {
- return content.substr(begin,end-begin);
- }
- return "none2";
- }
复制代码 调解后,再次查看对应文档的 content 的 desc 形貌。
不难发现,此时 desc 字段已经具有了数据,此 bug 修复乐成。
bug2
在通过关键词 split 举行查找时,我们发现 split 对应的文档的 content 的 desc 字段出现了 none2 。
这是为什么呢?
颠末多次排查终极发现这是由于在 GetDesc 函数中,我们使用了 size_t 类型的变量举行比较运算,但是在比较的语句中,会有对应的减运算,两个 size_t 类型的变量举行相减利用,终极就会酿成一个负数,但是负数对于 size_t 类型而言其实就对应了一个很大的整数,所以在原来条件不满足的时间,条件满足了,终极导致了 begin 和 end 位置的变化,终极就有大概导致 begin 的值比 end 的值大,所以终极就会导致 content 的 desc 形貌为 none2 的情况出现。
对 GetDesc 函数举行第二次调解。
- std::string GetDesc(const std::string& content,const std::string& word)
- {
- //在DocInfo对象的content中找到InvertedElem中word首次出现的位置
-
- auto iter= std::search(content.begin(),content.end(),word.begin(),word.end(),[](const char &x,const char &y){return std::tolower(x)==std::tolower(y);});
- if(iter==content.end())
- {
- return "none1";
- }
- int pos =iter-content.begin();
- int begin=0;
- int end=content.size()-1;
- int step_front =50;
- int step_back=100;
- if(pos-step_front>begin) begin=pos-step_front;
- if(pos+step_back<end) end=pos+step_back;
- if(begin<end)
- {
- return content.substr(begin,end-begin);
- }
- return "none2";
- }
复制代码 调解之后,再次查看 对应文档的 content 的 desc 形貌。
此 bug 修复乐成。
检验查询出来的 html 文档是否是按照关键词的权值举行表现的。
- elem["id"]=(int)doc->doc_id;
- elem["weight"]=em.weight;
复制代码
不难发现,关键词对应的文档终极确实是由权值举行降序排序的。
编写 http_sever 模块
http_sever 本质上就是一个 sever 服务器网络服务,即一个网络进程,可以让其他客户端进程跨网络访问。如果我们使用之前学习的 socket 编程代码自己实现一个 sever 服务器也不是不可以,但是代价太大,我们选择使用 现成的第三方库 cpp-httplib 库(推荐下载v.0.7.15),下载压缩包,然后使用 rz 指令上传至项目目录下,使用 unzip 指令压缩即可获得 cpp-httplib 目录,我们主要使用 cpp-httplib 目录下的 httplib.h 头文件。
同时,在下载好 cpp-httplib 库之后,应该使用较新的 gcc 编译器,centos7下默以为 gcc 4.8.5 版本,大家可以自行查找升级,同时升级之后,使用对应的指令启动最新的 gcc 编译器。
cpp-httplib 测试代码
- #include"cpp-httplib/httplib.h"
- const std::string root_path="./wwwroot";
- int main()
- {
- httplib::Server s;
- s.set_base_dir(root_path.c_str());
- s.Get("/s",[](const httplib::Request& req,httplib::Response& res)
- {
-
- res.set_content("hello yjd","text/plain;charset=utf8");
- });
- s.listen("0.0.0.0",8081);
- return 0;
- }
复制代码 其中 root_path 为 sever 服务器返回的网络主页面所对应的目录。
运行 http_sever,使用 client 欣赏器客户端连接,访问服务器主页面。
加载对应的资源界面。
编写 http_sever 模块
- #include"cpp-httplib/httplib.h"
- #include"searcher.hpp"
-
- const std::string input="./data/raw_html/raw.txt";
-
- const std::string root_path="./wwwroot";
-
- int main()
- {
- ns_searcher::Searcher searcher;
- searcher.InitSearcher(input);
-
- httplib::Server s;
- s.set_base_dir(root_path.c_str());
- s.Get("/s",[&searcher](const httplib::Request& req,httplib::Response& res)
- {
- if(!req.has_param("word")){
- res.set_content(" 必须要有关键字 ","text/plain; charset=utf-8");
- return;
- }
- std::string word=req.get_param_value("word");
- std::cout<<"用户搜索的关键词为: "<<word<<std::endl;
- std::string json_string;
- searcher.Search(word,&json_string);
- res.set_content(json_string,"application/json");
- });
- s.listen("0.0.0.0",8081);
- return 0;
- }
复制代码 在搜刮框中通过给 word 字段传入关键词,后端获取到关键词请求之后,在后端举行查询,将查询到的 序列化 json 串返回到欣赏器客户端。
编写前端模块
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8">
- <meta http-equiv="X-UA-Compatible" content="IE=edge">
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
- <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
- <title>boost 搜索引擎</title>
- <style>
- /* 去掉网页中的所有的默认内外边距,html的盒子模型 */
- * {
- /* 设置外边距 */
- margin: 0;
- /* 设置内边距 */
- padding: 0;
- }
- /* 将我们的body内的内容100%和html的呈现吻合 */
- html,
- body {
- height: 100%;
- }
- /* 类选择器.container */
- .container {
- /* 设置div的宽度 */
- width: 800px;
- /* 通过设置外边距达到居中对齐的目的 */
- margin: 0px auto;
- /* 设置外边距的上边距,保持元素和网页的上部距离 */
- margin-top: 15px;
- }
- /* 复合选择器,选中container 下的 search */
- .container .search {
- /* 宽度与父标签保持一致 */
- width: 100%;
- /* 高度设置为52px */
- height: 52px;
- }
- /* 先选中input标签, 直接设置标签的属性,先要选中, input:标签选择器*/
- /* input在进行高度设置的时候,没有考虑边框的问题 */
- .container .search input {
- /* 设置left浮动 */
- float: left;
- width: 600px;
- height: 50px;
- /* 设置边框属性:边框的宽度,样式,颜色 */
- border: 1px solid black;
- /* 去掉input输入框的有边框 */
- border-right: none;
- /* 设置内边距,默认文字不要和左侧边框紧挨着 */
- padding-left: 10px;
- /* 设置input内部的字体的颜色和样式 */
- color: #6f6e6e;
- font-size: 18px;
- }
- /* 先选中button标签, 直接设置标签的属性,先要选中, button:标签选择器*/
- .container .search button {
- /* 设置left浮动 */
- float: left;
- width: 150px;
- height: 52px;
- /* 设置button的背景颜色,#4e6ef2 */
- background-color: #4e6ef2;
- /* 设置button中的字体颜色 */
- color: #FFF;
- /* 设置字体的大小 */
- font-size: 19px;
- font-family:Georgia, 'Times New Roman', Times, serif;
- }
- .container .result {
- width: 100%;
- }
- .container .result .item {
- margin-top: 15px;
- }
- .container .result .item a {
- /* 设置为块级元素,单独占一行 */
- display: block;
- /* a标签的下划线去掉 */
- text-decoration: none;
- /* 设置a标签中的文字的字体大小 */
- font-size: 20px;
- /* 设置字体的颜色 */
- color: #4e6ef2;
- }
- .container .result .item a:hover {
- text-decoration: underline;
- }
- .container .result .item p {
- margin-top: 5px;
- font-size: 16px;
- font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;
- }
- .container .result .item i{
- /* 设置为块级元素,单独占一行 */
- display: block;
- /* 取消斜体风格 */
- font-style: normal;
- font-size: 14px;
- color: green;
- }
- </style>
- </head>
- <body>
- <div class="container">
- <div class="search">
- <input type="text" value="请输入搜索关键字">
- <button onclick="Search()">搜索一下</button>
- </div>
- <div class="result">
- <!-- 动态生成网页内容 -->
- <!-- <div class="item">
- <a href="#">这是标题</a>
- <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p>
- <i>https://www.baidu.com</i>
- </div> -->
- </div>
- </div>
- <script>
- function Search(){
- // 是浏览器的一个弹出框
- // alert("hello js!");
- // 1. 提取数据, $可以理解成就是JQuery的别称
- let query = $(".container .search input").val();
- console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据
- //2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的
- $.ajax({
- type: "GET",
- url: "/s?word=" + query,
- success: function(data){
- console.log(data);
- BuildHtml(data);
- }
- });
- }
- function BuildHtml(data){
- // 获取html中的result标签
- let result_lable = $(".container .result");
- // 清空历史搜索结果
- result_lable.empty();
- for( let elem of data){
- // console.log(elem.title);
- // console.log(elem.url);
- let a_lable = $("<a>", {
- text: elem.title,
- href: elem.url,
- // 跳转到新的页面
- target: "_blank"
- });
- let p_lable = $("<p>", {
- text: elem.desc
- });
- let i_lable = $("<i>", {
- text: elem.url
- });
- let div_lable = $("<div>", {
- class: "item"
- });
- a_lable.appendTo(div_lable);
- p_lable.appendTo(div_lable);
- i_lable.appendTo(div_lable);
- div_lable.appendTo(result_lable);
- }
- }
- </script>
- </body>
- </html>
复制代码 前端页面以 html+css 技术为根本,使用传统的 javascript 技术举行前后端数据交互太过繁琐,所以我们会使用第三方库 jquery库。通过 jquery 库中的 ajax 函数向后端服务器发送 http 请求,并获取后端服务器返回的响应,获取到响应之后,调用回调函数,终极由 jquery 动态构建前端页面。
为项目添加日志信息
在没有添加日期之前,我们是以标准输出和标准错误的形式去反映代码的实行结果。有了日志信息之后,可以更进一步详细的知道代码的实行情况,以及代码实行到了那边,在哪里出现了错误,迅速举行错误的定位。
添加日志代码如下。
- #pragma once
- #include<iostream>
- #include<string>
- #include<ctime>
- #define NORMAL 1
- #define WARNING 2
- #define DEBUG 3
- #define FATAL 4
- #define LOG(LEVEL,MESSAGE) log(#LEVEL ,MESSAGE,__FILE__,__LINE__)
- void log(std::string level,std::string message,std::string file,int line)
- {
- std::cout<<"["<<level<<"]"<<"["<<time(nullptr)<<"]"<<"["<<message<<"]"<<"["<<file<<"]"<<"["<<line<<"]"<<std::endl;
- }
复制代码 将项目摆设到 LINUX 服务器
将项目摆设到 linux 服务器,其实就是让我们当前的服务器端进程在服务器的后台运行,使用的指令如下。
- nohup ./http_server > log/log.txt 2>&1 &
复制代码 nohup 可以将进程输出的日志信息保存在一个自动生成的 nohub.out 文件中,这里将 nuhub 指令将进程输出的日志信息全部重定向输出到了 log/log.txt 中。
此时其实我们关闭了 Xshell ,在欣赏器端仍然可以访问 http_server 服务。
项目展示
前端展示:
后端展示:
项目总结
以上便是boost搜刮引擎项目标所有内容。
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。 |