首页 > 其他分享 >Boost搜索引擎

Boost搜索引擎

时间:2023-09-10 18:01:09浏览次数:47  
标签:std qkj string doc 摘要 搜索引擎 amp Boost

项目背景

先说一下什么是搜索引擎,很简单,就是我们平常使用的百度,我们把自己想要所有的内容输入进去,百度给我们返回相关的内容.百度一般给我们返回哪些内容呢?这里很简单,我们先来看一下.

image-20230909140654900

搜索引擎基本原理

这里我们简单的说一下我们的搜索引擎的基本原理.

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

image-20230909141200300

Boost库

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

image-20230909141645320

但是我们的boost库不支持,不知道我们后面支不支持.

image-20230909141829732

项目目的

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

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

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

Boost搜索引擎宏观流程

清晰数据

我们把boost库下载下来,此时我们想要把所有的后缀是html的文件进行处理,也就是清晰数据.我们先来看一个简单的html文件.我们把其中的title,content,url进行保存.

构建索引

我们把清晰出来的标签构建好索引,为了后期便于查找.这里细节很多,我们后面说/

处理请求

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

前端页面

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

image-20230909143431419

技术栈与环境

技术栈

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

环境

  • Centos7虚拟机,vim,gcc(g++),Makefile,Vscode

认识索引

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

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

这里们说大家可能觉得有点不太清楚,这里我们举一个例子,这里有两个文件.

image-20230909144505988

正排索引

我们对每一个文件进行编号.

文档ID 文档名称 文档内容
1 文档A 你好,我是大学生
2 文档B 你好,我是社会人

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

倒排索引

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

关键字 文档ID
你好 1, 2
1, 2
1, 2
大学生 1
社会人 2

倒排索引,就是根据关键字,拿到我们的文档ID.

如何分词

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

  • 你好,我是大学生: 你好/我/是/大学生

  • 你好,我是社会人: 你好/我/是/社会人

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

模拟查找

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

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

数据清洗

我们先下载一下boost库,直接使用最新版本的,我这里是1.83.0.我们下载到桌面,然后在centos下使用指令rz传入虚拟机中,然后解压一下就可以了.

image-20230909151825742

[qkj@localhost install]$ rz -E 

[qkj@localhost install]$ ll
total 141256
-rw-r--r--. 1 qkj qkj 144645738 Sep  9 00:15 boost_1_83_0.tar.gz
[qkj@localhost install]$ tar xzf boost_1_83_0.tar.gz 
[qkj@localhost install]$ ll
total 141260
drwxr-xr-x. 8 qkj qkj      4096 Aug  8 14:40 boost_1_83_0
-rw-r--r--. 1 qkj qkj 144645738 Sep  9 00:15 boost_1_83_0.tar.gz
[qkj@localhost install]$ 

下面看一下这个库的内容.

[qkj@localhost install]$ cd boost_1_83_0/
[qkj@localhost boost_1_83_0]$ ll
total 112
drwxr-xr-x. 139 qkj qkj  8192 Aug  8 14:40 boost
-rw-r--r--.   1 qkj qkj   851 Aug  8 14:02 boost-build.jam
-rw-r--r--.   1 qkj qkj 20245 Aug  8 14:02 boostcpp.jam
-rw-r--r--.   1 qkj qkj   989 Aug  8 14:02 boost.css
-rw-r--r--.   1 qkj qkj  6308 Aug  8 14:02 boost.png
-rw-r--r--.   1 qkj qkj  2486 Aug  8 14:02 bootstrap.bat
-rwxr-xr-x.   1 qkj qkj 10811 Aug  8 14:02 bootstrap.sh
drwxr-xr-x.   7 qkj qkj   196 Aug  8 14:14 doc
-rw-r--r--.   1 qkj qkj   769 Aug  8 14:02 index.htm
-rw-r--r--.   1 qkj qkj  5418 Aug  8 14:40 index.html
-rw-r--r--.   1 qkj qkj   291 Aug  8 14:02 INSTALL
-rw-r--r--.   1 qkj qkj 11947 Aug  8 14:02 Jamroot
drwxr-xr-x. 148 qkj qkj  4096 Aug  8 14:40 libs
-rw-r--r--.   1 qkj qkj  1338 Aug  8 14:02 LICENSE_1_0.txt
drwxr-xr-x.   4 qkj qkj   159 Aug  8 14:02 more
-rw-r--r--.   1 qkj qkj   542 Aug  8 14:02 README.md
-rw-r--r--.   1 qkj qkj  2608 Aug  8 14:02 rst.css
drwxr-xr-x.   2 qkj qkj   171 Aug  8 14:02 status
drwxr-xr-x.  14 qkj qkj   256 Aug  8 14:02 tools
[qkj@localhost boost_1_83_0]$ 

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

boost_1_83_0/doc/html
[qkj@localhost doc]$ cd html/
[qkj@localhost html]$ ll
total 2900
-rw-r--r--.  1 qkj qkj   3476 Aug  8 14:24 about.html
drwxr-xr-x.  2 qkj qkj     82 Aug  8 14:25 accumulators
-rw-r--r--.  1 qkj qkj   5858 Aug  8 14:25 accumulators.html
drwxr-xr-x.  2 qkj qkj    168 Aug  8 14:26 align
-rw-r--r--.  1 qkj qkj   4440 Aug  8 14:26 align.html
drwxr-xr-x.  2 qkj qkj     78 Aug  8 14:26 any
-rw-r--r--.  1 qkj qkj   9011 Aug  8 14:26 any.html
drwxr-xr-x.  3 qkj qkj     78 Aug  8 14:26 array
-rw-r--r--.  1 qkj qkj   8377 Aug  8 14:26 array.html
-rw-r--r--.  1 qkj qkj  36597 Aug  8 14:30 array_types.html
-rw-r--r--.  1 qkj qkj 286811 Aug  8 14:29 asio_HTML.manifest
-rw-r--r--.  1 qkj qkj   6685 Aug  8 14:35 Assignable.html
-rw-r--r--.  1 qkj qkj    700 Aug  8 14:02 atomic.html
-rw-r--r--.  1 qkj qkj  20627 Aug  8 14:30 auxiliary.html
drwxr-xr-x.  2 qkj qkj     31 Aug  8 14:02 bbv2
...

下面我们要做的就是就是把boost_1_83_0/doc/html里面的所有内容保存到一个文件中.

