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

news2024/9/20 13:27:03

目录

  • 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

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

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

相关文章

HPM6E00:PWM V2使用指南

先楫推出的HPM6E00系列芯片&#xff0c;PWM功能升级到了V2版本。和V1版本不同的是&#xff0c;V2版本的每组PWM模块包含4个独立的PWM生成模块&#xff0c;每个PWM生成模块包含一个counter和4个比较器&#xff0c;可以生成4组频率不同的PWM波。每个PWM生成模块&#xff0c;对应生…

​​​​通过给定一个全屏的位置得到该位置处是哪一个控件、 遍历窗口中的每一个元素

通过给定一个全屏的位置得到该位置处是哪一个控件&#xff08;以下方法&#xff09; [static] QWidget *QApplication::widgetAt(const QPoint &point) 场景&#xff1a;通过位置获取该位置处的widget后&#xff0c;然后进行判断&#xff0c;是不是某个或某些控件&#x…

韩语中的多义词 (치다)柯桥学韩语到蓝天广场附近

치다 1. 表示用毛笔、铅笔等点点、划线或者绘图。 예: 밑줄을 치다. 划底线 중요한 부분에 동그라미를 쳤다. 在重要的部分画上圆圈。 2. 表示倾倒少量液体或者粉末之类的东西。 예: 싱거우니 소금을 쳐야겠다. 味道淡&#xff0c;得再撒点盐。 기계에 기름을 치다. 给机…

小众创新组合!LightGBM+BO-Transformer-LSTM多变量回归交通流量预测(Matlab)

小众创新组合&#xff01;LightGBMBO-Transformer-LSTM多变量回归交通流量预测(Matlab) 目录 小众创新组合&#xff01;LightGBMBO-Transformer-LSTM多变量回归交通流量预测(Matlab)效果一览基本介绍程序设计参考资料 效果一览 基本介绍 1.Matlab实现LightGBMBO-Transformer-L…

陈坤2024行走的力量 走向山野感受距离自然更近的地方

近日&#xff0c;由陈坤发起的心灵建设类项目“行走的力量”在西藏林芝圆满完成&#xff0c;今年陈坤和行者们重返西藏&#xff0c;在海拔3500-4700的高原行走了6天5夜&#xff0c;从城市走向山间&#xff0c;感受自然里的生活&#xff0c;用行走的方式&#xff0c;让自己慢下来…

【C++ Primer Plus习题】15.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站&#xff0c;通俗易懂&#xff0c;风趣幽默&#xff0c;忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream> #include "sales.h"…

第二期: 第三节 裸机代码如何烧写

这个 过程其实 需要在 编写了 驱动之后&#xff0c; 再进行。 因为 要烧写 代码&#xff0c; 你必须 要有一份&#xff0c; 可以烧写的代码。 这里比较重要的是 secureCRT 的安装流程。 这里忘记怎么安装的了。 大致就是 &#xff0c; 先安装 secureCRT , 然后 在破解。不破…

博弈论专题一(NIM游戏)

Nim游戏 重点结论&#xff1a;对于一个Nim游戏的局面(a1,a2,...,an)&#xff0c;它是P-position当且仅当a1^a2^...^an0&#xff0c;其中^表示位异或(xor)运算。 (本篇只做简单的结论描述,详细证明过程请看这篇博客) Nim和 堆物品&#xff0c;每堆 ai 个&#xff0c;两个玩家…

Linux系统:chown命令

1、命令详解&#xff1a; chown命令用于设置文件所有者和文件关联组的命令&#xff0c;全称为change directory。在Linux当中默认文件均有拥有者&#xff0c;可以利用 chown 将指定文件的拥有者改为指定的用户或组&#xff0c;输入参数时用户可以是用户名或者用户 ID&#xff0…

零基础如何学会Appium自动化测试?

前言 appium是一款移动自动化测试工具&#xff0c;经常被用于实现UI自动化测试&#xff0c;其可支持安卓和IOS两大平台&#xff0c;还支持多种编程&#xff0c;因而得到了广泛的应用。此处便是立足于安卓平台&#xff0c;借助appium工具&#xff0c;使用python语言实现简单的自…

