[C++项目] Boost文档 站内搜索引擎(5): cpphttplib实现网络服务、html页面实现、服务器部署...

news2024/11/23 15:06:40

|cover


在前四篇文章中, 我们实现了从文档文件的清理 到 搜索的所有内容:

  1. 项目背景: 🫦[C++项目] Boost文档 站内搜索引擎(1): 项目背景介绍、相关技术栈、相关概念介绍…
  2. 文档解析、处理模块parser的实现: 🫦[C++项目] Boost文档 站内搜索引擎(2): 文档文本解析模块parser的实现、如何对文档文件去标签、如何获取文档标题…
  3. 文档 正排索引与倒排索引 建立的接口的实现: 🫦[C++项目] Boost文档 站内搜索引擎(3): 建立文档及其关键字的正排 倒排索引、jieba库的安装与使用…
  4. 文档的 搜索功能 接口的实现: 🫦[C++项目] Boost文档 站内搜索引擎(4): 实现搜索的相关接口、线程安全的单例index接口、cppjieba分词库的使用…
  5. 建议先阅读上面四篇文章

后端的主要功能接口完成之后, 就可以结合网络将其设计为服务器 然后部署到网络上了

网络服务

我们使用cpphttplib库 实现搜索引擎服务器. 所以要先安装cpphttplib

cpphttplib

使用cpphttplib时, gcc版本不能太低. 而CentOS 7默认的版本是4.8.5, 太低了. 所以安装使用cpphttplib之前, 要先升级gcc到至少7.x以上

gcc升级

我们将gcc升级到8.3.1. 非常的简单, 只需要一共6条指令 就可以完成:

# 安装 centos-release-scl
sudo yum install centos-release-scl
# 安装 devtoolset-8-gcc* (gcc8相关软件包)
sudo yum install devtoolset-8-gcc*

# 安装完成, 需要建立软连接
mv /usr/bin/gcc /usr/bin/gcc-4.8.5
ln -s /opt/rh/devtoolset-8/root/bin/gcc /usr/bin/gcc
mv /usr/bin/g++ /usr/bin/g++-4.8.5
ln -s /opt/rh/devtoolset-8/root/bin/g++ /usr/bin/g++

然后就可以看到:

❯ 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)
❯ g++ -v
Using built-in specs.
COLLECT_GCC=g++
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)

安装cpphttplib

cpphttplib库的安装非常简单, 因为整个库中 只需要用到一个httplib.h的头文件.

但是, 我们需要选择版本安装, 不能直接安装最新版的. 因为gcc编译器版本不匹配的话 可能 会出现无法编译或运行时错误的情况

这里推荐0.7.16的版本: https://github.com/yhirose/cpp-httplib/tree/v0.7.16

可以直接获取此版本的源码:

wget https://codeload.github.com/yhirose/cpp-httplib/zip/refs/tags/v0.7.16

然后解压出来, 将httplib.h拷贝到项目目录下:

wget https://codeload.github.com/yhirose/cpp-httplib/zip/refs/tags/v0.7.16
--2023-08-08 14:24:23--  https://codeload.github.com/yhirose/cpp-httplib/zip/refs/tags/v0.7.16
Resolving codeload.github.com (codeload.github.com)... 20.205.243.165
Connecting to codeload.github.com (codeload.github.com)|20.205.243.165|:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: unspecified [application/zip]
Saving to: ‘v0.7.16’

    [   <=>                                                                                                                                     ] 586,948     1.10MB/s   in 0.5s

2023-08-08 14:24:25 (1.10 MB/s) - ‘v0.7.16’ saved [586948]unzip v0.7.16
Archive:  v0.7.16
    ... 解压过程
 extracting: cpp-httplib-0.7.16/test/www3/dir/test.html
❯ ll
total 588K
drwxr-xr-x 6 July July 4.0K Nov 30  2020 cpp-httplib-0.7.16
drwxr-xr-x 9 July July 4.0K Aug  7 00:16 cppjieba
drwxr-xr-x 6 July July 4.0K Aug  8 13:52 gitHub
-rw-r--r-- 1 July July 574K Aug  8 14:24 v0.7.16

# 将httplib.h 拷贝到项目目录下:
cp cpp-httplib-0.7.16/httplib.h gitHub/Boost-Doc-Searcher/.

这就算在项目中安装成功了

cpphttplib的简单使用

关于cpphttplib的使用, Github文档有简单的使用介绍

直接使用这段代码 可以实现怎么样的结果呢?

#include <iostream>
#include <pthread.h>
#include "httplib.h"

// 由于库中使用了线程相关接口, 所以要包含线程头文件

int main() {
	httplib::Server svr;

	svr.Get("/hi", [](const httplib::Request&, httplib::Response& res) {
		res.set_content("Hello World!", "text/plain");
	});

	svr.listen("0.0.0.0", 8080);

	return 0;
}

直接访问根url, 没有任何响应. 但是如果我们在url之后添加/hi. 就能看到Hello World!的字样.

