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

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, urlstd::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. 字符串进行填充到DocIinfoDocInfo doc;doc.title = results[0]; //titledoc.content = results[1]; //contentdoc.url = results[2];   ///urldoc.doc_id = forward_index.size(); //先进行保存id,在插入,对应的id就是当前doc在vector中的下标!//3. 插入到正排索引的vectorforward_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,HELLOfor(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.txtbool 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 deubgcontinue;}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(){} //但是一定要有函数体,不能deleteIndex(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索引构建模块中的查找倒排索引函数,找到与关键词相关的文档,再将这些文档存入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是文档的去标签的结果,但是不是我们想要的,我们要的是一部分 TODOelem["url"]  = doc->url;//for deubg, for deleteelem["id"] = (int)item.doc_id;elem["weight"] = item.weight; //int->stringroot.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; //内部InvertedElemstd::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是文档的去标签的结果,但是不是我们想要的,我们要的是一部分 TODOelem["url"]  = doc->url;//for deubg, for deleteelem["id"] = (int)item.doc_id;elem["weight"] = item.weight; //int->stringroot.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. 截取子串,returnif(start >= end) return "None2";std::string desc = html_content.substr(start, end - start);desc += "...";return desc;

 GetDesc()函数这个函数没什么技术难度,就是在简单的字符串查找,以及字符串截取,至于截取多少,因人而异,同时也要切合实际。将截取的摘要放到json串中。 

以上就是用户搜素内容和文档内容之间建立联系的过程,如有不懂尽可留言,你的留言是我最大的收获。

相关文章:

  • 某某物联rabbitmqhttp二轮充电桩协议充电协议对接
  • 【.NET】asp.net core 程序重启容器后redis无法连接,连接超时
  • mariadb安装centos再次踩坑
  • 数学建模学习(1)遗传算法
  • 数据库结构之b树
  • canvas:矢量点转栅格
  • Google Cloud Platform数据工程简介
  • 网页隐藏版之一行小说阅读器
  • Pycharm软件Win 64位安装包+详细安装步骤 百度云
  • Window下安装Zookeeper
  • MYSQL存储引擎InnoDB, MyISAM简介
  • 高精度-----乘法
  • go--互斥锁
  • Linux发展史
  • Servlet生命周期
  • 【每日笔记】【Go学习笔记】2019-01-10 codis proxy处理流程
  • Angular 响应式表单之下拉框
  • ES6系统学习----从Apollo Client看解构赋值
  • Java的Interrupt与线程中断
  • Js实现点击查看全文(类似今日头条、知乎日报效果)
  • laravel with 查询列表限制条数
  • Magento 1.x 中文订单打印乱码
  • nginx(二):进阶配置介绍--rewrite用法,压缩,https虚拟主机等
  • TiDB 源码阅读系列文章(十)Chunk 和执行框架简介
  • vue 配置sass、scss全局变量
  • Vue--数据传输
  • 从PHP迁移至Golang - 基础篇
  • 对象管理器(defineProperty)学习笔记
  • 快速体验 Sentinel 集群限流功能,只需简单几步
  • 小程序测试方案初探
  • 做一名精致的JavaScripter 01:JavaScript简介
  • gunicorn工作原理
  • hi-nginx-1.3.4编译安装
  • ​软考-高级-系统架构设计师教程(清华第2版)【第15章 面向服务架构设计理论与实践(P527~554)-思维导图】​
  • #{} 和 ${}区别
  • #ifdef 的技巧用法
  • #数学建模# 线性规划问题的Matlab求解
  • #我与Java虚拟机的故事#连载02:“小蓝”陪伴的日日夜夜
  • (04)Hive的相关概念——order by 、sort by、distribute by 、cluster by
  • (Redis使用系列) Springboot 使用redis实现接口Api限流 十
  • (STM32笔记)九、RCC时钟树与时钟 第二部分
  • (二)换源+apt-get基础配置+搜狗拼音
  • (附源码)流浪动物保护平台的设计与实现 毕业设计 161154
  • (力扣记录)1448. 统计二叉树中好节点的数目
  • (南京观海微电子)——COF介绍
  • (亲测有效)解决windows11无法使用1500000波特率的问题
  • (亲测有效)推荐2024最新的免费漫画软件app,无广告,聚合全网资源!
  • (三分钟)速览传统边缘检测算子
  • (十三)MipMap
  • (限时免费)震惊!流落人间的haproxy宝典被找到了!一切玄妙尽在此处!
  • (源码版)2024美国大学生数学建模E题财产保险的可持续模型详解思路+具体代码季节性时序预测SARIMA天气预测建模
  • .jks文件(JAVA KeyStore)
  • .locked1、locked勒索病毒解密方法|勒索病毒解决|勒索病毒恢复|数据库修复
  • .NET 5种线程安全集合
  • .NET CLR基本术语