[qkj@localhost boost_searcher]$ mkdir data/input -p
[qkj@localhost boost_searcher]$ cp -rf ../../install/boost_1_83_0/doc/html/* data/input/

我们看一下.

[qkj@localhost boost_searcher]$ cd data/input/
[qkj@localhost input]$ ll
total 2900
-rw-r--r--.  1 qkj qkj   3476 Sep  9 00:31 about.html
drwxr-xr-x.  2 qkj qkj     82 Sep  9 00:31 accumulators
-rw-r--r--.  1 qkj qkj   5858 Sep  9 00:31 accumulators.html
drwxr-xr-x.  2 qkj qkj    168 Sep  9 00:31 align
-rw-r--r--.  1 qkj qkj   4440 Sep  9 00:31 align.html
drwxr-xr-x.  2 qkj qkj     78 Sep  9 00:31 any
-rw-r--r--.  1 qkj qkj   9011 Sep  9 00:31 any.html
drwxr-xr-x.  3 qkj qkj     78 Sep  9 00:31 array
-rw-r--r--.  1 qkj qkj   8377 Sep  9 00:31 array.html

下面就可以去去标签了,这里创建一个文件.

[qkj@localhost boost_searcher]$ touch parser.cc

认识标签

在谈去标签之前,我们需要先认识一下标签.,我们随便打开的一个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 rel="nofollow" href="../../index.html">Home</a></td>    
<td align="center"><a rel="nofollow" href="../../libs/libraries.htm">Libraries</a></td>    
<td align="center"><a rel="nofollow" href="http://www.boost.org/users/people.html">People</a></td>  

像这种由<>包含的就是标签,一般而言,标签是成对出现的.这些标签对我们来说现在是没有价值的.我们需要把它给清晰了.对与清晰的数据我们也保存在一个文件中.

[qkj@localhost boost_searcher]$ mkdir data/raw_html -p
[qkj@localhost boost_searcher]$ cd data/
[qkj@localhost data]$ ll
total 16
drwxrwxr-x. 58 qkj qkj 12288 Sep  9 00:31 input     // 这里保存源html
drwxrwxr-x.  2 qkj qkj     6 Sep  9 00:44 raw_html  // 这里保存清晰后的html
[qkj@localhost data]$  

下面说一下我们该如何保存这些清晰后的文档内容,看一我们源html文件有多少个.

[qkj@localhost input]$ ls -Rl | grep -E "*.html" | wc -l
8581
[qkj@localhost input]$

这里我们可以对每一个源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

我们创建一个文件来保存我们去标签之后的内容.

drwxrwxr-x. 58 qkj qkj 12288 Sep  9 01:03 input
drwxrwxr-x.  2 qkj qkj     6 Sep  9 01:03 raw_html
[qkj@localhost data]$ 
[qkj@localhost data]$ cd raw_html/
[qkj@localhost raw_html]$ touch raw.txt
[qkj@localhost raw_html]$ ll
total 0
-rw-rw-r--. 1 qkj qkj 0 Sep  9 02:32 raw.txt

清晰标签框架

下面我们开始编写parser.cc简单框架内,我们看一下.

#include <iostream>
#include <string>
#include <vector>
#include <cassert>
// 这是一个目录,下面放的是所有的html网页
const std::string src_path = "data/input";

// 下面是一个文本文件,该文件保存所有的 网页清洗后的数据
const std::string output = "data/raw_html/raw.txt";

// 解析网页格式
typedef struct DocInfo
{
  std::string title;   // 文档标题
  std::string content; // 文旦内容
  std::string url;     // 该文档在官网的的url
} DocInfo_t;

static bool EnumFile(const std::string &src_path, std::vector<std::string> *file_list);
static bool ParseHtml(const std::vector<std::string> &file_list, std::vector<DocInfo_t> *results);
static bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);

int main(void)
{
  // 保存所有的 html 的文件名
  std::vector<std::string> file_list;

  // 第一步: EnumFile 枚举所有的文件名(带路径),仅限 网页,方便后期对一个一个文件进行读取
  if (false == EnumFile(src_path, &file_list))
  {
    std::cerr << "枚举文件名失败" << std::endl;
    return 1;
  }

  // 第二部:读取每一个文件的内容,进行解析,解析的格式 为DocInfo_t
  std::vector<DocInfo_t> results;
  if (false == ParseHtml(file_list, &results))
  {
    std::cerr << "解析文件失败" << std::endl;
    return 2;
  }

  // 第三步: 把解析文件的内容写入到output中,按照\3\n 作为每一个文档的分割符
  if (false == SaveHtml(results, output))
  {
    std::cerr << "保存文件失败" << std::endl;
    return 3;
  }
  return 0;
}

我们的的基本思路是下面这样的.

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

Boost库的安装

在实现上面的接口前,我们这里需要下载一个boost库,这是因为我们需要使用他们的函数.

[qkj@localhost BoostSearchEngine]$ sudo yum install -y boost-devel
[sudo] password for qkj: 

我们这里简单认识一下boost,下面是使用手册.

image-20230909162417630

我们要使用是的关于文件的函数,这里我们看一下.

image-20230909162857940

EnumFile函数实现

下面开始EnumFil函数的实现,它的功能是把我们给定src_path目录下的所有后缀是html的文件名字给保存下了,存在在一个file_list数组中.

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

具体的实现是.

static bool EnumFile(const std::string &src_path, std::vector<std::string> *file_list)
{
  assert(file_list);
  namespace fs = boost::filesystem; // 这是一个习惯, C++支持
  fs::path root_path(src_path);     // 定义一个path对象

  if (fs::exists(root_path) == false) // 判断路径是不是存在
  {
    std::cerr << src_path << " 路径是不存在的" << 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) == false)
    {
      // 这里是目录一类的
      continue;
    }

    // 普通文件需要 html 文件后缀结束
    if (iter->path().extension() != ".html")
    {
      continue;
    }

     std::cout << "debug: " << iter->path().string() << std::endl;

    // 此时一定 是以 html 后缀结尾的普通文件
    file_list->push_back(iter->path().string());
  }

  return true;
}

下面我们测试一下,写一些Makefile.

cc=g++
parser:parser.cc 
	$(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem
.PHONY:clean
clean:
	rm parser

下面运行一下,我们发现成功了.

[qkj@localhost BoostSearchEngine]$ make
g++ -o parser parser.cc -std=c++11 -lboost_system -lboost_filesystem
[qkj@localhost BoostSearchEngine]$ ll
total 104
drwxrwxr-x. 4 qkj qkj    35 Sep  9 01:03 data
-rw-rw-r--. 1 qkj qkj   117 Sep  9 01:41 Makefile
-rwxrwxr-x. 1 qkj qkj 89152 Sep  9 01:43 parser
-rw-rw-r--. 1 qkj qkj  8398 Sep  9 01:43 parser.cc
[qkj@localhost BoostSearchEngine]$ ./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
...

ParseHtml实现

这里我们开始解析我们的每一个html目录.

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

下面是我们的框架.

static bool ParseTitle(const std::string &file, std::string *title);
static bool ParseContent(const std::string &file, std::string *content);
static bool ParseUrl(const std::string &file_path, std::string *url);

static bool ParseHtml(const std::vector<std::string> &file_list, std::vector<DocInfo_t> *results)
{
  assert(results);
  for (auto &file_path : file_list)
  {
    // 1. 读取文件
    std::string result;
    if (false == ns_util::FileUtil::ReadFile(file_path, &result))
    {
      continue;
    }

    DocInfo_t doc;
    // 2. 提取title
    if (false == ParseTitle(result, &doc.title))
    {
      continue;
    }
    // 3. 提取content  本质时 去标签
    if (false == ParseContent(result, &doc.content))
    {
      continue;
    }
    // 4. 提取url
    if (false == ParseUrl(file_path, &doc.url))
    {
      continue;
    }
    // 到这里一定时完成了解析任务
    results->push_back(std::move(doc)); // 右值引用
  }
  return true;
}

我们说一下我们的流程

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

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

读取文件内容

对于这个函数,我们把它放在一个工具集中,后面可能会使用到.

#pragma once
#include <iostream>
#include <assert.h>
#include <fstream>
#include <string>
// 这是一个工具集
namespace ns_util
{
  /// @brief  这是为了解析文件
  class FileUtil
  {
  public:
    /// @brief 读取文件内容到 out中
    /// @param file_path
    /// @param out
    /// @return
    static bool ReadFile(const std::string &file_path, std::string *out)
    {
      assert(out);
      std::ifstream in(file_path, std::ios::in);
      if (in.is_open() == false)
      {
        std::cerr << file_path << " 打开失败" << std::endl;
        return false;
      }

      std::string line;
      // 注意 getline 不会 读取 \n
      while (std::getline(in, line))
      {
        *out += line;
      }

      in.close();
      return true;
    }
  };
}
提取titile

我们这里继续看一下我们的一个html文件,title是在一个标签里面的.

image-20230909165910185

下面根据字符串来进行提取title.

static bool ParseTitle(const std::string &file, std::string *title)
{
  assert(title);
  std::size_t begin = file.find("<title>");

  if (begin == std::string::npos)
  {
    return false;
  }

  std::size_t end = file.find("</title>"); // 反方向查
  if (end == std::string::npos)
  {
    return false;
  }

  begin += std::string("<title>").size();
  if (begin > end)
  {
    return false;
  }
  *title = file.substr(begin, end - begin);
  return true;
}
提取content

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

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

static bool ParseContent(const std::string &file, std::string *content)
{
  assert(content);
  // 这就是我们去标签最重要的地方
  // 我们这里使用一个简单的状态机
  enum status
  {
    LABLE,
    CONTENT
  };
  
  enum status s = LABLE; // 默认第一个是 '<'

  for (char ch : file) // 注意这里我没有使用引用,后面解释
  {
    switch (s)
    {
    case LABLE:
      if (ch == '>')
      {
        // 此时意味这当前的标签被处理完毕
        s = CONTENT;
      }
      break;

    case CONTENT:
      if (ch == '<')
      {
          // 这里有可能是<><>这样的情况
        s = LABLE;
      }
      else
      {
        // 这里有一个细节 我们不想要'\n' 字符
        // 我们希望用'\n' 作为分隔符
        // 注意,这个应该不会出现\n,
        // 毕竟我们读取文件的时候使用的getline,可是不我们不能把希望寄托到被人身上
        if (ch == '\n')
        {
          ch = ' ';
        }
        content->push_back(ch);
      }
      break;

    default:
      break;
    }
  }
  return true;
}
提取url

这里面有一个需要谈的.我们这里是要凭借url,那么我么看一下官网的url和我们的本地的url是有什么关系的.

官网url: https://www.boost.org/doc/libs/1_83_0/doc/html/accumulators.html
本地url: data/input/accumulators.html                   // 这是因为为我们把doc/html/里面的内容拷贝到data/input中的

// 这里我们要拼接url
url_head = "https://www.boost.org/doc/libs/1_83_0/doc/html";
url_tail = [data/input](删除) /accumulators.html
         => url_tail = /accumulators.html

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

下面就是我们的代码

static bool ParseUrl(const std::string &file_path, std::string *url)
{
  assert(url);
    
  //  url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html"
  //  url_tail = "/accumulators.html"
  std::string url_head = "https://www.boost.org/doc/libs/1_83_0/doc/html";
  std::string url_tail = file_path.substr(src_path.size());
  *url = url_head + url_tail;

  return true;
}

下面我们测试验证一下,使用一个函数.

void ShowDoc(const DocInfo_t &doc)
{
  std::cout << "title: " << doc.title << std::endl;
  std::cout << "content: " << doc.content << std::endl;
  std::cout << "url: " << doc.url << std::endl;
}
static bool ParseHtml(const std::vector<std::string> &file_list, std::vector<DocInfo_t> *results)
{
  assert(results);
  for (auto &file_path : file_list)
  {
    // 1. 读取文件
    std::string result;
    if (false == ns_util::FileUtil::ReadFile(file_path, &result))
    {
      continue;
    }

    DocInfo_t doc;
    // 2. 提取title
    if (false == ParseTitle(result, &doc.title))
    {
      continue;
    }
    // 3. 提取content  本质时 去标签
    if (false == ParseContent(result, &doc.content))
    {
      continue;
    }
    // 4. 提取url
    if (false == ParseUrl(file_path, &doc.url))
    {
      continue;
    }
    // for debug
    ShowDoc(doc);
    // break;
    // 到这里一定时完成了解析任务
    results->push_back(std::move(doc)); // 右值引用
  }

  return true;
}

这个是我们的测定结果.

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_83_0/doc/html/boost/proto/functional/distance/resu_1_3_32_5_26_2_1_1_2_4.html

我们拿到这个url去官网上看看是不是,我们发现是的.

image-20230909172834315

SaveHtml实现

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

我们已经得到每一个文件的结构体了,下面我们开始保存文件到要求的文件中.

static bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP "\3"
  // 我们按照下面的方式,要知道我们把文档的内容去掉了\n
  // title\3content\3url\n title\3content\3url\n title\3content\3url\n return true;

  // explicit basic_ofstream (const char* filename,
  //                       ios_base::openmode mode = ios_base::out);
  std::ofstream out(output, std::ios::out | std::ios::binary);

  if (out.is_open() == false)
  {
    std::cerr << "打开文件失败 " << output << std::endl;
    return false;
  }

  for (auto &e : results)
  {
    std::string str = e.title;
    str += SEP;

    str += e.content;
    str += SEP;

    str += e.url;
    str += "\n";
    out.write(str.c_str(), str.size());
  }
  out.close();
  return true;
}

这里验证是不是保存了.

image-20230909173908116

这里我们验证下是不是保存完全了.

[qkj@localhost BoostSearchEngine]$ ls ./data/input/ -Rl | grep -E "*.html" | wc -l
8581
[qkj@localhost BoostSearchEngine]$ cat ./data/raw_html/raw.txt | wc -l
8581
[qkj@localhost BoostSearchEngine]$ 

建立索引

下面我们就要建立索引的,建立索引实际上就是构建存储+搜索的数据结构,来加快我们对于关键字->文档ID->文档内容的搜索过程。根据上面谈的,我们建立正派索引和倒排索引.

jieba安装与使用

对于分词,这里我们使用cppjieba分词工具,我们执行下面的命令就可以了.

[qkj@localhost install]$ git clone https://github.com/yanyiwu/cppjieba.git

这里我们看一下cppjieba的具体内容.

[qkj@localhost install]$ tree cppjieba/
cppjieba/
├── 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
├── 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
│       ├── MixSegment.hpp
│       ├── MPSegment.hpp
│       ├── PosTagger.hpp
│       ├── PreFilter.hpp
│       ├── QuerySegment.hpp
│       ├── SegmentBase.hpp
│       ├── SegmentTagged.hpp
│       ├── TextRankExtractor.hpp
│       ├── Trie.hpp
│       └── Unicode.hpp
├── LICENSE
├── 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

16 directories, 98 files
[qkj@localhost install]$ 

这里我们要关注的是两个文件.

  • cppjieba/include : 我们的头文件
  • cppjiba/dict : 我们的字典

下面我们开始jiebba分词的使用,里面存在一个demo.cpp文件供我们测试在,这里我们把它拷贝到一个位置.

[qkj@localhost test]$ pwd
/home/qkj/install/cppjieba/test
[qkj@localhost test]$ ll
total 16
-rw-rw-r--. 1 qkj qkj  148 Sep  9 03:38 CMakeLists.txt
-rw-rw-r--. 1 qkj qkj 2797 Sep  9 03:38 demo.cpp
-rw-rw-r--. 1 qkj qkj 1532 Sep  9 03:38 load_test.cpp
drwxrwxr-x. 4 qkj qkj 4096 Sep  9 03:38 testdata
drwxrwxr-x. 2 qkj qkj  255 Sep  9 03:38 unittest
[qkj@localhost test]$ cp demo.cpp ../..
[qkj@localhost test]$ cd ../../
[qkj@localhost install]$ ll
total 8
drwxr-xr-x. 8 qkj qkj 4096 Aug  8 14:40 boost_1_83_0
drwxrwxr-x. 8 qkj qkj  215 Sep  9 03:38 cppjieba
-rw-rw-r--. 1 qkj qkj 2797 Sep  9 03:49 demo.cpp
[qkj@localhost install]$ 

首先,我们不能直接编译,它会报错.

[qkj@localhost install]$ g++ demo.cpp 
demo.cpp:1:10: fatal error: cppjieba/Jieba.hpp: No such file or directory
 #include "cppjieba/Jieba.hpp"
          ^~~~~~~~~~~~~~~~~~~~
compilation terminated.
[qkj@localhost install]$ 

这是因为我们这里的库和头文件的路径是不对的,这里添加软链接.

[qkj@localhost install]$ ln -s  cppjieba/include/ inc
[qkj@localhost install]$ ln -s  cppjieba/dict/ dict
[qkj@localhost install]$ ll
total 8
drwxr-xr-x. 8 qkj qkj 4096 Aug  8 14:40 boost_1_83_0
drwxrwxr-x. 8 qkj qkj  215 Sep  9 03:38 cppjieba
-rw-rw-r--. 1 qkj qkj 2797 Sep  9 03:49 demo.cpp
lrwxrwxrwx. 1 qkj qkj   14 Sep  9 03:50 dict -> cppjieba/dict/
lrwxrwxrwx. 1 qkj qkj   17 Sep  9 03:50 inc -> cppjieba/include/
[qkj@localhost install]$ cp -rf cppjieba/deps/limonp/ cppjieba/include/cppjieba/
[qkj@localhost install]$ 

下面我们要修改demo.cpp文件.

image-20230909202943634

下面我们继续编译,我们发现还是出现错误.

[qkj@localhost install]$ g++ demo.cpp 
In file included from inc/cppjieba/Jieba.hpp:4,
                 from demo.cpp:1:
inc/cppjieba/QuerySegment.hpp:7:10: fatal error: limonp/Logging.hpp: No such file or directory
 #include "limonp/Logging.hpp"
          ^~~~~~~~~~~~~~~~~~~~
compilation terminated.

这是因为cppjieba/deps/limonp实际上是空文件夹

[qkj@localhost install]$ cd  cppjieba/include/cppjieba/limonp/
[qkj@localhost limonp]$ ll
total 0
[qkj@localhost limonp]$ 

这里需要我们手动去下载这个目录.

[qkj@localhost install]$ git clone https://github.com/yanyiwu/limonp.git

然后把我们下载好的目录拷贝到cppjieba/deps/limonp,然后重新拷贝到cppjieba/include/cppjieba/.

[qkj@localhost install]$ cp -rf limonp/include/limonp/ cppjieba/deps/
[qkj@localhost install]$ cp -rf cppjieba/deps/limonp/ cppjieba/include/cppjieba/
[qkj@localhost install]$ 

这样就可以了,我们这里编译一下.

[qkj@localhost install]$ g++ demo.cpp -std=c++11
[qkj@localhost install]$ ll
total 480
-rwxrwxr-x. 1 qkj qkj 482896 Sep  9 05:50 a.out
drwxr-xr-x. 8 qkj qkj   4096 Aug  8 14:40 boost_1_83_0
drwxrwxr-x. 8 qkj qkj    215 Sep  9 03:38 cppjieba
-rw-rw-r--. 1 qkj qkj   2852 Sep  9 05:28 demo.cpp
lrwxrwxrwx. 1 qkj qkj     14 Sep  9 03:50 dict -> cppjieba/dict/
lrwxrwxrwx. 1 qkj qkj     17 Sep  9 03:50 inc -> cppjieba/include/
drwxrwxr-x. 6 qkj qkj    171 Sep  9 05:46 limonp
[qkj@localhost install]$ ./a.out 
他来到了网易杭研大厦
[demo] Cut With HMM
他/来到/了/网易/杭研/大厦
[demo] Cut Without HMM 
他/来到/了/网易/杭/研/大厦
我来到北京清华大学
[demo] CutAll
我/来到/北京/清华/清华大学/华大/大学
小明硕士毕业于中国科学院计算所,后在日本京都大学深造
[demo] CutForSearch
小明/硕士/毕业/于/中国/科学/学院/科学院/中国科学院/计算/计算所/,/后/在/日本/京都/大学/日本京都大学/深造

索引框架

下面我们创建一个文件.

[qkj@localhost BoostSearchEngine]$ touch index.hpp
[qkj@localhost BoostSearchEngine]$ ll
total 124
drwxrwxr-x. 4 qkj qkj     35 Sep  9 01:03 data
-rw-rw-r--. 1 qkj qkj      0 Sep  9 02:48 index.hpp
-rw-rw-r--. 1 qkj qkj    117 Sep  9 01:41 Makefile
-rwxrwxr-x. 1 qkj qkj 110008 Sep  9 02:48 parser
-rw-rw-r--. 1 qkj qkj   6361 Sep  9 02:47 parser.cc
-rw-rw-r--. 1 qkj qkj    783 Sep  9 02:48 util.hpp
[qkj@localhost BoostSearchEngine]$ 

这里我们需要明确是我们要建立正排和倒排索引.并且我们还要提供一个两个查找的接口.

namespace ns_index
{
  struct DocInfo
  {
    std::string title;   // 文档标题
    std::string content; // 文档内容
    std::string url;     // 官网url

    uint64_t doc_id; // 文旦的id 暂时不做理解
  };

  /// @brief 作为倒排索引的辅助
  struct InvertedElem
  {
    uint64_t doc_id;  // 文旦id
    std::string word; // 关键字
    int weight;       // 权重 -->后面解释
  };

  // 倒排拉链  -- 根据用一个关键字 来拿到一组的InvertedElem
  typedef std::vector<struct InvertedElem> InvertedList;

  class Index
  {
  public:

    /// @brief 根据doc_id来获取正派索引 ,也就是文旦内容
    /// @param doc_id  文旦id
    /// @return 返回文档结构体的地址
    struct DocInfo *GetForwardIndex(const uint64_t doc_id)
    {
        return nullptr;
    }

    /// @brief 根据关键字 获取倒排拉链
    /// @param word 关键
    /// @return
    InvertedList *GetInvertedList(const std::string &word)
    {
		return nullptr;
    }

    /// @brief 根据目录 文件 构建 正派和倒排索引,这里是最重的一步
    /// @param src_path 去标签后目录文件目录
    /// @return
    bool BuildIndex(const std::string &src_path)
    {
      // 建立正排
      // 建立倒排
      return true;
    }
      
    /// @brief 根据字符串建立正派索引  也就是根据文旦id找到 文档内容
    /// @param line 一个字符串,该字符串保留一个html文档的所有内容
    /// @return
    DocInfo *BuildForwardIndex(const std::string &line)
    {
        return nullptr;
    }
private:
    // 这两个结构不暴露给外部
    /// @brief 根据一个文档内容的结构体建立倒排索引,需要经行分词 
    /// @param doc  这个是一个结构体
    /// @return
    bool BuildInvertedIndex(const DocInfo &doc)
    {
      return true;
    }
  private:
    // 正排索引 -- 根据vector下标可以更加高效作为id找到内容
    std::vector<struct DocInfo> forward_index;
    // 倒排索引 一个关键字 可能在很多的文档中出现,一定是一个关键字和一组InvertedElem对应
    std::unordered_map<std::string, InvertedList> inverted_index;
  };
}

下面我们依次实现这里面的函数.

BuildIndex 构建索引

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

这个是根据我们已经清洗好的数据,通过它来构建索引.

bool BuildIndex(const std::string &src_path)
{
  std::ifstream in(src_path, std::ios::in | std::ios::binary);

  if (in.is_open() == false)
  {
    std::cerr << "文件目录 " << src_path << "无效" << std::endl;
    return false;
  }

  int count = 0; // 他的作用是让我们看到构建索引的过程
  std::string line; 
  while (std::getline(in, line))
  {
    // 此时我们已经提取到每一个html内容了
    // 建立正派索引
    DocInfo *doc = BuildForwardIndex(line); 
    
    if (doc == nullptr)
    {
      std::cerr << "建立一个正派索引失败" << line << std::endl;
      continue;
    }

    // 建立 倒排索引
    BuildInvertedIndex(*doc);
    count++;
    if (count % 50 == 0)
    {
      // 后期加上一个进度条
       std::cout << "当前已经处理了 索引文档 " << count << std::endl;
    }
  }
  return true;
}
建立正排索引

这个是在是太好实现了,我们数组下标天然是我们的文档ID,只需要把清晰后每一个文档的内容处理成结构体,然后添加到数组中就可以了.

/// @brief 根据字符串建立正派索引  也就是根据文旦id找到 文档内容
/// @param line 一个字符串,该字符串保留一个html文档的所有内容
/// @return
DocInfo *BuildForwardIndex(const std::string &line)
{
  // title\3content\3url\n

  std::vector<std::string> results;
  const std::string sep = "\3";
  ns_util::StringUtil::Split(line, &results, sep); // 这里是工具集里面切分字符串

  if (results.size() != 3)
    return nullptr;

  DocInfo doc;
  doc.title = results[0];
  doc.content = results[1];
  doc.url = results[2];
  // 文档id,就是数组下标
  doc.doc_id = forward_index.size(); // 注意这里是 正派拉链

  forward_index.push_back(std::move(doc));
  return &(forward_index[forward_index.size() - 1]);
}

把工具集里面的代码写一下.

/// @brief 字符串切分
class StringUtil
{
public:
    static void Split(const std::string &target, std::vector<std::string> *out, const std::string sep)
    {
      assert(out);
      // 我们这里使用现成的切分函数
      boost::split(*out, target, boost::is_any_of(sep),
                   boost::token_compress_on);
	}
};
建立倒排索引

下面我们开始根据最新的结构体建立倒排索引.这里我们需要分词.

struct word_cnt
{
  int title_cnt;
  int content_cnt;
  word_cnt() : title_cnt(0), content_cnt(0) {}
};

bool BuildInvertedIndex(const DocInfo &doc)
{

  // 用来暂存 词频
  std::unordered_map<std::string, word_cnt> word_map;
  
  // 1.对标题 分词
  std::vector<std::string> title_words;
  ns_util::JiebaUtil::CutString(doc.title, &title_words);

  // 不区分大小写
  // 那么用户也不因该区分大小写
  for (std::string s : title_words)
  {
    boost::to_lower(s); 
    word_map[s].title_cnt++; // 解释一下
  }

    
  // 对文档内容分词
  std::vector<std::string> content_words;
  ns_util::JiebaUtil::CutString(doc.content, &content_words);

  for (auto s : content_words)
  {
    boost::to_lower(s);
    word_map[s].content_cnt++;
  }
  
  // 到这里每一个词都有它的在标题和内容中出现的次数
   
  // 3 构建倒排拉链
  for (auto &word_pair : word_map)
  {
    /*
    struct InvertedElem
    {
        uint64_t doc_id;  // 文旦id
        std::string word; // 关键字
        int weight;       // 权重 -->后面解释
    };
    */
    
    InvertedElem item; 
    
    item.doc_id = doc.doc_id; // 这里解释了上面我们为何添加了id
    item.word = word_pair.first;
    item.weight = _build_relevance(word_pair.second); // 这里是计算权重的
    
    
    // 加入倒排拉链中
    // typedef std::vector<struct InvertedElem> InvertedList;
    // std::unordered_map<std::string, InvertedList> inverted_index;
    InvertedList &inverted_list = inverted_index[word_pair.first];
    inverted_list.push_back(std::move(item));
  }

  return true;
}
引入jieba

