1. 搜索引擎。提供了数据存储、数据处理、数据查询、聚合统计的能力。
2. 创始人说:“不要求你必须是一个数据科学家才能把它用好”
Elasticsearch 是一个很有意思的产品,不同岗位的人,对它的关注维度区别比较大
主要可以分三个层面
对比传统数据库的区别主要在于
传统关系型数据库
Elasticsearch
本文是系列第一篇,会大体讲述 Elasticsearch 的开发视角下的必知必会内容,从存储层设计,索引机制,分词查询的基本原理、到分布式架构设计,做一个整体梳理;后续会继续运维关注的部署、灾备等,以及查询结果优化方面的两块内容梳理
Elasticsearch
Elasticsearch是一个基于Lucene库的搜索引擎。它提供了一个分布式、支持多租户的全文搜索引擎,具有HTTP Web接口和无模式JSON文档。Elasticsearch是用Java开发的,并在Apache许可证下作为开源软件发布。官方客户端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和许多其他语言中都是可用的。[5]根据DB-Engines的排名显示,Elasticsearch是最受欢迎的企业搜索引擎,其次是Apache Solr,也是基于Lucene。[维基百科]
Lucene
Lucene是一套用于全文检索和搜索的开放源码程序库,由Apache软件基金会支持和提供。Lucene提供了一个简单却强大的应用程序接口,能够做全文索引和搜索。Lucene是现在最受欢迎的免费Java信息检索程序库。[维基百科]
Lucene 是一个基于 Java 语言开发的搜索引擎类库,作者是 Doug Cutting - Wikipedia 同时也是 Hadoop 之父,
Lucent 有一些局限性如
于是在 Lucene 的基础上,诞生了 Elasticsearch
Elastic Stack 生态
作为一个数据库,先来看看 ES 的存储设计,有以下几个概念:
_doc
不严谨地类比关系型数据库的概念如下图
RDBMS | Elasticsearch |
---|---|
Table | Index(Type) |
Row | Document |
Column | Field |
Schema | Mapping |
SQL | Query DSL |
Mapping,定义文档的 Schema,有两种定义方式
为了减少工作量和出错的次数,可以给出显式定义的一般建议操作流程
ES 支持的 字段类型 也很丰富,比较重要的,区别于一般数据库的两类如下
一些常见的 Mapping 字段属性
能否修改字段类型呢?分两种情况
注意:索引一旦创建,如果希望改变字段类型,就必须 ReIndex
除定义数据结构的 Mapping 外,Index 还有另一个重要的熟悉 Setting,用户定义数据的分布式属性,如:分片数、副本数
索引的不同语义 名词:一个 Elasticsearch 集群中的许多索引 动词:把一个文档保存到 Elasticsearch 的过程(indexing),主要就是创建一个倒排索引的过程
Elasticsearch 是面向文档的,文档是所有可搜索数据的最小单位
文档会被序列化为 JSON 保存
每个文档都有一个 Unique ID,字段名为 _id
文档的一部分元数据(基本字段)列举
首先为什么需要做分布式?
相比单机的 Lucene,分布式架构的好处主要有
节点就是一个 ES 的实例,本质上就是一个 Java 进程。一台机器上可以运行多个 ES 进程,单数生产环境一般建议一台机器上只运行一个 ES 实例
每个节点都有一个名字,也是一样可以通过配置文件或命令行参数指定
每个节点在启动后,会分配一个 UID,保存在 data 目录下
常见节点类型
配置节点类型的建议: - 开发环境一个节点可以承担多种角色 - 生产环境中,应该设置单一角色(dedicated node)
主分片(Primary Shard),解决数据水平扩展问题
副本分片(Replica Shard),解决数据高可用问题
分片的设定,容量规划考虑因素
集群(分片)的健康情况
文档需要能均匀地分布在所有分片上,充分利用硬件资源,避免部分机器繁忙,部分机器空闲
比较常见的路由算法
ES 实际使用的路由算法:shard = hash(_routing) % number_of_primary_shards
routing 默认是文档 ID,也可以自行指定哈希字段
这个路由规则,也正是当 Index 创建后,主分片数,不能随意修改的根本原因
在进一步了解分片的内部原理前,先思考一些问题:
首先,需要了解 Lucene 的倒排索引采用不可变设计(Immutable Design),一旦生成,不可更改
不可变带来的好处如下
但不可变性带来的问题是,如果要让一个新的文档可以被搜索,需要重建整个索引
在 Lucene 中,单个的倒排索引被称为 Segment。多个 Segment 汇总在一起,就被称为 Lucene 的 Index,也就是对应 ES 中的 Shard
Lucene Index 中有一个 Commit Point
,记录了所有 Segment 的信息,如果有新文档插入,则会生成新的 Segment;查询时会同时查询所有的 Segments,并对结果汇总
另一个文件 .del
,记录了删除的文档信息;搜索的结果还会根据该文件中的内容,对结果进行过滤
每当有新 Document 要写入时
其中第 2、3 两步合并成为 Refresh,默认 1 秒(index.refresh_interval
)执行一次,这也就是为什么 ES 是近实时搜索引擎的原因(高版本的 ES 默认落盘);另外 Index Buffer 被写满时也会触发,默认大小是 JVM 内存的 10%
其中第 4 步,其实是归属于 Flush 操作的一个步骤。Flush 默认 30 分钟执行一次,或者 Transcation Log 满(默认 512MB)也会触发。Flush 执行包含的流程有
但这里有个问题,按这个机制,在文档写入文档的情况下,默认每秒会产生至少一个 Segment,时间久了之后就会产生非常多的 Segment,此时需要进行 Segment 的合并操作,即 Merge 操作,该操作除了会合并多个 Segement 外,还会根据 .del
文件的内容,在合并的过程中实际删除已经标记删除的文档
ES 会自动执行 Merge 操作,如果需要人为触发,可以执行 POST index_name/_forcemerge
ES 提供的查询接口是 HTTP 协议的,通用性很强,方便使用,主要有两种方式
// 几种类型,可以选定不同的 index 范围
/_search
/index1/_search
/index1,index2/_search
/index*/_search
// 怎么填查询参数?查 name=John
/_search?q=name:John
// Request Body 的方式
curl -XPOST ""
-H "Content-Type: application/json" -d
{
"query": {
"match_all": {}
}
}
URL 中直接填写查询参数
q:指定查询语句 df:默认字段,不指定时对所有字段进行查询 sort 排序,from、size 用于分页 profile 可以查看语句执行计划
在 Request Body 里面写 JSON 格式的查询语句,语法是 ES 自己的一套 DSL,支持功能完整,项目中主要还是使用这种方式
{
"script_fields": {
"new_field": {
"script": {
"lang": "painless",
"source": "doc['order_date'].value+'_hello'" } }
},
"query": {
"match_all": {}
}
}
ES 除简单搜索外,还提供数据的聚合统计功能
文档 ID | 文档内容 |
---|---|
1 | Mastering Elasticsearch |
2 | Elasticsearch Server |
3 | Elasticsearch Essentials |
Term | Count | DocumentID:Position |
---|---|---|
Elasticsearch | 3 | 1:1, 2:0, 3:0 |
Mastering | 1 | 1:0 |
Server | 1 | 2:1 |
Essentials | 1 | 3:1 |
倒排索引的核心组成
数据结构 | 优缺点 |
---|---|
排序列表 Array/List | 二分法查找,不平衡 |
HashMap/TreeMap | 高性能,内存消耗大 |
Skip List | 跳表,可快速查找词语,在 Lucene、Redis、Hbase 等里都有实现 |
Trie | 前缀树,适合英文词典,如果系统中存在大量字符串且基本没有公共前缀,则相应的 Trie 树将非常消耗内存 |
Double Array Trie | 适合做中文词典,内存占用小,很多分词工具都采用 |
Termary Search Tree | 三叉树,每个 Node 有 3 个节点,兼具查询快和节省内存的优点 |
Finite State Transducers(FST) | 有限状态转移机,Lucene 4 有开源实现,并大量使用 |
ES 的 JSON 文档中的每个字段,都有自己的倒排索引,当然也可以指定对某些字段不做索引,节省存储空间,但也就自然而然不能搜索了
如 Elasticsearch
这个 Term 在前面文档列表里面,对应的倒排列表可能是
DocID | TF | Position | Offset |
---|---|---|---|
1 | 1 | 1 | <10, 23> |
3 | 1 | 0 | <0, 13> |
Analysis,文本分析,是把全文本转换成一系列单词(term/token)的过程,也叫分词
Analysis 是通过 Analyzer 分词器来实现的,ES 内置多种分词器,也可以按需定制分词器
除了数据入库的时候需要分词,查询语句也需要用相同的分词器对查询语句进行分析
分词器主要由三部分组成
ES 的内置分词器
// 直接指定分词器进行测试
Get /_analyze
{
"analyzer": "standard",
"text": "Mastering Elasticsearch, elasticsearch in Action"
}
// 指定索引的字段进行测试
POST books/_analyze
{
"field": "title",
"text", "Mastering Elasticsearch"
}
// 自定义分词器进行测试
POST /_analyze
{
"tokenizer": "standard",
"filter": ["lowercase"],
"text": "Mastering Elasticsearch"
}
中文句子要切分成一个个词(不是一个个字) 英文中,单词有自然的空格作为分隔 一句中文,在不同的上下文,有不同理解(这个苹果,不大好吃/这个苹果,不大,好吃;全民,k歌/全民k歌)
ICU Analyzer,提供了 Unicode 的支持,更好地支持亚洲语言
安装插件:Elasticsearch-plugin install analysis-icu
更多的中文分词器
Term 是表达语义的最小单位
Term Level Query
在 ES 中,Term 查询,对输入不做分词,会将输入作为一个整体,在倒排索引中查询准确的词项,并使用相关度打分公式为每个包含该词项的文档进行相关性打分
可以用 Constant Score Query 将查询转换为一个 Filtering,避免打分,利用缓存提高性能
查询的时候,会对输入的查询进行分词,生成一个供查询的词项列表,然后每个词项进行底层查询,最终将结果合并,并给每个文档生成一个相似度打分
对应的,在数据入库 Index 阶段,如果字段类型是 text 则会分词,keyword 类型不会分词
结构化搜索(Structured search)是指对结构化数据的搜索
结构化数据顾名思义也就是遵循严格定义的结构的数据
结构化结果只有“是”和“否”两个值,根据场景的需要,一样可以控制结构化结果是否需要打分
用户关心的几类问题
衡量相关性 Information Retrieval
Precision = TP / ALL
Recall = TP / (TP + FN)
调整 ES 查询参数,可以调优这两个参数
搜索的相关性打分,描述了一个文档和查询语句匹配的程度,ES 会对每个匹配查询条件的结果进行打分 _score 打分的本质是排序,需要把最符合用户需求的文档排在最前面,ES 5 之前,默认的相关性打分算法是 TF-IDF,现在采用 BM 25
词频 TF,Term Frequency,检索词在文档中出现的频率 本质上描述了两个简单的规则
举例,输入查询“我的苹果”,我在文档 1 中出现,苹果在文档 1、2 中出现
Term | Doc ID |
---|---|
我 | 1 |
苹果 | 1, 2 |
计算一个词的词频的简单方式可以是
注意这里“的”是一个与语义无关的停用词,不应该考虑 TF(的) 的影响
文档频率 DF,Document Frequency,检索词在所有文档中出现的频率
逆文档频率 IDF,Inverse Document Frequency
TF-IDF 本质上就是把 TF 求和变成了加权求和
出现的文档数 | 总文档数 | IDF | |
---|---|---|---|
我 | 200 万 | 10 亿 | log(500)=8.96 |
苹果 | 5 亿 | 10 亿 | log(2)=1 |
TF-IDF 被公认为是信息检索领域最重要的发明 除了信息检索,在文献分类和其它相关领域有着非常广泛的应用 IDF 的概念,最早是剑桥大学的斯巴克·琼斯提出,1972 年 《关键词特殊性的统计解释和它在文献检索中的应用》,不过并没有从理论上解释为啥 IDF 是要用 log(全部文档数/检索词出现过的文档总数),也没有进行进一步研究 1970,1980 年代萨尔顿和罗宾逊,进行了进一步的证明和研究,并用香农信息论进行了证明 http://www.staff.city.ac.uk/~sbrp622/papers/foundations_bm25_review.pdf 现代搜索引擎,对 TF-IDF 进行了许多优化
q:查询语句
d: 匹配的文档
t:分词后的词项
boost:权重提升,ES 查询时可以自行指定来改变 Boosting query
norm(t,d):文档越短,相关性越高
BM 25,相比 TF-IDF,解决了一个问题:当 TF 无限增加时,BM 25 的算分会趋于一个固定值,而不是无限增长
官方文档参考链接:https://www.elastic.co/guide/cn/elasticsearch/guide/current/pluggable-similarites.html
当处理自然语言时,有时候尽管搜索与原文不完全匹配,但是还是希望搜索到一些内容
一些可采取的优化
混合多语言的挑战
分词的挑战
英文分词:You’re 分成一个还是多个?Half-baked 要不要切分?
中文分词:
中文分词方法的演变
字典法(北京航空航天大学,梁元楠教授):一个句子从左到右扫描一遍,遇到有的词就标示出来;找到复合词,就找最长的;不认识的词就分割成单字词
最小词数的分词理论(哈尔滨工业大学,王晓龙博士)
统计语言模型(清华大学郭进博士):解决了二义性问题,将中文分词的错误率降低了一个数量级,动态规划+维特比算法快速找到最佳分词
基于统计的机器学习算法:这类目前常用的算法是 HMM、CRF、SVM、深度学习等算法
中文分词器现状
中文分词器以统计语言模型为基础,经过几十年发展,到今天为止可以看做一个已经解决的问题
不同分词器的好坏,主要差别在于数据的使用和工程使用的精度
常见的分词器都是使用机器学习算法和词典结合,一方面能提高分词准确率,另一方面能改善领域适应性
ES 中提供的一些分词器
Search Template,用于解耦程序和搜索 DSL
在开发初期就能明确查询参数,但最终的 DSL 结构无法确定,这时候可以通过 Search Template 定义一个 Contract,开发人员和搜索优化人员可以分头并行开发
Alias API,索引可以设置别名,实现零停机运维
ES 默认会以文档的相关度算法进行排序
Function Score Query 可以在查询结束后,对每个匹配的文档进行重新算分,根据新生成的得分进行排序
Function Score Query 提供了一些默认的打分函数
因为有了 Script Score, 可以做的事情就很多了,比如实现海明距离、余弦距离等算法,实现对非文本类型数据的打分查询,比如指纹相似度、声纹相似度等
Complete Suggerster 提供了自动完成的功能,用户每输入一个字符,就需要即时发送一个查询请求到后端查询匹配项
对性能要求很苛刻,ES 采用了不同的数据结构,而非倒排索引来完成。ES 会将 Analyze 的数据编码成 FST 和索引一起存放,FST 会被 ES 整个加载进内存,速度很快
FST:Finite StateTransducers,有穷状态转换器
需要启用该特性的话,需要在创建 Mapping 时指定 "type": "completion"
另外还可以指定 context,来使用 suggerster 的基于上下文的提示
ES 的搜索有两个阶段
用户发出查询请求到达 Coordinating 节点(默认每个节点都是),节点会在所有主、副分片中随机出一个完整的数据分片列表组,然后将请求转发给它们,随后每个分片都会执行查询请求并排序,然后每个分片都会返回 From+Size 个排序后的文档 ID 和排序值给 Coordinating 节点
Coordinating 节点会将 Query 阶段,从每个分片获取的排序后的文档 ID 列表,进行合并排序,并选取合并后列表的 [From, From+Size) 文档的 ID 子列表;接下来再以 multi get 的请求方式,到相应的分配去获取详细的文档数据
性能方面
分片数量 * (From+Size)
这么多的文档 ID相关性打分方面
解决方案主要由两种
_search?search_type=dfs_query_then_fetch
排序,也就是将查询结果根据指定的字段进行排序。排序是针对字段原始内容进行的,所以倒排索引在这里无法发挥作用,需要用到正排索引,即通过 ID 字段快速得到字段的原始内容
ES 有两种实现方式
Doc Values | Field Data | |
---|---|---|
创建时机 | 索引时,和倒排索引一起创建 | 搜索时动态创建 |
创建位置 | 磁盘文件 | JVM Heap |
优点 | 内存占用少 | 新文档索引快,不用占用磁盘空间 |
缺点 | 新文档索引慢 | 文档过多时,动态创建开销大,占用内存多 |
默认值 | >ES 2.x | ES 1.x |
Doc Values 特性默认是启用的,可以在 Index Mapping 中设置关闭,关闭可以提升新文档索引的速度,减少磁盘空间;如果明确某些字段不需要做排序和聚合,可以设置关闭 Doc Values
如前文所述,Query Then Fetch 模式存在深度分页时的性能问题,ES 为了保护自身不被深度分页查询请求拖死,默认有一个限制 From+Size 不能大于 10000
而 ES 对这种深度分页的场景,提供了两种解决方式
Search After API 的玩法大概就是说,首次请求时定义排序字段,且排序字段不能重复(可以多字段联合,所以可以引入 _id 字段来确保唯一),然后每次查询只会返回一部分结果,需要翻页查询下一份数据时,需要将前一次查询的结果带上
Scroll API 是在查询调用的第一次,就创建一个快照(指定有效期),每次查询都需要带上上一次的 scroll ID;注意因为是快照,所以新写入的文档,在这个快照中是无法查询到的
不同的查询方式的选型
在并发更新文档的场景下,ES 是采用乐观锁版本号的方式来实现并发控制
如前文所述,ES 的文档其实是不可变的,所以对文档的更新,其实就是先标记原文档被删除,然后创建一个新文档,这两个文档的版本号不同
_seq_no + _primary_term
version + version_type=external
定义 Index 的模板,并且按照一定规则,在 Index 创建的时候自动使用
可以创建多个模板,多个模板可以 merge 在一起,且 merge 过程可以指定 order 来控制生效顺序
当一个索引被创建时
位于某个 Index 的 Mapping 中,根据 ES 识别的数据类型,结合字段名称,来动态设定字段类型
可以做到的事情比如
参考资料:
QQ音乐招聘后端开发,点击左下方“查看原文”投递简历~
也可将简历发送至邮箱:tmezp@tencent.com