资讯详情

基于boost库的搜索引擎

文章目录

  • 一.项目介绍
  • 二.与搜索引擎相关的宏观原理
  • 三.搜索引擎技术栈和项目环境
  • 四.正排索引vs反向索引-搜索引擎的具体原理
  • 五.编写数据去标签和数据清洗模块Parser
    • 1.下载数据源
    • 2.建立项目结构
    • 3.编写Parser基本结构
    • 4.编写枚举文件名模块
    • 5.html编写文件分析模块
      • (1).html文件读取代码代码
      • (2).解析title代码的编写
      • (3).编写标签代码
      • (4).构建URL编写和测试代码
    • 6.文件写入模块编写
  • 六.编写建立索引的模块Index
    • 1.构建索引代码基本结构
    • 2.编写索引代码以建立索引代码
      • (1).正排索引
        • 编写字符串切分代码
      • (2).倒排索引
        • 倒排索引的基本原理
          • cppjieba的使用
        • 编写倒排索引代码
  • 七.编写搜索引擎模块Searcher
    • 1.index单例与searcher基本代码结构
    • 2.Search编写代码
      • (1).获取摘要
  • 八.搜索调试使用的命令行
  • 九.编写http_server模块
    • 1.引入cpp-httplib库
    • 2.http_server代码的编写
  • 十.编写前端模块
    • 1.编写html
    • 2.编写css样式
    • 3.编写js前后端交互
  • 十一.项目的优化
    • 1.发现重复内容问题
    • 2.添加日志
  • 十二.项目扩展方向
下面是我的gitee仓库地址包含本项目的所有代码 https://gitee.com/tie-zhuTX/SearchEngineOfBoostLibrary

一.项目介绍

网上搜索引擎很多,比如百度、搜狗、360搜索、头条新闻客户端。 这些公司生产的搜索引擎是一个非常大的项目,需要很高的技术门槛,所以只有百度在早期做搜索引擎。 这些公司的搜索引擎都属于。其高门槛体现在:如何捕获整个网络相关网页信息,保存整个网络数据,建立相关后端索引模块,这是一项非常大的工作,以后搜索客户关键词排名,设置显示网页相关优先级,网页与网页之间的相关性等。

所以我们自己实现一个完整的搜索引擎是不可能的,不过我们可以写一个简单的搜索引擎——。 站内搜索最典型的代表是我们使用的cplusplusC 标准文档。与全网搜索最大的区别之一是搜索。这意味着数据量更小。

搜索引擎的效果如何?以百度为例,搜索腾讯官网后效果如下: 在这里插入图片描述 可以发现: 1.网页的title:可以让我们知道网页在做什么,有点击功能,点击标题,跳转到目标网站 2.网页内容摘要描述 3.目标网页对应的网站url 我们的搜索引擎以这三个内容为标准。

图片,广告?搜索引擎在广告中的盈利方式,baidu可以卖自己的关键词,谁的钱多,谁的内容更高。搜索头痛-推广,广告

大多数商业公司通过竞价排名获利。

为什么要做boost库的搜索引擎?我们可以进入boost库的官网(boost.org)查看一些 可以发现boost库的官方网站没有真正的搜索功能。虽然官方提供了它,但我们无法搜索某个词boost一些相关的方法,标准库中的一些接口,但我们想看到官方文档的成本相对较高,所以我们可以在网站上进行搜索。

二.与搜索引擎相关的宏观原理

搜索引擎中的数据是如何流动的? 以下是我们项目工作的流程 首先从全网爬到网页,存储在服务器磁盘中,然后处理网页,建立索引。当客户端向我们发送请求时,我们根据客户的关键字进行检索,然后建立一个新的网页返回给用户。由于我国对爬虫有相关法律法规,我们获取网页的方式改为合法下载网页信息,因此我们项目的部分是左边红圈的部分。

三.搜索引擎技术栈和项目环境

本项目所需的技术栈:C/C 、C 11、STL、准标准库Boost(文件处理),jsoncpp(数据交换),cppjieba(分词)、cpp-httplib(构建http服务器)、html5、css、js、jQuery、Ajax

项目环境:Centos7 云服务器、vim/gcc(g )/Makefile、vs2022或vs Code。

四.正排索引vs倒排索引-搜索引擎的具体原理

这是对正排和倒排原理的介绍,让我们了解正排和倒排的特点以及它们在搜索引擎中扮演什么角色?

假设有两个文档:

文档ID 文档内容
1 关羽今天吃了一斤大力丸
2 关羽砍死了外星人

在搜索时,我们使用关键字进行搜索。首先,我们应该对目标文档进行分词(目的:为了便于建立倒排索引和搜索)

  • 文档1:关羽今天吃了一斤大力丸->关羽 /今天/吃/一斤/大力丸/一斤大力丸/一斤
  • 文档2:关羽砍死了外星人->关羽/砍死/外星人