这就是我们设定的 申请/hi资源时, 会响应的内容:

|inline

httplib::Server::Get()是用来处理HTTPGET方法的接口.

  1. 第一个参数, 用来指定处理 申请某内容的请求.

    如果传入/hi, 就会处理 请求的urlwwwRoot/hi的请求. 如果传入/index.html, 就会处理 请求的urlwwwRoot/hi的请求

    wwwRoot表示web根目录, 没有设置 即为服务器运行路径

  2. 第二个参数, 是一个回调函数 用来 接收请求 对请求进行处理, 并响应

    此回调函数的第一个参数 就是用来接收请求的.

    第二个参数, 可以看作一个输出型参数. 是用来填充响应的

    在例子中, 使用httplib::Response::set_content(), 接口设置响应正文以及相应的类型

最后监听指定端口, 就可以通过ip:port的形式访问服务器.

项目网络服务 **

了解了cpphttplib的最基本使用. 就可以为项目创建网络服务了

但是, 创建网络服务之前. 可以先了解一下 搜索引擎的搜索结果是怎么出现的?

当我们搜索时, 会申请/search这个服务. 并携带了?q=Searcher这个key(q)=value(Searcher)属性.

然后, 就会将搜索结果显示出来.

cpphttplib提供了检索url中是否存在key的接口, 并且可以通过key获取value值的接口, 所以我们就可以这样来向页面设置内容:

svr.Get("/search", [](const httplib::Request& request, httplib::Response& response) {
    if (!request.has_param("word")) {
        // url中没有 word 键值
        // set_content() 第一个参数是设置正文内容, 第二个参数是 正文内容类型等属性
        response.set_content("请输入内容后搜索", "text/plain; charset=utf-8");
    }
});

然后运行服务器并访问/search:

url中没有keyword的键值时, 就会显示 请输入内容后搜索

如果有keyword的键值, 因为我们没有做任何操作, 所以不会有任何内容:

除了判断是否存在key, 还可以通过接口获得对应的value:

svr.Get("/search", [](const httplib::Request& request, httplib::Response& response) {
		if (!request.has_param("word")) {
			// url中没有 word 键值
			// set_content() 第一个参数是设置正文内容, 第二个参数是 正文内容类型等属性
			response.set_content("请输入内容后搜索", "text/plain; charset=utf-8");
		}
		std::string word = request.get_param_value("word");
		response.set_content(word, "text/plain; charset=utf-8");
});

此时, 再携带key=value键对:

就获取到了value的内容, 并设置为了响应内容.

既然可以获取url中的键值, 那么 就可以实现根据键值调用searcher::search()接口, 搜索相关文档:

#include <iostream>
#include <pthread.h>
#include "util.hpp"
#include "searcher.hpp"
#include "httplib.h"

const std::string& input = "./data/output/raw";

int main() {
    ns_searcher::searcher searcher;
    searcher.initSearcher(input);

    httplib::Server svr;

    svr.Get("/s", [&searcher](const httplib::Request& request, httplib::Response& response {
        if (!request.has_param("word")) {
            // url中没有 word 键值
            // set_content() 第一个参数是设置正文内容, 第二个参数是 正文内容类型等属性
            response.set_content("请输入内容后搜索", "text/plain; charset=utf-8");
        }
        std::string searchContent = request.get_param_value("word");
        std::cout << "User search:: " << searchContent << std::endl;

        std::string searchJsonResult;
        searcher.search(searchContent, &searchJsonResult);
        // 搜获取到搜索结果之后 设置相应内容
        response.set_content(searchJsonResult, "application/json");
    });

    std::cout << "服务器启动成功..." << std::endl;
    svr.listen("0.0.0.0", 8080);

    return 0;
}

编译代码 g++ httpServer.cc -lpthread -ljsoncpp

运行程序. 建立索引 等待服务器开启成功之后:

直接在url添加键值 就可以看到直接的搜索结果.

至此, 网络服务的编写就完成了.

下面要做的, 就是通过网页发送请求, 并根据响应构建结果网页.

网页构建

由于博主没有学过前端的代码, 所以做出来的网页只是能用. 也没有能力去解释一些原理或底层的实现. 只能介绍一下基本功能

所以, 直接列出代码:

./wwwRoot/index.html:

<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
    <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
    <title>Boost库 文档搜索</title>
    <style>
      /* 去掉网页中的所有的默认内外边距,html的盒子模型 */
      * {
        background-color: #f5f5f7;
        /* 设置外边距 */
        margin: 0;
        /* 设置内边距 */
        padding: 0;
      }

      /* 将我们的body内的内容100%和html的呈现吻合 */
      html,
      body {
        height: 100%;
      }

      /* 类选择器.container */
      .container {
        text-align: center;
        /* 设置div的宽度 */
        width: 800px;
        /* 通过设置外边距达到居中对齐的目的 */
        margin: 0px auto;
        /* 设置外边距的上边距,保持元素和网页的上部距离 */
        margin-top: 100px;
      }

      /* 复合选择器,选中container 下的 search */
      .container .search {
        /* 宽度与父标签保持一致 */
        width: auto;
        /* 高度设置为52px */
        height: 52px;
      }

      .container .result {
        margin-top: 30px;
        text-align: left;
        width: 100%;
      }

      .container .result .item {
        height: auto;
        border-radius: 13px;
        background-color: #fff;
        box-shadow: 0 0 5px rgb(0, 0, 0, 0.2);
        margin-top: 15px;
        padding-bottom: 10px;
        padding-top: 10px;
      }

      .container .result .item a {
        margin-left: 10px;
        margin-right: 10px;
        /* 设置为块级元素,单独站一行 */
        display: block;
        background-color: #fff;
        /* a标签的下划线去掉 */
        text-decoration: none;
        /* 设置a标签中的文字的字体大小 */
        font-size: 20px;
        /* 设置字体的颜色 */
        color: #4e6ef2;
        word-break: break-all;
      }

      .container .result .item a:hover {
        /*设置鼠标放在a之上的动态效果*/
        text-decoration: underline;
      }

      .container .result .item p {
        margin-left: 10px;
        margin-top: 10px;
        margin-right: 10px;
        display: block;
        background-color: #fff;
        font-size: 16px;
        word-break: break-all;
        font-family: "Lucida Sans", "Lucida Sans Regular", "Lucida Grande",
          "Lucida SansUnicode", Geneva, Verdana, sans-serif;
      }

      .container .result .item i {
        margin-left: 10px;
        margin-right: 10px;
        /* 设置为块级元素,单独站一行 */
        display: block;
        font-size: 12px;
        /* 取消斜体风格 */
        font-style: normal;
        background-color: #fff;
        color: gray;
        word-break: break-all;
      }

      #INDEXBLOGS {
        text-align: center;
        width: 75%;
      }

      .search-box {
        width: 666px;
        margin: auto;
        display: flex;
        background-color: #fff;
        align-items: center;
        border: 1px solid #ddd;
        border-radius: 25px;
        height: 44px;
        box-shadow: 0 0 5px rgb(0, 0, 0, 0.2);
      }

      .search-input {
        flex: 1;
        padding: 0 15px;
        border: none;
        background-color: #fff;
        border: 0px solid #ddd;
        border-radius: 25px;
        font-size: 16px;
        height: 43px;
      }

      .search-input:focus {
        outline: none;
      }

      .search-button {
        padding: 0 18px;
        height: 100%;
        border: none;
        border-radius: 0 25px 25px 0;
        background: #fef9f2;
        color: #666;
        font-size: 16px;
        cursor: pointer;
      }

      .suggestion {
        margin-bottom: 5px;
        color: #000000;
        font-size: 14px;
      }
    </style>
  </head>

  <body>
    <div class="container">
      <img
        src="https://dxyt-july-image.oss-cn-beijing.aliyuncs.com/202308080011153.png"
        id="INDEXBLOGS"
      />
      <p class="suggestion">
        服务器配置原因, 若搜索结果过多 可能响应较慢, 请耐心等待哦~
      </p>
      <div class="search-box">
        <input
          type="text"
          id="search-input"
          class="search-input"
          placeholder=""
        />
        <button onclick="Search()" class="search-button">&#9829; Search</button>
      </div>
      <div class="result">
     	// 这里是展示搜索结果的地方
      </div>
    </div>
    <script>
      // 获取输入框元素
      const input = document.getElementById("search-input");

      // 输入框按键按下事件监听
      input.addEventListener("keydown", function (event) {
        // 判断按键为回车键
        if (event.keyCode === 13) {
          // 模拟按钮点击事件
          document.querySelector(".search-button").click();
        }
      });
      function Search() {
        // 是浏览器的一个弹出框
        // alert("hello js!");
        // 1. 提取数据, $可以理解成就是JQuery的别称
        let query = $(".container .search-input").val();
        console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据

        //2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的
        $.ajax({
          type: "GET",
          url: "/s?word=" + query,
          success: function (data) {
            console.log(data);
            BuildHtml(data);
          },
        });
      }

      function BuildHtml(data) {
        // 获取html中的result标签
        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 i_lable = $("<i>", {
            text: elem.url,
          });
          let p_lable = $("<p>", {
            text: elem.desc,
          });
          let div_lable = $("<div>", {
            class: "item",
          });
          a_lable.appendTo(div_lable);
          i_lable.appendTo(div_lable);
          p_lable.appendTo(div_lable);
          div_lable.appendTo(result_lable);
        }
      }
    </script>
  </body>
</html>

这个html文件是创建在项目目录下的wwwRoot目录下的:

一个是页面html文件, 一个是图标文件

