✨✨欢迎来到T_X_Parallel的博客!!
🛰️博客主页:T_X_Parallel
🛰️项目代码仓库:站内搜索引擎项目代码仓库
🛰️专栏 : 站内搜索引擎项目
🛰️欢迎关注:👍点赞🙌收藏✍️留言
项目环境:vscode、wsl(Ubuntu22.04)、g++/CMake
技术栈:C/C++ C++11、STL、标准库 Boost、Jsoncpp、cppjieba、cpp-httplib、html
1. 添加日志模块
程序中有很多打印内容,但是打印内容比较单一,一个成熟的程序需要一个日志模块,接下来简单实现一个日志模块,让每条打印信息包含日志等级、时间、内容、所处文件以及所处行号。这都是为了如果出现错误能知道哪时哪里出错了。这里只是简单实现,打印结果还是打印在命令行中,如果可以的话,可以自行实现一个将打印结果放进文件中的日志模块
日志模块加入前的打印内容
日志模块加入后的打印内容
简单的日志模块中就是将想要的打印的内容进行格式化,也是通过std::cout
打印信息,所以直接给出我设置的格式
std::cout << '[' << level << ']' << '[' << timeStr << ']' << '[' << message << ']' << '[' << file << ']' << '[' << line << ']' << std::endl;
上面的打印内容包括:事件等级、时间、信息、文件以及行号
- 事件等级:这里使用
#define
进行宏定义,事件等级可以设置为:一般(NORMAL)、警告(WARNING)、错误(ERROR)、调试(DEBUG)、致命(FATAL)#define NORMAL 1 #define WARNING 2 #define ERROR 3 #define DEBUG 4 #define FATAL 5
- 时间:使用C/C++库函数来获取时间和格式化时间——
std::time(nullptr)
获取时间戳,std::strftime()
格式化时间(需提前引头文件#include <ctime>
)std::time_t t = std::time(nullptr); char timeStr[100]; std::strftime(timeStr, sizeof(timeStr), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
- 信息:调用者传入的打印信息
- 文件/行号:可以使用C/C++中的预定义宏——
__FILE__
和__LINE__
由上面可以看出,其实调用者如果想要使用该模块只需传入事件等级和打印信息即可,所以可以将log()
进行宏定义
#define LOG(LEVEL, MESSAGE) log(#LEVEL, MESSAGE, __FILE__, __LINE__)
这样就实现了一个简单的日志模块,该模块如果感兴趣可以自行优化或改进
2. 解决结果中重复问题
在实现搜索模块的时候,应该当时提到有个位置是有问题的,如果对这个模块有遗忘的,可以去看博主该模块的实现过程(Boost搜索引擎——03_Searcher搜索模块)
在Search()
函数中,第二步获得索引并汇总结果时,会出现一种情况,当输入的关键词中的多个词都匹配到同一个文档,那么汇总后的结果数组中会存在这个文档多次,最后返回给用户的结果也会存在多个相同的文档,所以接下来想办法解决这个问题
这里如果存在多个词对应同一个文档id,那么可以将这些文档放入一个数组中,然后该数组对应着一个文档id,这时就可以重新创建一个结构体来做到这个功能
struct InvertedElemPrint { uint64_t doc_id; int weight; std::vector<std::string> words; InvertedElemPrint() : doc_id(0), weight(0) {} };
为了检查该文档id之前是否得到过,可以使用哈希表来查找,如
unordered_map<uint64_t, InvertedElemPrint>
。遍历关键词的分词结果数组,然后逐个获取倒排索引,再遍历查找出来的倒排索引结果数组,然后往哈希表中填值,因为unordered_map
的运算符重载operator[]
中,当[]
中的值存在时,返回该值对应的值,如果不存在,会将该值插入到哈希表中并返回所对应的值注:权重weight应该累加起来
std::unordered_map<uint64_t, InvertedElemPrint> hash; for (std::string word : words) { boost::to_lower(word); ns_index::InvertedElemList *inverted_list = index->GetInvertedIndex(word); if (inverted_list == nullptr) continue; for (const auto &elem : *(inverted_list)) { auto &iter = hash[elem.doc_id]; iter.doc_id = elem.doc_id; iter.weight += elem.weight; iter.words.push_back(elem.word); } }
然后再将结果汇总,遍历哈希表,将每个键值对的第二个值即每个
InvertedElemPrint
放入结果数组中std::vector<InvertedElemPrint> inverted_list_all; for (const auto &e : hash) inverted_list_all.push_back(std::move(e.second));
最后再进行排序,和之前的版本一样的,只是改动一下类名
sort(inverted_list_all.begin(), inverted_list_all.end(), [](const InvertedElemPrint &e1, const InvertedElemPrint &e2){ return e1.weight > e2.weight; });
优化后的结果如下
3. 去掉暂停词
由于我们在分词时没有去掉暂停词,如:the、is、a等,当我们再去搜索含有这些暂停词或者只搜索这些暂停词时,结果一定有很多不相关的文档,而且也会影响搜索效率,那么接下来就是去掉暂停词
首先要去掉暂停词,得先知道有哪些暂停词,当时在实现索引模块时,为了能方便使用cppjieba库,在util模块中封装了
jieba.CutForSearch()
函数,但是在封装之前还初始化了Jieba类的静态变量jieba,初始化时用到的就有一个文件是内置的暂停词const char *const STOP_WORD_PATH = "../dict/stop_words.utf8";
>
那么就直接用该文件里的暂停词,而为了能判读某个词是否在这些暂停词中,应该使用哈希表来实现,所以接下来就是遍历一遍暂停词文件的每一行,将所有暂停词存入哈希表中std::unordered_set<std::string> stop_set; void InitJiebaUtil() { std::ifstream in(STOP_WORD_PATH); if (!in.is_open()) { LOG(FATAL, "stop words file open fail!"); return; } std::string line; while (std::getline(in, line)) { stop_set.insert(line); } in.close(); }
然后就是将目标关键词切词之后,所得到的结果,遍历该结果,检查每个词是否是暂停词,如果是,直接删除
void CutStringHelper(const std::string &src, std::vector<std::string> *out) { jieba.CutForSearch(src, *out); for (auto iter = out->begin(); iter != out->end();) { if (stop_set.count(*iter)) out->erase(iter); else iter++; } }
因为这个暂停词的哈希表每个程序只需要进行一次构建,那么可以将该类设置为单例模式,由于之前在索引模块中实现了单例模式的
Index
类,这里就不过多赘述,如有细节不懂的,请看博主索引模块的博客(Boost搜索引擎——02_正排与倒排索引构建)namespace ns_util { class JiebaUtil { private: cppjieba::Jieba jieba; std::unordered_set<std::string> stop_set; private: JiebaUtil() : jieba(DICT_PATH, HMM_PATH, USER_DICT_PATH, IDF_PATH, STOP_WORD_PATH) {} JiebaUtil(const JiebaUtil &) = delete; JiebaUtil &operator=(const JiebaUtil &) = delete; static JiebaUtil *instance; private: static JiebaUtil *GetInstance() { static std::mutex mtx; if (instance == nullptr) { mtx.lock(); if (instance == nullptr) { instance = new JiebaUtil(); instance->InitJiebaUtil(); } } return instance; } void InitJiebaUtil() { std::ifstream in(STOP_WORD_PATH); if (!in.is_open()) { LOG(FATAL, "stop words file open fail!"); return; } std::string line; while (std::getline(in, line)) { stop_set.insert(line); } in.close(); } void CutStringHelper(const std::string &src, std::vector<std::string> *out) { jieba.CutForSearch(src, *out); for (auto iter = out->begin(); iter != out->end();) { if (stop_set.count(*iter)) out->erase(iter); else iter++; } } public: static void CutString(const std::string &src, std::vector<std::string> *out) { ns_util::JiebaUtil::GetInstance()->CutStringHelper(src, out); } }; JiebaUtil *JiebaUtil::instance = nullptr; }
只对外开放
CutString()
一个函数供外部调用注:由于需要遍历结果中的每个词是否是暂停词,所以在索引构建的过程需要事件会增长,但是搜索时的效率会提高
去掉暂停词后的效果如下
4. 前端模块小优化
之前在实现搜索框的时候忘记了像百度、搜狗这些搜索引擎的搜索框输入完关键词按回车即可进行搜索,那么在这个小项目中也可以添加这样一个机制,使用的就是监听机制
$(document).ready(function () {
// 监听搜索框的keydown事件
$("#search-input").on("keydown", function (event) {
if (event.key === "Enter") {
Search();
}
});
});
还有当搜索结果为空时,之前没有对这种情况进行处理,只需在BuildHtml(data)
函数最开始添加一个检查即可实现
function BuildHtml(data) {
let results_label = $(".container .results");
results_label.empty();
if (data == "" || data == null) {
let div_label = $("<div>", {
class: "result",
text: "无相关内容",
});
div_label.appendTo(results_label);
return;
}
for (let elem of data) {
if (elem == "" || elem == null) {
continue;
}
let a_label = $("<a>", {
text: elem.title, // 设置链接文本
href: elem.url, // 设置链接地址
target: "_blank", // 设置在新标签页打开
});
let p_label = $("<p>", {
text: elem.summary, // 设置文本
});
let i_label = $("<i>", {
text: elem.url, // 设置文本
});
let div_label = $("<div>", {
class: "result", //设置类名
});
a_label.appendTo(div_label); // 添加到result容器中
p_label.appendTo(div_label); // 添加到result容器中
i_label.appendTo(div_label); // 添加到result容器中
div_label.appendTo(results_label); // 将result添加到results容器中
}
}
5. 改进程序启动方式
可以使用下面的命令启动程序,可以在后台运行该程序(需关闭程序需使用kill -9 PID
命令结束程序或者重启机器),还可以将打印的日志信息重定向至文件中
nohup ./http_server > log/log.txt 2>&1 &
6. 总结
该项目虽然到这里已经完结了,但是还有许多可以优化和扩展的方向
- 建立整站搜索(因为当时数据清洗时用到的只是一个文件夹中的html文件,但是其他有些文件夹中仍存在一些html文件)
- 设计一个可以实时更新数据的搜索引擎,这就需要使用爬虫、信号等技术来实现
- 当将搜索数据换成其他内容时,可以做一个竞价排名,让这些内容的所有者使用高价来竞争最前面的位置
- 做一个热词统计模块,能根据之前所有用户搜索的关键词构建一个热词统计队列,能智能推荐和补全用户关键词,这就可能需要使用上字典树、优先级队列等
- 设计一个登录注册模块,这就需要引入MySQL等数据库技术
最完整个项目后,知道了自己在这些方面的缺点,并且回顾了多个关键知识,如:单例模式、STL的使用、网络相关的知识还有外部库的链接以及封装的过程等
建立整站搜索(因为当时数据清洗时用到的只是一个文件夹中的html文件,但是其他有些文件夹中仍存在一些html文件)
- 设计一个可以实时更新数据的搜索引擎,这就需要使用爬虫、信号等技术来实现
- 当将搜索数据换成其他内容时,可以做一个竞价排名,让这些内容的所有者使用高价来竞争最前面的位置
- 做一个热词统计模块,能根据之前所有用户搜索的关键词构建一个热词统计队列,能智能推荐和补全用户关键词,这就可能需要使用上字典树、优先级队列等
- 设计一个登录注册模块,这就需要引入MySQL等数据库技术
最完整个项目后,知道了自己在这些方面的缺点,并且回顾了多个关键知识,如:单例模式、STL的使用、网络相关的知识还有外部库的链接以及封装的过程等
专栏:站内搜索引擎项目
项目代码仓库:站内搜索引擎项目代码仓库(随博客更新)
都看到这里了,留下你们的珍贵的👍点赞+⭐收藏+📋评论吧