c++实现boost库 搜索引擎(详细介绍和代码),cppjieba的下载和使用,正排/倒排索引的查询和建立,cpp-httplib的下载和使用

目录

boost库 搜索引擎

项目背景

引入

表现形式

boost库介绍

项目环境

数据源

下载文档

页面目录

查看html文件的数量

技术栈

原理

过程

正排/倒排索引

正排索引

分词

暂停/停止词

倒排索引

模拟查找过程

parser模块

读取文件

标签

如何存放

代码编写思路

遍历目录

读取文件

格式化

标题

正文

代码

注意

构建url

借助move

添加分隔符

建立索引模块

正排索引

倒排索引

查找索引

建立索引

读取文档

建立正排索引

切分字符串 -- split

插入元素

代码 

建立倒排索引

思路

分词 -- cppjieba

确定权重

忽略大小写

插入元素

代码

搜索引擎模块     

索引对象单例化                                                

搜索

流程

忽略大小写

查询倒排索引

得到集合

去重

排序

查询正排索引

引入jsoncpp

截取文档摘要

代码 

服务器模块

cpp-httplib

devtoolset

创建对象

设置根目录

处理请求

获取请求

获取搜索内容

调用搜索模块

代码 

运行结果

前端模块

代码

运行结果

代码结构


boost库 搜索引擎

项目背景

引入

市面上已经有很多公司设计出很多的搜索引擎

  • 百度,搜狗,goole,360搜索等等
  • 这种大型的项目我们一个人是没办法完成的,因为它是全网搜索,涉及到的内容过于庞大

但我们可以实现站内搜索

  • 比如cplusplus.com

与全网搜索的不同点在于,站内搜索得到的结果更垂直,数据量更小

  • 垂直 -- 数据之间具有很强的相关性

表现形式

搜索引擎以什么方式显示给用户?

  • 搜索框+搜索按钮

搜索关键字后,每个搜索结果基本由三大部分组成:

  • 并且,提供跳转网页的功能
  • 其他的搜索引擎也基本是这些内容,只是细节有所不同

boost库介绍

Boost库是一个广泛使用的C++库集合,提供了许多功能强大且高效的工具和组件,可以帮助开发者提高代码的质量和效率

  • Boost库包含超过160个独立的库,这些库各自提供特定的功能,包括数据结构、算法、并发编程、网络编程等

但是boost库的官网并没有提供搜索功能,但网站提供的资源量非常大

  • 所以需要我们自己做一个boost库的搜索引擎
  • 注意,搜索的是boost官网上罗列的手册(接口什么的介绍),而不是boost库的源码

项目环境

centos7云服务器,vim,gcc/g++,makefile,vscode

数据源

下载文档

在boost官网下可以下载 -- Boost C++ Libraries

  • 点击下图的download

然后将下载的文件上传到云服务器下

  • 如果在上传过程中出现乱码(文件如果很大,就可能会出现乱码)
  • 可以在使用rz命令时添加E选项

上传成功后,进行解压缩

页面目录

我们在boost官网下的文档进行查询时,点击其中某个标签页,会发现得到的新页面大多数是放在/doc/html目录下的

  • 所以我们在后续使用该网页上的文档作为搜索引擎数据源时,就只需要使用/doc/html目录下的文件即可

这些文件就是boost各个组件对应的手册内容,以网页形式存在

  • 我们后面就用它们来建立索引
  • 所以我们直接将这个目录拷贝到我们项目文件中

不过也有不在doc下的网页文件,但我们不考虑这些

查看html文件的数量

我们可以查看一下我们的目标目录中一共有多少html文件:

  • ls -Rl | grep -E '*.html' | wc-l

技术栈

  • c/c++,c++11,stl,boost库
  • jsoncpp -- cs两端进行数据交换的格式
  • cppjieba -- 分词工具(对文档和用户输入内容进行分词)
  • cpp-httplib -- 构建http服务器
  • html5,css,JavaScript,jQuery,ajax -- 前端内容

原理

过程

客户端(pc或手机)

  • 上面会运行着很多软件(浏览器)

服务器(比如公司服务主机)

  • 上面也会服务运行软件(searcher)

在提供服务前,需要在各个网站中抓取网页,放入主机磁盘中

  • 并且对抓取到的数据进行处理,并建立索引
  • 完成之后,就可以基于搜索引擎进行搜索了

用户通过浏览器

  • 向服务器发送http请求,其中携带有搜索关键字

服务器接收后

  • 使用关键字在索引中进行检索,得到相关网页
  • 最后将多个网页信息经过包装(title+desc+url),形成一个新的网页后,返回给客户端

我们这里要完成的内容,就是红框内的功能

  • 至于爬虫,我们可以通过正规渠道将目标网站的内容下载下来,也是一样的(也就是前面说的数据源)

正排/倒排索引

假设有两个文档

正排索引

文档id 找到 文档内容

建立正排索引:

分词

将一段文本切分成更小的单元(称为“词”或“标记”)

我们建立搜索引擎,需要对目标文档分词

  • 目的 -- 方便建立倒排索引和查找

可以分出来的不止这些词语

  • 因为有些词语是可以连起来的
  • 比如四斤小米,或是直接是整句话,总之会有很多种组合方式
暂停/停止词

对文本的意义或分析贡献较小的常见词汇

  • 通常用于构建句子的结构,帮助表达语法关系,但对于主题识别和信息检索的贡献有限
  • 所以,区分唯一性的价值不大,还会增加搜索成本,一般会在分词的时候忽略掉
  • 比如 : 了,的,在,是,吗,a,the 

倒排索引

根据关键字(也就是我们对文档进行分词后得到的结果),找到文档id

  • 关键字具有唯一性,如果关键字在多个文档中出现,只保留一份

