目录
- 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.正排、倒排索引原理
- 正排索引:正排索引是从文档到关键词的映射,也就是说,对于每一个文档,存储该文档中包含的所有关键词及其相关信息。
- 倒排索引 :倒排索引是从关键词到文档的映射,也就是说,对于每一个关键词,存储包含该关键词的所有文档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