Boost搜索引擎:如何建立 用户搜索内容 与 网页文件内容 之间的关系
如果想使“用户搜索内容”和“网页文件内容”之间产生联系,就应该将“用户搜索内容”和“网页文件”分为很小的单元 (这个单元就是关键词),寻找用户搜索单元是否出现在这个文档之中,如果出现就证明这个网页文件和用户搜索内容有关系,如果该搜索单元在这篇文章中出现的次数较高,也就证明:这篇文章与搜索内容有很强的相关性,这就是权值(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;
}
搜索模块:
搜索模块是在服务器构建索引之后进行的,在构建好的索引的服务器上进行关键词搜索。
首先将用户提供的搜索内容进行,关键词分割,将分割好的关键词存放到一个数组中,再去遍历这个数组,里面的每一个元素都是一个搜索关键词,再调用Index索引构建模块中的查找倒排索引函数,找到与关键词相关的文档,再将这些文档存入tokens_map的map容器中。
如果用户搜索关键词在网页文档中存在的情况下,一个关键词对应一个倒排索引拉链(需要了解倒排索引拉链,以及每个结构体中的成员)。
tokens_map的map容器中存储的是文档ID和struct InvertedElemPrint结构体之间的对应关系。
struct InvertedElemPrint{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint():doc_id(0), weight(0){}
};
该结构体中存放的是这篇文档的文档ID,权值(所有关键词权值的总和),words容器中存的是那些关键词出现在了这篇文档中。我们可以利用这个words容器进行文章摘要的的提取,下面会提到。
将不同关键词出现在同一文档中的权值进行加和,为了体现这篇文章与搜索内容之间的关系,权值越大表明这篇文章与搜索内容具有很强的相关性。
std::vector<InvertedElemPrint> inverted_list_all;
将 std::unordered_map<uint64_t, InvertedElemPrint> tokens_map 中的文档全部放到inverted_list_all的vector容器利用总权值中进行排序,为用户呈现出最想要的内容。
std::sort(inverted_list_all.begin(), inverted_list_all.end(),\
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2){
return e1.weight > e2.weight;
});
排序语句是一条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);
最后将vector排好序的数据进行json串的构建,传递出去。 对于json相关知识不太了解的话,请搜所相关资料简单学习。
搜索模块代码:
//query: 搜索关键字
//json_string: 返回给用户浏览器的搜索结果
void Search(const std::string &query, std::string *json_string)
{
//1.[分词]:对我们的query进行按照searcher的要求进行分词
std::vector<std::string> words;
ns_util::JiebaUtil::CutString(query, &words);
//2.[触发]:就是根据分词的各个"词",进行index查找,建立index是忽略大小写,所以搜索,关键字也需要
//ns_index::InvertedList inverted_list_all; //内部InvertedElem
std::vector<InvertedElemPrint> inverted_list_all;
std::unordered_map<uint64_t, InvertedElemPrint> tokens_map;
for(std::string word : words){
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if(nullptr == inverted_list){
continue;
}
//不完美的地方:暂时可以交给大家 , 你/是/一个/好人 100
//inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
for(const auto &elem : *inverted_list){
auto &item = tokens_map[elem.doc_id]; //[]:如果存在直接获取,如果不存在新建
//item一定是doc_id相同的print节点
item.doc_id = elem.doc_id;
item.weight += elem.weight;
item.words.push_back(elem.word);
}
}
for(const auto &item : tokens_map){
inverted_list_all.push_back(std::move(item.second));
}
//3.[合并排序]:汇总查找结果,按照相关性(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;
// });
std::sort(inverted_list_all.begin(), inverted_list_all.end(),\
[](const InvertedElemPrint &e1, const InvertedElemPrint &e2){
return e1.weight > e2.weight;
});
//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);
}
文档摘要:
在讲struct InvertedElemPrint结构体时,我就提过摘要的获取.
struct InvertedElemPrint{
uint64_t doc_id;
int weight;
std::vector<std::string> words;
InvertedElemPrint():doc_id(0), weight(0){}
};
这里详细讲一下,对于words容器中存的是用户传上来的搜索关键词,是部分也可能是全部,这不重要。
我们在实现摘要提取时,是以words中第一个关键词为准。这里有人会问,为什么这样做?
原因是:我想这么做,图方便。但是有没有更优的办法,当然有,不然我也不肯提这个问题。
那怎么做呢?
for(std::string word : words){
boost::to_lower(word);
ns_index::InvertedList *inverted_list = index->GetInvertedList(word);
if(nullptr == inverted_list){
continue;
}
//不完美的地方:暂时可以交给大家 , 你/是/一个/好人 100
//inverted_list_all.insert(inverted_list_all.end(), inverted_list->begin(), inverted_list->end());
for(const auto &elem : *inverted_list){
auto &item = tokens_map[elem.doc_id]; //[]:如果存在直接获取,如果不存在新建
//item一定是doc_id相同的print节点
item.doc_id = elem.doc_id;
item.weight += elem.weight;
item.words.push_back(elem.word);
}
}
上面代码是Search()函数中,提取用户搜索关键词的倒排索引拉链,大家应该不陌生了吧。其实看懂上面的Search()函数,也可以想出来这样的解决方法,就是利用该关键词对应的权值进行排序。
我么可以创建一个优先级队列,再创建一个结构体,这个结构体成员就是:该关键词 和 该关键词对应的权值,再写一个仿函数compare()比较函数(利用权值去比较),将存进去的这些结构体进行排序,优先级队列实则就是一个大堆,第一个元素就是权值最大的,最后再对优先级队列进行遍历,将里面的元素全部插入到words容器中,这样就实现了关键词的排序。
我们在传入第一个关键词,给GetDesc()函数,去寻找该关键词周围的摘要。
std::string GetDesc(const std::string &html_content, const std::string &word)
{
//找到word在html_content中的首次出现,然后往前找50字节(如果没有,从begin开始),往后找100字节(如果没有,到end就可以的)
//截取出这部分内容
const int prev_step = 50;
const int next_step = 100;
//1. 找到首次出现
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. 获取start,end , std::size_t 无符号整数
int start = 0;
int end = html_content.size() - 1;
//如果之前有50+字符,就更新开始位置
if(pos > start + prev_step) start = pos - prev_step;
if(pos < end - next_step) end = pos + next_step;
//3. 截取子串,return
if(start >= end) return "None2";
std::string desc = html_content.substr(start, end - start);
desc += "...";
return desc;
GetDesc()函数这个函数没什么技术难度,就是在简单的字符串查找,以及字符串截取,至于截取多少,因人而异,同时也要切合实际。将截取的摘要放到json串中。
以上就是用户搜素内容和文档内容之间建立联系的过程,如有不懂尽可留言,你的留言是我最大的收获。
原文地址:https://blog.csdn.net/2201_75324712/article/details/140647238
免责声明:本站文章内容转载自网络资源,如本站内容侵犯了原著者的合法权益,可联系本站删除。更多内容请关注自学内容网(zxcms.com)!