由于倒排索引需要分词,这里我们引入jiebe,这里我们把切分字符串写成一个工具.这是使用软链接.

[qkj@localhost BoostSearchEngine]$ ln -s /home/qkj/install/cppjieba/include/cppjieba cppjieba
[qkj@localhost BoostSearchEngine]$ ln -s /home/qkj/install/cppjieba/dict/ dict
[qkj@localhost BoostSearchEngine]$ ll
total 24
lrwxrwxrwx. 1 qkj qkj   43 Sep  9 06:00 cppjieba -> /home/qkj/install/cppjieba/include/cppjieba
drwxrwxr-x. 4 qkj qkj   35 Sep  9 01:03 data
lrwxrwxrwx. 1 qkj qkj   32 Sep  9 06:01 dict -> /home/qkj/install/cppjieba/dict/
-rw-rw-r--. 1 qkj qkj 6379 Sep  9 03:15 index.hpp
-rw-rw-r--. 1 qkj qkj  117 Sep  9 01:41 Makefile
-rw-rw-r--. 1 qkj qkj 6361 Sep  9 02:47 parser.cc
-rw-rw-r--. 1 qkj qkj 1199 Sep  9 03:15 util.hpp
[qkj@localhost BoostSearchEngine]$ 

这里就可以编写我们的切词工具了.

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

