基于Boost库的搜索引擎开发实践

目录

  • 1.项目相关背景
  • 2.宏观原理
  • 3.相关技术栈和环境
  • 4.正排、倒排索引原理
  • 5.去标签和数据清洗模块parser
      • 5.1.认识标签
      • 5.2.准备数据源
      • 5.3.编写数据清洗代码parser
        • 5.3.1.编写读取文件Readfile
        • 5.3.2.编写分析文件Anafile
        • 5.3.2.编写保存清洗后数据SaveHtml
        • 5.3.2.测试parser
  • 6.编写索引模块index
      • 6.1.编写index.hpp基本框架
      • 6.2.编写建立正排函数Establish_Front_index
      • 6.3.编写建立倒排函数Establish_inverted_index
  • 7.编写搜索模块Search.hpp
      • 7.1.Search.hpp基本代码框架
      • 7.2.编写search代码
      • 7.3.测试
  • 8.编写网络服务http_server模块
      • 8.1.升级gcc安装cpp-httplib库
      • 8.2.编写http_server代码
  • 9.添加日志服务
  • 10.前端代码
  • 11.总结
      • 11.1.去掉暂停词
      • 11.2.效果演示

1.项目相关背景

日常我们会使用一些搜索引擎:例如百度、搜狗、Edge等,用来搜索相关资讯,那么我们能否自己实现一个搜索引擎呢?当然是可以的,但是无法实现如此大量级的引擎,我们可以对某些网站内:实现一个站内的搜索引擎。例如在cplusplus中就有站内搜索。这样我们的搜索结果数据也更加垂直。
我们随机在搜索引擎上搜索关键词:
在这里插入图片描述
可以观察到一个搜索结果大致由三部分组成,然后一个搜索页面内有多条结果。后续我们的搜索引擎的设计就可以参考这种形式。

2.宏观原理

基本宏观原理如下图所示:
在这里插入图片描述

3.相关技术栈和环境

技术栈:C/C++、C++11、STL、准标准库boost、cppjieba、cpp-httplib、jsoncpp
前端仅基本使用:html5、js、css、ajax、jQuery
环境:centos7.6云服务器、vim、vscode

4.正排、倒排索引原理

  1. 正排索引:正排索引是从文档到关键词的映射,也就是说,对于每一个文档,存储该文档中包含的所有关键词及其相关信息。
  2. 倒排索引 :倒排索引是从关键词到文档的映射,也就是说,对于每一个关键词,存储包含该关键词的所有文档ID。一个关键词可能对应多个文档。

正排索引示例

文档ID词汇
1搜索引擎排序
2信息检索排序

倒排索引示例

词汇文档ID列表
搜索引擎[1]
排序[1, 2]
信息检索[2]

当然在倒排索引不仅包含关键词和对应的文档id,还会有类似权重的概念。根据词频用来标识此搜索结果在页面的前后排序。

暂停词:在搜索引擎中暂停词是指那些在文本处理中被认为不具有实际检索意义的常见词汇。这些词通常非常频繁出现,但它们对查询结果的相关性没有直接帮助,因此在索引和查询处理阶段经常被忽略,例如:a, an, the, and, or, but, is, are, to, from,的, 了, 在, 是, 和, 也, 与

暂停词也是在后续我们要去掉的。

5.去标签和数据清洗模块parser

5.1.认识标签

<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">
<html>
  <head>
  <!-- Copyright (C) 2002 Douglas Gregor <doug.gregor -at- gmail.com>

      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>Redirect to generated documentation</title>
    <meta http-equiv="refresh" content="0; URL=http://www.boost.org/doc/libs/master/doc/html/signals.html">
  </head>
  <body>
    Automatic redirection failed, please go to
    <a href="http://www.boost.org/doc/libs/master/doc/html/signals.html">http://www.boost.org/doc/libs/master/doc/html/signals.html</a>
  </body>
</html>

<> : 是html的标签,去标签是数据清洗的重要一环,我们要去掉<>以及<>中间包含的内容,提取网页中的核心文本信息。
示例如下:
原始html内容

<div class="header">
    <h1>Welcome to My Website</h1>
</div>
<p>This is a sample paragraph about <strong>search engines</strong> and their importance.</p>
<a href="http://example.com">Click here</a> to learn more.

经过去标签的纯文本内容

Welcome to My Website
This is a sample paragraph about search engines and their importance.
Click here to learn more.

5.2.准备数据源

正如项目宏观原理图所示,我们既然要对数据做去标签和清洗,首先我们要有数据,所以我们先来到boost官网将我们需要的数据下载下来,这里使用的是1_78_0的版本。
在这里插入图片描述
我们将boost_1_78_0/doc/html目录下的html文件保存下来,当做数据源。再在项目目录下建立data/input下保存我们的数据源。
在这里插入图片描述
raw_html用来存放我们清洗完成的数据。

5.3.编写数据清洗代码parser

我们搜索出的结果由标题title、内容content、网址url构成,所以我们在数据清洗时,应该规定统一格式便于后续处理。这里我们采用的方案是:title\3content\3url \n title\3content\3url \n title\3content\3url \n …
用换行符标识一个文件的内容提取完毕,也便于我们后续从文件中读取内容。
我们先来编写大致的逻辑代码:

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

