boost搜索引擎

1. 项目背景

什么是搜索引擎呢?其实我们平常使用的百度就是搜索引擎,我们把自己想要搜索的内容输入进去,百度就会给我们返回相关的内容,百度一般给我们返回哪些内容呢?如下我们先来看一下。

1.1 搜索引擎基本原理

简单的了解一下搜索引擎的基本原理:

  • 我们给服务器发起请求,例如搜索关键字"boost,服务器拿到请求之后,此时检索自己的资源,然后把结果构成响应发送给我们。

1.2 Boost库

boost库是一个经过千锤百炼、可移植、提供源代码的 C++ 库,作为标准库的后备。他的供能很强大,但是有一个小小的缺陷,它不支持搜索,例如我们想要搜索一个函数,cplusplus库是支持的。


boost库是不支持,但是不知道后面会不会支持。

1.3 项目的目标

下面我们就要说一下我们的项目的目标是什么了,很简单,我们给boost添加一个搜索的功能,这里要说一下,我们服务器上面说了,我们需要搜索资源,可以通过两个方式:

  • 搜索其他的网页资源:这里需要使用爬虫,有一定的技术要求。
  • 把boost下载下来,我们在本地搜索资源。

这里我们使用第二个方式,下载一下boost库。

2. Boost搜索引擎宏观流程

(1)整理数据

  • 把boost库下载下来,此时我们想要把所有的后缀是html的文件进行处理,也就是整理数据(去标签)。如下先看一个简单的html文件,我们需要把其中的title(标题)、content(内容)、url(链接)内容进行保存。
<!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 30. Boost.Process</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="poly_collection/acknowledgments.html" title="Acknowledgments">
 <link rel="next" href="boost_process/concepts.html" title="Concepts">
比特就业课
</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>

(2)构建索引

  • 我们把整理出来的标签构建好索引,为了后期便于查找,这里有许多的细节,后面索引详谈。

(3)处理请求

  • 我们把请求处理好,然后根据索引拿到结果,由于我们的结果很多,这里我们把众多的结果根据权重排好序之后,发送给客户端。

(4)前端页面

  • 根据返回的结果,我们使用前端技术进行处理,让后我们就可以完成这个项目了。

3. 技术栈与环境

3.1 技术栈

后端:C/C++、C++11、STL、boost标准库、Jsoncpp、cppjieba、cpp-httplib。
前端:html5、css、js、jQuery、Ajax。

3.2 环境

  • Centos7远端服务器、vim、gcc(g++)、Makefile、Vscode。

4. 认识什么是索引

下面我们就要了解什么是索引?这里很简单,我们给文档上编号,我们可以根据编号找到唯一确定的文,这就是索引的基本的原理,不过这里的索引分为正排索引和倒排索引。

  • 正排索引:根据编号找到文件,这里的结果是唯一的。
  • 倒排索引:根据关键字,找到文件id,结果不是唯一的。

可以通过一个例子来理解,这里有两个文件:

4.1 正排索引

我们对每一个文件进行编:

文档ID文档名称文档内容
1文档A我的手机牌子是华为的
2文档B我的手机牌子是小米的

这里的正派索引很简单,我们根据文档编号,直接就可以找到文档的内容。

4.2 倒排索引

我们把每一个文档都进行分词,拿出来不重复的词,对于每一个不重复的词,下面都挂着我们的文档的编号。

关键字(具有唯一性)文档ID
我的1,2
手机1,2
牌子1,2
华为1
小米1

倒排索引就是根据关键字拿到我们的文档I。

4.3 我们如何分词?

上面我们说了把文档进行分词,那么我们为何要分词?分词是为了提高查找的效率,那么我们该如何分词呢?我们可以自己手动词,但是已经有大佬给我们编写好了一个库,我们直接使用就可以了,但是如果我们手动分?这里该如何分,很简单.

  • 我的手机牌子是华为的:我的/手机/牌子/是/华为/的。
  • 我的手机牌子是小米的:我的/手机/牌子/是/小米/的。

注意:上面的分词我们随意分的,不一定就是这样的,不过这里我们要谈一下我们一个提高效率的方法。我们发现一个文旦里面的了" 、“从” 、 “吗” , “the” 、 “a” 有的时候意义不是很大,那么我们在分词的时候就可以直接忽略,可以提高我们的效率,像这一种词,我们称为停止词。

4.4 模拟查找过程

下面我们模拟一下查找的流程的:

用户输入:我的 -> 倒排索引中查找 -> 提取出文档ID(1,2) -> 根据正排索引 -> 找到文档的内容 ->title+conent(desc)+url 文档结果进行摘要->构建响应结果。

5. 数据处理

5.1 下载boost库到本地

(1)我们先下载一下boost库,直接使用最新版本的,我这里是1.84.0.我们下载到桌面,然后在centos7下使用指令rz传入远端服务器当中,然后解压一下就可以了。

[xiaomaker@VM-28-13-centos data]$ rz -E

[xiaomaker@VM-28-13-centos data]$ ll
total 141756
-rw-r--r-- 1 xiaomaker xiaomaker 145151722 Feb 13 17:54 boost_1_84_0.tar.gz
[xiaomaker@VM-28-13-centos data]$ tar xzf boost_1_84_0.tar.gz 
[xiaomaker@VM-28-13-centos data]$ ll
total 141760
drwxr-xr-x 8 xiaomaker xiaomaker      4096 Dec  7 05:37 boost_1_84_0
-rw-r--r-- 1 xiaomaker xiaomaker 145151722 Feb 13 17:54 boost_1_84_0.tar.gz
[xiaomaker@VM-28-13-centos data]$ 

(2)查看一下boost库的内容:


这里面就是我们boost库的全部内容,为了我们的项目简单一些,这里我们使用boost里面的doc里面的html目录下的的html文件。如果我们想要搭建所有的html文件,后面可以慢慢的完善。

boost_1_84_0/doc/html
[xiaomaker@VM-28-13-centos doc]$ cd html
[xiaomaker@VM-28-13-centos html]$ ll
total 3080
-rw-r--r--  1 xiaomaker xiaomaker   3476 Dec  7 05:22 about.html
drwxr-xr-x  2 xiaomaker xiaomaker   4096 Dec  7 05:23 accumulators
-rw-r--r--  1 xiaomaker xiaomaker   5858 Dec  7 05:23 accumulators.html
drwxr-xr-x  2 xiaomaker xiaomaker   4096 Dec  7 05:23 align
-rw-r--r--  1 xiaomaker xiaomaker   4440 Dec  7 05:23 align.html
drwxr-xr-x  2 xiaomaker xiaomaker   4096 Dec  7 05:23 any
-rw-r--r--  1 xiaomaker xiaomaker   9102 Dec  7 05:23 any.html
drwxr-xr-x  3 xiaomaker xiaomaker   4096 Dec  7 05:23 array
-rw-r--r--  1 xiaomaker xiaomaker   8377 Dec  7 05:23 array.html
-rw-r--r--  1 xiaomaker xiaomaker  36597 Dec  7 05:27 array_types.html
-rw-r--r--  1 xiaomaker xiaomaker 288197 Dec  7 05:26 asio_HTML.manifest
-rw-r--r--  1 xiaomaker xiaomaker   6685 Dec  7 05:32 Assignable.html
-rw-r--r--  1 xiaomaker xiaomaker    700 Dec  7 05:02 atomic.html
-rw-r--r--  1 xiaomaker xiaomaker  20627 Dec  7 05:27 auxiliary.html
drwxr-xr-x  2 xiaomaker xiaomaker   4096 Dec  7 05:02 bbv2
-rw-r--r--  1 xiaomaker xiaomaker    640 Dec  7 05:02 bbv2.html
-rw-r--r--  1 xiaomaker xiaomaker   9635 Dec  7 05:32 BidirectionalIterator.html
drwxr-xr-x 39 xiaomaker xiaomaker   4096 Dec  7 05:32 boost
...

(3)下面我们要做的就是就是把boost_1_84_0/doc/html里面的所有内容保存到data/input文件中。