/// @brief 这是一个jieba分词
class JiebaUtil
{
public:
    static void CutString(const std::string &src, std::vector<std::string> *out)
    {
    	assert(out);
    	jieba.CutForSearch(src, *out);
	}
private:
	static cppjieba::Jieba jieba;
};
cppjieba::Jieba JiebaUtil::jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
权重计算

先来解释一下什么是权重,可以这么理解.对于搜索频率高的单词,我们认为它的权重高.同时对一个文档,如果关键字出现的次数越多,起权重越大.这里我么权重结算简单些.

    int _build_relevance(const struct word_cnt &word)
    {
#define X 10
#define Y 1
      return X * word.title_cnt + Y * word.content_cnt;
    }

那么权重有什么作用呢?这里可以等我们搜索的时候,一个关键字可以对应多个文档,那么此时我们可以把权重高的放在前面.

现在我们的结构是这样的.

image-20230909183122524

GetForwardIndex

这个是根据文档的id找到文档的内容.

struct DocInfo *GetForwardIndex(const uint64_t doc_id)
{
  if (doc_id < 0 || doc_id >= forward_index.size())
  {
    std::cerr << "索引id " << doc_id << " 越界了" << std::endl;
    return nullptr;
  }

  return &(forward_index[doc_id]);
}

GetInvertedList

这个是根据关键字拿到倒排拉链.

InvertedList *GetInvertedList(const std::string &word)
{
  auto it = inverted_index.find(word);
  if (it == inverted_index.end())
  {
    std::cerr << "关键字 " << word << " 不存在" << std::endl;
    return nullptr;
  }

  return &(it->second);
}

这里还剩下一个小工作,后面我们把index设置为单例模式.

设置成单例

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

namespace ns_index
{
  struct DocInfo
  {
    std::string title;   // 文档标题
    std::string content; // 文档内容
    std::string url;     // 官网url

    uint64_t doc_id; // 文旦的id 暂时不做理解
  };

  /// @brief 作为倒排索引的辅助
  struct InvertedElem
  {
    uint64_t doc_id;  // 文旦id
    std::string word; // 关键字
    int weight;       // 权重
  };

  // 倒排拉链  -- 根据用一个关键字 来拿到一组的InvertedElem
  typedef std::vector<struct InvertedElem> InvertedList;

