基于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

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

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

无人机动力系统设计之电调芯片参数选型

无人机动力系统设计之电调芯片参数选型 1. 源由2. 关键因素2.1 电压范围2.2 电流处理能力2.3 控制方式2.4 PWM输出与分辨率2.5 通讯接口2.6 保护功能2.7 支持霍尔传感器与无传感器模式2.8 集成度与外围器件2.9 效率与散热2.10 市场供应与成本 3. 因素阐述3.1 PWM工作频率3.1.1 …

Seata

TC - 事务协调者 维护全局和分支事务的状态&#xff0c;驱动全局事务提交或回滚。 TM - 事务管理器 定义全局事务的范围&#xff1a;开启全局事务、提交或回滚全局事务。 RM - 资源管理器 管理分支事务处理的资源&#xff0c;向 TC 注册分支事务&#xff0c;报告分支事务的…

Chainlit集成LlamaIndex并使用通义千问模型实现AI知识库检索网页对话应用

前言 之前使用Chainlit集成Langchain并使用通义千问大语言模型的API接口&#xff0c;实现一个基于文档文档的网页对话应用。 可以点击我的上一篇文章《Chainlit集成Langchain并使用通义千问和智谱AI实现AI知识库检索网页对话应用》 查看。 本次将Langchain框架更改为LlamaInde…

Hive任务优化参数整理

Hive本身是个基于hdfs的结构化数据管理工具&#xff0c;虽然在后面的发展中允许底层接入其他的数据源&#xff0c;比如第三方数据服务这种基础架构&#xff0c;但是它从立意上来说&#xff0c;它不适合用来做高性能查询引擎&#xff0c;反而在传统离线数据仓库中它有着自身的优…

使用 Elastic 和 LM Studio 的 Herding Llama 3.1

作者&#xff1a;来自 Elastic Charles Davison, Julian Khalifa 最新的 LM Studio 0.3 更新使 Elastic 的安全 AI Assistant 能够更轻松、更快速地与 LM Studio 托管模型一起运行。在这篇博客中&#xff0c;Elastic 和 LM Studio 团队将向你展示如何在几分钟内开始使用。如果你…

校园生活点餐外卖配送到宿舍小程序源码系统 带完整的安装代码包以及搭建部署教程

系统概述 随着移动互联网的普及和校园生活的多元化&#xff0c;学生们对于线上点餐、外卖配送等服务的依赖度越来越高。然而&#xff0c;传统的校园餐饮服务模式往往存在效率低下、覆盖范围有限、信息不透明等问题&#xff0c;难以满足学生多样化的需求。因此&#xff0c;开发…

30万月变现3-6万,你也可以

30万月变现3-6万&#xff0c;你也可以 我相信&#xff0c;这些年我的IP还是吸引了不少的中产阶级关注我&#xff0c;什么叫中产阶级&#xff0c;就是兜里还是能随便掏出大几十大几百万的现金的&#xff0c;这群人&#xff0c;有着自己稳定的职业和收入来源&#xff0c;生活水平…

实例讲解电动汽车换挡控制策略及Simulink模型搭建方法

轻型电动汽车一般采用直驱的方式作为驱动方式&#xff0c;即不经过变速箱进行速比的调节&#xff0c;电机输出轴直接经过主减速器将动力传输至车轮。换挡器的换挡方式一般为VCU接收档位器的换挡请求后&#xff0c;根据整车当前状态判断换挡状态&#xff0c;最后输出当前档位状态…

已经30岁了,想转行从头开始现实吗?什么样的工作算好工作?

我是29岁那年&#xff0c;完成从转行裸辞副业的职业转型。 如果你把职业生涯看成是从现在开始30岁&#xff0c;到你退休那年&#xff0c;中间这么漫长的30年&#xff0c;那么30岁转行完全来得及&#xff1b; 如果你觉得必须在什么年纪&#xff0c;什么时间内必须完成赚到几十…

拯救手残党,一键转换季节天气变化效果图!