[xiaomaker@VM-28-13-centos boost_searcher]$ mkdir data/input -p
[xiaomaker@VM-28-13-centos boost_searcher]$ cp -rf ../../data/boost_1_84_0/doc/html/* data/input/

这样就成功将boost_1_84_0/doc/html的内容拷贝到了data/input当中。

接下来就可以去标签了,创建一个.cpp文件编写parser。

[xiaomaker@VM-28-13-centos boost_searcher]$ touch parser.cpp

5.2 认识标签

(1)在谈去标签之前,我们需要先认识一下标签,我们随便打开的一个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">    
<meta name="viewport" content="width=device-width, initial-scale=1">    
</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>  

(2)像这种由<>包含的就是标签,一般而言,标签是成对出现的。这些标签对我们搜索是没有价值的。我们需要把它给清除了,对与清除后的数据我们也保存在一个文件当中。

[xiaomaker@VM-28-13-centos boost_searcher]$ mkdir data/raw_html -p
[xiaomaker@VM-28-13-centos boost_searcher]$ cd data/
[xiaomaker@VM-28-13-centos data]$ ll
total 20
drwxrwxr-x 58 xiaomaker xiaomaker 16384 Feb 13 18:02 input     // 这里保存源html
drwxrwxr-x  2 xiaomaker xiaomaker  4096 Feb 13 18:15 raw_html  // 这里保存去标签后的html
[xiaomaker@VM-28-13-centos data]$

下面说一下我们该如何保存这些清除标签后的文档内容,首先看一我们源html文件有多少个。

(3)这里我们可以对每一个源html都创建一个文件,但是这样文件就非常多了,不如我们把所有的文档去标签之后结果放在一个文件中,文件与文件之间使用’\3’隔开。就像下面的格式:

XXXXXXXXXXXXXXXXX\3YYYYYYYYYYYYYYYYYYYYY\3ZZZZZZZZZZZZZZZZZZZZZZZZZ\3

解释一下为什么使用’\3’为分隔符:因为在ASCII表中,控制字符是不可显示字符,即无法打印。在我们获取的文档内容(即data/input中的html网页文件)中,里面基本上都是可打印字符,基本上不会有不可显示的控制字符。如此以来也就不会污染我们的文档内容啦。

不过我们不使用上面的格式,这里我们想办法把一个文档的’\n’全部去掉。然后我们使用这样的格式:

类似:title\3content\3url \n title\3content\3url \n title\3content\3url \n ...
方便我们getline(ifsream, line),直接获取文档的全部内容:title\3content\3url

(4)我们创建一个文件来保存我们去标签之后的内容。

[xiaomaker@VM-28-13-centos data]$ cd raw_html/
[xiaomaker@VM-28-13-centos raw_html]$ touch raw.txt
[xiaomaker@VM-28-13-centos raw_html]$ ll
total 0
-rw-rw-r--. 1 xiaomaker xiaomaker 0 Feb 14 20:17 raw.txt

5.3 清除标签的整体框架

(1)parser.cpp的简单框架:

#include <iostream>
#include <string>
#include <vector>
#include <boost/filesystem.hpp>

const std::string src_path = "data/input"; //是一个目录,里面放的是所有的html网页
const std::string output = "data/raw_html/raw.txt";  //将上面的网页去标签后存放在该目录下

typedef struct DocInfo
{
    std::string title;    //文档的标题
    std::string content;  //文档内容
    std::string url;      //读文档在官网中的url
}DocInfo_t;

//函数声明
bool EnumFile(const std::string& src_path, std::vector<std::string>& file_list);
bool ParseHtml(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文件名带路径保存到file_list当中,方便后期进行一个一个文件进行读取
    if(!EnumFile(src_path, file_list))
    {
        std::cerr << "enum file name error" << std::endl;
        return 1;
    }

    //第二步:按照file_list读取每个文件的内容,并进行解析
    std::vector<DocInfo_t> results;
    if(!ParseHtml(file_list, results))
    {
        std::cerr << "parse html error" << std::endl;
        return 2;
    }

    //第三步:把解析完毕的文件写入到output,按照\3作为每个文档的分隔符
    if(!SaveHtml(results, output))
    {
        std::cerr << "save html error" << std::endl;
        return 3;
    }

    return 0;
}

基本思路:

  1. 拿到我们所有的源html文件名,然后把这些文件名保存在一个数组中。
  2. 依次遍历数组,把文件进行去标签,然后把去掉的内容整理成一个DocInfo_t结构体,里面保存title、content、url的结果并存放到一个数组中。
  3. 遍历结构体数组,然后把内容写入到我们的目的文件中,按照一定的格式。

(2)实现如上三个函数我们可以安装boost库,使用boost当中的接口实现:

[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install -y boost-devel
[sudo] password for xiaomaker: 

简单认识一下boost库,如下是使用手册:

5.4 EnumFile函数的实现

EnumFil函数的功能是把我们给定src_path目录下的所有后缀是html的文件名字给保存下了,存在在一个file_list数组当中。

bool EnumFile(const std::string& src_path, std::vector<std::string>& file_list);

(1)具体的实现:

bool EnumFile(const std::string& src_path, std::vector<std::string>& file_list)  //第一步
{
    namespace fs = boost::filesystem;
    fs::path root_path(src_path);
    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))  //判断文件是否是普通文件  .html文件都是普通文件
        {
            continue;
        }

        if(iter->path().extension() != ".html")   //判断是否是以.html结尾
        {
            continue;
        }
        //std::cout << "debug: " << iter->path().string() << std::endl;
        //当前路径是合法的,以.html结束的普通文件
        file_list.push_back(iter->path().string());  //将所有带路径的html保存到file_list。方便后续进行文本分析
    }

    return true;
}

(2)代码解析:

(3)运行一下看看结果是否正确:

[xiaomaker@VM-28-13-centos boost_searcher]$ ./parser 
debug: data/input/about.html
debug: data/input/accumulators/user_s_guide.html
debug: data/input/accumulators/acknowledgements.html
debug: data/input/accumulators/reference.html
debug: data/input/accumulators.html
...

如上就说明成功了。

5.5 ParseHtml函数的实现

我们要开始解析我们的每一个html目录。

bool ParseHtml(const std::vector<std::string>& file_list, std::vector<DocInfo_t>& results);

(1)如下是ParseHtml函数的框架:

bool ParseHtml(const std::vector<std::string>& file_list, std::vector<DocInfo_t>& results)  //第二步
{
    for(auto& file : file_list)
    {
        //1. 读取文件. Read();
        std::string result;
        if(!ns_util::FileUtil::ReadFile(file, result))
        {
            continue;
        }

        DocInfo_t doc;

        //2. 解析指定的文件, 提取title
        if(!ParseTitle(result, &doc.title))
        {
            continue;
        }

        //3. 解析指定的文件, 提取content
        if(!ParseContent(result, &doc.content))
        {
            continue;
        }

        //4. 解析指定的文件, 构建url
        if(!ParseUrl(file, &doc.url))
        {
            continue;
        }

        //完成了解析任务,当前文档的相关结果都保存在了doc当中
        results.push_back(std::move(doc));  //效率可能会很低,加上move就是移动构造,减少拷贝
        //ShowDoc(doc);
        //break;
    }

    return true;
}

(2)ParseHtml函数的大致流程:

  • 对于每一个文件,我们把它读取到一个字符串中。
  • 根据字符串拿到title。
  • 根据字符串拿到content。
  • 根据字符串拿到url。

下面我们分别实现这些函数的功能。

5.5.1 实现读取文件内容的ReadFile函数

(1)对于该函数,我们可以把它放在一个工具集(util.hpp)当中,因为后面可能其它函数会使用到。

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <fstream>

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())
            {
                LOG(FATAL, "open file" + file_path + "error");
                //std::cerr << "open file" << file_path << "error" << std::endl;
                return false;
            }
            
            std::string line;
            while(std::getline(in, line))  //如何判断一个文件读取结束呢??getline的返回值是&,但是whlie(bool),那是因为重载了强转
            {
                out += line;
            }

            in.close();
            return true;
        }
    };
}

(2)代码解析:

5.5.2 实现提取titile的函数ParseTitle

(1)看看html文件的内容,title是在一个标签里面。

(2)根据字符串来进行提取title。

bool ParseTitle(const std::string& file, std::string* title)  //解析指定的文件, 提取title
{
    size_t begin = file.find("<title>");
    if(begin == std::string::npos)
    {
        LOG(FATAL, "获取<title>字符串失败");
        return false;
    }

    size_t end = file.find("</title>");
    if(end == std::string::npos)
    {
        LOG(FATAL, "获取</title>字符串失败");
        return false;
    }

    begin += std::string("<title>").size();
    if(begin > end)
    {
        LOG(FATAL, "获取的下标不正确");
        return false;
    }

    *title = file.substr(begin, end - begin);

    return true;
}

(3)代码解析:

5.5.3 实现提取content的函数ParseContent

我们获取content,不是把所有的内容都拿出来,而是要去标签,这里需要借助一个状态。

(1)我们知道标签是<>这样的表示。那么我们这里使用一个状态。我们默认第一个字符是<。

static bool ParseContent(const std::string& file, std::string* content)  //解析指定的文件, 提取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;
}

(2)代码解析:

5.5.4 实现提取url函数ParseUrl

(1)boost库的官方文档,和我们下载下来的文档,是有路径的对应关系的。如下:

官网url:https://www.boost.org/doc/libs/1_84_0/doc/html/accumulators.html
我们下载下来的url:boost_1_84_0/doc/html/accumulators.html

我们拷贝到我们项目中的样例:data/input/accumulators.html
我们把下载下来的boost库当中的 doc/html/* 拷贝到了data/input/

url_head = “https://www.boost.org/doc/libs/1_84_0/doc/html”;这是固定的
url_tail = data/input /accumulators.html 转换成 url_tail = /accumulators.html

url = url_head + url_tail就相当于形成了一个官网链接。

(2)具体实现:

static bool ParseUrl(const std::string& file_path, std::string* url)  //解析指定的文件, 构建url
{
    std::string url_head = "https://www.boost.org/doc/libs/1_84_0/doc/html";
    std::string url_tail = file_path.substr(src_path.size());

    *url = url_head + url_tail;
    return true;
}

(3)代码解析:

(4)使用如下函数检测是否构建成功:

void ShowDoc(DocInfo_t& doc)
{
    std::cout << "title: " << doc.title << std::endl;
    std::cout << "content: " << doc.content << std::endl;
    std::cout << "url: " << doc.url << std::endl;
}

(5)测试结果:

title: Struct template result&lt;This(InputIterator, InputIterator)&gt;
content: Struct template result&lt;This(InputIterator, InputIterator)&gt;HomeLibrariesPeopleFAQMoreStruct template result&lt;This(InputIterator, InputIterator)&gt;boost::proto::functional::distance::result&lt;This(InputIterator, InputIterator)&gt;Synopsis// In header: &lt;boost/proto/functional/std/iterator.hpp&gt;template&lt;typename This, typename InputIterator&gt; struct result&lt;This(InputIterator, InputIterator)&gt; {  // types  typedef typename std::iterator_traits&lt;      typename boost::remove_const&lt;        typename boost::remove_reference&lt;InputIterator&gt;::type      &gt;::type    &gt;::difference_type type;};Copyright © 2008 Eric Niebler        Distributed under the Boost Software License, Version 1.0. (See accompanying        file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)      
url: https://www.boost.org/doc/libs/1_84_0/doc/html/boost/proto/functional/distance/resu_1_3_32_5_26_2_1_1_2_4.html

(6)我们可以将拿到的这个url去官网上看看是不是正确的:

5.6 SaveHtml函数的实现

bool SaveHtml(const std::vector<DocInfo_t>& results, const std::string& output);

(1)我们已经得到每一个文件的结构体了,现在开始保存文件到要求的文件当中:

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())
    {
        LOG(FATAL, "open: " + output + "failed");
        //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());
    }

    out.close();
    return true;
}

(2)代码解析:

(3)验证是否保存成功:

(4)验证是否保存完全:

[xiaomaker@VM-28-13-centos boost_searcher]$ ls ./data/input/ -Rl | grep -E "*.html" | wc -l
8586
[xiaomaker@VM-28-13-centos boost_searcher]$ cat ./data/raw_html/raw.txt | wc -l
8586
[xiaomaker@VM-28-13-centos boost_searcher]$

6. 建立索引

下面我们就要建立索引了,建立索引实际上就是构建存储+搜索的数据结构,来加快我们对于关键字->文档ID->文档内容的搜索过程。根据上面了解,我们建立正派索引和倒排索引。
在建立索引之前我们需要安装jieba这个分词工具来帮助我们分词。

6.1 jieba的安装与使用

(1)对于分词,我们可以直接使用cppjieba分词工具即可。我们执行下面的命令将github上面的jieba库下载到本地。

[xiaomaker@VM-28-13-centos jieba]$ git clone git clone https://gitcode.net/qq_55172408/cppjieba.git

如下是cppjieba的具体内容:

[xiaomaker@VM-28-13-centos jieba]$ tree cppjieba/
cppjieba/
├── appveyor.yml
├── ChangeLog.md
├── CMakeLists.txt
├── deps
│   ├── CMakeLists.txt
│   ├── gtest
│   │   ├── CMakeLists.txt
│   │   ├── include
│   │   │   └── gtest
│   │   │       ├── gtest-death-test.h
│   │   │       ├── gtest.h
│   │   │       ├── gtest-message.h
│   │   │       ├── gtest-param-test.h
│   │   │       ├── gtest-param-test.h.pump
│   │   │       ├── gtest_pred_impl.h
│   │   │       ├── gtest-printers.h
│   │   │       ├── gtest_prod.h
│   │   │       ├── gtest-spi.h
│   │   │       ├── gtest-test-part.h
│   │   │       ├── gtest-typed-test.h
│   │   │       └── internal
│   │   │           ├── gtest-death-test-internal.h
│   │   │           ├── gtest-filepath.h
│   │   │           ├── gtest-internal.h
│   │   │           ├── gtest-linked_ptr.h
│   │   │           ├── gtest-param-util-generated.h
│   │   │           ├── gtest-param-util-generated.h.pump
│   │   │           ├── gtest-param-util.h
│   │   │           ├── gtest-port.h
│   │   │           ├── gtest-string.h
│   │   │           ├── gtest-tuple.h
│   │   │           ├── gtest-tuple.h.pump
│   │   │           ├── gtest-type-util.h
│   │   │           └── gtest-type-util.h.pump
│   │   └── src
│   │       ├── gtest-all.cc
│   │       ├── gtest.cc
│   │       ├── gtest-death-test.cc
│   │       ├── gtest-filepath.cc
│   │       ├── gtest-internal-inl.h
│   │       ├── gtest_main.cc
│   │       ├── gtest-port.cc
│   │       ├── gtest-printers.cc
│   │       ├── gtest-test-part.cc
│   │       └── gtest-typed-test.cc
│   └── limonp
│       ├── ArgvContext.hpp
│       ├── BlockingQueue.hpp
│       ├── BoundedBlockingQueue.hpp
│       ├── BoundedQueue.hpp
│       ├── Closure.hpp
│       ├── Colors.hpp
│       ├── Condition.hpp
│       ├── Config.hpp
│       ├── FileLock.hpp
│       ├── ForcePublic.hpp
│       ├── LocalVector.hpp
│       ├── Logging.hpp
│       ├── Md5.hpp
│       ├── MutexLock.hpp
│       ├── NonCopyable.hpp
│       ├── StdExtension.hpp
│       ├── StringUtil.hpp
│       ├── Thread.hpp
│       └── ThreadPool.hpp
├── dict
│   ├── hmm_model.utf8
│   ├── idf.utf8
│   ├── jieba.dict.utf8
│   ├── pos_dict
│   │   ├── char_state_tab.utf8
│   │   ├── prob_emit.utf8
│   │   ├── prob_start.utf8
│   │   └── prob_trans.utf8
│   ├── README.md
│   ├── stop_words.utf8
│   └── user.dict.utf8
├── include
│   └── cppjieba
│       ├── DictTrie.hpp
│       ├── FullSegment.hpp
│       ├── HMMModel.hpp
│       ├── HMMSegment.hpp
│       ├── Jieba.hpp
│       ├── KeywordExtractor.hpp
│       ├── limonp
│       │   ├── ArgvContext.hpp
│       │   ├── BlockingQueue.hpp
│       │   ├── BoundedBlockingQueue.hpp
│       │   ├── BoundedQueue.hpp
│       │   ├── Closure.hpp
│       │   ├── Colors.hpp
│       │   ├── Condition.hpp
│       │   ├── Config.hpp
│       │   ├── FileLock.hpp
│       │   ├── ForcePublic.hpp
│       │   ├── LocalVector.hpp
│       │   ├── Logging.hpp
│       │   ├── Md5.hpp
│       │   ├── MutexLock.hpp
│       │   ├── NonCopyable.hpp
│       │   ├── StdExtension.hpp
│       │   ├── StringUtil.hpp
│       │   ├── Thread.hpp
│       │   └── ThreadPool.hpp
│       ├── MixSegment.hpp
│       ├── MPSegment.hpp
│       ├── PosTagger.hpp
│       ├── PreFilter.hpp
│       ├── QuerySegment.hpp
│       ├── SegmentBase.hpp
│       ├── SegmentTagged.hpp
│       ├── TextRankExtractor.hpp
│       ├── Trie.hpp
│       └── Unicode.hpp
├── README_EN.md
├── README.md
└── test
    ├── CMakeLists.txt
    ├── demo.cpp
    ├── load_test.cpp
    ├── testdata
    │   ├── curl.res
    │   ├── extra_dict
    │   │   └── jieba.dict.small.utf8
    │   ├── gbk_dict
    │   │   ├── hmm_model.gbk
    │   │   └── jieba.dict.gbk
    │   ├── jieba.dict.0.1.utf8
    │   ├── jieba.dict.0.utf8
    │   ├── jieba.dict.1.utf8
    │   ├── jieba.dict.2.utf8
    │   ├── load_test.urls
    │   ├── review.100
    │   ├── review.100.res
    │   ├── server.conf
    │   ├── testlines.gbk
    │   ├── testlines.utf8
    │   ├── userdict.2.utf8
    │   ├── userdict.english
    │   ├── userdict.utf8
    │   └── weicheng.utf8
    └── unittest
        ├── CMakeLists.txt
        ├── gtest_main.cpp
        ├── jieba_test.cpp
        ├── keyword_extractor_test.cpp
        ├── pos_tagger_test.cpp
        ├── pre_filter_test.cpp
        ├── segments_test.cpp
        ├── textrank_test.cpp
        ├── trie_test.cpp
        └── unicode_test.cpp

17 directories, 136 files
[xiaomaker@VM-28-13-centos jieba]$

这里我们只需要关注的是两个文件:

  • cppjieba/include:头文件。
  • cppjiba/dict:字典。

(2)下面我们了解jieba分词的使用,里面存在一个demo.cpp文件供我们测试。

[xiaomaker@VM-28-13-centos test]$ pwd
/home/xiaomaker/code_cpp/jieba/cppjieba/test
[xiaomaker@VM-28-13-centos test]$ ll
total 20
-rw-rw-r-- 1 xiaomaker xiaomaker  148 Feb 14 14:02 CMakeLists.txt
-rw-rw-r-- 1 xiaomaker xiaomaker 2797 Feb 14 14:02 demo.cpp
-rw-rw-r-- 1 xiaomaker xiaomaker 1532 Feb 14 14:02 load_test.cpp
drwxrwxr-x 4 xiaomaker xiaomaker 4096 Feb 14 14:02 testdata
drwxrwxr-x 2 xiaomaker xiaomaker 4096 Feb 14 14:02 unittest
[xiaomaker@VM-28-13-centos test]$

①我们不能直接编译,它会报错。

[xiaomaker@VM-28-13-centos test]$ g++ demo.cpp 
demo.cpp:1:30: fatal error: cppjieba/Jieba.hpp: No such file or directory
 #include "cppjieba/Jieba.hpp"
          ^~~~~~~~~~~~~~~~~~~~                   ^
compilation terminated.
[xiaomaker@VM-28-13-centos test]$

这是因为我们这里的库和头文件的路径是不对的,这里添加软链接即可。链接的路径是自己下载jieba的路径。

[xiaomaker@VM-28-13-centos test]$ ln -s ~/code_cpp/jieba/cppjieba/include/ include
[xiaomaker@VM-28-13-centos test]$ ln -s ~/code_cpp/jieba/cppjieba/dict/ dict
[xiaomaker@VM-28-13-centos test]$ ll
total 20
-rw-rw-r-- 1 xiaomaker xiaomaker  148 Feb 14 14:02 CMakeLists.txt
-rw-rw-r-- 1 xiaomaker xiaomaker 2853 Feb 24 12:16 demo.cpp
lrwxrwxrwx 1 xiaomaker xiaomaker   45 Feb 24 12:18 dict -> /home/xiaomaker/code_cpp/jieba/cppjieba/dict/
lrwxrwxrwx 1 xiaomaker xiaomaker   48 Feb 24 12:17 include -> /home/xiaomaker/code_cpp/jieba/cppjieba/include/
-rw-rw-r-- 1 xiaomaker xiaomaker 1532 Feb 14 14:02 load_test.cpp
drwxrwxr-x 4 xiaomaker xiaomaker 4096 Feb 14 14:02 testdata
drwxrwxr-x 2 xiaomaker xiaomaker 4096 Feb 14 14:02 unittest
[xiaomaker@VM-28-13-centos test]$

②接下来就需要修改demo.cpp的头文件。

③我们继续编译,我们发现还是出现错误。

[xiaomaker@VM-28-13-centos test]$ g++ demo.cpp 
In file included from include/cppjieba/Jieba.hpp:4,
                 from demo.cpp:1:
include/cppjieba/QuerySegment.hpp:7:10: fatal error: limonp/Logging.hpp: No such file or directory
 #include "limonp/Logging.hpp"
          ^~~~~~~~~~~~~~~~~~~~
compilation terminated.

这是因为找不到limonp/Logging.hpp文件。这时候我们只需要将deps/limonp目录拷贝到include/cppjieba当中即可。

[xiaomaker@VM-28-13-centos cppjieba]$ cp deps/limonp/ include/cppjieba/ -rf

④这样我们就可以编译通过了:

[xiaomaker@VM-28-13-centos test]$ g++ demo.cpp
[xiaomaker@VM-28-13-centos test]$ ll
total 460
-rwxrwxr-x 1 xiaomaker xiaomaker 447008 Feb 24 12:46 a.out
-rw-rw-r-- 1 xiaomaker xiaomaker    148 Feb 14 14:02 CMakeLists.txt
-rw-rw-r-- 1 xiaomaker xiaomaker   2861 Feb 24 12:20 demo.cpp
lrwxrwxrwx 1 xiaomaker xiaomaker     45 Feb 24 12:18 dict -> /home/xiaomaker/code_cpp/jieba/cppjieba/dict/
lrwxrwxrwx 1 xiaomaker xiaomaker     48 Feb 24 12:17 include -> /home/xiaomaker/code_cpp/jieba/cppjieba/include/
-rw-rw-r-- 1 xiaomaker xiaomaker   1532 Feb 14 14:02 load_test.cpp
drwxrwxr-x 4 xiaomaker xiaomaker   4096 Feb 14 14:02 testdata
drwxrwxr-x 2 xiaomaker xiaomaker   4096 Feb 14 14:02 unittest
[xiaomaker@VM-28-13-centos test]$ ./a.out 
他来到了网易杭研大厦
[demo] Cut With HMM
他/来到//网易/杭研/大厦
[demo] Cut Without HMM 
他/来到//网易///大厦
我来到北京清华大学
[demo] CutAll
我/来到/北京/清华/清华大学/华大/大学
小明硕士毕业于中国科学院计算所,后在日本京都大学深造
[demo] CutForSearch
小明/硕士/毕业//中国/科学/学院/科学院/中国科学院/计算/计算所////日本/京都/大学/日本京都大学/深造
[xiaomaker@VM-28-13-centos test]$

6.2 索引框架

(1)创建index.hpp文件后,我们要建立正排和倒排索引,并且我们还要提供查找的接口。index.hpp的整体框架如下:

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <unordered_map>
#include "util.hpp"

namespace ns_index
{
    struct DocInfo
    {
        std::string title;   // 文档的标题
        std::string content; // 文档内容
        std::string url;     // 读文档在官网中的url
        uint64_t doc_id;          //文档id
    };

    struct InvertedElem
    {
        int doc_id;
        std::string word;
        int weight;
    };

    typedef std::vector<InvertedElem> InvertedList_t;   //倒排拉链

    class Index
    {
    public:
		Index()
        {}

        DocInfo* GetForwardIndex(uint64_t doc_id)  //正排索引:根据doc_id找到文档内容
        {
            return nullptr;
        }

        InvertedList_t* GetInveredList(std::string& word)  //倒排索引:根据关键字获取倒排拉链
        {
            return nullptr;
        }

        //根据去标签,格式化之后的文档,构建正排索引和倒排索引 
        bool BuildIndex(const std::string& input)  //parse处理完毕的数据交给我
        {
            return true;
        }
	
		~Index()
        {}
    private:
        DocInfo* BuildForwardIndex(std::string& line)
        {
            return nullptr;
        }

        bool BuildInveredIndex(const DocInfo& doc)
        {
            return true;
        }

        //正排索引使用数组,数组的下标就是文档id
        std::vector<DocInfo> forward_index;  //正排索引
        //倒排索引一定是一个关键字和一组InvertedElem对应
        std::unordered_map<std::string, InvertedList_t> inverted_index;  //倒排索引
    };
}

下面我们依次实现index.hpp里面的函数。

6.3 BuildIndex函数的实现

bool BuildIndex(const std::string& input);

(1)根据我们已经处理好的数据,通过它来构建索引。

bool BuildIndex(const std::string& input)  //parse处理完毕的数据交给我
{
    std::fstream in(input, std::ios::in | std::ios::binary);
    if (!in.is_open())
    {
        LOG(FATAL, "sorry" + input + "error");
        //std::cerr << "sorry" << input << "error" << std::endl;
        return false;
    }

    std::string line;
    int count = 0;
    while (std::getline(in, line))
    {
        DocInfo* doc = BuildForwardIndex(line);
        if (doc == nullptr)
        {
            LOG(WARNING, "build" + line + "error");
            //std::cerr << "build" << line << "error" << std::endl;
            continue;
        }

        BuildInveredIndex(*doc);
        count++;
        if (count % 50 == 0)
        {
            LOG(NORMAL, "当前已经建立索引文档: " + std::to_string(count));
            //std::cout << "当前已经建立索引文档:" << count << std::endl;
        }
    }

    return true;
}

(2)代码解析:

6.3.1 建立正排索引函数BuildForwardIndex

(1)这个非常容易实现,因为我们数组下标天然是我们的文档ID,只需要把处理后每一个文档的内容处理成结构体,然后添加到数组中就可以了。

DocInfo* BuildForwardIndex(std::string& line)
{
    //1. 解析line, 字符串切分
    std::vector<std::string> results;
    std::string sep = "\3";
    ns_util::StringUtil::Cutstring(line, results, sep);
    if (results.size() != 3)  //当切分的字符串不是3说明切割出现错误
    {
        LOG(FATAL, "build error");
        return nullptr;
    }

    //2. 字符串进行填充到DocInfo
    DocInfo doc;
    doc.title = results[0];    //title
    doc.content = results[1];  //content
    doc.url = results[2];      //url
    doc.doc_id = forward_index.size();   //文档id

    //3. 插入到vector当中
    forward_index.push_back(std::move(doc));

    return &forward_index.back();
}

(2)将Cutstring函数写到工具集(util.hpp)当中。split是boost库当中的接口。

namespace ns_util
{
	class StringUtil
    {
    public:
        static void Cutstring(const std::string& target, std::vector<std::string>& out, std::string sep)
        {
            boost::split(out, target, boost::is_any_of(sep), boost::token_compress_on);
        }
    };
}

6.3.2 建立倒排索引函数BuildInveredIndex

(1)我们开始根据最新的结构体建立倒排索引,这里就需要我进行分词了。也就需要引入jieba库帮助我们分词。使用软连接将下载好的jieba路径链接到项目当中:

[xiaomaker@VM-28-13-centos boost_searcher]$ ln -s /home/xiaomaker/code_cpp/jieba/cppjieba/include/cppjieb cppjieba
[xiaomaker@VM-28-13-centos boost_searcher]$ ln -s /home/xiaomaker/code_cpp/jieba/cppjieba/dict/ cppjieba
[xiaomaker@VM-28-13-centos boost_searcher]$ ll
total 24
lrwxrwxrwx 1 xiaomaker xiaomaker   57 Feb 14 14:04 cppjieba -> /home/xiaomaker/code_cpp/jieba/cppjieba/include/cppjieba/
drwxrwxr-x 4 xiaomaker xiaomaker 4096 Feb 13 18:10 data
lrwxrwxrwx 1 xiaomaker xiaomaker   45 Feb 14 14:08 dict -> /home/xiaomaker/code_cpp/jieba/cppjieba/dict/
-rw-rw-r-- 1 xiaomaker xiaomaker 6101 Feb 15 18:24 index.hpp
-rw-rw-r-- 1 xiaomaker xiaomaker  379 Feb 15 14:21 Makefile
-rw-rw-r-- 1 xiaomaker xiaomaker 6963 Feb 15 18:32 parser.cpp
-rw-rw-r-- 1 xiaomaker xiaomaker 1842 Feb 15 18:38 util.hpp
[xiaomaker@VM-28-13-centos boost_searcher]$

(2)这样我们就可以编写我们的切词工具了。

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;

    public:
        static void Split(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);
}

(3)具体实现:

bool BuildInveredIndex(const DocInfo& doc)
{
    struct word_cnt  //统计词汇出现的次数
    {
        int title_cnt = 0;   //标题出现的次数
        int content_cnt = 0; //内容出现的次数
    };

    //分词---标题
    std::unordered_map<std::string, word_cnt> word_map;  //统计暂存词频率的映射表
    std::vector<std::string> title_word;
    ns_util::JiebaUtil::Split(doc.title, title_word);
    //对标题词频进行统计
    for (auto& s : title_word)
    {
        boost::to_lower(s);  //统一转换成小写
        word_map[s].title_cnt++;
    }

    //分词---内容
    std::vector<std::string> content_word;
    ns_util::JiebaUtil::Split(doc.content, content_word);
    //对内容词频进行统计
    for (auto& s : content_word)
    {
        boost::to_lower(s);  //统一转换成小写
        word_map[s].content_cnt++;
    }

    for (auto& word_pair : word_map)
    {
        InvertedElem item;
        item.doc_id = doc.doc_id;
        item.word = word_pair.first;
        item.weight = word_pair.second.title_cnt * X + word_pair.second.content_cnt * Y;
        InvertedList_t& inverted_list = inverted_index[word_pair.first];
        inverted_list.push_back(std::move(item));
    }

    return true;
}

(4)代码解析:

(5)权重计算:
什么是权重:对于搜索频率高的单词,我们认为它的权重高,同时对一个文档,如果关键字出现的次数越多,权重越大,这里我么权重结算简单些。

#define X 10   //标题出现的次数乘10
#define Y 1    //内容出现的次数乘1

那么权重有什么作用呢?当我们搜索的时,一个关键字可以对应多个文档,那么此时我们可以把权重高的放在前面。
现在我们的结构是这样的:

6.4 GetForwardIndex函数

(1)根据文档的id找到文档的内:

DocInfo* GetForwardIndex(uint64_t doc_id)  //正排索引:根据doc_id找到文档内容
{
    if (doc_id >= forward_index.size())
    {
        LOG(FATAL, "doc_id out range. error");
        //std::cerr << "doc_id out range. error" << std::endl;
        return nullptr;
    }

    return &forward_index[doc_id];
}

6.5 GetInvertedList函数

(1)根据关键字拿到倒排拉链:

InvertedList_t* GetInveredList(std::string& word)  //倒排索引:根据关键字获取倒排拉链
{
    auto iter = inverted_index.find(word);
    if (iter == inverted_index.end())
    {
        LOG(FATAL, "no have InvertedList_t");
        //std::cerr << word << "no have InvertedList_t" << std::endl;
        return nullptr;
    }

    return &(iter->second);
}

完上述所有工作后可以将index设置为单例模式。

6.6 将index设置成单例

下面我们把index设置成单例模式,这是因为在boost搜索引擎项目当中,事实上不需要建立多个Index索引对象,只需要建立一个索引对象就可以完成查找工作了。而且我们建立一个索引对象的成本事实上是极高的,因为我们需要将所有的网页信息分词、统计、填充、插入、效率上会受极大损失。

index整体代码(单例模式):

#pragma once
#include <iostream>
#include <string>
#include <vector>
#include <fstream>
#include <mutex>
#include <unordered_map>
#include "util.hpp"
#include "log.hpp"

#define X 10
#define Y 1

namespace ns_index
{
    struct DocInfo
    {
        std::string title;   // 文档的标题
        std::string content; // 文档内容
        std::string url;     // 读文档在官网中的url
        uint64_t doc_id;          //文档id
    };

    struct InvertedElem
    {
        int doc_id;
        std::string word;
        int weight;
    };

    typedef std::vector<InvertedElem> InvertedList_t;   //倒排拉链

    class Index
    {
    private:
        Index()
        {}

        Index(const Index&) = delete;
        Index& operator=(const Index&) = delete;

        ~Index()
        {}

        static Index* instance;
        static std::mutex mtx;

    public:
        static Index* GetInstance()
        {
            if(instance == nullptr)
            {
                mtx.lock();  //多线程安全
                if(instance == nullptr)
                {
                    instance = new Index();
                }

                mtx.unlock();
            }

            return instance;
        }

        DocInfo* GetForwardIndex(uint64_t doc_id)  //正排索引:根据doc_id找到文档内容
        {
            if(doc_id >= forward_index.size())
            {
                LOG(FATAL, "doc_id out range. error");
                //std::cerr << "doc_id out range. error" << std::endl;
                return nullptr;
            }

            return &forward_index[doc_id];
        }

        InvertedList_t* GetInveredList(std::string& word)  //倒排索引:根据关键字获取倒排拉链
        {
            auto iter = inverted_index.find(word);
            if(iter == inverted_index.end())
            {
                LOG(FATAL, "no have InvertedList_t");
                //std::cerr << word << "no have InvertedList_t" << std::endl;
                return nullptr;
            }

            return &(iter->second);
        }

        //根据去标签,格式化之后的文档,构建正排索引和倒排索引 
        bool BuildIndex(const std::string& input)  //parse处理完毕的数据交给我
        {
            std::fstream in(input, std::ios::in | std::ios::binary);
            if(!in.is_open())
            {
                LOG(FATAL, "sorry" + input + "error");
                //std::cerr << "sorry" << input << "error" << std::endl;
                return false;
            }

            std::string line;
            int count = 0;
            while(std::getline(in, line))
            {
                DocInfo* doc = BuildForwardIndex(line);
                if(doc == nullptr)
                {
                    LOG(WARNING, "build" + line + "error");
                    //std::cerr << "build" << line << "error" << std::endl;
                    continue;
                }

                BuildInveredIndex(*doc);
                count++;
                if(count % 50 == 0)
                {
                    LOG(NORMAL, "当前已经建立索引文档: " + std::to_string(count));
                    //std::cout << "当前已经建立索引文档:" << count << std::endl;
                }
            }

            return true;
        }

    private:
        DocInfo* BuildForwardIndex(std::string& line)
        {
            //1. 解析line, 字符串切分
            std::vector<std::string> results;
            std::string sep = "\3";
            ns_util::StringUtil::Cutstring(line, results, sep);
            if(results.size() != 3)
            {
                LOG(FATAL, "build error");
                return nullptr;
            }

            //2. 字符串进行填充到DocInfo
            DocInfo doc;
            doc.title = results[0];    //title
            doc.content = results[1];  //content
            doc.url = results[2];      //url
            doc.doc_id = forward_index.size();   //文档id

            //3. 插入到vector当中
            forward_index.push_back(std::move(doc));

            return &forward_index.back();
        }

        bool BuildInveredIndex(const DocInfo& doc)
        {
            struct word_cnt  //统计词汇出现的次数
            {
                int title_cnt = 0;   //标题出现的次数
                int content_cnt = 0; //内容出现的次数
            };

            //分词---标题
            std::unordered_map<std::string, word_cnt> word_map;  //统计暂存词频率的映射表
            std::vector<std::string> title_word;
            ns_util::JiebaUtil::Split(doc.title, title_word);
            //对标题词频进行统计
            for(auto& s : title_word)
            {
                boost::to_lower(s);  //统一转换成小写
                word_map[s].title_cnt++;
            }

            //分词---内容
            std::vector<std::string> content_word;
            ns_util::JiebaUtil::Split(doc.content, content_word);
            //对内容词频进行统计
            for(auto& s : content_word)
            {
                boost::to_lower(s);  //统一转换成小写
                word_map[s].content_cnt++;
            }

            for(auto& word_pair : word_map)
            {
                InvertedElem item;
                item.doc_id = doc.doc_id;
                item.word = word_pair.first;
                item.weight = word_pair.second.title_cnt * X + word_pair.second.content_cnt * Y;
                InvertedList_t& inverted_list = inverted_index[word_pair.first];
                inverted_list.push_back(std::move(item));
            }

            return true;
        }

        //正排索引使用数组,数组的下标就是文档id
        std::vector<DocInfo> forward_index;  //正排索引
        //倒排索引一定是一个关键字和一组InvertedElem对应
        std::unordered_map<std::string, InvertedList_t> inverted_index;  //倒排索引
    };

    Index* Index::instance = nullptr;   //单例模式的指针初始化
    std::mutex Index::mtx;
}

7. 搜索引擎模块

下面我们开始编写搜索模块,创建一个searcher.hpp 文件。

(1)搜索引擎代码框架:

#pragma once
#include "index.hpp"
#include "util.hpp"
#include <algorithm>
#include <jsoncpp/json/json.h>

namespace ns_searcher
{
    struct InvertedElemPrint
    {
        uint64_t doc_id = 0;
        std::vector<std::string> word;
        int weight = 0;
    };

    class Searcher
    {
    public:
        Searcher()
        {}

        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即关键字进行分词
            //2. 触发:就是根据分词的各个词,进行index查找
            //3. 合并排序,汇总结果,按照相关性降序排序
            //4. 构建:根据查找的结果,构建json串---jsoncpp
        }

        ~Searcher()
        {}

    private:
        std::string GetDesc(const std::string& html_content, const std::string& word)
        {
            //找到word在html_content中首次出现,往前找50字节(如果没有从开始找),往后100字节
            //1. 找到首次出现
            //2. 获取start, end
            //3. 截取子串
        }

        ns_index::Index* index;  //供系统进行查找的索引
    };
}

7.1 InitSearcher函数

(1)这个是我们初始化的工作,一共两个内容:

  • 拿到index对象
  • 根据index建立索引
void InitSearcher(const std::string& input)  //input是数据源的地址
{
	//1. 获取或者创建index对象
	index = ns_index::Index::GetInstance();
	LOG(NORMAL, "获取单例index成功");
	//std::cout << "获取单例index成功" << std::endl;

	//2. 根据index对象建立索引
	index->BuildIndex(input);
	LOG(NORMAL, "建立正排和倒排成功");
	//std::cout << "建立正排和倒排成功" << std::endl;
}

7.2 Search函数

(1)这个是我们查找实现的具体流程,我们输入我们想要查找的内容,下面是我们函数的流程:

  • 切分输入的内容,统一小写的保存在数组中。
  • 根据数组的每一个元素,拿到倒排拉链,然后把所有的倒排拉量的内容保存在一个拉链中。
  • 我们以降序的方式排序整个拉链。
  • 根据拉链的id找到文档内容,构建json串。
//query:搜索的关键字
//json_string:返回个用户浏览器的数据搜索结果
void Search(const std::string& query, std::string& json_string)
{
    //1. 对query即关键字进行分词
    std::vector<std::string> words;
    ns_util::JiebaUtil::Split(query, words);

    //2. 触发:就是根据分词的各个词,进行index查找
    //ns_index::InvertedList_t inverted_list_all;
    std::vector<InvertedElemPrint> inverted_list_all;

    std::unordered_map<uint64_t, InvertedElemPrint> token_map;

    for (auto& s : words)
    {
        boost::to_lower(s);
        ns_index::InvertedList_t* inverted_list = index->GetInveredList(s);
        if (inverted_list == nullptr)
        {
            LOG(WARNING, "通过关键字寻找文章失败");
            continue;
        }

        //不完美
        //inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
        for (auto& elem : *inverted_list)
        {
            auto& item = token_map[elem.doc_id];
            item.doc_id = elem.doc_id;
            item.weight += elem.weight;
            item.word.push_back(elem.word);
        }
    }

    for (const auto& item : token_map)
    {
        inverted_list_all.push_back(std::move(item.second));
    }

    //3. 合并排序,汇总结果,按照相关性降序排序
    /*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串---jsoncpp
    Json::Value root;
    for (auto& item : inverted_list_all)
    {
        ns_index::DocInfo* doc = index->GetForwardIndex(item.doc_id);
        if (doc == nullptr)
        {
            continue;
        }

        Json::Value elem;
        elem["title"] = doc->title;
        elem["desc"] = GetDesc(doc->content, item.word[0]);  //只需要展示一部分内容就可以
        elem["url"] = doc->url;

        root.append(elem);
    }

    //Json::StyledWriter w;
    Json::FastWriter w;
    json_string = w.write(root);
}

(2)上面代码的实现有一个完美的地方,我们知道一个词可以映射到多个文档的id,那么多个关键字映射的文档id,就有可能进行冲突,例如下面的例子:

关键字(具有唯一性)文档ID
我的1,2
手机1,2
牌子1,2
华为1
小米1

我们把"我的、手机、牌子"进行分词,然后得到拉链,放在总拉链里面,这就是[文档1, 文档2,文档1, 文档2]这样就重复了,上面代码解决了该问题(解决问题的解析见本章第十小节)。

(3)代码解析:

7.3 jsoncpp安装与使用

下面我们需要说一下jsoncpp的安装与使用,前面使用了jsoncpp构建了json串,json是序列化和反序列化的。

[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install -y jsoncpp-devel

(1)下面演示一下json的使用:

#include <iostream>
#include <string>
#include <jsoncpp/json/json.h>

int main()
{
    Json::Value root;

    Json::Value item1;
    item1["key1"] = "value11";
    item1["key2"] = "value22";

    Json::Value item2;
    item2["key1"] = "value1";
    item2["key2"] = "value2";

    root.append(item1);
    root.append(item2);

    Json::StyledWriter writer;
    std::string s = writer.write(root);
    std::cout << s << std::endl;
    return 0;
}

(2)运行结果:

[xiaomaker@VM-28-13-centos boost_searcher]$ g++ test.cpp  -ljsoncpp
[xiaomaker@VM-28-13-centos boost_searcher]$ ./a.out 
[
   {
      "key1" : "value11",
      "key2" : "value22"
   },
   {
      "key1" : "value1",
      "key2" : "value2"
   }
]

[xiaomaker@VM-28-13-centos boost_searcher]$ 

7.4 搜索功能的测试

(1)下面是搜索测试代码:

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

    while (1)
    {
        std::cout << "请输入关键字# ";
        //std::cin >> query;
        std::getline(std::cin, query);
        //std::cout << query;
        search->Search(query, &json_string);
        std::cout << json_string << std::endl;
    }
    
    return 0;
}

下面我们测试一下,这是一个html文档的内容,由于内容实在是太多了,我们应该把内容给裁出来一部分,这样比较好。

{
	"desc" : "Struct template bound_launcherHomeLibrariesPeopleFAQMoreStruct template bound_launcherboost::process::v2::bound_launcher — Utility class to bind initializers to a launcher. Synopsis// In header: &lt;boost/process/v2/bind_launcher.hpp&gt;template&lt;typename Launcher, typename ... Init&gt; struct bound_launcher {  // construct/copy/destruct  template&lt;typename Launcher_, typename ... Init_&gt;     bound_launcher(Launcher_ &amp;&amp;, Init_ &amp;&amp;...);  // public member functions  template&lt;typename ExecutionContext, typename Args, typename ... Inits&gt;     auto operator()(ExecutionContext &amp;,                     const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp;,                     Args &amp;&amp;, Inits &amp;&amp;...);  template&lt;typename ExecutionContext, typename Args, typename ... Inits&gt;     auto operator()(ExecutionContext &amp;, error_code &amp;,                     const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp;,                     Args &amp;&amp;, Inits &amp;&amp;...);  template&lt;typename Executor, typename Args, typename ... Inits&gt;     auto operator()(Executor,                     const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp;,                     Args &amp;&amp;, Inits &amp;&amp;...);  template&lt;typename Executor, typename Args, typename ... Inits&gt;     auto operator()(Executor, error_code &amp;,                     const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp;,                     Args &amp;&amp;, Inits &amp;&amp;...);  // private member functions  template&lt;std::size_t ... Idx, typename ExecutionContext, typename Args,            typename ... Inits&gt;     auto invoke(unspecified, ExecutionContext &amp;,                 const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp;,                 Args &amp;&amp;, Inits &amp;&amp;...);  template&lt;std::size_t ... Idx, typename ExecutionContext, typename Args,            typename ... Inits&gt;     auto invoke(unspecified, ExecutionContext &amp;, error_code &amp;,                 const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp;,                 Args &amp;&amp;, Inits &amp;&amp;...);  template&lt;std::size_t ... Idx, typename Executor, typename Args,            typename ... Inits&gt;     auto invoke(unspecified, Executor,                 const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp;,                 Args &amp;&amp;, Inits &amp;&amp;...);  template&lt;std::size_t ... Idx, typename Executor, typename Args,            typename ... Inits&gt;     auto invoke(unspecified, Executor, error_code &amp;,                 const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp;,                 Args &amp;&amp;, Inits &amp;&amp;...);};DescriptionThis can be used when multiple processes shared some settings, e.g. Template Parameterstypename LauncherThe inner launcher to be used typename ... Initbound_launcher         public       construct/copy/destructtemplate&lt;typename Launcher_, typename ... Init_&gt;   bound_launcher(Launcher_ &amp;&amp; l, Init_ &amp;&amp;... init);bound_launcher public member functionstemplate&lt;typename ExecutionContext, typename Args, typename ... Inits&gt;   auto operator()(ExecutionContext &amp; context,                   const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp; executable,                   Args &amp;&amp; args, Inits &amp;&amp;... inits);template&lt;typename ExecutionContext, typename Args, typename ... Inits&gt;   auto operator()(ExecutionContext &amp; context, error_code &amp; ec,                   const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp; executable,                   Args &amp;&amp; args, Inits &amp;&amp;... inits);template&lt;typename Executor, typename Args, typename ... Inits&gt;   auto operator()(Executor exec,                   const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp; executable,                   Args &amp;&amp; args, Inits &amp;&amp;... inits);template&lt;typename Executor, typename Args, typename ... Inits&gt;   auto operator()(Executor exec, error_code &amp; ec,                   const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp; executable,                   Args &amp;&amp; args, Inits &amp;&amp;... inits);bound_launcher private member functionstemplate&lt;std::size_t ... Idx, typename ExecutionContext, typename Args,          typename ... Inits&gt;   auto invoke(unspecified, ExecutionContext &amp; context,               const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp; executable,               Args &amp;&amp; args, Inits &amp;&amp;... inits);template&lt;std::size_t ... Idx, typename ExecutionContext, typename Args,          typename ... Inits&gt;   auto invoke(unspecified, ExecutionContext &amp; context, error_code &amp; ec,               const typename std::enable_if&lt; std::is_convertible&lt; ExecutionContext &amp;, boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp; executable,               Args &amp;&amp; args, Inits &amp;&amp;... inits);template&lt;std::size_t ... Idx, typename Executor, typename Args,          typename ... Inits&gt;   auto invoke(unspecified, Executor exec,               const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp; executable,               Args &amp;&amp; args, Inits &amp;&amp;... inits);template&lt;std::size_t ... Idx, typename Executor, typename Args,          typename ... Inits&gt;   auto invoke(unspecified, Executor exec, error_code &amp; ec,               const typename std::enable_if&lt; boost::asio::execution::is_executor&lt; Executor &gt;::value||boost::asio::is_executor&lt; Executor &gt;::value, filesystem::path &gt;::type &amp; executable,               Args &amp;&amp; args, Inits &amp;&amp;... inits);Copyright © 2006-2012 Julio M. Merino Vidal, Ilya Sokolov,      Felipe Tanus, Jeff Flinn, Boris SchaelingCopyright © 2016 Klemens D. Morgenstern        Distributed under the Boost Software License, Version 1.0. (See accompanying        file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)      ",
	"title" : "Struct template bound_launcher",
    "url" : "https://www.boost.org/doc/libs/1_84_0/doc/html/boost/process/v2/bound_launcher.html"
}

7.5 获取内容摘要

当我们搜索出结果的时候并不需要全部展示内容,所以需要获取内容的摘要。
(1)具体实现:

std::string GetDesc(const std::string& html_content, const std::string& word)
{
    //找到word在html_content中首次出现,往前找50字节(如果没有从开始找),往后100字节
    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
    int start = 0;
    int end = html_content.size() - 1;
    //如果之前有50个就更新
    if (pos > start + prev_step)
    {
        start = pos - prev_step;
    }

    //如果后面有100个就更新
    if (pos < end - next_step)
    {
        end = pos + next_step;
    }

    //3. 截取子串
    if (start >= end)
    {
        return "None2";
    }

    std::string desc = html_content.substr(start, end - start);
    desc += "...";
    return desc;
}


(2)测试结果:

请输入关键字# filesystem
[
   {
      "desc" : "boost::asio::execution_context &amp; &gt;::value, filesystem::path &gt;::type &amp;,                     Args &amp;&amp;, Inits &amp;&amp;...);  templ...."...,
      "title" : "Struct template bound_launcher",
      "url" : "https://www.boost.org/doc/libs/1_84_0/doc/html/boost/process/v2/bound_launcher.html"
   },
   .....
]

8. 搜索服务端

接下来开始编写网络版本的服务端,创建http_server.cpp文件。

#include "searcher.hpp"
int mian()
{
	return 0;
}

这里通信我们可以自己写,后面我们会升级,不过这里我们使用cpp-httplib库,这个库很简单,但是cpp-httplib有点问题,我们需要较新版本的编译器,否则就是编译不通过,或者是运行出现错误。

8.1 升级gcc版本

下面是更新新的gcc版本:

//安装scl
[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install centos-release-scl scl-utils-build
//安装新版本gcc
[xiaomaker@VM-28-13-centos boost_searcher]$ sudo yum install -y devtoolset-7-gcc devtoolset-7-gcc-c++
//启动: 细节,命令行启动只能在本会话有效
[xiaomaker@VM-28-13-centos boost_searcher]$ scl enable devtoolset-7 bash

8.2 引入cpp-httplib库

  1. 这里我们选择下载0.7.15版本,这是因为较新版本的可能运行时会报错。

  2. 这里我们选择下载到桌面,然后拖拽到轻量级服务器上,这些方法都试一遍.

(1)cpp-httplib库链接:https://gitee.com/linzhipong/cpp-httplib/tree/v0.7.15

[xiaomaker@VM-28-13-centos http]$ rz -E 

[xiaomaker@VM-28-13-centos http]$ ll
total 4
drwxr-xr-x 6 root root 4096 Nov 19  2020 cpp-httplib-v0.7.15
[xiaomaker@VM-28-13-centos http]$

(2)这时候可以创建软链接到我们的项目中:

[xiaomaker@VM-28-13-centos boost_searcher]$ ln -s /home/xiaomaker/code_cpp/http/cpp-httplib-v0.7.15/ cpp-httplib

8.3 测试cpp-httplib

在测试一下httplib库之前,我们不仅需要链接cpp-httplib库,还需要链接pthread库,因为该库用到了多线程,所以需要链接多线程库。
(1)测试代码:

#include "cpp-httplib/httplib.h"

int main()
{
    httplib::Server svr;
    svr.Get("hi", [](const httplib::Request& req, httplib::Response& rsp) 
    {
        rsp.set_content("hello word!", "text/plain; charset=utf-8");
    });
    
    svr.listen("0.0.0.0", 8081);
    
    return 0;
}

(2)运行之后查看服务器:

[xiaomaker@VM-28-13-centos boost_searcher]$ netstat -ntlp
(Not all processes could be identified, non-owned process info
 will not be shown, you would have to be root to see it all.)
Active Internet connections (only servers)
Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
tcp        0      0 127.0.0.1:44227         0.0.0.0:*               LISTEN      1903/node           
tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:8081            0.0.0.0:*               LISTEN      4191/./http_server  
tcp        0      0 192.168.122.1:53        0.0.0.0:*               LISTEN      -                   
tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:631           0.0.0.0:*               LISTEN      -                   
tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      -                   
tcp6       0      0 :::111                  :::*                    LISTEN      -                   
tcp6       0      0 :::22                   :::*                    LISTEN      -                   
tcp6       0      0 ::1:631                 :::*                    LISTEN      -                   
tcp6       0      0 ::1:25                  :::*                    LISTEN      -                   
[xiaomaker@VM-28-13-centos boost_searcher]$

运行之前自己的服务器需要开放端口号才可以。

(3)运行结果:

8.4 设置根目录

一般而言我们需要有一个根目录,这样就可以实现前端,创建wwwroot目录。
(1)在服务器上面设置跟目录:

#include "cpp-httplib/httplib.h"

const std::string root_path = "./wwwroot";

int main()
{
    httplib::Server svr;
    
    // 设置跟目录
    svr.set_base_dir(root_path.c_str());
    svr.Get("hi", [](const httplib::Request& req, httplib::Response& rsp) 
    {
        rsp.set_content("hello word!", "text/plain; charset=utf-8");
    });
    
    svr.listen("0.0.0.0", 8080);
    
    return 0;
}

(2)测试结果:


(3)这是因为我们的根目录下面什么都没有,我们在wwwroot目录下创建index.html文件,在这里设置一下:

(4)再次测试:

8.5 编写搜索服务端

(1)下面编写我们的服务端了:

#include "searcher.hpp"
#include "cpp-httplib/httplib.h"
#include <iostream>
#include <string>

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());
    svr.Get("/s", [&search](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");
        LOG(NORMAL, "用户在搜索" + word);
        //std::cout << "用户在搜索" << word << std::endl;

        std::string json_string;
        search.Search(word, json_string);
        rsp.set_content(json_string, "application/json");
        //rsp.set_content("hello world", "text/plain: charset=utf-8");
    });

    LOG(NORMAL, "服务器启动成功");
    svr.listen("0.0.0.0", 8080);

    return 0;
}

(2)代码解析:

9. 前端代码

前端部分我们可以选学,这里我们也不谈,如果想学,可以去下面的网站:

  • HTML:编写网页结构,网页的骨骼。
  • CSS:网页样式,网页的皮肉。
  • Js:前后端交互,网页的灵魂。

前端学习网站推荐:http://www.w3school.com.cn

9.1 网页结构

(1)设置的网页结构是这样的:


(2)按照上面的内容,我们的html代码:

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

(3)运行结果:

9.2 网页样式

(1)上面的网有点丑,所以这里我们要给他美颜一下:

<!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">
  <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: #CCC;
      font-size: 15px;
    }

    /* 先选中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 {
      /*设置鼠标放在a之上的动态效果*/
      text-decoration: underline;
    }

    .container .result .item p {
      margin-top: 5px;
      font-size: 16px;
      font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida SansUnicode', Geneva, Verdana, sans-serif;

    }

    .container .result .item i {
      /* 设置为块级元素,单独站一行 */
      display: block;
      /* 取消斜体风格 */
      font-style: normal;
      color: green;
    }
  </style>