大概解释一下这个html代码:

  1. 首先最外层 是html最基本的框架:

    <!DOCTYPE html>
    <html lang="en">
        <head>
            <meta charset="UTF-8">
            <meta name="viewport" content="width=device-width, initial-scale=1.0">
            <title></title>
        </head>
        <body>
    
        </body>
    </html>
    

    <body> </body>之间的内容, 就是要在页面中显示的内容

  2. <body> </body>之间. 先设置了一个<div class="container"> </div>

    可以看作是在页面内容中设置了一个框架, 之后只要在这个<div>内部的 都会显示在这个框架中

  3. 然后<div class="container"></div>内最主要的就是:

    1.  <div class="search-box">
           <input
                  type="text"
                  id="search-input"
                  class="search-input"
                  placeholder=""
                  />
           <button onclick="Search()" class="search-button">&#9829; Search</button>
       </div>
      

      又设置了一个<div>并在其内部设置了:

      一个搜索框 <input type="text" id="search-input" class="search-input" placeholder="" />

      一个搜索按钮 <button onclick="Search()" class="search-button">&#9829; Search</button>

      <button> </button>之间是按钮上显示的内容, onclick="Search()"表示点击按钮执行的函数

    2.  <div class="result">
           // 这里是展示搜索结果的地方
       </div>
      

      搜索框下面就是要展示的内容了

      设置了<div class="result"> </div>, 这个<div>内部就是展示搜索结果用的

      搜索结果用这个元素item表示:

      <div class="item">
          <a href="" target="_blank">跳转标题</a>
          <i>url</i>
          <p>摘要</p>
      </div>
      
  4. 布局设置完毕之后, 就需要使用JavaScript JQuery ajax来发送请求, 接收响应 和 设置搜索结果了

    <script>
        // 获取输入框元素
        const input = document.getElementById("search-input");
    
        // 输入框按键按下事件监听
        input.addEventListener("keydown", function (event) {
            // 判断按键为回车键
            if (event.keyCode === 13) {
                // 模拟按钮点击事件
                document.querySelector(".search-button").click();
            }
        });
        function Search() {
            // 是浏览器的一个弹出框
            // alert("hello js!");
            // 1. 提取数据, $可以理解成就是JQuery的别称
            let query = $(".container .search-input").val();
            console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据
    
            //2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的
            $.ajax({
                type: "GET",
                url: "/s?word=" + query,
                success: function (data) {
                    console.log(data);
                    BuildHtml(data);
                },
            });
        }
    
        function BuildHtml(data) {
            // 获取html中的result标签
            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 i_lable = $("<i>", {
                    text: elem.url,
                });
                let p_lable = $("<p>", {
                    text: elem.desc,
                });
                let div_lable = $("<div>", {
                    class: "item",
                });
                a_lable.appendTo(div_lable);
                i_lable.appendTo(div_lable);
                p_lable.appendTo(div_lable);
                div_lable.appendTo(result_lable);
            }
        }
    </script>
    

    <script> </script>内部, 首先设置了一个监听按键的函数. 为了实现 按下回车搜索

    然后就是Search()函数, 获取search-input搜索框内的数据为query, 然后创建HTTPGET方法请求, 并携带?word=query 发送给服务器.

    然后成功接收到响应之后, 根据响应数据 执行Build()函数 在<div class="result"></div>内部 设置item元素


编写完index.html之后, 需要在httpServer.cc主函数内, 将服务器的web根文件设置为./wwwRoot/index.html

const std::string& rootPath = "./wwwRoot/index.html";

svr.set_base_dir(rootPath.c_str());

然后再编译运行服务器:

  1. 没有执行搜索的界面:

    |wide

  2. 执行了搜索之后的界面:

    |wide

    搜索结果, 都会按照权重一个个排列在下面

至此, 我们的Boost搜索引擎就可以使用了!

不过, 还有一些地方需要优化和修改

代码优化

