当前位置: 首页 > news >正文

基于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 athttp://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.分析读取后的文件,结果放到outcomevector<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;//文档idstring keyword;//关键词int weight;//权重Inverted_zipper():weight(0){}}Inverted_zipper;class index{private:vector<Format> Front_index;//正排索引 下标模拟文档idunordered_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){//切分tempvector<string> result;string sep = "\3";bool flag = project_tool::stringtool::Slice_strings(temp,&result,sep);if(!flag){LOG(Warning,"Slice_strings WARNING");return nullptr;}//切分好后放到FormatFormat 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;  // 设置文档IDtemp.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]);//debugtemp["url"] = doc->url;//debugtemp["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 负数和整形提升bugbegin = 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';//0keyword = 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

相关文章:

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

目录 1.项目相关背景2.宏观原理3.相关技术栈和环境4.正排、倒排索引原理5.去标签和数据清洗模块parser5.1.认识标签5.2.准备数据源5.3.编写数据清洗代码parser5.3.1.编写读取文件Readfile5.3.2.编写分析文件Anafile5.3.2.编写保存清洗后数据SaveHtml5.3.2.测试parser 6.编写索引…...

【2023年】云计算金砖牛刀小试3

A场次题目:OpenStack平台部署与运维 业务场景: 某企业拟使用OpenStack搭建一个企业云平台,用于部署各类企业应用对外对内服务。云平台可实现IT资源池化,弹性分配,集中管理,性能优化以及统一安全认证等。系统结构如下图: 企业云平台的搭建使用竞赛平台提供的两台云服务…...

在以太坊中不同合约之间相互调用的场景有哪些?

在以太坊中&#xff0c;合约调用合约的场景有很多&#xff0c;以下是一些常见的情况&#xff1a; 一、复杂业务逻辑的拆分 模块化设计&#xff1a; 当一个智能合约的业务逻辑变得复杂时&#xff0c;可以将其拆分为多个较小的合约&#xff0c;每个合约负责特定的功能。例如&…...

关于 PC打开“我的电脑”后有一些快捷如腾讯视频、百度网盘、夸克网盘、迅雷等各种捷方式在磁盘驱动器上面统一删除 的解决方法

若该文为原创文章&#xff0c;转载请注明原文出处 本文章博客地址&#xff1a;https://hpzwl.blog.csdn.net/article/details/142029325 长沙红胖子Qt&#xff08;长沙创微智科&#xff09;博文大全&#xff1a;开发技术集合&#xff08;包含Qt实用技术、树莓派、三维、OpenCV…...

数据结构——初识数据结构

数据结构——初识数据结构 数据结构的概念数据的类型时间复杂度 数据结构的概念 相互之间存在一种或多种特定关系的数据元素的集合。数据结构是计算机科学中的一个基本概念&#xff0c;它是指数据元素之间的关系和组织方式。数据结构是计算机存储、组织数据的方式&#xff0c;…...

每日搜索论坛回顾:2024年9月13日

Google正在测试一个新的广告标签标题&#xff0c;使广告更加明显。Google搜索排名的波动仍然非常剧烈&#xff0c;即使在核心更新完成一周后仍然如此。Google正在向本地服务广告的广告主发送验证通知。Bing正在测试带有评论来源图标的本地包。Google AdSense正在将自动广告扩展…...

猎板PCB大讲堂:PCB设计铺铜技巧与策略全解析

在电子工程领域&#xff0c;PCB的设计不仅仅是连接电子元件的桥梁&#xff0c;更是确保设备性能和稳定性的关键。铺铜&#xff0c;作为PCB设计中的一个微妙而强大的环节&#xff0c;常常被低估。 猎板PCB带您深入了解铺铜的艺术&#xff0c;探讨其背后的科学原理&#xff0c;以…...

Matplotlib - Statistical Distribution作图

1. 前言 在数据分析和统计学中&#xff0c;绘制统计分布图是非常重要的&#xff0c;因为它帮助我们直观地理解数据的特性&#xff0c;并为进一步的分析提供基础。统计分布图能够揭示数据集的结构、趋势、集中趋势和离散程度等信息&#xff0c;从而使我们更容易做出合理的假设、…...

【机器学习】9 ——最大熵模型的直观理解

系列文章目录 提示&#xff1a;写完文章后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 系列文章目录前奏例子硬币垃圾邮件代码 前奏 【机器学习】6 ——最大熵模型 例子 硬币 假设我们有一枚硬币&#xff0c;可能是公平的&#xff0c;…...