建立倒排索引 -- 我们将文档内容分词后,整理出不重复的关键字,与文档id联系起来 

模拟查找过程

接下来,基于这两个索引,我们来模拟一次查找的过程:

假设用户输入"小米"

  • 那第一步一定是使用"小米"在倒排索引中进行查找,提取出对应的文档id(1,2)
  • 再拿着文档id在正排索引中查找,找到两个文档对应的内容,然后分别对文档结果进行格式化(提取出title+desc+url)
  • 最后将结果以网页的形式构建出响应,返回给用户

因为可能有多个文档与关键字匹配

  • 所以还需要给每个文档赋权值,谁的权重更高,就把哪个文档显示在前面
  • 就像是在各种搜索引擎上搜索出来的内容,一定会经过排序的,而顺序一定是由权重决定的

parser模块

整体思路 -- 对下载下来的文档信息(网页)进行去标签和数据清洗

读取文件

首先肯定是要先拿到文件,再对文件进行处理

  • 也就是需要先找到所有文件名,以vector的形式组织起来
  • 然后挨个进行读取

因为c++和stl对文件系统的支持不是特别好,所以我们需要使用boost库中的filesystem

  • boost开发库的安装 -- sudo yum install boost-devel
  • centos7下默认下载的是1.53版本的boost开发库,所以我们去官网上查看接口使用方法时,也应该查看1.53版本的
  • 注意:开发库 和 官网上下载到的文档(手册网页信息) ,不是一个东西

查看手册中filesystem的教程,并随便点击一个函数,进入filesystem库的接口说明:

因为boost库并不是c++的标准库,所以我们需要编译语句中指明我们要使用boost库

  • 否则会链接错误 -- -lboost_system  和 -lboost_filesystem

标签

这里截取一部分html目录下某文件的内容:

html的标签都被<>包裹起来

  • 对搜索没有意义,只是用来组成网页格式,并且这些标签在每个html文件中都会存在
  • 我们需要的是去掉这些标签后的部分,这才是对于搜索引擎来说的有效数据
  • 所以需要去标签

标签有成对出现的,也有单独出现的

  • eg:<head> 和 </head>,<link...>

如何存放

将文件数据做完去标签化后,该放在哪里呢?

  • 新建一个文件夹,并只创建一个文件,再将每个文档去标签的内容都写入到该文件
  • 每个文档之间用特殊符号隔开,相当于每个文档的内容只占"一行"

因为文档中的字符基本都是打印字符,是可显的

  • 控制字符是不可显的,就不会污染我们形成的新的文档
  • 所以我们可以选择使用控制字符作为分隔符

于是我们就有了html文档的原始数据和有效内容

代码编写思路

遍历目录

将html文档所在目录定义成一个path对象

  • 并在核心逻辑开始前,判断该路径是否存在

我们这里使用boost库提供的迭代器(注意,一定要使用递归迭代器recursive_directory_iterator,但在前期测试的时候可以使用普通迭代器directory_iterator,不然每次运行都可慢)

  • 迭代遍历目录下的子文件,直到为空
  • 迭代方式就和迭代查询一样,会将目录下所有文件都查出来,包括子目录下的文件

但遍历时,可能遍历到的文件不是html文件,而是其他文件

  • 所以,需要判断
  • 是否是常规文件 -- is_regular_file()
  • 后缀是否是.html -- extension()

path()

  • 提取迭代器指向的路径对象

string()

  • 获取路径对应的字符串形式(可以用来debug,以及传参)
读取文件

我们可以借助c++的文件流对象

  • 打开文件可以使用直接构建文件流对象的形式,然后结合getline读取

如何理解getline读取到文件结束?

  • getline的返回值是一个引用,但while判断的是bool类型
  • 判断过程的本质是因为重载了强制类型转化

我们可以将像这样的辅助函数全部写在一个文件中,并且限制下作用域

格式化

根据每个文档的内容都形成三个部分,方便我们进行后续操作

接下来是提取三大部分

标题

每个文档的标题都在<titile>...</title>内

  • 所以,提取的重点就是识别出这一对标签

然后根据迭代器位置,进行截取

  • 注意,在截取之前,我们还需要考虑一种情况,虽然这种情况的概率很小:begin在end之后
  • 这样的话就会导致截取到的字符串的字符个数是负数,不符合常理

 

正文

其实就是去掉标签后的内容,需要我们基于一个简单的状态机来实现

  • 因为文档中的内容,要么是标签,要么是有效内容
  • 所以我们就定义两种状态

如何判断状态切换的时机呢?

  • 遇到'<'代表进入标签 
  • 遇到'>'代表出标签

注意,出标签后不代表就进入了有效内容部分,也可能接在后面的是一个新标签

  • 所以,处于content状态时,还需要进行判断当前字符是否是'<'
  • 如果是,则说明又是个标签
  • 如果不是,则确实是有效内容
  • 无论是单标签还是双标签,都适用这套规则

小细节:

这里我们不能直接将有效内容直接插入

  • 要去掉\n
  • 因为我们后续还要添加一个分隔符\n,为了更好地读取
  • 所以我们需要将文档内容中原有的\n去掉,改成' '
