c++实现boost搜索引擎功能扩展 介绍+代码(日志,处理暂停词,增加数据源,引入广告竞价,增加用户管理,连接mysql)
目录
boost搜索引擎 扩展
引入日志
代码
运行结果
处理暂停词
思路
代码
运行结果
增加数据源
思路
代码
运行结果
引入广告
介绍
目标
思路
标识/存放广告
如何处理
代码
运行结果
增加用户管理
目标
前端
代码
运行结果
前文 -- c++实现boost库 搜索引擎(详细介绍和代码),cppjieba的下载和使用,正排/倒排索引的查询和建立,cpp-httplib的下载和使用-CSDN博客
boost搜索引擎 扩展
引入日志
我们直接将之前写过的日志功能引入就行,然后将日志信息写入到文件中
代码
#pragma once#include <iostream> #include <time.h> #include <stdarg.h> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> #include <unistd.h> #include <stdlib.h>#define INFO 0 #define DEBUG 1 #define WARNING 2 #define ERROR 3 #define FATAL 4 // 致命的错误#define SCREEN 1 #define ONEFILE 2#define DEF_NAME "log.txt" #define DEF_PATH "/home/mufeng/c++/Search_Engines/log/"#define SIZE 1024class Log { public:Log(int method = SCREEN): method_(method), path_(DEF_PATH){}void operator()(int level, const char *format, ...){time_t t = time(nullptr);struct tm *ctime = localtime(&t);char leftbuffer[SIZE];snprintf(leftbuffer, sizeof(leftbuffer), "[%s][%d-%d-%d %d:%d:%d]", levelToString(level).c_str(),ctime->tm_year + 1900, ctime->tm_mon + 1, ctime->tm_mday,ctime->tm_hour, ctime->tm_min, ctime->tm_sec);va_list s;va_start(s, format);char rightbuffer[SIZE];vsnprintf(rightbuffer, sizeof(rightbuffer), format, s);va_end(s);// 格式:默认部分+自定义部分char logtxt[SIZE * 2];snprintf(logtxt, sizeof(logtxt), "%s %s\n", leftbuffer, rightbuffer);printLog(logtxt);}~Log(){}private:std::string levelToString(int level){switch (level){case INFO:return "INFO";case DEBUG:return "DEBUG";case WARNING:return "WARNING";case ERROR:return "ERROR";case FATAL:return "FATAL";default:return "NONE";}}void printLog(const std::string &logtxt){switch (method_){case SCREEN:std::cout << logtxt;break;case ONEFILE:printOneFile(logtxt);break;default:break;}}void printOneFile(const std::string &info){std::string path = path_ + DEF_NAME;int fd = open(path.c_str(), O_WRONLY | O_CREAT | O_APPEND, 0666);if (fd > 0){write(fd, info.c_str(), info.size());close(fd);}else{return;}}private:int method_;std::string path_; };Log lg(ONEFILE);
这个头文件中自带Log对象,只要包含这个头文件,就可以使用日志功能
- 并且直接在本文件中对写入属性进行设置(直接打印/写入文件)
运行结果
可以看到,我们成功将日志信息写入到文件中了:
每次查看时,看到的日志信息都会增加:
处理暂停词
思路
我们之前说过,暂停词在检索流程中并不能提供帮助,甚至会拖慢搜索效率
- 所以,我们要去掉暂停词
实际上,在cppjiba中,本身就有一个汇总了很多暂停词的文件 -- stop_words.utf8
- 我们只需要把它读出来,然后在分词结果中筛掉暂停词就行
我们可以借助unordered_map的查找效率,查询分词结果中是否存在暂停词
代码
const char *const DICT_PATH = "/home/mufeng/c++/Search_Engines/build/dict/jieba.dict.utf8";const char *const HMM_PATH = "/home/mufeng/c++/Search_Engines/build/dict/hmm_model.utf8";const char *const USER_DICT_PATH = "/home/mufeng/c++/Search_Engines/build/dict/user.dict.utf8";const char *const IDF_PATH = "/home/mufeng/c++/Search_Engines/build/dict/idf.utf8";const char *const STOP_WORD_PATH = "/home/mufeng/c++/Search_Engines/build/dict/stop_words.utf8";class jieba_util{private:cppjieba::Jieba jieba_;std::unordered_map<std::string, bool> stop_words_; // bool是辅助参数,没啥用private:jieba_util() : jieba_(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH) {}jieba_util(const jieba_util &) = delete;jieba_util &operator=(const jieba_util &) = delete;static jieba_util *instance_;public:void init(){std::ifstream in(STOP_WORD_PATH);if (!in.is_open()){lg(ERROR, "file: stop_words.utf8 open failed");}std::string line;while (std::getline(in, line)){stop_words_[line] = true;}}static jieba_util *get_instance(){static std::mutex mtx;if (nullptr == instance_) //防止已有对象后,还要进来加锁{mtx.lock();if (nullptr == instance_) // 防止多线程下重复创建对象{instance_ = new jieba_util();lg(DEBUG, "JiebaUtil success");}mtx.unlock();}return instance_;}void cut_helper(const std::string &src, std::vector<std::string> &out){jieba_.CutForSearch(src, out);for (auto word = out.begin(); word != out.end();){auto pos = stop_words_.find(*word);if (pos != stop_words_.end()){// 当前词就是暂停词word = out.erase(word);}else{++word;}}}public:static void CutString(const std::string &src, std::vector<std::string> &out){ns_helper::jieba_util::get_instance()->cut_helper(src, out);}};jieba_util *jieba_util::instance_ = nullptr; };
运行结果
当我们搜索暂停词时,就已经搜索不到了
虽然会拖慢建立索引的速度,但会提高后续的搜索效率
- 我们输入关键词后,搜索结果出现的比之前要快很多
- 因为数据量少了,建立的索引大小也小了
增加数据源
思路
之前我们只添加了/boost_1_86_0/doc/html目录下的所有html文件作为数据源
- boost官网上还有很多网页在其他路径下,我们可以将这些文件也添加进去
也就是将/boost_1_86_0下所有html文件,全都作为数据源
- 只需要更改url的构建方式即可(不要忘记将所有文件都拷贝到我们项目的input目录下)
重新拷贝文件后,html文件数量从8000多激增到27000多:
- 这意味着我们建立索引的过程变得更加漫长
代码
运行结果
可以看到,确实是可以搜索出其他路径下的文档:
- (注意该截图的最后一个结果)
我们的boost搜索引擎,也可以不止步于boost库,因为这仅仅取决于数据源是什么
- 我们还可以引入很多功能,向市面上已经推出的搜索引擎靠拢
引入广告
介绍
向搜索引擎中引入广告竞价功能
- 广告主在用户搜索特定关键字时竞价,让他们的广告出现在搜索结果页面的顶部或其他显著位置
目标
如果要真的将市面上的网站设置为向我们投过广告费的
- 就需要将它们也作为搜索引擎的数据源,也就需要源文档
- 但直接获取不太方便,所以我们直接以boost官网上的网站做模拟
并且,可以设置一个常驻置顶
- 比如我们的博客链接
思路
标识/存放广告
广告一般不都是有"广告"标识吗?
- 我们可以简单一点,就把广告字样放在标题里
也就是要实现:
- 如果搜索结果有广告表中的网站,就修改标题
- 并根据广告费提高权值
如何确定哪个网站投放了广告呢?
- 我们可以以url作为标识符
- 因为标题/正文可能会重复,而文档id我们只在建立索引时才会用到,想来想去也只有url合适
可以把mysql引入进来,创建一个广告表,存放[投放了广告的网站url,广告费]
数据我们直接在mysql下插入就行:
如何处理
要在将不重复文档进行排序前就把广告代表的权值设置好
- 那么,就要在查询倒排索引的时候,判断当前文档是否投过广告,且查询基础是倒排拉链中的元素:
- 所以,需要在倒排索引的元素结构中添加url字段,用于这里的比对过程:
目前权值的影响因素有:在标题/正文中出现的次数+是否投过广告
标题该怎么办呢?
- 注意,我们不能直接在建立索引时就修改标题,不然每次更新表中数据,都得重新建立索引才能让用户看见
- 所以,我们就在构建json结果串时修改即可,这样每次的搜索都能使用最新的数据
- 并且,每次搜索都会重新从mysql中读取数据,所以数据库中对广告表的修改,会自动反应在每次的搜索中
常驻置顶可以让它不参与权值排序
- 直接固定成搜索结果的第一项
代码
search.hpp
void search(const std::string &data, std::string *json){// 进行分词std::vector<std::string> words;ns_helper::jieba_util::CutString(data, words);struct words_info{std::vector<std::string> words_; // 多个分词可以在某文档中查找到doc_id_t doc_id_ = 0;int weight_ = 0; // 这个词在文档中的权重bool is_ad_ = false;};std::unordered_map<doc_id_t, words_info> Non_duplicate_map;// 引入广告功能advertising_table ad;std::unordered_map<std::string, float> ads;ad.read_advertising_information(ads);// 得到不重复的文档集合for (auto word : words){boost::to_lower(word);inverted_zipper zipper;index_->search_inverted_index(word, zipper);for (auto &it : zipper){Non_duplicate_map[it.doc_id_].doc_id_ = it.doc_id_;std::string url = it.url_;Non_duplicate_map[it.doc_id_].weight_ += it.weight_;const float alpha = 0.1f;if (ads.find(url) != ads.end()){Non_duplicate_map[it.doc_id_].is_ad_ = true;Non_duplicate_map[it.doc_id_].weight_ += (int)(ads[url] * alpha);}Non_duplicate_map[it.doc_id_].words_.push_back(std::move(it.word_));}}// 将文档集合转换类型(转成vector方便排序)std::vector<words_info> doc_map;for (auto &it : Non_duplicate_map){doc_map.push_back(std::move(it.second));}// 按相关性排序std::sort(doc_map.begin(), doc_map.end(),[](const words_info &x, const words_info &y){ return x.weight_ > y.weight_; });// 查询正排索引,并构建json串Json::Value root;// 添加常驻置顶Json::Value top;top["title"] = "我的博客";top["desc"] = "这是本人的博客,欢迎大家浏览";top["url"] = "https://blog.csdn.net/m0_74206992?type=blog";root.append(top);for (const auto &it : doc_map){docInfo_index doc;if (!index_->search_positive_index(it.doc_id_, doc)){continue;}Json::Value item;std::string title = doc.title_;if (it.is_ad_ == true){title += "[广告]"; // 这里加的空格会被json忽略}// title += ",weight=" + std::to_string(it.weight_);item["title"] = std::move(title);item["desc"] = get_desc(doc.content_, it.words_[0]);item["url"] = doc.url_;root.append(item);}Json::FastWriter writer;// Json::StyledWriter writer;*json = writer.write(root);}
mysql.hpp
class my_mysql { protected:MYSQL *mysql_;public:my_mysql(){connect();}~my_mysql(){mysql_close(mysql_);}private:void connect(){mysql_ = mysql_init(nullptr);std::cout << "connecting\n";mysql_ = mysql_real_connect(mysql_, "101.126.142.54", "mufeng", "599348181", "conn", 3306, nullptr, 0);if (nullptr == mysql_){std::cout << "connect failed\n";exit(1);}std::cout << "connect success\n";mysql_set_character_set(mysql_, "utf8");} };class advertising_table : public my_mysql { public:void read_advertising_information(std::unordered_map<std::string, float> &data){std::string sql;sql = "select url,fee from ad";int ret = mysql_query(mysql_, sql.c_str());if (ret == 0){MYSQL_RES *res = mysql_store_result(mysql_);if (res == nullptr){std::cout << "mysql_store_result failed\n";}else{int row_num = mysql_num_rows(res);int field_num = mysql_num_fields(res);// // 打印列名// MYSQL_FIELD *field;// while ((field = mysql_fetch_field(res)) != NULL)// {// std::cout << field->name << " ";// }// std::cout << std::endl;// 拿出表数据for (int i = 0; i < row_num; ++i){MYSQL_ROW row = mysql_fetch_row(res);// 转换广告费类型std::string fee = row[1];data[row[0]] = std::stof(fee);}}}else{std::cout << mysql_error(mysql_) << std::endl;}} };
index.hpp
struct word_info {std::string word_;doc_id_t doc_id_;int weight_; // 这个词在文档中的权重std::string url_; };void create_positive_index(const std::string &path) // 以文档为单位{std::ifstream in(path, std::ios_base::in | std::ios_base::binary);if (!in.is_open()){lg(ERROR, "file: %s open failed", path.c_str());return;}std::string doc;while (std::getline(in, doc)){// 拿到一个文档,进行解析docInfo_index di;if (!analysis(doc, delimiter, &di)){lg(ERROR, "analysis faild");continue;}di.doc_id_ = pos_index_.size();// 解析完成后,插入到索引中pos_index_.push_back(std::move(di));}}void create_inverted_index(const docInfo_index &doc) // 以文档为单位{struct word_cnt{int title_cnt_;int content_cnt_;word_cnt() : title_cnt_(0), content_cnt_(0) {}~word_cnt() {}};std::unordered_map<std::string, word_cnt> cnt_map;// 统计每个词在所属文档中的相关性std::vector<std::string> content_words;ns_helper::jieba_util::CutString(doc.content_, content_words);for (auto it : content_words){// 为了实现匹配时忽略大小写,将所有单词转换为小写boost::to_lower(it);++cnt_map[it].content_cnt_;}std::vector<std::string> title_words;ns_helper::jieba_util::CutString(doc.title_, title_words);for (auto it : title_words){boost::to_lower(it);++cnt_map[it].title_cnt_;}// 计算权值 #define title_count 10 #define content_count 1for (const auto &it : cnt_map){word_info t;t.doc_id_ = doc.doc_id_;t.url_ = doc.url_;t.word_ = it.first;t.weight_ = (it.second).title_cnt_ * title_count + (it.second).content_cnt_ * content_count;inv_index_[t.word_].push_back(t); // 插入的是小写单词}}
运行结果
并且,这里是可以根据数据库中数据的更新而动态变化搜索结果的
- 我们把第三个网页(change log)也加进广告表中:
- 然后我们重新搜索,可以看到change log确实显示出了广告字样,并且因为金额更大,显示在了更前面:
增加用户管理
目标
继续使用我们的mysql
- 创建一个用户表 -- 包含用户名,用户密码
我们在搜索引擎上增加用户注册/登录功能,当用户执行搜索操作后:
如果还处于未登录状态:
- 提示用户还未登陆
如果用户已登录:
- 即使刷新网页/关闭网页也可以保持登录状态
前端
这是我们的主界面:
点击注册/登录按钮:
- 点击空白处可退出弹框
如果未登录/登录成功,会有提示:
代码
mysql.hpp
class user_table : public my_mysql { public:bool write_user_information(const std::string &name, const std::string &password){std::string sql = "insert into user(name,password) values('" + name + "','" + password + "')";int ret = mysql_query(mysql_, sql.c_str());if (ret == 0){return true;}else{lg(ERROR, "%s", mysql_error(mysql_));return false;}}void read_user_information(std::unordered_map<std::string, std::string>& users){std::string sql;sql = "select name,password from user";int ret = mysql_query(mysql_, sql.c_str());if (ret == 0){MYSQL_RES *res = mysql_store_result(mysql_);if (res == nullptr){std::cout << "mysql_store_result failed\n";}else{int row_num = mysql_num_rows(res);int field_num = mysql_num_fields(res);// 拿出表数据for (int i = 0; i < row_num; ++i){MYSQL_ROW row = mysql_fetch_row(res);users[row[0]] = row[1];}}}else{std::cout << mysql_error(mysql_) << std::endl;}} };
server.cpp
#include "/home/mufeng/cpp-httplib/httplib.h" #include <pthread.h>#include "searcher.hpp" #include "mysql.hpp"#define root_path "../wwwroot"enum user_status {SUCCESS,EXIST,WRONG,FAILED };std::unordered_map<std::string, std::string> sessions; // session_id -> username// 登录函数 int login(user_table &u_tb, std::unordered_map<std::string, std::string> &users, const std::string &name, const std::string &password, std::string &session_id) {// 先更新下用户表u_tb.read_user_information(users);// 比对账号密码if (users.count(name)){if (users.find(name) == users.end() || (users[name] != password)){return user_status::WRONG;}// 生成session idsession_id = name + "_session"; // 简化的会话 IDsessions[session_id] = name;return user_status::SUCCESS;}return user_status::FAILED; }// 注册函数 int register_user(user_table &u_tb, std::unordered_map<std::string, std::string> &users, const std::string &name, const std::string &password) {// 判断用户表中是否有这个账号if (users.find(name) != users.end()){return user_status::EXIST;}// 添加到数据库中if (!u_tb.write_user_information(name, password)){return user_status::FAILED;}return user_status::SUCCESS; }// 搜索功能 void search(const std::string &session_id, const std::string &word, Searcher &s, httplib::Response &rsp) {if (sessions.count(session_id) == 0){rsp.status = 401; // 未登录rsp.set_content("未登录,请先登录。", "text/plain; charset=utf-8");}else{std::string json_string;s.search(word, &json_string);rsp.set_content(json_string, "application/json");} }int main() {Searcher s;ns_helper::jieba_util::get_instance()->init();std::cout << "init success\n";httplib::Server svr;svr.set_base_dir(root_path);// 读取用户表user_table user_tb;std::unordered_map<std::string, std::string> users;user_tb.read_user_information(users);// 用户注册svr.Post("/register", [&user_tb, &users](const httplib::Request &req, httplib::Response &rsp){Json::Value json_body;Json::CharReaderBuilder reader;std::istringstream s(req.body);std::string errs;if (!Json::parseFromStream(reader, s, &json_body, &errs)) {rsp.status = 400; // 请求格式错误rsp.set_content("请求格式错误", "text/plain; charset=utf-8");return;}std::string username = json_body["username"].asString();std::string password = json_body["password"].asString();int ret= register_user(user_tb,users,username, password);if(ret==user_status::EXIST){rsp.set_content("账号已存在", "text/plain; charset=utf-8");}else if(ret==user_status::FAILED){rsp.set_content("注册失败,请联系开发者", "text/plain; charset=utf-8");}else{rsp.set_content("注册成功", "text/plain; charset=utf-8");} });// 用户登录svr.Post("/login", [&user_tb, &users](const httplib::Request &req, httplib::Response &rsp){Json::Value json_body;Json::CharReaderBuilder reader;std::istringstream s(req.body);std::string errs;if (!Json::parseFromStream(reader, s, &json_body, &errs)) {rsp.status = 400; // 请求格式错误rsp.set_content("请求格式错误", "text/plain; charset=utf-8");return;}std::string username = json_body["username"].asString();std::string password = json_body["password"].asString();std::string session_id;int ret= login(user_tb,users,username, password,session_id);//分类if (ret==user_status::SUCCESS) {rsp.set_content("登录成功, 会话ID: " + session_id, "text/plain; charset=utf-8");} else if(ret==user_status::WRONG){rsp.set_content("账号或密码输入错误", "text/plain; charset=utf-8");}else {rsp.status = 401;rsp.set_content("登录失败", "text/plain; charset=utf-8");} });// 搜索svr.Get("/s", [&s](const httplib::Request &req, httplib::Response &rsp){if (!req.has_param("word")){rsp.set_content("必须要有搜索关键字", "text/plain; charset=utf-8");return;}std::string word = req.get_param_value("word");std::string session_id = req.get_param_value("session_id"); // 获取会话 IDstd::cout << "用户在搜索:" << word << std::endl;search(session_id, word, s, rsp); // 调用搜索函数});std::cout << "Starting server on port 8080..." << std::endl;svr.listen("0.0.0.0", 8080);std::cout << "Server is listening..." << std::endl;return 0; }
运行结果
在未登录状态下,不可以搜索:
登录账号:
接下来就可以开始搜索了: