站内搜索引擎——06_最后优化及结项总结

请添加图片描述

✨✨欢迎来到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的使用、网络相关的知识还有外部库的链接以及封装的过程等


请添加图片描述

专栏:站内搜索引擎项目
项目代码仓库:站内搜索引擎项目代码仓库(随博客更新)
都看到这里了,留下你们的珍贵的👍点赞+⭐收藏+📋评论吧

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

T_X_Parallel〆

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值