using namespace std;

const string src_path = "data/input";
const string raw = "data/raw_html/raw.txt";

typedef struct format
{
    string title;//标题
    string content;//内容
    string url;//url
}Format;
int main()
{
    vector<string> files_gather;
    //1.读取html文件的路径保存到files_gather,用于后续分析
    if(!Readfile(src_path,&files_gather))
    {
        cerr<<"Readfile is error"<<endl;
        return 1;
    }

    //2.分析读取后的文件,结果放到outcome
    vector<Format> outcome;
    if(!Anafile(files_gather,&outcome))
    {
        cerr<<"Anafile is error"<<endl;
        return 2;
    }

    //3.解析完的结果放到raw,用\3分隔
    if(!SaveHtml(outcome,raw))
    {
        cerr<<"SaveHtml is error"<<endl;
        return 3;
    }

    return 0;
}

首先我们将数据源的文件路径读取保存到files_gather,接着读取分析文件为Format格式并保存起来。分析后的结果放到指定的文件下并按照我们规定的格式写入。

5.3.1.编写读取文件Readfile

首先我们要打开保存数据源的文件,遍历文件夹内容,挑选出是普通文件并且后缀为.html的文件保存。

bool Readfile(const string &src_path,vector<string> *files_gather)
{
    boost::filesystem::path file_path(src_path);
    if(!boost::filesystem::exists(file_path))//判断stc_path路径是否不存在
    {
        cerr<<"src_path is does not exist"<<endl;
        return false;
    }
	//boost::filesystem::directory_iterator 用于迭代指定目录的直接内容,不会递归遍历子目录

    //boost::filesystem::recursive_directory_iterator 用于递归遍历目录及其子目录的内容
    boost::filesystem::recursive_directory_iterator end; //空迭代器,标志结束
    for(boost::filesystem::recursive_directory_iterator iter(file_path);iter!=end;iter++)//遍历
    {
        if(!boost::filesystem::is_regular_file(*iter))//我们需要后缀.html并且是普通文件
        {
           continue;
        }
        if(iter->path().extension()!=".html")
        {
          continue;
        }
     
        files_gather->push_back(iter->path().string());

    }
    return true;
}

其中我们使用到了boost库中的方法,所以要再云服务器下安装boost开发库,指令:

sudo yum install -y boost-devel
5.3.2.编写分析文件Anafile

刚刚我们已经将文件路径都保存了,接下来根据文件路径读取文件内容,并且分析并结构体形式保存并返回即可,首先读取文件内容,我们封装到另一个文件下tool.hpp用来实现功能模块。

#pragma once

#include<iostream>
#include<string>
#include<istream>
#include <fstream>
#include<vector>
#include <boost/algorithm/string.hpp>     //使用boost split
using namespace std;

