原理
索引原理
docid
ES数据库里的每条记录,都会分配到一个ID,称为doc id。
docid | name | age |
---|---|---|
1 | Johnny Depp | 25 |
2 | Aby Homie | 18 |
3 | Johnny Wei | 25 |
倒排索引(Posting)
ES会为表中的每个字段都维护一个倒排索引。 倒排索引有两个主要字段,一个负责将表中该字段的每一行分成单词存储起来,一个则负责存储这些单词对应的docid(出现多次则以数组保存)。 查询时,通过分词去匹配索引,匹配到之后,根据后面的ID去查找记录。
name字段的倒排索引:
Term | Posting |
---|---|
Johnny | [1,3] |
Depp | 1 |
Aby | 2 |
Homie | 2 |
Wei | 3 |
age字段的倒排索引:
Term | Posting |
---|---|
25 | [1,3] |
18 | 2 |
倒排索引提供了模糊搜索的一种解决方案,但是当分词的数量很多(比如千万级),那么检索分词会很慢。 因此,ES又引出了分词词典这个概念。
分词词典(Term Dictionary)
分词词典,是对分词进行排序后,使其可以通过二分查找达到log(n)级的查询效率。 对倒排索引(posting)排序后获得的就是Term Dictionary。
name字段的分词词典:
Term | Posting |
---|---|
Aby | 2 |
Depp | 1 |
Homie | 2 |
Johnny | [1,3] |
Wei | 3 |
age字段的分词词典:
Term | Posting |
---|---|
18 | 2 |
25 | [1,3] |
分词词典解决了查询效率问题,但是若数据量太大,则无法将全部数据都加载到内存。 为了解决这个问题,分词索引出现了。
分词索引(Term Index)
通过分词中的前缀,为分词词典再维护一个B-树索引。 通过分词索引可快速定位到Term Dictionary里的某个offset,再沿着这个offset往下查询。
name字段的分词索引:
Index | Posting |
---|---|
A | 1 |
Ab | 1 |
Aby | 1 |
D | 2 |
H | 3 |
J | 4 |
W | 5 |
... |
若分词索引数据量也很大,内存无法加载完,此时可以通过FST方法,压缩分词索引,提高存储效率。
FST 压缩算法,提高存储效率
分词器
什么是分词器?
ES将一段文本分成多个单词的工具。
在哪个步骤会用到分词器?
- 保存数据时
ES在将一条数据保存到表之后,为了生成倒排索引,还需要通过分词器,将这条数据的每一个字段,分成多个单词,保存到不同字段对应的倒排索引中。 生成倒排索引的原理,可见本文中的“倒排索引”介绍。
- 查询数据时(全文检索)
以下是一段全文检索请求(使用match),意思就是查表index1中,字段title匹配"BROWN DOG!"这个查询条件的数据:
GET /index1/_search
{
"query":{
"match":{
"title":"BROWN DOG!"
}
}
}
全文检索时,ES会先使用分词器,将查询条件分成多个词([brown,dog]),只要字段中有其中一个词,便会命中。 比如会命中以下数据:
- title = i like brown,i don't like dog.
- title = there is a brown tree.
- title = a white dog.
如果我们想查两个词都有的数据,可以像下面这样请求:
GET /index1/_search
{
"query":{
"match":{
"title":"BROWN DOG!"
"operator":"and" //默认情况下是or
}
}
}
有哪些分词器?
ES自带以下分词器:
- Standard:默认分词器,支持多语言,不分大小写。
- Simple:非字母作为分隔符(即不会将数字分成一个单词)
- Whitespace:空格、制表符、换行作为分隔符
- Keyword:不分词
- Pattern:正则表达式
分词示例:
//【1.】假设使用不同分词器对以下句子进行分词。
“text”: “The 2 QUICK Brown-Foxes jumped over the lazy dog’s bone.”
//默认的分词器下,以空格、标点符号作为分隔符,并将单词小写处理
Standard:['the','2','quick','brown','foxes','jumped','over','the','lazy','dog's','bone']
//Simple分词器下,除了空格、标点符号,数字也会作为分隔符,同样也会将单词小写处理
Simple:['the','quick','brown','foxes','jumped','over','the','lazy','dog','s','bone']
//Whitespace分词器下,以空格、制表符、换行作为分隔符,但不会将单词小写处理
Whitespace:['The','2','QUICK','Brown-Foxes','jumped','over','the','lazy','dog's','bone']
//【2.】对中文进行分词。
“text”: “我想买3台空调”
//若用上面的分词器,基本上都会分成['我','想','买','3','台','空','调']
//此时,若搜索'空调',是搜不出来这条数据的。因为从上面可以看出,没有分出'空调'这个分词
//由此可见,自带的分词对中文搜索不是很友好。此时我们可以使用IK分词器。
其他分词器实现:
- IK:更高效的中文分词器
IK分词器有两种模式:ik_max_word和ik_smart模式。
假设对“我是乒乓球冠军”进行分词。
ik_max_word:最细粒度分词,会分成:[我,是,乒乓,乒乓球,球,冠军]
ik_smart:最粗粒度分词,会分成:[我,是,乒乓球,冠军]
#测试分词(analyzer:standard、simple、whitespace、keyword、pattern...)
curl -X POST 'localhost:9200/city/_analyze'
{
"analyzer":"standard",
"text":"你是"
}
#返回结果
{
"tokens":[
{
"token":"你",
"start_offset":0,
"end_offset":1,
"type":"<IDEOGRAPHIC>"
"position":0
},
{
"token":"是",
"start_offset":2,
"end_offset":3,
"type":"<IDEOGRAPHIC>"
"position":1
}
]
}
#创建索引时,为某个字段指定分词(查询时会自动走分词)
curl -X PUT 'localhost:9200/test'
{
"mapping":{
"properties":{
"name":{ #创建一个name字段
"type":"text", #定义其类型为text
"analyzer":"ik_max_word" #分词器使用ik_max_word
},
"englishname":{ #创建一个englishname字段
"type":"text", #定义其类型为text
"analyzer":"standard" #分词器使用standard
},
"sex":{ #创建一个sex字段
"type":"keyword", #定义其类型为keyword,无需分词
},
"age":{ #创建一个age字段
"type":"long", #定义其类型为long,无需分词
}
}
}
}
MYSQL与ES的数据一致性
【需求描述】
我在mysql数据库有一张表A,为了加快搜索速度,将数据迁移到了ES。
但表A仍然会有增量数据进来,对于这些增量数据,要如何保证数据写进了MYSQL后,也会写进ES呢?
【解决方案】
3种:
- 数据双写
- MQ异步同步
- 基于Binlog实现数据同步(Canal)
【数据双写】
原理:在程序中,在写入数据时,先写入数据到Mysql,然后再写入到ES。
优点:实现简单,时效性高
缺点:硬编码问题严重(每个表都要重写一次);如果服务或ES宕机,会有数据丢失风险
【MQ异步同步】
原理:在程序中,在写入数据时,先写入数据到Mysql,然后再写入一条消息到MQ,通过MQ告诉ES需要进行数据同步。
优点:
- 这个方案最直接的点就是性能高,并且实现了业务的解耦合,并且可以利用 MQ 的重试机制,在写入失败的时候进行重试,降低了数据丢失的风险。
- 这样还支持多个数据源的写入,提高了扩展性,不会出现由于单个数据源写入异常从而导致其他数据源写入受到影响的问题。
缺点: - 硬编码问题,在接入新的数据源的时候需要实现新的消费者代码,代码侵入性较强
- 引入了消息队列,提高了运维的成本,增加了系统的复杂程度
- 可能出现延时问题,因为消息队列是异步消费模型,用户写入的数据不一定可以马上看到结果,有一定的延迟。
【Canal(基于Binlog实现数据同步)】
原理:
- Mysql的主节点会将增删改操作都写入binlog日志
- Canal伪装成Mysql的从节点,订阅Mysql的binlog日志 优点:
- 没有代码侵入,没有硬编码
- 原有的系统没有任何变化,可以实现无感知,性能较高
- 业务解耦合,这个和消息队列是差不多实现思路的,不过这个不需要关注原来系统的业务实现