当前的搜索引擎还有问题:

  1. 没有搜索到内容时, 不会有任何反应. 可能会让用户认为服务器没有运作.

    |wide

    所以可以考虑在没有搜索到任何文档的时候, 响应一个没有任何内容的item元素. 并实现, 点击标题 跳转回主页:

    /* searcher.hpp */
    
    // 排序之后, allInvertedElemOut 中文档的排序就是倒序了
    // 然后 通过遍历此数组, 获取文档id, 根据id获取文档在正排索引中的内容
    // 然后再将 所有内容序列化
    Json::Value root;
    if (allInvertedElemOut.empty()) {
        // 如果没有查找到一个文档
        Json::Value elem;
        elem["url"] = "http://119.3.223.238:8080";
        elem["title"] = "Search nothing!";
        // 关于文档的内容, 搜索结果中是不展示文档的全部内容的, 应该只显示包含关键词的摘要, 点进文档才显示相关内容
        // 而docInfo中存储的是文档去除标签之后的所有内容, 所以不能直接将 doc._content 存储到elem对应key:value中
        elem["desc"] = "Search nothing!";
        root.append(elem);
        // 处理url 都设置为无效值
    }
    else {
        for (auto& elemOut : allInvertedElemOut) {
            // 通过Json::Value 对象, 存储文档内容
            Json::Value elem;
            // 通过elemOut._docId 获取正排索引中 文档的内容信息
            ns_index::docInfo_t* doc = _index->getForwardIndex(elemOut._docId);
            // elem赋值
            elem["url"] = doc->_url;
            elem["title"] = doc->_title;
            // 关于文档的内容, 搜索结果中是不展示文档的全部内容的, 应该只显示包含关键词的摘要, 点进文档才显示相关内容
            // 而docInfo中存储的是文档去除标签之后的所有内容, 所以不能直接将 doc._content 存储到elem对应key:value中
            elem["desc"] = getDesc(doc->_content, elemOut._keywords[0]); // 只根据第一个关键词来获取摘要
            // for Debug
            // 这里有一个bug, jsoncpp 0.10.5.2 是不支持long或long long 相关类型的, 所以需要转换成 double
            // 这里转换成 double不会有什么影响, 因为这两个参数只是本地调试显示用的.
            elem["docId"] = (double)doc->_docId;
            elem["weight"] = (double)elemOut._weight;
    
            root.append(elem);
        }
    }
    

    此时, 搜索不到内容:

    |wide

    点击就会跳转至主页.

  2. 可能没有标题:

    当搜索到的文章没有标题时, 就不会显示出来. 显示不出来也就无法通过标题跳转至指定的页面:

    |wide

    为什么没有标题呢? 不是因为出错了, 是因为 这篇文章本身就没有标题:

    |wide

    所以, 我们可以考虑修改搜索时获取标题的代码:

    /* searcher.hpp */
    
    Json::Value root;
    if (allInvertedElemOut.empty()) {
        Json::Value elem;
        elem["url"] = "http://119.3.223.238:8080";
        elem["title"] = "Search nothing!";
        // 关于文档的内容, 搜索结果中是不展示文档的全部内容的, 应该只显示包含关键词的摘要, 点进文档才显示相关内容
        // 而docInfo中存储的是文档去除标签之后的所有内容, 所以不能直接将 doc._content 存储到elem对应key:value中
        elem["desc"] = "Search nothing!";
        root.append(elem);
    }
    else {
        for (auto& elemOut : allInvertedElemOut) {
            // 通过Json::Value 对象, 存储文档内容
            Json::Value elem;
            // 通过elemOut._docId 获取正排索引中 文档的内容信息
            ns_index::docInfo_t* doc = _index->getForwardIndex(elemOut._docId);
            // elem赋值
            elem["url"] = doc->_url;
            elem["title"] = doc->_title;
            if (doc->_title.empty()) {
                // 如果无标题, 将标题设置为TITLE
                elem["title"] = "TITLE";
            }
            // 关于文档的内容, 搜索结果中是不展示文档的全部内容的, 应该只显示包含关键词的摘要, 点进文档才显示相关内容
            // 而docInfo中存储的是文档去除标签之后的所有内容, 所以不能直接将 doc._content 存储到elem对应key:value中
            elem["desc"] = getDesc(doc->_content, elemOut._keywords[0]); // 只根据第一个关键词来获取摘要
            // for Debug
            // 这里有一个bug, jsoncpp 0.10.5.2 是不支持long或long long 相关类型的, 所以需要转换成 double
            // 这里转换成 double不会有什么影响, 因为这两个参数只是本地调试显示用的.
            elem["docId"] = (double)doc->_docId;
            elem["weight"] = (double)elemOut._weight;
    
            root.append(elem);
        }
    }
    

    然后, 再搜索:

    |wide

  3. 我们之前为了方便观测调试, 把文档的docIdweight也存储并发送了. 现在可以去除

  4. 在使用parser模块处理文档html文件的时候, 有三个符号被转换成了编码<: &lt; >: &gt; &: &amp;

    |wide

    搜索的结果在页面中显示的时候, < > & 符号会以编码的形式显示. 所以我们可以在构建结果的的时候, 再将其转换回去:

    /*index.html*/
    
    for (let elem of data) {
        // console.log(elem.title);
        // console.log(elem.url);
        let a_lable = $("<a>", {
            text: elem.title.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&"),
            href: elem.url,
            // 跳转到新的页面
            target: "_blank",
        });
        let i_lable = $("<i>", {
            text: elem.url,
        });
    
        let p_lable = $("<p>", {
            text: elem.desc.replace(/&lt;/g, "<").replace(/&gt;/g, ">").replace(/&amp;/g, "&"),
        });
        let div_lable = $("<div>", {
            class: "item",
        });
        a_lable.appendTo(div_lable);
        i_lable.appendTo(div_lable);
        p_lable.appendTo(div_lable);
        div_lable.appendTo(result_lable);
    }
    

添加日志 并 部署服务器

这部分涉及到守护进程相关内容, 建议阅读博主文章了解:

🫦[Linux] 守护进程介绍、服务器的部署、日志文件…

直接在项目中引入两个文件, 这两个文件都是之前实现过 只不过做了一点点修改的. 很简单:

