欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

【项目介绍】搜索引擎

程序员文章站 2024-02-27 19:22:03
...


BoostSearch / 搜索引擎

开发语言

C/C++,HTML,CSS,JavaScript

开发环境

CentOS7、vim、g++、gdb、git、Makefile

依赖库

jsoncpp, cppjieba, http-lib, boost

项目简介

BoostSearch是一个基于boost文档的站内搜索引擎,当用户在页面上输入查询词后,就会快速的查询出相关的boost在线文档,弥补了boost在线文档中没有搜索的缺陷。

项目特点

  • 对离线版本的html文档进行解析,将解析结果整理为一个行本文文件。
  • 读取处理好的行文本文件进行分词、权重计算等操作,在内存中构造出正排索引和倒排索引。
  • 对查询词进行分词、触发,依据相关度对查询结果进行排序,并以Json格式进行包装后序列化为字符串返回。
  • 通过HTTP服务器搭载搜索页面, 为外部提供服务。

项目链接

github

演示

【项目介绍】搜索引擎

【项目介绍】搜索引擎


项目介绍与难点分析

【项目介绍】搜索引擎

具体的模块划分以及功能分析我以上图的形式展现了出来,本文主要说明项目中的一些难点以及问题分析,至于具体实现以及其他内容请参考源代码
https://github.com/HONGYU-LEE/BoostSearch

解析

无论是站内搜索引擎还是全网搜索引擎,第一步都必须对获取的文件进行一个预处理

预处理分为两个步骤

  1. 找出所有的HTML文档
  2. 提取出文档中的关键信息

无论是官方提供的文档文件,还是通过爬虫在网上爬取的文件,都会夹杂着许多除了HTML外的例如图片、音乐、测试代码等搜索引擎不需要的东西,所以第一步就是将从这些杂乱的文件中,找出所有的HTML。

而C++标准库中并没有提供一个能够遍历文件的方法,而linux的系统函数dirent也可以做到,但是在我上一个项目MiniFTP中获取列表那一块可以看到,这玩意并不好用,所以这里我用的是Boost库中的filesystem模块。它会通过一个迭代器来递归找到该目录下的所有文件,来达到遍历的效果

找到文件后,第二步就是将文章中的关键信息提取出来

我们需要的信息只有三个,标题、URL、正文

标题:标题的提取方法很简单,只需要找到HTML中title这个标签中的内容即可

URL:URL的处理也很简单,通过查询官方在线文档,可以看到URL的构成就是官方文档地址加上文件名,所以我们只需要提取出之前枚举的文件的名字,将其和文档地址进行组合,就可以获得到URL

正文:接下来我们需要做的,就是过滤掉所有的标签以及标签中的内容,剩下的text内容就是我们需要的正文。并且为了之后方便处理,将所有的换行符全部替换成为空格,将每篇的正文都单独处理成一行。

提取出信息后,就需要对信息进行划分并保存。

我们所要做的,就是将每一篇文档的信息全部汇总到一行中,即处理为一个行文本文件,使得每行数据就对对应着一篇文档,这样我们在后续处理的时候只需要按行来提取出信息,就可以达到遍历所有数据的效果。

接着,还需要通过某种方式来划分开标题、URL、正文

考虑到这是一个行文本文件,换行、空格、回车等符号则直接pass,并且作为分隔符的符号必须要在文章中没有出现过,考虑到这一点,就可以使用不可见字符(文章中不可能出现的字符)

【项目介绍】搜索引擎

在这里我选择的是\1(SOH符号),通过这个符号对所有的标题、URL、正文进行划分。

搜索的本质其实就是找到出现过用户查询内容的文章

要做到这一点其实并不复杂,可以通过比对文章内容以及用户查询词来完成。

所以这时需要进行两个步骤,一个是分词,一个是索引

分词

分词很好理解,就是将用户输入的字符串,分解成为一个一个的关键词,然后再通过关键词去进行内容的查找。

例如对“今天的天气很好啊”这句话进行分词

今天/的/天气/很好/啊

对于我们人来说,由于我们有自主思考的能力,加上了出生至今的经验以及积累,分词对于我们来说是很简单的一件事,但是对于计算机而言,这并不是一件简单的事情。

例如这句出自神雕侠侣中的一句话,需要结合其中的背景来理解,机器则很难理解

“我也想过过儿过过的生活” = 我/也想/过/过儿/过/过的/生活

并且有些句子可能会解析出多种结果

“在北京大学生活区喝进口红酒”

1.在/北京/大学/生活区/喝/进口/红酒

2.在/北京大学/生活区/喝/进口红酒

3.在/北京/大学生活区/喝/进口红酒

并且在句子中会经常出现一些如“的”,“了”,“呢”,“吗”,"像"等等等语气词或者结束词会不可避免的大量出现,但是这些词又没有什么实际上的意义,所以我们将其称为**“暂停词/停用词”**,不会将其放入权重的计算中。

考虑到分词的困难巨大,甚至比我们整个项目都要复杂,所以这里就不再自己造*,而是引入了第三方库结巴分词——https://github.com/yanyiwu/cppjieba

索引

接着就到了整个项目中最核心的部分,索引。

即使本项目是一个站内的搜索引擎,只有8000多篇文档, 但是实际算下来字符数量可能多达千万甚至更多,如果直接进行遍历搜索,那么速度将会慢的吓人。而在庞大的数据中想要快速的查询到指定的数据,那么马上就能想到的方法就是建立索引

