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

BoostSearcher搜索引擎项目

BoostSearcher搜索引擎项目

1.BoostSearcher这个项目是什么?

答:一个为Boost文档建立索引的站内搜索引擎,简单的说就是一个类似于csdn站内文档搜索框。

项目展示:

在这里插入图片描述
在这里插入图片描述

gitee:https://gitee.com/zxlfx/boost-search-engine-project

2.为什么做BoostSearcher?

答:学习和了解搜索引擎的底层原理,虽然无法做出一个像百度、360搜索这样的全网搜索引擎,但可以从中窥探出搜索引擎所具备的一些宏观设计。

3.怎么做BoostSearcher?

答:我分为以下几个步骤完成BoostSearcher项目:

  1. 了解BoostSearcher宏观原理。
  2. 获取数据源(html网页)
  3. 对数据建立正排、倒排索引(核心)
  4. 编写html服务,为前端提供服务。
  5. 日志的补充及项目的扩展。

4.项目设计的技术

前端:HTML5、CSS、JS、jQuery、Ajax(不需要了解太多,该项目主要是后端编写,不写前端也行)

后端:C/C++、C++11,STL库、标准库Boost(主要文件库)、jsoncpp(序列化和反序列化)、cppjieba(分词工具)、cpphttp-lib(提供http服务)

5.介绍宏观原理

宏观图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZaCyru4b-1677284957534)(BoostSearcher搜索引擎项目.assets/image-20230223192434446.png)]

宏观图解释:

  1. 做法是先获取数据源(html网页),如何获取?

    答:从Boost官网下载文档HTML网页。

  2. 对HTML网页进行解析和提取,如何解析和提取?

    答:解析网页HTML,提取出title、摘要、url。组成一个个结构体,构成关键字的查找依据。

  3. 建立正排和倒排索引?什么是正排索引?什么是倒排索引?

    答:正排索引如下(文档id和正排文档的映射关系)。

    文档id正排文档(DocInfo)
    1正排文档1
    2正排文档2
    3正排文档3
    4正排文档4

    倒排索引如下(关键字和倒排拉链的映射关系):

    关键字倒排拉链(Inverted_List)
    倒排文档1、倒排文档2、倒排文档3
    不吃倒排文档2
    香菜倒排文档1、倒排文档3
    struct DocInfo//正排文档
    {int id;//文档id,这个作用后续会解答。std::string title;//文档标题std::string content;//文档内容,这里的内容指的是全html文本去掉标签后的字符串,如:<head>你好<head/>去掉之后就是"你好”std::string url;//文档url
    }struct Inverted_ele//倒排文档
    {int id;//文档iddouble weight;//关键字在该文档的权重std::string word;//关键字,这个作用后续会解答
    }typedef std::vector<Inverted_ele> Inverted_List;//倒排拉链
    

    如果我们需要查找这样的一句话:我必须全力守卫我的祖国

    根据分词工具分解成以下关键字:“我”、“必须”、“全”、“力”、“全力”、“守卫”、“我的祖国”、“我的”、“祖国”。

    具体分解成什么样的关键字,我们不需要关心,这是分词工具做的事情,本项目只使用分词工具,不设计原理细节。

    接下来,只需要把这些关键字对应的倒排拉链(Inverted_List)全部组合到一起,组合方式:相同的倒排文档权重相加,然后放到一个新的Inverted_List中。并进行权重降序排序,然后就可以通过倒排文档(Inverted_ele)找到对应的正排文档(DocInfo),即找到一个个(title、content、url),然后将一个个正排文档序列化组成字符串返回给客户端。 注:序列化:将结构体数据转化为字符串。

6、项目编写

6.1 获取数据源
  1. 第一步:从Boost官网下载文档网页集合,然后用rz命令上传至服务器。

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-uuQfoSW6-1677284957534)(BoostSearcher搜索引擎项目.assets/image-20230223195604905.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MtIauYA7-1677284957535)(BoostSearcher搜索引擎项目.assets/image-20230223195649646.png)]

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-q0D5YVAo-1677284957535)(BoostSearcher搜索引擎项目.assets/image-20230223202934406.png)]

​		[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-vdZ5cb84-1677284957536)(BoostSearcher搜索引擎项目.assets/image-20230223203253802.png)]

​ 我们只需要html中的.html文件,其他的不需要。

在这里插入图片描述

​ 将html目录下的所有文件拷贝到raw目录下。

在这里插入图片描述

提取raw下的所有HTML文件为一个个DocInfo结构体,用\3分割:title\3content\3url,再用\n分割DocInfo结构体,最后组成一个大文本文件。

struct DocInfo
{std::srting title;std::string content;std::string url;
}

大文本文件内容简述:title\3content\3url \n title\3content\3url \n …(这里我用二进制写文件,不用二进制写文件也行)

编写解析代码:parser.cc

***第一步搭建框架***

#include<iostream>
#include<string>
#include<vector>
#include<boost/filesystem.hpp>
#include"util.hpp"const std::string src_path = "./data/input";//源目录
const std::string raw_path = "./data/raw/raw.txt";//解析源目录下的所有.html文件,组成一个raw.txt大文本文件typedef struct DocInfo{std::string title;//标题std::string content;//内容std::string url;//url
}DocInfo_t;int main()
{std::vector<std::string> filenames;std::vector<DocInfo_t> results;//将src_path目录下的所有.html普通文件全部列举到filenames容器中if(!EnumFile(src_path,filenames)){std::cerr<<"EnumFile() error"<<std::endl;return 1;}   //读取一个个filenames中的带路径的文件名,进行解析成一个个DocInfo_t结构体保存到results容器中if(!ParseHtml(filenames,results)){std::cerr<<"ParseHtml() error"<<std::endl;return 2;}//将results容器中的一个个DocInfo结构体保存到raw_path中,DocInfo的成员变量之间用\3分隔,DocInfo用\n分割。//如:itle\3content\3url\ntitle\3content\3url\n//为什么用\n当DocInfo结构体的分割符?\3当DocInfo成员变量的分隔符?//答:为了之后读取方便,后续我们可以用getline(cin,s)可以非常方便读取一个完整的DocInfo,选择\3主要它是一个不可显字符,html文本中一般不会含有该字符。//注意:getline不读取\n到s中,同时会去掉缓冲区中的\n。if(!SaveHtml(results,raw_path)){std::cerr<<"SaveHtml() error"<<std::endl;return 3;}return 0;
}