1.单例模式

目录 简介 饿汉式 懒汉式 双重检测锁式 静态内部类式 枚举单例 测试 测试单例模式&#xff1a; 测试五种单例模式在多线程环境下的效率 问题&#xff08;拓展&#xff09; 例&#xff1a;反射破解单例模式 例&#xff1a;反序列化破解单例模式 总结&#xff1a;如何…...

数据倾斜问题

数据倾斜&#xff1a;主要就是在处理MR任务的时候&#xff0c;某个reduce的数据处理量比另外一些的reduce的数据量要大得多&#xff0c;其他reduce几乎不处理&#xff0c;这样的现象就是数据倾斜。 官方解释&#xff1a;数据倾斜指的是在数据处理过程中&#xff0c;由于某些键…...

大龄焦虑?老码农逆袭之路:拥抱大模型时代,焕发职业生涯新活力!

其实我很早就对大龄程序员这个话题感到焦虑&#xff0c;担心自己35岁之后会面临失业&#xff0c;有时和亲戚朋友聊天时&#xff0c;也会经常拿这个出来调侃。现在身边已经有很多35岁左右的同事&#xff0c;自己过两年也会步入35岁的行列&#xff0c;反倒多了一份淡定和从容。 …...

Vue 页面反复刷新常见问题及解决方案

Vue 页面反复刷新常见问题及解决方案 引言 Vue.js 是一个流行的前端框架&#xff0c;旨在通过其响应式的数据绑定和组件化的开发模式简化开发。然而&#xff0c;在开发 Vue.js 应用时&#xff0c;页面反复刷新的问题可能会对用户体验和开发效率产生负面影响。本文将深入探讨 …...

Windows上指定盘符-安装WSL虚拟机(机械硬盘)

参考来自于教程1&#xff1a;史上最全的WSL安装教程 - 知乎 (zhihu.com)https://zhuanlan.zhihu.com/p/386590591#%E4%B8%80%E3%80%81%E5%AE%89%E8%A3%85WSL2.0 教程2&#xff1a;Windows 10: 将 WSL Linux 实例安装到 D 盘&#xff0c;做成移动硬盘绿色版也不在话下 - 知乎 (z…...

ffmpeg实现视频的合成与分割

视频合成与分割程序使用 作者开发了一款软件&#xff0c;可以实现对视频的合成和分割&#xff0c;界面如下&#xff1a; 播放时&#xff0c;可以选择多个视频源&#xff1b;在选中“保存视频”情况下&#xff0c;会将多个视频源合成一个视频。如果只取一个视频源中一段视频…...

团体标准的十大优势

一、团体标准是什么 团体标准是指由社会团体&#xff08;行业协会、联合会、企业联盟等&#xff09;按照自己确立的制定程序&#xff0c;自主制定、发布、采纳&#xff0c;并由社会自愿采用的标准。简单的说&#xff0c;就是社会团体为了满足市场和创新需要&#xff0c;协调相…...

java spring boot 动态添加 cron(表达式)任务、动态添加停止单个cron任务

java spring boot 动态添加 cron&#xff08;表达式&#xff09;任务、动态添加停止单个cron任务 添加对应的maven <dependency><groupId>org.quartz-scheduler</groupId><artifactId>quartz</artifactId><version>2.3.0</version…...

sqlgun靶场漏洞挖掘

1.xss漏洞 搜索框输入以下代码&#xff0c;验证是否存在xss漏洞 <script>alert(1)</script> OK了&#xff0c;存在xss漏洞 2.SQL注入 经过测试&#xff0c;输入框存在SQL注入漏洞 查询数据库名 查询管理员账号密码 此处密码为MD5加密&#xff0c;解码内容如下 找…...

好用的 Markdown 编辑器组件

ByteMD bytedance/bytemd: ByteMD v1 repository (github.com) 这里由于我的项目是 Next&#xff0c;所以安装 bytemd/react&#xff0c; 阅读官方文档&#xff0c;执行命令来安装编辑器主体、以及 gfm&#xff08;表格支持&#xff09;插件、highlight 代码高亮插件&#xf…...

uniapp vite3 require导入commonJS 的js文件方法

vite3 导入commonJS 方式导出 在Vite 3中&#xff0c;你可以通过配置vite.config.js来实现导入CommonJS&#xff08;CJS&#xff09;风格的模块。Vite 默认支持ES模块导入&#xff0c;但如果你需要导入CJS模块&#xff0c;可以使用特定的插件&#xff0c;比如originjs/vite-pl…...

