Boost搜索引擎实战项目


项目宏观原理

有如下两句话:

  • 文档1:雷军买了四斤小米
  • 文档2:雷军发布了小米手机

一、正排索引

文档ID文档内容
文档1雷军买了四斤小米
文档2雷军发布了小米手机

正排索引是直接使用文档ID找到文档的内容。

二、倒排索引

倒排索引是根据文档内容,分词,整理不重复的各个关键字,使用关键字来找到文档的ID。

关键字是不重复的,具有唯一性的。

关键字文档ID
雷军文档1,文档2
文档1
发布文档2
四斤文档1
小米文档1,文档2
四斤小米文档1
手机文档2
小米手机文档2

特别注意的是:“了”, “的” 等语气词不是关键字。

模拟查找过程

用户输入:小米

倒排索引查找关键字–>定位到文档ID–>正排索引–>还原文档内容–>title+content(desc)+url文档结果进行摘要—>构建响应结果

三、标签和去标签和清洗模块parser.cc

本项目的数据源是boost_1_78_0/doc/html目录下的所有文件和文件夹内容。

所以需要实现一个parser.cc源文件来对boost_1_78_0/doc/html目录的数据源做索引和清洗。

什么叫做标签?

在这里插入图片描述

包含有<>的,就叫做标签。
这些标签在我们的索引中没有什么用,所以要去掉。
通常来说,标签都是成对出现的。比如 head 和 /head
但也有一些标签只有开始,没有结束。

[@**** data]$ ll
total 20
drwxrwxr-x 60 dzt dzt 16384 Mar 16 12:38 input
//这里存放原始的html文件内容
drwxrwxr-x 2 dzt dzt 4096 Mar 16 12:51 raw_html
//这里存放去标签后干净的文档

目的:把每个文档都去标签,然后将干净的内容放到写入到同一个文件中,每个文档内容不需要携带\n,用 \3 , \4等作为分割符即可!

xxxxxxxxxxxxxx\3yyyyyyyyyyyyyy\3zzzzzzzzzzzzz\4
因为\3,\4这些分割符是控制字符,不会显示出来。

四、parser.cc代码结构

从数据源读取文件名+路径

  • 1.将要去标签和解析的源数据进行读取。符合要求的所有 .html文件放入vector容器中。

std::vectorstd::string files_list中

  • 2.按照files_list读取到的文件内容,按照文件名一个个进行解析,解析成 标签+内容+url的方式保存在一个个结构体中

std::vector<DocInfo_t> results;

  • 3.把解析好的内容塞到一个文件中

std::string output = “/data/raw_html/raw.txt”;

文件中的每一行都是 title\3content\3url \n 的形式
一行就是一个文件,方便后续提取构建正排和倒排索引。

//要读取的所有文件,放的全是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 ParserHtml(const std::vector<std::string>& files_list,std::vector<DocInfo_t>* results);
bool SaveHtml(const std::vector<DocInfo_t>& results,const std::string& output); //output作为一个将要被打开的文件,是输入型参数

int main()
{
    //第一步,递归式读取文件名+路径,保存到files_list中,方便后续一个个的搜索
    std::vector<std::string> files_list;
    //  读取失败
    if(!EnumFile(src_path,&files_list))
    {
        std::cerr<< "Enum File error" << std::endl;
        return 1;
    }

    // 第二步,按照files_path读取的内容,一个个进行解析文件名。
    std::vector<DocInfo_t> results;
    if(ParserHtml(files_list, &results))
    {
        std::cerr<< "ParserHtml error" << std::endl;
        return 2;
    }
    
    //第三步:把解析好的文件内容写入到output中,按照\3作为分隔符
    if(SaveHtml(results,output))
    {
        std::cerr<< "SaveHtml error" << std::endl;
        return 3;
    }

    return 0;
}

tips:boost库和boost搜索手册区别

在编写parser.cc文件时,需要用到boost库中的源代码,一些源函数进行编写, 所以要下载

  • sudo yum install -y boost-devel

这个库的版本是1.53的版本。

而我们搜索数据源时,搜索的是boost库 boost_1_78_0版本中的html网页!

这两个boost文件是不一样的!

一个是写代码需要用到源代码库,一个是搜索数据源需要用到的库。

4.2 编写EnumFile模块

该模块的功能就是将boost_178_0库下的所有.html文件全部检索出来,并存放到一个vector中。

4.3 编写ParseHtml模块

该模块就是对每一个.html文件进行解析。

具体操作如下:

  • 1.先读取文件,如果读取失败, 继续读取下一个文件

  • 2.解析该文件,提取title

  • 3.解析该文件,提取content

  • 4.解析该文件,构建url

这里解析url的时候,我遇到了一个坑:传参问题。
前面两个的传参都是传的result,也就是传文件的内容过去解析。
而这个文件的url,是要解析文件的路径,所以要传一个文件的路径过去的。

4.4 将解析后的数据源保存

五、建立文件索引

图解正/倒排索引

在这里插入图片描述

首先应该有几个内容:

  • 1.一个文档的信息:
struct DocInfo
{
    std::string title; // 解析的文档的标题
    std::string content; // 要解析文档的去标签后的内容
    std::string url;    // 文档在官网中的url
    uint64_t doc_id;  // 文档id
};
  • 2.单个倒排索引元素结构体

这里由于需要建立倒排索引
而倒排索引是根据关键字,提取出一串含有该关键字的倒排拉链(也就是包含该关键字的多个文档)。通过获取倒排拉链的元素,就能获取到文档id,再通过文档id,进行正排索引,获取对应文档。

//倒排索引的结构体
struct InvertedElem 
{
    uint64_t doc_id; // 文档id
    std::string word; //关键字
    int weight; // 权重
};

1. 建立正排索引

根据读到的内容,建立正排索引

  • 1.首先解析line内容,将内容按照分隔符’\3’,分割成三部分:title content url
  • 2.将字符串填充到DocInfo中
  • 3.将DocInfo插入到正排索引的vector中。
 //根据读到的那一行内容建立正排索引
 DocInfo* BuildForwardIndex(const std::string& line)
 {
     //1.解析line内容 --> title content url
     std::vector<std::string> results; //把一个字符串line打散成三个字符串,放到results中
     std::string sep = "\3";
     ns_util::CutUtil::CutString(line,&results,sep);
     if(results.size() != 3) // 不满足 title content url 完整的三部分
     {
         return nullptr;
     }
     //2.将字符串填充到DocInfo中
     DocInfo doc;
     doc.title = results[0];
     doc.content = results[1];
     doc.url = results[2];
     doc.doc_id = forward_index.size(); //先进行保存id,再插入,对应的id就是当前doc在vector中的下标,无需-1了
     //3.将DocInfo插入到正排索引的vector中
     forward_index.push_back(doc);

     return &doc;
 }

这里建立正排索引的过程使用vector容器的原因:
vector容器的下标天然可以作为文档id

2. 建立倒排索引

  • 1.将正排索引获取到的title和content进行切分。
  • 2.遍历title_words和content_words容器,统计每个关键字出现的次数,即统计词频。
  • 3.构建倒排拉链,一个关键字对应一个倒排拉链。
  • 4.将所有的关键字汇总的所有倒排拉链集合起来,就是用户想要搜索的信息。

完整的搜索过程

以上的汇总:

在这里插入图片描述

引入jieba库

细节:编写倒排索引时,假如出现下面情况:
用户在输入Hello,hello,HELLO几种情况时,搜索到的文档是否应该一致。

jieba库的作用:使用jieba函数,对字符串进行切分
切分的比较完善。

八、编写searcher搜索代码

1.获取Index索引单例对象

void InitSearcher(const std::string& input)
{
    index = ns_index::Index::GetInstance(); //获取单例对象
    LOG(NORMAL,"获取单例索引对象成功!");
    
    index->BuildIndex(input); // 建立索引模块
    LOG(NORMAL,"建立索引成功!");

}

2.搭建Search模块

1)分词

//1.[分词]:对用户想要查找的query按照searcherd的要求进行分词
std::vector<std::string> words;
ns_util::JiebaUtil::Split(query,&words); //对用户想要发起搜索的字符串进行切分

从引入jieba库中,使用Split函数对用户输入的字符串进行切分。

2)触发

根据关键字信息,获取倒排拉链。
并对倒排拉链中的节点信息,进行去重处理。

//2.[触发]:根据分词的各个"词",进行index查找 ---注意细节:查找时不区分大小写
 //一个关键字有一条拉链,用户输入的可能不止一个关键字,就会搜索到多条拉链,要将这些拉链保存起来
 //ns_index::InvertedList inverted_list_all; // typedef std::vector<InvertedElem> InvertedList; 
 std::vector<InvertedElemPrint> inverted_list_all; // 保存对每个倒排拉链去重后的节点信息
 std::unordered_map<uint64_t,InvertedElemPrint> tokens_map; //用来将多个文档去重,结果是map中保存的是每一个去重后的
 //节点信息

 for(auto &word : words)
 {
     boost::to_lower(word);
     //先获取倒排拉链,
     ns_index::InvertedList* inverted_list = index->GetInvertedIndex(word);
     //一个关键字对应一条拉链,有可能该关键字不存在于文档中
     if(nullptr == inverted_list)
     {
         continue;
     }
     // 你/是/一个/好人
     //这里可能出现两个关键字获取到的倒排拉链都是一样的,就会导致在统一保存拉链时,
     //inverted_list_all 内部会出现重复的拉链
     //为了实现倒排拉链中节点的去重,不能再直接拿到拉链后就插入,需要先去重
     for(auto& elem : *inverted_list)
     {
         auto& item = tokens_map[elem.doc_id]; //把拉链中的相同id文档提出来,[]:不存在就新建,存在就直接返回
         item.doc_id = elem.doc_id;
         item.weight += elem.weight; 
         item.words.push_back(std::move(elem.word));
     }
     //inverted_list_all.insert(inverted_list_all.end(),inverted_list->begin(),inverted_list->end());
 }

 //保存对每个倒排拉链去重后的节点信息
 for(auto &item :tokens_map)
 {
     inverted_list_all.push_back(item.second);
 }

3)合并排序

使用sort函数,对所有的倒排拉链,按照关键字出现的权重,进行降序排序。

 //3.[合并排序]:汇总查找结果,按照相关性(weight)进行降序排序
 //sort函数对weight相关性进行降序排序即可
 // std::sort(inverted_list_all.begin(),inverted_list_all.end(),
 // [](const ns_index::InvertedElem& e1,const ns_index::InvertedElem& e2)
 // {return e1.weight > e2.weight;});//Lambda匿名函数

 std::sort(inverted_list_all.begin(),inverted_list_all.end(),
 [](const InvertedElemPrint& e1,const InvertedElemPrint& e2)
 {return e1.weight > e2.weight;});//Lambda匿名函数

4)构建json

//4.[构建]:根据查找出来的结果,构建json串 --jsoncpp 根据文档id正排索引即可
//遍历inverted_list_all,将文档id一个个拿出来,进行正排索引
Json::Value root;
for(auto &item : inverted_list_all)
{
    //从已经排好序的inverted_list_all获取文档内容
    ns_index::DocInfo* doc = index->GetForwardIndex(item.doc_id);
    if(doc == nullptr)  continue;
    //构建json串
    Json::Value elem;
    elem["title"] = doc->title;
    elem["desc"] = GetDesc(doc->content,item.words[0]); //这里的文件内容不是我们想要的,我们只想要一部分,后续会对这个地方处理
    //GetDesc-->获取摘要的内容,不是要全部内容,只需要在摘要中显示出关键字
    elem["url"] = doc->url;
    //debug
    //elem["weight"] = (int)item.weight;
    //elem["id"] = (int)item.doc_id;

    root.append(elem); //总的json串信息
}

//Json::StyledWriter writer; // 方便调试
Json::FastWriter writer; //正常使用
*json_string = writer.write(root); //形成搜索结果返回(输出型参数)
}

1.从content提取关键字摘要的模块编写

在这里插入图片描述

std::string GetDesc(const std::string& html_content, const std::string& word)
{
    //思路:找到关键字word在html_content中的位置,往前找50字节(如果没有50字节,就从begin开始)
    //往后找100字节(如果没有100字节,就从end结束) ,再截取这部分内容
    //1.找到关键字word在content中的位置
    //std::size_t pos = html_content.find(word);  //这里也有问题 ,find函数不忽略大小写
    //而之前在倒排索引插入时,已经统一将关键字全部小写化了。
    //所以在find原文档时,就会出现大小写不匹配导致无法查找到文档的情况
    //这里需要用c语言库的search函数
    //if(pos == std::string::npos)    //这种情况是绝对不会存在的
    //    return "None";   
    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.获取begin,end
    int prev_step = 50;
    int next_step = 100;

    int begin = 0;
    int end = html_content.size() - 1; //std:;size_t 无符号整形,这是大坑   
    if(pos > begin + prev_step) begin = pos - prev_step; 
    if(pos < end - next_step ) end = pos + next_step;

    //3.截取内容
    if(begin >= end) return "None2";

    std::string desc =  html_content.substr(begin, end - begin);
    desc += "...";
    return desc;    
}

引入cpp-httplib库并编写httplib调用

#include "cpp-httplib/httplib.h"
#include "searcher.hpp" // 引入搜索引擎

const std::string root_path = "./wwwroot";
const std::string input = "data/raw_html/raw.txt";

int main()
{
    ns_searcher::searcher search;
    search.InitSearcher(input);

    httplib::Server svr;
    svr.set_base_dir(root_path.c_str()); // 设置默认路径

    //lambda
    svr.Get("/s",[&search](const httplib::Request& req,httplib::Response& rsp){
            if(!req.has_param("word"))  //没有搜索关键字,不行
            {
                rsp.set_content("必须要有搜索关键字!","text/plain; charset=utf-8"); //返回一个普通文件plain ,也可以返回一个网页文件text/html 
                return;
            }
            //rsp.set_content("你好 world!","text/plain; charset=utf-8");
            //到这里就获取到了关键字了,要提出来
            std::string word = req.get_param_value("word");
            std::cout << "用户输入的关键字是:" << word << std::endl;
            std::string json_string; // 保存搜索结果
            search.Search(word,&json_string); //按照关键字索引
            rsp.set_content(json_string,"application/json");
            });

    svr.listen("0.0.0.0",8081);

    return 0;
}
 

写在最后:关于实现该项目时遇到的一些bug

1.parser模块解析指定文件的url时,传参传的是文件路径,而不是文件内容

2.静态成员在类外初始化时需要指定类域的问题

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

邓富民

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

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

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

打赏作者

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

抵扣说明:

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

余额充值