当前位置: 首页 > news >正文

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;
}

运行结果

在未登录状态下,不可以搜索:

登录账号:

接下来就可以开始搜索了:


http://www.mrgr.cn/news/58558.html

相关文章:

  • ECharts图表图例知识点小结
  • HTML/CSS/JS在线运行代码工具
  • Tomcat隐藏版本号和报错信息
  • 重学SpringBoot3-Spring WebFlux之HttpHandler和HttpServer
  • AnaTraf | 网络性能监控与TCP响应时延:保障高效运维的核心要素
  • 基于阿里云服务的移动应用日志管理方案—日志的上传、下传、存档等
  • Nestjs请求处理顺序
  • 【信息系统管理工程师】与【信息系统项目管理师】傻傻分不清楚?一文说清楚
  • 谷歌开发者账号,为什么新号老是因为高风险被封?
  • 如何将原本打开Edge呈现出的360浏览器,更换成原本的Edge页面或者百度等其他页面
  • uniapp开发Web页面之动态菜单配置攻略
  • LEG引擎装备升级脚本,BLUE引擎传奇添加升级装备的NPC示例
  • 卷积神经网络评价指标
  • 客服的沟通技巧与策略
  • Sei 生态迎首个 MMORPG 游戏伙伴 Final Glory,开启新篇章
  • [Java进阶] 并发编程之进程、线程和协程
  • 23种设计模式
  • Vue3 + TypeScript 实现 iframe 嵌入与通信的完整指南以及全屏弹窗方案
  • 动态规划-子序列问题——376.摆动序列
  • 青训营 X 豆包MarsCode 技术训练营--最大矩形面积问题
  • MATLAB锂电概率分布模型
  • 微积分复习笔记 Calculus Volume 1 - 3.7 Derivatives of Inverse Functions
  • 学习webservice的心得
  • TinTin Web3 动态精选:Vitalik 探讨以太坊协议,Solana ETN 开启质押功能
  • Unpaired Image-to-Image Translation using Cycle-Consistent Adversarial Networks
  • Openpyxl--学习记录