从0开始完成基于异步服务器的boost搜索引擎
文章目录
- 前言
- 一、本项目涉及的技术栈和环境
- 二、boost是什么?
- 三、项目的相关背景
- 四、项目的相关原理
- 五、正排索引 vs 倒排索引 - 搜索引擎具体原理
- 六、 编写数据去标签与数据清洗的模块 Parser
- 6.1 下载boost的文档库
- 6.2 去标签
- 6.3 代码编写
- 七、索引模块
- 7.1. 整体框架
- 7.2 cppjieba安装
- 7.3 index具体实现
- 八、搜索模块
- 8.1 整体框架
- 8.2 安装JsonCpp
- 8.3 searcher具体实现
- 九、 服务器模块
- 十、 前端页面
- 十一、竞价模块
- 十二、热词统计模块
- 十三、 注册登录模块
- 最终效果展示:
前言
全部代码地址:https://gitee.com/qi-haozhe/boost_searcher
超链接点击直达
一、本项目涉及的技术栈和环境
技术栈: C/C++ C++11, STL, 准标准库Boost(提供在当前目录下遍历所有子目录文件的迭代器),Jsoncpp(提供可以将格式化的数据和json字符串相互转换的接口),cppjieba(提供分词的相关接口),cpp-httplib (提供http相关接口), 选学: html5,css,js、jQuery、Ajax。
选学部分大家完全不会也没关系,用的不多,在用的时候我给大家简单介绍一下,如果还是不懂也没关系,我会提供这部分的代码,大家到时候可以直接使用我的代码,将整个项目拼接起来,以后有时间再去看看选学部分。
项目环境: Centos 7云服务器,vim/gcc(g++)/Makefile , vs2022 or vs code
我们的项目主要是在云服务器下在Linux进行操作和编写的,涉及到vim、g++(gcc)的使用以及makefile的编写。当然不会使用vim的也可以用vs code连接上你的Linux服务器,在vs code里完成代码的编写。
对于不会使用Linux的读者也不用太担心,我会将每一步的指令写在文章中并把这条指令的效果截图放在文章中。我这里使用的是腾讯云的服务器,各位用其他的服务器也行,在自己的电脑上用VMware安装虚拟机应该也可以。
二、boost是什么?
首先我们肯定要知道boost是个啥,连boost是个啥都不知道就做boost搜索引擎那岂不是扯淡嘛。
最简单来说boost就是一个库,里面实现了很多工具,在包含了头文件之后我们就可以使用这些工具,来提高我们开发的效率。
下面是关于boost的一些详细介绍,各位可以看一下。
引用于电子发烧友和C语言中文网,这俩我都设置了超链结,大家点一下可以看更详细的介绍。
boost是一个准标准库,相当于STL的延续和扩充,它的设计理念和STL比较接近,都是利用泛型让复用达到最大化。不过对比STL,boost更加实用。STL集中在算法部分,而boost包含了不少工具类,可以完成比较具体的工作。
boost主要包含一下几个大类:字符串及文本处理、容器、迭代子(Iterator)、算法、函数对象和高阶编程、泛型编程、模板元编程、预处理元编程、并发编程、数学相关、纠错和测试、数据结构、输入/输出、跨语言支持、内存相关、语法分析、杂项。 有一些库是跨类别包含>的,就是既属于这个类别又属于那个类别。
Boost 是一个功能强大、构造精巧、跨平台、开源并且完全免费的 C++ 程序库。
1998 年,Beman G.Dawes(C++标准委员会成员之一)发起倡议并建立了 Boost 社区,其目的是向 C++ 程序员提供免费的、经同行审查的、可移植的、高质量的 C++ 源程序库。
Boost 强调程序库要与 C++ 标准库很好地共同工作,建立在“既有的实践”之上并提供参考实现,因此 Boost 库可以适合最后的标准化。
自创立以来,Boost 社区的工作已经取得了卓越的成果,C++ 标准库中有三分之二来自 Boost 库,而且将来 Boost 库中还会有更多的库进入新标准。
C++ 四十余年的发展历史中产生了数不清的程序库,有影响力的程序库也不计其数,然而其中没有一个程序库能够与 Boost 相提并论,Boost 有着其他程序库无法比拟的优点,具体如下:
许多 Boost 库的作者本身就是 C++ 标准委员会成员,因此,Boost“天然”成了标准库的后备,负责向新标准输送组件,这也使得 Boost 获得了“准”标准库的美誉。
Boost 独特的同行审查制度保证了每一个 Boost 库组件都经过了严格的审查和验证,使其具有很高的工业强度,甚至超过大多数商业产品的实现。
Boost 采用了类似 STL 的编程范式,但却并没有 STL 那样晦涩难懂,其代码格式优美清晰、易于阅读,而且 Boost 附带丰富的说明文档——它既是一个程序库,也是一个很有价值的学习现代 C++ 编程的范本。
Boost 的发布采用 Boost Software License,这是一个不同于 GPL 和 Apache 的非常宽松的许可证,该许可证允许库用户将 Boost 用于任何用途,既鼓励非商业用途,也鼓励商业用途。用户无须支付任何费用,不受任何限制,即可轻松享有 Boost 的全部功能。
Boost 官方于 2019 年 12 月发布的 1.72 版本,共包含 160余个库/组件,涵盖字符串与文本处理、容器、迭代器、算法、图像处理、模板元编程、并发编程等多个领域,使用 Boost,将大大增强 C++ 的功能和表现力。
这个是boost官网链接:https://www.boost.org/
也可以PC端直接点击蓝色字体boost库官网进入官网。
三、项目的相关背景
搜索本质上都是在自己的数据库中检索,那么根据一般根据搜索的范围划分,我们可以将搜索技术分为全网搜索和站内搜索。
- 全网搜索,如google,百度
- 站内搜索,如 淘宝、微信里的搜索
两者的区别在于,全网搜索需要检索全网的内容,所以搜索引擎需要利用爬虫技术,爬取网页的资料,整合到自己的数据库中,才能被用户搜到。例如google 的 spider 爬虫机器人,就会定期爬取全网的所有页面,收录到 google 的系统中。只有被收录的网页,才能够在 google 搜索到。
而站内搜索,因为搜索的内容都是自己的,所以更重要的是怎样将内容更好地组织好,放到搜索引擎中,让用户更快搜索到。
举一个大家日常都在使用的例子 —— 微信搜索:微信就将自己的搜索结果分为聊天记录、联系人、文章、表情、百科……等等不同的分类,让用户更快地找到想搜的内容。
大家肯定都用过百度,搜狗等等这样的搜索引擎,但我们如果想自己手搓一个百度,手搓一个搜狗,这显然是不可能的,工作量太大了,只有很多技术高超的程序员相互合作,每个部门相互配合才能完成。
URL:统一资源定位符(或称统一资源定位器/定位地址、URL地址),有时也被俗称为网页地址(网址)。URL就如同在网络上的门牌,是因特网上标准的资源的地址(Address)。
因此我们选择设计一个站内搜索,百度它们是从全网搜集信息,然后将这些信息经过一系列操作最终呈现在我们眼前。而我们要做的就是给定一个关键词然后在boost网站内进行搜索,最终将搜索到的结果按一定规则显示出来,这就是我们要做的boost搜索引擎。
还有一个因素是boost网站是没有站内搜索的,因此我们可以自己给它设计一个站内搜索。
总结:
- 公司:百度、搜狗、360搜索、头条新闻客户端 - 我们自己实现是不可能的!
- 站内搜索:搜索的数据更垂直,数据量其实更小
- boost的官网是没有站内搜索的,需要我们自己做一个
四、项目的相关原理
以上部分都是前言,大家可以不看,但下面的内容还请仔细阅读,文字说明的原理极有可能写代码的时候要用,如果文字没看懂,在读代码时可能会看不懂。
第一步: 我们需要去boost官网下载boost库,这个库里面包含boost官网的所有文档的html文件。
第二步: 我们写一个解析程序从一个个html文件的源码中提取标题、内容和url,将他们保存到硬盘的一个data.txt文件中。
第三步: 读取data.txt文件,建立正排和倒排索引,提供索引的接口来获取正排和倒排数据
第四步: 写一个html页面,提供给用户一个搜索功能。
一次访问过程: 当用户通过浏览器向服务器发送搜索信息时,服务器会根据搜索的关键字获取对应倒排数据,然后通过倒排数据找到正排ID,从而找到正排的文档内容。然后构建出网页的标题,简述(内容的一部分),url,通过json字符串响应回去,然后在用户的浏览器显示出一个个网页信息。
宏观原理
五、正排索引 vs 倒排索引 - 搜索引擎具体原理
下面我们将一篇文章主要分为两部分其一是文档ID,其二是文档内容。比如说:
文档ID | 文档内容 |
---|---|
文档1 | 雷军买了四斤小米 |
文档2 | 雷军发布了小米手机 |
我们需要对目标文档进行分词(目的:方便建立倒排索引和查找):
- 文档1[雷军买了四斤小米 ]: 雷军/买/四斤/小米/四斤小米
- 文档2[雷军发布了小米手机]:雷军/发布/小米/小米手机
停止词:了,的,吗,a,the,一般我们在分词的时候可以不考虑
倒排索引:根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案
关键字(具有唯一性) | 文档ID, weight(权重) |
---|---|
雷军 | 文档1, 文档2 |
买 | 文档1 |
四斤 | 文档1 |
小米 | 文档1, 文档2 |
四斤小米 | 文档1 |
发布 | 文档2 |
小米手机 | 文档2 |
所谓倒排索引就是比如说你输入了小米两个字,它能通过小米这个关键字,根据这个关键字在所有文档中出现的权重,给你找出文档1和文档2来(实际上肯定不止只搜出来两条结果,会有很多)。
现在我们只是靠倒排索引获得了文档的ID,我们实际想要的还有文档的内容,这就需要正排索引来打配合。
正排索引:就是从文档ID找到文档内容(文档内的关键字)
文档ID | 文档内容 |
---|---|
文档1 | 雷军买了四斤小米 |
文档2 | 雷军发布了小米手机 |
在上面的倒排索引我们成功获得了文档1和文档2的ID,然后我们就可以靠ID用正排索引找到文档1和文档2的具体内容。
模拟一次查找的过程:
用户输入:小米 -> 倒排索引中查找 -> 提取出文档ID(1,2) -> 根据正排索引 -> 找到文档的内容 ->title+conent(desc)+url 文档结果进行摘要->构建响应结果
六、 编写数据去标签与数据清洗的模块 Parser
6.1 下载boost的文档库
//官网链接
https://www.boost.org/
打开官网后直接点击Download。
然后选择这个进行下载,这里我的boost版本是1_84_0,大家看我的文章时可能以及过了好久了,所以版本可能不太一样,反正认准后缀.tar.gz下载就行。
在Linux中我们找个地方先创建个文件夹
//在终端中输入,建立一个名叫boost_searcher的文件夹
mkdir boost_searcher
//输入ll观察是否存在文件夹boost_searcher
ll
//输入cd boost_searcher,进入到这个文件夹中
cd boost_searcher
进入到boost_searcher这个文件夹下我们输入以下代码
rz
//输入rz后点击回车
然后选择你刚才下载下的文件点打开,这样就把文件上传到你的服务器中去了。
然后我们输入ll命令就会出现一个文件
可以输入ll观看里面有啥
我们检索只需要文档中.html后缀的文档,所以我们要将.html后缀的放到一个input文件夹中
.html文件的路径是boost_1_84_0/doc/html
//输入命令
tar xzf boost_1_84_0.tar.gz
//如果版本不一样的话,你们要把指令中的1_84_0换成你们下载下的版本。
之后我们在该目录下在建立一个文件夹data,进入文件夹data再创建俩文件夹,一个叫input一个叫raw_html。input里放的是原始的html文档,raw_html里放的是去标签之后的干净文档
mkdir data //建立一个文件夹data
cd data //进入文件夹data
mkdir input //建立文件夹input
mkdir raw_html //建立文件夹raw_html
之后在和boost_1_84_0同级的目录下输入如下命令,将所有的.html文件放入到input文件夹中。
cp -rf boost_1_84_0/doc/html/* data/input/
我们可以利用这个指令看看有几个html文件,我这边显示有8586个。
ls -Rl | grep -E '*.html' | wc -l
6.2 去标签
我们在查找的时候这些标签是没有用的,为了提高效率我们需要写一个程序去掉这些标签,并把去除标签的文档放入raw_html中。
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"
"http://www.w3.org/TR/html4/loose.dtd">
<html> <!--这是一个标签-->
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Chapter 30. Boost.Process</title>
<link rel="stylesheet" href="../../doc/src/boostbook.css" type="text/css">
<meta name="generator" content="DocBook XSL Stylesheets V1.79.1">
<link rel="home" href="index.html" title="The Boost C++ Libraries BoostBook Documentation
Subset">
<link rel="up" href="libraries.html" title="Part I. The Boost C++ Libraries (BoostBook
Subset)">
<link rel="prev" href="poly_collection/acknowledgments.html" title="Acknowledgments">
<link rel="next" href="boost_process/concepts.html" title="Concepts">
</head>
<body bgcolor="white" text="black" link="#0000FF" vlink="#840084" alink="#0000FF">
<table cellpadding="2" width="100%"><tr>
<td valign="top"><img alt="Boost C++ Libraries" width="277" height="86"
src="../../boost.png"></td>
<td align="center"><a href="../../index.html">Home</a></td>
<td align="center"><a href="../../libs/libraries.htm">Libraries</a></td>
<td align="center"><a href="http://www.boost.org/users/people.html">People</a></td>
<td align="center"><a href="http://www.boost.org/users/faq.html">FAQ</a></td>
<td align="center"><a href="../../more/index.htm">More</a></td>
</tr></table>
.........
// <> : html的标签,这个标签对我们进行搜索是没有价值的,需要去掉这些标签,一般标签都是成对出现的!
目标: 把每个文档都去标签,然后写入到同一个文件中!每个文档内容不需要任何\n!文档和文档之间用 \3 区分
类似: aaaaaaaaa\3bbbbbbbbbbbbbb\3cccccccccccc\3
6.3 代码编写
在编写代码之前,我们需要用到boost库中的一些东西,所以需要先安装一下boost库。
# 枚举文件的时候我们需要用到boost库,下面是安装命令
sudo yum install -y boost-devel
首先在boost_1_84_0同级目录下建一个C++文件parser.cc顺便建立一个makefile
touch parser.cc
touch makefile
如图所示
下面是框架代码
#include <iostream>
#include <vector>
#include <string>
const char* src_path = "./boost_1_80_0"; // html文档的根目录
const char* dest_path = "./data.txt"; // 保存数据的文件路径
struct DocInfo
{
std::string title; // 标题
std::string conent; // 内容
std::string url; // 链接
};
void EnumFile(const std::string& src_path, std::vector<std::string>* files_path)
{}
void Parser(const std::vector<std::string>& files_path, std::vector<DocInfo>* doc_list)
{}
void Save(const std::vector<DocInfo>& doc_list, const std::string& dest_path)
{}
int main()
{
// 一. 枚举所有html文件
std::vector<std::string> files_path;
EnumFile(src_path, &files_path);
// 二. 读取文件,解析出标题、内容、url
std::vector<DocInfo> doc_list;
Parser(files_path, &doc_list);
// 三. 保存解析出来的信息
Save(doc_list, dest_path);
return 0;
}
- EnumFile函数用来遍历所有的html文件,将每个html文件的路径保存下来,后续操作可以直接更具这个路径找到该html文件;
- Parser函数根据html文件的路径,依次遍历每一个html文件,对每一个html文件进行分割,分割成标题,内容和url链接,保存到DocInfo中;
- void Save函数用来将Paser中保存的函数写入到文件中去,便于后续直接进行处理。标题、内容、url之间用
'\3'
分割,不同html的数据之间用\n
进行分割
下面是具体实现:
#include <iostream>
#include <vector>
#include <string>
#include <boost/filesystem.hpp>
#include "util.hpp"
const std::string src_path = "../boost_1_87_0/doc/html";
//const std::string src_path = "data/input/";
const std::string output = "data/raw_html/raw.txt";
typedef struct DocInfo
{
std::string title; // 文档的标题
std::string content; // 文档的内容
std::string url; // 该文档在官网中的url
} DocInfo_t;
// const &: 输入
//*: 输出
//&:输入输出
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list);
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results);
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output);
int main()
{
std::vector<std::string> files_list;
// 第一步: 递归式的把每个html文件名带路径,保存到files_list中,方便后期进行一个一个的文件进行读取
if (!EnumFile(src_path, &files_list))
{
std::cerr << "enum file name error" << std::endl;
return 1;
}
std::vector<DocInfo_t> results;
// 第二步: 按照files_list读取每个文件的内容,并进行解析
if (!ParseHtml(files_list, &results))
{
std::cerr << "parse html error" << std::endl;
return 2;
}
// 第三步: 把解析完毕的各个文件内容,写入到output,按照\3作为每个文档的分割符
if (!SaveHtml(results, output))
{
std::cerr << "save html error" << std::endl;
return 3;
}
return 0;
}
bool EnumFile(const std::string &src_path, std::vector<std::string> *files_list)
{
namespace fs = boost::filesystem;
fs::path root_path(src_path);
// 判断路径是否存在,不存在,就没有必要再往后走了
if (!fs::exists(root_path))
{
std::cerr << src_path << " not exists" << std::endl;
return false;
}
// 定义一个空的迭代器,用来进行判断递归结束
fs::recursive_directory_iterator end;
for (fs::recursive_directory_iterator iter(root_path); iter != end; iter++)
{
// 判断文件是否是普通文件,html都是普通文件
if (!fs::is_regular_file(*iter))
{
continue;
}
if (iter->path().extension() != ".html")
{ // 判断文件路径名的后缀是否符合要求
continue;
}
//std::cout << "debug: " << iter->path().string() << std::endl;
// 当前的路径一定是一个合法的,以.html结束的普通网页文件
files_list->push_back(iter->path().string()); // 将所有带路径的html保存在files_list,方便后续进行文本分析
}
return true;
}
static bool ParseTitle(const std::string &file, std::string *title)
{
std::size_t begin = file.find("<title>");
if (begin == std::string::npos)
{
std::cout<<"1:";
std::cout<<file<<std::endl;
return false;
}
std::size_t end = file.find("</title>");
if (end == std::string::npos)
{
std::cout<<"2"<<std::endl;
return false;
}
begin += std::string("<title>").size();
if (begin > end)
{
std::cout<<"3"<<std::endl;
return false;
}
*title = file.substr(begin, end - begin);
//std::cout<<"title:"<<*title<<std::endl;
return true;
}
static bool ParseContent(const std::string &file, std::string *content)
{
enum status{
LABEL,
CONTENT
};
enum status s = LABEL;
for(char c : file){
switch (s){
case LABEL:
if(c == '>') s = CONTENT;
break;
case CONTENT:
if(c == '<') s = LABEL;
else{
if(c=='\n') c = ' ';
content->push_back(c);
}
break;
default:
break;
}
}
//std::cout<<"content:"<<*content<<std::endl;
return true;
}
static bool ParseUrl(const std::string &file_path, std::string *url)
{
std::string url_head = "https://www.boost.org/doc/libs/1_87_0/doc/html";
std::string url_tail = file_path.substr(src_path.size());
*url = url_head + url_tail;
//std::cout<<"url:"<<*url<<std::endl;
return true;
}
//for debug
static void ShowDoc(const DocInfo_t &doc)
{
std::cout << "title:" << doc.title << std::endl;
std::cout << "content:" << doc.content << std::endl;
std::cout << "url:"<< doc.url << std::endl;
}
bool ParseHtml(const std::vector<std::string> &files_list, std::vector<DocInfo_t> *results)
{
for (const std::string &file : files_list)
{
std::string result;
// 1.读取文件
if (!ns_util::FileUtil::ReadFile(file, &result))
{
std::cerr<<"读取文件失败"<<std::endl;
continue;
}
//std::cout<<"result:"<<result<<std::endl;
//std::cout<<result<<std::endl;
DocInfo_t doc;
// 2.解析指定的文件提取title
if (!ParseTitle(result, &doc.title))
{
std::cerr<<"ParseTitle failed"<<std::endl;
continue;
}
if (!ParseContent(result, &doc.content))
{
std::cerr<<"ParseContent filed"<<std::endl;
continue;
}
if (!ParseUrl(file, &doc.url))
{
std::cerr<<"ParseUrl failed"<<std::endl;
continue;
}
//ShowDoc(doc);
results->push_back(std::move(doc));
//for debug
//break;
}
return true;
}
bool SaveHtml(const std::vector<DocInfo_t> &results, const std::string &output)
{
#define SEP '\3'
//按照二进制方式进行写入
std::ofstream out(output, std::ios::out | std::ios::binary);
if(!out.is_open()){
std::cerr << "open " << output << " failed!" << std::endl;
return false;
}
//就可以进行文件内容的写入了
for(auto &item : results){
std::string out_string;
out_string = item.title;
out_string += SEP;
out_string += item.content;
out_string += SEP;
out_string += item.url;
out_string += '\n';
out.write(out_string.c_str(), out_string.size());
}
out.close();
return true;
}
七、索引模块
7.1. 整体框架
- 首先索引模块整个程序中只需要一个实例,所以就设置成单例模式,构造函数私有,拷贝构造,复制重载全部delete掉。用GetInstance()来获取单例。
BuildForwardIndex
和BuildInvertedIndex
分别用来建立正排索引和倒排索引,最终在BuildIndex
中调用前面那俩函数,完成index地建立。GetInvertedList
用来获取倒排拉链,就是根据某一个关键词,来获取一个vector,vectir中保存的是所有含有该关键字的文章,里面存有对应文章id和权重。根据id去正排索引中找到对应文章进行处理。GetForwardIndex
就是根据id获取对应文章的。
#pragma once
#include <fstream>
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <mutex>
#include "util.hpp"
namespace ns_index
{
struct Doc_Info
{
std::string title; // 文档的标题
std::string content; // 文档对应的去标签之后的内容
std::string doc_url; // 官网文档url
uint64_t id; // 文档的ID,暂时先不做过多理解
};
struct InvertedElem
{
uint64_t doc_id;
std::string word;
int weight;
};
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
class Index
{
private:
// 正排索引的数据结构用数组,数组的下标天然是文档的ID
std::vector<Doc_Info> forward_list; // 正排索引
// 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string, InvertedList> inverted_index; // 倒排索引
private:
Index(){}
Index(const Index&)=delete;
Index& operator=(const Index&)=delete;
static Index* instance;
static std::mutex mtx;
public:
~Index(){}
public:
static Index* GetInstance()
{}
public:
// 根据doc_id找到找到文档内容
Doc_Info *GetForwardIndex(uint64_t doc_id)
{}
// 根据关键字string,获得倒排拉链
InvertedList *GetInvertedList(const std::string &word)
{}
// 根据去标签,格式化之后的文档,构建正排和倒排索引
// data/raw_html/raw.txt
bool BuildIndex(const std::string &input) // parse处理完毕的数据交给我
{}
private:
Doc_Info* BuildForwardIndex(const std::string &line)
{}
bool BuildInvertedIndex(const Doc_Info & doc)
{}
};
Index* Index::instance=nullptr;
std::mutex Index::mtx;
}
7.2 cppjieba安装
此处需要去gitee上下载个cppjieba库,下载完解压就好。
# cppjieba是一个用来分词的库
tar -zxf cppjieba.tgz # 解压命令
# 为了能够正常使用这个库我们还需要将deps目录下的limonp复制一份到include/cppjieba目录下
cp -rf deps/limonp/ include/cppjieba # 复制的命令,注意这条命令是在解压出来的cppjieba目录下执行的
然后需要建立个软链接
# 首先是在代码所在目录(我的是newboost)建立软链接,方便使用
ln -s tool/cppjieba/dict ./dict
ln -s tool/cppjieba/include/cppjieba/ ./cppjieba
测试代码:
#include "cppjieba/Jieba.hpp" // 我的cppjieba已经软链接到当前目录了
#include <iostream>
using namespace std;
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";
int main()
{
cppjieba::Jieba jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH);
vector<string> words; // 接收切分的词
string s;
s = "小明硕士毕业于中国科学院计算所";
cout << "原句:" << s << endl;
jieba.CutForSearch(s, words);
cout << "分词后:";
for (auto& word : words)
{
cout << word << " | ";
}
cout << endl;
}
7.3 index具体实现
#pragma once
#include <fstream>
#include <iostream>
#include <vector>
#include <unordered_map>
#include <string>
#include <mutex>
#include "util.hpp"
namespace ns_index
{
struct Doc_Info
{
std::string title; // 文档的标题
std::string content; // 文档对应的去标签之后的内容
std::string doc_url; // 官网文档url
uint64_t id; // 文档的ID,暂时先不做过多理解
};
struct InvertedElem
{
uint64_t doc_id;
std::string word;
int weight;
};
// 倒排拉链
typedef std::vector<InvertedElem> InvertedList;
class Index
{
private:
// 正排索引的数据结构用数组,数组的下标天然是文档的ID
std::vector<Doc_Info> forward_list; // 正排索引
// 倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系]
std::unordered_map<std::string, InvertedList> inverted_index; // 倒排索引
private:
Index(){}
Index(const Index&)=delete;
Index& operator=(const Index&)=delete;
static Index* instance;
static std::mutex mtx;
public:
~Index(){}
public:
static Index* GetInstance()
{
if(nullptr == instance){
mtx.lock();
if(nullptr == instance){
instance = new Index();
}
mtx.unlock();
}
std::cout<<"获取单例成功"<<std::endl;
return instance;
}
public:
// 根据doc_id找到找到文档内容
Doc_Info *GetForwardIndex(uint64_t doc_id)
{
if (doc_id >= forward_list.size())
{
std::cerr << "doc_id out range, error!" << std::endl;
return nullptr;
}
return &forward_list[doc_id];
}
// 根据关键字string,获得倒排拉链
InvertedList *GetInvertedList(const std::string &word)
{
auto iter = inverted_index.find(word);
if (iter == inverted_index.end())
{
std::cerr << word << " have no InvertedList" << std::endl;
return nullptr;
}
return &(iter->second);
}
// 根据去标签,格式化之后的文档,构建正排和倒排索引
// data/raw_html/raw.txt
bool BuildIndex(const std::string &input) // parse处理完毕的数据交给我
{
std::ifstream in(input, std::ios::in | std::ios::binary);
if(!in.is_open())
{
std::cerr << "sorry, " << input << " open error" << std::endl;
return false;
}
std::string line;
int cnt=0;
while(std::getline(in,line))
{
Doc_Info *doc=BuildForwardIndex(line);
if(doc==nullptr)
{
std::cerr << "build " << line << " error" << std::endl; //for deubg
continue;
}
BuildInvertedIndex(*doc);
++cnt;
if(cnt%50==0)
std::cout<<"索引构件完成:"<<cnt<<std::endl;
}
return true;
}
private:
Doc_Info* BuildForwardIndex(const std::string &line)
{
std::vector<std::string> result;
const std::string sep="\3";
ns_util::StringUtil::Split(line,&result,sep);
if(result.size()!=3)
{
return nullptr;
}
Doc_Info doc;
doc.title=result[0];
doc.content=result[1];
doc.doc_url=result[2];
doc.id=forward_list.size();
forward_list.push_back(std::move(doc));
return &forward_list.back();
}
bool BuildInvertedIndex(const Doc_Info & doc)
{
struct word_cnt
{
int title_cnt;
int content_cnt;
word_cnt():title_cnt(0),content_cnt(0) {}
};
std::unordered_map<std::string,word_cnt> word_map;
std::vector<std::string> title_words;
ns_util::JiebaUtil::Split(doc.title,&title_words);
for(std::string s : title_words)
{
boost::to_lower(s);
word_map[s].title_cnt++;
}
std::vector<std::string> content_words;
for(std::string s : content_words)
{
boost::to_lower(s);
word_map[s].content_cnt++;
}
#define X 10
#define Y 1
for(auto word_pair : word_map)
{
InvertedElem item;
item.doc_id=doc.id;
item.word=word_pair.first;
item.weight=X*word_pair.second.title_cnt+Y*word_pair.second.content_cnt;
InvertedList &inverted_list = inverted_index[word_pair.first];
inverted_list.push_back(std::move(item));
}
return true;
}
};
Index* Index::instance=nullptr;
std::mutex Index::mtx;
}
八、搜索模块
8.1 整体框架
- 私有成员变量有个
ns_index::Index *index
,用来获取倒排索引和正排索引,该变量在Searcher的InitSearcher
函数中被初始化,并调用index->BuildIndex(input)
先把索引建立好。 Search
函数用来对传入的字符串,进行处理查询,最终返回一个字符串。GetDesc
用来找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以)
#pragma once
#include "index.hpp"
#include <iostream>
#include "util.hpp"
#include <jsoncpp/json/json.h>
#include <algorithm>
namespace ns_searcher
{
struct InvertedElemPrint{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint() : doc_id(0), weight(0){}
};
class Searcher{
private:
ns_index::Index *index;
public:
Searcher() {}
~Searcher() {}
public:
void InitSearcher(const std::string &input)
{}
void Search(const std::string &query, std::string *json_string)
{}
std::string GetDesc(const std::string &html_content, const std::string &word)
{}
};
}
8.2 安装JsonCpp
安装命令:
# 安装命令
sudo yum install -y jsoncpp-devel
JsonCpp的简单实用:
#include <iostream>
#include <jsoncpp/json/json.h>
using namespace std;
int main()
{
Json::Value root;
root["name"] = "李华"; // 可以看成是一种key-value结构
root["ID"] = "0001";
Json::FastWriter write;
string json_str = write.write(root); // 将root对象转换成字符串
cout << json_str << endl;
}
8.3 searcher具体实现
- searcher函数详解:
- 先把传入的字符串用cppjieba切分了,把切分好的字符串存vector words中。
- 然后再依次遍历words中的字符串,根据每个字符,把对应的倒排拉链获取。
- 然后根据获取到的倒排拉链,通过id为索引,把权重都加起来,最终组织到一个map中去。
- 然后根据权重进行排序,权重高的放在前面
- 最后用Json进行序列化,返回序列化后的字符串
// 提供搜索,获取倒排和正排索引数据
bool Searcher(const std::string &input, std::string *json_str)
{
// 将输入的关键字进行分词
std::vector<std::string> words;
ns_index::jieba_util::CutString(input, &words);
// 获取倒排索引
std::vector<InvertedElems> inverted_list_all;
std::unordered_map<uint32_t, InvertedElems> tokens_map;
for (const auto &word : words)
{
std::cout << word << std::endl;
std::vector<ns_index::InvertedElem> *inverted_list = index->GetInvetedIndex(word);
if (nullptr == inverted_list)
{
return false;
}
for (const auto &elem : *inverted_list)
{
InvertedElems &item = tokens_map[elem.doc_id];
item.doc_id = elem.doc_id;
item.words.push_back(elem.word);
item.weight += elem.weight;
}
for (auto &pair : tokens_map)
{
inverted_list_all.push_back(std::move(pair.second));
}
}
// 排序
std::sort(inverted_list_all.begin(), inverted_list_all.end(), [](const InvertedElems &e1, const InvertedElems &e2)
{ return e1.weight > e2.weight; });
// 获取正排索引,将数据写入json字符串里面
Json::Value root;
for (const InvertedElems &elem : inverted_list_all)
{
bt_index::DocInfo *doc = index->GetForwardIndex(elem.doc_id);
Json::Value item;
item["title"] = doc->title;
item["desc"] = GetDescribe(elem.words[0], doc->content);
item["url"] = doc->url;
root.append(item);
}
Json::FastWriter write;
*json_str = write.write(root);
return true;
}
#pragma once
#include "index.hpp"
#include <iostream>
#include "util.hpp"
#include <jsoncpp/json/json.h>
#include <algorithm>
namespace ns_searcher
{
struct InvertedElemPrint
{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint() : doc_id(0), weight(0)
{
}
};
class Searcher
{
private:
ns_index::Index *index;
public:
Searcher() {}
~Searcher() {}
public:
void InitSearcher(const std::string &input)
{
index = ns_index::Index::GetInstance();
index->BuildIndex(input);
}
bool Searcher(const std::string &input, std::string *json_str)
{
// 将输入的关键字进行分词
std::vector<std::string> words;
ns_index::jieba_util::CutString(input, &words);
// 获取倒排索引
std::vector<InvertedElems> inverted_list_all;
std::unordered_map<uint32_t, InvertedElems> tokens_map;
for (const auto &word : words)
{
std::cout << word << std::endl;
std::vector<ns_index::InvertedElem> *inverted_list = index- >GetInvetedIndex(word);
if (nullptr == inverted_list)
{
return false;
}
for (const auto &elem : *inverted_list)
{
InvertedElems &item = tokens_map[elem.doc_id];
item.doc_id = elem.doc_id;
item.words.push_back(elem.word);
item.weight += elem.weight;
}
for (auto &pair : tokens_map)
{
inverted_list_all.push_back(std::move(pair.second));
}
}
// 排序
std::sort(inverted_list_all.begin(), inverted_list_all.end(), [](const InvertedElems &e1, const InvertedElems &e2)
{ return e1.weight > e2.weight; });
// 获取正排索引,将数据写入json字符串里面
Json::Value root;
for (const InvertedElems &elem : inverted_list_all)
{
bt_index::DocInfo *doc = index->GetForwardIndex(elem.doc_id);
Json::Value item;
item["title"] = doc->title;
item["desc"] = GetDescribe(elem.words[0], doc->content);
item["url"] = doc->url;
root.append(item);
}
Json::FastWriter write;
*json_str = write.write(root);
return true;
}
std::string GetDesc(const std::string &html_content, const std::string &word)
{
// 找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)
// 截取出这部分内容
const int prev_step = 50;
const int next_step = 100;
// 1. 找到首次出现
auto iter = std::search(html_content.begin(), html_content.end(), word.begin(), word.end(), [](int x, int y)
{ return (std::tolower(x) == std::tolower(y)); });
if (iter == html_content.end())
{
return "None1";
}
int pos = std::distance(html_content.begin(), iter);
// 2. 获取start,end , std::size_t 无符号整数
int start = 0;
int end = html_content.size() - 1;
// 如果之前有50+字符,就更新开始位置
if (pos > start + prev_step)
start = pos - prev_step;
if (pos < end - next_step)
end = pos + next_step;
// 3. 截取子串,return
if (start >= end)
return "None2";
std::string desc = html_content.substr(start, end - start);
desc += "...";
return desc;
}
};
}
九、 服务器模块
使用自己实现的仿照moduo实现的高并发服务器,作为网络通信模块。
仿muduo库的源码参照:https://gitee.com/qi-haozhe/reactor_server
超链接点击直达
#include "searcher.hpp"
#include "../ServerSource/Log.hpp"
#include "../ServerSource/HttpServer.hpp"
#include "Login.hpp"
#include "Register.hpp"
#define WWWROOT "./wwwroot/"
const std::string input = "./data/raw_html/raw.txt";
const string root_path = "./wwwroot";
int main()
{
ns_searcher::Searcher searcher;
searcher.InitSearcher(input);
HttpServer server(8081);
server.SetThreadCount(3);
server.SetBaseDir(WWWROOT); // 设置静态资源根目录,告诉服务器有静态资源请求到来,需要到哪里去找资源文件
server.Get("/s", [&searcher](const HttpRequest &req, HttpResponse *rsp)
{
if (!req.HasParam("word"))
{
(*rsp).SetContent("必须要有搜索关键字!", "text/plain; charset=utf-8");
return;
}
std::string word = req.GetParam("word");
lg(Info, "用户搜索的: %s", word.c_str());
std::string json_string;
searcher.Search(word, &json_string);
(*rsp).SetContent(json_string, "application/json"); });
server.Post("/login", handle_login);
server.Post("/register", handle_register);
server.Listen();
return 0;
}
十、 前端页面
由于不是重点就不过多描述了
直接上代码:
主页面搜索模块:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Boost搜索引擎</title>
<link rel="shortcut icon" href="./image/index.png" type="image/png" />
<style>
/* 基本样式重置 */
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
html,
body {
height: 100%;
font-family: Arial, sans-serif;
font-size: 16px;
line-height: 1.7;
color: #333;
background-color: #f7f7f7;
}
/* 页面布局 */
.container {
width: 100%;
max-width: 100%;
margin: 100px auto;
padding: 30px;
background-color: #fff;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* 搜索区域 */
.search {
display: flex;
align-items: center;
gap: 10px;
height: 52px;
padding: 0 10px;
background-color: #f0f0f0;
border-radius: 4px;
}
.search input[type="text"] {
flex-grow: 1;
height: 100%;
padding: 0 50px;
border: 1px solid #ccc;
border-right: none;
outline: none;
color: #666;
}
.search button {
width: 150px;
height: 100%;
border: none;
background-color: #4e6ef2;
color: #fff;
font-size: 18px;
font-weight: bold;
cursor: pointer;
transition: background-color 0.2s ease;
}
.search button:hover {
background-color: #3b59e9;
}
/* 搜索结果 */
.result {
margin-top: 20px;
padding: 0 10px;
}
.result .item {
margin-top: 15px;
}
.result .item a {
display: block;
text-decoration: none;
color: #4e6ef2;
font-size: 20px;
line-height: 1.3;
transition: color 0.2s ease;
}
.result .item a:hover {
color: #3b59e9;
}
.result .item p {
margin-top: ⅔px;
font-size: 16px;
line-height: 1.5;
}
.result .item i {
display: block;
font-style: normal;
color: #008000;
}
/* 热词统计区域样式 */
.hotwords {
width: 30%;
margin-left: auto;
/* 使其右对齐 */
padding: 10px;
background-color: #f0f0f0;
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
display: flex;
flex-direction: column;
align-items: flex-start;
}
.hotwords h3 {
margin-bottom: 10px;
font-size: 18px;
font-weight: bold;
}
.hotwords ul {
list-style-type: none;
margin: 0;
padding: 0;
}
.hotwords li {
margin-bottom: ½em;
font-size: 16px;
}
.hotwords span {
color: #4e6ef2;
}
</style>
<!-- ... 其他已存在的 head 内容 ... -->
<script>
document.addEventListener("DOMContentLoaded", function () {
const searchInput = document.querySelector('.search input[type="text"]');
const searchButton = document.querySelector('.search button');
searchInput.addEventListener('keyup', function (event) {
if (event.key === 'Enter') {
searchButton.click(); // 模拟点击搜索按钮
event.preventDefault(); // 阻止默认行为(如表单提交)
}
});
});
</script>
</head>
<body>
<h2 style="text-align: center;">基于One Thread One Loop式并发服务器实现Boost搜索引擎</h2>
<div class="container">
<div class="search">
<input type="text" placeholder="请输入搜索关键字">
<button onclick="Search()">搜索一下</button>
</div>
<div class="hotwords">
<!-- 热词统计内容将在此处动态填充 -->
</div>
<div class="result"></div>
</div>
<script src="http://code.jquery.com/jquery-2.1.1.min.js"></script>
<script>
document.addEventListener("DOMContentLoaded", function () {
checkCookieAndRedirect();
});
function checkCookieAndRedirect() {
const requiredCookieName = "SessionID"; // 假定需要检查的Cookie名称
// 检查特定Cookie是否存在
const cookieValue = getCookie(requiredCookieName);
if (cookieValue == '') {
alert('请先登录');
// 如果Cookie不存在,则重定向到登录页面
window.location.href = "/login.html"; // 替换为你的登录页面URL
}
}
// 获取Cookie值的辅助函数
function getCookie(name) {
const cookieArr = document.cookie.split(";");
for (let i = 0; i < cookieArr.length; i++) {
let cookiePair = cookieArr[i].split("=");
/* Removing whitespace at the beginning of the cookie name
and compare it with the given string */
if (name == cookiePair[0].trim()) {
// Decode the cookie value and return
return decodeURIComponent(cookiePair[1]);
}
}
// Return null if the cookie wasn't found
return "";
}
function Search() {
const query = $(".container .search input").val();
$.ajax({
type: "GET",
url: "/s?word=" + query,
success: data => BuildHtml(data),
});
}
function BuildHtml(data) {
const resultContainer = $(".container .result");
resultContainer.empty();
data.forEach((elem) => {
const item = `
<div class="item">
<a href="${elem.url}" target="_blank">${elem.title}</a>
<p>${elem.desc}</p>
<i>${elem.url}</i>
</div>
`;
resultContainer.append(item);
});
}
function renderHotWords(jsonData) {
const hotWordsContainer = $(".container .hotwords");
const hotWordsList = jsonData.hotWords.map(wordData => {
return `<li><span>${wordData.word}</span>: ${wordData.count}</li>`;
}).join("");
hotWordsContainer.html(`
<h3>热词统计</h3>
<ul>${hotWordsList}</ul>
`);
}
// 假设您通过Ajax或其他方式从服务器获取热词统计JSON数据
$.ajax({
type: "GET",
url: "/hotwords", // 替换为实际获取热词统计数据的API URL
success: data => renderHotWords(data),
});
</script>
</body>
</html>
登录模块:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户登录</title>
<link rel="shortcut icon" href="./image/login.png" type="image/png" />
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f1f1f1;
}
.login-container {
width: 300px;
padding: 2em;
border-radius: 6px;
box-shadow: 0 2px 9px rgba(0, 0, 0, 0.1);
background-color: white;
}
h1 {
text-align: center;
margin-bottom: 1em;
}
form {
display: flex;
flex-direction: column;
gap: 1em;
}
label {
font-weight: bold;
}
input[type="text"],
input[type="password"] {
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
input:invalid {
outline: none;
box-shadow: 0 0 ¼em red;
}
button[type="submit"] {
cursor: pointer;
padding: 0.75em 1em;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
text-transform: uppercase;
transition: background-color 0.2s ease-in-out;
}
button[type="submit"]:hover {
background-color: #3e8e41;
}
</style>
</head>
<body>
<div class="login-container">
<h1>用户登录</h1>
<form id="login-form" action="/login" method="post"> <label for="username">用户名:</label> <input type="text"
id="username" name="username" pattern="^[a-zA-Z0-9._-]{3,}$" required
title="请输入至少3个字符,只允许字母、数字、点、下划线和破折号">
<label for="password">密码:</label>
<input type="password" id="password" name="password" minlength="6" required title="请输入至少6个字符">
<div id="error-message" class="error" style="display: none;">
<p>登录失败,请检查用户名和密码是否正确。</p>
</div>
<button type="submit">登录</button>
<button id="register-button" type="button" class="login-submit">注册</button>
</form>
</div>
<script>
const form = document.querySelector('#login-form');
const errorMessage = document.getElementById('error-message');
form.addEventListener('submit', async (event) => {
event.preventDefault();
// 检查是否有输入错误
if (!form.checkValidity()) {
form.reportValidity();
return;
}
// 清除之前的错误消息
errorMessage.style.display = 'none';
try {
// 创建JSON对象,将表单数据转换为JSON格式
const data = {
username: form.elements.username.value,
password: form.elements.password.value
};
const response = await fetch(form.action, {
method: form.method,
headers: {
'Content-Type': 'application/json' // 设置正确的Content-Type头
},
body: JSON.stringify(data) // 将JSON对象序列化为字符串作为请求主体
});
if (!response.ok) {
throw new Error('登录失败');
}
// 通常情况下,登录成功后服务器会返回一些有用的数据(如用户信息、JWT令牌等)
// 根据您的后端接口文档,解析并使用这些数据
const responseData = await response.json();
// 登录成功,执行后续操作(如跳转到主页、存储用户信息等)
alert('登录成功');
form.reset();
// 跳转到本地的index.html页面
window.location.assign('./index.html');
} catch (error) {
alert(error);
errorMessage.style.display = 'block';
console.error('Login error:', error);
}
});
// 为注册按钮添加点击事件监听器
const registerButton = document.getElementById('register-button');
registerButton.addEventListener('click', async () => {
try {
// 跳转到本地的register.html页面
window.location.assign('./register.html');
} catch (error) {
console.error('Register error:', error);
// 可以在此处添加错误提示或处理逻辑
}
});
</script>
</body>
</html>
注册模块:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>用户注册</title>
<link rel="shortcut icon" href="./image/register.png" type="image/png" />
<style>
body {
font-family: Arial, sans-serif;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
margin: 0;
background-color: #f1f1f1;
}
.login-container {
width: 300px;
padding: 2em;
border-radius: 6px;
box-shadow: 0 2px 9px rgba(0, 0, 0, 0.1);
background-color: white;
}
h1 {
text-align: center;
margin-bottom: 1em;
}
form {
display: flex;
flex-direction: column;
gap: 1em;
}
label {
font-weight: bold;
}
input[type="text"],
input[type="password"] {
padding: 0.5em;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 1rem;
}
input:invalid {
outline: none;
box-shadow: 0 0 ¼em red;
}
button[type="submit"] {
cursor: pointer;
padding: 0.75em 1em;
background-color: #4CAF50;
color: white;
border: none;
border-radius: 4px;
font-size: 1rem;
text-transform: uppercase;
transition: background-color 0.2s ease-in-out;
}
button[type="submit"]:hover {
background-color: #3e8e41;
}
</style>
</head>
<body>
<div class="login-container">
<h1>用户注册</h1>
<form id="login-form" action="/register" method="post">
<label for="username">用户名:</label>
<input type="text" id="username" name="username" pattern="^[a-zA-Z0-9._-]{3,}$" required
title="请输入至少3个字符,只允许字母、数字、点、下划线和破折号">
<label for="password">密码:</label>
<input type="password" id="password" name="password" minlength="6" required title="请输入至少6个字符">
<div id="error-message" class="error" style="display: none;">
<p>注册失败,请检查用户名和密码是否正确。</p>
</div>
<button type="submit">注册</button>
<button id="register-button" type="button" class="login-submit">返回登录</button>
</form>
</div>
<script>
const form = document.querySelector('#login-form');
const errorMessage = document.getElementById('error-message');
form.addEventListener('submit', async (event) => {
event.preventDefault();
// 检查是否有输入错误
if (!form.checkValidity()) {
form.reportValidity();
return;
}
// 清除之前的错误消息
errorMessage.style.display = 'none';
try {
// 创建JSON对象,将表单数据转换为JSON格式
const data = {
username: form.elements.username.value,
password: form.elements.password.value
};
const response = await fetch(form.action, {
method: form.method,
headers: {
'Content-Type': 'application/json' // 设置正确的Content-Type头
},
body: JSON.stringify(data) // 将JSON对象序列化为字符串作为请求主体
});
if (!response.ok) {
throw new Error('注册失败');
}
const responseData = await response.json();
// 登录成功,执行后续操作(如跳转到主页、存储用户信息等)
alert('注册成功');
form.reset();
// 跳转到本地的index.html页面
window.location.assign('./login.html');
} catch (error) {
errorMessage.style.display = 'block';
console.error('Login error:', error);
}
});
// 为注册按钮添加点击事件监听器
const registerButton = document.getElementById('register-button');
registerButton.addEventListener('click', async () => {
try {
// 跳转到本地的register.html页面
window.location.assign('./login.html');
} catch (error) {
console.error('Register error:', error);
// 可以在此处添加错误提示或处理逻辑
}
});
</script>
</body>
</html>
十一、竞价模块
竞价模块
在实际的搜索中,可以添加一个竞价模块,把广告的信息存储在一个文件当中,当用户进行访问的时候,会根据广告厂商提供的资金作为判别标准,分配一个比较高的权重,这样就可以把信息优先显示
而具体的实施环节,当把分词结果要构建json串的时候,直接把广告的信息加到json串中,这样就能直接显示在页面上了
void Read_AD_Messages(std::vector<std::string> *Addwords)
{
// 读取竞价配置文件,把网站标题和内容放到数组中
std::ifstream inputFile("./data/Ad/Ad.txt");
if (!inputFile)
{
std::cerr << "Error: Unable to open file.txt" << std::endl;
return;
}
// 把读取到的信息放入到结构体中
std::string line;
while (getline(inputFile, line))
{
std::string title, content, url;
int weight;
std::stringstream iss(line);
iss >> title >> content >> url >> weight;
Addwords->push_back(title);
Addwords->push_back(content);
}
inputFile.close();
}
// 把竞价的关键字加入到分词关键字当中
void AddAdvertise(std::vector<std::string> *ResAddWords)
{
std::vector<std::string> Addwords;
std::vector<std::string> CutAddWords;
Read_AD_Messages(ResAddWords);
}
// 加信息
void InsertAddContent(std::vector<InvertedElemPrint> *inverted_list_all)
{
std::ifstream inputFile("./data/Ad/Ad.txt");
if (!inputFile)
{
std::cerr << "Error: Unable to open file.txt" << std::endl;
return;
}
// 把读取到的信息放入到结构体中
std::string line;
int id = 0;
while (getline(inputFile, line))
{
int weight = 0;
std::string title, content, url;
std::stringstream iss(line);
iss >> title >> content >> url >> weight;
title += "(广告)";
inverted_list_all->push_back(InvertedElemPrint(id++, weight, title));
}
}
十二、热词统计模块
这个模块主要是把用户提交过的数据缓存起来,再单独开一个新的界面进行展示,就是把出现的数据降序排列,返回一个json串即可
// 把热词统计的数据返回
void HotWords(std::string *json_string)
{
sw::redis::Redis redis("tcp://127.0.0.1:6379");
std::vector<std::pair<std::string, double> > hotWordsVector;
auto it = back_inserter(hotWordsVector);
redis.zrange("users:hotwords", 0, -1, it);
Json::Value root;
Json::Value hotWordsArray;
// 将 hotwords_map 转换为 vector 并按词频降序排序
std::sort(hotWordsVector.begin(), hotWordsVector.end(), [](const std::pair<std::string, double> &p1, const std::pair<std::string, double> &p2)
{
return p1.second > p2.second;
});
for (const auto &entry : hotWordsVector)
{
Json::Value hotWordObject;
hotWordObject["word"] = entry.first;
hotWordObject["count"] = entry.second;
hotWordsArray.append(hotWordObject);
}
root["hotWords"] = hotWordsArray;
Json::StreamWriterBuilder builder;
builder["indentation"] = " ";
std::unique_ptr<Json::StreamWriter> writer(builder.newStreamWriter());
std::stringstream json_stream;
writer->write(root, &json_stream);
*json_string = json_stream.str();
}
// 把当前传入进来的关键字放到Redis当中
void AddToRedis(const std::string &query)
{
sw::redis::Redis redis("tcp://127.0.0.1:6379");
redis.zincrby("users:hotwords", 1, query);
}
十三、 注册登录模块
登录模块:
#pragma once
#include <cstring>
#include <./mysql_include/mysql.h>
#include <string>
#include <stdexcept>
#include <nlohmann/json.hpp>
#include <sw/redis++/redis++.h>
#include "../ServerSource/Http.hpp"
using namespace std;
using json = nlohmann::json;
// 建立数据库连接
MYSQL *establish_db_connection()
{
MYSQL *conn = mysql_init(NULL);
if (conn == NULL)
{
fprintf(stderr, "mysql_init() failed\n");
return NULL;
}
const char *host = "127.0.0.1"; // 本地主机名
const char *user = "root"; // 数据库用户名
const char *pass = "123456"; // 数据库用户密码
const char *db = "boost_search"; // 数据库名
if (mysql_real_connect(conn, host, user, pass, db, 0, NULL, 0) == NULL)
{
fprintf(stderr, "mysql_real_connect() failed: %s\n", mysql_error(conn));
mysql_close(conn);
return NULL;
}
return conn;
}
bool FindInRedis(const char* username, const char* password)
{
sw::redis::Redis redis("tcp://127.0.0.1:6379");
auto result = redis.hget("user:username:password", username);
if(result)
return result.value() == password;
return false;
}
void AddToRedis_login(const string& username, const string& password)
{
sw::redis::Redis redis("tcp://127.0.0.1:6379");
redis.hset("user:username:password", username, password);
}
// 验证用户名和密码是否正确
bool authenticate(const char *username, const char *password, string& sessionid)
{
string str1 = username;
string str2 = password;
// 先到Redis里面找,如果没有再到MySQL中找
if(FindInRedis(username, password))
return true;
MYSQL *conn = establish_db_connection();
if (conn == NULL)
{
return false; // 连接失败,返回false
}
// 构造SQL查询语句
std::string query = "SELECT COUNT(*) FROM users WHERE username = '" + std::string(username) + "' AND password = '" + std::string(password) + "';";
// SELECT COUNT(*) FROM users WHERE username = 'testonline' AND password = 'testonline';
if (mysql_query(conn, query.c_str()))
{
fprintf(stderr, "MySQL query failed: %s\n", mysql_error(conn));
mysql_close(conn);
return false;
}
MYSQL_RES *result = mysql_store_result(conn);
if (result == NULL)
{
fprintf(stderr, "Failed to get query result: %s\n", mysql_error(conn));
mysql_close(conn);
return false;
}
MYSQL_ROW row = mysql_fetch_row(result);
int count = atoi(row[0]);
mysql_free_result(result);
mysql_close(conn);
// 如果查询结果为非零值,表示找到了匹配的用户名和密码记录,返回true;否则返回false
if(count > 0)
{
AddToRedis_login(username, password);
sessionid = str1 + str2;
return true;
}
return false;
}
void handle_login(const HttpRequest &request, HttpResponse *response)
{
// 检查请求方法是否为POST
if (request._method != "POST")
{
response->_statu = 405; // Method Not Allowed
response->_body = "Only POST requests are allowed for login.";
return;
}
// 检查Content-Type是否为application/json
string contentType = request.GetHeader("Content-Type");
if (contentType.find("application/json") == string::npos)
{
response->_statu = 415; // Unsupported Media Type
response->_body = "Login request must have a Content-Type of application/json.";
return;
}
// 获取请求正文长度
size_t contentLength = request.ContentLength();
if (contentLength == 0)
{
response->_statu = 400; // Bad Request
response->_body = "Login request must have a non-empty JSON body.";
return;
}
// 读取请求正文
string requestBody(request._body, 0, contentLength);
// 使用nlohmann/json库解析请求正文
json j;
try
{
j = json::parse(requestBody);
}
catch (const json::parse_error &e)
{
response->_statu = 400; // Bad Request
response->_body = "Invalid JSON in login request.";
return;
}
// 提取账号和密码
string username = j.value("username", "");
string password = j.value("password", "");
string sessionid = "";
// 实现账号和密码验证逻辑
bool isValid = authenticate(username.c_str(), password.c_str(), sessionid);
if (isValid)
{
// 账号和密码验证成功
response->_statu = 200; // OK
response->SetHeader("Content-Type", "application/json");
response->SetHeader("Set-Cookie", "SessionID=" + username + password);
response->_body = "{\"message\":\"Login successful\"}";
}
else
{
// 账号和密码验证失败
response->_statu = 401; // Unauthorized
response->SetHeader("Content-Type", "application/json");
response->_body = "{\"message\":\"Invalid username or password\"}";
}
}
注册模块:
#pragma once
#include <cstring>
//#include <mysql.h>
#include <./mysql_include/mysql.h>
#include <string>
#include <stdexcept>
//#include <jsoncpp/json/json.h>
#include <nlohmann/json.hpp>
#include <sw/redis++/redis++.h>
#include "../ServerSource/Http.hpp"
using namespace std;
using json = nlohmann::json;
// 建立数据库连接
MYSQL *establish_db_connection_()
{
MYSQL *conn = mysql_init(NULL);
if (conn == NULL)
{
fprintf(stderr, "mysql_init() failed\n");
return NULL;
}
const char *host = "127.0.0.1"; // 本地主机名
const char *user = "root"; // 数据库用户名
const char *pass = "123456"; // 数据库用户密码
const char *db = "boost_search"; // 数据库名
if (mysql_real_connect(conn, host, user, pass, db, 0, NULL, 0) == NULL)
{
fprintf(stderr, "mysql_real_connect() failed: %s\n", mysql_error(conn));
mysql_close(conn);
return NULL;
}
return conn;
}
// 尝试插入用户信息,成功返回 true,失败返回 false
bool RegisterInfo(const char *username, const char *password)
{
MYSQL *conn = establish_db_connection_();
if (conn == NULL)
{
return false; // 连接失败,返回false
}
// 构造SQL插入语句
std::string query = "INSERT INTO users (username, password) VALUES ('" + std::string(username) + "', '" + std::string(password) + "')";
if (mysql_query(conn, query.c_str()))
{
fprintf(stderr, "MySQL insert failed: %s\n", mysql_error(conn));
mysql_close(conn);
return false;
}
// 插入成功,关闭连接并返回true
mysql_close(conn);
return true;
}
void AddToRedis(const string &username, const string &password)
{
sw::redis::Redis redis("tcp://127.0.0.1:6379");
redis.hset("user:username:password", username, password);
}
void handle_register(const HttpRequest &request, HttpResponse *response)
{
// 检查请求方法是否为POST
if (request._method != "POST")
{
response->_statu = 405; // Method Not Allowed
response->_body = "Only POST requests are allowed for login.";
return;
}
// 检查Content-Type是否为application/json
string contentType = request.GetHeader("Content-Type");
if (contentType.find("application/json") == string::npos)
{
response->_statu = 415; // Unsupported Media Type
response->_body = "Login request must have a Content-Type of application/json.";
return;
}
// 获取请求正文长度
size_t contentLength = request.ContentLength();
if (contentLength == 0)
{
response->_statu = 400; // Bad Request
response->_body = "Login request must have a non-empty JSON body.";
return;
}
// 读取请求正文
string requestBody(request._body, 0, contentLength);
// 使用nlohmann/json库解析请求正文
json j;
try
{
j = json::parse(requestBody);
}
catch (const json::parse_error &e)
{
response->_statu = 400; // Bad Request
response->_body = "Invalid JSON in login request.";
return;
}
// 提取账号和密码
string username = j.value("username", "");
string password = j.value("password", "");
// 实现账号和密码验证逻辑
bool isValid = RegisterInfo(username.c_str(), password.c_str());
// 把信息在Redis中缓存一份
AddToRedis(username, password);
if (isValid)
{
// 账号和密码验证成功
response->_statu = 200; // OK
response->SetHeader("Content-Type", "application/json");
response->_body = "{\"message\":\"Login successful\"}";
}
else
{
// 账号和密码验证失败
response->_statu = 401; // Unauthorized
response->SetHeader("Content-Type", "application/json");
response->_body = "{\"message\":\"Invalid username or password\"}";
}
}
最终效果展示:
服务器运行:
主界面:
注册界面:
主界面:
搜索界面:
后端显示处理: