如果想使“用户搜索内容”和“网页文件内容”之间产生联系,就应该将“用户搜索内容”和“网页文件”分为很小的单元 (这个单元就是关键词),寻找用户搜索单元是否出现在这个文档之中,如果出现就证明这个网页文件和用户搜索内容有关系,如果该搜索单元在这篇文章中出现的次数较高,也就证明:这篇文章与搜索内容有很强的相关性,这就是权值(weight)。
权值可以自己定义:比如标题出现一次对应的权值为10,内容出现一次对应的权值为5,再分别统计标题和文档内容中该搜素单元出现的次数。总权值(该搜索单元)= 标题出现的次数*10 +文档内容出现的次数*5;再将用户所有的搜索单元的总权值加在一起就是这篇文章与用户搜索内容的相关性。我们可以通过每一篇文档的权值去进行排序,给用户呈现出最想要的文档内容。
如何去存储这些网页文档内容呢?
网页文档内容有 标题,网页文档内容 url网址三个部分。所以就需要结构体将他们组织在一起。我们可以选择线性容器进行存储,因为线性容器存储的位置就可以代表这篇文章的 文档ID。
那么现在面临的问题就是,用户搜索单元(用户搜索关键词)和文档单元(文档关键词)之间如何建立联系。下面采用正排索引和倒排索引去建立它们之间的关系。
建立索引:
什么是正排索引?
正排索引就是文档ID与文档之间的关系。
文档ID | 文档内容 |
0 | 文档1 |
1 | 文档2 |
正排索引的建立,就是将文档ID与文档内容之间进行直接关联。如上表所示。
那问题来了,该如何关联呢?我们可以利用线性表,如数组,数组下标与文档ID正好是对应的,我们将解析出来的数据进行提取,存放到一个包含 标题(title),内容(content),url(网址信息)的结构体,再将结构体放到数组中,这样就建立好了正排索引。
什么是倒排索引?
比如用户搜索 菜鸡爱玩,分词工具将菜鸡爱玩分为 菜鸡和爱玩,分别用菜鸡和爱玩去文档中找对应的关键词。再将关键词存在的 文档ID 与 搜索关键词 之间建立关系。
关键词(唯一性)(关键词) | 文档ID,权重weigh(倒排索引拉链) |
菜鸡 | 文档2,文档1 |
爱玩 | 文档2 |
首先将处理好的数据进行关键词分割,用inverted_index(是map容器,map<关键词,倒排索引拉链>)统计关键词都出现在那些文档中,将关键词出现的这些文档放进倒排索引拉链中,这就行形成了关键词与文档ID之间的对应关系。从上面表可以看出,同一个文档ID是可以出现在不同的倒排索引拉链中的。
然而,刚开始建立索引的过程是有些慢的,很吃系统资源,所以关于网页文档内容太大并且服务器资源比较少的话,就会建立失败,因此前面才会下载Boost库的部分文件,也就是网络文件,而不是全部文件。虽然这个过程慢,但是带来的好处,还是不小的,因为索引建立过程是不会进行搜索的,当建立好之后,只要你有搜索内容,我就去inverted_index的map容器中进行查找,找到对应的倒排索引拉链,再返回。
当搜索关键词到来时,我就在inverted_index中利用关键词去找,如果存在这个关键词,那所有与这个关键词相关的文档我都找到了,如果不存在,那真就不存在。
这里的搜索关键词可能不止一个,搜索者会输入一段搜索语句,比如"菜鸡爱玩"可能会被分成“菜”“鸡”“菜鸡“”爱"“玩""爱玩”等。
正排索引代码:
DocInfo *BuildForwardIndex(const std::string &line) { //1. 解析line,字符串切分 //line -> 3 string, title, content, url std::vector<std::string> results; const std::string sep = "\3"; //行内分隔符 ns_util::StringUtil::Split(line, &results, sep); //ns_util::StringUtil::CutString(line, &results, sep); if(results.size() != 3){ return nullptr; } //2. 字符串进行填充到DocIinfo DocInfo doc; doc.title = results[0]; //title doc.content = results[1]; //content doc.url = results[2]; ///url doc.doc_id = forward_index.size(); //先进行保存id,在插入,对应的id就是当前doc在vector中的下标! //3. 插入到正排索引的vector forward_index.push_back(std::move(doc)); //doc,html文件内容 return &forward_index.back(); }
正排索引建立好之后,将构建好的结构体返回回去,交给倒排索引进行构建倒排索引拉链。
因为倒排索引的构建需要文档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];
因为正排索引被构建了,所以直接利用文档ID在正排索引拉链(存放文档的结构体数组)中进行查找就可以了。
什么是权值?
权值决定这篇文档与用户搜索内容之间是否存在关系以及体现出它们之间相关性的强弱,因为每篇文章关于一个话题的侧重点不一样,所以我们就用权值的大小来区分是否是用户最想要的,将文档与搜索关键词之间的关系用关键词出现在标题和文档内容中的次数 和自定义权值大小 进行相关计算。
比如标题出现一次对应的权值为10,内容出现一次对应的权值为5,再分别统计标题和文档内容中该搜素单元出现的次数。总权值(该搜索单元)= 标题出现的次数*10 +文档内容出现的次数*5;再将用户所有的搜索单元的总权值加在一起就是这篇文章与用户搜索内容的相关性。我们可以通过每一篇文档的权值去进行排序,给用户呈现出最想要的文档内容。
你认为标题与搜索关键词的相关性大,就将标题的权值设置高点,同理,文档内容也是一样的。
倒排索引代码:
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_map<std::string, word_cnt> word_map; //用来暂存词频的映射表 //对标题进行分词 std::vector<std::string> title_words; ns_util::JiebaUtil::CutString(doc.title, &title_words); //if(doc.doc_id == 1572){ // for(auto &s : title_words){ // std::cout << "title: " << s << std::endl; // } //} //对标题进行词频统计 for(std::string s : title_words){ boost::to_lower(s); //需要统一转化成为小写 word_map[s].title_cnt++; //如果存在就获取,如果不存在就新建 } //对文档内容进行分词 std::vector<std::string> content_words; ns_util::JiebaUtil::CutString(doc.content, &content_words); //if(doc.doc_id == 1572){ // for(auto &s : content_words){ // std::cout << "content: " << s << std::endl; // } //} //对内容进行词频统计 for(std::string s : content_words){ boost::to_lower(s); word_map[s].content_cnt++; } #define X 10 #define Y 1 //Hello,hello,HELLO for(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 = inverted_index[word_pair.first]; inverted_list.push_back(std::move(item)); } return true; }
重点代码讲解:
1 —— InvertedList &inverted_list = inverted_index[word_pair.first]; 2 —— inverted_list.push_back(std::move(item));
倒排索引拉链inverted_index是一个map<关键词,倒排索引拉链>,上面代码第一条就是将关键词对应的倒排索引拉链获取到,再将新的InvertedElem结构体插到倒排索引拉链中。这两条语句是可以合并的,看起来就会有些复杂。
经过上述操作于是就成功建立了的关键词和文档ID之间的关系,也就是说,我输入一段关键词,用分词工具将关键词进行分离,用分离的关键词,在文档(标题,文档内容也进行了分词)中进行查找,因为使用了同一套分词工具,所以不会出现,文档中有该关键词,而搜不到的情况。
获取倒排索引拉链:
//根据关键字string,获得倒排拉链 InvertedList *GetInvertedList(const std::string &word) { auto iter = inverted_index.find(word); if(iter == inverted_index.end()){ std::cerr << word << " have no InvertedList" << std::endl; return nullptr; } return &(iter->second); }
在倒排索引构建好之后,所有的倒排索引拉链都存放在inverted_index的map容器中,只需要提供关键词进行查找即可,将找到的倒排索引拉链返回出去。
构建索引(整合正排索引和倒排索引的构建):
//根据去标签,格式化之后的文档,构建正排和倒排索引 //data/raw_html/raw.txt bool BuildIndex(const std::string &input) //parse处理完毕的数据交给我 { std::ifstream in(input, std::ios::in | std::ios::binary); if(!in.is_open()){ std::cerr << "sorry, " << input << " open error" << std::endl; return false; } std::string line; int count = 0; while(std::getline(in, line)){ DocInfo * doc = BuildForwardIndex(line); if(nullptr == doc){ std::cerr << "build " << line << " error" << std::endl; //for deubg continue; } BuildInvertedIndex(*doc); count++; //if(count % 50 == 0){ //std::cout <<"当前已经建立的索引文档: " << count <<std::endl; LOG(NORMAL, "当前的已经建立的索引文档: " + std::to_string(count)); //} } return true; }
首先将处理好的网页文件读取取进来,利用std::ifstream类对文件进行相关操作,因为是以'\n'为间隔,将处理好的网页文件进行了分离,所以就采用getline(in,line)循环将文件中的数据读取到。
首先建立正排索引,其次再建立倒排索引,因为倒排索引的建立是基于正排索引的。
单例模式:
Index(){} //但是一定要有函数体,不能delete Index(const Index&) = delete; Index& operator=(const Index&) = delete; static Index* instance; static std::mutex mtx; public: ~Index(){} public: static Index* GetInstance() { if(nullptr == instance){ mtx.lock(); if(nullptr == instance){ instance = new Index(); } mtx.unlock(); } return instance; }
单例模式,就是禁掉这个类的,拷贝构造和赋值重载,让这个类不能赋给别人,所有对象共用一个instance变量
因为在多线程模式下,会有很用户进行搜素,需要加把锁保证临界区资源不被破坏。
索引构建模块的整体代码Index.hpp:
#pragma once #include <iostream> #include <string> #include <vector> #include <fstream> #include <unordered_map> #include <mutex> #include "util.hpp" #include "log.hpp" namespace ns_index{ struct DocInfo{ std::string title; //文档的标题 std::string content; //文档对应的去标签之后的内容 std::string url; //官网文档url uint64_t doc_id; //文档的ID,暂时先不做过多理解 }; struct InvertedElem{ uint64_t doc_id; std::string word; int weight; InvertedElem():weight(0){} }; //倒排拉链 typedef std::vector<InvertedElem> InvertedList; class Index{ private: //正排索引的数据结构用数组,数组的下标天然是文档的ID std::vector<DocInfo> forward_index; //正排索引 //倒排索引一定是一个关键字和一组(个)InvertedElem对应[关键字和倒排拉链的映射关系] std::unordered_map<std::string, InvertedList> inverted_index; private: Index(){} //但是一定要有函数体,不能delete Index(const Index&) = delete; Index& operator=(const Index&) = delete; static Index* instance; static std::mutex mtx; public: ~Index(){} public: static Index* GetInstance() { if(nullptr == instance){ mtx.lock(); if(nullptr == instance){ instance = new Index(); } mtx.unlock(); } return instance; } //根据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 = inverted_index.find(word); if(iter == inverted_index.end()){ std::cerr << word << " have no InvertedList" << std::endl; return nullptr; } return &(iter->second); } //根据去标签,格式化之后的文档,构建正排和倒排索引 //data/raw_html/raw.txt bool BuildIndex(const std::string &input) //parse处理完毕的数据交给我 { std::ifstream in(input, std::ios::in | std::ios::binary); if(!in.is_open()){ std::cerr << "sorry, " << input << " open error" << std::endl; return false; } std::string line; int count = 0; while(std::getline(in, line)){ DocInfo * doc = BuildForwardIndex(line); if(nullptr == doc){ std::cerr << "build " << line << " error" << std::endl; //for deubg continue; } BuildInvertedIndex(*doc); count++; //if(count % 50 == 0){ //std::cout <<"当前已经建立的索引文档: " << count <<std::endl; LOG(NORMAL, "当前的已经建立的索引文档: " + std::to_string(count)); //} } return true; } private: DocInfo *BuildForwardIndex(const std::string &line) { //1. 解析line,字符串切分 //line -> 3 string, title, content, url std::vector<std::string> results; const std::string sep = "\3"; //行内分隔符 ns_util::StringUtil::Split(line, &results, sep); //ns_util::StringUtil::CutString(line, &results, sep); if(results.size() != 3){ return nullptr; } //2. 字符串进行填充到DocIinfo DocInfo doc; doc.title = results[0]; //title doc.content = results[1]; //content doc.url = results[2]; ///url doc.doc_id = forward_index.size(); //先进行保存id,在插入,对应的id就是当前doc在vector中的下标! //3. 插入到正排索引的vector forward_index.push_back(std::move(doc)); //doc,html文件内容 return &forward_index.back(); } 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_map<std::string, word_cnt> word_map; //用来暂存词频的映射表 //对标题进行分词 std::vector<std::string> title_words; ns_util::JiebaUtil::CutString(doc.title, &title_words); //if(doc.doc_id == 1572){ // for(auto &s : title_words){ // std::cout << "title: " << s << std::endl; // } //} //对标题进行词频统计 for(std::string s : title_words){ boost::to_lower(s); //需要统一转化成为小写 word_map[s].title_cnt++; //如果存在就获取,如果不存在就新建 } //对文档内容进行分词 std::vector<std::string> content_words; ns_util::JiebaUtil::CutString(doc.content, &content_words); //if(doc.doc_id == 1572){ // for(auto &s : content_words){ // std::cout << "content: " << s << std::endl; // } //} //对内容进行词频统计 for(std::string s : content_words){ boost::to_lower(s); word_map[s].content_cnt++; } #define X 10 #define Y 1 //Hello,hello,HELLO for(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 = inverted_index[word_pair.first]; inverted_list.push_back(std::move(item)); } return true; } }; Index* Index::instance = nullptr; std::mutex Index::mtx; }
排序语句是一条lambda表达式,你也可以写个仿函数传递给sort系统函数。
//4.[构建]:根据查找出来的结果,构建json串 -- jsoncpp --通过jsoncpp完成序列化&&反序列化 Json::Value root; for(auto &item : inverted_list_all){ ns_index::DocInfo * doc = index->GetForwardIndex(item.doc_id); if(nullptr == doc){ continue; } Json::Value elem; elem["title"] = doc->title; elem["desc"] = GetDesc(doc->content, item.words[0]); //content是文档的去标签的结果,但是不是我们想要的,我们要的是一部分 TODO elem["url"] = doc->url; //for deubg, for delete elem["id"] = (int)item.doc_id; elem["weight"] = item.weight; //int->string root.append(elem); } //Json::StyledWriter writer; Json::FastWriter writer; *json_string = writer.write(root);