  class Index
  {

  private:
    Index() {}
    Index(const Index &) = delete;
    Index &operator=(const Index &) = delete;
    static Index *instance;
    static std::mutex mtx;

  public:
    ~Index()
    {
    }
    static Index *GetInstance()
    {
      // 线程不安全,加锁
      if (nullptr == instance)
      {
        mtx.lock();
        if (instance == nullptr)
        {
          instance = new Index;
        }
        mtx.unlock();
      }
      return instance;
    }

    /// @brief 根据doc_id来获取正派索引 ,也就是文旦内容
    /// @param doc_id  文旦id
    /// @return 返回文档结构体的地址
    struct DocInfo *GetForwardIndex(const uint64_t doc_id)
    {
      if (doc_id < 0 || doc_id >= forward_index.size())
      {
        std::cerr << "索引id " << doc_id << " 越界了" << std::endl;
        return nullptr;
      }

      return &(forward_index[doc_id]);
    }

    /// @brief 根据关键字 获取倒排拉链
    /// @param word 关键
    /// @return
    InvertedList *GetInvertedList(const std::string &word)
    {
      auto it = inverted_index.find(word);
      if (it == inverted_index.end())
      {
        std::cerr << "关键字 " << word << " 不存在" << std::endl;
        return nullptr;
      }

      return &(it->second);
    }

    /// @brief 根据目录 文件 构建 正派和倒排索引,这里是最重的一步
    /// @param src_path 去标签后目录文件目录
    /// @return
    bool BuildIndex(const std::string &src_path)
    {
      std::ifstream in(src_path, std::ios::in | std::ios::binary);

      if (in.is_open() == false)
      {
        std::cerr << "文件目录 " << src_path << "无效" << std::endl;
        return false;
      }

      int count = 0;
      std::string line;
      while (std::getline(in, line))
      {
        // 此时我们已经提取到每一个html内容了
        // 建立正派索引
        DocInfo *doc = BuildForwardIndex(line);
        if (doc == nullptr)
        {
          std::cerr << "建立一个正派索引失败" << line << std::endl;
          continue;
        }

        // 建立 倒排索引
        BuildInvertedIndex(*doc);
        count++;
        if (count % 50 == 0)
        {
          // 后期加上一个进度条
          // LOG(NORMAL, "当前已经处理了 " + std::to_string(count) + " 个文档");
          std::cout << "当前已经处理了 索引文档 " << count << std::endl;
        }
      }

      return true;
    }

  private:
    /// @brief 根据字符串建立正派索引  也就是根据文旦id找到 文档内容
    /// @param line 一个字符串,该字符串保留一个html文档的所有内容
    /// @return
    DocInfo *BuildForwardIndex(const std::string &line)
    {
      // title\3content\3url\n

      std::vector<std::string> results;
      const std::string sep = "\3";
      
     ns_util::StringUtil::Split(line, &results, sep);

      if (results.size() != 3)
        return nullptr;

      DocInfo doc;
      doc.title = results[0];
      doc.content = results[1];
      doc.url = results[2];
      doc.doc_id = forward_index.size(); // 注意这里是 正派拉链

      forward_index.push_back(std::move(doc));
      return &(forward_index[forward_index.size() - 1]);
    }

    // 为了词频统计
    struct word_cnt
    {
      int title_cnt;
      int content_cnt;
      word_cnt() : title_cnt(0), content_cnt(0) {}
    };

    /// @brief 根据一个文档内容的结构体建立倒排索引,需要经行分词  --
    /// @param doc  这个是一个结构体
    /// @return
    bool BuildInvertedIndex(const DocInfo &doc)
    {

      // 用来暂存 词频
      std::unordered_map<std::string, word_cnt> word_map;
      // 1.对标题 分词
      std::vector<std::string> title_words;
      ns_util::JiebaUtil::CutString(doc.title, &title_words);

      // 不区分大小写
      // 那么用户也不因该区分大小写
      for (std::string s : title_words)
      {
        boost::to_lower(s);
        word_map[s].title_cnt++; // 解释一下
      }

      std::vector<std::string> content_words;
      ns_util::JiebaUtil::CutString(doc.content, &content_words);

      for (auto s : content_words)
      {
        boost::to_lower(s);
        word_map[s].content_cnt++;
      }
      // 3 构建倒排拉链
      for (auto &word_pair : word_map)
      {
        InvertedElem item;
        item.doc_id = doc.doc_id; // 这里解释了上面我们为何添加了id
        item.word = word_pair.first;
        item.weight = _build_relevance(word_pair.second);

        // 加入倒排拉链中
        InvertedList &inverted_list = inverted_index[word_pair.first];
        inverted_list.push_back(std::move(item));
      }

      return true;
    }

  private:
    /// @brief 构建权重
    /// @param word
    /// @return
    int _build_relevance(const struct word_cnt &word)
    {
#define X 10
#define Y 1
      return X * word.title_cnt + Y * word.content_cnt;
    }

  private:
    // 正排索引 -- 根据vector下标可以更加高效作为id找到内容
    std::vector<struct DocInfo>
        forward_index;
    // 倒排索引 一个关键字 可能在很多的文档中出现,一定是一个关键字和一组InvertedElem对应
    std::unordered_map<std::string, InvertedList> inverted_index;
  };

  Index *Index::instance = nullptr;
  std::mutex Index::mtx;
}

搜索引擎模块

下面我们开始编写搜索模块,这里我们先来写出基本代码结构.我们也创建一个文件.

[qkj@localhost BoostSearchEngine]$ touch searcher.hpp 

下面是我们的框架.

namespace ns_searcher
{

  struct InvertedElemPrint
  {
    uint64_t doc_id; // 文旦id

    int weight;                     // 权重
    std::vector<std::string> words; // 关键字>
    InvertedElemPrint() : doc_id(0), weight(0) {}
  };

  class Searcher
  {
  public:
    Searcher() {}
    ~Searcher() {}
    //input 这个是我们去标签后面的文件
    void InitSearcher(const std::string &input)
    {
        // 1. 获取index
        // 2. 根绝index建立索引
    }
    // query: 这个是我们要搜索的词或者是语句
    // json_string: 这个是我们结果,是一个json串
    void Search(const std::string &query, std::string *json_string)
    {
        //1. 分词 我们的搜索的语句,注意转成小写
        //2. 根据关键字,拿到倒排拉链,
        //3. 合并排序: 根据我们的结果按照权重进行降序排序
        //4. 构建json串
    }

  private:
    ns_index::Index *index; // 提供系统经行查找索引
  };
}

InitSearcher

这个是我们初始化的工作,一共两个内容.

  • 拿到index对象
  • 根据index建立索引
void InitSearcher(const std::string &input)
{
  // 获取创建index对象
  index = ns_index::Index::GetInstance();
  // std::cout << "获取单例成功" << std::endl;
  //  根据index对象建立索引
  index->BuildIndex(input);
  // std::cout << "建立正派倒排索引成功" << std::endl;
}

Search

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

  • 切分输入的内容,小写的保存在数组中
  • 根据额数组的每一个元素,拿到倒排拉链,然后把所有的倒排拉量的内容保存在一个拉链中
  • 我们以降序的方式排序整个拉链
  • 根据拉链的id找到文档内容,构建json串
void Search(const std::string &query, std::string *json_string)
{
  // 1 分词  先来分词后面在进行查找
  std::vector<std::string> words;
  ns_util::JiebaUtil::CutString(query, &words);
  // 2 根据分词结果依次触发  搜索
  ns_index::InvertedList inverted_list_all; // 保存所有的倒排拉链里面的内容

  for (std::string s : words)
  {
    boost::to_lower(s); // 建立索引的时候是忽略大小写的,我们搜索的时候也需要

    // 先查倒排
    ns_index::InvertedList *inverted_list = index->GetInvertedList(s);
    if (nullptr == inverted_list)
    {
      continue;
    }

    // 此时找到了 保存所有的 拉链里面的值
    // 不完美 一个词可能和多个文档相关 一个文档可以和多个关键词相关.
    inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
  }
  std::sort(inverted_list_all.begin(), inverted_list_all.end(),
            [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
            {
              return e1.weight > e2.weight;
            });

  // 4 构建json串 使用序列化和反序列化
}
*json_string = writer.write(root);

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

关键字 文档ID
你好 1, 2
1, 2
1, 2
大学生 1
社会人 2

我们把"你好,我"进行分词,然后得到拉链,放在总拉链里面,这就是[文档1, 文档2,文档1, 文档2],这我们后期弥补.

jsoncpp安装与使用

下面我们需要说一下jsoncpp的安装与使用.毕竟我们这里要构建json串.json是序列化和反序列化的.

[qkj@localhost BoostSearchEngine]$ sudo yum install -y jsoncpp-devel

下面我们使用一下json.

[qkj@localhost install]$ touch test.cc
#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;
}

下面就是我们的结果.

[qkj@localhost install]$ g++ test.cc  -ljsoncpp
[qkj@localhost install]$ ./a.out 
[
   {
      "key1" : "value11",
      "key2" : "value22"
   },
   {
      "key1" : "value1",
      "key2" : "value2"
   }
]

[qkj@localhost install]$ 

下面我们继续编写这个代码.

