如何做一个搜索引擎
茶杯狐是我大学课余时间做的一个搜电影的网站,最初只是利用协程实时爬取百度的搜索结果然后再进行过滤聚合。后来随着访问量的逐渐增加百度终于对我限流了,于是我对它进行了一些改造,使其成为了一个真正的搜索引擎。下面就简单分享一下我是怎么做的吧。
一个简单的搜索引擎应该由这些模块组成(括号里是茶杯狐选择使用的技术栈,由于各模块完全独立可以替换成任意具有相同功能的工具)。
其中搜索引擎里最复杂的部分已经被搜索引擎数据库解决了,我们要写的其实主要是爬虫的部分。但是爬虫也没什么好说的,我主要还是想分享一下搜索引擎数据库相关的内容。
搜索引擎数据库简介
我们为什么需要搜索引擎数据库?它都做了哪些事情?用普通的关系型数据库或者 key-value 数据库可以做吗?
其实搜索引擎数据库主要做了一件事件,那就是倒排索引。所谓倒排索引就是将查询关键字作为索引的 key,将其出现的文档的 id 及出现的位置和次数等作为 value 来建立索引。
普通关系型数据库的索引是直接对字段建立索引,不会提取字段中的关键词,因此使用模糊查询的时候只有LIKE 'X%'
是会走索引的,LIKE '%X%'
和LIKE '%X'
都找不到索引。
Redis 倒是有一个扩展模块可以支持倒排索引:RediSearch,其原理是在 redis 的 hashmap 基础上就可以很容易实现倒排索引的结构。RediSearch 倒排索引除了实现了基础功能外,还引入了内存管理等优化功能。在数据量和机器配置有限的情况下 RediSearch 也是一个非常不错的选择,详见 RediSearch 与 Elasticsearch 的 Benchmark 对比。
搜索引擎数据库横向对比
前面已经提到了一些搜索引擎数据库,比如 RediSearch 和 Elasticsearch,那为什么我最终选择了 Solr 呢?很简单,请看下表(这是我通过查阅资料文档和自己的一系列尝试总结出的结果):
搜索引擎数据库 | 社区活跃度 | 文档丰富度 | 系统资源占用 | 中文分词支持 |
---|---|---|---|---|
Elasticsearch | 高 | 高 | 高 | ik、jieba 等 |
Solr | 高 | 高 | 一般 | ik、jieba 等 |
RediSearch | 一般 | 高 | 低 | Friso |
Sphinx | 已经一年多没有版本更新了 | 一般 | 低 | mmseg 等 |
对于我的需求,Elasticsearch 与 Solr 都是可以的,它们之间有一点区别在于 Elasticsearch 对实时搜索的支持更好,而 Solr 在创建索引的过程中搜索效率会明显下降,由于我选择在夜间低峰期爬取数据并创建索引,因此这并没有多大影响。由于我的服务器资源实在有限,不足以支撑起一套 Elastic Stack,于是我选择了 Solr。目前,Solr 在搜索引擎数据库的流行度排行榜上排名第三。
分词器与词库
分词器和词库的选择会直接影响到搜索的效果。
这里我推荐一个 Solr 中比较好的解决方案:https://github.com/magese/ik-analyzer-solr
它对 ik 原有的词库进行了扩充:
分词工具 | 词库中词的数量 | 最后更新时间 |
---|---|---|
ik | 27.5万 | 2012年 |
mmseg | 15.7万 | 2017年 |
word | 64.2万 | 2014年 |
jieba | 58.4万 | 2012年 |
jcesg | 16.6万 | 2018年 |
sougou词库 | 115.2万 | 2019年 |
将以上词库进行整理后约187.1万条词汇。
Solr
关于 Solr 本身的使用建议以官方文档为准,因为它的配置比较繁杂,不同版本的配置有可能是不一样的,况且它的文档已经可以说是相当完善了。但我还是要介绍几个关键点,方便新手快速入门,具体操作的时候再参考一下官方文档就行了(我使用的版本是 8.4,文档版本可以直接通过修改 URL 中的版本号来切换)。
安装与启动
如果为了在本地尝试或学习,可以按照 Solr 的入门指南来安装,安装包中已经自带了入门指南中用到的示例数据和配置。
而如果你要在生产环境部署,则需参考生产环境部署文档,Solr 提供了一个部署脚本,十分方便。
值得注意的是,Solr 提供了两种模式:一种是使用 ZooKeeper 管理的集群模式 SolrCloud,另一种则是单机模式。如果仅使用一台服务器来运行 Solr 选择单机模式即可。
另外,Solr 默认的 JVM 堆内存大小是 512M,生产环境可以根据服务器的配置和需要进行调整,不然当内存不足时 Solr 程序就会通过 OOM 脚本将 Solr 实例杀死,导致程序无法正常使用。JVM 配置修改 /etc/defualt/solr.in.sh 文件即可。另外,Solr 的时区、端口和日志路径等也可以在该文件中修改。
设置 Schema
启动成功后,我们就可以访问 Solr 的后台管理界面了。首先我们需要新建一个 Core,Core 在 Solr 中类似数据库表的概念,每个 Core 都通过一个 XML 文件来定义其中数据的 Schema。下面就是茶杯狐的 Schema 定义(在 Core 创建完成之后会在当前 Core 路径下的 config 文件夹中生成一个文件,名叫 managed-schema,虽然它提示不要直接编辑这个文件,但是增加 fieldType 等操作在管理后台中无法完成,这种情况其实直接编辑这个文件就可以了):
<!-- Solr managed schema - automatically generated - DO NOT EDIT -->
<schema name="cupfox" version="1.6">
<uniqueKey>id</uniqueKey>
<fieldType name="plong" class="solr.LongPointField" docValues="true"/>
<fieldType name="string" class="solr.StrField" sortMissingLast="true"/>
<fieldType name="text_ik" class="solr.TextField">
<analyzer type="index">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" conf="ik.conf" useSmart="false"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
<analyzer type="query">
<tokenizer class="org.wltea.analyzer.lucene.IKTokenizerFactory" conf="ik.conf" useSmart="false"/>
<filter class="solr.LowerCaseFilterFactory"/>
</analyzer>
</fieldType>
<field name="_root_" type="string" indexed="true" stored="false"/>
<field name="_version_" type="plong" indexed="true" stored="true"/>
<field name="id" type="string" multiValued="false" indexed="true" required="true" stored="true"/>
<field name="tags" type="string" omitNorms="true" omitTermFreqAndPositions="true" indexed="true" stored="true" uninvertible="true" required="true"/>
<field name="text" type="text_ik" omitTermFreqAndPositions="true" indexed="true" stored="true" uninvertible="false" required="true"/>
<field name="type" type="string" omitNorms="true" omitTermFreqAndPositions="true" indexed="true" stored="true" uninvertible="false" omitPositions="true" required="true"/>
<field name="url" type="string" omitNorms="true" omitTermFreqAndPositions="true" indexed="false" stored="true" uninvertible="false" omitPositions="true" required="true"/>
<field name="website" type="string" omitNorms="true" omitTermFreqAndPositions="true" indexed="true" stored="true" uninvertible="true" required="true"/>
</schema>
可以看到,我只是简单的定义了一些 field 和它们所用到的 fieldType,其中需要中文分词的 field 使用了 text_ik 这个类型,在这个类型中我分别指定了索引和查询时所用的分词器为 IKTokenizerFactory,注意:useSmart = "false" 的意思是使用细颗粒度进行分词,而 LowerCaseFilterFactory 是把大写字母统一转成小写。分词效果可以在 Solr 后台的 Analysis 页面进行测试:
从数据库导入数据
从数据库导入数据需要配置两个 XML,首先是数据源的定义:
xxxxxxxxxx
<dataConfig>
<dataSource driver="org.mariadb.jdbc.Driver" url="jdbc:mariadb://0.0.0.0:3306/cupfox" user="write" password="123456"/>
<document>
<entity name="resource" query="select * from resource" deltaImportQuery="select * from resource where id=${dataimporter.delta.id}" deltaQuery="select id from resource where is_deleted = 0 and update_time > '${dataimporter.last_index_time}'" deletedPkQuery="select id from resource where is_deleted = 1">
<field column="text" name="text" />
<field column="url" name="url" />
<field column="website" name="website" />
<field column="type" name="type" />
<field column="tags" name="tags" />
</entity>
</document>
</dataConfig>
我使用了 mariadb,因此需要在 solrconfig.xml 里引入 mariadb 的 jar 包(这个在 mariadb 官网就可以下载):
xxxxxxxxxx
<lib dir="${solr.install.dir:../../../..}/lib/" regex="mariadb-java-client-\d.*\.jar" />
除此之外,solrconfig.xml 里还要加上:
x<lib dir="${solr.install.dir:../../../..}/dist/" regex="solr-dataimporthandler-.*\.jar" />
<requestHandler name="/dataimport" class="solr.DataImportHandler">
<lst name="defaults">
<str name="config">solr-data-config.xml</str>
</lst>
</requestHandler>
因为 /dataimport 接口默认是关闭的,数据源配置文件需要我们自己指定。
当这些都配置好之后,就可以通过 /dataimport API 导入数据库了,当然,也可以在管理后台手动导入数据:
有两种数据导入方式,分别是全量导入和增量导入,这两种导入方式对应的数据的 SQL 需要在 XML 中指明。
查询
相比于其繁杂的配置,Solr 的查询方法可以说是相当简单了,基本上就是一个接口加上若干参数,参数取值与含义示例:
参数 | 取值示例 | 取值含义 |
---|---|---|
q | 阿甘正传 AND type:0 | 查询匹配关键词为“阿甘正传”且 type 字段值是 0 的记录 |
start | 0 | 偏移量为 0 |
rows | 10 | 从偏移量开始向后取 10 条记录 |
sort | score desc, website asc | 先按 score 倒序,再按 website 字段正序排列(Solr 中的记录都有一个隐藏的字段 score,即匹配度的评分) |
hl | on | 开启匹配关键词高亮,会多返回一项 highlighting 字段 |
其他可用参数我就不列了,请参阅官方文档。
总结
搜索其实是一个非常普遍的场景,并不仅仅是搜索引擎才会遇到搜索场景,有时候它可能看起来并不像是个搜索场景。就比如数据的可视化展示,你需要通过多种条件筛选出你需要展示的数据,有时候还需要根据多种条件进行排序,对于传统数据库来说这些场景不方便建立索引。因此,在数据量较大的情况下,凡是需要进行筛选、排序或匹配查询的场景都可以考虑使用搜索引擎数据库建立索引来进行优化。