停止词(stopwords):是的,a、the…一般来说,我们在分词时不能考虑。因为这些词的出现频率太高,如果我们保留它们,它们在搜索时没有太大的独特性价值,这将增加我们建立索引甚至搜索的成本。

/tr>
关键词(独一无二) 文档ID、weight(权重)
关羽 文档1、文档2
今天 文档1
文档1
一斤 文档1
大力丸 文档1
一斤大力丸 文档1
砍死 文档2
外星人 文档2

模拟一次查找的过程: 用户输入:关羽->在倒排索引中查找->提取出文档ID(1,2)->根据正排索引->找到文档的内容->title+connect(desc)+url文档结果进行摘要->构建响应结果。

我们发现如果用户输入了关羽,那文档1和文档2都有这个关键字,我是先显示文档1还是文档2呢?所以每一个文档都会有一个权值(weight)

五.编写数据去标签与数据清洗的模块Parser

1.下载数据源

boost 官网:https://www.boost.org/

点击DOCUMENTATION可以看到各种boost相关的库 直接选择最新的 这里就是它按照字典序列排序的内容 这是文档的内容,怎么下载?

回到首页,下载哪里就有最新版的 选择boost_1_79_0.tar.gz

2.建立项目结构

1.在云服务器上建立好项目文件夹