一键可转换四季或多种天气效果图 AI现在已经发展到这种程度了吗&#xff1f; 神采PromeAI也是卷起来了 以往通过【草图渲染】功能 就生成转化各种效果图 但这对用户来说还不够 又给大家上线了「场景转换器」功能 一键就能生成想要的各种时节场景的变化 一张效果图可以给…

机器学习之实战篇——MNIST手写数字0~9识别(全连接神经网络模型)

机器学习之实战篇——Mnist手写数字0~9识别&#xff08;全连接神经网络模型&#xff09; 文章传送MNIST数据集介绍&#xff1a;实验过程实验环境导入模块导入MNIST数据集创建神经网络模型进行训练&#xff0c;测试&#xff0c;评估模型优化 文章传送 机器学习之监督学习&#…

Qt绘制动态仪表(模仿汽车仪表指针、故障灯)

背景&#xff1a; 项目需要&#xff0c;可能需要做一些仪表显示。此篇除了介绍实现方法&#xff0c;还要说明心路历程。对我而言&#xff0c;重要的是心理&#xff0c;而不是技术。写下来也是自勉。 本人起初心里是比较抵触的&#xff0c;从业20多年了&#xff0c;深知所谓界…

springboot Web基础开发

Spring Boot 是一个用于简化 Spring 应用开发的框架&#xff0c;它通过自动配置和开箱即用的功能&#xff0c;使得创建和部署 Spring 应用变得更为高效。以下是 Spring Boot 基础 Web 开发的一些关键点和实操总结&#xff1a; 1. 项目搭建 使用 Spring Initializr: 访问 Spring…

【Centos】关于容器启动Centos7缺失字符集的问题

最近做一些软件测试和打包&#xff0c;需要频繁的装系统&#xff0c;索性用docker启Centos容器来做&#xff0c;基础镜像做好直接启就好&#xff0c;过程中遇到一个和字符集有关的问题&#xff0c;记录一下&#xff0c;方便遇到同样问题的小伙伴。 问题描述 运行某些python脚本…

Stable Diffusion绘画 | ControlNet应用-Inpaint(局部重绘):更完美的重绘

Inpaint(局部重绘) 相当于小号的AI版PS&#xff0c;不但可以进行局部画面的修改&#xff0c;还可以去除背景中多余的内容&#xff0c;或者是四周画面内容的扩充。 预处理器说明 Inpaint_Global_Harmonious&#xff1a;重绘-全局融合算法&#xff0c;会对整个图片的画面和色调…

达梦CASE_SENSITIVE参数解析

1. 参数含义 标识符大小写敏感&#xff0c;默认值为 Y。 当大小写敏感时&#xff0c;小写的标识符应用双引号括起&#xff0c;否则被转换为大写&#xff1b;当大小写不敏感时&#xff0c;系统不自动转换标识符的大小写&#xff0c;在标识符比较时也不区分大小写。 CASE_SENS…

QtCreator学习(二).在stm32mp1中使用

0.配置编译环境 复制【正点原子】STM32MP157开发板&#xff08;A盘&#xff09;-基础资料\05、开发工具\01、交叉编译器st-example-image-qtwayland-openstlinux-weston-stm32mp1-x86_64-toolchain-3.1-snapshot.sh到虚拟机chmod添加可执行文件&#xff0c;./st*运行&#xff…

【JAVA】Tomcat性能优化、安全配置、资源控制以及运行模式超详细

文章目录 一、Tomcat性能优化application.yml配置maxThreads 连接数限制压缩传输AJP禁用 二、JVM方向优化设置并行垃圾回收器查看gc日志文件 三、Tomcat安全配置入侵防范禁用非法HTTP请求方法禁止目录列出防止恶意关闭服务配置HTTPS加密协议HttpOnly标记安全头配置 四、Tomcat资…

【鸿蒙】HarmonyOS NEXT星河入门到实战8-自定义组件-组件通信

目录 1、模块化语法 1.1 模块化基本认知 1.2 默认导出和导入 1.3 按需导出和导入 1.4 全部导入 2、自定义组件 -基础 2.1 自定义组件 - 基本使用 2.2 自定义组件 -通用样式 2.3 自定义组件 -成员函数变量 3、 状态管理 3.1 状态管理概述 3.2 State 自己的状态 3.3…