通义灵码用户说:“人工编写测试用例需要数十分钟,通义灵码以毫秒级的速度生成测试代码,且准确率和覆盖率都令人满意”

通过一篇文章&#xff0c;详细跟大家分享一下我在使用通义灵码过程中的感受。 一、定义 通义灵码&#xff0c;是一个智能编码助手&#xff0c;它基于通义大模型&#xff0c;提供代码智能生成、研发智能问答能力。 在体验过程中有任何问题均可点击下面的连接前往了解和学习。 …...

MySQL中的约束

约束概述 1.1 为什么需要约束 数据完整性&#xff08;Data Integrity&#xff09;是指数据的精确性&#xff08;Accuracy&#xff09;和可靠性&#xff08;Reliability&#xff09;。它是防止数据库中存在不符合语义规定的数据和防止因错误信息的输入输出造成无效操作或错误信…...

Leetcode 寻找重复数

可以使用 位运算 来解决这道题目。使用位运算的一个核心思想是基于数字的二进制表示&#xff0c;统计每一位上 1 的出现次数&#xff0c;并与期望的出现次数做比较。通过这种方法&#xff0c;可以推断出哪个数字重复。 class Solution { public:int findDuplicate(vector<i…...

大一新生以此篇开启你的算法之路

各位大一计算机萌新们&#xff0c;你们好&#xff0c;本篇博客会带领大家进行算法入门&#xff0c;给各位大一萌新答疑解惑。博客文章略长&#xff0c;可根据自己的需要观看&#xff0c;在博客中会有给大一萌新问题的解答&#xff0c;请不要错过。 入门简介&#xff1a; 算法…...

【AI大模型】ChatGPT模型原理介绍(上)

目录 &#x1f354; 什么是ChatGPT&#xff1f; &#x1f354; GPT-1介绍 2.1 GPT-1模型架构 2.2 GPT-1训练过程 2.2.1 无监督的预训练语言模型 2.2.2 有监督的下游任务fine-tunning 2.2.3 整体训练过程架构图 2.3 GPT-1数据集 2.4 GPT-1模型的特点 2.5 GPT-1模型总结…...

基于UE5和ROS2的激光雷达+深度RGBD相机小车的仿真指南(五):Blender锥桶建模

前言 本系列教程旨在使用UE5配置一个具备激光雷达深度摄像机的仿真小车&#xff0c;并使用通过跨平台的方式进行ROS2和UE5仿真的通讯&#xff0c;达到小车自主导航的目的。本教程默认有ROS2导航及其gazebo仿真相关方面基础&#xff0c;Nav2相关的学习教程可以参考本人的其他博…...

C++竞赛初阶L1-15-第六单元-多维数组(34~35课)557: T456507 图像旋转

题目内容 输入一个 n 行 m 列的黑白图像&#xff0c;将它顺时针旋转 90 度后输出。 输入格式 第一行包含两个整数 n 和 m&#xff0c;表示图像包含像素点的行数和列数。1≤n≤100&#xff0c;1≤m≤100。 接下来 n 行&#xff0c;每行 m 个整数&#xff0c;表示图像的每个像…...

无线领夹麦克风哪个牌子好?西圣、罗德、猛犸领夹麦克风深度评测

​如今短视频和直播行业蓬勃发展&#xff0c;无线领夹麦克风成为了许多创作者不可或缺的工具。然而&#xff0c;市场上的无线领夹麦克风品牌众多、质量参差不齐&#xff0c;为了帮助大家挑选到满意的产品&#xff0c;我作为数码测评博主&#xff0c;对无线领夹麦克风市场进行了…...

React Native 0.76,New Architecture 将成为默认模式,全新的 RN 来了

关于 React Native 的 New Architecture 概念&#xff0c;最早应该是从 2018 年 RN 团队决定重写大量底层实现开始&#xff0c;因为那时候 React Native 面临各种结构问题和性能瓶颈&#xff0c;最终迫使 RN 团队开始进行重构。 而从 React Native 0.68 开始&#xff0c;New A…...

Java并发:互斥锁,读写锁,Condition,StampedLock

3&#xff0c;Lock与Condition 3.1&#xff0c;互斥锁 3.1.1&#xff0c;可重入锁 锁的可重入性&#xff08;Reentrant Locking&#xff09;是指在同一个线程中&#xff0c;已经获取锁的线程可以再次获取该锁而不会导致死锁。这种特性允许线程在持有锁的情况下&#xff0c;可…...