void Search(const std::string &query, std::string *json_string)
{
  // 1 分词  先来分词后面在进行查找
  std::vector<std::string> words;
  ns_util::JiebaUtil::CutString(query, &words);
  // 2 根据分词结果依次触发  搜索
  ns_index::InvertedList inverted_list_all; // 保存所有的倒排拉链里面的内容

  for (std::string s : words)
  {
    boost::to_lower(s); // 建立索引的时候是忽略大小写的,我们搜索的时候也需要

    // 先查倒排
    ns_index::InvertedList *inverted_list = index->GetInvertedList(s);
    if (nullptr == inverted_list)
    {
      continue;
    }

    // 此时找到了 保存所有的 拉链里面的值
    // 不完美 一个词可能和多个文档相关 一个文档可以和多个关键词相关.
    inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
  }
  std::sort(inverted_list_all.begin(), inverted_list_all.end(),
            [](const ns_index::InvertedElem &e1, const ns_index::InvertedElem &e2)
            {
              return e1.weight > e2.weight;
            });
  // 4 构建json串 使用序列化和反序列化
  Json::Value root;
  for (auto &item : inverted_list_all)
  {
    // 此时拿到正派
    ns_index::DocInfo *doc = index->GetForwardIndex(item.doc_id);
    if (nullptr == doc)
    {
      continue;
    }

    // 获取了 文档内容
    Json::Value elem;
    elem["title"] = doc->title;
    elem["desc"] = doc->content;
    elem["url"] = doc->url;

    root.append(elem); // 这里是有序的
  }

   Json::StyledWriter writer; // 这里我们暂时用这个格式
  *json_string = writer.write(root);
}

搜索测试

下面我们这里统一做一个搜索测试.

#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 (true)
  {
    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;
}

下面是Mekefile.

cc=g++
PARSER=parser
SSVR=search_server 

.PHONY:all
all:$(PARSER) $(SSVR)

$(SSVR):server.cc
	$(cc) -o $@ $^ -std=c++11  -lboost_system -lboost_filesystem -ljsoncpp

$(PARSER):parser.cc
	$(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem

.PHONY:clean
clean:
	rm -f $(PARSER) $(SSVR)

下面我们测试一下.这是一个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_83_0/doc/html/boost/process/v2/bound_launcher.html"
   },

获取摘要

void Search(const std::string &query, std::string *json_string)
{
  // ...
  // 4 构建json串 使用序列化和反序列化
  Json::Value root;
  for (auto &item : inverted_list_all)
  {
    // ....
    // 获取了 文档内容
    Json::Value elem;
    elem["title"] = doc->title;
    elem["desc"] = make_summary(doc->content, item.word); // 我们需要根据关键字来提取摘要
    elem["url"] = doc->url;

    root.append(elem); // 这里是有序的
  }

   Json::StyledWriter writer; // 这里我们暂时用这个格式
  *json_string = writer.write(root);
}

首先我们可以随便切分,但是一般我们想要与搜索关键字相关的内容.

std::string make_summary(const std::string &content, const std::string &word)
{
  // 这里有点问题  content是正排索引的里面的内容,是区分大小写的 是文档内容,不区分大小写  word 确是 小的的
  //  这里获取摘要有点问题,关键字不一定会出现在内容中, 注意是非常小的概率
  // std::size_t pos = content.find(words);
  // if (pos == std::string::npos)
  //   return "Node";

  auto item = std::search(content.begin(), content.end(), word.begin(), word.end(),
                          [](int x, int y)
                          {
                            return std::tolower(x) == std::tolower(y);
                          });
  if (item == content.end())
    return "Node";

  // 找到了 计算 跌打器到begin的距离
  std::size_t pos = std::distance(content.begin(), item);
  const std::size_t prev_step = 50;
  const std::size_t next_step = 100;
  // 先前找 50个 向后找 50个
  std::size_t begin = 0;
  // 注意szie_t是一个无符号数,这里我们-1 绝对有问题
  if (pos > prev_step)
  {
    begin = pos - prev_step;
  }
   
  std::size_t end = pos + next_step;
  if (end > content.size())
  {
    end = content.size();
  }
  //这里是是避只有关键
  if (end > begin)
  {
    std::string desc = content.substr(begin, end - begin);
    desc += "....";
    return desc;
  }
  else
    return "Node";
}

image-20230910102749554

这里测试一下.

请输入关键字# 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_83_0/doc/html/boost/process/v2/bound_launcher.html"
   },
   .....
]

image-20230910103120881

综合调试

下面我们这里要测试上面我们写的内容,是不是按照权重从大到小进行排序的,这里在json串哪里测试一下.

image-20230910104317877

这个我们思路是.我们拿到所有的倒排拉链里面的内容,根据id找正文.但是我们倒排拉链哪里也是存在权重的.

请输入关键字# split
[
   {
      "desc" : "Class template split_iteratorHomeLibrariesPeopleFAQMoreClass template split_iteratorboost::algorithm::split_iterato....",
      "title" : "Class template split_iterator",
      "url" : "https://www.boost.org/doc/libs/1_83_0/doc/html/boost/algorithm/split_iterator.html",
      "weight" : 37
   },
   {
      "desc" : "ual, BucketTraits, SizeType, BoolFlags &gt;::type split_bucket_hash_equal_t;  typedef split_bucket_hash_equal_t::key_equal                            ....",
      "title" : "Struct template hashdata_internal",
      "url" : "https://www.boost.org/doc/libs/1_83_0/doc/html/boost/intrusive/hashdata_internal.html",
      "weight" : 20
   },
   .....
]

关于调试我们这里需要总结几个内容.

  • 计算权重时,我们先去拿了标题,但是在内容中我们是对整个内容去标题.所以我们标题计算权重时要计算两次,那么一个标题是11
  • 我们分词的具体规则不知道,不够这里我们就不关心了
  • 上面我们还剩下最后一个内容,就是重复文档的问题.

调试后,我们修改一下文件名.

[qkj@localhost BoostSearchEngine]$ mv server.cc debug.cc

同时也修改一下makefile.

cc=g++
PARSER=parser
DUG=debug

.PHONY:all
all:$(PARSER) $(DUG)

$(DUG):debug.cc
	$(cc) -o $@ $^ -std=c++11  -lboost_system -lboost_filesystem -ljsoncpp