namespace project_tool
{
    class Filetool
    {
        public:
        static bool divestfile(const string &files_gather,string *result)
        {
            ifstream in(files_gather, ios::in);

            if(!in.is_open()){
                cerr << "open file " << files_gather << " error" << endl;
                return false;
            }

            string line;
            while(getline(in, line)){ 
                *result += line;
            }

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

Anafile函数主逻辑:

bool Anafile(vector<string> &files_gather,vector<Format> *outcome)
{
    for(string &file : files_gather)
    {
        string result;//读取文件内容
        if(!project_tool::Filetool::divestfile(file,&result))
        {
          continue;
        }
        
        Format temp;

        if(!partitle(result,&temp.title))//读取文档标题
        {
            continue;
        }
        
        if(!parcontent(result,&temp.content))//去标签
        {
            continue;
        }

        if(!parturl(file,&temp.url))
        {
            continue;
        }
        outcome->push_back(move(temp));//性能提升
    }
    return true;
}

partitle提取title比较简单,在html中<title></title> ,中间的内容就是html网页的标题了,所以代码:

static bool partitle(const string &result,string *title)
{
    size_t begin = result.find("<title>");
    if(begin == string::npos)
    {
        return false;
    }

    size_t end = result.find("</title>");
    if(end == string::npos)
    {
        return false;
    }

    begin += string("<title>").size();

    if(begin>end)
    {
        return false;
    }

    *title = result.substr(begin,end-begin);
    return true;
}

parcontent提取文档内容,即是去标签,在这里我们使用了一个状态机来标识,进而提取内容:

static bool parcontent(const string &result,string *content)
{
    enum state
    {
        Label,
        Content
    };

    state a =Label;
    for(char c : result)
    {
        switch (a)
        {
        case Label:
            if(c == '>')
                a =Content;
            break;
        case Content:
            if(c=='<')
                a=Label;
            else
            {
                if(c =='\n') c=' ';
                content->push_back(c);
            }
            break;
        default:
            break;
        }
    }    
    return true;
}

parturl提取文档url,首先我们要搞懂官网url与我们项目中文件路径的关系。
官网url:https://www.boost.org/doc/libs/1_78_0/doc/html/chrono.html
项目下文件路径:data/input/chrono.html
拼接:https://www.boost.org/doc/libs/1_78_0/doc/html + /chrono.html
所以:

static bool parturl(const string &file,string *url)
{
    string url_head = "https://www.boost.org/doc/libs/1_78_0/doc/html";
    
    string url_tail = file.substr(src_path.size());
    *url =(url_head+url_tail);
    return true;
}
5.3.2.编写保存清洗后数据SaveHtml

数据已经清洗完毕,将其以二进制形式写入到我们预留的data/raw_html/raw.txt文件中即可。

bool SaveHtml(vector<Format> &outcome,const string &raw)
{
    const char c = '\3';

    ofstream out(raw, ios::out | ios::binary);
    if(!out.is_open()){
        cerr << "open " << raw << " failed!" << endl;
        return false;
    }

    for(Format &item : outcome){
        string temp_out;

        temp_out = item.title;
        temp_out += c;
        temp_out += item.content;
        temp_out += c;
        temp_out += item.url;
        temp_out += '\n';

        out.write(temp_out.c_str(), temp_out.size());
        if (out.fail()) {
        std::cerr << "Error occurred while writing to the file." << std::endl;
        return 1;
        }   
    }

    out.close();

    return true;
}
5.3.2.测试parser

首先raw.txt下并无内容:
在这里插入图片描述
执行parser后:
在这里插入图片描述
在这里插入图片描述
可以看到一共有8141个文档,其中的^C就是\3,所以是符合我们的预期的。

6.编写索引模块index

6.1.编写index.hpp基本框架

#pragma once

#include<mutex>
#include <iostream>
#include <string>
#include <vector>
#include <unordered_map>
#include <fstream>
#include "tool.hpp"

using namespace std;

namespace project_index
{
    typedef struct format
    {
        string title;
        string url;
        string content;
        uint64_t docid;//文档id

    }Format;
    
    typedef struct Inverted_zipper//倒排拉链
    {
        uint64_t docid;//文档id
        string keyword;//关键词
        int weight;//权重
        Inverted_zipper()
            :weight(0){}
    }Inverted_zipper;

    class index
    {
      private:
        vector<Format> Front_index;//正排索引 下标模拟文档id

        unordered_map<string,vector<Inverted_zipper>> inverted_index;//倒排 关键词与多个(一个)倒排拉链的对应

        static index * Index;
        static mutex mtx; 
        index(const index &)=delete;
        index& operator=(const index&)=delete;

        index()
        {}
      public:
        ~index()
        {}
   static index* GetIndex()
   {
    if(nullptr == Index)
     {
        mtx.lock();
        if(nullptr == Index){
            Index = new index();
        }
        mtx.unlock();
    }
        return Index;
   }
    //id获得文档内容
    Format* GetFront_index(uint64_t docid)
    {
    	  if(docid>=Front_index.size())
        {
            LOG(Warning,"docid>=Front_index.size");
            return nullptr;
        }

        return &Front_index[docid];
    }

    //关键词获得倒排拉链
    vector<Inverted_zipper>* Getinverted_index(const string &keyword)
    {
    	auto it = inverted_index.find(keyword);
        if(it == inverted_index.end())
        {
            LOG(Warning,"keyword find Warning");
            return nullptr;
        }
        
        return &(it->second);
    }

    //建立索引 数据源:parser处理完的数据
    bool Establish_index(const string &raw)
    {
    	ifstream in(raw,ios::in | ios::binary);
        if(!in.is_open())
        {
            LOG(Warning,"in.is_open Warning");
            return false;
        }

        string temp;
        int count =0;
        while(getline(in,temp))
        {
            Format* doc = Establish_Front_index(temp);//建立正排索引
            if(doc == nullptr)
            {
                LOG(Warning,"Establish_Front_index warning");
                continue;
            }
          
            bool flag = Establish_inverted_index(*doc);//建立倒排索引
            count++;
            LOG(Info,"当前已经建立索引的文档 :" + to_string(count));
        }
        return true;
    }   

    private:
        Format* Establish_Front_index(string &temp)
        {}

        bool Establish_inverted_index(Format &doc)//建立倒排
        {}


    };
    index * index::Index = nullptr;
    mutex index::mtx;
}

其中正排索引使用vector的下标来当做文档id,Format结构体标识了一个文档的标题内容url和id。倒排索引是关键词与多个(一个)倒排拉链的对应,倒排拉链vector<Inverted_zipper>.

6.2.编写建立正排函数Establish_Front_index

在编写Establish_Front_index函数之前我们又要在tool中加入一个功能模组:

 class stringtool
    {
    public:
        static bool Slice_strings(string &line,vector<string> *out,const string sep)
        {
            boost::split(*out,line,boost::is_any_of(sep),boost::token_compress_on);//"\3"
            return true;
        }
    };

boost中的split用于将字符串拆分为多个子字符串:
参数说明

  • results:接收拆分结果的容器,通常是 vectorstd::string dequestd::string。
  • text:要拆分的源字符串。
  • boost::is_any_of(“,”):指定分隔符。可以使用各种 boost::algorithm 的函数对象来指定分隔符,也可以使用自定义的分隔符。
  • 使用 boost::token_compress_on 来忽略连续的分隔符:

Establish_Front_index

Format* Establish_Front_index(string &temp)
        {
            //切分temp
            vector<string> result;
            string sep = "\3";

            bool flag = project_tool::stringtool::Slice_strings(temp,&result,sep);
            if(!flag)
            {
                LOG(Warning,"Slice_strings WARNING");
                return nullptr;
            }
            //切分好后放到Format

            Format doc;
            if(result.size() != 3)
            {
                LOG(Warning,"Slice_strings WARNING");
                return nullptr;
            }
            doc.title = result[0];
            doc.content = result[1];
            doc.url = result[2];
            //id为vector下标
            doc.docid = Front_index.size();

            //结果插入正排索引
            Front_index.push_back(move(doc));//move性能优化
            return &Front_index.back();
        }

我们将一行格式化好的文档交给Establish_Front_index后,函数会根据格式切分,并保存到Format中,并插入到正排中。

在 C++ 中,当你向一个容器(如 std::vector)使用 push_back 方法添加元素时,使用 std::move 可以显著提升性能。

6.3.编写建立倒排函数Establish_inverted_index

倒排函数Establish_inverted_index是根据结构体Format,对文档标题和内容进行分词,然后统计词频,最后插入倒排当中。
其中分词用到了cppjieba,cppjieba库码云链接,同样这里分词功能也加在tool中:

	const char* const DICT_PATH = "./dict/jieba.dict.utf8";
    const char* const HMM_PATH = "./dict/hmm_model.utf8";
    const char* const USER_DICT_PATH = "./dict/user.dict.utf8";
    const char* const IDF_PATH = "./dict/idf.utf8";
    const char* const STOP_WORD_PATH = "./dict/stop_words.utf8";

    class jiebatool
    {
    private:
		static cppjieba::Jieba jieba;
    public:
       
        static void CutString(const std::string &src, std::vector<std::string> *out)
        {
           jieba.CutForSearch(src, *out);
        }
    };
    cppjieba::Jieba jiebatool::jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH);//不用赋值初始化

Establish_inverted_index函数编写:注意在建立倒排的时候我们要忽略大小写,我们统一转换成小写。


bool Establish_inverted_index(Format &doc) 
{
    
    struct word_count
    {
        int title_count;  // 标题中的词频
        int content_count;  // 内容中的词频
        word_count() : title_count(0), content_count(0) {}  // 默认构造函数,初始化为0
    };

    // 存储从标题中提取的词
    vector<string> title_result;
    // 使用分词工具将标题切分成词,并存储到 title_result 中
    project_tool::jiebatool::CutString(doc.title, &title_result);

    // 创建一个哈希表,用于记录每个词及其在标题和内容中的出现次数
    unordered_map<string, word_count> word_map;

    // 遍历标题中的每个词
    for (string &s : title_result)
    {
        // 将词转换为小写,确保忽略大小写
        boost::to_lower(s);
        // 更新该词在标题中的出现次数
        word_map[s].title_count++;
    }

    // 存储从内容中提取的词
    vector<string> content_result;
    // 使用分词工具将内容切分成词,并存储到 content_result 中
    project_tool::jiebatool::CutString(doc.content, &content_result);

    // 遍历内容中的每个词
    for (string &s : content_result)
    {
        // 将词转换为小写,确保忽略大小写
        boost::to_lower(s);
        // 更新该词在内容中的出现次数
        word_map[s].content_count++;
    }

    // 设置标题权重因子
    const int title_corr = 10;

    // 遍历所有的词和其出现次数
    for (auto &iter : word_map)
    {
        // 创建一个倒排索引条目
        Inverted_zipper temp;
        temp.docid = doc.docid;  // 设置文档ID
        temp.keyword = iter.first;  // 设置词汇
        // 计算词的权重:标题中的出现次数乘以权重因子加上内容中的出现次数
        temp.weight = title_corr * (iter.second.title_count) + iter.second.content_count;
        // 获取倒排索引中的词汇对应的词条列表
        vector<Inverted_zipper> &vector_temp = inverted_index[iter.first];
        // 将倒排索引条目添加到词条列表中
        vector_temp.push_back(move(temp));
    }
    return true;
}

7.编写搜索模块Search.hpp

7.1.Search.hpp基本代码框架

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


namespace project_search
{
    struct more_Inverted_zipper
    {
      uint64_t docid;
      vector<string> words;
      int weight;
    more_Inverted_zipper():docid(0),weight(0){};
    };

