百度,搜狗,头条新闻客户端等搜索是全网搜索。而我做的是基于boost库的站内搜索,类似于cplusplus.com,这类搜索的特点是:搜索的数据更加垂直,范围中的关联性更强,数据量更小。
网页返回的信息有三部分:
网页的title,网页内容的摘要描述,即将跳转的网址url。
boost的官网是没有站内搜索的。建立doc/库下的搜索引擎。
获得数据源的方式是把库下下来(可升级为爬虫程序)
访问方式浏览器输入IP,端口号8082
去标签和数据处理。在全网当中通过爬虫程序取出广告,保留标题,内容和url。
建立索引信息,方便快速查找。正排索引(id->文档),倒排索引(文档关键字->所在文档的id)
http请求的方式,进行搜索任务。通过GET的方式,上传搜索关键字。
server检索索引,得到对应的标题,内容和网址,相关的html。
拼接多个网页的title+desc+url,构建一个网页,返回给用户。
数据清理应该得到的文档信息:
就是从文档ID找到文档内容(文档中的关键字),数组下标就是一种映射关系
搜索引擎停止词:了,的,吗,一般是在分词的时候先忽略,区分唯一性价值不大。
根据关键字找到文档ID,根据文档内容,分词,整理不重复的各个关键字,对应联系到文档ID的方案。
用户输入:大学生->倒排索引->提取文档ID(1,2)->根据正排索引找到文档内容->title+desc+url文档结果进行摘要->构建响应结果。
下载Boost库:doc/html目录里面都是各种网页html充当数据源。
data/input下放的就是从doc目录下拷贝过来的数据源,建立索引,拼接url,构建跳转url。
<>括里面的:html的标签,对我们进行搜索的操作是没用的,所以需要去掉这个标签,一般的标签是成对出现的。
清洗之后的结果放到raw_html中,干净的文档放到input文件夹中。我们下载下来的一共有8171个。
目标:把每个文档都去标签清洗之后,然后写入到同一个文件中,每个文档内容之间不需要换行,只占一行!文档和文档之间用\3进行区分。控制字符属于不可显示字符中3是^C,不会污染原来的内容。
如果参数的类型是const& :输入,*:输出,&:输入输出
将文档内容进行读取,放到vector
中,EnumFile();
引入boost库中的filesystem中的方法,boost库的安装。链接时报错,makefile中需要引入两个库,-lboost_system -lboostfilesystem
首先需要用路径构建对象才能够调用接下来的方法。
判断你给的起始路径是否存在,如果没有数据源就没有了
boost::filesystem::exists();
判断路径中的文件是否是普通文件(不能是图片之类的)引入boost库中的is_regular_fille();
挑选后缀是.html的文件-> 迭代器对象中的iter->path().extension()=='.html';
判断路径 名的后缀是否是.html
将符合条件的文件的路径插入到file_list中,因为我们用原目录构造了一个Boost库中的对象采用的上面的方法,但是插入的时候需要将路径对象转化为字符串类型才能插入,所以引入对象.string()的方法。
读取file_list中的内容,并对内容进行解析ParseHtml();放到DocInfo_t结构体对象中,最后形成了8000+个对象,放到vector
读取文件,util.hpp/FileUtil::ReadFile();打开文件失败就去找下一个文件
采用std::ifstream对文件进行打开,然后使用getline()将文件中的内容按行读取出来。
解析指定文件提取title-》ParseTitle();
就是在文档中搜索
,中间就是title内容,查找关键字,然后前闭后开区间
解析指定文件提取content-》ParseContent();
解析指定文件路径构建url-》ParseUrl();
Boost库的官方文档,和我们下载的文档是有路径的关联的。
下载文件的路径:
官网路径:
为了拼接网页url,所以只需要用https://www.boost.org/doc/libs/1_80_0/doc/html/替换data/input就行了。
解析文件内容放到output中,按照\3为分隔符SaveHtml();
同一个文档中区分出来的title content url部分之间用\3分割,不同文档之间用\n来分割,这样保存文件便于在下一次读取的时候方便用getline()依次读取到各个文档的完整内容。
title\3content\3url\ntitle\3content\3url
以二进制的方式写入文件,这样原文档是什么样写入的就是什么样。
数据结构采用类似数组,数组下标天然是文档的id。vector<>
会存在多个结果对应一个关键字,一个关键字和一组InvertedElem对应。采用unordered_map对应关系,用vector 承装一组节点,给他整体typedef起个名字InvertedList,倒排拉链。InvertedElem结构体中成装的是对应文档的id,关键字,和权重(便于后续构建网页时的排序)。
构成了关键字和倒排拉链的关系。
根据docid获取文档内容。vector<>根据下标直接返回DocInfo* 。
根据关键字string unordered_map查找关键字返回拉链指针。
需要parse处理完毕的数据的路径给我,构建正排倒排索引
读取文件内容,按行读取文件
建立正排索引,返回根据读取的line填充好的DocInfo信息结构体
解析Line,字符串切分Util.hpp/StringUtil::Split();返回值是vector<>
->Boost库中的split()方法
字符串进行填充DocInfo中
doc_id其实就是数组下标,我第一次插,之前的大小是0,我差的位置也是0。所以先更新id再插入。
doc.doc_id=forward_index.size();
插入到正排索引的vector中
返回最后一个元素的地址DocInfo*,采用vector<>.back();
建立倒排索引
//我们拿到的文档内容
struct DocInfo{std::string title; //文档标题std::string content; //文档对应的去标签之后的内容std::string url; //官网文档urluint64_t doc_id; //文档的id};
//拉链节点
struct InvertedElem{uint64_t doc_id;int weight;std::string word;};
//根据文档内容,形成一个或者多个文档描述结构体INvertedElem。
//一个文档包含很多词,都应该对应到当前的dic_id
1. 对title&content进行分词--使用cppjieba分词
2. 词频统计来充当相关性(标题中出现相关性更高一点)
遍历内容,词频++。
unordered_map count;
class word_cnt
{title_cnt;word_cnt;
};
3. 自定义相关性
遍历word_cnt,拿到每一个词,由map得到具体一个词的和文档的对应关系,当有多个不同的词指向同一个文档,由相关性决定优先显示哪一个。
获取jieba,然后git clone即可。词库在dict软连接下,头文件在inc软连接下。将cppjieba/deps下的limonp拷贝到include/cppjieba 中,各种头文件.为了让demo.cpp能够编译通过。
编写到倒排索引
对标题内容进行分词和词频统计,word_map(unordered_map)用来暂存词频的映射表.[]如果存在就操作,如果不存在就新建添加。
对文档中内容进行分词和词频统计,大小写不区分,所以都转换成小写再进行统计。使用boost::tolower();因为不对原来词汇进行修改,所以for()遍历时不加&,都是拷贝。呈现上不修改原文档,算法上统一用小写实现了忽略大小写的目的。
建立倒排索引-因为我们是按个文档走下来的,所以只需要遍历词频表,计算当前文档和分出来的那些词之间的关系,然后放在InvertedElem结构体中,构建一个倒排拉链的节点就行。
然后将词依次插入到倒排索引的unorered_map中,映射的是一个倒排拉链。[]的优势就是如果有就是拉链的引用,如果没有就新建一个。然后因为倒排拉链是vector所以只需要将填充好的item插入就行了。
- 随后会建立各种分词和此文档之间的关系,即倒排拉链倒排索引。
初始化InitSeacher():
获取创建index对象-单例模式(最好只有一份index)
index的拷贝构造和赋值构造都禁用,设置static Index* instance;初始化:Index* Index::instance=nullptr;
编写获取单例的函数GetInstance();会存在线程安全问题,引入c++中的mutex。
两层判断是否是第一次建立索引,减少很多加锁和申请锁的过程。
根据Index对象建立索引
index->BuildIndex();
搜索服务的接口Seacher():搜索关键字qurey,返回给用户浏览器的json_string串。
[分词]:我们的搜索关键字query也要在服务端进行分词方便查找
util.hpp/JiebaUtil::Cutstring();
[触发]:就是根据分词的各个词进行index查找
->对关键字的每一个分词进行遍历,
->查找每个词的倒排拉链,index->GetIndexList();
->将所有的拉链整合到vector<>数组中,inverted_list_all.insert(inverted_list_all.end(),inverted_list->begin(),inverted_list->end());
帮助理解图:
- 存在问题:可能不同的两个词的倒排索引是一样的,所以就要去重。(最后处理)
[合并排序]:汇总查找结果,按照相关性降序排列sort();
根据相关性倒排,所以需要写回调函数用lambda表达式,每两个倒排拉链之间要进行排序,也就是不同关键词分词在文档中的相关性进行大小排序。
[构建]:根据查找结果构建json_string(jsoncpp)
根据拉链中元素的doc_id进行正排索引获取到DocInfo*=index->GetForwordList();
将拉链中每一个节点的doc结构体中的属性填充给Json::Value::item,title,content并不要全部只要一部分就行,url。
string GetDesc();找到在文档中首次出现的位置,往前一部分,往后一部分截取出来。
判断边界时,都用加法实现都是正整数。一部分随意决定字符个数,如果前面没有50个就从开头来,同理结尾。
int start =0;int end =html_content.size()-1;//如果之前没有50字节,就从头开始//如果有50字节,就从倒数第50字节开始//加法全变成正整数if(pos>start+prev_step ) start = pos-prev_step;if(pos =end) return "None2";std::string desc = html_content.substr(start,end-start);desc+="...";return desc;
检查条件:url是否正确,排序是否是按照权重排序的。
填充进Json::Value::root中,然后整体写入到*json_string中返回。
搜索结果desc的时候出现空值的情况,也就是索引有问题或者是摘要有问题。
排序是否是按照权重排序的?权值和预期结果不太匹配
cpp-libhttplib需要更高版本的gcc编译器。gcc -v查看编译器版本信息。配置bash/profile文件使得每次都是较为新版本的gcc。
安装cpp-httplib
最新的版本会使得不是较新的gcc编译之后在运行的时候出现问题。
只需要httplib.h文件引入到项目中就行。
编译时需要引入-lpthread
首先需要初始化Search,也就是需要创建单例,获取对象索引信息。
content_type对照表
去重,也就是很多分词都是同一个文档,导致收到同一个内容。
struct InvertedElemPrint{}; tokes_map;
去重去掉的就是相同的Inverted_Elem,如果同一个doc_id之下的文档的很多分词,每一个分词分别获取自己的倒排拉链,遍历这些个分词的拉链节点,doc_id相同的就重叠处理,然后权值相加,并把它们放到一个数组当中,整体处理统一返回。将重新处理之后的Elem插入到InvertedListall链表之中(之前是在遍历关键字切分数组的时候直接将链表插入InvertedListall链表)。
此时获取摘要的时候item 就不止是一个单词了,而是一个填充了很多docid相同的词的数组 words[]数组,那么我只需要取其中一个获取摘要就行了。