项目开源代码分享:boost_searcher
可能会被问到的面试内容:
-
为什么要做这个项目?/ 项目背景
主要是日常生活中经常使用搜索引擎,对于搜索引擎技术是如何实现的很感兴趣,再加上很想锻炼自己的C++工程代码能力,于是就在github上找到了这个开源项目,所以就选择开始做这个项目
-
介绍一下项目流程和架构设计
(先说一下项目背景)
项目主要分成下列模块:数据清洗模块,对boost的所有html文档进行解析,去除标签,提取核心数据,所有文档内容都在output普通文件中,\n作为每个文档的分隔符;索引模块,构建正排索引和倒排索引;搜索模块,基于索引,按照查询词查找出匹配的html文档;web模块,基于cpp-httplib搭建http服务器,提供http接口,编写前端页面。
(不是重点)数据清洗模块具体步骤:第一步,遍历boost的每个html文档,它们的 文件名+路径 保存在files_list中,方便后期对文档进行一个一个的读取。第二步,从files_list中读取每个文件的内容并进行解析,解析成一个一个的DocInfo_t结构体,结构体内部是文档的标题、文档内容、文档在官网的url,结构体放在results里。第三步,把解析完毕的各个文件内容写入到output普通文件中,\n作为每个文档的分隔符。
-
制作索引的流程是什么样?
数据清洗,对boost的所有html文档进行解析,去除标签,提取html中的标题,正文,url。所有文档内容都在output普通文件中,\n作为每个文档的分隔符。构建正排索引,对一个文档的字符串进行拆分,分成3个字符串,再把拆分后的字符串填充到DocInfo,构造成文档对象,通过一个数组管理起来,数组下标就是"文档id",得到文档id=>文档内容的映射。构建倒排索引,针对每个文档对象的标题和正文,进行分词和词频统计,分词结果作为key,文档id列表作为value,添加到一个哈希表中,得到词=>文档id列表的映射。在构建倒排过程中,在该文档中分别统计每个分词在标题和正文中出现的次数,作为后续排序依据(权重)
-
详细描述一下检索Search的流程?
分词,对用户输入的查询词进行分词;触发,根据分词结果,查倒排索引,获取文档id列表;归并,针对多个词触发的文档id进行归并,把相同的id的触发结果合并成一个,并提高权重;排序,根据权重的值降序排序;包装结果,根据文档id,去正排索引中查找,生成描述,最终构造出完整的返回结果
-
http服务器是如何搭建的?
这部分我没有自己实现,因为自己主要以业务为导向,为了节省开发时间所以直接选用成熟的开源库,虽然没有从头实现,但事先对候选库做了充分调研。我调研了httplib、mongoose、httpd等轻量http库,最终选择httplib,原因如下:httplib是单头文件实现的,克隆仓库后包含一个头文件即可,无需额外编译链接;httplib是基于select或其他多路转接技术管理IO事件,配合线程池处理就绪连接;httplib内部维护URI正则和回调函数的映射表,只需实现业务处理函数并注册到对应路径,十几行代码即可完成服务器搭建;项目基于C++编写,httplib使用C++实现,集成比C语言的httpd更自然,相比同样为C++的mongoose,httplib使用门槛更低
6.搜索结果是如何排序的?
在搜索结果排序上,我采用了基于词频加权的相关性打分策略。构建倒排索引时,对每个文档分别统计每个分词在标题和正文中出现的次数,并按照"标题权重系数10、正文权重系数1"计算该词在当前文档中的权重值。用户查询时,先将查询语句分词,再按照这些分词查找每个词的倒排拉链;如果同一个文档命中了多个查询词,则将这些词对应的权重相加得到文档的总分。最终,所有文档按总分降序排列返回给用户,总分越高的文档被认为与查询越相关
- 你在项目中遇到了哪些挑战?你是如何解决这些问题的?
1.在构建索引前,需要提取分散在多层子目录下的html文档,若手动用opendir/readdir递归遍历,代码会非常冗长,且容易出错,同时还要高效过滤出html文件,避免把无关文件读入内存。
解决:我使用了Boost库中的filesystem模块,特别是recursion_directory_iterator。这个迭代器会自动完成深度优先的目录递归,每步都能拿到完整的path对象。我只需检查当前项是否为普通文件,再通过extension()判断后缀是否为.html,将符合条件的绝对路径存入files_list里。这样可以把十几行的递归逻辑压缩为几行循环,还保证了代码在Windows/Linux上都能正确运行。
2.从html中提取去除标签的纯净文本及标题、url。html包含、<div>等标签,直接保存会干扰索引的准确性。<br/> 解决:去标签,实现简易状态机(LABLE/CONTENT),遇到>进入内容区,遇到<退回标签区,只保留文本字符并过滤换行符(\n作为每个文档的分隔符,所以一个文档内部不能有\n)。提取标题用find("<title>")和find("")截取中间内容。构建url,官网固定前缀+相对路径拼接。
3.用户搜索可能中英文混合,且需忽略英文大小写差异;中文必须用词典分词。
解决:我使用了cppjieba库进行混合分词,对分词后的每个词调用boost::to_lower统一转小写,保证大小写命中同一个倒排拉链,在构建倒排索引和搜索时都进行相同的小写化处理。
4.正排索引用vector,倒排用unordered_map<string, InvertedList>,需保证全局单例且并发构建索引安全。
解决:使用单例模式+双重检查锁(mutex)确保Index对象全局唯一,所有词频统计、加权操作都在内存中完成,不依赖外部数据库。
5.格式的统一。
解决:搜索时用Json::FastWriter将结果序列化为JSON字符串,通过httplib库返回给HTTP客户端。
6.参考8
8.例如一次输入两个词A和B,如果A和B的倒排拉链中都包含同一个文档,此时如何处理?
这样的文档包含多个查询词,相关性理解成更高,应该要排到更靠前的位置。
做法就是针对同一个文档进行权重合并。比如遍历A的倒排拉链,把这里的文档信息记录到一个哈希表中。然后再遍历B的倒排拉链,依次在A中查询。如果发现存在相同的文档,则把两个文档的权重进行相加,并且在最终合并多个拉链时进行去重。
-
项目性能如何?有没有尝试优化过性能呢?
项目的性能主要瓶颈在离线索引构建阶段。我通过添加日志分段计时,发现数据清洗(HTML去标签)和Jieba分词这两个环节占用了超过80%的总时间。针对这个瓶颈,我引入了多线程并发处理:将HTML文件分发给多个工作线程,每个线程独立完成解析、分词和局部索引构建,最后进行索引合并。优化后,处理同样规模的文档集,索引构建总时间从原来的80多秒缩短到了27秒左右,性能提升了3倍以上,同时在线搜索阶段由于完全基于内存的倒排索引,平均响应时间保持在10毫秒以内。
-
这个项目为啥和上一个同学的项目一样(主题/功能)?
因为这个项目本身就是开源项目,网上一搜就会出现好多相关博客,优质博客就那几篇,可能我们参考了一样的博客分析吧
-
项目最终成果如何?你从这个项目中学到了什么?
通过浏览器访问服务器获取搜索页面,输入关键字进行搜索,能够达成针对boost文档的搜索预期结果。通过这个项目,我进一步了解了搜索引擎的工作原理,对Linux、HTTP、数据结构等核心操作有了进一步的提高,锻炼了项目设计、解决问题的能力。
-
你在这个项目中使用了哪些技术?这些技术是怎么选型的,对比过其他的相关技术吗?
STL:C++常用技术;cpp-httplib:非常轻量的http库,相比于cpp-netlib来说,cpp-httplib引入的依赖更少,无需编译,直接引入头文件就可以使用,并且官方文档简洁清晰,非常快速就能上手;cppjieba:是一个分词库,在开源的分词库中,cppjieba分词效果较好,足够满足本项目的需求。
-
你在项目中扮演了什么角色?你负责了哪些模块或功能?
我自己独立完成项目的设计、实现、测试、部署
-
在项目中,你是如何平衡技术实现和项目时间线的?
这个项目我花了2个月的时间来完成,从项目目标的确定,到项目原型图的敲定,到各个模块的拆分,再到最后的开发测试部署。关于时间,其实我没有制定详细的计划,是因为我也是抱着学习的心态来完成这个项目,在开发中可能会遇到一些问题,解决问题也是我学习的过程,因此时间就不能确定下来。
-
你是如何测试和部署这个项目的?
测试参考"测试课程-测试用例设计"。部署:构建自动化,通过脚本和Makefile来进行项目构建和程序部署;环境准备,确保部署环境(开发、生产)都已正确设置;部署,将包发布在阿里云服务器并后台启动;日志,部署后打印日志以便于问题诊断。
-
你是如何设计项目以支持未来的扩展?你是如何确保项目代码的可维护性的?
当前是针对boost文档进行解析和制作索引的,后续也可以引入其他的html文档,做更丰富的搜索功能
-
正排索引和倒排索引的结构是什么样子?举例说明
正排索引:根据文档id找到文档内容
倒排索引:根据 关键词 找到 文档id列表(倒排拉链)
-
你是如何实现的分词?分词的原理是?
分词的原理是基于词典的分词,中文词汇数量庞大,但是对于计算机来说是可以穷尽枚举的;我是直接使用第三方库cppjieba进行分词,并未关注具体分词算法的实现
-
词和文档的相关性是如何衡量的?
通过词频衡量,使用的公式是 标题中出现的次数 * 10 + 正文中出现的次数 来作为权重。