    class search
    {
    private:
      project_index::index * Index;
    public:
      search(){}
      ~search(){}

      void Initsearch(const string &input)
      {
          Index = project_index::index::GetIndex();
          LOG(Info,"获取索引单例成功");

          Index->Establish_index(input);
          LOG(Info,"构建正排倒排索引成功");
      }

      void Search(string &keyword,string *json_word)
      {}
    };
} 

7.2.编写search代码

主逻辑搜索代码主要分为四部分:

  • 对keyword分词
  • 对分出的词在索引中查找
  • 根据权重对搜索结果排降序
  • 构建Json串返回

安装jsoncpp:

sudo yum install -y jsoncpp-devel
struct more_Inverted_zipper
    {
      uint64_t docid;
      vector<string> words;
      int weight;
    more_Inverted_zipper():docid(0),weight(0){};
    };
    
void Search(string &keyword,string *json_word)
      {

        vector<string> result;

        project_tool::jiebatool::CutString(keyword,&result);


        
        //vector<project_index::Inverted_zipper> Inverted_listmax;
        vector<more_Inverted_zipper> Inverted_listmax;

        unordered_map<uint64_t,more_Inverted_zipper> part_map;

        for(string s :result)
        {   
            boost::to_lower(s);
            vector<project_index::Inverted_zipper> *Inverted_list = Index->Getinverted_index(s);
            if(nullptr == Inverted_list)
            {
                continue;
            }
            
            //Inverted_listmax.insert(Inverted_listmax.end(),Inverted_list->begin(),Inverted_list->end());//重复插入的问题
            for(auto &it:*Inverted_list)
            {
                auto &temp = part_map[it.docid];
                temp.docid = it.docid;
                temp.weight += it.weight;
                temp.words.push_back(move(it.keyword));
            }
        }

        for(const auto &it : part_map){
                    Inverted_listmax.push_back(move(it.second));
                }

        sort(Inverted_listmax.begin(), Inverted_listmax.end(),
                          [](const more_Inverted_zipper &e1, const more_Inverted_zipper &e2){
                          return e1.weight > e2.weight;
                          });

        Json::Value root;
        for(auto &it : Inverted_listmax)
        {
               project_index::Format * doc  = Index->GetFront_index(it.docid);
               if(nullptr == doc)
               {
                  continue;
               }
               Json::Value temp;
               temp["title"] = doc->title;
               temp["summary"] = Getsummary(doc->content,it.words[0]);//debug
               temp["url"] = doc->url;

               //debug
               temp["weight"] = it.weight;
               temp["docid"] = (int)it.docid;
               
               root.append(temp);
        }
        Json::FastWriter writer;
        *json_word = writer.write(root);
      }

其中more_Inverted_zipper中vector< string>使用这样的结构是因为会出现多个关键词指向同一个文档,这时候结构体如果只有一个words 那么在索引搜索过后插入就会有重复,不必要的浪费,还会导致搜索结果可能出现重复文档的情况

Getsummary获取摘要函数,一个文档中内容是非常多的,难道我们都要在搜索结果中显示出来吗?当然不是,这里我们就要设定一个从内容从获取摘要的函数逻辑:

string Getsummary(const string &content,const string &keyword)//摘要
      {
          int Front_loaded = 30;
          int Back_loaded = 70;

 
          auto it = std::search(content.begin(),content.end(),
          keyword.begin(),keyword.end(),[](int x,int y){
              return (tolower(x) == tolower(y));
              });

          int pos = distance(content.begin(),it);

          int begin = 0;
          int end = content.size()-1;

          if(pos-Front_loaded>begin)//size_t 负数和整形提升bug
              begin = pos - Front_loaded;
          if(pos+Back_loaded<end)
            end = pos + Back_loaded;

          string temp = content.substr(begin,end-begin);
          temp += "...";
          return temp;
      }

7.3.测试

测试代码debug:

#include <iostream>
#include "Search.hpp"
#include <cstdio>

const string input = "data/raw_html/raw.txt";

int main()
{
    project_search::search* test_search =  new project_search::search();
    test_search->Initsearch(input);

    string keyword;
    string json_word;

    char inbuffer[1024];

    while(true)
    {
      cout<<"Please enter keyword :";
      fgets(inbuffer,sizeof(inbuffer)-1,stdin);
      cout << strlen(inbuffer) << endl;
      inbuffer[strlen(inbuffer)-1]= '\0';//0
      keyword = inbuffer;
      test_search->Search(keyword,&json_word);
      cout<<keyword<<endl;
      cout<<json_word<<endl;
    }
    return 0;
}

在这里插入图片描述
根据提示输入想要搜索的词后:
在这里插入图片描述
就可以看到很多搜索结果根据权重大小排列了出来。

8.编写网络服务http_server模块

8.1.升级gcc安装cpp-httplib库

首先我们gcc默认的版本是4.8.5
而cpp-httplib库则需要新版本的gcc,所以我们要升级下gcc:

curl -sLf https://gitee.com/lpsdz-ybhdsg-jk/yum-source-update/raw/master/install.sh -o ./install.sh && bash ./install.sh 

执行命令后再安装scl和新版本的gcc:

sudo yum install centos-release-scl scl-utils-build
sudo yum install -y devtoolset-7-gcc devtoolset-7-gccc++

升级之后可以查看当前gcc的版本已经更新:
在这里插入图片描述

接着我们安装cpp网络库,下面是链接,这里注意我们安装0.7.15版本的cpp-httplib网络库

8.2.编写http_server代码

#include <iostream>  
#include <string>    
#include "Search.hpp"  
#include "cpp-httplib/httplib.h"  // 使用 httplib 库处理 HTTP 请求
#include "log.hpp" 

using namespace std; 
const string input = "data/raw_html/raw.txt"; 
const string root = "wwwroot"; // 服务器的根目录,存储静态文件

#define PORT 8081 

int main()
{
   
    project_search::search Search;

    Search.Initsearch(input);

    // 创建 HTTP 服务器对象 `svr`
    httplib::Server svr;

    // 设置服务器根目录
    svr.set_base_dir(root.c_str());

    // 处理 GET 请求,路径为 `/s`,用于处理搜索请求
    svr.Get("/s", [&Search](const httplib::Request &req, httplib::Response &res) {
        // 检查请求中是否包含查询参数 "word"
        if (!req.has_param("word"))
        {
            // 如果没有提供 "word" 参数,返回错误提示
            res.set_content("必须要有搜索关键字!", "text/plain; charset=utf-8");
            return;
        }

        // 获取查询参数 "word" 的值,表示用户搜索的关键词
        string word = req.get_param_value("word");
        // 记录用户搜索关键词到日志中
        LOG(Info, "用户在搜索 :" + word);

        string json_string;  // 存储搜索结果的 JSON 格式字符串

       
        Search.Search(word, &json_string);
        res.set_content(json_string, "application/json");
    });

    // 记录服务器启动成功的信息,输出端口号
    LOG(Info, "服务器成功启动 port :" + to_string(PORT));

    // 启动 HTTP 服务器,监听 0.0.0.0(所有网络接口)的 8081 端口
    svr.listen("0.0.0.0", PORT);

    return 0;  // 程序正常结束
}

9.添加日志服务

在源代码中我们多用cerr来打印一些错误信息,在工程中更倾向用日志来打印信息:



#pragma once

#include <iostream>
#include <string>
#include <ctime>
#include <iomanip>  // 用于格式化输出
#include <time.h>
#include <stdarg.h>
#include <unistd.h>
#include <stdlib.h>
using namespace std;

#define Info 0
#define Debug 1
#define Warning 2
//#define Error 3
#define Fatal 4

#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)

void log(const string& level, const string& message, const string& file, int line)
{
    cout << "[" << level << "] ";
    time_t t = time(nullptr);
    struct tm *ctime = localtime(&t);
    char leftbuffer[1024];
    snprintf(leftbuffer, sizeof(leftbuffer), "[%d:%d:%d]",ctime->tm_hour, ctime->tm_min, ctime->tm_sec);

    // 输出日志信息
    cout << "[" << message << "] "<<leftbuffer ;

    cout << "[" << file << " : " << line << "]" << endl;
}

在这里插入图片描述
如上图所示就能实时打印写信号供我们了解程序运行状况。

10.前端代码

前端代码主要涉及的技术栈有html5、css、JQuery。这里不做重点讲解,本项目主研究后端技术。
wwwroot/index.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">
    <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
    <title>Boost 搜索引擎</title>
    <style>
        * {
            margin: 0;
            padding: 0;
            box-sizing: border-box;
        }