接下来只需要完成EnumFile()、ParseHtml()、SaveHtml()函数的编写

  1. EnumFIle()函数

    由于C++的文件操作不适应递归遍历目录文件,因此我选择使用Boost文件系统库。
    安装boost库命令:sudo yum install -y boost-devel
    如何使用Boost文件系统库呢?
    这里只对用到的函数代码做解释,具体学习还需要到Boost官网看文档学习。


bool EnumFile(const std::string& src_path,std::vector<std::string>& filenames)
{namespace fs = boost::filesystem;//引用命名空间。fs::path root_path(src_path);//定义path变量,用src_path初始化。if(!fs::exists(root_path))//判断root_path目录是否存在,即判断src_path目录是否存在。{std::cerr<<src_path<<"is not exists"<<std::endl;return false;}fs::recursive_directory_iterator end;//迭代器的结束位置。for(fs::recursive_directory_iterator iter(root_path);iter != end;iter++)//迭代器遍历。{if(!fs::is_regular_file(*iter))//判断是不是普通文件。{continue;//如果是目录,则直接判断下一个。}if(iter->path().extension() != ".html")//判断普通文件的后缀是不是.html。{continue;//后缀不满足,判断下一个。}//得到了一个带路径的html普通文件。filenames.push_back(iter->path().string());}return true; 
}

​ 2.ParseHtml()函数

​ 该函数的作用是:读取一个个带路径的html文件,然后解析提取为一个个DocInfo结构体保存到results容器中(vector)。

ParseHtml()函数基本框架:

bool ParseHtml(const std::vector<std::string>& filenames,std::vector<DocInfo_t>& results)
{for(std::string file:filenames)//遍历读取带路径的html文件名{std::string result;//读取html文件内容到result中if(!ns_util::FileUtil::ReadFile(file,&result))//由于可能多个地方需要读取文件,于是将ReadFile写到了一个工具类{continue;}DocInfo_t doc;//提取html文件的内容到doc结构体中if(!ParseTitle(result,&doc.title))//提取标题,需要的是result。{continue;}if(!ParseContent(result,&doc.content))//提取内容,需要的是result。{continue;}if(!ParseUrl(file,&doc.url))//提取url,需要的是html文件路径名。{continue;}results.push_back(std::move(doc));//doc插入到results容器中,使用move调用移动赋值,减少拷贝。}return true;
}

util.hpp中ReadFIle()的编写

#pragma once 
#include<string>
#include<iostream>
#include<fstream>
#include<vector>
#include<boost/algorithm/string.hpp>
#include<unordered_map>namespace ns_util
{namespace FileUtil{static bool ReadFile(const std::string file,std::string* result){std::ifstream ifs(file,std::ios::in);if(!ifs.is_open()){std::cerr<<"file open error"<<std::endl;return false;}std::string line;while(getline(ifs,line)){*result += line;}return true;}}
}

接下来依次完成ParseTitle()、ParseContent()、ParseUrl()编写。

ParseTitle():提取标题

static bool ParseTitle(const std::string& result,std::string* title)
{//比如解析:<title>你是个好人</title>std::size_t begin = result.find("<title>");if(begin == std::string::npos){return false;}begin += std::string("<title>").size();std::size_t end = result.find("</title>");if(end == std::string::npos){return false;}if(begin > end){return false;}*title = result.substr(begin,end-begin);return true;
}

ParseContent():提取内容

static bool ParseContent(const std::string& result,std::string* content)
{//简易的状态机,读一读代码很好理解enum status{LABLE,CONTENT };status s = LABLE;for(char c : result){switch(s){case LABLE:if(c == '>')s = CONTENT;break;case CONTENT:if(c == '<')s = LABLE;else {if(c == '\n')//这里我把html文本中的所有'\n'全部替换成了' ',就是为了用getline后面读取一个个DocInfo结构体方便,所以需要替换掉'\n',选择' '可以,选择'\4'可以。但'\3'不行,会和DocInfo成员变量的SEP分隔符冲突。c = ' ';*content += c;}  break;default:break;}}return true;
}

ParseUrl():提取Url

static bool ParseUrl(const std::string& file,std::string* url)
{std::string url_head = "https://www.boost.org/doc/libs/1_81_0/doc/html";//官方的文档访问路径std::string url_end = file.substr(src_path.size());//因为file是带路径的文件名,需要把它前面的本地路径去掉,换上官网的路径。*url = url_head + url_end;return true;//注:搜索引擎不存储站点网站,只是将搜索结果的url返回,告诉你该访问哪个网站。
}
  1. SaveHtml()函数:保存results的内容为一个大文本文件。
bool SaveHtml(const std::vector<DocInfo_t>& results,const std::string raw_path)
{
#define SEP '\3'//DocInfo成员函数分隔符,如:title\3content\3urlstd::ofstream ofs(raw_path,std::ios::out | std::ios::binary);//二进制写打开文件if(!ofs.is_open()){std::cerr<<raw_path<<"open error"<<std::endl;return false;}for(auto& item : results)//遍历results中的一个个DocInfo结构体{std::string out_string;out_string += item.title;out_string += SEP;out_string += item.content;out_string += SEP;out_string += item.url;out_string += '\n';ofs.write(out_string.c_str(),out_string.size());//将out_string写入打开的文件}ofs.close();return true;
}

至此,完成了一个文件的清洗,最后得到了一个包含若干个DocInfo字符串的大文本文件。

6.2 建立正排和倒排索引

本项目终于迎来了它的核心部分:正排索引和倒排索引

第一步:建立正排索引

我们前面简述过了一遍正排索引的原理,这里我再次分析一遍。

文档id文档(DocInfo)
1文档1
2文档2
3文档3
4文档4