logMessage.hpp:

/* 日志相关 */

#pragma once

#include <cassert>
#include <cerrno>
#include <cstdarg>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <ctime>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>

// 宏定义 四个日志等级
#define DEBUG 0
#define NOTICE 1
#define WARNING 2
#define FATAL 3

#define LOGFILEPATH "serverLog.log"

const char* log_level[] = {"DEBUG", "NOTICE", "WARNING", "FATAL"};

class log {
public:
    log()
        : _logFd(-1) {}

    void enable() {
        umask(0);

        _logFd = open(LOGFILEPATH, O_WRONLY | O_CREAT | O_APPEND, 0666);
        assert(_logFd != -1);
        dup2(_logFd, STDOUT_FILENO);
        dup2(_logFd, STDERR_FILENO);
    }

    ~log() {
        if (_logFd != -1) {
            // 将系统缓冲区内容刷入文件
            fsync(_logFd);
            close(_logFd);
        }
    }

private:
    int _logFd;
};

#define LOG(LEVEL, MESSAGE, ...) logMessage(LEVEL, (__FILE__), (__LINE__), MESSAGE, ##__VA_ARGS__)
// 实现一个 可以输出: 日志等级、日志时间、用户、以及相关日志内容的
// 日志消息打印接口
void logMessage(int level, const char* file, int line, const char* format, ...) {
    // 通过可变参数实现, 传入日志等级, 日志内容格式, 日志内容相关参数

    // 确保日志等级正确
    assert(level >= DEBUG);
    assert(level <= FATAL);

    // 获取当前用户名
    char* name = getenv("USER");

    // 简单的定义log缓冲区
    char logInfo[1024];

    // 定义一个指向可变参数列表的指针
    va_list ap;
    // 将 ap 指向可变参数列表中的第一个参数, 即 format 之后的第一个参数
    va_start(ap, format);

    // 此函数 会通过 ap 遍历可变参数列表, 然后根据 format 字符串指定的格式,
    // 将ap当前指向的参数以字符串的形式 写入到logInfo缓冲区中
    vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);

    // ap 使用完之后, 再将 ap置空
    va_end(ap); // ap = NULL

    // 通过判断日志等级, 来选择是标准输出流还是标准错误流
    FILE* out = (level == FATAL) ? stderr : stdout;

    // 获取本地时间
    time_t tm = time(nullptr);
    struct tm* localTm = localtime(&tm);
    char* localTmStr = asctime(localTm);
    char* nC = strstr(localTmStr, "\n");
    if (nC) {
        *nC = '\0';
    }
    fprintf(out, "%s | %s | %s | %s | %s:%d\n", log_level[level], localTmStr,
            name == nullptr ? "unknow" : name, logInfo, file, line);

    // 将C缓冲区的内容 刷入系统
    fflush(out);
    // 将系统缓冲区的内容 刷入文件
    fsync(fileno(out));
}

daemonize.hpp:

/* 守护进程接口 */
#pragma once

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

void daemonize() {
    int fd = 0;

    // 1. 忽略SIGPIPE
    signal(SIGPIPE, SIG_IGN);
    // 2. 改变工作路径
    // chdir(const char *__path);
    // 3. 不要成为进程组组长

    if (fork() > 0) {
        exit(0);
    }
    // 4. 创建独立会话
    setsid();
    // 重定向文件描述符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);

        // dup2三个标准流之后, fd就没有用了
        if (fd > STDERR_FILENO) {
            close(fd);
        }
    }
}

在项目中引入这两个文件之后, 就可以将httpServer.cc设置为守护进程.

并将 整个项目中所有向标准输出和标准错误打印日志的信息, 都改为LOG(LEVEL, MESSAGE, ...)形式 向文件中打印日志:

#include <iostream>
#include <pthread.h>
#include "util.hpp"
#include "daemonize.hpp"
#include "searcher.hpp"
#include "logMessage.hpp"
#include "httplib.h"

const std::string& input = "./data/output/raw";
const std::string& rootPath = "./wwwRoot";