索引就相当与我们看书时文章的一个目录,我们可以通过目录上的页数来快速定位到需要查询的数据,所以我们以下两种索引,正排索引倒排索引

正排索引

正排索引的作用即根据文档ID来查找文档信息

//文章信息
struct DocInfo
{
    int64_t doc_id;	//文档ID
    string title;	//文档标题
    string url;		//文档URL
    string content; //文档正文
};

因为我们最终需要查询的结果只需要文档的标题、URL、正文,所以正排索引所存储的文档信息就使用上面这种结构。

vector<DocInfo> forward_index;  //正排索引

接着,就需要建立起ID与文档信息的映射关系,由于文档本身并没有规定固定的ID,所以这里我就直接使用数组下标来作为文档的ID,将其以vector的形式进行存储(如果规定了ID就使用unordered_map)

倒排索引

倒排索引的作用即根据文档片段来查找相关的文档ID

//权重信息
struct Weight
{
    int64_t doc_id;	//文档ID
    int weight; 	//权重
    string word;	//关键词
};
typedef vector<Weight> InvertedList;	//倒排拉链

我使用上面的Weight结构来存储每个关键词所对应出现的文章与该文章的权重大小。

由于一个Weight对应着该词的其中一篇相关文章,所以为了方便查询,我将该词的所有相关文章以一个vector进行存储,构成一个倒排拉链的结构,并将这个倒排拉链作为倒排索引的节点。这样通过关键词来查找到倒排拉链时,就可以通过直接遍历倒排拉链来获取到所有的相关文档。

unordered_map<string, InvertedList> invert_index;   //倒排索引

关键词与相关文档的映射使用哈希表(unordered_map)建立

下面介绍下索引的建立流程

假设我们此时有两篇文章,首先进行分词

【项目介绍】搜索引擎

接着,根据文档信息构建出正排索引

文档ID——》文档信息

【项目介绍】搜索引擎

接着,依据每个关键词出现过的文章,构建出倒排索引

关键词——》相关文档ID

【项目介绍】搜索引擎

建立了这样一个索引体系之后,在查询时,我们只需要对查询词进行分词,再通过倒排索引查询到对应的倒排拉链,权重信息,来为每个倒排拉链的文档进行排序,就可以根据相关度高低以此获取到对应的文档ID,再通过正排索引就可以获取到我们需要的文档信息了。

权重

由于我们存储的数据量不是很大,只有大概8000多个文档,并且该项目只是用于学习搜索引擎原理,所以这里就使用线性公式来进行权重的计算,计算的依据就是关键词的词频

首先用以下结构来统计关键词在文章的正文以及标题中分别出现的次数

struct WordCount
{
    WordCount() 
    : title_count(0)
    , content_count(0)
    {}    
    
    int title_count; //标题出现频率
    int content_count;   //正文出现频率
};

关键词在正文中和标题中出现的权重应该不一样。

因为标题即对正文的高度概括,在标题中出现的内容往往与实际内容相关,而在正文即使出现,也有可能只是顺口一提或者引用,所以我认为在标题中出现的权重应该要比在正文出现要高

所以推导的公式如下:权重 = 标题出现次数 * 10 + 正文出现次数

//权重 = 标题出现次数 * 10 + 正文出现次数
weight.weight = word_pair.second.title_count * 10 + word_pair.second.content_count;

包装

对于查询的结果,由于我们并不需要将所有的信息都展示在搜索页面上,只需要展示关键信息,所以我们需要的内容只有标题、URL、描述。

前两个已经获取了,但是描述还需要我们自己从正文中提取。

关于描述信息的提取,我的思路如下。

  1. 查询关键词在文章中第一次出现的位置,获取接近这个位置的前后内容当作描述信息
  2. 因为大部分描述信息的规律都是先提出关键词,然后对关键词进行解释,所以我们尽量保证关键词的位置在描述片段中靠前
  3. 描述信息要简短,如果内容过长则用省略号代替
  4. 如果关键词未找到,则说明关键词在标题中,直接从头截取一段内容当作描述

根据以上条件,我推导出以下描述信息提取公式

返回内容 = 150字节 = 关键词往前50字节 + 关键词(包括关键词)往后100个字节

  1. 正常情况下,选择关键词位置的前50个字节的位置作为起始位置,返回起始位置往后的150个字节。
  2. 当前面不足50个时,就从起点位置往后直接返回150个字节。当末尾不足时,直接返回到结束位置。如果后面还有数据用省略号表示。
  3. 如果正文中未找到关键词,则说明关键词在标题中,直接从起始位置开始返回150字节

这样,我们就可以构建出最后查询出的结果。

但是我们最终查询的结果并不能直接返回给调用者,还需要对其进行包装

这里我采用的是JSON格式,借助了第三方库JSONCPP(https://github.com/open-source-parsers/jsoncpp)来进行包装

JSON的格式如下

[
    {
        "title" : "标题",
        "url"	: "url",
        "desc"	: "描述"
    }
    
    {
        "title" : "标题",
        "url"	: "url",
        "desc"	: "描述"
    }

    {
        "title" : "标题",
        "url"	: "url",
        "desc"	: "描述"
    }
	..................
]

之后将结果序列化为字符串回复给调用者