正排索引即文档id与文档DocInfo的映射关系,这里的DocInfo结构体为:

struct DocInfo//正排文档
{int id;//文档id,这个作用后续会解答。std::string title;//文档标题std::string content;//文档内容,这里的内容指的是全html文本去掉标签后的字符串,如:<head>你好<head/>去掉之后就是"你好”std::string url;//文档url
}

注意与parser.cc里面的DocInfo结构体区分,这里多了一个id,作用会后续解答。

倒排索引即关键字与倒排拉链的映射关系。

什么是关键字?

如果一句话为:”我不吃香菜“

那么关键字可以为:“我”、“不吃”、“香菜”(具体取决于分词工具)

关键字倒排拉链(Inverted_List)
倒排文档1、倒排文档2、倒排文档3
不吃倒排文档2
香菜倒排文档1、倒排文档3

倒排拉链是什么?

struct Inverted_ele//倒排文档
{int id;//文档iddouble weight;//关键字在该文档的权重std::string word;//关键字,这个作用后续会解答
}typedef std::vector<Inverted_ele> Inverted_List;//我称它为倒排拉链

倒排拉链即 std::vector<Inverted_ele>

如果用户搜索“我不吃”,那么分词工具大概率分为:“我”、“不吃”。

然后查找倒排索引,找到各个关键字对应的倒排拉链,就是:

(倒排文档1、倒排文档2、倒排文档3)

(倒排文档2)

再合并成一个新的倒排拉链(倒排文档1、倒排文档2、倒排文档3),相同的倒排文档权重需要相加,因为倒排文档2中既有关键字“我”也有“不吃”,权重需要相加。

然后通过倒排文档中的id查正排索引,找到对应的正排文档(正排文档1、正排文档2、正排文档3)。

然后将各个正排文档由结构体转化为字符串组合成一个Json字符串返回(由于内存对齐等平台原因,网络上直接发送结构体二进制数据容易出错。),这里需要用序列化与反序列化工具:Json、Xml、Protobuf,我选择的是Json。

说了这么多了,现在开始完成索引的建立

索引框架的编写:

#pragma once 
#include<iostream>
#include<string>
#include<vector>
#include<unordered_map>
#include<fstream>
#include"util.hpp"
#include<mutex>namespace ns_index
{struct ForwardElem//正排文档{std::string title;//标题std::string content;//内容std::string url;//urluint64_t id;//正排文档id};struct InvertedElem//倒排文档{uint64_t id;//正排文档idint weight;//文档权重std::string word;//关键字};typedef std::vector<InvertedElem> InvertedList;//倒排拉链class Index{private:std::vector<ForwardElem> forward_index;//正排索引std::unordered_map<std::string,InvertedList> inverted_index;//倒排索引public:Index(){};~Index(){};public:ForwardElem* GetForwardIndex(uint64_t id)//根据文档id获取正排文档,函数1{}InvertedList* GetInveredList(std::string& word)//根据关键字获取倒排拉链,函数2{}bool BuildIndex(const std::string& raw_path)//根据parser.cc得到的大文本文件,建立正排和倒排索引,函数3{}private:ForwardElem* BuildForwardIndex(const std::string& line)//建立正排索引,函数4{}bool BuildInvertedIndex(const ForwardElem& fe)//建立倒排索引,函数5{}};
}

现在剩下的是我们需要完成对上面五个函数代码编写。

先来GetForwardIndex()函数:通过文档id获取正排文档

 ForwardElem* GetForwardIndex(uint64_t id){if(id > forward_index.size())//防止越界{std::cerr<<"id index beyond range"<<std::endl;return nullptr;}return &forward_index[id];//这里需要用返回值来建立倒排索引,后面会用到该返回值。}

编写GetInveredList()函数:通过关键字获取倒排拉链

InvertedList* GetInveredList(std::string& word)
{auto iter = inverted_index.find(word);//查找关键字是否被建立倒排索引,iter类型为pair<bool,>if(iter == inverted_index.end())//关键字不在倒排索引内{std::cerr<<"this word not exists invertedlist"<<std::endl;return nullptr;}return &iter->second;//返回倒排拉链
}

接下来写BuildIndex()函数:建立索引(正排索引和倒排索引)

bool BuildIndex(const std::string& raw_path)//parser.cc处理后保存的大文本文件的路径
{std::ifstream ifs(raw_path,std::ios::in | std::ios::binary);//保存文件时是按照二进制方式的if(!ifs.is_open()){std::cerr<<raw_path<<"open is error"<<std::endl;return false;}int count = 0;std::string line;while(std::getline(ifs,line))//按行读取一个个DocInfo序列化的字符串:title\3content\3url。{ForwardElem* fe = BuildForwardIndex(line);//根据读取的字符串建立正排索引。if(fe == nullptr)//建立失败{std::cerr<<"build "<<line<<" error"<<std::endl;continue;}BuildInvertedIndex(*fe);//根据正排文档建立倒排索引count++;std::cout<<"当前已建立的索引: "<<std::to_string(count)<<std::endl;//为了观察建立索引的进度}return true;
}

最后完成BuildForwardIndex()函数和BuildInvertedIndex()函数就大功告成了。

BuildForwardIndex()函数

BuildForwardIndex函数的主要目的是将title\3content\3url这样的字符串转化为DocInfo这样的结构体,所以需要采用字符串截取操作,可以使用find()+ substr(),但比较麻烦。不推荐使用strtok,因为它会改变原始字符串。这里我选择使用Boost库中的split函数。

我把split函数写到工具类(utill.hpp)中:

namespace StringUtil
{static void Split(const std::string line,std::vector<std::string>* result,const std::string& sep){boost::split(*result,line,boost::is_any_of(sep),boost::token_compress_on);}
}

这里的split第三个参数是处理特殊情况:比如title\3\3\3\3\3\3content\3url

如果写为boost::token_compress_off分隔结果中会有多个空串,因此强烈建议使用token_compress_on

ForwardElem* BuildForwardIndex(const std::string& line)
{//line:title\3content\3\urlstd::vector<std::string> results;const std::string sep = "\3";ns_util::StringUtil::Split(line,&results,sep);//根据sep切分字符串。//results为:[0]title、[1]content、[2]urlif(results.size() != 3)//不为3则出错处理{return nullptr;}ForwardElem fe;fe.title = results[0];fe.content = results[1];fe.url = results[2];fe.id = forward_index.size();//一开始size()为0forward_index.push_back(std::move(fe));//move是减少拷贝的操作return &forward_index.back();//将fe返回给倒排索引,建立倒排索引。//注意:这里不能用&fe,因为上面用了move(),这是一个大坑
}

编写BuildInvertedIndex()函数

该函数的需要对DocInfo正排文档进行title和content的分词来建立倒排索引,这里我使用的是cppjieba分词。

第一步:

安装cppjieba网址:https://github.com/yanyiwu/cppjieba

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TXXTRAmp-1677284957538)(BoostSearcher搜索引擎项目.assets/image-20230224164636720.png)]

