个人商城网站备案,阿里巴巴logo设计理念,网站建设环境配置,外贸网站推广和建站目录
搜索引擎项目背景
搜索引擎的宏观原理
搜索引擎技术栈和项目环境
搜索引擎具体原理(正排索引和倒排索引)
正排索引
倒排索引 编写数据去标签与数据清洗的模块 Parser
从boost官网导入HTML网页数据
去标签
构建 Parser 模块
递归式获取 HTML 文件的带文件…
目录
搜索引擎项目背景
搜索引擎的宏观原理
搜索引擎技术栈和项目环境
搜索引擎具体原理(正排索引和倒排索引)
正排索引
倒排索引 编写数据去标签与数据清洗的模块 Parser
从boost官网导入HTML网页数据
去标签
构建 Parser 模块
递归式获取 HTML 文件的带文件名称路径
对 HTML 文件内容进行解析 ParseHtml
将解析之后的 HTML 文件内容拼接并写入对应的文本文件中
Parser 模块整体代码
编写建立索引的模块 Index 编写正排索引模块 按行读取文档 切分行数据并进行行数据对应的文档的正排索引结构体对象构建
编写倒排索引模块
通过文档 id 获取对应文档的正排索引对象
根据关键词获取关键词对应的倒排拉链
index设置为单例模式
编写搜索模块 Searcher
创建 index 对象构建索引
获取 content 的摘要 通过关键词进行查询构建搜索模块 项目中期测试
debug本地测试 bug1
编写 http_sever 模块
编写前端模块 为项目添加日志信息
项目展示 项目地址
项目总结 搜索引擎项目背景 搜索引擎是一个大家众所周知的一个搜索工具常见的搜索引擎有百度搜索搜狗搜索360搜索等等我们以百度搜索为例。百度搜索的主页面如下 我们可以在搜索框中输入我们想要搜索的内容点击搜索就会出现如下界面。
点击搜索之后跳转的主页会展现大量的相关关键字的网页信息。 我们对其中一个网页信息进行分析。 网页信息就包含了网页的标题网页内容的摘要和网页资源对应的url。
我们自己可以实现这样一个大的搜索引擎吗对与个人而言实现这样一个大的搜索引擎代价太大将全网的数据整合就是一个巨大的难题所以我们实现这样一个进行全网搜索的搜索引擎是明显不现实的但是有不少的网页具有站内搜索的引擎我们以常见的boost库为例。图示如下 boost库官网中就存在这样一个站内的搜索引擎可以搜索boost库中的相关知识。基于以上的背景此项目旨在设计开发一款如boost库官网界面站内搜索引擎的boost搜索引擎实现与之类似的站内搜索功能。 搜索引擎的宏观原理 那么像百度搜索360搜索等等这些当今互联网上应用较为广泛的搜索引擎它们搜索的宏观原理是什么呢我们通过一个图示为大家大概的讲解。 我们实现的boost搜索引擎实际上只涉及了蓝方框内的宏观原理相应的html我们不是通过爬虫获取的而是直接在官网上下载下来的html文件。 搜索引擎技术栈和项目环境 技术栈C/CC11STL准标准库BoostJsoncpp(数据交换)cppjieba(搜索关键词的分词)cpp-httplib(构建http服务器)html5cssjQueryjsAjax。 项目环境centos7云服务器vim(gcc/g)makefilevscode。 搜索引擎具体原理(正排索引和倒排索引)
在搜索引擎中我们在通过关键字进行查询时往往会使用到倒排索引和正排索引。那么倒排索引和正排索引是什么呢
正排索引 比如现在有两个文档两个文档的文档id分别为1和2两个文档的内容分别为雷布斯发布了小米手机和雷布斯发布了小米su7. 所谓正排索引很好理解就是通过文档id查询文档内容。 倒排索引
倒排索引其实就是通过文档的内容和文档的关键字查询文档的文档id。
那么怎么样获取文档的关键字呢此时我们就要对文档进行分词。 文档1分词(雷布斯发布了小米手机)雷布斯/发布/小米/手机/小米手机文档2分词(雷布斯坐的小米su7)雷布斯/坐/小米/su7/小米su7 不难发现我们在进行分词的时候将了/的这两个关键字给省略掉了这是因为在搜索引擎中我们有了停止词的概念停止词就是在多个文档中都会出现的共性词如中文中的 的/了/是等等英文中的 a/the 等等如果将这些字作为了关键字将来查询到的文档就非常多可以理解为就是查询所有的文档所以会降低查询的效率所以我们在关键字拆分的时候不将停止词作为关键字。 需要注意的是多个文档的重复关键字我们最终只保留唯一的一份也就意味着关键字也必须和文档id一样是唯一的。 其实在大家使用关键字在百度等搜索引擎上进行搜索时查找出来多个同种类型的多个去标签的网页内容会在一个页面先后展示为什么会先后展示这是因为每个网页的权值是不一样的权值高的会优先展示。所以我们也会为每个文档进行权值的设定 所以搜索引擎的具体查询原理就是sever端先用关键字进行倒排索引查找到文档的id然后再通过文档id查询到文档内容再对查询到的文档内容进行去标签操作得到titledesc和url最终对多个文档的tiledesc和url进行组合然后通过文档的权值进行排序最终将拼装好的页面返回给client端展示。 编写数据去标签与数据清洗的模块 Parser
从boost官网导入HTML网页数据 boost官网主界面如图所示。 我们的网页不是通过爬虫获取的而是直接下载了boost官网中的对应的html网页。 下载之后使用 rz -E 指令将下载下来的含有 html 网页的 boost 文件导入我们自己创建的目录中。 使用 tar xzf 指令对对应的文件进行解包解压解压之后的目录如图所示 boost_1_87_0 目录中的文件就是我们在boost官网上看到的所有的内容。在boost_searcher 下创建一个与 boost_1_87_0 同级的目录 data在 data 里面创建一个input 目录用于存放 boost_1_87_0/doc/html 目录下的所有 html 文件和目录类似于爬虫获取的大量 html 网页数据源。
去标签 何为标签
titleChapter 9. Boost.Container/title上述html代码中符号 以及 符号 内的内容组合起来我们称之为一个标签。以 为开始标签/ 为结束标签。 何为去标签 所谓去标签其实就是不用去关心标签内的数据只关心标签外的数据比如上述标签我们只关心 Chapter 9. Boost.Container 在与 input 同级的目录下创建一个 raw_html 目录目录里创建对应的文件用于存放每一个 html 文件去标签之后的数据在 raw_html 内的文件中存放且每个 html 文件对应的去标签之后的数据以 \3 进行分隔因为 \3 是不可显字符。
构建 Parser 模块
paser模块的构建主要分3步。 递归式的获取 input 目录里的所有 html 文件的带文件名称的路径名并将每个 html 文件的带文件名称的路径名保存在一个vector容器中。根据第一步获取的 html 文件的带文件名的路径依次打开每个文件依次读取每个文件的内容对读取出来的内容进行解析将解析每个 html 文件的 titlecontent和url保存在一个vector容器中。对第2步获取的每个文件的 titlecontenturl 内容进行拼接写入 raw.txt 文件中 。 递归式获取 HTML 文件的带文件名称路径 递归式获取文件的带文件名称路径的方法我们采用的是 boost官网的 Filesystem Library 库要使用该库必须先安装 boost 库安装指令如下。
sudo yum install -y boost-devel
枚举文件 EnumFile代码如下。
//枚举文件
bool EnumFile(const std::string src_path, std::vectorstd::string *files_list)
{//利用boost库文件操作读取文件namespace fsboost::filesystem;fs::path root_path(src_path);if(!fs::exists(root_path)){std::cerrsrc_pathnot existsstd::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;}//提取文件后缀.htmlif(iter-path().extension()!.html){continue;}//std::coutdebugiter-path().string()std::endl;//当前路径一定是一个合法的以.html结束的普通文件//将路径对象转换成字符串放入files_list中files_list-push_back(iter-path().string());//路径data/input/intrusive/index.html}return true;
}
对 HTML 文件内容进行解析 ParseHtml 对 html 文件内容分析主要分为四步。 从 file_list 中依次读取每个 html 文件的的内容。 从 html 文件内容中解析文件 title。 从 html 文件内容中解析文件 content。 从 html 文件内容中解析文件 url最终将解析的 titlecontenturl全部保存进一个 DocInfo 结构体对象中最终保存进 vector 中。 基于此我们要先创建一个DocInfo结构体用于保存每个 html 文件内容的titlecontenturl。
typedef struct DocInfo{std::string title;//文档标题std::string content;//文档内容std::string url;//文档地址
} DocInfo_t;
//解析文件
bool ParseHtml(const std::vectorstd::string files_list, std::vectorDocInfo_t *results){for(const std::string file: files_list)//for(auto file:files)for(const std::string file :file_list){//1.读取html文件std::string result;//读取的结果放到resultif(!ns_util::FileUtil::ReadFile(file,result)){//读取失败继续读取continue;}//2.解析指定文件提取titleDocInfo_t doc;//这个内容不需要了所以我们直接move即push_back(move(doc))就是防止拷贝docif(!ParseTitle(result,doc.title)){continue;}//3.解析指定文件提取contentif(!ParseContent(result,doc.content)){continue;}//std::couterrorstd::endl;//4.解析指定文件的路径构建urlif(!ParseUrl(file,doc.url)){continue;file: 路径data/input/intrusive/index.html}//走到这里一定是完成了解析任务当前文档的相关结果都保存在doc里面results-push_back(std::move(doc));//for debug//ShowDoc(doc);//break;} return true;
} 正如上述所说我们要 读取html文件ReadFile(file,result)----解析指定文件提取title ParseTitle(result,doc.title)------解析指定文件提取content, ParseContent(result,doc.content)-----解析指定文件的路径构url ParseUrl(file,doc.url) 读取html文件
//读取文件static bool ReadFile(const std::string file_path,std::string *out){std::ifstream in(file_path,std::ios::in);if(!in.is_open()){std::cerropen filefile_patherrorstd::endl;return false;}std::string line;//从文件流in按照行读取//getline是stringliuwhile(std::getline(in,line)){*outline;}//关闭文件in.close();return true;}};解析 HTML 文件的 title
static bool ParseTitle(const std::string file,std::string *title)
{std::size_t beginfile.find(title);//寻找的是title中的第一个位置if(beginstd::string::npos){return false;}std::size_t endfile.find(/title);//寻找的是/title中的第一个位置if(endstd::string::npos){return false;}beginstd::string(title).size();if(beginend){return false;}*titlefile.substr(begin,end-begin);return true;
}
解析 HTML 文件的 content
static bool ParseContent(const std::string file,std::string *content)
{//去标签基于一个简易的状态机enum status{LABLE,//标签CONTENT//内容};enum status sLABLE;for(char c:file){switch(s){case LABLE:if(c) sCONTENT;break;//break跳出switch语句case CONTENT:if(c) sLABLE;else{//不保留原始文件的\n因为后续我们想用\n作为html解析之后文本的分隔符if(c\n) c ;content-push_back(c);}break;default:break;}} return true;} 解析 HTML 文件的 url
bool ParseUrl(const std::string filepath,std::string* url)
{std::string url_headhttps://www.boost.org/doc/libs/1_87_0/doc/html;std::string url_tailfilepath.substr(src_path.size());*urlurl_headurl_tail;return true;} 将解析之后的 HTML 文件内容拼接并写入对应的文本文件中
//保存文件
bool SaveHtml(const std::vectorDocInfo_t results,const std::string output){#define SEP \3//分割符std::ofstream out(output,std::ios::out | std::ios::binary);//binary按照二进制方式写入if(!out.is_open()){std::cerropen outputfalid! std::endl;return false;}//对文件内容进行写入for(auto item:results)//results是个vector{std::string out_string;out_stringitem.title;out_stringSEP;out_stringitem.content;out_stringSEP;out_stringitem.url;out_string\n;out.write(out_string.c_str(),out_string.size());}out.close();return true;
} 我们以 \3 区分每个html文件的titlecontenturl。以 \n 区分每个文件的解析之后的内容。 Parser 模块整体代码
int main()
{std::vectorstd::string files_list;//第一步递归式的把每个html文件名带路径保存在files_list中,方便后期进行一个一个的文件进行读取if(!EnumFile(src_path,files_list)){std::cerrenum flie name error!std::endl;return 1;}//第二步 按照files_list读取每个文件的内容并进行解析std::vectorDocInfo_t results;if(!ParseHtml(files_list,results)){std::cerrparse html error!std::endl;return 2;}//第三步 把解析完毕的各个文件内容写入到output,按照\3作为每个稳定的分隔符if(!SaveHtml(results,output)){std::cerr save html errorstd::endl;//results是结构体return 3;}//***********// std::string s(你好吃了吗);// std::vectorstd::string v;// ns_util::JiebaUtil::CutString(s,v);// for(auto iter:v)// {// std::coutiterstd::endl;// }return 0;
} 这样我们就实现了对boost库的html文件进行了去标签提取title,content,url并保存到rwa_html中。接下来我们就要针对raw_html文件进行创建索引模块。 编写建立索引的模块 Index 编写 index 主要分为两步。 编写正排索引模块即文档 id 和文档内容的关系。编写倒排索引模块即关键词和文档 id 的关系。 在此之前我们已经将所有的 html 文件进行了解析将解析之后所有 html 文件的titlecontenturl全部保存在了 raw.txt 文本文件中并且每个文件之间的解析之后的数据以 \n作为分隔符所以将来可以使用getline 一次获取 raw.txt 的一行数据因为一行的数据刚好是一个文档解析之后的数据所以我们可以以这行数据建立该行数据所对应的文档的正排索引结构体和倒排索引结构体 正排索引结构体如下。 // 正排struct DocInfo{std::string title; // 文档的标题std::string content; // 文档对于的去标签之后的内容std::string url; // 官网文档urluint64_t doc_id; // 文档id}; 正派索引结构体是对于文档而言的表示当前文档对应的titlecontenturl和doc_id文档id一个 html 文档对应一个正排索引结构体对象。因为文档 id 和 html 文档是一一对应的关系。 倒排索结构体如下。 // 倒排struct InvertedElem{uint64_t doc_id; // 文档idstd::string word; // 文档关键字 -----找到对于的文档idint weight; // 文档权重};倒排索引结构体是对于关键词而言的一个关键词可能对应多个倒排索引结构体对象。因为一个关键词可能出现在多个文档中。 正排索引为一个vector容器该容器的每个元素为一个正排索引结构体对象。 // 正排索引的数据结构用数组数组的下标天然是文档的IDstd::vectorDocInfo forward_index; // 正排索引
倒排索引为一个unordered_map容器该容器的每个元素的 first 对应一个关键词每个元素的 second 表示该元素对应的倒排拉链保存 first 对应的关键词的所有倒排索引的结构体对象。 // 倒排拉链typedef std::vectorInvertedElem InvertedList; 编写正排索引模块 正排索引的编码主要分为两步。 对从 raw.txt 中读取的一行数据因为我们之前已经将每个 html 文件解析之后的内容通过 \n 进行分隔所以从 raw.txt 中读取的一行数据就是一个文档解析之后的数据。 对读取的一行数据进行切分得到这行数据对应的文档的titlecontenturl。创建一个正排索引结构体对象将切分之后获取的 titlecontenturl 分别设置进这个正派索引结构体对象中正派索引的 doc_id 成员我们用正排索引的vector下标表示. 按行读取文档
std::ifstream in(input, std::ios::in | std::ios::binary); // 读取文件if (!in.is_open()){std::cerr sorry, input open error std::endl;return false;}// 打开raw.txt文件之后就要进行读取(按照行读取) *****\3*******\3******\n// 按照行读取能够保证我们能够读取一个完整的信息std::string line;int count0;while (std::getline(in, line)){DocInfo *doc BuildForwardIndex(line); // 读取一行之后建立正排索引if (doc nullptr){// 如果一行建立索引失败,继续读取下一行std::cerr build line error std::endl;continue;} 切分行数据并进行行数据对应的文档的正排索引结构体对象构建 在对读取的行数据进行切分时我们使用boost库中的split函数进行切分。
// 这两个接口我们不想暴漏给别人搞成私有函数DocInfo *BuildForwardIndex(const std::string line){// 1.解析line,字符串切分// line- 3 string title content urlconst std::string sep \3; // 分隔符std::vectorstd::string results;ns_util::StringUtil::Split(line, results, sep);if (results.size() ! 3){return nullptr;}// 字符串 std::vectorstd::string results;进行填充到DocInFoDocInfo doc;doc.title results[0];doc.content results[1];doc.url results[2];doc.doc_id forward_index.size();// 插入到正排索引的vectorforward_index.push_back(std::move(doc)); // forward_index 不是创建的局部变量所以是全局变量return forward_index.back(); // 返回vector的最后一个元素}
编写倒排索引模块
倒排索引主要分为两步。 根据创建的正排索引结构体对象的title和content进行分词分词之后通过一个unordered_map 对象依次统计关键词在title和content中出现的次数。根据 unordered_map 对象中统计出的关键词在title和content中出现的次数建立关键词的倒排索引结构体对象并将该结构体对象插入关键词对应的倒排拉链中。 // 建立倒排bool BuildInvertedIndex(const DocInfo doc){// DocInfo{title,content,url,doc_id}// word-倒排拉链struct word_cnt{int title_cnt;int content_cnt;word_cnt() : title_cnt(0), content_cnt(0){}};std::unordered_mapstd::string, word_cnt word_map; // 用来暂存词频的映射表// 对标题进行分词std::vectorstd::string title_words;ns_util::JiebaUtil::CutString(doc.title, title_words);// 对标题进行词频统计for (auto s : title_words){boost::to_lower(s);word_map[s].title_cnt;}// 对文档内容进行分词std::vectorstd::string content_words;ns_util::JiebaUtil::CutString(doc.content, content_words);// 对文档内容进行词频统计for (auto s : content_words){boost::to_lower(s);word_map[s].content_cnt;}
#define X 10
#define Y 1for (auto word_pair : word_map){InvertedElem item;item.doc_id doc.doc_id;item.word word_pair.first;item.weight X * word_pair.second.title_cnt Y * word_pair.second.content_cnt;InvertedList inverted_list invereted_index[word_pair.first];inverted_list.push_back(std::move(item));}return true;}};
对 doc 中的 title 和 content 进行分词时我们使用cppjieba库进行分词。分词代码如下。
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;class JiebaUtil{public://string分词static void CutString(const std::string src,std::vectorstd::string *out){jieba.CutForSearch(src,*out);}private:static cppjieba::Jieba jieba;};cppjieba::Jieba JiebaUtil::jieba(DICT_PATH,HMM_PATH,USER_DICT_PATH,IDF_PATH,STOP_WORD_PATH);
通过文档 id 获取对应文档的正排索引对象 // 根据doc_id找到文档内容,正排索引DocInfo *GetForwardIndex(uint64_t doc_id){if (doc_id forward_index.size()){std::cerr doc_id out range, error! std::endl;return nullptr;}return forward_index[doc_id];}根据关键词获取关键词对应的倒排拉链
// 根据关键字string,获取倒排拉链InvertedList *GetInvertedList(const std::string word){auto iter invereted_index.find(word);if (iter invereted_index.end()){std::cerr word have no InvertedList std::endl;return nullptr;}return (iter-second);}
index设置为单例模式 class Index{// 方法private:Index() {}Index(const Index ) delete;//拷贝构造deleteIndex operator(const Index ) delete;//重载deletestatic Index *instance;//私有成员静态static std::mutex mtx;//获取单例static Index *GetInstance(){if (instance nullptr){mtx.lock();if (nullptr instance){instance new Index();}mtx.unlock();}return instance;}Index *Index::instance nullptr;std::mutex Index::mtx;编写搜索模块 Searcher
当我们在搜索引擎的搜索框中输入关键词之后进行查询时返回的网页中一定是含有当前的关键词吗我们以百度搜索引擎为例
不难发现搜索出来的网页中既含有我们搜索框中的关键词也含有搜索框中关键词的一部分。所以也就说明当我们在使用关键词搜索时要先对关键词进行分词分词之后形成的多个关键词才是我们最终在倒排索引中查找的关键词 。 Searcher 模块的编写主要分为三步 创建 index 对象并进行索引的构建。获取文档 content 的摘要 desc。通过关键词在服务器中进行倒排索引查找然后通过倒排索后引进行正排索引找到关键词对应的文档的 titlecontent 和 url并将这三个内容转为 json 串返回到浏览器。 创建 index 对象构建索引
class Searcher{private:ns_index::Index *index;public:Searcher(){}~Searcher(){}public:void InitSearcher(const std::string input){//1.获取或者创建index对象indexns_index::Index::GetInstance();//index是单例则searcher也是单例//std::cout获取单例成功....std::endl;LOG(NORMAL,获取单例成功....);//2.根据index对象建立索引index-BuildIndex(input);//std::cout建立倒排正排索引成功.....std::endl;LOG(NORMAL,建立倒排正排索引成功.....);}获取 content 的摘要
此时我们要注意一个点就是我们最终在浏览器上显示对应的 html 模块时显示的是文档标题 title文档内容描述 desc文档的 url。 所以此时我们要获取的不是 文档的 content而是文档的 content 进行处理之后的 描述 desc。所以此时我们就要使用 GetDesc 函数获取 content 的 desc。
GetDesc 的实现逻辑就是在文档 content 中查找关键词 word 的位置如果 word 可以通过倒排索引和正排索引获取到一个文档那么这个文档的 content 中一定是含有关键词 word 的。因为生成关键词的步骤就是对文档的 title 先进行分词然后对文档 content 分词之后获得的关键词而且 content 中是包含 title 的内容的所以可以说 html 文档所有的关键词 word 产生于文档的 content 中。
std::string GetDesc(const std::string html_content,const std::string word){//找到word在html中的首次出现然后往前找50字节,往后找100字节//截取这部分内容const int prev_step50;const int next_step100;//1.找到首次出现// std::size_t poshtml_content.find(word);//利用std::search中的函数进行查找wordauto iterstd::search(html_content.begin(),html_content.end(),word.begin(),word.end(),[](int x,int y){return xy;});if(iterhtml_content.end()){return None1;//这种情况不可能出现}int posstd::distance(html_content.begin(),iter);//2.获取satrt,endint start0;int endhtml_content.size()-1;//如果pos之前有50字节就更新位置if(posstartprev_step) startpos-prev_step;if(pos(end-next_step)) endposnext_step;//截取子串returnif(startend) return None2;std::string deschtml_content.substr(start,end-start);desc...;return desc;} 通过关键词进行查询构建搜索模块 void Search(const std::string query, std::string *json_string){//1.[分词]对我们的query进行按照searcher的要求进行分词std::vectorstd::string words;ns_util::JiebaUtil::CutString(query,words);for(auto word: words){boost::to_lower(word);//字符串转换成小写//获取倒排拉链ns_index::InvertedList *invertedlistindex-GetInvertedList(word);if(nullptrinvertedlist){continue;}
但是这样会出现一个问题比如我们对乔布斯发布ipone分割获取倒排拉链会出现可能的情况比如乔布斯----文档id2,发布----文档id4,ipone------文档id2,这样就会出现重复文档信息因此我们要降重即我们引进一个新的结构体struct ,使其 id--------vector关键词 struct InvertedElemPrint{uint64_t doc_id;int weight;std::vectorstd::string words;//多个词对应同一个idInvertedElemPrint():doc_id(0),weight(0){}}; void InitSearcher(const std::string input){//1.获取或者创建index对象indexns_index::Index::GetInstance();//index是单例则searcher也是单例//std::cout获取单例成功....std::endl;LOG(NORMAL,获取单例成功....);//2.根据index对象建立索引index-BuildIndex(input);//std::cout建立倒排正排索引成功.....std::endl;LOG(NORMAL,建立倒排正排索引成功.....);}//query:搜索关键字//json_string 返回给用户浏览器的搜索结果void Search(const std::string query, std::string *json_string){//1.[分词]对我们的query进行按照searcher的要求进行分词std::vectorstd::string words;ns_util::JiebaUtil::CutString(query,words);//2.[触发]就是根据分词的各个词进行index查找//std::vectorns_index::InvertedList inverted_list_all;std::vectorInvertedElemPrint inverted_list_all;std::unordered_mapuint64_t,InvertedElemPrint tokens_map;//ns_index::InvertedList inverted_list_all;for(auto word: words){boost::to_lower(word);//字符串转换成小写//获取倒排拉链ns_index::InvertedList *invertedlistindex-GetInvertedList(word);if(nullptrinvertedlist){continue;}//把获取到的倒排拉链放到inverted_list_all中//这里相当于把一个vector的所有元素插入一个vector中//inverted_list_all.insert(inverted_list_all.end(),invertedlist-begin(),invertedlist-end());//invertedlist对应的是word分词之后对应的文档可能会重复所以我们对invertedlist去重for(const auto elem : *invertedlist){auto itemtokens_map[elem.doc_id];//如果存在就直接获取不存在就新建item.doc_idelem.doc_id;item.weightelem.weight;item.words.push_back(elem.word);//这样就完成了去重}}for(const auto item :tokens_map){inverted_list_all.push_back(std::move(item.second));//此时tokens_map中的元素id,... 不重复我们插入vector中}//3.[合并排序]汇总查找结果按照相关性(weight)降序排序// std::sort(inverted_list_all.begin(),inverted_list_all.end(),\// [](const ns_index::InvertedElem e1,const ns_index::InvertedElem e2){ return e1.weighte2.weight;});std::sort(inverted_list_all.begin(),inverted_list_all.end(),\[](const InvertedElemPrint e1,const InvertedElemPrint e2){ return e1.weighte2.weight;});//4.[构建]根据查出的结果构建json串----jsoncpp//得到的内容是结构体要进行序列化成为字节流传给服务器Json::Value root;for(auto item :inverted_list_all){ns_index::DocInfo *docindex-GetForwardIndex(item.doc_id);if(nullptrdoc){continue;}//得到了一个结构体Docinfo要进行序列化处理Json::Value elem;elem[title]doc-title;elem[desc]GetDesc(doc-content,item.words[0]);//获取摘要//这里注意item.word都是小写的而content有大小写所以会出现content匹配不上word而出现none1elem[url]doc-url;// elem[id](int)item.doc_id;// elem[weight]item.weight;//这样就把一个结构体的title,content,url转换了字节流root.append(elem);}//Json::StyledWriter writer;//stylewriter会格式处理方便调试Json::FastWriter writer;//fastwriter不会做格式处理速度块*json_stringwriter.write(root);}项目中期测试
debug本地测试
const std::string inputdata/raw_html/raw.txt;
int main()
{//for testns_searcher::Searcher *searchnew ns_searcher::Searcher();search-InitSearcher(input);std::string query;std::string json_string;char buffer[1024];while (true){std::coutpleasr enter you search querystd::endl;//std::cinquery;fgets(buffer,sizeof(buffer)-1,stdin);buffer[strlen(buffer)-1]0;querybuffer;search-Search(query,json_string);std::coutjson_stringstd::endl;}return 0;
} bug1 在通过 filesystem 关键词进行查找时我们发现 filesystem 关键词对应的文档的 content 的 desc 字段变成了 none 1。 这就意味着我们在构建 content 的 desc 时没有在 content 中找到我们当前查询的关键词信息。 可是我们在官方文档下进行查找时我们在对应文档中查找到了对应的关键词呀可是为什么在运行结果中没有在对应的文档中找到关键词呢 经过多次排查最终发现这是因为 在 search 函数中没有进行转换成小写处理进行查找 对 GetDesc 函数进行第一次调整 std::string GetDesc(const std::string html_content,const std::string word){//找到word在html中的首次出现然后往前找50字节,往后找100字节//截取这部分内容const int prev_step50;const int next_step100;//1.找到首次出现// std::size_t poshtml_content.find(word);//利用std::search中的函数进行查找wordauto iterstd::search(html_content.begin(),html_content.end(),word.begin(),word.end(),[](int x,int y){return std::tolower(x)std::tolower(y);});if(iterhtml_content.end()){return None1;//这种情况不可能出现}int posstd::distance(html_content.begin(),iter);//2.获取satrt,endint start0;int endhtml_content.size()-1;//如果pos之前有50字节就更新位置if(posstartprev_step) startpos-prev_step;if(pos(end-next_step)) endposnext_step;//截取子串returnif(startend) return None2;std::string deschtml_content.substr(start,end-start);desc...;return desc;}
调整后再次查看对应文档的 content 的 desc 描述。 不难发现此时 desc 字段已经具有了数据此 bug 修复成功。 编写 http_sever 模块
http_sever 本质上就是一个 sever 服务器网络服务即一个网络进程可以让其他客户端进程跨网络访问。如果我们使用之前学习的 socket 编程代码自己实现一个 sever 服务器也不是不可以但是代价太大我们选择使用 现成的第三方库 cpp-httplib 库(推荐下载v.0.7.15)下载压缩包然后使用 rz 指令上传至项目目录下使用 unzip 指令压缩即可获得 cpp-httplib 目录我们主要使用 cpp-httplib 目录下的 httplib.h 头文件。 同时在下载好 cpp-httplib 库之后应该使用较新的 gcc 编译器centos7下默认为 gcc 4.8.5 版本为了避免出现编译和运行错误我们要使用对应的指令对 gcc 编译器进行升级。
#include cpp-httplib-v0.7.15/httplib.h
#include searcher.hppconst std::string root_path./wwwroot;
const std::string inputdata/raw_html/raw.txt;int main()
{ns_searcher::Searcher search;//这里已经构造了对象search.InitSearcher(input);//根据input创建索引建立索引//编写httplib对应的调用httplib::Server svr;//什么都不输入的时候默认会把./wwwroot的index.html返回浏览器svr.set_base_dir(root_path.c_str());//默认把wwwroot的首页返回浏览器//lambda表达式[val]代表引用作用域的变量[val]代表值传递作用域的变量[]引用所有变量svr.Get(/s, [search](const httplib::Request req, httplib::Response rsp){ if(!req.has_param(word)){rsp.set_content(必须要有搜索关键字,text/plain; charsetutf-8);return;}std::string wordreq.get_param_value(word);//std::cout用户在搜索wordstd::endl;LOG(NORMAL,用户搜索: word);std::string json_string;search.Search(word,json_string);//这个是lamba表达式,需要外面的search我们[search]就是对外面的search进行引用//给用户返回jison_stringrsp.set_content(json_string,application/json);//res.set_content(Hello Word!, text/plain); });//text/plain 代表返回的是hello word是文本 LOG(NORMAL,服务器启动成功.....);svr.listen(0.0.0.0, 8080);return 0;
} 在搜索框中通过给 word 字段传入关键词后端获取到关键词请求之后在后端进行查询将查询到的 序列化 json 串返回到浏览器客户端。 编写前端模块 前端页面主要通过deepseek进行美化。前端页面以 htmlcss 技术为基础使用传统的 javascript 技术进行前后端数据交互太过繁琐所以我们会使用第三方库 jquery库。通过 jquery 库中的 ajax 函数向后端服务器发送 http 请求并获取后端服务器返回的响应获取到响应之后调用回调函数最终由 jquery 动态构建前端页面。 !DOCTYPE html
html langzh-CN
headmeta charsetUTF-8meta nameviewport contentwidthdevice-width, initial-scale1.0titleBoost 智能搜索引擎/titlelink hrefhttps://fonts.googleapis.com/css2?familyNotoSansSC:wght300;400;500;700familyRoboto:wght300;400;500displayswap relstylesheetlink relstylesheet hrefhttps://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.cssscript srchttps://code.jquery.com/jquery-3.6.0.min.js/scriptstyle:root {--primary-color: #4a6bdf;--primary-light: #6d8aff;--secondary-color: #2ecc71;--accent-color: #ff6b6b;--text-color: #2d3436;--light-gray: #f5f6fa;--medium-gray: #dfe6e9;--dark-gray: #636e72;--box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);--box-shadow-hover: 0 8px 24px rgba(0, 0, 0, 0.12);--transition: all 0.3s cubic-bezier(0.25, 0.8, 0.25, 1);--border-radius: 12px;}* {margin: 0;padding: 0;box-sizing: border-box;}html, body {height: 100%;font-family: Noto Sans SC, Roboto, PingFang SC, Microsoft YaHei, sans-serif;color: var(--text-color);background-color: #f9fafc;line-height: 1.6;}.container {width: 100%;max-width: 800px;margin: 0 auto;padding: 30px 20px;}.logo {text-align: center;margin-bottom: 40px;position: relative;}.logo h1 {font-size: 2.8rem;background: linear-gradient(135deg, var(--primary-color), var(--primary-light));-webkit-background-clip: text;background-clip: text;color: transparent;font-weight: 700;letter-spacing: -1px;margin-bottom: 8px;}.logo p {color: var(--dark-gray);font-size: 1rem;font-weight: 300;}.search-container {width: 100%;margin-bottom: 40px;position: relative;}.search-box {display: flex;width: 100%;box-shadow: var(--box-shadow);border-radius: var(--border-radius);overflow: hidden;transition: var(--transition);background: white;}.search-box:focus-within {box-shadow: var(--box-shadow-hover);transform: translateY(-2px);}.search-box input {flex: 1;height: 60px;border: none;padding: 0 24px;font-size: 16px;outline: none;background-color: transparent;font-family: Noto Sans SC, sans-serif;}.search-box input::placeholder {color: var(--dark-gray);opacity: 0.6;}.search-box button {width: 140px;height: 60px;background: linear-gradient(135deg, var(--primary-color), var(--primary-light));color: white;border: none;font-size: 16px;font-weight: 500;cursor: pointer;transition: var(--transition);display: flex;align-items: center;justify-content: center;font-family: Noto Sans SC, sans-serif;}.search-box button i {margin-right: 8px;}.search-box button:hover {background: linear-gradient(135deg, var(--primary-light), var(--primary-color));}.result-stats {color: var(--dark-gray);font-size: 14px;margin-bottom: 20px;padding-bottom: 10px;border-bottom: 1px solid var(--medium-gray);display: flex;align-items: center;}.result-stats i {margin-right: 8px;color: var(--primary-color);}.result-item {margin-bottom: 30px;padding: 20px;border-radius: var(--border-radius);transition: var(--transition);background: white;box-shadow: var(--box-shadow);}.result-item:hover {box-shadow: var(--box-shadow-hover);transform: translateY(-2px);}.result-title {color: var(--primary-color);font-size: 1.25rem;font-weight: 500;margin-bottom: 12px;text-decoration: none;display: block;transition: var(--transition);}.result-title:hover {color: var(--primary-light);text-decoration: underline;}.result-snippet {color: var(--text-color);font-size: 0.95rem;margin-bottom: 12px;line-height: 1.6;}.result-url {color: var(--secondary-color);font-size: 0.85rem;display: flex;align-items: center;font-weight: 500;}.result-url i {margin-right: 8px;font-size: 0.9rem;}.loading {text-align: center;padding: 40px 0;}.loading-spinner {display: inline-block;width: 40px;height: 40px;border: 4px solid rgba(74, 107, 223, 0.2);border-radius: 50%;border-top-color: var(--primary-color);animation: spin 1s ease-in-out infinite;}keyframes spin {to { transform: rotate(360deg); }}.footer {text-align: center;margin-top: 60px;padding-top: 20px;border-top: 1px solid var(--medium-gray);color: var(--dark-gray);font-size: 0.85rem;}.footer a {color: var(--primary-color);text-decoration: none;transition: var(--transition);}.footer a:hover {color: var(--primary-light);text-decoration: underline;}media (max-width: 768px) {.container {padding: 20px 15px;}.logo h1 {font-size: 2.2rem;}.search-box input {height: 52px;padding: 0 18px;}.search-box button {height: 52px;width: 100px;font-size: 15px;}.result-item {padding: 16px;}}/style
/head
bodydiv classcontainerdiv classlogoh1Boost/h1p智能、快速、精准的搜索引擎/p/divdiv classsearch-containerdiv classsearch-boxinput typetext placeholder输入您想搜索的内容... idsearch-input autocompleteoffbutton onclickSearch()i classfas fa-search/i搜索/button/div/divdiv classresult-stats idresult-stats styledisplay: none;i classfas fa-chart-bar/ispan idstats-text/span/divdiv classresult idresult-container!-- 搜索结果将在这里动态生成 --/divdiv classfooterp© 2025 a href#Boost 智能搜索引擎/a · 为您提供优质的搜索体验/p/div/divscript$(document).ready(function() {// 输入框获取焦点时清空placeholder$(#search-input).focus(function() {$(this).attr(placeholder, );});// 输入框失去焦点时恢复placeholder$(#search-input).blur(function() {if ($(this).val() ) {$(this).attr(placeholder, 输入您想搜索的内容...);}});// 按回车键触发搜索$(#search-input).keypress(function(e) {if (e.which 13) {Search();}});});function Search() {let query $(#search-input).val().trim();if (query ) {return;}console.log(搜索关键词: query);// 显示加载状态$(#result-container).html(div classloadingdiv classloading-spinner/divp stylemargin-top: 15px; color: var(--dark-gray);正在为您搜索.../p/div);$(#result-stats).hide();// 发起HTTP请求$.ajax({type: GET,url: /s?word encodeURIComponent(query),success: function(data) {console.log(data);BuildHtml(data);// 更新搜索结果统计$(#stats-text).text(找到约 ${data.length} 条结果 (${((Math.random() * 0.1) 0.05).toFixed(2)} 秒));$(#result-stats).fadeIn();},error: function(xhr, status, error) {$(#result-container).html(div styletext-align: center; padding: 40px 20px;i classfas fa-exclamation-triangle stylefont-size: 2.5rem; color: var(--accent-color); margin-bottom: 15px;/ip stylecolor: var(--accent-color); font-size: 1.1rem; margin-bottom: 10px;搜索失败/pp stylecolor: var(--dark-gray);请检查网络连接后重试/p/div);console.error(搜索请求失败: , error);}});}function BuildHtml(data) {let resultContainer $(#result-container);resultContainer.empty();if (data.length 0) {resultContainer.html(div styletext-align: center; padding: 40px 20px;i classfar fa-frown stylefont-size: 2.5rem; color: var(--dark-gray); margin-bottom: 15px;/ip stylecolor: var(--text-color); font-size: 1.1rem; margin-bottom: 10px;没有找到相关结果/pp stylecolor: var(--dark-gray);请尝试其他关键词/p/div);return;}for (let elem of data) {let resultItem $(div, { class: result-item });$(a, {class: result-title,text: elem.title || 无标题,href: elem.url,target: _blank}).appendTo(resultItem);$(p, {class: result-snippet,text: elem.desc || 暂无描述信息}).appendTo(resultItem);$(div, {class: result-url,html: i classfas fa-link/i${elem.url}}).appendTo(resultItem);resultItem.appendTo(resultContainer);}}/script
/body
/html 为项目添加日志信息
在没有添加日期之前我们是以标准输出和标准错误的形式去反映代码的执行结果。有了日志信息之后可以更进一步详细的知道代码的执行情况以及代码执行到了那里在哪个文件那一行出现了错误迅速进行错误的定位。
#pragma once
#include iostream
#include string
#include ctime#define NORMAL 1 //正常的
#define WARNING 2
#define DEBUG 3
#define FATAL 4//日志 日志等级日志信息日志时间文件多少行#define LOG(LEVEL,MESSAGE) log(#LEVEL,MESSAGE, __FILE__, __LINE__) //#LEVEL ##define 是把宏名转换成字符串
void log(const std::string level,std::string message,std::string file,int line)
{std::cout[level][time(nullptr)][message][file : line]std::endl;
} 将项目部署到 LINUX 服务器 nohup ./http_server log.txt 21 nohup 可以将进程输出的日志信息保存在一个自动生成的 nohub.out 文件中这里将 nuhub 指令将进程输出的日志信息全部重定向输出到了 log.txt 中。 此时其实我们关闭了 Xshell 在浏览器端仍然可以访问 http_server 服务。 项目展示 项目地址 https://gitee.com/liu-taoloveqingxin/boost-search-engine.git 项目总结