</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>

(2)运行结果:

9.3 前后端交互

(1)下面我们继续使用前后端交互,也是直接贴代码:

<!-- 形成骨架 -->
<!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>
    * {
      /* 设置外边距 */
      margin: 0;
      /* 设置内边距 */
      padding: 0;
    }

    html,
    body {
      height: 100%;
    }

    /* 居中显式  以点开头的我们称之类选择器 */
    .container {
      /* 这是最大框架 */
      width: 800px;
      margin: 0px auto;
      margin-top: 15px;
    }

    /* 复合选择器 */
    .container .search {
      width: 100%;
      /* 为何是52我们后面解释 */
      height: 52px;

    }

    .container .search input {
      /* 加上浮动 */
      float: left;
      width: 600px;
      height: 50px;
      /* 设置边框 */
      border: 1px solid black;
      /* 去掉右边距 */
      border-right: none;
      padding-left: 10px;
      color: #ccc;
      font-size: 15px;
    }

    .container .search button {
      /* 加上浮动 */
      float: left;
      width: 120px;
      height: 52px;

      /* 设置背景颜色 */
      background-color: #4e6ef2;
      /* 设置字体颜色 */
      color: #fff;
      /* 设置字体大小 */
      font-size: 19px;
      /* 设置字体样式 */
      font-family: 'Times New Roman', Times, serif;

    }


    .container .result {
      width: 100%;
    }

    .container .result .item {
      margin-top: 15px;
    }

    .container .result .item a {
      display: block;
      /* 去掉下划线 */
      text-decoration: none;
      font-size: 20px;
      color: #4e6ef2;

    }

    .container .result .item a:hover {
      text-decoration: underline;
    }

    .container .result .item p {
      margin: 5px;
      font-size: 16px;
      font-family: 'Times New Roman', Times, serif;

    }

    .container .result .item i {
      display: block;

      /* 取消斜体 */
      font-style: normal;
      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.bilibili.com/</i>
      </div>
      <div class="item">
        <a href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div> -->
    </div>
  </div>
  <script>
    function Search() {
      // alert("hello js");
      // 1. 提取数据 jquery

      let query = $(".container .search input").val();
      if(query == '' || query == null){
        return;
      }
      console.log("query = " + query);

      // 2. 发起http 请求
      $.ajax({
        type: "GET",
        url: "/s?word=" + query,
        success: function (data) {
          console.log(data);
          // 构建新网页  -- 动态的
          BuildHtml(data);
        }
      });
    }


    function BuildHtml(data) {

      if(date == '' || data == null){
        document.write("搜索的内容没有");
        return;
      }
      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>

(2)最后整体运行结果:

10. 项目补充

现在完善一下系统的一些小细节。

10.1 取重完善

我们在搜索服务那里说过,对于我们关键词的搜索结果,在多个关键字之间,我们的文档id可能会重复,这个时候我们需要进行去重分为两步:

  • 找到在重复的id
  • 把id里面的权重尽心相加
  • 重新构造,让后进行查找构建json串

下面是我们的遇到的情况:

10.2 添加日志

我们可以添加日志,创建一个Log.hpp文件。
(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;
}

可以在各个需要打印的地方加入日志,区分日志的等级即可(本篇文章附带的代码基本上都已经加入了日志)。

11. 项目拓展

这里我们可以扩展一下项目内容。

11.1 摘要完善

我们知道分词的时候是可以去掉暂停词的,上面的我们都没有这么做,这是因为我们的如果加上去掉暂停词,此时对资源的要求非常大,那么这里可以作为一个扩展.jieba里面也有暂停词的集合,我们可以使用一下:

class JiebaUtil
{
public:
    static void CutString(const std::string& src, std::vector<std::string>* out)
    {
        assert(out);
        ns_util::JiebaUtil::get_instance()->CutStringHelper(src, out);
    }

private:
    /// @brief 这里是分词
    /// @param src
    /// @param out
    void CutStringHelper(const std::string& src, std::vector<std::string>* out)
    {
        jieba.CutForSearch(src, *out);
        for (auto iter = out->begin(); iter != out->end();)
        {
            auto it = stop_words.find(*iter);
            if (it != stop_words.end())
            {
                // 此时是暂停词 删除
                //  避免迭代器失效
                // std::cout << *iter << std::endl;
                iter = out->erase(iter);
            }
            else
            {
                iter++;
            }
        }
    }
    
    static JiebaUtil* get_instance()
    {
        static std::mutex mtx;
        if (nullptr == instance)
        {
            mtx.lock();
            if (nullptr == instance)
            {
                instance = new JiebaUtil;
                instance->InitJiebaUtil();
            }
            mtx.unlock();
        }
        return instance;
    }
    // 这是我们的切分词

    void InitJiebaUtil()
    {
        std::ifstream in(STOP_WORD_PATH);
        if (in.is_open() == false)
        {
            LOG(FATAL, "加载暂停词错误");
            return;
        }
        std::string line;
        while (std::getline(in, line))
        {
            stop_words.insert(std::make_pair(line, true));
        }
        
        in.close();
    }

private:
    static JiebaUtil* instance;

    cppjieba::Jieba jieba;
    std::unordered_map<std::string, bool> stop_words;
    JiebaUtil() : jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH) {}
    // 拷贝构造等 delte
};

JiebaUtil* JiebaUtil::instance = nullptr;

11.2 后台部署服务

我们可以把它设置为精灵进程.

(1)nohup指令
nohup指令:将服务进程以守护进程的方式执行,使关闭XShell之后仍可以访问该服务
例如:nohup ./http_server

如果让程序在后台执行,可以在末尾加上 &,程序就会隐身,不会显示在终端
例如:nohup ./http_server &

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

(3)setsid
我们也是可以使用下面的方式进行守护进程化:

#pragma once

#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize()
{
    int fd = 0;
    // 1. 忽略SIGPIPE
    signal(SIGPIPE, SIG_IGN);
    // 2. 更改进程的工作目录
    // chdir();
    // 3. 让自己不要成为进程组组长
    if (fork() > 0)
        exit(0);
    // 4. 设置自己是一个独立的会话
    setsid();
    // 5. 重定向0,1,2
    if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3
    {
        dup2(fd, STDIN_FILENO);
        dup2(fd, STDOUT_FILENO);
        dup2(fd, STDERR_FILENO);
        // 6. 关闭掉不需要的fd
        if (fd > STDERR_FILENO) close(fd);
        // 6. close(0,1,2)// 严重不推荐
    }
}

11.3 其他拓展

  • 我们在搜索引擎中,对于权重的设置先后显示顺序,我们其实可以叠加一些算法,比如可以设置竞价排,热点统计,额外增加某些文档的权重。
  • 我们可以利用数据库,设置用户登录注册,引入对MySQL的使用。
  • 11
    点赞
  • 26
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: Boost是一个功能强大的C++库,它提供了许多常用的功能和数据结构的实现,包括字符串处理、文件系统操作、线程管理、日期时间处理等等。Mingw是一个提供了GCC编译器和一系列相关工具的软件集合,它可以在Windows平台上进行C和C++的开发。 要在Mingw上使用Boost库,需要按照以下步骤进行配置和安装: 1. 首先,从Boost官网(https://www.boost.org/)下载最新的Boost库源代码,并解压到一个合适的目录。 2. 打开Mingw的安装目录,找到其中的bin目录,将其添加到系统的环境变量中。这样可以确保可以通过命令行调用到Mingw的各种工具。 3. 打开命令行终端,进入到Boost库的源代码目录。在该目录下执行命令"bootstrap.bat",这会生成一个名为b2的构建工具。 4. 接着,在终端中执行命令"b2",这会开始编译Boost库的各个组件。这个过程可能会需要一些时间。 5. 编译完成后,在终端中执行命令"b2 install",这会将Boost库的文件复制到Mingw的安装目录中。 6. 最后,在你的C++项目中,可以通过添加"-lboost_xxx"的方式来链接使用Boost库的对应组件。其中"xxx"是你要使用的具体组件的名称。 通过以上步骤,就可以在Mingw上成功配置和使用Boost库了。需要注意的是,不同版本的Boost库可能存在一些差异,因此建议参考Boost官方文档或网上的教程进行更详细的安装和使用说明。 ### 回答2: Mingw是一个在Windows平台上开发C和C++应用程序的开源软件集合。Boost是一个广泛使用的C++库,提供了许多实用工具和算法,可以大大提高程序的效率和功能。那么如何在Mingw中使用Boost呢? 首先,我们需要下载和安装Boost库。我们可以在Boost官方网站上找到适合Mingw的Boost二进制文件,并按照官方的说明进行下载和安装。 安装完成后,我们需要设置Mingw的环境变量。打开Mingw的安装目录,在bin文件夹下找到mingw32-make.exe所在的路径,将该路径添加到系统的环境变量中。 接下来,我们需要在Mingw的编译器中指定Boost库的位置。在C或C++源代码中,我们需要包含相关的头文件,并在编译时链接Boost库。 假设我们的Boost库安装在C:\boost目录下,我们可以通过在源代码中添加以下代码来包含Boost头文件: ```cpp #include <boost/algorithm/string.hpp> ``` 在编译时,我们需要使用以下命令来指定Boost库的位置: ```shell g++ -I C:\boost program.cpp -o program.exe ``` 其中,-I参数用于指定头文件的搜索路径,-L参数用于指定库文件的搜索路径,在这里我们需要将C:\boost添加到-I参数中。 最后,我们可以使用Mingw编译源代码并生成可执行文件。如果一切顺利,我们将能够成功使用Boost库来提高C和C++程序的性能和功能。 综上所述,使用Boost库来增强Mingw的开发能力需要下载和安装Boost库,设置Mingw的环境变量,指定Boost库的位置,并在源代码中包含相关的头文件。通过这些步骤,我们可以成功使用Boost库来提升Mingw开发环境下的编程能力。 ### 回答3: Mingw是一个在Windows操作系统上用于开发和编译C和C++程序的工具集合。然而,由于某些原因,Mingw的性能可能不如其他工具集。为了提高Mingw的性能,可以采取以下方法: 1.更新Mingw版本:Mingw是一个开源工具集,经常会发布新的版本来修复bug和增加新特性。因此,确保使用最新版本的Mingw可以提高其性能。 2.优化编译选项:在使用Mingw编译程序时,可以使用一些编译选项来优化程序的性能。例如,可以使用优化级别选项来提高代码的执行速度,通过启用链接时优化选项来减少可执行文件的大小等。 3.使用优化的算法和数据结构:在编写程序时,可以选择使用一些优化的算法和数据结构来提高程序的性能。例如,使用二分查找算法而不是线性查找算法来查找元素,使用哈希表而不是线性表来存储数据等。 4.合理编写代码:编写高效的代码可以提高程序的性能。避免使用不必要的循环和递归,减少函数调用的次数,合理使用指针和引用等,都可以改善程序的性能。 5.使用并行编程:对于一些计算密集型任务,可以考虑使用并行编程来充分利用多核处理器的优势。Mingw支持OpenMP等并行编程框架,可以通过使用这些框架来实现并行计算。 综上所述,通过更新Mingw版本、优化编译选项、使用优化的算法和数据结构、编写高效的代码以及使用并行编程等方法,可以提高Mingw的性能。这些方法可以根据具体的需求和情况选择使用,以达到最佳的效果。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值