2.在文件夹下输入rz命令,把下载好的boost库上传到云服务器( 上传的时候有可能会出现乱码,这是因为我们传的文件太大导致的,可以加一个-E选项

rz -E 

传上来以后就相当于我们有了对应的网页信息

上传完成后,进行解包解压

解包解压:tar xzf boost_1_79_0.tar.gz

完成后会有一个boost_1_79_0的目录,里面保存的就是所有的boost内容,这里面就是我们在官网里看到的所有内容

我们在网页上查的手册,是在一个boost版本下的/doc/html文件里面。也就是说我们搜索的时候我们只用boost库html里的文件内容。这就是标准库的各种boost组件对应的手册内容,有些也在别的文件里面,暂时先不考虑

有了数据源,在项目里创建一个data目录,把数据拷贝进data目录下的input目录里。

mkdir -p data/input

input里面放的就是数据源,也就是我们要搜索的html文档,可以拷贝进去

cp -rf boost_1_79_0/doc/html/* data/input/

拷贝完成后,boost库对我们就没用了,input里面就包含了各种各样的需要我们做检索,建立索引的各种网页信息了。后续搜索结果是以data/input作为数据源,建立索引,然后自己拼接一些url,构建一个跳转url就可以。 数据建立好以后,我们就要建立我们的第一个模块,构建索引的模块

在工作目录下

touch parser.cc

编写parser文件对网页信息进行去标签动作(数据清洗) 做数据处理,那一定要有原始数据,然后把它变成去标签之后的数据

什么是标签? 用nano随便进入一个.html文件

<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Chapter 45. Boost.YAP</title>
<link rel="stylesheet" href="../../doc/src/boostbook.css" type="text/css">
<meta name="generator" content="DocBook XSL Stylesheets V1.79.1">
<link rel="home" href="index.html" title="The Boost C++ Libraries BoostBook Documentation Subset">
<link rel="up" href="libraries.html" title="Part I. The Boost C++ Libraries (BoostBook Subset)">
<link rel="prev" href="xpressive/appendices.html" title="Appendices">
<link rel="next" href="boost_yap/manual.html" title="Manual">
</head>
<body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF">
<table cellpadding="2" width="100%"><tr>
<td valign="top"><img alt="Boost C++ Libraries" width="277" height="86" src="../../boost.png"></td>
<td align="center"><a href="../../index.html">Home</a></td>
<td align="center"><a href="../../libs/libraries.htm">Libraries</a></td>
<td align="center"><a href="http://www.boost.org/users/people.html">People</a></td>
<td align="center"><a href="http://www.boost.org/users/faq.html">FAQ</a></td>
<td align="center"><a href="../../more/index.htm">More</a></td>
</tr></table>
<hr>
<div class="spirit-nav">
<a accesskey="p" href="xpressive/appendices.html"><img src="../../doc/src/images/prev.png" alt="Prev"></a><a accesskey="u" href="libraries.html"><img src="../../d$
</div>
<div class="chapter">
<div class="titlepage"><div>
<div><h2 class="title">
<a name="yap"></a>Chapter 45. Boost.YAP</h2></div>
<div><div class="author"><h3 class="author">
<span class="firstname">Zach</span> <span class="surname">Laine</span>
</h3></div></div>
<div><p class="copyright">Copyright © 2018 T. Zachary Laine</p></div>
<div><div class="legalnotice">
.........

用<>括起来的就叫做html的标签,标签会被浏览器解释呈现出不同的形态,也就是我们看到的网页信息。但是这个标签对我们进行搜索是没有价值的,所以我们要把他们处理掉。一般标签都是成对出现的。成对出现的标签,比如< head>…< /head>、还有一类是只有<>括起来,这两类我们去标签时都要考虑。

去标签就相当于是一个数据清洗的工作,数据清洗完的结果就可以放在data目录下的raw_html文件

查看我们下载了多少html文件:ls -Rl | grep -E '*.html' | wc -l

一共8000多个文件!

我们的是想把每个文档都去标签,然后写入到同一个文件中,每个文档的内容不换行!用特殊字符‘\3’区分不同文档

为什么用\3?下面是一张ACSII码表 可以看到有的字符属于,是的,有的字符是打印字符。我们获得的文档内容基本都属于打印字符。3对应^C,是不可显示的,所以也就不会污染我们形成的新文档,用别的控制字符也可以

我们要把处理完的内容放在data目录的raw_html目录下,那我们就要有一个文件存放数据,可以用一个raw.txt文件存放处理的结果

3.编写Parser基本结构

首先,我们一定会涉及到读取文件的操作,读取文件之前必须待先把所有带路径的文件名全部罗列出来,然后一个文件一个文件的读。 所以可以先定义一个source_path,表示所有源html文件的路径,定义一个raw_txt表示最终结果要被写入的文件路径+文件名。

要读取文件,那我就要知道文件名和文件路径,所以,我们可以先根据源文件的目录,列举出所有的文件名+路径——我们用函数实现

,我们已经拿到了所有的文件名和对应的地址,就可以一个一个的读取文件,读取文件的目的,就是对文件进行去标签,然后提取出我们需要的内容,那我们就可以定义一个结构体表示每一个文件被解析后的格式,然后把这些被处理过后的文件存在一个vector中组织起来。——我们用函数实现

,文件解析完成,我们就可以把解析好的文件存放到我们指定的raw.txt文件中了,这时就要注意我们自己定义的区分文档的方法。——我们用函数实现

下面是parser代码的基本结构

    1 #include <iostream>
    2 #include <string>
    3 #include <vector>
    4 
    5 //目录下面放的是所以的html网页
    6 const std::string source_path="data/input/";//数据源目录
    7 const std::string raw_txt="data/raw_html/raw.txt";//处理后的文件
    8 
    9 //定义文件被解析的格式
   10 typedef struct Format{ 
        
   11     std::string title;    //文档标题
   12     std::string describes;//文档内容介绍
   13     std::string url;      //文裆网页
   14 }Format_t;
   15 
   16 int main()
   17 { 
        
   18     std::vector<std::string> file_name_list;//用来存放所有的文件名和其路径
   19     //第一步
   20     //递归式的把每个html文件名+路径保存到file_name_list中。方便后期对一个一个的文件进行读取 
E> 21     if(!EnumFile(source_path,&file_name_list));
   22     { 
        
   23         std::cout<<"Enum error"<<std::endl;
   24         return 1;
   25     } 
   26     //第二步
   27     //根据file_name_list读取每个文件,并按照内容进行解析
   28     std::vector<Format_t> result;//用来保存解析后的结果
E> 29     if(!ParseHtml(file_name_list,result)){ 
        
   30         std::cout<<"parse error"<<std::endl;
   31         return 2;
   32     }
   33     //第三步
   34     //把解析完成的各个文件的内容写入到raw.txt中,按照'\3'作为每个文档的分隔符
E> 35     if(!SaveHtml(result,&raw_txt)){ 
        
   36         std::cout<<"save error"<<std::endl;
   37         return 3;
   38     }
   39     return 0;
   40 }   

4.枚举文件名模块的编写

枚举文件名我们使用的是EnumFile函数。

因为C++和STL对文件操作的支持不太好,所以要完成这样的动作,需要使用到boost库的file system模块。

boost开发库的安装

sudo yum install -y boost-devel

这就安装完成了 可以上boost官网查看手册了解boost的相关接口怎么用。注意,我们安装的是1.53的版本,所以要查1.53版本的手册。我们重点要找的是Filesystem 点进去后就是一个filesystem的教程,但是这里面只有一部分的函数。如果想要更详细的了解,可以在下面随便点击一个函数,就可以看到file system库相关的接口说明了

头文件:<boost/filesystem.hpp>

首先我们要定义一个,然后从这个对象的地方开始遍历,那我就要先,使用的方法叫做。 递归遍历文件,可以定义一个boost库的,可以让我们

遍历拿到的文件可能有各种各样的类型,所以我们对这些文件名进行一下筛选,使用的方法是表示 常规文件中,我们只要文件路径后缀为html的文件,所以还需要进行一次过滤,可以用迭代器里的用来提取当前路径字符串,然后用,判断是否符合要求

拿到所以符合的html文件以后,就要把这些文件push到file_name_list中,因为现在是一个path对象,所以要push就要先用path的string方法把对象转换成一个string。 这系列操作完成后,先不急往下写,先测试一下代码有没有问题,可以把每条即将插入到file_name_list的路径打印一下。 以下是EnumFile方法的实现

因为我们使用的boost库,所以编译时除了要加-std=c++11之外,还要加上以下两条

-lboost_system
-lboost_filesystem

编译完成,就可以查看到我们的文件是依赖boost库的 完成之后,运行parser,如果成功打印了所有的html文件,就说明当前代码是没问题的。

查看打印的文件数量:./parser | wc -l

如果还不放心,可以查看打印了多少文件。

接下来就是解析html的编写

5.html文件解析模块的编写

解析html文件我们使用的函数是ParseHtml

要解析文件,肯定要对文件的内容进行遍历,把内容读出来,然后再解析。我们ParseHtml的功能就是通过我们上上面拿到的文件名+路径,对每个文件进行解析,解析成Format_t的形式存放到叫result的vector中。Format_t包含文档的标题,内容和url。

首先,遍历file_name_list中的文件名+路径。 ,可以用一个ReadFile函数把读取文件的所有内容

为了方便管理,可以定义一个Tool.hpp作为工具集,在里面定义一个FileTool类,而ReadFile就可以写成该类的静态成员函数。

读取出来的文件可以放在一个string中,那我们就可以再项目目录下创建一个util.hpp文件,里面存放我们使用过的工具

怎么找到文档的标题? 一般title只有一个,是双标签之间的内容 我们就可以写一个ParseTitle函数从读取的文件中解析出title

提取文档内容,本质就是,只保留网页内容的部分

下面就是ParseHtml的基本结构

这里向result里push解析结果时,这个结果可能会比较大,而直接push会发生一次拷贝,所以可以使用C++的move,以减少拷贝,那么这句代码就可以这样改写 result->push_back(std::move(fmt));

主要包含4个函数,ReadFile:文件读取、ParseTitle:解析标题、ParseDsecribles:解析内容、ParesUrl:解析并构建url。因为这些函数基本都只在我当前文件使用所以我把它们写成静态的。

下面分别对这四个内容进行实现

(1).html文件读取代码的编写

可以使用C++的文件操作对传入的文件名进行读取。 首先初始化一个ifstream对象,以in的方式打开路径对应的文件。 打开完成后,用getline读取输入流中的内容,这里可以哟用while循环,在判断的地方读取,虽然getline的返回值是一个引用对象,而while判断的是bool类型,但是可以这样写的原因是因为返回的对象中重载了强制类型转换。所以就可以通过getline返回引用的方式判断文件结尾。

实现代码如下:

(2).解析title代码的编写

要解析title,其实就是在整个文档里搜索< title> 关键字和< /title>关键字,可以通过string的find方法找到两个关键字的位置,然后第一个title的位置向后移动该关键字的长度,就是我们需要部分的第一个元素的位置,再组合上第二个关键字的起始位置,就可以形成一个左闭右开区间,从而提取出title。 代码如下:

(3).去标签代码的编写

要把文档中双标签,单标签,也就是凡是< xxx >和< /xxx >内部的xxx的内容全部去掉,正常的标签上的数据都保留。 文档读取到string上,就是一个个的字符,而我们字符向后遍历的过程中,要么就是在读取标签的内容,要么就是在读取我们需要的内容,所以实际实现的过程中我们要基于一个来编写。

文件的第一个字符肯定是<,所以

当我的遍历文件内容时,只要碰,就意味着当前的标签被处理完了,就可以

当前标签结束,下一个位置可能是正常内容,也可能是下一个标签的开始,所以当时,要判断当前字符,如果是,则表示即将。如果,则可以把当前字符中。

这里有一点需要注意,我们不想保留原始文件的\n,因为我们想用\n作为html解析之后的文本分隔符,所以这里我们

下面是实现代码

(4).构建URL代码的编写并测试

我们的文档是从官网上下载下来的,而官网的url的路径和我们的路径其实是有一定关联的。

这是我从boost库的官方文档随便打开的一个文件,可以看到他的url是这样的: 而在我们下载的boost文档中,我们也可以在文档里面的doc/html目录下找到该文件 把这些地址罗列下来:

官网:https://www.boost.org/doc/libs/1_79_0/doc/html/accumulators.html

下载的路径:boost_searcher/boost_1_79_0/doc/html/accumulators.html

在项目里的路径:data/input/accumulators.html
//我们把boost文档的html目录下的内容都拷贝进了data/input/目录下

所以我们要拼接的url应该分为两个部分:

url_head = https://www.boost.org/doc/libs/1_79_0/doc/html;
url_tail = [data/input](删除掉)/accumulators.html->/accumulators.html
url= url_head+url_tail;

所以接下来我们的任务就是我们要构建出一个url,那ParserUrl的参数就一定要有我们在外面构建的结构的url成员,并且这是一个输出型参数。

url_head的部分是不变的,所以我们可以把这部分直接写在代码里

要截取我们自己路径下的后半部分,我们可以在文件的路径名截,而所有文件的路径名我们已经在第一步列举文件名是存放在了file_name_list中,把这里的路径传进函数然后用,然后拼接起来即可。

下面是代码:


以上我们就完成了文件解析模块的编写,现在可以对该模块进行测试。可以写一个PrintFmt,把我们解析的结果打印出来

但是如果我们全部打印的化,我们预计打印的量会非常大,也不方便我们观察,所有我们可以设计打印一个或者几个观察以下就可

这是我打印出来的一个文档: 可以看到内容方面是正常的,已经不存在标签的内容了,再用url去官网验证以下 可以基本确认我们的代码是没有问题的。

完成了文件列举和文件解析,下一步就是把我们解析的结果写入到我们的目标文件中。

6.文件写入模块的编写

现在我们要做的就是,数据源和目标文件我们都有了,那其实就是文件操作,但是有几点要注意以下

我们之前的规定是每个文件之间用’\3’间隔,但是我们解析出来的文件包含三个部分,如果把他们放在一起,写的时候没问题,但是读取的时候就很麻烦,那我写的时候,就可以在每一个部分的后面增加标识,而string里面有一种方法叫getline,那为了我们操作方便,我们可以更改一下标识的意义,

文档保存的内容:title/3describes/3url/ntitle/3describes/3url/n......

剩下就是编码工作

首先要打开我们的目标文件,这里我们可以用二进制方式写入,因为二进制方式的最大特点是写入什么,文档里就保存什么,用别的方式也可以

以下是实现代码:


以上我们就完成了对Parser模块的编写,去验证一下raw.txt文件 8171行,对应8171个文件,打开这个文件我们就可以看到这里面就是我们解析之后的结果

六.编写建立索引的模块Index

1.搭建索引代码基本结构

建立索引,那我们就要把正排索引和倒排索引都建立好,所以我们的index里面一定要有正排索引和倒排索引的结果!,可以用一个类来组织索引。

成员变量: 正排索引是通过文档id找到文档,那我们就可以用数组来组织正排索引的结构,数组下标表示文档id,然后用我们定义的文档结构struct Format表示文档,这里的文档结构可以因为要与文档id关联,所以还要在原来三个元素的基础上再加上文档id,而为了防止文档id出现不必要的错误,可以用更大的数据比如

倒排索引是通过关键词找文档id,但是一个关键词可能出现在很多文档id中,如何表达他们的先后,就还需要一个值代表该关键词下的该文档id权重,这三个数据也可以用一个结构体组织起来,可以定义为倒排节点,整个倒排索引就可以用一个组织起来,K代表用来查找关键字,,里面就是某一个关键字能够查找出来的所有的文档id、id的权值等信息组织起来的一个个节点,可以

成员方法 一定要有的两个方法,获得正排索引和获得倒排索引, 正排索引就是通过文档id找到文档内容,所以参数就是文档id,返回的就是该文档id对应的文档,还是用我们前面定义的结构表示文档。 要获得正排索引的方式也非常简单,因为我们构建索引肯定是连续构建的,所以我们,如果合法,那我们直接从我们的正排索引返回id对应的文档即可,如果不合法,就返回空,并打印原因。

就是,所以参数就是要查找的关键字,而关键字与文档id的关系我们已经在上面把他们定义成了倒排拉链,所以我们这里。 实现方法也比较简单,直接用查找方法看能否在哈希表中找到该关键字的倒排拉链,找到就返回,找不到就返回空。

最后,我们还需要的一个方法就是,建立索引需要的肯定就是我们经过Parser处理后的数据了,可以返回一个bool值判断是否建立成功,其实现比较复杂,后面再详细说

结构如下图 (这里的index类名最好首字母大写:Index,方便区分类与对象)

2.建立索引代码的编写

首先我们就要把我们之前处理好的文件打开,因为我们之前已经定义好了文件之间用’\n’分隔,所以直接

建立,我们直接,填写到对应文件的结构中,然后即可

而建立,需要对我们的每一个文档的title,describe,分词的操作我们可以自己完成,也可以

我们的操作就是不断的getline,然后先构建正排。正排完成后

以下就是构建索引代码的基本结构:

(1).正排索引

需要根据我们输入的一行文件内容进行构建,我们构建正排的本质就是构建出一个Format_t,然后把里面的值填好,把这个构建好的数据插入到正排的vector中,而我们正好可以用vector的下标充当我们的文档id

我们要把读到的字符串切分成title,describe,url三个部分,那就可以用一个vector把被切好的三个部分组织起来,那我们现在还需要一个字符串切割的功能,可以定义一个专门负责字符串切割的函数,可以把它放在之前写好的工具集文件中,定义一个类代表,里面

插入时可以用move减少拷贝,提高效率。插入完成后,我们在BuildIndex方法中计划的方法是用刚刚构建好的Format不断的构建倒排。所以我们返回的应该是我们刚刚构建好的Format。 其结构如下:

字符串切分代码的编写

字符串切分的工作可以我们自己写,也可以用,其头文件为< boost/algorithm/string.hpp>

不建议使用strtok接口,虽然也可以完成,但是它会对原始字符串做修改,所以这里使用split

如何使用?

boost::split(result, target,boost::is_any_of("sep"), boost::token_compress_on);
result:分割出来以后是多个结果,所以是vector<string>类型
target:要被分割的内容
is_any_of(""):凡是这里的任何一个字符都作为分隔符
token_compress_on:选项,选择是否需要压缩(比如多个分隔符相连,相邻两个分隔符之间没有内容,
不压缩就认为内容是空,就会保留一个空,压缩就不保留),不写就是默认,等于token_compress_off

下面直接用split完成切分即可

(2).倒排索引

上一步正排完成了对字符串的解析,而这个解析就包含了文件的title,describe,url,file_id的内容,我们现在的任务就是通过这些信息,完成从关键词到倒排拉链的映射,就需要我们对title,describe的内容进行分析,比如统计一下词频等

倒排索引的基本原理

首先我们先列出与倒排相关的几个数据结构

//我们拿到的文档内容
typedef struct Format
{
    std::string title;   //文档标题
    std::string describe;//文档去标签之后的内容
    std::string url;     //文档url
    uint64_t file_id;    //文件id
}Format_t;

//把倒排的关键字和文档id,权重捆绑在一起
struct InvertNode
{
    std::string key_word;
    uint64_t file_id;
    int weight;
};
//倒排拉链
typedef std::vector<InvertNode> InvertList;
//关键字与倒排拉链的映射关系
std::unordered_map<std::string,InvertList> inverted_index;

InvertNode表示的就是关键词与文档之间的关系,我们倒排的最终目的就是要通过文档内容,建立一个或多个InvertNode!! 因为一个文档的标题和内容都可能包含很多词,而每一个词都可能在很多文档中出现

但是由于我们是一个文档一个文档建立的,索引我们每一次建立的应该是当前文档里所有的关键词与当前文档的关系。

而与我们搜索强相关的是就是标题和内容,

如果一个词在文档中的出现次数特别多,那它被搜索时也应该高优先级的搜索出来。也就是词和文档的相关性。 相关性实际在衡量时有多种维度,所以大多数的搜索引擎的相关性设置上非常复杂,并且需要有大量的积累,所以我们这里就简单的用词频设计相关性,一个词的出现频率越高,其相关性就越高。但是我们认为:在标题中出现的词,其相关性会比内容中出现的词更高一些。相关性就是我们设计Format中的weight。

cppjieba的使用

我们分词使用的工具就是cppjieba 这是我下载下来的cppjieba的内容,其test里面有一个demo.cpp,里面就是使用方法 这里面有它各种各样的分词方法,我们使用的是CutForSearch

在目录/cppjieba/dict目录下存放的就是我们jieba库的词库 词库就是决定我们分词的标准,比如那些属于同一种词,如何分。所以使用它时这些词库也要能够被找到,所以这个,可以使用软连接的方法在项目目录下建立对应的软连接

ln -s ~/mycode/cppjieba/dict dict
前面的cppjieba之前的部分是我的jieba库保存的地方

除了dict目录,,也可以建立对应的软连接

ln -s ~/mycode/cppjieba/include/cppjieba cppjieba

cd cppjieba: cp -rf deps/limonp include/cppjieba/

因为在这个项目中除了建立倒排索引需要分词,在我们搜索的时候也需要分词,所以我们可以把jieba分词中。 所以我们就可以直接在工具集中编写一个类JiebaTool,首先包含头文件 “cppjieba/jieba.hpp”;

其使用方法可以借鉴demo,首先要定义一个jieba对象,然后还要包含词库,要注意词库的的路径不能出错(根据自己的情况写),否侧编译可以通过但是运行就会报错!因为我们刚刚已经在当前目录下建立好了软连接,所以词库的路径如下:

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";

首先要有一个jieba对象,然后调用该对象的CutForSearch方法,我们把这些参数传进来就可以,而为了不让我们每一次分词都创建一个jieba对象,可以把该对象定义成静态,如下:

倒排索引代码的编写

在我们循环遍历文件建立正排时,建立完正排的结果都会交给我们,那我们的倒排就可以拿到文档,首先我们要先对标题和内容部分进行分词,并且分词的时候进行词频统计。

那就可先定义一个结构,里面有两个int成员用来表示一个关键词分别在标题于文档中的出现次数,以便于后面我们设置权重。而

然后我们就可以着手用我们封装的jieba分词对文档的标题和内容进行分词,分词完成后

在此之前,还有一点需要注意,我们可以观察一下,搜索引擎在搜索的时候其实是不区分大小写的,所以我们也不区分大小写,那我就可以,并且将来客户输入关键字搜索时,我也要先统一大小写再搜索,可以使用实现,并且

词频统计完成后,我们就要着手建立倒排索引了,我们设计的倒排索引是一个哈希表,是关键词与哈希桶之间的映射。哈希桶中存放的我们叫做节点,节点表示的就是关键词,文档id,和该关键词在该文档中的权重,那我们现在就可以用我们当前文档的信息,构建出当前文档的所有关键词与当前文档id映射和权值,构建好了以后,可以,就可以完成当前文档的所有关键词倒排索引的建立。

代码如下: 以上就完成了index模块的编写

七.编写搜索引擎模块Searcher

1.index单例与searcher基本代码结构

既然index里面已经提供了build方法,那我在searcher里面首先就是执行build任务,build任务的本质就是把磁盘上已经去标签化的文档以索引的形式加载到内存中,首先这是比较大的,再其次我们需要有一个调用的过程,而搜索引擎的索引只有一份就够了,所以我们可以,让searcher直接获取单例就可以了

index要build,是需要那个被处理过的文档的,所以Seacher的初始化首先就需要得到这个文档然后传给index,让index去建立索引。

然后就是Search查找代码的编写,首先我们要拿到用户输入的查找词,一定是一个字符串,然后我们的工作就是通过这个语句,构建出一个json串返回给用户。一共分为4步,首先要对用户的查找内容进行分词,找到里面的关键词,然后根据这些关键字在索引里查找,找到文件id与其权值,然后按照权值将文件进行降序排列,再通过查找的结果,构建一个json串,以便给用户返回结果。

这些是基本方法,后面还会新增方法

然后成员变量处增加一个,考虑到线程安全问题,还要再,所以这个锁也要在,如果指针为空就new一个对象,不为空直接返回对象,完成编写

然后在Seacher模块里,只需要用类域直接调用GetInstance就可以获得单例index,然后使用index的Build方法构建索引

以下是基本结构:

2.Search查找代码的编写

,那就可以引入我们的工具集,调用里面我们写好的jieba分词,把分词的结果用一个vector组织起来

,如果该关键词找不到对应的倒排拉链,则表示该次没有在文件中出现过,继续找下一个词即可,而为了方便我们排序,我们可以

有了整体的倒排拉链,我们再,可以使用sort即可。

排好序之后我就要,然后通过对文档内容进行

安装jsoncpp

sudo yum install -y jsoncpp-devel

头文件

<jsoncpp/json/json.h>

json里有3个类 Value:序列化与反序列化的中间类,要先把原始数据转化成Value类,然后才能转化,可以用append方法把多个Value对象顺序添加到一起

Reader:反序列化,把一个string类型转化成Value类型

Writer:分为两种FastWriter和StyledWriter,使用writer方法就可以把一个Value类型转换成string类型通过返回值返回,第一种是转成了一个一行的字符串,传输比较快,第二种它虽然变成了字符串,但是看起来还是原来的样子

编译时要加-ljsoncpp

通过遍历组合拉链,然后使用正排索引找到每个节点对应的文档内容,再根据每个文档内容,构建Value类型的对象,把title,describe,url都放进去,这里的间接我们只要一部分,不全要,所以需要处理,然后把所以文件的value对象全都append到一个整体的Vaule对象中,直接序列化这个整体的对象即可,因为我们的文档是排好序的,所以将来使用的时候直接直接从头开始用就可以了。

(1).获取摘要

如果把文档内容都加上,那内容太多了,所以这里可以编写一个方法,用来

理论上获取摘可以用一个特别简单粗暴的方法,直接获取文档内容的前一部分,但是前一部分不一定有我们的搜索关键字,最好能在标题和摘要里都凸显一下关键字,所以形成摘要还需要我们传入关键字。我们现在是根据倒排拉链在进行序列化,而倒排拉链的节点里面就包含了该文档的关键字,以次来获取该文档的摘要

搜索关键词位置时,不可以用find,因为find是区分大小写的,而我们建立索引的时候是使用小写建立的索引,并且没有修改原来的内容,如果用find搜,可能会出现大小写不匹配而找不到的情况,所以这里使用C++的一个接口 下面是代码:

八.调试使用的命令行式的搜索

这里可以先对上面的三个模块进行一下命令行测试

首先先创建一个Searcher对象,然后调用其InitSearcher方法初始化。这个初始化就会先获取单例的Index对象,然后根据我们传的处理好的结果建立索引。

然后我们就可以使用一个while循环,让用户输入搜索的内容,然后调用Search方法进行搜索,把结果打印出来。

为了方便观察,可以在Searcher模块获取单例index和索引建立成功后分别打印一条提示,然后在Index模块的BuildIndex函数中,设计一个计数器,然后每建立50个索引就打印一次提示。这样就可以看到代码运行的情况 我如何才能检测打印出来的内容是不是按照我们设置的相关性顺序打印的?

可以在searcher模块里面构建Value时加上权值,然后打印的时候就可以看到 我们可以发现,我们打印出来的权值确实是按顺序排序的,但是如果你用连接去官方文档找,然后自己计算一下会发现打印出来的权值可能会和自己计算的有一些差异,这有两个原因,首先是我们使用的jieba分词工具的分词方法可能和浏览器中分词方法不同,这就会导致最后计算权值时有一些差异,其次,我们的解析模块解析内容时,是先把整个文件都读到了内存,然后又对整个内容进行去标签,而这个去标签后的内容里面也是包含标题的!所以如果一个关键词在标题中出现了,那么它也一定会在内容中出现,也就是说标题中出现过的关键词统计权值时的值会比正常计算多一个。

实际的调试可以根据不同的情况进行,

九.编写http_server模块

1.引入cpp-httplib库

可以用来部署一个http服务,下载方法可以在gitee上直接搜索就可以找到

链接:https://gitee.com/sumert/cpp-httplib?_from=gitee_search

这个库只有一个头文件httplib.h

注意事项:cpp-httplib在使用的时候,而centos 7默认的gcc版本是4.8.5,直接用要么编译不通过,要么就运行时报错,centos为了确保工具集的稳定性,yum默认支持的工具一般都比较老,所以要获得新的编译器要是有scl工具集进行安装。

sudo yum install centos-release-scl scl-utils-build

sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++

上面两步做完就可以使用了,可以在目录:/opt/rh/下看到安装的内容

scl enable devtoolset-7 bash
gcc -v

这个命令行启动,重新登录以后又会回到老版本,要用新版本就待再启动,如果不想每次登录都启动一次,可以设置登录的时候就执行一次启动命令,可以用vim把该命令写在下面这个文件的后面

~/.bash_profile

这是登录的时候默认会执行的一个登录脚本,这就可以保证每次启动的时候都会执行这个命令。(最好不要写在全局)

最新的cpp-httplib在使用时,如果gcc版本不是特别新,也可能导致运行时错误的问题。 在gitee标签里找到历史版本,就可以找到这个版本,可以直接把安装包下载下来。直接拖拽到终端或者使用rz -E命令,把安装包转移到云服务器上

解压:unzip cpp-httplib-v0.7.15.zip

然后可以把库拷贝到项目文件中,也可以建立软连接把库引入到项目文件下

cpp-httplib库的使用方法非常简单,在gitee仓库下面就有各种使用方法 这就完成了一个简单的http server,浏览器访问/hi资源就会返回Hello World!

web服务器需要有一个保存网页资源,所以还要在项目目录下,在里面可以遍历index.html首页信息,然后使用httplib::Server类对象的,()里面填上字符串形式的根目录的路径就可以设计根目录了。

2.http_server代码的编写

首先,然后获取单例index与建立索引。

然后用来构建服务,先调用set_base_dir方法设置web根目录。

然后调用Get方法,第一个参数设置成/s ,表示搜索,第二个参数可以用lambda表达式,第一个参数(我简称为req),表示请求,第二个参数是(我简称为rsp),表示响应。

可以用,如果没有参数,使用rsp对象调用set_content方法给用户返回一条提示信息。

获取到参数以后,可以使用,获取到之后服务端也可以打印一下。

得到用户的搜索内容,就可以了,这里lambda表达式要使用前面定义的变量,要先在捕获列表把search对象进来,调用Search方法前,创建一个字符串存储结果json串,然后r,json串的格式为application/json

然后,把我们的IP和开放的端口设置好即可。 Makefile也要做一下修改

这时候我们再把服务跑起来,在浏览器访问我们的IP+端口,在后面加上/s,如果直接访问,那就回给我们返回没有参数的提示,要加参数,就在后面加上一个?,然后添加word=[要搜索的内容],这时候再访问就可以看到服务端给浏览器返回的搜索结果了。

以上就完成了本项目所有的后端代码的编写。

十.编写前端模块

编写前端我们使用的工具是vscode。 vscode是微软开发的一款编辑器,可以直接百度vscode的官网加载,如果下载很慢的化可以把下载链接替换成国内的镜像,可以参考这篇文章:https://www.zhihu.com/search?type=content&q=vscode%20%E4%B8%8B%E8%BD%BD%E6%85%A2

可以在本地新建文件夹写,写好之后上传到云服务器,也使用remote-ssh插件远程连接云服务器直接把内容写在云服务器上

因为vscode和云服务器是可以同步的,所有在编写前端网页时,可以让服务器一直跑着,然后把网页同步上去,就可以直接在浏览器访问或刷新,比较方便

1.编写html

要包括的内容:标题,输入框,按钮(可以点击),显示搜索的内容的标题,简介和url。

<body>
    <div class= 

标签: 丸型连接器

锐单商城拥有海量元器件数据手册IC替代型号,打造 电子元器件IC百科大全!

锐单商城 - 一站式电子元器件采购平台