this is search engine project, and also the second project during the period of learning on WangDao
-
创建字典 输入文件,去掉数字,标点,停用词 存储结果为 word-frequency 形式
-
创建词典的索引文件 创建词典索引文件的原因是为了之后模糊匹配(最小编辑距离算法)。 索引以 26 个字母为key 以 set<line_no> 为 value,即存储该字母在字中出现的行号(set具有自动去重的功能)。如果是value直接存储整个单词,否则会存在很多冗余信息
注意:如果想显示一下索引文件行号,需要注意文本文件行号从 1 开始计算(调试时用,在内存中实际程序处理没必要考虑这个问题)
- 搭建服务器框架:使用 Reactor + ThreadPool 模型
- 客户端框架
- 关键词推荐模块
- 缓存系统
- 网页查询模块
- 协议解析模块
1. 首先客户端输入关键词,发送给服务器,服务器接收关键词
2. 将关键词传入 KeyRecommender 对象中,进行关键词查询
3. KeyRecommender将接收的关键词格式化(去数字,标点,转小写)
4. 按照单词逐字母去字典索引文件中查找匹配的关键词在字典文件的位置
1. 如果在字典中找到 pretty math 个数 >=5 , 用最小编辑距离算法直接排序,选出 5 个作为推荐词存到推荐词 vector<string> 数组中
2. 如果在字典中找到 pretty math 个数 < 5 , 讲现有的 pretty match 词压入推荐词数组中,剩下不足个数的需要去遍历整个字典,对每一个单词都是用最小编辑距离算法并排序,选出足够个数的单词,压入推荐词数组(模糊匹配)
5. 将推荐词数组vector<string>返回给Task中
6. 服务器先将数据用 json 打包,然后返回给客户端
由于上述关键词推荐模块的实现,每次服务端对输入的关键词进行解析后都要用短距离优先算法进行计算,然而类比现实生活中的情景往往是有一个 二八原则( 20% 的热词被查询的时间占 80% ),因此有必要引入一个缓存系统,其核心**是利用空间换时间,缓存系统的在计算机软硬件体系中非常常见也十分重要
方案一:
由于查询关键词过程是在线程池中的线程中实现,因此需要对每一个线程都设置一个缓存,同时为了保证缓存的一致性问题,需要引入一个定时器线程,每隔一段事件处理缓存一致性的问题,并且将缓存写入文件持久化(防止程序意外崩溃)。注意这里的定时器线程实际上只是一个定时器任务,将定时器功能打包成一个任务丢进线程池中(注册定时任务函数),
方案二:
只设置一个缓存,因为缓存在内存中,可能但缓存并不会称为该程序的性能瓶颈,其他所有线程去访问该缓存时需要加锁来防止冲突,但是也可以参考一点点redis的设计理念,使用epoll来管理所有访问缓存的事件,这样也能够实现无冲突访问缓存
方案三:
直接使用redis提供的接口
LURCache设计注意事项:
1. 要使用 unordered_map 来映射缓存链表,缓存链表的头尾分别表示最近被使用和最久未被使用的数据,unordered_map是为了将原本直接遍历访问Cache链表访问方式改成用哈希映射,从而将时间复杂度降到O1级别(空间换时间)
2. 每次写缓存要将旧的数据项(本来不在缓存中,和本来就在缓存中两种情况)和对应的哈希映射表中的项删掉
3. 每次读缓存,要将老的数据提到链表头部,表示这个数据最近被使用过(LRU理念)
4. 如果读缓存没有读到,则去读数据源的数据,读到之后同时需要进行更新缓存的操作
5. 由于在每个线程都单独开了一个缓存,为了解决缓存一致性问题,需要引入一个定时器线程,在每次时间到的时候更新。网上也有不用的方案,可以是引入一级缓存和二级缓存等概念,将所有的一级缓存数据同步到二级缓存中。方案很多。。。(这里我先留个坑。)如果是单缓存方案,就不会有一致性问题,只要每次在读写缓存时上锁就行。需要在性能和复杂度之间 trade—off.
最小编辑距离算法
动态规划算法
模糊匹配算法
1. 去词典寻找完美匹配的单词(最多仅有一个)
2. 使用最小编辑距离算法寻找模糊匹配的单词
优先级:编辑距离 > 词频 > (字母顺序);
这里我没有考虑字母的排序了,这个不是重点,写起来写很冗长
LRUCache 算法
借鉴 LeetCode 上的LRUCache算法,将其进一步封装,并将其改写成类模板形式,便于在项目中使用
-
离线部分
-
建立网页库:XML/RSS文件解析: 从配置文件读取: xml文件路径 放到vector xmlfilePaths 停用词文档路径 放到_stopWordPath; 存储parsedPage.dat 的路径,offset.dat路径; 一、 对指定路径中的XML文件进行解析,提取有用信息,将每一篇文章格式化为一篇WebPage,然后将所有Webpage 压入vector中形成网页库 vector,同时建立网页偏移库(类似于于网页的一个目录结构)
二、 统计每一篇文章词频和热词,因为有点复杂,将这一步单独写成一个函数,其中统计热词需要根据词频 map<string, int>,要利用第二个类型int对词频进行排序,但是map无法直接使用sort进行排序 ,需先将其转换成 vector<pair<int, string>>,然后使用sort,由于类型是pair,sort需要填入第三个参数,是个可调用对象,这里用 lambda 可以大大简化代码量
三、 根据文章的词频信息建立网页倒排索引库(wordStrength = TF * IDF)算法计算所有单词的 wordStrength,最后使用进行归一化处理后的权重作为实际的权重值放入倒排索引表中。 需要知道的信息: 某单词在本文章中的词频 = webPage._wordMap[word]; 含有该单词文章的篇数 ---> webPage.wordmap.find(word) != end(); 文章的总数 = _webPageLib.size();
TF: Term Frequency, 单词在某一篇文档中出现的次数; DF: Document Frequency, 在文档集合 N 中,包含该词语的文档数量; IDF: Inverse Document Frequency, 逆文档频率,表示某一单词对于该篇文章的重要性的一个系数,其计算公式为:IDF = log2(N/(DF+1))
一篇文档包含多个词语w1,w2,...,wn,权重系数进行归一化处理: w' = w /sqrt(w1^2 + w2^2 +...+ wn^2) w' 才是需要保存下来的
-
在线部分 这部分需要结合整个服务器框架来进行执行
输入n个关键词, 计算每个关键词的权重,拼接成一个X[_w1, _w2..., _wn向量,同时,通过关键词去倒排索引表中查询该这些关键词所在的网页(需要输入的单词在同一篇文章中同时出现),取出每个单词在该文章中的权重组成一个向量 Y[w1, w2, ...wn], 这意味着两个向量是同样的维度,因此才能够计算余弦相似度 我焯,这一部分属于是数学,针对嵌套容器操作有点复杂,理清楚逻辑关系就绪,其他到没有什么比较难的地方
注意事项:
错误写法, 使用下标运算符作为等号右边可能使得程序崩溃 // auto value = _cachesManager._LRUCacheGroup[thid].getValue(_userInputWord)
注意使用 at,使用at会进行检查 auto value = _cachesManager._LRUCacheGroup.at(thid).getValue(_userInputWord);
set_intersection 对容器元素取交集的时候需要操作的容器必须是有序的容器 set_intersection 可以填入一个适配器参数,让容器能实现对pair的第一个元素取交集
-
实现一个自定义的协议解析单元,参考网络协议的实现原理,客户端发送数据时,在【原始数据rawData】头部添加一个【协议号 protocol_ID】,将其打包 用于区分不同的命令, 服务器收到客户端发送的数据包,先将其按照约定好的规则进行解析,将其拆分为【协议号】 和【原始数据】,对协议号进行判断,将原始数据打包成对应不同的任务来执行
[1]<--->[100] 关键词推荐任务
[2]<--->[200] 网页搜索任务