        html, body {
            height: 100%;
            font-family: Arial, sans-serif;
        }

        .container {
            width: 800px;
            margin: 15px auto;
        }

        .search {
            width: 100%;
            display: flex;
            align-items: center;
        }

        .search input {
            flex: 1;
            height: 50px;
            border: 1px solid black;
            border-right: none;
            padding-left: 10px;
            font-size: 14px;
            color: #CCC;
        }

        .search button {
            width: 150px;
            height: 52px;
            background-color: #4e6ef2;
            color: #FFF;
            font-size: 19px;
            border: none;
            cursor: pointer;
        }

        .search button:hover {
            background-color: #3b5f9a;
        }

        .result {
            width: 100%;
        }

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

        .result .item a {
            display: block;
            text-decoration: none;
            font-size: 20px;
            color: #4e6ef2;
        }

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

        .result .item p {
            margin-top: 5px;
            font-size: 16px;
        }

        .result .item i {
            display: block;
            font-style: normal;
            color: green;
        }
    </style>
</head>

<body>
    <div class="container">
        <div class="search">
            <input type="text" placeholder="请输入搜索关键字">
            <button onclick="search()">搜索一下</button>
        </div>
        <div class="result"></div>
    </div>

    <script>
        async function search() {
            const query = $(".search input").val();
            console.log("query =", query);

            try {
                const response = await fetch(`/s?word=${encodeURIComponent(query)}`);
                const data = await response.json();
                buildHtml(data);
            } catch (error) {
                console.error("Error fetching data:", error);
            }
        }