然后git clone下来即可

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UW1K7Hi9-1677284957538)(BoostSearcher搜索引擎项目.assets/image-20230224165137692.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Msi1eE1O-1677284957539)(BoostSearcher搜索引擎项目.assets/image-20230224165208995.png)]

其中include/cppjieba包含我们需要的.h头文件,但这里有个小bug,需要将deps/limon目录拷贝到include/cppjieba目录下才能正常使用。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5rIfTvxL-1677284957540)(BoostSearcher搜索引擎项目.assets/image-20230224165449159.png)]

现在我们开始使用cppjieba中的test文件进行小测试

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DqKBLBm3-1677284957541)(BoostSearcher搜索引擎项目.assets/image-20230224165631541.png)]

用vim打开这个demo.cpp看看情况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-m4alw5we-1677284957541)(BoostSearcher搜索引擎项目.assets/image-20230224165718546.png)]

由于我把demo.cpp拷贝到了另外一个目录,这里面使用的相对路径就都失效了,需要我们解决。同时还需要包含、等头文件。

如何解决相对路径失效的问题?

答案:使用软连接解决。

如下:
在这里插入图片描述

在这里插入图片描述

然后就可以通过g++编译运行demo.cpp了
在这里插入图片描述

搞定cppjieba分词组件后,我们需要把它放到一个指定路径下,作为项目的第三方库路径,这样项目看起来更工程点。

比如~/third-lib

使用时通过软连接引用即可。

接下来我们需要用cppjieba完成一个易于调用的分词函数,我把它写到了工具类中。

namespace JiebaUtil
{//下面5行都是用于分词的词库const char* const DICT_PATH = "./dict/jieba.dict.utf8";const char* const HMM_PATH = "./dict/hmm_model.utf8";const char* const USER_DICT_PATH = "./dict/user.dict.utf8";const char* const IDF_PATH = "./dict/idf.utf8";const char* const STOP_WORD_PATH = "./dict/stop_words.utf8";cppjieba::Jieba jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH);std::unordered_map<std::string, bool> stop_words;void CutString(const std::string& line,std::vector<std::string>* out)//完成分词函数{jieba.Cut(line,*out);}
}

以后我们只需要调用工具类中的CutString即可完成分词。

分词工具搞定后,接下来我们需要完成的是BuildInvertedIndex函数的编写,这个函数是重点中的难点。

bool BuildInvertedIndex(const ForwardElem& fe)
{struct word_cnt//记录某个关键字在title中出现次数和在content中出现次数。{int title;int content;word_cnt():title(0),content(0){};};std::unordered_map<std::string,word_cnt> word_map;//词频统计//对title进行分词std::vector<std::string> title_words;ns_util::JiebaUtil::CutString(fe.title,&title_words);//统计title词频for(auto word : title_words)//这里需要用传值,否则会改变title_words{boost::to_lower(word); //将word转化为小写,全部建立小写的索引,查询时转换成小写再查询索引word_map[word].title++;}//对content进行分词std::vector<std::string> content_words;ns_util::JiebaUtil::CutString(fe.content,&content_words);//统计content词频for(auto word : content_words){boost::to_lower(word);//将word转化为小写,全部建立小写的索引,查询时转换成小写再查询索引word_map[word].content++;}#define X  10
#define Y  1for(auto& word : word_map){InvertedElem ie;//构建倒排文档ie.id = fe.id;//这是为什么DocInfo结构体为什么有id成员变量的原因,方便构建InvertedElemie.weight = X * word.second.title + Y * word.second.content;//权重设置ie.word = word.first;//设置关键字InvertedList& iel = inverted_index[ie.word]; //将相同关键字的倒排文档插入至倒排拉链iel.push_back(std::move(ie));//构建关键字和倒排拉链的映射}return true;
}

终于完成了Index.hpp的编写,但还有一点完善的是,为了项目的工程性,我们需要把index.hpp设置成单例模式