$(PARSER):parser.cc
	$(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem

.PHONY:clean
clean:
	rm -f $(PARSER) $(DUG)

搜索服务端

下面我们开始编写网络版本的服务端,我们先创建好文件.

[qkj@localhost BoostSearchEngine]$ touch http_server.cc
#include "searcher.hpp"
int mian()
{
  
  return 0;
}

这里也修改下makefile.

cc=g++
PARSER=parser
DUG=debug
HTTP_SERVER=http_server 
.PHONY:all
all:$(PARSER) $(DUG) $(HTTP_SERVER)

$(DUG):debug.cc
	$(cc) -o $@ $^ -std=c++11  -lboost_system -lboost_filesystem -ljsoncpp

$(PARSER):parser.cc
	$(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem

$(HTTP_SERVER):http_server.cc
	$(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem -ljsoncpp

.PHONY:clean
clean:
	rm -f $(PARSER) $(DUG) $(HTTP_SERVER)

这里测试一下.

[qkj@localhost BoostSearchEngine]$ make
g++ -o parser parser.cc -std=c++11 -lboost_system -lboost_filesystem
g++ -o debug debug.cc -std=c++11  -lboost_system -lboost_filesystem -ljsoncpp
g++ -o http_server http_server.cc -std=c++11 -lpthread -lboost_system -lboost_filesystem -ljsoncpp
[qkj@localhost BoostSearchEngine]$ ll
total 1548
lrwxrwxrwx. 1 qkj qkj     43 Sep  9 06:00 cppjieba -> /home/qkj/install/cppjieba/include/cppjieba
drwxrwxr-x. 4 qkj qkj     35 Sep  9 01:03 data
-rwxrwxr-x. 1 qkj qkj 658128 Sep  9 20:02 debug
-rw-rw-r--. 1 qkj qkj    483 Sep  9 09:16 debug.cc
lrwxrwxrwx. 1 qkj qkj     32 Sep  9 06:01 dict -> /home/qkj/install/cppjieba/dict/
-rwxrwxr-x. 1 qkj qkj 401400 Sep  9 20:02 http_server
-rw-rw-r--. 1 qkj qkj     51 Sep  9 20:02 http_server.cc
-rw-rw-r--. 1 qkj qkj   6102 Sep  9 08:33 index.hpp
-rw-rw-r--. 1 qkj qkj    446 Sep  9 19:58 Makefile
-rwxrwxr-x. 1 qkj qkj 481760 Sep  9 20:02 parser
-rw-rw-r--. 1 qkj qkj   6361 Sep  9 02:47 parser.cc
-rw-rw-r--. 1 qkj qkj   4626 Sep  9 19:42 searcher.hpp
-rw-rw-r--. 1 qkj qkj   1779 Sep  9 08:27 util.hpp

升级gcc

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

[qkj@localhost BoostSearchEngine]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/libexec/gcc/x86_64-redhat-linux/4.8.5/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --prefix=/usr --mandir=/usr/share/man --
infodir=/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enablebootstrap
--enable-shared --enable-threads=posix --enable-checking=release --with-systemzlib
--enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --
enable-linker-build-id --with-linker-hash-style=gnu --enable-languages=c,c++,objc,objc++,
java,fortran,ada,go,lto --enable-plugin --enable-initfini-array --disable-libgcj --
with-isl=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/isl-install --
with-cloog=/builddir/build/BUILD/gcc-4.8.5-20150702/obj-x86_64-redhat-linux/cloog-install -
-enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-
redhat-linux
Thread model: posix
gcc version 4.8.5 20150623 (Red Hat 4.8.5-44) (GCC)

下面直接升级.

[qkj@localhost BoostSearchEngine]$ sudo yum install centos-release-scl
[qkj@localhost BoostSearchEngine]$ sudo yum install devtoolset-8-gcc*
scl enable devtoolset-8 bash
[qkj@localhost BoostSearchEngine]$ source /opt/rh/devtoolset-8/enable
[qkj@localhost BoostSearchEngine]$ mv /usr/bin/gcc /usr/bin/gcc-4.8.5
[qkj@localhost BoostSearchEngine]$ ln -s /opt/rh/devtoolset-8/root/bin/gcc /usr/bin/gcc
[qkj@localhost BoostSearchEngine]$ mv /usr/bin/g++ /usr/bin/g++-4.8.5
[qkj@localhost BoostSearchEngine]$ ln -s /opt/rh/devtoolset-8/root/bin/g++ /usr/bin/g++
[qkj@localhost BoostSearchEngine]$ mv /usr/bin/c++ /usr/bin/c++-4.8.5
[qkj@localhost BoostSearchEngine]$ ln -s /opt/rh/devtoolset-8/root/bin/c++ /usr/bin/c++
[qkj@localhost BoostSearchEngine]$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/opt/rh/devtoolset-8/root/usr/libexec/gcc/x86_64-redhat-linux/8/lto-wrapper
Target: x86_64-redhat-linux
Configured with: ../configure --enable-bootstrap --enable-languages=c,c++,fortran,lto --prefix=/opt/rh/devtoolset-8/root/usr --mandir=/opt/rh/devtoolset-8/root/usr/share/man --infodir=/opt/rh/devtoolset-8/root/usr/share/info --with-bugurl=http://bugzilla.redhat.com/bugzilla --enable-shared --enable-threads=posix --enable-checking=release --enable-multilib --with-system-zlib --enable-__cxa_atexit --disable-libunwind-exceptions --enable-gnu-unique-object --enable-linker-build-id --with-gcc-major-version-only --with-linker-hash-style=gnu --with-default-libstdcxx-abi=gcc4-compatible --enable-plugin --enable-initfini-array --with-isl=/builddir/build/BUILD/gcc-8.3.1-20190311/obj-x86_64-redhat-linux/isl-install --disable-libmpx --enable-gnu-indirect-function --with-tune=generic --with-arch_32=x86-64 --build=x86_64-redhat-linux
Thread model: posix
gcc version 8.3.1 20190311 (Red Hat 8.3.1-3) (GCC) 
[qkj@localhost BoostSearchEngine]$ 

引入cpp-httplib库

这里我们选择下载0.7.15版本,这是因为较新版本的可能运行时会报错.
这里我们选择下载到桌面,然后拖拽到虚拟机上,这些方法都试一遍.

image-20230910112216548

[qkj@localhost install]$ rz -E 

[qkj@localhost install]$ ll
total 596
-rwxrwxr-x. 1 qkj qkj  15424 Sep  9 09:09 a.out
drwxr-xr-x. 8 qkj qkj   4096 Aug  8 14:40 boost_1_83_0
-rw-r--r--. 1 qkj qkj 584053 Sep  9 20:23 cpp-httplib-v0.7.15.zip
drwxrwxr-x. 8 qkj qkj    215 Sep  9 03:38 cppjieba
-rw-rw-r--. 1 qkj qkj    421 Sep  9 09:09 test.cc
[qkj@localhost install]$ 

然后我们创建软连接到我们的项目中.

[qkj@localhost BoostSearchEngine]$ ln -s /home/qkj/install/cpp-httplib-v0.7.15/ cpp-httplib
[qkj@localhost BoostSearchEngine]$ ll
total 1548
lrwxrwxrwx. 1 qkj qkj     38 Sep  9 20:30 cpp-httplib -> /home/qkj/install/cpp-httplib-v0.7.15/
lrwxrwxrwx. 1 qkj qkj     43 Sep  9 06:00 cppjieba -> /home/qkj/install/cppjieba/include/cppjieba
drwxrwxr-x. 4 qkj qkj     35 Sep  9 01:03 data
-rwxrwxr-x. 1 qkj qkj 658128 Sep  9 20:02 debug
-rw-rw-r--. 1 qkj qkj    483 Sep  9 09:16 debug.cc
lrwxrwxrwx. 1 qkj qkj     32 Sep  9 06:01 dict -> /home/qkj/install/cppjieba/dict/
-rwxrwxr-x. 1 qkj qkj 401400 Sep  9 20:02 http_server
-rw-rw-r--. 1 qkj qkj     51 Sep  9 20:02 http_server.cc
-rw-rw-r--. 1 qkj qkj   6102 Sep  9 08:33 index.hpp
-rw-rw-r--. 1 qkj qkj    446 Sep  9 19:58 Makefile
-rwxrwxr-x. 1 qkj qkj 481760 Sep  9 20:02 parser
-rw-rw-r--. 1 qkj qkj   6361 Sep  9 02:47 parser.cc
-rw-rw-r--. 1 qkj qkj   4626 Sep  9 19:42 searcher.hpp
-rw-rw-r--. 1 qkj qkj   1779 Sep  9 08:27 util.hpp
[qkj@localhost BoostSearchEngine]$ 

测试cpp-httplib

下面我们测试一下httplib库.

image-20230910113523085

这里我们先来测试一下.

[qkj@localhost BoostSearchEngine]$ make
g++ -o http_server http_server.cc -std=c++11  -lboost_system -lboost_filesystem -ljsoncpp
/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/libstdc++_nonshared.a(thread48.o): In function `std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)())':
(.text._ZNSt6thread15_M_start_threadESt10unique_ptrINS_6_StateESt14default_deleteIS1_EEPFvvE+0x11): undefined reference to `pthread_create'
/opt/rh/devtoolset-8/root/usr/lib/gcc/x86_64-redhat-linux/8/libstdc++_nonshared.a(thread48.o): In function `std::thread::_M_start_thread(std::shared_ptr<std::thread::_Impl_base>, void (*)())':
(.text._ZNSt6thread15_M_start_threadESt10shared_ptrINS_10_Impl_baseEEPFvvE+0x60): undefined reference to `pthread_create'
/tmp/ccGWpu61.o: In function `std::thread::thread<httplib::ThreadPool::worker, , void>(httplib::ThreadPool::worker&&)':
http_server.cc:(.text._ZNSt6threadC2IN7httplib10ThreadPool6workerEJEvEEOT_DpOT0_[_ZNSt6threadC5IN7httplib10ThreadPool6workerEJEvEEOT_DpOT0_]+0x21): undefined reference to `pthread_create'
collect2: error: ld returned 1 exit status
make: *** [http_server] Error 1
[qkj@localhost BoostSearchEngine]$ 

这是由于我们httplib需要引入pthread库.

cc=g++
PARSER=parser
DUG=debug
HTTP_SERVER=http_server 
.PHONY:all
all:$(PARSER) $(DUG) $(HTTP_SERVER)

$(DUG):debug.cc
	$(cc) -o $@ $^ -std=c++11  -lboost_system -lboost_filesystem -ljsoncpp

$(PARSER):parser.cc
	$(cc) -o $@ $^ -std=c++11 -lboost_system -lboost_filesystem

$(HTTP_SERVER):http_server.cc
	$(cc) -o $@ $^ -std=c++11 -lpthread -lboost_system -lboost_filesystem -ljsoncpp

.PHONY:clean
clean:
	rm -f $(PARSER) $(DUG) $(HTTP_SERVER)

image-20230910113735355

这里我们继续测试,先创建一个简单的功能.这个库是很好用的.image-20230910113849136

这是我们代码.

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

image-20230910114502524

[qkj@localhost install]$ 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      -                   
[qkj@localhost install]$ 

image-20230910114703214

开放端口号

这是因为我们的虚拟机没有开辟端口被外部网络进行访问.这里需要开放端口.我们看一下下面有那些端口被打开了.下面是打开的规则.

Centos开放端口号

image-20230910120405201

设置根目录

一般而言,我们都有一个根目录.这样就可以了.

[qkj@localhost BoostSearchEngine]$ mkdir wwwroot

这里在服务器上面设置跟目录.

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

我们继续测试.

image-20230910120916661

注意z合适因为我们的根目录下面什么都没有.一般而言,我们是名字为index.html文件.这里设置一下

[qkj@localhost wwwroot]$ touch index.html
[qkj@localhost wwwroot]$ ll
total 8
-rw-rw-r--. 1 qkj qkj    0 Sep  9 21:10 index.html

image-20230910121202431

image-20230910121228019

编写搜索服务端

下面我们就可以编写我们的服务端了.这里面是非常简单的.

#include "cpp-httplib/httplib.h"
#include "searcher.hpp"

const std::string root_path = "./wwwroot";
const std::string input = "data/raw_html/raw.txt";
int main()
{
  // 初始化sercher
  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") == false)
            {
              rsp.set_content("必须要搜索关键字", "text/plain; charset=utf-8");
              return;
            }

            std::string word = req.get_param_value("word");
            std::cout << "用户搜索的: " << word << std::endl;

            std::string json_string;
            search.Search(word, &json_string);
            rsp.set_content(json_string, "application/json"); });
  std::cout << "服务器启动成功" << std::endl;

  svr.listen("0.0.0.0", 8081);

  return 0;
}

image-20230910122016183

image-20230910122025197

前端代码

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

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

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

网页结构

我们设置的网页结构是这样的.

image-20230910122726672

按照上面的内容,我们的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 rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
    </div>
  </div>
</body>

</html>

image-20230910123040661

网页样式

上面我们发现有点丑,所以这里我们要给他美颜一下.

<!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 rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘要这是摘
          要这是摘要这是摘要这是摘要这是摘要</p>
        <i>https://search.gitee.com/?skin=rec&type=repository&q=cpp-httplib</i>
      </div>
    </div>
  </div>
</body>

</html>

image-20230910123352057

前后端交互

下面我们继续使用前后端交互.也是直接贴代码.

<!-- 形成骨架 -->
<!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 rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要,这是摘要这是摘要,这是摘要这是摘要,这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a rel="nofollow" href="#">这是标题</a>
        <p>这是摘要这是摘要</p>
        <i>https://www.bilibili.com/</i>
      </div>
      <div class="item">
        <a rel="nofollow" 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>

项目成果

下面我们就可以使用我们的项目做搜索服务了看一下.

image-20230910154052943

项目补充

下面我们补充点内容,有些小细节我们还没有谈.

取重完善

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

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

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

image-20230910124702711

这里我们应该要处理.

struct InvertedElemPrint
  {
    uint64_t doc_id; // 文旦id

    int weight;                     // 权重
    std::vector<std::string> words; // 一个id里面可以对饮多个词
    InvertedElemPrint() : doc_id(0), weight(0) {}
  };

  class Searcher
  {
  public:
    Searcher() {}
    ....
    void Search(const std::string &query, std::string *json_string)
    {
      // 1 分词  先来分词后面在进行查找
      std::vector<std::string> words;
      ns_util::JiebaUtil::CutString(query, &words);

      // 2 根据分词结果依次触发  搜索
      std::unordered_map<uint64_t, InvertedElemPrint> tokens_map; //根据id,找到InvertedElemPrint
      
      std::vector<InvertedElemPrint> inverted_list_all; // 为了去重

      for (std::string s : words)
      {
        boost::to_lower(s); 
        // 先查倒排
        ns_index::InvertedList *inverted_list = index->GetInvertedList(s);
        if (nullptr == inverted_list)
        {
          continue;
        }
       
        // 根据倒排拉量找到我们所有的文档id
        for (const auto &elem : *inverted_list)
        {
          // 去看这个id是不在哈希表中,如果在,拿到InvertedElemPrint
          auto &item = tokens_map[elem.doc_id]; 
          item.doc_id = elem.doc_id; 
          // 把关键字也插入其中
          item.words.push_back(elem.word);
          // 计算权重
          item.weight += elem.weight;
        }
        // 此时我们相同的id 已经被保存了
      }
      // 这里就把我们相同id的InvertedElemPrint插入所有的数组中
      for (const auto &item : tokens_map)
      {
        inverted_list_all.push_back(item.second);
      }

      // 3 合并排序  -- 按照相关性进行降序排序,这里是根据新的权重.
      std::sort(inverted_list_all.begin(), inverted_list_all.end(),
                [](const InvertedElemPrint &e1, const InvertedElemPrint &e2)
                {
                  return e1.weight > e2.weight;
                });


      // 4 构建json串 使用序列化和反序列化
      Json::Value root;
      for (auto &item : inverted_list_all)
      {
        // 此时拿到正派
        ns_index::DocInfo *doc = index->GetForwardIndex(item.doc_id);
        if (nullptr == doc)
        {
          continue;
        }

        // 获取了 文档内容
        Json::Value elem;
        elem["title"] = doc->title;
        elem["desc"] = make_summary(doc->content, item.words[0]); // 我们需要根据关键字来提取摘要
        elem["url"] = doc->url;

        // fordebug
        //  elem["id"] = (int)item.doc_id;
        //  elem["weight"] = item.weight; // 会自动转成string
        root.append(elem); // 这里是有序的
      }

      Json::StyledWriter writer; // 这里我们暂时用这个格式
      *json_string = writer.write(root);
    }

  private:
    ....
    ns_index::Index *index; // 提供系统经行查找索引
  };

添加日志

这里我们添加日志创建一个文件.

[qkj@localhost BoostSearchEngine]$ touch log.hpp
#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;
}