代码
     enum state{LABLE,CONTENT};enum state s = LABLE;for (char c : data){switch (s){case LABLE:if (c == '>')s = CONTENT;break;case CONTENT:if (c == '<')s = LABLE;else{// 我们不想保留原始文件中的\n,因为我们想用\n作为html解析之后文本的分隔符if (c == '\n')c = ' ';result->content_ += c;}break;default:break;}}
注意

我们是将整个html页面上的内容进行去标签化的

  • 所以标题也在正文里头
  • 那么,当我们在后面进行词频统计时,如果一个词在标题中出现了,那么它也会在正文中再出现一次
  • 相当于正文中多统计了一次

 

构建url

官网中url中的路径,是和我们下载下来的文档中的路径有对应关系的

  • 我们下载下来的文档: boost_1_86_9/doc/html/*​
  • 所以,/doc/libs/1_86_0/doc/html = boost_1_86_0/doc/html

而我们将boost库的doc/html/* 拷贝到我们项目中的指定目录下

  • 所以,boost_1_86_0/doc/html = 项目所在路径/input

所以/doc/libs/1_86_0/doc/html = 项目所在路径/input

因为我们要做的是搜索引擎,要将本地文档映射到官网网页上

  • 所以,如果我们要构建出 一个url,首先官网ip地址和使用协议不能少 --https://www.boost.org
  • 接着就是资源所属路径,本来文档在我们linux本地文件目录下 -- 项目所在路径/input/*,但因为实际应该使用网页中的路径,所以是/doc/libs/1_86_0/doc/html/*
  • 也就是说,https://www.boost.org/doc/libs/1_86_0/doc/html/是不变的,作为固定前缀
  • 所以只需要拼上当前文档在input中的相对路径,作为变化后缀
  • 就完成了url的构建​

借助move

格式化完成之后,需要将doc_info插入到容器中

  • 如果直接将结构体插入vector中,是直接拷贝进去,效率较低
  • 所以,我们可以引入移动语义,使用move函数来避免拷贝
  • 后面的一些拷贝也都可以这样做
添加分隔符

最后,将解析完成的各个文件内容,全部写入到指定文件中

  • 并以'\3'作为分隔符

但是,每个文档都被分成三部分,如果直接放进去,该如何区分这三部分呢?

  • 所以,我们需要再添加一个分隔符
  • 并且,考虑到后续对该文件的读取,如果我们能让每次读取操作都精准读完一个文档,可以使之后的处理更加方便
  • 那么,让一个文档的内容变成一行就好了,这样使用getline的话,就可以刚好每次读完一个文档,然后在文档内进行分割

所以,最终文档内的存放形式如下:

建立索引模块

我们两个索引都要建立好,这样才能完成一个完整的查找流程

正排索引

因为映射关系是id->内容,而id就是数字

  • 所以数组是最方便的,天然就有数字作为标识符
  • 而vector就是更高级的数组,可以自动管理内存,所以采用vector建立正排索引

倒排索引

一个关键字可以对应多个文档id,并且要存放每个关键字对应的权重

  • 所以,先定义一个结构体,表示关键字信息

因为要建立关键字->文档id(可能有多个)的映射关系

  • 所以,采用unordered_map最为合适,第二个元素用vector将多个文档信息组织起来

我们也可以将[用vector将多个关键字信息组织起来]的结构,重命名成一个新类型

  • 称为倒排拉链
  • 所以,倒排索引的结构就变成了关键字和倒排拉链之间的映射关系

查找索引

在索引结构中查找前,需要先确定使用的标识是否存在

比如,在正排索引中,判断拿到的文档id是否合法

  • 因为我们是直接用数组下标表示文档id的,并且一定是顺序排放的(索引中的某项一旦建立好,一般就不会再删除了)
  • 所以直接判读下标是否合法即可

而在倒排索引中,则是要判断关键字是否存在

建立索引

读取文档

除了要提供查找正排/倒排索引的接口,还要先建立好索引才行

  • 根据去标签化后的正文内容,建立两种索引结构

而文本内容已经被我们存入了文件中,所以我们只需要为该接口提供路径就可以了

  • 打开文件时,因为我们是以二进制方式写入的,所以也要以二进制方式读出
  • 因为我们之前存入文件时,已经用\n将每个文档内容区分开了,所以我们直接按行读取,读出来的就是一个完整的文档
  • 读出来一个文档,就进行索引的建立

建立正排索引
切分字符串 -- split

因为我们是以这种方式插入的,所以读出来也是按格式读

切分字符串我们可以用现成的接口,而不需要手动调用find去切

  • boost库中的algorithm库中的split函数

如果有多个分隔符连在一起

  • 默认下(token_compress_off)是保留的,也就是会切分出多个空串

如果设置成token_compress_on,会将多个分隔符压缩成一个

插入元素

提取出来后,构建文档信息对象

  • 注意,这个对象还要再增加一个字段 -- doc_id
  • 方便我们后续通过该结构获取对应的文档id

而插入到vector中,数组下标会自动增加,这个下标可以和元素个数对应

  • 元素下标=元素个数-1
  • 所以我们可以进行事务id的分配时,直接使用插入新元素前的元素个数即可

然后插入到代表正排索引的数组中

  • 注意这里和前面是一样的,可以结合move进行移动拷贝,提高效率
代码 
  void create_positive_index(const std::string &path) // 以文档为单位{std::ifstream in(path, std::ios_base::in | std::ios_base::binary);if (!in.is_open()){std::cerr << "file: " << path.c_str() << " open failed" << std::endl;return;}std::string doc;while (std::getline(in, doc)){// 拿到一个文档,进行解析docInfo_index di;if (!analysis(doc, delimiter, &di)){std::cerr << "analysis faild" << std::endl;continue;}di.doc_id_ = pos_index_.size();// 解析完成后,插入到索引中pos_index_.push_back(std::move(di));}}

建立倒排索引
思路

我们可以在正排索引的基础上,拿到对应的文档信息对象

  • 其中包含四部分,title+content+url+doc_id
  • 最重要的就是将content进行分词

因为我们已经建立了'倒排拉链'这一自定义结构

  • 所以,我们最终目的就是要根据所有文档的content,提取出关键字,以此形成一个/多个倒排拉链
  • 每个倒排拉链都对应一个词的信息

而我们的处理方式是读取一个文档,处理一个文档

  • 处理当前文档时,是提取出多个关键字->当前文档id
  • 处理很多个文档后,就会形成关键字和文档id的多对多关系
分词 -- cppjieba

那么,究竟如何分词呢?

  • --cppjieba工具

下载:

git clone --recurse-submodules git@github.com:yanyiwu/cppjieba.git
  • 词库: dict目录
  • 作为分词的基准,判断哪些词可以分开/合并

为了让程序可以找到我们下载的文件,可以建立两个软链接

  • 一个对应头文件,一个对应词库文件

当我们试着将demo代码编译时,却显示没有limonp目录下的一个文件

  • 需要我们手动将limonp目录拷贝进cppjieba/include/cppjieba中

我们需要使用的函数是CutForSearch

  • 我们直接仿照demo代码中的格式,传入需要进行分词的内容,存放分词后结果的vector容器即可
  • 我们可以将jieba变量设置成类内静态变量,因为每个文档进行分词时都要使用,而且用户输入的内容也要进行分词

注意,这里分词的时候,不会将英文单词分成两块

  • 比如typedef中,不会将它拆成typedef和type
  • 所以,如果我们搜type时,并不会和typedef匹配上

但在网页中使用ctrl f进行查询时是可以查到的

  • 问题不在于我们代码有问题,只是分词规则不同
确定权重

分词完成后,我们需要统计词和文档的相关性

  • 以此来确定某词在文档内的权重

但是,实际上相关性是可以由多种维度衡量的,很难去设计

  • 这里我们就使用简单的方法 -- 词频
  • 一个关键字在某文档出现的频率越高,如果用户搜索该词,就应该把这个文档放在更前面
  • 如果是在标题中出现的,相关性就更高一些;内容中出现的,相关性就低一些

我们可以定义一个结构体,来表示两个级别

然后分别对标题和内容进行统计,统计出每个词出现的频率

  • 因为某些词可能在标题和内容中都会出现,所以我们可以让词作为标识,也就是用unordered_map将不同的[词和对应词频]组织起来 
  • 可以称它为词频映射表
  • 于是,我们就能得到在文档的标题和内容中[每个词出现的次数],从而得到文档与词的相关性

如何制定计算权重的规则呢?

  • 因为设定上,标题中出现关键字比内容中出现的相关性更高,所以在计算权重时,可以是:
  • 某词的权重=在标题中出现的次数*10+在内容中出现的次数

最后,我们可以遍历cnt_map

  • 取出一个元素,就拿到了词+在标题/内容中的出现次数
  • 就可以构建一个倒排拉链中的元素,然后插入到关键字对应的倒排拉链中
忽略大小写

在构建倒排索引时,有一个非常重要的且容易被忽视的点

搜索不同大小写下的英文的搜索结果是否应该一致呢?

  • 应该,匹配的时候应该忽略大小写

如何实现?

  • 我们可以把所有英文单词全部转换为小写
  • 内部存放的都是小写单词,用户输入后也都转换成小写
  • 这样不管搜的是啥,原文内容是啥,都可以匹配上,只要是同一个字母
  • 所以,在插入到倒排索引(也就是词频映射表)前,我们要对分出来的词进行大小写转换

可以使用c++的接口,或是boost库提供的

插入元素

这里在构建对象时,需要当前文档id

  • 因为我们是以文档为单位处理的,所以提取出的关键字映射的都是当前文档
  • 而我们事先已经将文档id添加进docInfo_index中了,所以直接使用即可

在向vector插入时,可以继续使用move

代码
    void create_inverted_index(const docInfo_index &doc) // 以文档为单位{struct word_cnt{int title_cnt_;int content_cnt_;word_cnt() : title_cnt_(0), content_cnt_(0) {}~word_cnt() {}};std::unordered_map<std::string, word_cnt> cnt_map;// 统计每个词在所属文档中的相关性std::vector<std::string> content_words;ns_helper::JiebaUtil::CutString(doc.content_, &content_words);for (auto it : content_words){// 为了实现匹配时忽略大小写,将所有单词转换为小写boost::to_lower(it);++cnt_map[it].content_cnt_;}std::vector<std::string> title_words;ns_helper::JiebaUtil::CutString(doc.title_, &title_words);for (auto it : title_words){boost::to_lower(it);++cnt_map[it].title_cnt_;}// 计算权值
#define title_count 10
#define content_count 1for (const auto &it : cnt_map){word_info t;t.doc_id_ = doc.doc_id_;t.word_ = it.first;t.weight_ = (it.second).title_cnt_ * title_count + (it.second).content_cnt_ * content_count;inv_index_[t.word_].push_back(t); // 插入的是小写单词}}

 

搜索引擎模块     

索引对象单例化                                                

搜索模块中一定要包含前面的索引结构

  • 所以我们必须要构建索引对象

并且,索引结构在我们项目运行时,只存在一份就够了

  • 所以我们要索引设置为单例模式

设置单例模式时

  • 需要将构造函数私有化
  • 禁掉拷贝构造和赋值重载
  • 创建对象时,一定要加锁,否则可能会有并发问题导致创建出多个对象

为了解决效率问题(每个要获取对象的线程,都要经历加锁解锁)

  • 在外层再包一层判断,指针为空再加锁

搜索

流程

在搜索逻辑中,需要传入用户输入的内容,以及一个可以接收搜索结果的变量

  • 这个字符串肯定是要分词的,于是我们得到词的集合

搜索时需要将所有词进行搜索,先捋顺一下这里的逻辑:

  • 根据词,在倒排索引中找到对应的倒排拉链,然后就能拿到包含该词的文档id
  • 根据文档id,在正排索引中就能找到对应的文档内容

然后将结果合并起来返回给浏览器(合并起来后,需要对这些文档进行相关性排序,按照降序的方式,将高相关性显示在前面)

  • 其他搜索引擎也是这样做的

有了返回内容,还需要根据这些内容构建json串 -- jsoncpp

忽略大小写

在对分词结果进行遍历时,因为需要实现忽略大小写

  • 所以我们前面提到,我们计划是让存入内存的都是小写字符,在搜索时也将用户输入转换为小写,这样就可以实现大小写忽略
  • 所以这里要对分出来的词进行大小写转换

那这样的话,这里查询的都是小写字符

  • 确实可以成功匹配到文档(因为我们建立倒排索引时,就都是小写)

但我们后面在构建结果字符串时

  • 插入到文档对象中的content,依然是原单词
  • 到时候我们如果想显示文档内容的话,是得显示关键字周围的文字,如果我们依然选择这样的方式,到时候在content中查找关键字时就匹配不上了
  • 所以,我们可以借助stl库中的search接口或是boost库中的接口 -- 在content中查找摘要时,将原文中的字符转成小写后,再和我们的关键字进行匹配

查询倒排索引
得到集合

完成上述过程后,就该找到词对应的倒排拉链

  • 当然,这个拉链也可能不存在,也就是没有包含该词的文档
  • 也很正常,因为我们做的只是一个站内搜索

因为我们这里有多个词

  • 每个词都可能对应多个倒排拉链中的元素(也就是对应多个文档)
  • 同时,也可能存在多个词对应同一个文档,那岂不是就得拿到很多份同样的文档?
  • 或者没有这么极端,总之搜索结果是可能会有重复的
  • 所以,我们还需要在这个基础上进行改进
去重

如果搜到重复内容,说明输入内容有多处和该文档匹配,也就是多个分词对应一个文档id

  • 说明相关性更高,那权重就应该更高
  • 所以,应该将重复结果合并起来,并将权值相加

我们可以定义一个和关键字信息类似的新结构,里面存放的是一组关键字

  • 这样就可以表示一个文档id->多个分词的映射关系

我们在遍历[通过关键字找到的倒排拉链]的同时,查询在新结构中是否含有该文档的映射关系(用倒排拉链元素中的文档id)

  • 如果没有,就新建,调用我们定义的构造,将权值初始化为0,并在后续流程中,将当前文档的id和权值赋值给新创建的对象
  • 如果有,权值相加,并加入关键字
  • 这块的代码是可以重复利用的,所以不需要加判断

于是,就可以定义一个新结构来保存得到的不重复的倒排拉链结点

  • 插入到新结构,依然可以使用move来提高效率
  • 注意,遍历时不能将元素定义成const,这样和我们使用移动语义相违背

在后续进行构建json串时,因为可能会有多个关键字

  • 所以可以选取第一个关键字周围的文字作为摘要就行
排序

因为unordered_map中的元素没法进行排序

  • 所以我们将这个结构转换为vector后再进行

然后,我们需要将这些不重复的结点,根据相关性进行排序,这样我们在后续可以直接构建json串,而不用考虑权重

  • 可以直接使用sort,然后我们自定义让它根据对象中的weight字段进行降序排序

查询正排索引
引入jsoncpp

接下来就可以根据倒排索引查询出来的不重复集合,进行正排索引的查询

  • 很简单,调用我们定义好的接口即可
  • 查到文档内容后,通过jsoncpp完成序列化/反序列化

因为我们最终是要通过http协议,将查到的结果以网页的形式发送给浏览器,让浏览器来解析

  • 这就涉及到cv端通信时的粘包问题
  • 为了解决粘包问题,我们才引入了自定义协议,而序列化/反序列化就是自定义协议的一种方式
  • 其中,json库是现成的序列化工具,我们也曾经也使用过,并且手动序列化过 --网络计算器代码编写+注意点(序列化,反序列化,报头封装和解包,服务端和客户端,计算),客户端和服务端数据传递流程图,守护进程化+日志重定向到文件_计算器封装-CSDN博客网络计算器(使用json序列化/反序列化,条件编译,注意点),json介绍+语法介绍_json序列化和反序列化工具-CSDN博客

安装jsoncpp -- sudo yum install jsoncpp jsoncpp-devel

  • 使用时带上头文件 -- #include <jsoncpp/json/json.h>
  • 编译时需要带上-ljsoncpp

将jsoncpp引入到我们的项目后,将需要在网页上显示的部分,插入到value对象中

截取文档摘要

这里有个注意点:

  • 我们一般搜出来的结果网页中,文章内容都只是一小部分,所以还需要特殊处理下

显示给用户的内容,基本都是包含关键字的那部分

  • 所以我们可以截取[关键字首次出现时周围的文字]

假设截取关键字之前的50字节(如果不足,就从开头截取),之后到100字节(不足就到结尾结束)

  • 注意:如果我们使用无符号整数作为截取子串的头尾指针,那么就要注意不要做减法运算,因为负数+无符号->很大的整数,可能会导致结果出错(比如,left>right)

因为我们是按顺序遍历容器中的元素的

  • 这些元素已经经过了排序,json串显示的时候也是按插入顺序显示的
  • 所以,插入到value对象后,就已经是将相关性强的结果放在了前面,完成了我们想要实现的目标

 

代码 
  void search(const std::string &data, std::string *json){// 进行分词std::vector<std::string> words;ns_helper::JiebaUtil::CutString(data, &words);// 得到不重复的文档集合struct words_info{std::vector<std::string> words_; // 多个分词可以在某文档中查找到doc_id_t doc_id_ = 0;int weight_ = 0; // 这个词在文档中的权重};std::unordered_map<doc_id_t, words_info> Non_duplicate_map;for (auto word : words){boost::to_lower(word);inverted_zipper zipper;index_->search_inverted_index(word, zipper);for (auto &it : zipper){Non_duplicate_map[it.doc_id_].doc_id_ = it.doc_id_;Non_duplicate_map[it.doc_id_].weight_ += it.weight_;Non_duplicate_map[it.doc_id_].words_.push_back(std::move(it.word_));}}// 将文档集合转换类型(转成vector方便排序)std::vector<words_info> doc_map;for (auto &it : Non_duplicate_map){doc_map.push_back(std::move(it.second));}// 按相关性排序std::sort(doc_map.begin(), doc_map.end(),[](const words_info &x, const words_info &y){ return x.weight_ > y.weight_; });// 查询正排索引,并构建json串Json::Value root;for (const auto &it : doc_map){docInfo_index doc;if (!index_->search_positive_index(it.doc_id_, doc)){continue;}Json::Value item;item["title"] = doc.title_;item["desc"] = get_desc(doc.content_, it.words_[0]);item["url"] = doc.url_;root.append(item);}// Json::FastWriter writer;Json::StyledWriter writer;*json = writer.write(root);}std::string get_desc(const std::string &content, const std::string &word){
#define left_count 50
#define right_count 80// ssize_t pos = content.find(word);// 要实现大小写匹配,查找时要将原文也转换为小写auto iter = std::search(content.begin(), content.end(), word.begin(), word.end(), [](int x, int y){ return (std::tolower(x) == std::tolower(y)); });if (iter == content.end()){return "None1";}int pos = std::distance(content.begin(), iter);// 找范围ssize_t left = 0, right = content.size() - 1;if (pos > left_count + left){left = pos - left_count;}if (pos + word.size() + right_count < right){right = pos + word.size() + right_count;}if (left >= right){return "None2";}return content.substr(left, right - left);}

服务器模块

cpp-httplib

git clone https://github.com/yhirose/cpp-httplib.git

注意事项

  • 需要使用较新版本的gcc 7以上
  • 可以使用devsettool升级gcc

如果gcc不是特别新的话,不能使用最新的cpp-httplib,推荐使用0.7.15

  • 在标签中可以选择下载版本

然后在项目目录下创建指向该第三方库的软链接

  • 使用时,只需要包含头文件和系统pthread库的头文件即可,不需要在编译语句中指明使用了cpp-httplib

devtoolset

devtoolset是 Red Hat Developer Toolset(RHDTS)的一个组件,用于提供更新的 GNU 开发工具链,包括 GCC、GDB 等

  • software collections(简称scl) 是 CentOS 和 Red Hat 系统中的一个功能,用于在系统中同时安装和使用多个版本的软件包,而不会影响系统默认版本的运行环境
  • devtoolset是其中的一个集合,专注于提供更新的开发工具集
  • 常用于老旧的 CentOS 或 RHEL 系统上,这些系统自带的软件版本可能较老,无法满足现代开发需求
  • 查看工具集 -- ls/opt/rh
  • 启动centos中scl仓库 -- sudo yum install centos-release-scl
  • 安装devsettool -- sudo yum install devtoolset-7
  • 启动工具集 -- scl enable devtoolset-9 bash
  • 这种方式让gcc变成新版本,只在本会话中有效

如果我们想每次登录时自动启动工具集,可以设置进配置文件

  • 最好不要直接写进系统范围的配置文件(/etc/bash)中,这是全局添加,会让所有用户都会执行它
  • 添加进当前用户就可以,登录shell时,~/bash_profile会被自动加载

创建对象

httplib::Server是cpp-httplib中的核心类,用于创建和管理http服务器

设置根目录

作为web服务器,必须要有一个web目录,里面存放所有的网页资源 

  • 所以,需要设置根目录 set_base_dir()
  • 虽然设置了,但因为里面并没有网页,所以依然无法显示
  • 我们一般将默认网页(首页)的名字设置为index.html

 

处理请求

获取请求

我们需要使用Get()来指定在客户端发送 HTTP GET 请求时,服务器应该执行的操作

  • 第一个参数用于指定请求路径
  • 第二个参数是用于获取请求和响应对象,供我们使用 -- 一般会传入一个lambda 表达式作为处理程序

因为我们这里是一个搜索引擎,可以设置让用户在/s下请求时,进行搜索操作

获取搜索内容

搜索需要获得用户输入的内容

  • 而这个内容一般是被http协议的get方法获取,放在url中

并且,用户输入的关键字一般放在wd中,也就是word字段中

  • 所以提取word参数中填充的值,就可以获取到搜索内容

req.has_param() 可以检查请求中是否包含某个查询参数

  • 所以,一旦检查到有,就可以调用get_param_value()获取
调用搜索模块

一旦可以获取用户输入内容,就可以调用我们的search接口

  • 传入用户要搜索的内容,进行搜索
  • 并以输出型参数的形式返回搜索结果

然后使用set_content()

  • application/json的格式输出构建响应,服务器会将这个响应自动发送给对应的客户端

 

代码 

#include "searcher.hpp"
#include "/home/mufeng/cpp-httplib/httplib.h"
#include <pthread.h>#define root_path "../wwwroot"int main()
{Searcher s;httplib::Server svr;svr.set_base_dir(root_path);svr.Get("/s", [&s](const httplib::Request &req, httplib::Response &rsp){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;std::string json_string;s.search(word,&json_string);rsp.set_content(json_string, "application/json"); });svr.listen("0.0.0.0", 8080);return 0;
}

运行结果

 

前端模块

这里就不详细讲了,我也不太懂,等我再研究研究

  • 总之,当我们在网页中加入搜索功能后,我们的搜索引擎就有两个途径实现搜索功能了

代码

<!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;box-sizing: border-box;}/* 将我们的body内的内容100%和html的呈现吻合 */html, body {height: 100%;font-family: 'Arial', sans-serif;background-color: #f4f4f4;}/* 类选择器.container */.container {width: 800px;margin: 15px auto;background-color: #fff;border-radius: 8px;box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);padding: 20px;}/* 复合选择器,选中container 下的 search */.container .search {display: flex;align-items: center;margin-bottom: 20px;}/* input样式 */.container .search input {flex: 1;height: 50px;border: 1px solid #ddd;border-radius: 4px 0 0 4px;padding-left: 10px;font-size: 16px;transition: border-color 0.3s;}.container .search input:focus {border-color: #4e6ef2;outline: none;}/* button样式 */.container .search button {width: 150px;height: 50px;background-color: #4e6ef2;border: none;color: #fff;font-size: 16px;border-radius: 0 4px 4px 0;cursor: pointer;transition: background-color 0.3s;}.container .search button:hover {background-color: #3d5bbf;}/* result样式 */.container .result {margin-top: 20px;}.container .result .item {padding: 15px;margin-top: 10px;border: 1px solid #ddd;border-radius: 5px;background-color: #f9f9f9;transition: transform 0.2s;}.container .result .item:hover {transform: scale(1.02);}.container .result .item a {display: block;font-size: 18px;color: #4e6ef2;text-decoration: none;margin-bottom: 5px;}.container .result .item a:hover {text-decoration: underline;}.container .result .item p {font-size: 14px;color: #666;margin: 5px 0;}.container .result .item i {display: block;font-size: 12px;color: green;}.no-results {font-size: 16px;color: #ff0000;margin-top: 20px;}</style>
</head>
<body><div class="container"><div class="search"><input type="text" placeholder="请输入搜索关键字"><button onclick="Search()">搜索一下</button></div><div class="result"><!-- 动态生成网页内容 --></div></div><script>function Search() {let query = $(".container .search input").val();console.log("query = " + query);$.ajax({type: "GET",url: "/s?word=" + encodeURIComponent(query),success: function(data) {console.log(data);BuildHtml(data);},error: function() {$(".container .result").empty().append('<div class="no-results">请求失败,请重试</div>');}});}function BuildHtml(data) {let resultLabel = $(".container .result");resultLabel.empty();// 确保 data 是数组并检查长度if (!Array.isArray(data) || data.length === 0) {resultLabel.append('<div class="no-results">没有找到结果</div>');return;}for (let elem of data) {let divLabel = $("<div>", { class: "item" });let aLabel, pLabel, iLabel;// 检查是否有标题if (elem.title) {aLabel = $("<a>", {text: elem.title,href: elem.url,target: "_blank"});divLabel.append(aLabel);}// 显示描述pLabel = $("<p>", {text: elem.desc || "无描述"});divLabel.append(pLabel);// 显示链接iLabel = $("<i>", {text: elem.url});divLabel.append(iLabel);resultLabel.append(divLabel);}}</script>
</body>
</html>

运行结果

代码结构

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

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

相关文章

从零创建苹果App应用,不知道怎么申请证书的可以先去看我的上一篇文章

用大家自己的开发者账户&#xff0c;登录进入App Store Connect ,注册自己的应用 进入之后&#xff0c;点击增加 填写相关的信息 一切顺利的话&#xff0c;就可以来到这个页面

智能AI对话绘画二合一源码系统 内置所有大模型的接口 带完整的安装代码包以及搭建部署教程

系统概述 人工智能技术的飞速发展&#xff0c;越来越多的创新应用正在改变着我们的生活。本文将向大家介绍一款集成了智能对话与创意绘画功能的开源项目——“智能AI对话绘画二合一源码系统”。它不仅融合了最新的自然语言处理&#xff08;NLP&#xff09;和计算机视觉技术&am…

AGI 之 【Dify】 之 使用 Docker 在 Windows 端本地部署 Dify 大语言模型(LLM)应用开发平台

AGI 之 【Dify】 之 使用 Docker 在 Windows 端本地部署 Dify 大语言模型&#xff08;LLM&#xff09;应用开发平台 目录 AGI 之 【Dify】 之 使用 Docker 在 Windows 端本地部署 Dify 大语言模型&#xff08;LLM&#xff09;应用开发平台 一、简单介绍 二、Docker 下载安…

线上游戏 线下陪玩线下家政陪聊陪诊陪游系统多少钱

关于线上游戏、线下陪玩、线下家政、陪聊、陪诊、陪游等系统的价格&#xff0c;由于这些服务涉及多个不同的行业和领域&#xff0c;且每个行业内部的定价也会因服务内容、服务质量、服务地区、服务提供商等多种因素而有所不同&#xff0c;因此很难给出一个统一的答案。 一般来…

【Linux】解读信号的本质&相关函数及指令的介绍

前言 大家好吖&#xff0c;欢迎来到 YY 滴Linux系列 &#xff0c;热烈欢迎&#xff01; 本章主要内容面向接触过C的老铁 主要内容含&#xff1a; 欢迎订阅 YY滴C专栏&#xff01;更多干货持续更新&#xff01;以下是传送门&#xff01; YY的《C》专栏YY的《C11》专栏YY的《Lin…

[Linux#62][TCP] 首位长度:封装与分用 | 序号:可靠性原理 | 滑动窗口:流量控制

目录 一. 认识TCP协议的报头 1.TCP头部格式 2. TCP协议的特点 二. TCP如何封装与分用 TCP 报文封装与解包 如何封装解包&#xff0c;如何分用 分离有效载荷 隐含问题&#xff1a;TCP 与 UDP 报头的区别 封装和解包的逆向过程 如何分用 TCP 报文 如何通过端口号找到绑…

HNU-并行算法设计与分析-期末考查报告

《并行算法设计与分析》期末考查题实现与分析报告 by 甘晴void 1 题目重述 Consider a sparse matrix stored in the compressed row format (you may find a description of this format on the web or anysuitable text on sparse linear algebra). Write an OpenMP progra…

UE4 材质学习笔记08(雨滴流淌着色器/雨水涟漪着色器)

一.雨滴流淌着色器 法线贴图在红色通道和绿色通道上&#xff0c;那是法线的X轴和Y轴&#xff0c;在蓝色通道中 我有个用于雨滴流淌的蒙版&#xff0c;在Alpha通道中&#xff0c;有个时间偏移蒙版。这些贴图都是可以在PS上制作做来的&#xff0c;雨滴流淌图可以直接用笔刷画出来…

如何下载3GPP协议?

一、进入3GPP网页 https://www.3gpp.org/ 二、点击“Specifications &Technologies” 三、点击“FTP Server” 网址&#xff1a; https://www.3gpp.org/specifications-technologies 四、找到“latest”&#xff0c;查看最新版 网址&#xff1a; https://www.3gpp.org/ftp…

DeepFM模型预测高潜购买用户

关于深度实战社区 我们是一个深度学习领域的独立工作室。团队成员有&#xff1a;中科大硕士、纽约大学硕士、浙江大学硕士、华东理工博士等&#xff0c;曾在腾讯、百度、德勤等担任算法工程师/产品经理。全网20多万粉丝&#xff0c;拥有2篇国家级人工智能发明专利。 社区特色&…

十一、Linux 之Linux 磁盘分区、挂载

1、linux分区 1.1 原理介 Linux 来说无论有几个分区&#xff0c;分给哪一目录使用&#xff0c;它归根结底就只有一个根目录&#xff0c;一个独立且唯一的文件结构 , Linux中每个分区都是用来组成整个文件系统的一部分。Linux 采用了一种叫“载入”的处理方法&#xff0c;它的整…

Ubuntu22.04环境下源码安装OpenCV 4.8.1

因为项目需要用OpenCV对yolov8模型进行推理&#xff0c;通过DNN模块&#xff0c;之前本地的OpenCV版本是4.5.4&#xff08;好像安装完ROS2 humble之后系统就自带了opencv&#xff09;&#xff0c;加载onnx模型一直报错&#xff0c;网上查询到需要4.7以上&#xff0c;干脆直接升…

sql 语句相关的函数

1. 聚合函数 这些函数用于对一组值进行计算&#xff0c;并返回单个值。 1.COUNT(): 计算行数。count SELECT COUNT(*) FROM students;2.SUM(): 求和。sum SELECT SUM(salary) FROM employees;3.AVG(): 计算平均值。avg SELECT AVG(score) FROM test_scores;4.MAX(): 找到最…

思维,CF 1980E - Permutation of Rows and Columns

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 1980E - Permutation of Rows and Columns 二、解题报告 1、思路分析 我…

Golang | Leetcode Golang题解之第476题数字的补数

题目&#xff1a; 题解&#xff1a; func findComplement(num int) int {highBit : 0for i : 1; i < 30; i {if num < 1<<i {break}highBit i}mask : 1<<(highBit1) - 1return num ^ mask }

邻接矩阵的无向图(C语言代码)

无向图是对称的 所以 &#xff1a; G->matrix[i][j] 1; G->matrix[j][i] 1; AB线段为1的同时 BA的线段也为1 #define _CRT_SECURE_NO_WARNINGS 1 #include<stdio.h> #include<stdlib.h> #define MAXVEX 100//最大顶点数 typedef struc…

一键解锁新技能!2024年电脑录屏神器推荐

咱们现在这个时代&#xff0c;电脑录屏软件就跟手机一样&#xff0c;几乎人人都有。不管是教别人怎么做事&#xff0c;记录开会内容&#xff0c;还是把玩游戏时候的高光时刻分享给朋友&#xff0c;有个好用的录屏软件真的能让事情变得简单很多。今天我就来给你介绍四款2024年超…

解决低版本pytorch和onnx组合时torch.atan2()不被onnx支持的问题

解决这个问题&#xff0c;最简单的当然是升级pytorch和onnx到比较高的版本&#xff0c;例如有人验证过的组合: pytorch2.1.1cu118, onnxruntime1.16.3 但是因为你的模型或cuda环境等约束&#xff0c;不能安装这么高的版本的pytorch和onnx组合时(例如我的环境是pytorch1.12&…

数据结构5——队列

1. 队列的概念及结构 队列的概念&#xff1a; 与栈相比&#xff0c;队列也是一种特殊的线性表&#xff0c;不同的是&#xff0c;队列只允许在一端进行插入数据操作&#xff0c;在另一端进行删除数据操作。队列遵守先进先出 FIFO(First In First Out)的原则。 入队列&#xff1…

Qualitor checkAcesso.php 任意文件上传漏洞复现(CVE-2024-44849)

0x01 漏洞概述 Qualitor 8.24及之前版本存在任意文件上传漏洞,未经身份验证远程攻击者可利用该漏洞代码执行,写入WebShell,进一步控制服务器权限。 0x02 复现环境 FOFA:app="Qualitor-Web" 0x03 漏洞复现 PoC POST /html/ad/adfilestorage/request/checkAcess…