int main() {
    // 守护进程设置, 部署服务器
    daemonize();
    // 日志系统
    class log logSvr;
    logSvr.enable();

    ns_searcher::searcher searcher;
    searcher.initSearcher(input);

    httplib::Server svr;

    svr.set_base_dir(rootPath.c_str());
    svr.Get("/s", [&searcher](const httplib::Request& request, httplib::Response& response) {
        // 首先, 网页发起请求 如果需要带参数, 则是需要以 key=value的格式在url中 或者 正文有效中传参的
        // 就像我们使用一般搜索引擎搜索一样:
        // 如果在 google搜索http, 那么 url就会变为 https://www.google.com/search?q=http&sxsrf=AB5stBgDxDV91zrABB
        // 其中 q=http 就是一对 key=value 值, 而 httplib::Request::has_param() 就是识别请求url中是否携带了 某个key=value
        // 本项目中, 我们把搜索内容 的key=value对, 设置为word=搜索内容
        if (!request.has_param("word")) {
            // url中没有 word 键值
            // set_content() 第一个参数是设置正文内容, 第二个参数是 正文内容类型等属性
            response.set_content("请输入内容后搜索", "text/plain; charset=utf-8");
        }
        std::string searchContent = request.get_param_value("word");
        LOG(NOTICE, "User search:: %s", searchContent.c_str()); 	// 调用LOG()
        // std::cout << "User search:: " << searchContent << std::endl;

        std::string searchJsonResult;
        searcher.search(searchContent, &searchJsonResult);
        // 搜获取到搜索结果之后 设置相应内容
        response.set_content(searchJsonResult, "application/json");
    });
    // svr.Get("/hi", [](const httplib::Request&, httplib::Response& res) {
    //  res.set_content("Hello World!", "text/plain");
    // });

    LOG(NOTICE, "服务器启动成功...");
    // std::cout << "服务器启动成功..." << std::endl;
    svr.listen("0.0.0.0", 8080);

    return 0;
}

执行了daemonize()之后, 服务器就会变成守护进程. 只要服务器主机不关机 或者 不主动kill掉进程. 服务就会一直在后台运行. 所有人都可以随时随地访问.

欢迎访问: Boost库 文档搜索

不欢迎搞破坏!!

项目的完整目录结构 以及 完整代码 展示

目录结构

pwd
/home/July/gitCode/gitHub/Boost-Doc-Searcher
❯ tree -L 3
.
├── cppjieba
│   ├── DictTrie.hpp
│   ├── ...(jieba库相关头文件)
│   └── Unicode.hpp
├── cppjiebaDict
│   ├── hmm_model.utf8
│   ├── ...(jieba库提供的分词库)
│   └── user.dict.utf8
├── daemonize.hpp
├── data
│   ├── input
│   │   ├── about.html
│   │   ├── ...(Boost库文档文件)
│   │   └── yap.html
│   └── output
│       └── raw
├── httplib.h
├── httpServer.cc
├── index.hpp
├── LICENSE
├── logMessage.hpp
├── makefile
├── parser
├── parser.cc
├── README.md
├── searcher.hpp
├── searcherServerd
├── serverLog.log
├── util.hpp
└── wwwRoot
    ├── favicon.svg
    └── index.html

64 directories, 287 files

完整代码

整个项目的完整代码已提交至Github: Boost-Doc-Searcher

欢迎收藏使用~


本篇文章至此结束. 但此项目还有扩展内容, 可以关注一下专栏等待后续更新~

