【实战项目】Boost搜索引擎项目

目录

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.ccg++ -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.解析指定的文件,提取titleif(!ParseTitle(result,&doc.title)){continue;}//3. 解析指定的文件,提取content,就是去标签if(!ParseContent(result,&doc.content)){continue;}//4. 解析指定的文件路径,构建urlif(!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.解析指定的文件,提取titleif(!ParseTitle(result,&doc.title)){continue;}//3. 解析指定的文件,提取content,就是去标签if(!ParseContent(result,&doc.content)){continue;}//4. 解析指定的文件路径,构建urlif(!ParseUrl(file,&doc.url)){continue;}//5. 完成了解析任务,当前文档的相关结果都保存在了doc里面results->push_back(doc);//bug,push_back本质会发生拷贝,效率可能会比较低//for debugShowDoc(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.解析指定的文件,提取titleif(!ParseTitle(result,&doc.title)){continue;}//3. 解析指定的文件,提取content,就是去标签if(!ParseContent(result,&doc.content)){continue;}//4. 解析指定的文件路径,构建urlif(!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;//文档idint weight;//权重    };//倒排拉链typedef std::vector<InvertedElem> InvertedList;class Index{private://正排索引的数据结构用数组,数组的下标天然是文档的IDstd::vector<DocInfo> forward_index;//正排索引//倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]std::unordered_map<std::string,InvertedList> inverted_index;//关键字与文档id = 1:npublic: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.txtbool 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.txtbool 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, urlstd::vector<std::string> results; const std::string sep = "\3";ns_util::StringUtil::Split(line, &results, sep);if(results.size() != 3){return nullptr;}//2. 字符串进行填充到DocIinfoDocInfo doc;doc.title = results[0];doc.content = results[1];doc.url = results[2];doc.doc_id = forward_index.size();//先进行保存id,在插入,对应的id就是当前doc在vector中的下标!//3. 插入到正排索引的vectorforward_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:分隔符//这个方法需要被外部直接使用,所以要加staticstatic 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,HELLOfor(auto &word_pair:word_map){//一个关键词对应的其中一个文档id+weight信息InvertedElem item;item.doc_id = doc.doc_id;item.word = word_pair.first;item.weight = X*word_pair.second.title_cnt + Y*word_pair.second.content_cnt;//相关性//<字符串,数组>,,,,插入数组元素InvertedList &inverted_list = inverted_index[word_pair.first];//获取关键词为word_pair.first对应的倒排拉链的引用,没有的话新建inverted_list.push_back(std::move(item));//增加word_pair.first对应的倒排拉链的数量}return true;}};
} 

说明:在进行搜索时,我们并不需要关注关键词的大小写,因此,在统计关键词时,我们习惯上将所有的分词统一转换为小写(使用 boost::to_lower )。这意味着,在我们的倒排索引中,用于查找的关键词(即倒排表的左侧)都是小写形式。当用户在索引中进行搜索时,我们也会自动将输入的搜索词转换为小写进行检索。这样做可以确保搜索的准确性和一致性,无论用户输入的是大写、小写还是混合大小写。

以上就是我们建立索引部分的过程。

索引就是一个存储 + 查找的数据结构(其实就是填充两张表)

我们思考一下索引类,我们需要建立多个索引对象吗?

  • 我们实际上并不需要创建多个索引对象来完成查找任务。一个索引对象足以应对所有的搜索需求,因为它包含了所有必要的正排和倒排表,从而支持对网页信息的快速检索。
  • 创建一个索引对象的成本是相当高的。这涉及到对大量网页信息的分词、统计、填充和插入等操作,这些步骤在效率和资源消耗上都是不小的挑战。

为了优化性能和资源利用,我们将索引对象设计为单例模式。这意味着在整个应用程序运行期间,索引对象只会被创建一次,并在需要时被重复使用。这样不仅减少了创建和销毁索引对象的开销,还确保了数据的一致性和完整性。通过采用单例模式,我们能够有效地管理索引对象,提高搜索效率,同时减少资源消耗。

6.4 将索引类设计成为单例模式

6.4.1 私有化构造函数和delete拷贝构造

6.4.2 编写单例接口

注意:这里使用锁要#include <mutex> 

类外初始化静态成员

7. 编写搜索引擎模块 Searcher 

7.1 基本代码结构