#pragma once #include<iostream>
#include<string>
#include<vector>
#include<unordered_map>
#include<fstream>
#include"util.hpp"
#include<mutex>namespace ns_index
{struct ForwardElem{std::string title;std::string content;std::string url;uint64_t id;};struct InvertedElem{uint64_t id;int weight;std::string word;};typedef std::vector<InvertedElem> InvertedList;class Index{private:std::vector<ForwardElem> forward_index;std::unordered_map<std::string,InvertedList> inverted_index;private:static Index* instance;static std::mutex mtx;Index(){};Index(const Index&) = delete;Index& operator=(const Index&) = delete;public:~Index(){};public:static Index* GetInstance(){if(instance == nullptr){mtx.lock();if(instance == nullptr){instance = new Index();}mtx.unlock();}return instance;}ForwardElem* GetForwardIndex(uint64_t id){if(id > forward_index.size()){std::cerr<<"id index beyond range"<<std::endl;return nullptr;}return &forward_index[id];}InvertedList* GetInveredList(std::string& word){auto iter = inverted_index.find(word);if(iter == inverted_index.end()){std::cerr<<"this word not exists invertedlist"<<std::endl;return nullptr;}return &iter->second;}bool BuildIndex(const std::string& raw_path){std::ifstream ifs(raw_path,std::ios::in | std::ios::binary);if(!ifs.is_open()){std::cerr<<raw_path<<"open is error"<<std::endl;return false;}int count = 0;std::string line;while(std::getline(ifs,line)){ForwardElem* fe = BuildForwardIndex(line);if(fe == nullptr){std::cerr<<"build "<<line<<" error"<<std::endl;continue;}BuildInvertedIndex(*fe);count++;std::cout<<"当前已建立的索引: "<<std::to_string(count)<<std::endl;;}return true;}private:ForwardElem* BuildForwardIndex(const std::string& line){std::vector<std::string> results;const std::string sep = "\3";ns_util::StringUtil::Split(line,&results,sep);if(results.size() != 3){return nullptr;}ForwardElem fe;fe.title = results[0];fe.content = results[1];fe.url = results[2];fe.id = forward_index.size();forward_index.push_back(std::move(fe));return &forward_index.back();//注意:这里不能用&fe,因为上面用了move()}bool BuildInvertedIndex(const ForwardElem& fe){struct word_cnt{int title;int content;word_cnt():title(0),content(0){};};std::unordered_map<std::string,word_cnt> word_map;//对title进行分词std::vector<std::string> title_words;ns_util::JiebaUtil::CutString(fe.title,&title_words);//统计title词频for(auto word : title_words){boost::to_lower(word); word_map[word].title++;}//对content进行分词std::vector<std::string> content_words;ns_util::JiebaUtil::CutString(fe.content,&content_words);//统计content词频for(auto word : content_words){boost::to_lower(word);word_map[word].content++;}#define X  10
#define Y  1for(auto& word : word_map){InvertedElem ie;ie.id = fe.id;ie.weight = X * word.second.title + Y * word.second.content;ie.word = word.first;InvertedList& iel = inverted_index[ie.word]; iel.push_back(std::move(ie));}return true;}};Index* Index::instance = nullptr;std::mutex Index::mtx;
}
6.3为用户提供搜索服务

思路:先写本地的Search服务,再转为网络服务。

本地服务:Search.hpp

#pragma once #include<iostream>
#include<string>
#include<vector>
#include<unordered_map>
#include"index.hpp"
#include<jsoncpp/json/json.h>namespace ns_searcher
{struct InvertedInfoPrint{uint64_t id;int weight;std::vector<std::string> words;InvertedInfoPrint():id(-1),weight(0){};};class Searcher{private:ns_index::Index* index;//索引服务public:void InitSearcher(const std::string& input)//初始化Searcher{index = ns_index::Index::GetInstance();std::cout<<"获取单例成功!"<<std::endl;index->BuildIndex(input);std::cout<<"构建索引成功!"<<std::endl;}void Search(const std::string& query,std::string* json_string)//根据搜索内容,返回Json字符串{//对query进行分词std::vector<std::string> words;ns_util::JiebaUtil::CutString(query,&words);//对query进行分词std::unordered_map<uint64_t,InvertedInfoPrint> for_unique;//对倒排文档去重而创建std::vector<InvertedInfoPrint> InvertedList_all;//新的倒排拉链for(auto& word : words){boost::to_lower(word);//查找索引前需要转为小写,因为索引的关键字都是小写的ns_index::InvertedList* list = index->GetInveredList(word);//获取倒排拉链if(list == nullptr){continue;}for(auto& item : *list)//对组合的倒排拉链进行去重{InvertedInfoPrint& ele = for_unique[item.id];ele.id = item.id;ele.weight += item.weight;//相同文档,权重相加ele.words.push_back(item.word);}}for(const auto& e : for_unique)//得到新的倒排拉链{InvertedList_all.push_back(std::move(e.second));//}std::sort(InvertedList_all.begin(),InvertedList_all.end(),\[](const InvertedInfoPrint& e1,const InvertedInfoPrint& e2){return e1.weight > e2.weight;});//对新的倒排拉链进行权重降序排序//完成序列化与反序列化Json::Value root;for(auto& e : InvertedList_all)//根据倒排文档得到正排文档{ns_index::ForwardElem* fe = index->GetForwardIndex(e.id);//查找正排索引if(fe == nullptr){continue;}Json::Value ele;ele["title"] = fe->title;ele["desc"] = GetDesc(fe->content,e.words[0]);//根据内容得到摘要,这里是为什么InvertedEle中有word成员变量的原因。ele["url"] = fe->url;root.append(ele);}Json::FastWriter w;*json_string = w.write(root);}//摘要设计:找到word出现在content的第一个位置,向前找50个字符,再向后找100个字符构成摘要。如果前面不够50个,则从0下标开始截取。如果后面不够100个,则截取到结尾。std::string GetDesc(const std::string& content,const std::string& word){const int prev = 50;const int next = 100;//这里有一个大坑,不能使用string.find()查找,因为它不能忽略大小写查找。如果你查找的是filesystem,则找到的只有filesystem。auto iter = std::search(content.begin(),content.end(),word.begin(),word.end(),\[](int x,int y){return std::tolower(x) == std::tolower(y);});if(iter == content.end()){return "None1";}int pos = std::distance(content.begin(),iter);//得到找到位置的下标int start = 0;int end = content.size()-1;if(pos-prev > start){start = pos-prev;}if(pos + next < end){end = pos + next;}if(start > end){return "None2";}std::string desc = content.substr(start,end-start);desc += "....";return desc;}};
}

提供本地服务:

#include"searcher.hpp"const std::string input = "./data/raw/raw.txt";
char buf[256] = {0};
std::string query;
std::string json_string;int main()
{ns_searcher::Searcher s;s.InitSearcher(input);std::cout<<"Please Enter Your Query# ";fgets(buf,sizeof(buf)-1,stdin);//注:fgets会读取\n给buf。buffer[strlen(buffer)-1] = 0;//去掉buf里面的\nquery = buf;s.Search(query,&json_string);std::cout<<json_string<<std::endl;
}

提供网络服务:

这里使用现成的http服务:cpphttp-lib

编写http_server.cc

#include"searcher.hpp"
#include"cpp-httplib/httplib.h"
#include<string>
#include"log.hpp"const std::string input =  "./data/raw/raw.txt";
const std::string root_path = "./www.root";int main()
{ns_searcher::Searcher searcher;//创建serachersearcher.InitSearcher(input);//初始化searcherdaemonize();//将服务守护进程化Log log;log.enable();//设置日志httplib::Server svr;svr.set_base_dir(root_path.c_str());//设置web根目录svr.Get("/s",[&searcher](const httplib::Request& req,httplib::Response& res){if(!req.has_param("word"))//查看url是否携带word{res.set_content("必须要有搜索关键字","text/plain; charset = utf-8");//响应报文return;}std::string word = req.get_param_value("word");//获取word的value值std::string json_string;searcher.Search(word,&json_string);//获取Json字符串res.set_content(json_string,"application/json");//将Json串返回给客户端});std::cout<<"服务启动成功!!"<<std::endl;svr.listen("0.0.0.0",8081);//绑定和监听return 0;
}

将http_server.cc守护进程化

#pragma once#include <cstdio>
#include <iostream>
#include <signal.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>void daemonize()
{int fd = 0;// 1. 忽略SIGPIPEsignal(SIGPIPE, SIG_IGN);// 2. 更改进程的工作目录// chdir();// 3. 让自己不要成为进程组组长if (fork() > 0)exit(1);// 4. 设置自己是一个独立的会话setsid();// 5. 重定向0,1,2if ((fd = open("/dev/null", O_RDWR)) != -1) // fd == 3{dup2(fd, STDIN_FILENO);dup2(fd, STDOUT_FILENO);dup2(fd, STDERR_FILENO);// 6. 关闭掉不需要的fdif(fd > STDERR_FILENO) close(fd);}
}
6.4编写前端代码

这里的前端代码看看就行,主要是为了展示项目。

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><script src="http://code.jquery.com/jquery-2.1.1.min.js"></script><title>boost 搜索引擎</title><style>* {margin: 0;padding: 0;}html,body {height: 100%;}.container {width: 800px;margin: 0px auto;margin-top: 15px;}.container .search {width: 100%;height: 52px;}.container .search input {float: left;width: 600px;height: 50px;border: 1px solid black;border-right: none;padding-left: 10px;color: #CCC;font-size: 14px;}.container .search button {float: left;width: 150px;height: 52px;background-color: #4e6ef2;color: #FFF;font-size: 19px;font-family:Georgia, 'Times New Roman', Times, serif;}.container .result {width: 100%;}.container .result .item {margin-top: 15px;}.container .result .item a {display: block;text-decoration: none;font-size: 20px;color: #4e6ef2;}.container .result .item a:hover {text-decoration: underline;}.container .result .item p {margin-top: 5px;font-size: 16px;font-family:'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;}.container .result .item i{display: block;font-style: normal;color: green;}</style>
</head>
<body><div class="container"><div class="search"><input type="text" value="请输入搜索关键字"><button onclick="Search()">搜索一下</button></div><div class="result"></div></div><script>function Search(){let query = $(".container .search input").val();console.log("query = " + query); //console是浏览器的对话框,可以用来进行查看js数据//2. 发起http请求,ajax: 属于一个和后端进行数据交互的函数,JQuery中的$.ajax({type: "GET",url: "/s?word=" + query,success: function(data){console.log(data);BuildHtml(data);}});}function BuildHtml(data){// 获取html中的result标签let result_lable = $(".container .result");// 清空历史搜索结果result_lable.empty();for( let elem of data){// console.log(elem.title);// console.log(elem.url);let a_lable = $("<a>", {text: elem.title,href: elem.url,// 跳转到新的页面target: "_blank"});let p_lable = $("<p>", {text: elem.desc});let i_lable = $("<i>", {text: elem.url});let div_lable = $("<div>", {class: "item"});a_lable.appendTo(div_lable);p_lable.appendTo(div_lable);i_lable.appendTo(div_lable);div_lable.appendTo(result_lable);}}</script>
</body>
</html>
6.5日志的补充

编写log.hpp

#pragma once#include <cstdio>
#include <ctime>
#include <cstdarg>
#include <cassert>
#include <cassert>
#include <cstring>
#include <cerrno>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>#define DEBUG 0
#define NOTICE 1
#define WARINING 2
#define FATAL 3const char *log_level[] = {"DEBUG", "NOTICE", "WARINING", "FATAL"};#define LOGFILE "serverTcp.log"class Log
{
public:Log():logFd(-1){}void enable(){umask(0);logFd = open(LOGFILE, O_WRONLY | O_CREAT | O_APPEND, 0666);assert(logFd != -1);dup2(logFd, 1);dup2(logFd, 2);}~Log(){if(logFd != -1) {fsync(logFd);close(logFd);}}
private:int logFd;
};// logMessage(DEBUG, "%d", 10);
void logMessage(int level, const char *format, ...)
{assert(level >= DEBUG);assert(level <= FATAL);char *name = getenv("USER");char logInfo[1024];va_list ap; va_start(ap, format);vsnprintf(logInfo, sizeof(logInfo) - 1, format, ap);va_end(ap); // ap = NULLFILE *out = (level == FATAL) ? stderr : stdout;fprintf(out, "%s | %u | %s | %s\n",log_level[level],(unsigned int)time(nullptr),name == nullptr ? "unknow" : name,logInfo);fflush(out); // 将C缓冲区中的数据刷新到OSfsync(fileno(out));   // 将OS中的数据尽快刷盘
}
6.6项目扩展
  1. 建立全站搜索
  2. 设计一个在线更新的方案,信号,爬虫,完成整个服务器的设计
  3. 不使用组件,而是自己设计一下对应的各种方案
  4. 在我们的搜索引擎中,添加竞价排名
  5. 热次统计,智能显示搜索关键词(字典树,优先级队列)
  6. 设置登陆注册,引入对mysql的使用

相关文章:

BoostSearcher搜索引擎项目

BoostSearcher搜索引擎项目 1.BoostSearcher这个项目是什么&#xff1f; 答&#xff1a;一个为Boost文档建立索引的站内搜索引擎&#xff0c;简单的说就是一个类似于csdn站内文档搜索框。 项目展示&#xff1a; gitee:https://gitee.com/zxlfx/boost-search-engine-project …...

【模拟集成电路】频率综合器(Frequency Synthesizer,FS)设计

应用于无线局域网的频率综合器设计前言频率综合器简介各部分链接链接&#xff1a;前言 本文主要内容是对频率综合器或称为PLL 做出简单介绍&#xff0c;为课程设计部分章节内容&#xff0c;后需给出各部分的设计方案&#xff0c;以及测试结果。 频率综合器简介 无线收发系统中…...

实例8:机器人的空间描述和变换仿真

实例8&#xff1a;机器人的空间描述和变换仿真 实验目的 通过刚体与刚体的平动、转动基础知识的学习&#xff0c;熟悉位姿的描述通过Python编程实践&#xff0c;可视化学习坐标系的变换&#xff0c;熟悉空间变换 实验要求 通过python编程&#xff0c;输入一指定向量和对应的…...

网络 导航

目录内容链接网络网络参考文章&#xff1a;【网络】http请求 调试网络问题解决参考文章&#xff1a;【问题解决】网络 IP DNS解析网络问题解决参考文章&#xff1a;【问题解决】网络 TCP 规则 抓包网络问题解决参考文章&#xff1a;【问题解决】网络 Http请求 调试网络问题解决…...

Web Spider Ast-Hook 浏览器内存漫游-数据检索

文章目录一、资源下载二、通过npm安装anyproxy模块三、anyproxy的介绍以及基本使用1. anyproxy的功能介绍2. anyproxy的基本使用四、给浏览器挂代理五、实操极验demo案例总结提示&#xff1a;以下是本篇文章正文内容&#xff0c;下面案例可供参考 一、资源下载 Github&#x…...

计算机网络笔记、面试八股(二)——HTTP协议

本章目录2. HTTP协议2.1 HTTP协议简介2.2 HTTP协议的优点2.3 HTTP协议的缺点2.4 HTTP协议属于哪一层2.5 HTTP通信过程2.6 常见请求方法2.7 GET和POST的区别2.8 请求报文与响应报文2.8.1 HTTP请求报文2.8.2 HTTP响应报文2.9 响应状态码2.10 HTTP 1.0和1.1的区别2.10.1 长连接2.1…...

docker快速上手使用

文章目录一、docker概述1. 为什么需要docker2. 什么是docker3. docker和虚拟机的区别4. docker三要素二、docker安装1. 添加app源2. 安装docker社区版3. 更换国内docker镜像源三、docker基本使用方法1. 获取镜像2. 查看当前系统中的docker镜像3. 运行docker容器4. 查看当前存在…...

<c++> 类的构造函数与类的析构函数

文章目录类的构造函数什么是构造函数声明和定义构造函数如何使用构造函数默认构造函数类的析构函数什么是析构函数声明和定义析构函数小练习银行账户执行效果类的构造函数 什么是构造函数 Q&#xff1a;什么是类的构造函数 A&#xff1a;构造函数是类的一种特殊成员函数,不需…...

华为OD机试真题Java实现【玩牌高手】真题+解题思路+代码(20222023)

玩牌高手 给定一个长度为n的整型数组,表示一个选手在n轮内可选择的牌面分数。选手基于规则选牌, 请计算所有轮结束后其可以获得的最高总分数。 选择规则如下: 1、在每轮里选手可以选择获取该轮牌面,则其总分数加上该轮牌面分数,为其新的总分数。 2、选手也可不选择本轮…...

Hive Sql整体优化思路

如果遇到sql性能问题&#xff0c;可以先查看4040页面的sql执行信息。一个sql解析为多个stage&#xff0c;一个stage分为多个task。对问题Sql的某一个stage&#xff0c;基本的分析思路如下&#xff1a;所有的task都慢&#xff0c;检查下是否有笛卡尔积(关联字段重复值、关联字段…...

【华为OD机试模拟题】用 C++ 实现 - 数组的中心位置(2023.Q1)

最近更新的博客 华为OD机试 - 入栈出栈(C++) | 附带编码思路 【2023】 华为OD机试 - 箱子之形摆放(C++) | 附带编码思路 【2023】 华为OD机试 - 简易内存池 2(C++) | 附带编码思路 【2023】 华为OD机试 - 第 N 个排列(C++) | 附带编码思路 【2023】 华为OD机试 - 考古…...

取指定数值的地址 (int 转 void *)

int a 0x12345678 是一个地址void *p (void *)a; 提示下马错误&#xff1b;Error: cast to pointer from integer of different size [-Werrorint-to-pointer-cast]This error occurs when there is an attempt to convert an integer to a pointer of a different size. Thi…...

C#的多线程、线程池和Task

线程 被定义为程序的执行路径。每个线程都定义了一个独特的控制流。如果您的应用程序涉及到复杂的和耗时的操作&#xff0c;那么设置不同的线程执行路径往往是有益的&#xff0c;每个线程执行特定的工作。 线程是轻量级进程。一个使用线程的常见实例是现代操作系统中并行编程的…...

Day20【元宇宙的实践构想06】—— 元宇宙与Web3.0

&#x1f483;&#x1f3fc; 本人简介&#xff1a;男 &#x1f476;&#x1f3fc; 年龄&#xff1a;18 &#x1f91e; 作者&#xff1a;那就叫我亮亮叭 &#x1f4d5; 专栏&#xff1a;元宇宙 部分资料参考文献: 成生辉教授的《元宇宙&#xff1a;概念、技术及生态》和百度相关…...

极限熵和冗余度

本专栏包含信息论与编码的核心知识&#xff0c;按知识点组织&#xff0c;可作为教学或学习的参考。markdown版本已归档至【Github仓库&#xff1a;information-theory】&#xff0c;需要的朋友们自取。或者公众号【AIShareLab】回复 信息论 也可获取。 信息冗余度(多余度、剩余…...

女生学习大数据专业未来前景怎么样

学习大数据与性别没有什么太大关系&#xff0c;各有优势。就目前的发展前景来说&#xff0c;大数据还是非常不错的&#xff0c;至于好不好就业就要看你个人学习的怎么样&#xff0c;以及学历是否过关了~ 据《新职业——大数据工程技术人员就业景气现状分析报告》显示&#xff…...

主题模型实践

目录 一.TF-IDF 二.LSI 三.相似度 四.主题和主题分布 五. LDA计算的相似度 六.LDA过程 七.主题 八.主题和主题分布 九.数据处理流程 十.常用正则表达式 十一.代码 一.TF-IDF 二.LSI 三.相似度 四.主题和主题分布 五. LDA计算的相似度 六.LDA过程 七.主题 八.主题和主…...

按字典序排列的最小的等价字符串[拆解并查集]

并查集前言一、按字典序排列的最小的等价字符串二、并查集总结参考文献前言 并查集有什么用&#xff1f;并查集是什么&#xff1f;搞懂这两个问题&#xff0c;相关的并查集问题就变得非常easy&#xff01; 一、按字典序排列的最小的等价字符串 二、并查集 有一种方法&#x…...

操作系统——6.系统调用

目录 1.概述 2.系统调用的定义和作用 2.1 定义 2.2 功能 2.3 分类 3.系统调用和库函数的区别 4.系统调用背后的过程 5.小结 1.概述 这篇文章我们主要来介绍一下操作系统中的系统调用&#xff0c;下面来看一下具体的框架图&#xff1a; 2.系统调用的定义和作用 2.1 定…...

JavaScript DOM操作

目录 获取元素&#xff1a; 修改元素属性&#xff1a; 添加、删除、替换元素&#xff1a; 修改样式&#xff1a; DOM&#xff08;文档对象模型&#xff09;是一种用于操作 HTML 和 XML 文档的 API。JavaScript 通过 DOM API 可以访问和操作页面中的元素、属性和样式等。 获…...

【数据结构】顺序表

文章目录前言初始化顺序表打印顺序表检查容量判空顺序表数据个数尾部插入尾部删除头部插入头部删除在pos位置插入数据删除pos位置的数据查找数据修改数据销毁顺序表整体代码写在最后前言 顺序表作为数据结构中的小小弟&#xff0c;还是很好应付的。说到数据结构&#xff0c;顺序…...

【人工智能 AI 】RPA 架构师需要具备的技能有哪些?RPA Solution Architect

RPA 架构师需要具备的技能有哪些?使用markdown格式,不少于3000字,细化到3级目录。 文章目录 一、RPA架构师需要具备的技能1. 对RPA的理解2. 对RPA技术的熟练掌握2.1 RPA系统的架构模式2.2 RPA软件的操作模式2.3 RPA程序的编写方式3. 对RPA应用的知识4. 对软件开发的基本知识…...

【模拟集成电路】鉴频鉴相器设计(Phase Frequency Detector,PFD)

鉴频鉴相器设计&#xff08;Phase Frequency Detector&#xff0c;PFD&#xff09;前言一、 PFD的工作原理二、 PFD电路设计&#xff08;1&#xff09;PFD电路图&#xff08;2&#xff09;D触发器电路图&#xff08;3&#xff09;与非门&#xff08;NAND&#xff09;电路图&…...

【Linux】进程间通信介绍 | 管道

​&#x1f320; 作者&#xff1a;阿亮joy. &#x1f386;专栏&#xff1a;《学会Linux》 &#x1f387; 座右铭&#xff1a;每个优秀的人都有一段沉默的时光&#xff0c;那段时光是付出了很多努力却得不到结果的日子&#xff0c;我们把它叫做扎根 目录&#x1f449;进程间通信…...

这次说说腾讯的一场 35K—55K 的 Android 高工面试

一、面试的由来 事情是这样的&#xff0c;因为跟公司发展一些想法的不同&#xff0c;早在十月份的时候就有了跳槽的想法&#xff0c;但是碍于老大的面子就一直就没有跟人事说出口&#xff0c;打算着等到年后金三银四在试试跳槽。 但是发生一件事终于让我忍不住了&#xff0c;…...

Jenkins第一讲

目录 一、Jenkins 1.1 敏捷开发与持续集成 1.1.1 敏捷开发 1.1.2 持续集成 1.2 持续集成工具 1.2.1 jenkins和hudson 1.2.2 技术组合 1.2.3 部署方式对比 1.3 安装Jenkins 1.3.1 下载Jenkins的war包 1.3.2 开启Jenkins 1.4 Jenkins全局安全配置 1.5 使用Jenkins部…...

变分推断 | MATLAB实现VBMC变分贝叶斯蒙特卡洛模拟的贝叶斯推断

变分推断 | MATLAB实现变分贝叶斯蒙特卡洛模拟的贝叶斯推断 目录 变分推断 | MATLAB实现变分贝叶斯蒙特卡洛模拟的贝叶斯推断效果一览基本介绍研究内容模型描述模型设计参考资料效果一览 基本介绍 MATLAB实现变分贝叶斯蒙特卡洛模拟的贝叶斯推断。变分贝叶斯蒙特卡洛(VBMC)是…...

代码随想录【Day25】| 216. 组合总和 III、17. 电话号码的字母组合

216. 组合总和 III 题目链接 题目描述&#xff1a; 找出所有相加之和为 n 的 k 个数的组合。组合中只允许含有 1 - 9 的正整数&#xff0c;并且每种组合中不存在重复的数字。 说明&#xff1a; 所有数字都是正整数。 解集不能包含重复的组合。 示例 1: 输入: k 3, n 7 输…...

web中git漏洞的形成的原理及使用

目录 1.Git漏洞的成因 1.不正确的权限设置&#xff1a; 2.代码注入漏洞&#xff1a; 3.未经身份验证的访问&#xff1a; 4.非安全传输&#xff1a; 5.跨站脚本攻击&#xff08;XSS&#xff09;&#xff1a; 2.git泄露环境的搭建 git init&#xff1a; git add&#xff1…...

【SPSS】单样本T检验分析详细操作教程(附案例实战)

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…...