        function buildHtml(data) {
            const resultLabel = $(".result");
            resultLabel.empty();

            data.forEach(elem => {
                const divLabel = $("<div>", { class: "item" });
                $("<a>", { text: elem.title, href: elem.url, target: "_blank" }).appendTo(divLabel);
                $("<p>", { text: elem.summary }).appendTo(divLabel);
                $("<i>", { text: elem.url }).appendTo(divLabel);
                divLabel.appendTo(resultLabel);
            });
        }
    </script>
</body>

</html>

11.总结

11.1.去掉暂停词

这个项目中还有很多可扩展的地方,这里我先添加一个方向—去掉暂停词,在正排倒排索引中我们讲过暂停词的概念,去掉暂停词可以提升搜索的效率,提升搜索结果的相关性:


const char* const DICT_PATH = "./dict/jieba.dict.utf8";       
const char* const HMM_PATH = "./dict/hmm_model.utf8";          
const char* const USER_DICT_PATH = "./dict/user.dict.utf8";   
const char* const IDF_PATH = "./dict/idf.utf8";               
const char* const STOP_WORD_PATH = "./dict/stop_words.utf8";   


class jiebatool
{
private:
    cppjieba::Jieba jieba;  

    // 构造函数,初始化 jieba 分词器
    jiebatool()
        : jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH) 
    {}