#pragma once#include "index.hpp"
#include "util.hpp"namespace ns_searcher
{class Searcher{private:ns_index::Index *index; // 供系统进行查找的索引public:Searcher() {}~Searcher() {}public:void InitSearcher(const std::string &input){// 1. 获取或者创建index对象// 2. 根据index对象建立索引}// query: 搜索关键字// json_string: 返回给用户浏览器的搜索结果void Search(const std::string &query, std::string *json_string){// 1.[分词]:对我们的query进行按照searcher的要求进行分词// 2.[触发]:就是根据分词的各个"词",进行index查找// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序// 4.[构建]:根据查找出来的结果,构建json串 -- jsoncpp}};
}

7.2 对InitSearcher接口的实现

void InitSearcher(const std::string &input)
{// 1. 获取或者创建index对象index = ns_index::Index::GetInstance();// 2. 根据index对象建立索引index->BulidIndex(input);
}

7.3 对Search接口的实现

我们对搜索的关键字在服务端进行分词,然后才能进行查找index。

7.3.1安装josncpp及使用示例

sudo yum install -y jsoncpp-devel
  • 安装完成后,项目中加入头文件#include <jsoncpp/json/json.h>
  • 编译命令后面加上-ljsoncpp

7.3.2 Search()编写

        // query: 搜索关键字// json_string: 返回给用户浏览器的搜索结果void Search(const std::string &query, std::string *json_string){// 1.[query分词]:对我们的query进行按照searcher的要求进行分词std::vector<std::string> words;ns_util::jiebaUtil::CutString(query, &words);// 2.[触发]:就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索关键字也需要ns_index::InvertedList inverted_list_all; // 内部InvertedElemfor (std::string word : words){boost::to_lower(word);ns_index::InvertedList *inverted_list = index->GetInvertedList(word); if (nullptr == inverted_list){continue;}inverted_list_all.insert(inverted_list_all.end(),inverted_list->begin(),inverted_list->end());}// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序std::sort(inverted_list_all.begin(),inverted_list_all.end(),\[](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2){//return e1.weight > e2.weight;//这种情况称之为降序});// 4.[构建]:根据查找出来的结果,构建json串 -- jsoncppJson::Value root;for(auto &item : inverted_list_all){ns_index::DocInfo* doc = index->GetforwardIndex(item.doc_id);if(nullptr == doc) //查找失败{continue;}Json::Value elem;elem["title"] = doc->title;elem["desc"] = GetDesc(doc->content, item.word);//doc->content是文档去标签之后的内容,我们要的是截取其中一部分elem["url"] = doc->url;root.append(elem);}Json::StyledWriter writer;*json_string = writer.write(root);}

ns_util::jiebaUtil::CutString: 

namespace ns_util{....const char* const DICT_PATH = "dict/jieba.dict.utf8";const char* const HMM_PATH = "dict/hmm_model.utf8";const char* const USER_DICT_PATH = "dict/user.dict.utf8";const char* const IDF_PATH = "dict/idf.utf8";const char* const STOP_WORD_PATH = "dict/stop_words.utf8";class jiebaUtil{private:static cppjieba::Jieba jieba;//jieba对象定义成静态的,这样就不用每次使用都创建一个jieba对象public:static void CutString(const std::string& src, std::vector<std::string>* out)//要使用静态的jieba对象,函数也必须是静态的{jieba.CutForSearch(src, *out);}};//类静态成员函数,要在类外初始化//类型  成员  初始化  cppjieba::Jieba jiebaUtil::jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH);
}

7.3.3实现获取内容摘要接口GetDesc();

为什么需要摘要?
因为经过我们的测试,搜索出来的数据量很大,不是很美观。

获取内容摘要接口注意点:

(1)我们的建立的索引Index,其中的倒排表实际上是统一按照小写词段进行查找的,也就是说,我们倒排索引表的左侧是小写的词。

我们搜索到的倒排拉链里面的文档ID信息里面存储的也是小写化的搜索关键词word。

(2)而索引Index.其中的正排表中,文档id对应的文档内容,doc_info里面的_content内容,却是不区分大小写的。

所以我们不能直接用小写化的关键词word在content中find搜索,大小写不匹配,这是可能找不到包含该关键词的语句的。

我们应该在content中寻找word时,在检查对比时,统一小写化对比。

这里我们使用C++ <algorithm>中的search接口解决。

获取摘要

        std::string GetDesc(const std::string& html_content,const std::string& word){//找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)//截取出这部分内容const int prev_step = 50;const int next_step = 100;//1.找到首次出现auto iter = std::search(html_content.begin(),html_content.end(),word.begin(),word.end(),[](int x,int y){return (std::tolower(x) == std::tolower(y));});if(iter == html_content.end()){return "None1";}//获取迭代器距离int pos = std::distance(html_content.begin(),iter);////2. 获取start,end , std::size_t 无符号整数int start = 0;int end = html_content.size()-1;//如果之前有50+字符,就更新开始位置if(pos > start + prev_step) start = pos - prev_step;if(pos < end - next_step) end = pos + next_step;//3. 截取子串,returnif(start >= end) return "None2";std::string desc = html_content.substr(start,end - start);desc += "...";return desc;}

8. 编写http_server模块

8.1测试版debug.cc 

#include "searcher.hpp"const std::string input = "data/raw_html/raw.txt";int main()
{//建立搜索对象ns_searcher::Searcher* search = new ns_searcher::Searcher();search->InitSearcher(input);std::string query;//用户输入的关键字std::string json_string;//返回序列化后的结果char buffer[1024];while (true){std::cout << "Please Enter Your Search Query@ ";fgets(buffer, sizeof(buffer), stdin);//用数组来存放query,如果用cin的话输入空格会被分开buffer[strlen(buffer) - 1] = 0;query = buffer;search->Search(query, &json_string);std::cout << json_string << std::endl;}return 0;
}

改写编译规则Makefile

PARSER=parser
DUG=debug
cc=g++.PHONY:all
all:$(PARSER) $(DUG)$(PARSER):parser.cc$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
$(DUG):debug.cc$(cc) -o $@ $^ -ljsoncpp -std=c++11.PHONY:clean
clean:rm -f $(PARSER) $(DUG)

测试:

可以看到,当我们输入关键词split,就成功将对应的文档信息显示出来,并且按照权重进行倒排排序。 

8.2网络版http_server.cc 

8.2.1升级gcc

cpp_httplib在使用的时候需要使用较新版本的gcc(大约是7.0以上的版本)
centos 7默认版本是4.8.5

搜索:scl gcc devsettool升级gcc

我们更新gcc需要用到三条指令 + 一个配置 (需要用scl来启动gcc 后续的配置是方便使用) 

前三步: 

//安装scl源
sudo yum install centos-release-scl scl-utils-build
//安装新版本的gcc
sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
//启动新版本
[whb@VM-0-3-centos boost_searcher]$ scl enable devtoolset-7 bash

第四步(可选):如果想每次登陆的时候,都是较新的gcc 

我们启动scl更新gcc,实际上只有当前次会话有效而已,即下一次重新启动之后仍然需要启动scl更新gcc。所以才有了我们的第四步配置bash_profile,可以使每次启动的时候,都会自动执行更新gcc的指令:scl enable devtoolset-7 bash 。就不用劳烦大家每次都启动更新一下gcc。

执行前面三步之后,就可以使用gcc的升级版本了

8.2.2 引入cpp-httplib库

最新的cpp-httplib在使用的时候,如果gcc不是特别新的话有可能会有运行时错误的问题
建议:cpp-httplib 0.7.15
下载zip安装包,上传到服务器即可

我们在gitee网站上搜索该资源 

 

 将下载好的库文件上传入云服务器中 (依托lrzsz工具 , 没有可以sudo yum install -y lrzsz)

 

我们就有了对cpp-httplib库资源的链接

PS:这里注意,我们cpp-httplib的实现是运用到了原生线程库的,所以我们在编译的时候,需要链接线程库-lpthread

PARSER=parser
# DUG=debug
HTTP_SERVER=http_server
cc=g++.PHONY:all
all:$(PARSER) $(DUG)$(PARSER):parser.cc$(cc) -o $@ $^ -lboost_system -lboost_filesystem -std=c++11
# $(DUG):debug.cc
# 	$(cc) -o $@ $^ -ljsoncpp -std=c++11$(HTTP_SERVER):http_server.cc$(cc) -o $@ $^ -ljsoncpp -lpthread -std=c++11.PHONY:clean
clean:rm -f $(PARSER) $(DUG)

8.2.3 使用cpp-httplib库

首先: #include "cpp-httplib/httplib.h"

#include "cpp-httplib/httplib.h"int main()
{httplib::Server svr;svr.Get("/hi",[](const httplib::Request &req, httplib::Response &rsp){rsp.set_content("你好,世界!", "text/plain; charset=utf-8");});svr.listen("0.0.0.0", 8888);return 0;
}

8.2.4 编写搜索服务端http_server.cc

设置服务端的web根目录wwwroot/index.html,web根目录就是直接访问该IP: Port端口时默认显示的网页。web根目录这里我们在后续实现前端的网页HTML时再进行演示。 

#include "cpp-httplib/httplib.h"
#include "searcher.hpp"const std::string input = "data/raw_html/raw.txt";
const std::string root_path = "./wwwroot";int main()
{ns_searcher::Searcher search;search.InitSearcher(input);httplib::Server svr;svr.set_base_dir(root_path.c_str());//设置web根目录//以Get的方式给client发送信息svr.Get("/s",[&search](const httplib::Request &req, httplib::Response &rsp){//client客户端浏览器访问时,请求中必须输入 word项-搜索关键词if(!req.has_param("word")){rsp.set_content("必须要有搜索关键字", "text/plain; charset=utf-8");//return;}//提出用户输入的关键词word,用Search进行搜索,将结果存入jsonstring字符串std::string word = req.get_param_value("word");std::cout << "用户在搜索:" << word << std::endl;std::string jsonstring;search.Search(word,&jsonstring);rsp.set_content(jsonstring, "application/json");//rsp.set_content("你好,世界!", "text/plain; charset=utf-8");});svr.listen("0.0.0.0", 8888);return 0;   
}

http_server搜索端测试:

9. 编写前端模块

我们boost搜索引擎的主要代码(后端)已经完成,我们接下来简单介绍一下前端。 

9.1 了解 vscode

我们使用Vscode连接云服务器进行前端代码的编写 , 下面我们安装Vscode并进行连接。 

#1.下载链接:

官网:https://code.visualstudio.com/

下载慢:https://www.zhihu.com/searchtype=content&q=vscode%20%E4%B8%8B%E8%BD%BD%E6%85%A2

#2.安装插件

Chinese (Simplified) (简体中文) Language Pack for Visual Studio Code

 Open in Browser

3.远程链接Linux 

安装插件: Remote SSH

示例:

【1】安装好Remote - SSH之后 ,按F1打开输入对话框。

【2】输入remote-ssh

【3】ssh user_name@60.204.169.245

9.2 了解前端

1. !tab
2. 由标签构成:单标签,双标签 

了解前端三大件:html ,  css ,  javascript(js)

html:是网页的骨骼  --- 负责网页结构

css: 网页的皮肉        --- 负责网页的美观

js:网页的灵魂         --- 负责动态效果,以及前后端交互

前端学习网站教程:http://www.w3school.com.cn

9.3 Html网页结构书写

9.3.1boost搜索引擎网页基本结构

9.3.2 HTML网页代码基本实现 

进入wwwroot/index.html进行代码书写

下面我们先介绍一下快捷键:

!+ Tab :会自动设计出html网页代码基本结构

h1 + Tab :会自动设计出h1标签

我们按照9.3.1搜索引擎根目录网页基本结构设计 ,设计如下的HTML文件。

代码:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>boost 搜索引擎</title>
</head>
<body><div class="container"><div class="search">   <input type="text" value="输入关键字"><button>搜索一下</button></div><div class="result"><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div><div class="item"><a href="#">这是标题</a><p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要</p><i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i></div></div></div>
</body>
</html>

wwwroot/index.hmtl 网页测试:

9.4 CSS网页样式设置

设置样式的本质:找到要设置的标签,设置它的属性
1. 选择特定的标签:类选择器,标签选择器,复合选择器
2. 设置指定标签的属性:见代码

9.4.1 设置网页总体样式

测试: 

9.4.2 设置搜索框样式

9.4.3 设置搜索结果样式

测试:

9.5 编写前后端交互JS代码

9.5.1 增加搜索功能

点击“搜索一下”按钮,我们可以进行搜索功能。

即我们需要添加点击按钮button时,会发生搜索Search()事件,执行Search()函数

代码结构:

测试代码结构: 

 9.5.2 引入JQuery库

如果直接使用原生的js成本会比较高(xmlhttprequest),我们推荐使用JQuery.

我们使用JQuery库 (就像C++语言之于C++标准库的关系)

来进行前后端交互,Js代码部分的书写。

JQuery CDN: https://www.jq22.com/cdn/

下面利用JQuery进行前后端交互搜索服务模块的编写。

9.5.3 网页Search()搜索函数书写

#1.获取搜索结果data

将之前的静态显示的内容注释掉:

#2.根据搜索结果,建立搜索结果网页。

搜索功能测试:

10. 解决搜到重复内容的问题

之前在写searcher.hpp时,我们对搜索词query进行jieba分词后,由于分出的是多个词段,不同词段可能在同一份文档中找到,所以在Search();的第二步:根据分“词”进行index查找中,我们会查到重复的文档信息。

以搜索内容“吃葡萄不吐葡萄皮”为例:

分词后可能得到“吃”、“葡萄”、“吐”、“葡萄皮”等词汇。
文档内容:吃葡萄不吐葡萄皮吃葡萄不吐葡萄皮吃葡萄不吐葡萄皮
在根据分词进行查找时:
吃,葡萄,吐,等等很多词语都会指向这个文档内容
从而导致:输出结果会出现很多重复内容 

修改代码: 

我们新建一个InvertedElemPrint结构体,结构体中words成员变量来存放同一个query中不同词段存在在该文档。然后把inverted_list_all存放的节点换成InvertedElemPrint。

然后我们建立一个tokens_map,key值为文档id,value为InvertedElemPrint。然后再遍历每个关键词段的倒排拉链的时候,我们利用tokens_map,重复出现的文档我们就将词段存入到对应的InvertedElemPrint,然后权重值进行累加。

最后我们再将去重后的InvertedElemPrint节点再存进inverted_list_all 

#pragma once#include "index.hpp"
#include "util.hpp"
#include <algorithm>
#include <vector>
#include <jsoncpp/json/json.h>namespace ns_searcher
{//同一个query去重后要打印的节点结构struct InvertedElemPrint{uint64_t doc_id;int weight;std::vector<std::string> words;InvertedElemPrint():doc_id(0),weight(0){}};const std::string raw_file = "data/raw_html/raw.txt";class Searcher{private:ns_index::Index *index; // 供系统进行查找的索引public:Searcher() {}~Searcher() {}public:void InitSearcher(const std::string &input){// 1. 获取或者创建index对象index = ns_index::Index::GetInstance();// 2. 根据index对象建立索引index->BulidIndex(input);std::cout << "建立正排和倒排索引成功..." << std::endl;}// query: 搜索关键字// json_string: 返回给用户浏览器的搜索结果void Search(const std::string &query, std::string *json_string){// 1.[query分词]:对我们的query进行按照searcher的要求进行分词std::vector<std::string> words;ns_util::jiebaUtil::CutString(query, &words);// 2.[触发]:就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索关键字也需要// ns_index::InvertedList inverted_list_all; // 内部InvertedElemstd::vector<InvertedElemPrint> inverted_list_all; // 内部InvertedElemstd::unordered_map<uint64_t, InvertedElemPrint> tokens_map; //文件id作为key,进行去重for (std::string word : words){boost::to_lower(word);ns_index::InvertedList *inverted_list = index->GetInvertedList(word); if (nullptr == inverted_list){continue;}// inverted_list_all.insert(inverted_list_all.end(),inverted_list->begin(),inverted_list->end());//去重。不同同一个词段对应同一个文档,要把其权重加起来,以及所有对应该文档的词段都保存起来for(auto &elem:*inverted_list){auto& item = tokens_map[elem.doc_id];//[]:如果存在直接获取,如果不存在新建//item一定是doc_id相同的print节点item.doc_id = elem.doc_id;item.weight += elem.weight;item.words.push_back(elem.word);}//去重后的InvertedElemPrint节点再存进inverted_list_allfor(const auto& item:tokens_map){inverted_list_all.push_back(std::move(item.second));}}// 3.[合并排序]:汇总查找结果,按照相关性(weight)降序排序// std::sort(inverted_list_all.begin(),inverted_list_all.end(),\// [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2){////     return e1.weight > e2.weight;//这种情况称之为降序// });std::sort(inverted_list_all.begin(),inverted_list_all.end(),\[](const InvertedElemPrint &e1, const InvertedElemPrint &e2){//return e1.weight > e2.weight;//这种情况称之为降序});// 4.[构建]:根据查找出来的结果,构建json串 -- jsoncppJson::Value root;for(auto &item : inverted_list_all){ns_index::DocInfo* doc = index->GetforwardIndex(item.doc_id);if(nullptr == doc) //查找失败{continue;}Json::Value elem;elem["title"] = doc->title;elem["desc"] = GetDesc(doc->content, item.words[0]); //doc->content是文档去标签之后的内容,我们要的是截取其中一部分elem["url"] = doc->url;elem["id"] = (int)item.doc_id;elem["weight"] = (int)item.weight;root.append(elem);  }Json::StyledWriter writer;*json_string = writer.write(root);}std::string GetDesc(const std::string& html_content,const std::string& word){//找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)//截取出这部分内容const int prev_step = 50;const int next_step = 100;//1.找到首次出现auto iter = std::search(html_content.begin(),html_content.end(),word.begin(),word.end(),[](int x,int y){return (std::tolower(x) == std::tolower(y));});if(iter == html_content.end()){return "None1";}//获取迭代器距离,这里我们都用int,防止size_t的有坑int pos = std::distance(html_content.begin(),iter);//2. 获取start,end , std::size_t 无符号整数int start = 0;int end = html_content.size()-1;//如果之前有50+字符,就更新开始位置if(pos > start + prev_step) start = pos - prev_step;if(pos < end - next_step) end = pos + next_step;//3. 截取子串,returnif(start >= end) return "None2";std::string desc = html_content.substr(start,end - start);desc += "...";return desc;}};
}

修正之前

修正之后 

11.添加日志

11.1日志代码

#pragma once
#include <iostream>
#include <string>
#include <ctime>
#define NORMAL 1
#define WARNING 2
#define DEBUG 3
#define FATAL 4#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)
void log(std::string level, std::string message, std::string file, int line)
{std::cout << "[" << level << "]"<< "[" << time(nullptr) << "]"<< "[" << message << "]"<< "[" << file << " : " << line << "]" << std::endl;
}

#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__): 这是一个宏定义,用于简化日志记录的调用。当你使用LOG(NORMAL, "This is a normal message")时,它会展开为log("NORMAL", "This is a normal message", __FILE__, __LINE__)。其中,__FILE__和__LINE__是预处理器宏,分别表示当前代码的文件名和行号。

注意:#LEVEL #后面的宏:  对之不进行宏替换,而是自动将其作为一个字符串

举个例子:         LOG(WARNING,"这里有xxx警告")  ->

在预处理阶段处理成: log( "WARNING" , "这里有xxx警告" , "调用log的文件名" , "调用行数")

最后打印出来的信息: [WARNING][这里有xxx警告][searcher.hpp][50]

11.2进行日志部署

11.2.1在index.hpp中进行日志部署

首先: #include "log.hpp"

11.2.2在searcher.hpp中进行日志部署 

11.2.3在http_server.cc中进行日志部署

11.2.4日志打印测试

11.3 部署服务到 linux 上

11.3.1 nohup指令

nohup指令: 将服务进程以守护进程的方式执行 , 使关闭XShell之后仍可以访问该服务。

示例:

nohup ./http_server:这将会启动 http_server 程序,并确保它在后台运行,不受终端关闭的影响。
为了进一步使程序在后台静默运行,不显示在终端上,你可以在命令的末尾加上 & 符号。

示例:
nohup ./http_server &:这样,http_server 会在后台启动并运行,而不会显示在终端上。即使你关闭了终端或 SSH 连接,它也会继续运行。

nohup形成的文件:

执行完上述的nohup指令之后,将会形成一个nohup.out存储日志信息文件,可以cat查看该文件

11.3.2 使用nohup指令进行服务和日志部署

实现基本的 自动服务+更新日志 的部署

使用nohup指令后,我们的http_server就可以一直在后台执行了

下面验证即使我们关闭了XShell,而且日志信息也会自动同步更新到nohup.out中。

我们将xshell关掉后,同样可以使用搜索服务,并且我们看到了日志信息也会自动同步更新到nohup.out中。

事实上,我们也可以存一个专门的日志文件夹,进行日志信息的存储。 

而这之前我们要删除上一条对于日志 服务的部署。

我们先使用以下指令找到相应服务的PID

ps axj | head -1 && ps axj | grep http_server   //找到相应服务的PID。

然后使用kill-9 将其进程杀掉

我们下面我们再来 自动服务+更新日志 的部署 

可以看到我们完成了自动服务+更新日志 的部署  

12. 结项与项目扩展方向

至此,我们就完成了boost搜索引擎项目,我们可以随时访问该服务网址,进行在boost准标准库相应接口的站内搜索。

这里我们再总结一下该项目扩展方向

2.我们在搜索引擎中,对于权重的设置先后显示顺序,我们其实可以叠加一些算法,比如可以设置竞价排名,热点统计,额外增加某些文档的权重。

3.我们可以利用数据库,设置用户登录注册,引入对MySQL的使用。

1. 建立整站搜索。受限于云服务器配置,我们没有做boost库的整站搜索,而只是做了1.8.0版本下的doc/html/*下的文件搜索。
2. 在我们的搜索引擎中,添加竞价排名
3. 热次统计,智能显示搜索关键词(字典树,优先级队列)
4. 设置登陆注册,引入对mysql的使用

备注

项目源代码地址:

GTY/boost_search - 码云 - 开源中国 (gitee.com)

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/736004.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

《操作系统真相还原》读书笔记八:获取物理内存容量以及本书源代码

编写mbr.S汇编代码 ;LOADER_BASE_ADDR equ 0x900 ;LOADER_START_SECTOR equ 0x2 %include "boot.inc"SECTION MBR vstart0x7c00mov ax,csmov ds,axmov es,axmov ss,axmov fs,axmov sp,0x7c00mov ax,0xb800mov gs,axmov ax,0x0600mov bx,0x0700mov cx,0mov dx, 184fh…

力扣图论篇

以下思路来自代码随想录以及官方题解。 文章目录 797.所有可能的路径200.岛屿数量130.被围绕的区域1020.飞地的数量 797.所有可能的路径 给你一个有 n 个节点的 有向无环图&#xff08;DAG&#xff09;&#xff0c;请你找出所有从节点 0 到节点 n-1 的路径并输出&#xff08;不…

USB协议学习(三)大容量存储设备SCSI协议分析

笔者来简单介绍一下SCSI得协议命令 1、SCSI协议认识 SCSI&#xff1a;Small Computer System Interface&#xff0c;用于计算机外部设备得接口标准&#xff0c;定义了与外部设备得一套协议。SCSI标准协议族支持很多钟SCSI设备&#xff0c;像盘&#xff0c;打印机&#xff0c;扫…

Ubuntu23.10安装FFmpeg及编译FFmpeg源码

安装FFmpeg: 打开终端: 输入 sudo apt install ffmpeg 安装成功: 验证FFmpeg 默认安装位置与库与头文件位置 使用FFmpeg源码编译: 1.安装YASM sudo apt-get install yasm

[mmucache]-ARMV8-aarch64的虚拟内存(mmutlbcache)介绍-概念扫盲

&#x1f525;博客主页&#xff1a; 小羊失眠啦. &#x1f3a5;系列专栏&#xff1a;《C语言》 《数据结构》 《C》 《Linux》 《Cpolar》 ❤️感谢大家点赞&#x1f44d;收藏⭐评论✍️ 思考: 1、cache的entry里都是有什么&#xff1f; 2、TLB的entry里都是有什么? 3、MMU操作…

10-ARM gicv3/gicv4的总结-基础篇

目录 1、gic的版本2、GICv3/gicv4的模型图3、gic中断号的划分4、GIC连接方式5、gic的状态6、gic框架7、gic Configuring推荐 本文转自 周贺贺&#xff0c;baron&#xff0c;代码改变世界ctw&#xff0c;Arm精选&#xff0c; armv8/armv9&#xff0c;trustzone/tee&#xff0c;s…

遗传算法(GA)求解基于栅格地图的机器人最优路径规划,可以自行修改地图(提供MATLAB代码)

通过栅格法建立栅格地图作为机器人路径规划的工作环境,采用遗传算法作为机器人路径搜索的规则.将所有机器人放置于初始位置.经过NC次无碰撞迭代运动找到最优路径.到达目标位置.为防止机器人在路径搜索过程中没有达到最大迭代次数时路径大小已不发生变化而陷入局部最优。 一、部…

JavaWeb-Maven基础

Maven是专门用于管理和构建Java项目的工具&#xff0c;是 Apache 下的一个纯 Java 开发的开源项目&#xff0c;基于项目对象模型&#xff08;POM&#xff09;概念。先来学习一下Maven基础&#xff0c;等后面学完开发框架后再学Maven高级&#xff0c;这次的内容如下 一、概述 …

Linux学习——线程的控制

目录 ​编辑 一&#xff0c;线程的创建 二&#xff0c;线程的退出 1&#xff0c;在子线程内return 2,使用pthread_exit(void*) 三&#xff0c;线程等待 四&#xff0c;线程获取自己的id值 五&#xff0c;线程取消 六&#xff0c;线程分离 一&#xff0c;线程的创建 在对…

企微hook源码

企微hook源码已经在QQ群内开源。速度进群下载&#xff0c;避免和谐。 QQ群&#xff1a;649480745 //pc端发送消息回调 void RecvPcMsgRes(DWORD eax) { // OutputDebugStringA("pc发送消息"); PWX_STRING pMsgPb (PWX_STRING)(eax);//消息pb结构体 string pbDa…

打卡--MySQL8.0 一(单机部署)

一路走来&#xff0c;所有遇到的人&#xff0c;帮助过我的、伤害过我的都是朋友&#xff0c;没有一个是敌人。如有侵权&#xff0c;请留言&#xff0c;我及时删除&#xff01; MySQL 8.0 简介 MySQL 8.0与5.7的区别主要体现在&#xff1a;1、性能提升&#xff1b;2、新的默认…

鸿蒙开发学习入门教程之环境配置

最近鸿蒙开发越来越火&#xff0c;各个大厂都有鸿蒙版本的计划和宣传&#xff0c;看这个趋势&#xff0c;可能会在几年内发展壮大&#xff0c;为我们移动端码农开辟一片新的职场。所以现在开始学起来还是很有必要的。今天就一起开始配置环境搞起来吧。 首先&#xff0c;找到官…

用户管理【MySQL】

文章目录 查看用户信息创建用户修改密码删除用户授予权限收回权限 查看用户信息 在名为mysql的数据库中有一个表user维护着 MySQL 的用户信息。 其中&#xff1a; user&#xff1a; 表示该用户的用户名。host&#xff1a; 表示该用户可以从哪个主机登录&#xff0c;localhost…

【Spark编程基础】实验一Spark编程初级实践(附源代码)

文章目录 一、实验目的二、实验平台三、实验内容和要求1. 计算级数2. 模拟图形绘制3.统计学生成绩 一、实验目的 1.掌握 Scala 语言的基本语法、数据结构和控制结构&#xff1b; 2.掌握面向对象编程的基础知识&#xff0c;能够编写自定义类和特质&#xff1b; 3.掌握函数式编程…

SSL证书:构建网络安全的基石

&#x1f90d; 前端开发工程师、技术日更博主、已过CET6 &#x1f368; 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 &#x1f560; 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 &#x1f35a; 蓝桥云课签约作者、上架课程《Vue.js 和 E…

简单接入电商API接口|轻松实现实时采集淘宝、抖音、快手、1688商品,挖掘潜力款

今天给大家带来一款非常实用的电商API接口&#xff0c;这款数据采集接口支持淘宝采集、抖音采集、快手采集、1688采集以及潜力款分析&#xff0c;功能强大&#xff0c;助您在电商领域更上一层楼。 首先&#xff0c;我们来了解一下淘宝采集功能。作为国内最大的电商平台&#xf…

基于Java+springboot+VUE+redis实现的前后端分类版网上商城项目

基于Java springbootVUEredis实现的前后端分类版网上商城项目 博主介绍&#xff1a;多年java开发经验&#xff0c;专注Java开发、定制、远程、文档编写指导等,csdn特邀作者、专注于Java技术领域 作者主页 央顺技术团队 Java毕设项目精品实战案例《1000套》 欢迎点赞 收藏 ⭐留言…

Contact-GraspNet: Efficient 6-DoF Grasp Generationin Cluttered Scenes

总结 提出一种端到端的网络&#xff0c;解决复杂场景中对未知物体的抓取。将六自由度抓取投影到观察到的点云中的接触点&#xff0c;表示只有4-DoF。 摘要 我们提出了一个端到端网络&#xff0c;可以直接从场景的深度记 录中有效地生成六自由度平行颚抓取的分布。 我们的新…

06 数据结构之树

引言&#xff1a; 数的代码实现&#xff0c; 先序遍历、中序、后序、层次遍历 /* binary_tree.h */ #ifndef _BINARY_TREE_H #define _BINARY_TREE_H#include <stdio.h> #include <stdlib.h> #include <string.h>#define DEBUG(msg) \printf("--%s--, %…

车载诊断协议DoIP系列 —— 诊断报文和诊断报文应答传输层安全(TLS)

车载诊断协议DoIP系列 —— 诊断报文和诊断报文应答&传输层安全(TLS) 我是穿拖鞋的汉子,魔都中坚持长期主义的汽车电子工程师(Wechat:gongkenan2013)。 老规矩,分享一段喜欢的文字,避免自己成为高知识低文化的工程师: 本就是小人物,输了就是输了,不要在意别人怎…