从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);// 二. 读取文件,解析出标题、内容、urlstd::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.解析指定的文件提取titleif (!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; // 官网文档urluint64_t id; // 文档的ID,暂时先不做过多理解};struct InvertedElem{uint64_t doc_id;std::string word;int weight;};// 倒排拉链typedef std::vector<InvertedElem> InvertedList;class Index{private:// 正排索引的数据结构用数组,数组的下标天然是文档的IDstd::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.txtbool 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; // 官网文档urluint64_t id; // 文档的ID,暂时先不做过多理解};struct InvertedElem{uint64_t doc_id;std::string word;int weight;};// 倒排拉链typedef std::vector<InvertedElem> InvertedList;class Index{private:// 正排索引的数据结构用数组,数组的下标天然是文档的IDstd::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.txtbool 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 deubgcontinue;}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 1for(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. 截取子串,returnif (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 nameand compare it with the given string */if (name == cookiePair[0].trim()) {// Decode the cookie value and returnreturn decodeURIComponent(cookiePair[1]);}}// Return null if the cookie wasn't foundreturn "";}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 URLsuccess: 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,}$" requiredtitle="请输入至少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,}$" requiredtitle="请输入至少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;否则返回falseif(count > 0){AddToRedis_login(username, password);sessionid = str1 + str2;return true;}return false;
}void handle_login(const HttpRequest &request, HttpResponse *response)
{// 检查请求方法是否为POSTif (request._method != "POST"){response->_statu = 405; // Method Not Allowedresponse->_body = "Only POST requests are allowed for login.";return;}// 检查Content-Type是否为application/jsonstring contentType = request.GetHeader("Content-Type");if (contentType.find("application/json") == string::npos){response->_statu = 415; // Unsupported Media Typeresponse->_body = "Login request must have a Content-Type of application/json.";return;}// 获取请求正文长度size_t contentLength = request.ContentLength();if (contentLength == 0){response->_statu = 400; // Bad Requestresponse->_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 Requestresponse->_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; // OKresponse->SetHeader("Content-Type", "application/json");response->SetHeader("Set-Cookie", "SessionID=" + username + password);response->_body = "{\"message\":\"Login successful\"}";}else{// 账号和密码验证失败response->_statu = 401; // Unauthorizedresponse->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;}// 插入成功,关闭连接并返回truemysql_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)
{// 检查请求方法是否为POSTif (request._method != "POST"){response->_statu = 405; // Method Not Allowedresponse->_body = "Only POST requests are allowed for login.";return;}// 检查Content-Type是否为application/jsonstring contentType = request.GetHeader("Content-Type");if (contentType.find("application/json") == string::npos){response->_statu = 415; // Unsupported Media Typeresponse->_body = "Login request must have a Content-Type of application/json.";return;}// 获取请求正文长度size_t contentLength = request.ContentLength();if (contentLength == 0){response->_statu = 400; // Bad Requestresponse->_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 Requestresponse->_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; // OKresponse->SetHeader("Content-Type", "application/json");response->_body = "{\"message\":\"Login successful\"}";}else{// 账号和密码验证失败response->_statu = 401; // Unauthorizedresponse->SetHeader("Content-Type", "application/json");response->_body = "{\"message\":\"Invalid username or password\"}";}
}
最终效果展示:
服务器运行:
主界面:
注册界面:
主界面:
搜索界面:
后端显示处理:
相关文章:

从0开始完成基于异步服务器的boost搜索引擎
文章目录 前言一、本项目涉及的技术栈和环境二、boost是什么?三、项目的相关背景四、项目的相关原理五、正排索引 vs 倒排索引 - 搜索引擎具体原理六、 编写数据去标签与数据清洗的模块 Parser6.1 下载boost的文档库6.2 去标签6.3 代码编写 七、索引模块7.1. 整体框…...

炒菜本质是 “能量(火候)与食材特性”的动态平衡
炒菜看似简单,但想要做到色香味俱全,需遵循一套清晰的逻辑链条。以下从底层逻辑到实操步骤拆解: 一、核心逻辑:控制变量,精准匹配 炒菜本质是 “能量(火候)与食材特性”的动态平衡,…...

AI Copilot——维新派的贾维斯,守旧派的墓志铭(程序员视角)
6500万年前的那颗陨石好像要落下来了 这一段时间,伴随着claude sonnet 3.7的发布 以及cursor,windsurf 等一众AI智能编辑器的涌现,社区的programming自媒体坐不住了,有一个观点已经快要溢出屏幕:程序员这个岗位要黄&a…...

Java Spring MVC (2)
常见的Request Controller 和 Response Controller 的区别 用餐厅点餐来理解 想象你去一家餐厅吃饭: Request Controller(接单员):负责处理你的点餐请求,记录你的口味、桌号等信息。Response Controller(…...
对开源VLA sota π0的微调——如何基于各种开源数据集、以及你自己的私有数据集微调π0(含我司的微调实践)
前言 25年2.4日,几个月前推出π0的公司Physical Intelligence (π)宣布正式开源π0及π0-FAST,如之前所介绍的,他们对用超过 10,000 小时的机器人数据进行了预训练 该GitHub代码仓库「 π0及π0-FAST的GitHub地址:github.com/Ph…...

[排序算法]直接插入排序
1.基本思想 直接插入排序是一种简单的插入排序法,其基本思想是:把待排序的记录按其关键码值的大小逐个插入到一个已经排好序的有序序列中,直到所有的记录插入完为止,得到一个新的有序序列。 实际中我们玩扑克牌时,就用…...

四、云原生应用监控-Etcd
Etcd 是 Kubernetes 内部核心组件之一,作为分布式键值存储,天然支持 Prometheus 监控,自带 /metrics 端点,可直接被 Prometheus 抓取。 Etcd监控需要使用到证书。 一、检查云原生Etcd 检查节点上的Etcd [root@k8s-master01 manifests]#netstat -lnpt |grep etcd tcp …...

STM32-I2C通信外设
目录 一:I2C外设简介 二:I2C外设数据收发 三:I2C的复用端口 四:主机发送和接收 五:硬件I2C读写MPU6050 相关函数: 1.I2C_ GenerateSTART 2.I2C_ GenerateSTOP 3.I2C_ AcknowledgeConfig 4.I2C…...

CTA策略【量化理论】
CTA策略演变史 全称:Commodity Trading Advisor (商品交易顾问) CTA最开始是指通过为客户提供期权、期货方面的交易建议,或者直接通过受管理的期货账户参与实际交易,来获得收益的机构或个人。 随着市场的发展&#…...

基于AMD AU15P FPGA的SLVS-EC桥PCIe设计方案分享
作者:Hello,Panda 各位FPGAer周末愉快,今天熊猫君分享一个基于AMD AU15P FPGA的SLVS-EC桥PCIe设计方案。 一、方案背景 先说方案的应用背景:众所周知,较为上层的如基于AI的机器视觉应用,大多基于高端的专用SoC、AI专…...

②Modbus TCP转Modbus RTU/ASCII网关同步采集无需编程高速轻松组网
Modbus TCP转Modbus RTU/ASCII网关同步采集无需编程高速轻松组网https://item.taobao.com/item.htm?ftt&id784749793551 网关 MS-A1-5081 MS-A1-5081 网关通过 MODBUS TCP 协议与 Modbus RTU/ASCII 协议的相互转换,可以将 Modbus 串口设备接入 MODBUS TCP 网络…...

游戏引擎学习第145天
仓库:https://gitee.com/mrxiao_com/2d_game_3 今天的计划 目前,我们正在完成遗留的工作。当时我们已经将声音混合器(sound mixer)集成到了 SIMD 中,但由于一个小插曲,没有及时完成循环内部的部分。这个小插曲主要是…...

【Kotlin】Kotlin基础笔记
一、数据类型 1.1 变量声明与类型推导 变量声明 使用 val 声明不可变变量(相当于常量);使用 var 声明可变变量。 val a 10 // 类型自动推断为 Int,不可变 var b: Double 5.0 // 显示声明为 Double,可变变量…...

Jump( 2015-2016 ACM-ICPC Northeastern European Regional Contest (NEERC 15). )
Jump( 2015-2016 ACM-ICPC Northeastern European Regional Contest (NEERC 15). ) 题目大意: 在这个交互式问题中,你需要通过查询系统,逐步找出隐藏的位字符串 S。给定一个偶数 n,表示目标位字符串 S 的长度,你需要通…...

uniapp uniCloud引发的血案(switchTab: Missing required args: “url“)!!!!!!!!!!
此文章懒得排版了,为了找出这个bug, 星期六的晚上我从9点查到0点多,此时我心中一万个草泥马在崩腾,超级想骂人!!!!!!!!! uniCloud 不想…...

【Linux】冯诺依曼体系与操作系统理解
🌟🌟作者主页:ephemerals__ 🌟🌟所属专栏:Linux 目录 前言 一、冯诺依曼体系结构 二、操作系统 1. 操作系统的概念 2. 操作系统存在的意义 3. 操作系统的管理方式 4. 补充:理解系统调用…...

STM32之软件SPI
SPI传输更快,最大可达80MHz,而I2C最大只有3.4MHz。输入输出是分开的,可以同时输出输入。是同步全双工。仅支持一主多从。SS是从机选择线。每个从机一根。SPI无应答机制的设计。 注意:所有设备需要共地,时钟线主机输出&…...

Python零基础学习第三天:函数与数据结构
一、函数基础 函数是什么? 想象你每天都要重复做同一件事,比如泡咖啡。函数就像你写好的泡咖啡步骤说明书,每次需要时直接按步骤执行,不用重新想流程。 # 定义泡咖啡的函数 def make_coffee(sugar1): # 默认加1勺糖 print("…...

启动wsl里的Ubuntu24报错:当前计算机配置不支持 WSL2,HCS_E_HYPERV_NOT_INSTALLED
问题:启动wsl里的Ubuntu24报错 报错信息: 当前计算机配置不支持 WSL2。 请启用“虚拟机平台”可选组件,并确保在 BIOS 中启用虚拟化。 通过运行以下命令启用“虚拟机平台”: wsl.exe --install --no-distribution 有关信息,请访…...

顶点着色器和片段着色器
在Unity渲染中,**顶点着色器(Vertex Shader)和片段着色器(Fragment Shader)**是图形渲染管线中的两个核心阶段。我们可以通过一个比喻来理解它们的分工:想象你要画一幅由三角形组成的3D模型,顶点…...

std::optional详解
基础介绍 c17版本引入了std::optional特性,这一个类模板,基本的使用方法如下: std::optional<T> 这个新特性的含义是利用std::optional<T>创建的某个类型的对象,这个对象存储某个类型的值,这个值可能存在…...

Web三件套学习笔记
<!-- HTML --> HTML是超文本标记语言 1、html常用标签 块级标签 独占一行 可以设置宽度,高度,margin,padding 宽度默认所在容器的宽度 标签作用table定义表格h1 ~ h6定义标题hr定义一条水平线p定义段落li标签定义列表项目ul定义无序列表ol定…...

Scala 中trait的线性化规则(Linearization Rule)和 super 的调用行为
在 Scala 中,特质(Trait)是一种强大的工具,用于实现代码的复用和组合。当一个类混入(with)多个特质时,可能会出现方法冲突的情况。为了解决这种冲突,Scala 引入了最右优先原则&#…...

C++入门——引用
C入门——引用 一、引用的概念 引用不是新定义一个变量,而是给已存在变量取了一个别名,编译器不会为引用变量开辟内存空间,它和它引用的变量共用同一块内存空间。这就好比《水浒传》中,一百零八位好汉都有自己的绰号。通过&…...

深度学习模型组件之优化器—Lookahead:通过“快慢”两组优化器协同工作,提升训练稳定性
深度学习模型组件之优化器—Lookahead:通过“快/慢”两组优化器协同工作,提升训练稳定性 文章目录 深度学习模型组件之优化器—Lookahead:通过“快/慢”两组优化器协同工作,提升训练稳定性1. Lookahead优化器的背景2. Lookahead优…...

K8s 1.27.1 实战系列(五)Namespace
Kubernetes 1.27.1 中的 Namespace(命名空间)是集群中实现多租户资源隔离的核心机制。以下从功能、操作、配置及实践角度进行详细解析: 一、核心功能与特性 1、资源隔离 Namespace 将集群资源划分为逻辑组,实现 Pod、Service、Deployment 等资源的虚拟隔离。例如,…...

Spring Boot整合ArangoDB教程
精心整理了最新的面试资料和简历模板,有需要的可以自行获取 点击前往百度网盘获取 点击前往夸克网盘获取 一、环境准备 JDK 17Maven 3.8Spring Boot 3.2ArangoDB 3.11(本地安装或Docker运行) Docker启动ArangoDB docker run -d --name ar…...

虚幻基础:动画层接口
文章目录 动画层:动画图表中的函数接口:名字,没有实现。动画层接口:由动画蓝图实现1.动画层可直接调用实现功能2.动画层接口必须安装3.动画层默认使用本身实现4.动画层也可使用其他动画蓝图实现,但必须在角色蓝图中关联…...

从 GitHub 批量下载项目各版本的方法
一、脚本功能概述 这个 Python 脚本的主要功能是从 GitHub 上下载指定项目的各个发布版本的压缩包(.zip 和 .tar.gz 格式)。用户需要提供两个参数:一个是包含项目信息的 CSV 文件,另一个是用于保存下载版本信息的 CSV 文件。脚本…...

一、对lora_sx1278v1.2模块通信记录梳理
一、通信测试: 注意: 1、检查供电是否满足。 2、检测引脚是否松动或虚焊。 3、检测触发是否能触发。 引脚作用: SPI:通信(仅作一次初始化,初始化后会进行模块通信返回测试,返回值和预定值相否即…...