GUI编程09:鼠标监听事件、模拟画图工具

视频链接&#xff1a;11、鼠标监听事件、模拟画图工具_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1DJ411B75F?p11&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5 模拟画图工具的实现逻辑图&#xff1a; 实现代码&#xff1a; package com.yundait.lesson03;impo…

大模型分离架构学习记录

GPU Direct GPU 网络的情况已经发生了很大变化。每个 GPU 都有自己的内部互联&#xff0c;例如 NVIDIA 的 A100 或 H800&#xff0c;它们内部的 NVLink 互联可以达到 600GB 甚至 900GB。这种内部互联与外部以太网网络集群设计之间存在耦合关系。GPU 是单机多网卡的&#xff0c…

Mini打印机复刻过程(外设绘制)

充电管理 充电管理模块采用ME4054BM5G-N 典型应用电路 ME4054B-N的典型应用电路中&#xff0c;输入为4.5V-6.5V&#xff0c;用于给4.2V的锂电池充电。关键元件包括&#xff1a; LED指示灯&#xff1a;通过1kΩ电阻限流&#xff0c;显示充电状态。2kΩ电阻&#xff08;PROG引脚…

视频孪生市场有望达千亿级

近日&#xff0c;中国互联网协会数字孪生技术工委会副主任委员、智汇云舟创始人兼总裁周舟对数字孪生技术的市场潜力与发展前景进行了深度剖析。她表示&#xff0c;在国家政策与全球数字化转型的双重推动下&#xff0c;数字孪生技术发展迅速&#xff0c;市场潜力巨大&#xff0…

如何将任何文本语料转换为知识图谱?

转自&#xff1a;吴建明利驰软件 几个月前&#xff0c;基于知识的问答系统&#xff08;Knowledge Base Question Answering&#xff0c;KBQA&#xff09;还是个新概念。 现在&#xff0c;随着大型语言模型&#xff08;LLMs&#xff09;的发展&#xff0c;带有检索增强生成&am…

实用类工具!目前5款很火的AI写论文网站测评

在当今科技飞速发展的时代&#xff0c;人工智能&#xff08;AI&#xff09;技术已经深入到各个领域&#xff0c;包括学术研究和论文写作。AI论文写作工具的出现极大地提高了写作效率&#xff0c;帮助学者们快速生成高质量的论文内容。以下是五款非常受欢迎且功能强大的AI论文写…

MySQL零基础入门教程-8.1 表的连接\增删数据、表结构的增删改、字段约束(非空、唯一性、主键、外键、检查约束),基础+实战

教程来源&#xff1a;B站视频BV1Vy4y1z7EX 001-数据库概述_哔哩哔哩_bilibili 我听课收集整理的课程的完整笔记&#xff0c;供大家学习交流下载&#xff1a;夸克网盘分享 本文内容为完整笔记的第八篇的第一部分 目录 1、表怎么进行连接的 2、insert语句可以一次插入多条记…

深入探索 Ubuntu:从基础到高级应用

本文深入探讨了 Ubuntu 操作系统&#xff0c;涵盖了其起源与发展、安装与配置、软件管理、系统优化、网络配置、安全防护以及在不同领域的应用等多个方面。 在起源与发展部分&#xff0c;介绍了 Ubuntu 于 2004 年创立的背景以及其版本的演进。安装与配置环节详细阐述了系统安…

SprinBoot+Vue药房管理系统的设计与实现

目录 1 项目介绍2 项目截图3 核心代码3.1 Controller3.2 Service3.3 Dao3.4 application.yml3.5 SpringbootApplication3.5 Vue 4 数据库表设计5 文档参考6 计算机毕设选题推荐7 源码获取 1 项目介绍 博主个人介绍&#xff1a;CSDN认证博客专家&#xff0c;CSDN平台Java领域优质…

R语言论文插图模板第10期—正负柱状图

在之前的文章中&#xff0c;分享了R语言各式柱状图的绘制模板&#xff1a; 进一步&#xff0c;再来分享一种特殊的柱状图&#xff1a;正负柱状图。 先来看一下成品效果&#xff1a; 特别提示&#xff1a;本期内容『数据代码』已上传资源群中&#xff0c;加群的朋友请自行下载。…