感谢阅读~

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【使用基于二阶积分器的结构生成正交信号】基频共振而无延迟地滤波信号的正交信号生成模块,为单相系统创建 α/β 信号(Simulink实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

Nacos本地部署-开发使用

有些项目&#xff0c;需要在本地启动nacos&#xff0c;否则会与其他开发人员产生冲突&#xff0c;这里是写给该场景下的开发人员使用的文档 一、如何本地部署和使用 Nacos 1.下载编译后的压缩包 可以在 github 上 Nacos Release 版本列表中下载最新的稳定版本。地址&#xf…

用Shap-E生成3D模型

Shap-E 是 OpenAI 开发的突破性模型&#xff0c;它使用文本或图像作为输入生成一系列 3D 对象&#xff0c;以其创新方法改变了 3D 应用领域。 这项非凡的技术可以在 GitHub 上免费获取&#xff0c;允许用户在计算机上无缝运行它&#xff0c;而无需 OpenAI API 密钥或互联网连接…

AAAI论文阅读

文章目录 Open-Vocabulary Multi-Label Classifcation via Multi-Modal Knowledge Transfer——知识蒸馏的范畴Med-EASi: Finely Annotated Dataset and Models for Controllable Simplifcation of Medical Texts——医学领域数据集构建“Nothing Abnormal”: Disambiguating M…

测试数据不会造?可以用这个工具Faker

在测试过程中&#xff0c;大家应该都遇到过各种各样的数据构造问题。e.g. 构造一批通讯录、构造一批用户三要素(姓名手机号身份证)、构造一批银行卡数据…… 这时候&#xff0c;测试数据大多数可能是这样的: 张三, 130 0000 0001 李四, 130 0000 0002 王五, 130 0000 0003 …

Scons编译lib库

实例目录结构&#xff1a; include文件夹&#xff1a;test.hsrc文件夹&#xff1a;test.cSConscriptSConstruct 如下图所示&#xff1a; SConstruct&#xff1a; #执行当前目录下的SConscript SConscript(SConscript);SConscript&#xff1a; import os from SCons.Script…

模拟实现消息队列项目(系列6) -- 网络通信协议的设计

目录 前言 1. 明确需求 2. 设计应用层协议 2. 定义Request 和 Response 3. 定义参数的父类 4. 定义其他参数类 4.1 创建交换机 4.2 删除交换机 4.3 创建队列 4.4 删除队列 4.5 创建绑定 4.6 删除绑定 4.7 发布消息 4.8 订阅消息 4.9 推送消息 结语 前言 上一章节,我们完成了虚拟…

实现栈和队列

文章目录 1.栈 1.1 栈的概念及结构 1.2 栈的实现 实现栈的标识索引 数组实现栈 栈的结构定义 栈空间数据的初始化和销毁 入栈和出栈 获取栈顶元素、计算栈空间元素个数、判空 2.队列 2.1 队列的概念及结构 2.2 链表的实现 实现链表的标识索引 链表实现队列 定…

JavaScript基础 第一天

本套笔记是通过学习B站Pink老师JavaScript核心进阶 前端必学总结的学习笔记&#xff0c;希望自己之后在使用的过程中能够将所学知识融会贯通 学习目标 1. 理解变量是存储数据的容器 2.理解什么是数据并知道数据的类型 3.知道JavaScript数据类型转换的特征 学习目录 1.Jav…

java 自定义xss校验注解实现

自定义一个注解Xss。名字随意 import javax.validation.Constraint; import javax.validation.Payload; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Targe…

JVM垃圾回收篇-垃圾回收算法与五种引用

JVM垃圾回收篇-垃圾回收算法与五种引用 引用计数法 引用计数&#xff08;英语&#xff1a;reference counting&#xff0c;RC&#xff09;是计算机编程语言中的一种内存管理技术&#xff0c;是指将资源&#xff08;可以是对象、内存或磁盘空间等等&#xff09;的被引用次数保…

mac安装open3d时候出现错误

在测试open3d是否正常顺利安装时&#xff0c;出现了如下错误&#xff1a; python -c "import open3d as o3d; print(o3d.__version__)" Traceback (most recent call last):File "<string>", line 1, in <module>File "/Users/huangzhe/…

【产品设计】消息通知系统设计

消息通知可以将内容实时送达用户手机页面&#xff0c;但是泛滥的消息通知会引起用户的反感&#xff0c;也违背了这个设计的初衷。 消息通知可以及时地将状态、内容的更新触达到用户&#xff0c;用户则可以根据收到的消息做后续判断。但是如果没有及时将重要消息触达到用户或者滥…

Java基础入门篇——数据类型(六)

目录 一、基本数据类型 1.1整型类型变量 1.2浮点型 1.3字符型 1.4布尔型 二、引用数据类型 Java是一个强类型语言&#xff0c;Java中的数据必须明确数据类型。数据类型的作用就是约束变量存储数据的形式。例如&#xff0c;定义为int类型存储整数&#xff0c;定义为double…

【分布式流控组件 Sentinel 快速入门】——图文详解操作流程

&#x1f4a7; 分布式流控组件 S e n t i n e l 快速入门 \color{#FF1493}{分布式流控组件 Sentinel 快速入门} 分布式流控组件Sentinel快速入门&#x1f4a7; &#x1f337; 仰望天空&#xff0c;妳我亦是行人.✨ &#x1f984; 个人主页——微风撞见云的博客&#…

ThreadPoolExecutor线程池详解

ThreadPoolExecutor线程池详解 1. 背景 项目最近的迭代中使用到了ThreadPoolExecutor线程池&#xff0c;之前都只是知道怎么用&#xff0c;没有了解过线程池的底层原理&#xff0c;项目刚上线&#xff0c;有时间整理一下线程池的用法&#xff0c;学习一下线程池的底层实现与工…

Stable Diffusion - Style Editor 和 Easy Prompt Selector 提示词插件配置

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132122450 Stable Diffusion 的 Prompt 的功能&#xff0c;可以用文字来描述想要生成的图像&#xff0c;根据输入来创造出逼真的图像。Prompt 支持…

Mysql面试题(查询重复数据删除重复数据)

Create table A (id int) 注意&#xff1a;id列非自增&#xff0c;由代码产生并输入&#xff0c;但代码可能产生重复id 1.业务定义中&#xff0c;id列不允许重复&#xff0c;用什么方式保证重复的id不会被输入表中&#xff1f; 2.若已经发生数据重复&#xff0c;请写出SQL语…

【C++进阶之路】继承与多态的概念考察

文章目录 一、问答题二、概念题三、答案与解析问答题概念题 一、问答题 什么是菱形继承&#xff1f;菱形继承的问题是什么&#xff1f;什么是菱形虚拟继承&#xff1f;如何解决数据冗余和二义性的。继承和组合的区别&#xff1f;什么时候用继承&#xff1f;什么时候用组合&…

9:00开始面试,9:08就出来了,这问题问的实在是····

外包工作3年&#xff0c;今年裸辞跳槽&#xff0c;很幸运的是找到了下家&#xff0c;不过 自从加入到这家公司&#xff0c;每天不是在加班就是在加班的路上&#xff0c;薪资倒是给的不少&#xff0c;所以我也就忍了。没想到6月一纸通知&#xff0c;所有人都不许加班&#xff0…