    unordered_map<string, bool> stop_word_map; 

    // 禁用拷贝构造函数
    jiebatool(const jiebatool&) = delete;
    jiebatool& operator=(const jiebatool&) = delete;

    static jiebatool* instances;  

public:
   
    static jiebatool* Get_instances()
    {
        static mutex mtx;  
        if (instances == nullptr) 
        {
            mtx.lock();  // 加锁,保证线程安全
            if (instances == nullptr)  
            {
                instances = new jiebatool();  
                instances->Initjiebatoolstop();  
            }
            mtx.unlock(); 
        }
        return instances;  
    }

    // 初始化停止词映射表
    void Initjiebatoolstop()
    {
        ifstream in(STOP_WORD_PATH); 
        if (!in.is_open()) 
        {
            LOG(Fatal, "STOP_WORD_PATH open error");
            return;
        }
        string temp;
        while (getline(in, temp))  
        {
            stop_word_map.insert({temp, true});
        }
        in.close(); 
    }

    // 对字符串进行分词,并移除停止词
    void Curstringstop(const std::string &src, std::vector<std::string> *out)
    {
        jieba.CutForSearch(src, *out);  // 使用 jieba 进行搜索模式分词
        for (auto it = out->begin(); it != out->end();)  // 遍历分词结果
        {
            auto temp = stop_word_map.find(*it);  
            if (temp != stop_word_map.end())  
            {
                it = out->erase(it);  // 移除该词
            }
            else
            {
                it++;  
            }
        }
    }


    static void CutString(const std::string &src, std::vector<std::string> *out)
    {
        project_tool::jiebatool::Get_instances()->Curstringstop(src, out);  // 调用单例实例的分词方法
    }
};

jiebatool* jiebatool::instances = nullptr;

  

11.2.效果演示

我们在浏览器输入云服务器ip加上开放的端口号即可访问服务,进入前端实现的页面:

在这里插入图片描述
在搜索框中输入我们要搜索的内容,点击搜索,则出现的由多条搜索结果根据权重组成的网页:
在这里插入图片描述

我们随机点一个也能正常跳转:
在这里插入图片描述

项目源码;点击跳转码云:adexiur

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

STM32内部闪存FLASH(内部ROM)、IAP

1 FLASH简介 1 利用程序存储器的剩余空间来保存掉电不丢失的用户数据 2 通过在程序中编程(IAP)实现程序的自我更新 &#xff08;OTA&#xff09; 3在线编程&#xff08;ICP把整个程序都更新掉&#xff09; 1 系统的Bootloader写死了&#xff0c;只能用串口下载到指定的位置&a…

从基础到进阶:利用EasyCVR安防视频汇聚平台实现高效视频监控系统的五步走

随着科技的飞速发展&#xff0c;视频监控技术在社会安全、企业管理、智慧城市构建等领域扮演着越来越重要的角色。一个高效智能的视频监控管理系统不仅能够提升监控效率&#xff0c;还能在预防犯罪、事故预警、数据分析等方面发挥巨大作用。 一、需求分析 在设计视频监控管理…

sql中索引查看是否生效

在pg数据库中有多种索引存在&#xff0c;在一般情况下我们取使用普通索引 以下是一些常见导致索引未命中的原因和优化策略 1.如果查询中的条件与索引字段的顺序不匹配&#xff0c;或者索引字段没有完全包含在查询条件中&#xff0c;索引可能不会被使用。 2.在查询中使用函数…

【Python 数据分析学习】Matplotlib 的基础和应用

题目 1 Matplotlib 主要特性2 Matplotlib 基础知识2.1 导入模块2.2 图形构成2.2.1 图形&#xff08;Figure&#xff09;2.2.2 轴 &#xff08;Axes&#xff09;2.2.3 轴线&#xff08;axis&#xff09; 2.5 中文设置2.5.1 借助rcParams修改字体实现设置2.5.2 增加一个fontprope…

结构体的内存对齐

对⻬规则&#xff1a; 1.结构体的第⼀个成员对⻬到和结构体变量起始位置偏移量为0的地址处 2.其他成员变量要对⻬到某个数字&#xff08;对⻬数&#xff09;的整数倍的地址处。 对⻬数编译器默认的⼀个对⻬数与该成员变量⼤⼩的较⼩值。 但一些编译器下并没有默认对其数 3.结…

Python 解析 JSON 数据

1、有如下 JSON 数据&#xff0c;存放在 data.json 文件&#xff1a; [{"id":1, "name": "小王", "gender": "male", "score": 96.8}, {"id":2, "name": "小婷", "gender&qu…

【踩坑】装了显卡,如何让显示器从主板和显卡HDMI都输出

转载请注明出处&#xff1a;小锋学长生活大爆炸[xfxuezhagn.cn] 如果本文帮助到了你&#xff0c;欢迎[点赞、收藏、关注]哦~ 背景介绍 装了显卡后&#xff0c;开机默认是从显卡的HDMI输出&#xff0c;但这很不方便。如何让视频仍然从主板输出&#xff1f;或者说让显卡HDMI和主板…

黑马十天精通MySQL知识点

一. MySQL概述 安装使用 MySQL安装完成之后&#xff0c;在系统启动时&#xff0c;会自动启动MySQL服务&#xff0c;无需手动启动。 也可以手动的通过指令启动停止&#xff0c;以管理员身份运行cmd&#xff0c;进入命令行执行如下指令&#xff1a; 1 、 net start mysql80…

stm32单片机个人学习笔记3(GPIO输出)

前言 本篇文章属于stm32单片机&#xff08;以下简称单片机&#xff09;的学习笔记&#xff0c;来源于B站教学视频。下面是这位up主的视频链接。本文为个人学习笔记&#xff0c;只能做参考&#xff0c;细节方面建议观看视频&#xff0c;肯定受益匪浅。 STM32入门教程-2023版 细…

用Docker 安装Nacos

目录 持久化到数据库 即&#xff1a;Docker MySql 访问管理页面 持久化到数据库 即&#xff1a;Docker MySql 在本机下 新建数据库 进下载中心&#xff1a;https://github.com/alibaba/nacos/releases 下载&#xff1a;https://github.com/alibaba/nacos/releases/tag/2.…

Linux操作系统文件权限管理

Linux操作系统下文件的权限分为当前用户权限、用户组权限和其他用户权限&#xff0c;然后每一类用户或组又分为读权限(r)、写权限(w)和可执行权限(x)。 如图1&#xff0c;打开任一目录&#xff0c;右键单击文件&#xff0c;在弹出菜单选择“属性”&#xff0c;在弹出的属性选项…

【医药行业】实施SAP有哪些医药行业GXP的合规要求和注意事项

作为实施过辉瑞和赛诺菲医药行业的项目&#xff0c;总结了如下&#xff1a; 在医药行业中&#xff0c;GxP&#xff08;Good Practices&#xff0c;良好规范&#xff09;是一系列标准与指南&#xff0c;旨在确保制药、医疗设备和生物制品的质量与合规性。GxP包括多个领域&#x…

Leetcode 找到字符串中所有字母异位词

在 C 中&#xff0c;两个 vector<int> 类型的变量进行 操作时&#xff0c;会逐个比较它们的元素&#xff0c;只有当两个向量的长度相同且每个位置上的元素值都相同时&#xff0c; 操作才会返回 true。 因此&#xff0c;在这道题的代码中&#xff0c;sCount pCount 这一…

【Linux篇】TCP/IP协议(笔记)

目录 一、TCP/IP协议族体系结构 1. 数据链路层 &#xff08;1&#xff09;介绍 &#xff08;2&#xff09;常用协议 ① ARP协议&#xff08;Address Resolve Protocol&#xff0c;地址解析协议&#xff09; ② RARP协议&#xff08;Reverse Address Resolve Protocol&…

基于对数变换的图像美白增强,Matlab实现

博主简介&#xff1a;matlab图像处理&#xff08;QQ:3249726188&#xff09; ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 本次案例是基于对数变换的图像美白增强&#xff0c;用matlab实现。 一、案例背景和算法介绍 这次案例是美白算法&…

JavaScript 事件处理

一、简介 ​ 事件&#xff1a;发生在HTML元素上的事情&#xff0c;可以是用户的行为&#xff0c;也可以是浏览器的行为&#xff0c;如 用户点击了某个HTML元素用户将鼠标移动到某个HTML元素上用户输入数据时光标离开页面加载完成 ​ 事件源&#xff1a;事件触发的源头&#xf…

知识|智能网联汽车多域电子电气架构会如何发展?

摘要&#xff1a;随着汽车智能化和网联化技术的快速发展&#xff0c;传统的电子电气架构已经无法满足未来车路云网一体化发展的新需求。本文聚焦于未来智能网联汽车的多域电子电气架构&#xff0c;并从总体设计、硬件系统、通信系统和软件系统四个方面对现有技术进行了详细的综…

STL-vector练习题

118. 杨辉三角 思路&#xff1a; 杨辉三角有以下性质使我们要用到的&#xff1a; ● 每行数字左右对称&#xff0c;由 1 开始逐渐变大再变小&#xff0c;并最终回到 1。 ● 第 n 行&#xff08;从 0 开始编号&#xff09;的数字有 n1 项&#xff0c;前 n 行共有 2n(n1)个数。…

使用ShardingSphere实现MySql的分库分表

目录 一 什么是ShardingSphere分库分表 二 代码实现 1.导入相关依赖 2.配置相关参数 3.创建学生类以及mapper接口 4.实现 StandardShardingAlgorithm接口自定义分片算法 唐洋洋我知道你在看!!!嘿嘿 一 什么是ShardingSphere分库分表 我们平时在设计数据库的时候&#xf…

基于UDP的简易网络通信程序

目录 0.前言 1.前置知识 网络通信的大致流程 IP地址 端口号&#xff08;port&#xff09; 客户端如何得知服务器端的IP地址和端口号&#xff1f; 服务器端如何得知客户端的IP地址和端口号&#xff1f; 2.实现代码 代码模块的设计 服务器端代码 成员说明 成员实现 U…