在索引那里建立日志

image-20230910130424510

在搜索那里建立日志

image-20230910130509233

在服务端那里建立日志

image-20230910130646957

项目拓展

这里我们可以扩展一下项目.

摘要完善

我们知道,分词的时候是可以去掉暂停词的.上面的我们都没有这么做.这是因为我们的如果加上去掉暂停词,此时对资源的要求非常大.那么这里可以作为一个扩展.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;

后台部署服务

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

nohup指令

nohup的执行:

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

例如 nohup ./http_server

如果让程序在后台执行, 可以在末尾加上 & , 程序就会隐身 , 不会显示在终端。

例如 nohup ./http_server &

nohup形成的文件:

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

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)// 严重不推荐
}

其他拓展

  • 我们在搜索引擎中,对于权重的设置先后显示顺序,我们其实可以叠加一些算法,比如可以设置竞价排名,热点统计,额外增加某些文档的权重。
  • 我们可以利用数据库,设置用户登录注册,引入对MySQL的使用。

标签:std,qkj,string,doc,摘要,搜索引擎,amp,Boost
From: https://blog.51cto.com/byte/7427307

相关文章

  • 构建高性能全文搜索引擎:Java与Elasticsearch
    在今天的应用程序中,全文搜索功能变得越来越重要。无论是在线商店、博客网站还是企业应用,用户都希望快速而准确地找到他们需要的信息。Elasticsearch是一个强大的全文搜索引擎,可以轻松应对这一需求。本文将向你展示如何使用Java与Elasticsearch构建高性能的全文搜索引擎。什么是Elas......
  • Boost.Asio网络库 学习笔记
    Asio网络库学习笔记Boost官网:https://www.boost.org/Boost库许可证://CopyrightJoeCoder2004-2006.//DistributedundertheBoostSoftwareLicense,Version1.0.//(SeeaccompanyingfileLICENSE_1_0.txtorcopyat//https://www.boos......
  • boost的协程切换分析
    主要有两个接口,一个make_fcontext(),一个jump_fcontext()相比ucontext,boost的切换模式,少了单独对context进行保存(getcontext)和切换(setcontext)过程,而是把两者合并到一起,通过jump_fcontext接口实现直接切换这样做有个好处,就是更加容易进行优化,使得整个切换过程更加的紧凑boost......
  • 网站优化搜索引擎与关键词
    网站优化搜索引擎与关键词人们不应该高估搜索引擎的智商。这不利于seo的研究,事实上,搜索引擎是非常愚蠢的,让我们举一个非常简单的例子,你在搜索引擎中输入“教师”这个词,搜索引擎就会给出一个准确的搜索列表。我们不会给出“教师”一词的检索信息,但我们认为,“教师”和“教师”的含义......
  • 《自己动手建搜索引擎》日志分析类代码解析与修正为兼容lucene3.0.2
    搜索日志是用来分析用户搜索行为和信息需求的重要依据。一般记录如下信息:搜索关键字用户来源IP本次搜索返回结果数量搜索时间其他需要记录的应用相关信息  例如:搜索时间|日志类型|搜索类型|搜索关键字|IP地址|本次搜索返回结果数量, 存放日志文件内容如下:2008-04......
  • 搜索引擎搜藏
    www.jarhoo.com 一个根据类名查找类所在包的搜索引擎,这又实现了我的思路。必将成为java程序员的必备工具之一,不过好像目前是收费的。www.jguru.com专门搜索与java相关的资源。......
  • 向量搜索技术:基于Elasticsearch/PostgreSQL/Redis扩展的向量搜索数据库或独立向量搜索
    理论基础与研究向量数据库用于非结构化文本、图片、音频、视频搜索、推荐,将他们转换为数字向量表示来进行相似性(ANN)搜索。存储和搜索高维向量是其特征之一,通常采用高级索引技术和算法如HNSW,Annoy,或Faiss来实现。不同于SQL数据库,向量数据库更像nosql,用户接受使用sdk/API......
  • XGboost详解
    一概述XGBoost提供梯度提升树(也称为GBDT,GBM),可以快速准确地解决许多数据科学问题,相同的代码可以在主要分布式环境运行(ApacheHadoop,ApacheSpark,ApacheFlink)。系统优化:并行计算:支持并行计算。树剪枝:用贪心算法来选择最佳分裂点,然后开始剪枝。硬件优化:有效利用硬件资源。......
  • 对话谷歌首席技术官肖恩,搜索引擎的里程碑,来看看搜索引擎界的大哥Algolia的“快、准、
    原创|文BFT机器人人物背景Character BackgroundSeanMullaney是Algolia(端到端人工智能搜索和发现平台)的首席技术官,也是前Stripe和谷歌高管,拥有扩展工程组织、开发人工智能驱动的搜索和发现工具以及在全球范围内发展API优先解决方案的背景。在Algolia,他负责监督仅次于谷歌的第......
  • Boost库的安装与使用
    一、下载Boost库下载地址:https://www.boost.org/users/download/二、编译Boost库cdboost_1_72_0./bootstrap.sh--with-python=python2.7#修改Python2.7的路径,改为:/usr/include/python2.7vimproject-config.jam#将boost库相关文件安装至/usr/localsudo./b2--prefix=/usr......