《极客时间教程 - Elasticsearch 核心技术与实战》笔记二
《极客时间教程 - Elasticsearch 核心技术与实战》笔记二
第四章:深入搜索
基于词项和基于全文的搜索
基于词项的查询
Term 是表达语意的最小单位。搜索和利用统计语言模型进行自然语言处理都需要处理 Term
Term 级别查询:Term / Range / Exists / Prefix / Wildcard
在 ES 中,Term 查询,对输入不做分词。会将输入作为一个整体,在倒排索引中查找准确的词项,并且使用相关度计算公式为每个包含该词项的文档进行相关度计算。
可以通过 Constant Score 将查询转换成一个 Filtering,避免算法,并利用缓存,提高性能。
基于文本的查询
文本查询:match、match_phrase、query_string
索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的词项列表。
查询时,会先对输入的查询进行分词,然后每个词项柱哥进行底层的查询,最终将结果进行合并,并为每个文档计算一个相关度分值。
【示例】
DELETE products
PUT products
{
"settings": {
"number_of_shards": 1
}
}
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "productID" : "XHDK-A-1293-#fJ3","desc":"iPhone" }
{ "index": { "_id": 2 }}
{ "productID" : "KDKE-B-9947-#kL5","desc":"iPad" }
{ "index": { "_id": 3 }}
{ "productID" : "JODL-X-1937-#pV7","desc":"MBP" }
GET /products
POST /products/_search
{
"query": {
"term": {
"desc": {
//"value": "iPhone"
"value":"iphone"
}
}
}
}
POST /products/_search
{
"query": {
"term": {
"desc.keyword": {
//"value": "iPhone"
//"value":"iphone"
}
}
}
}
POST /products/_search
{
"query": {
"term": {
"productID": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}
POST /products/_search
{
//"explain": true,
"query": {
"term": {
"productID.keyword": {
"value": "XHDK-A-1293-#fJ3"
}
}
}
}
POST /products/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}
}
}
#设置 position_increment_gap
DELETE groups
PUT groups
{
"mappings": {
"properties": {
"names":{
"type": "text",
"position_increment_gap": 0
}
}
}
}
GET groups/_mapping
POST groups/_doc
{
"names": [ "John Water", "Water Smith"]
}
POST groups/_search
{
"query": {
"match_phrase": {
"names": {
"query": "Water Water",
"slop": 100
}
}
}
}
POST groups/_search
{
"query": {
"match_phrase": {
"names": "Water Smith"
}
}
}
结构化搜索
结构化搜索是指对结构化数据的搜索。
日期、布尔、和数字类型都是结构化的。它们都有精确的格式,可以根据这些格式进行逻辑操作,如:比较范围、判定大小。
文本也可以是结构化的。结构化的文本可以精确匹配(term)或部分匹配(prefix)
结构化结果只有是或否两个选项。
【示例】
#结构化搜索,精确匹配
DELETE products
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }
GET products/_mapping
#对布尔值 match 查询,有算分
POST products/_search
{
"profile": "true",
"query": {
"term": {
"avaliable": true
}
}
}
#对布尔值,通过 constant score 转成 filtering,没有算分
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": true
}
}
}
}
}
#数字类型 Term
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"price": 30
}
}
}
#数字类型 terms
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"price": [
"20",
"30"
]
}
}
}
}
}
#数字 Range 查询
GET products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lte" : 30
}
}
}
}
}
}
# 日期 range
POST products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"date" : {
"gte" : "now-1y"
}
}
}
}
}
}
#exists 查询
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"exists": {
"field": "date"
}
}
}
}
}
#处理多值字段
POST /movies/_bulk
{ "index": { "_id": 1 }}
{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy"}
{ "index": { "_id": 2 }}
{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"] }
#处理多值字段,term 查询是包含,而不是等于
POST movies/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"genre.keyword": "Comedy"
}
}
}
}
}
#字符类型 terms
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"terms": {
"productID.keyword": [
"QQPX-R-3956-#aD8",
"JODL-X-1937-#pV7"
]
}
}
}
}
}
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"price": 30
}
}
}
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"date": "2019-01-01"
}
}
}
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"date": "2019-01-01"
}
}
}
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}
}
}
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}
#对布尔数值
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": "false"
}
}
}
}
}
POST products/_search
{
"query": {
"term": {
"avaliable": {
"value": "false"
}
}
}
}
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"term": {
"price": {
"value": "20"
}
}
}
}
POST products/_search
{
"profile": "true",
"explain": true,
"query": {
"match": {
"price": "20"
}
}
POST products/_search
{
"query": {
"constant_score": {
"filter": {
"bool": {
"must_not": {
"exists": {
"field": "date"
}
}
}
}
}
}
}
搜索的相关性算分
搜索的相关性打分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分(_score)。
ES5 之前,默认的相关性算法是 TD-IDF;ES5 后,采用 BM25。
词频(Term Frequency,TF) - 检索词在一篇文档中出现的频率
逆文档频率(Inverse Document Frequency,IDF) - log(全部文档数/检索词出现过的文档总数),用以表示检索词在所有文档中出现的频率。
Stop Word - 词项出现频率岁高,但对相关度几乎没有用户,例如:的、the、a 之类的词。
TF-IDF 本质上就是将 TF 求和变成了加权求和。
和 TF-IDF 相比,当 TF 无限增加时, BM 25 分支会趋于一个平稳值。
Boosting 是控制相关度的一种手段。
- boost > 1,打分的权重提升;
- 0 < boost < 1,打分的权重降低
- boost < 0,贡献负分
【示例】
PUT testscore
{
"settings": {
"number_of_shards": 1
},
"mappings": {
"properties": {
"content": {
"type": "text"
}
}
}
}
PUT testscore/_bulk
{ "index": { "_id": 1 }}
{ "content":"we use Elasticsearch to power the search" }
{ "index": { "_id": 2 }}
{ "content":"we like elasticsearch" }
{ "index": { "_id": 3 }}
{ "content":"The scoring of documents is caculated by the scoring formula" }
{ "index": { "_id": 4 }}
{ "content":"you know, for search" }
POST /testscore/_search
{
//"explain": true,
"query": {
"match": {
"content":"you"
//"content": "elasticsearch"
//"content":"the"
//"content": "the elasticsearch"
}
}
}
POST testscore/_search
{
"query": {
"boosting" : {
"positive" : {
"term" : {
"content" : "elasticsearch"
}
},
"negative" : {
"term" : {
"content" : "like"
}
},
"negative_boost" : 0.2
}
}
}
POST tmdb/_search
{
"_source": [
"title",
"overview"
],
"query": {
"more_like_this": {
"fields": [
"title^10",
"overview"
],
"like": [
{
"_id": "14191"
}
],
"min_term_freq": 1,
"max_query_terms": 12
}
}
}
Query & Filtering 实现多字符串多字段查询
ES 中,有 Query 和 Filter 两种不同的 Context
- Query - 有相关性计算
- Filter - 没有相关性计算,可以利用缓存,性能更好
【示例】
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }
#基本语法
POST /products/_search
{
"query": {
"bool" : {
"must" : {
"term" : { "price" : "30" }
},
"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
},
"should" : [
{ "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
{ "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
],
"minimum_should_match" :1
}
}
}
#改变数据模型,增加字段。解决数组包含而不是精确匹配的问题
POST /newmovies/_bulk
{ "index": { "_id": 1 }}
{ "title" : "Father of the Bridge Part II","year":1995, "genre":"Comedy","genre_count":1 }
{ "index": { "_id": 2 }}
{ "title" : "Dave","year":1993,"genre":["Comedy","Romance"],"genre_count":2 }
#must,有算分
POST /newmovies/_search
{
"query": {
"bool": {
"must": [
{"term": {"genre.keyword": {"value": "Comedy"}}},
{"term": {"genre_count": {"value": 1}}}
]
}
}
}
#Filter。不参与算分,结果的 score 是 0
POST /newmovies/_search
{
"query": {
"bool": {
"filter": [
{"term": {"genre.keyword": {"value": "Comedy"}}},
{"term": {"genre_count": {"value": 1}}}
]
}
}
}
#Filtering Context
POST _search
{
"query": {
"bool" : {
"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
}
}
}
}
#Query Context
POST /products/_bulk
{ "index": { "_id": 1 }}
{ "price" : 10,"avaliable":true,"date":"2018-01-01", "productID" : "XHDK-A-1293-#fJ3" }
{ "index": { "_id": 2 }}
{ "price" : 20,"avaliable":true,"date":"2019-01-01", "productID" : "KDKE-B-9947-#kL5" }
{ "index": { "_id": 3 }}
{ "price" : 30,"avaliable":true, "productID" : "JODL-X-1937-#pV7" }
{ "index": { "_id": 4 }}
{ "price" : 30,"avaliable":false, "productID" : "QQPX-R-3956-#aD8" }
POST /products/_search
{
"query": {
"bool": {
"should": [
{
"term": {
"productID.keyword": {
"value": "JODL-X-1937-#pV7"}}
},
{"term": {"avaliable": {"value": true}}
}
]
}
}
}
#嵌套,实现了 should not 逻辑
POST /products/_search
{
"query": {
"bool": {
"must": {
"term": {
"price": "30"
}
},
"should": [
{
"bool": {
"must_not": {
"term": {
"avaliable": "false"
}
}
}
}
],
"minimum_should_match": 1
}
}
}
#Controll the Precision
POST _search
{
"query": {
"bool" : {
"must" : {
"term" : { "price" : "30" }
},
"filter": {
"term" : { "avaliable" : "true" }
},
"must_not" : {
"range" : {
"price" : { "lte" : 10 }
}
},
"should" : [
{ "term" : { "productID.keyword" : "JODL-X-1937-#pV7" } },
{ "term" : { "productID.keyword" : "XHDK-A-1293-#fJ3" } }
],
"minimum_should_match" :2
}
}
}
POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "brown" }},
{ "term": { "text": "red" }},
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }}
]
}
}
}
POST /animals/_search
{
"query": {
"bool": {
"should": [
{ "term": { "text": "quick" }},
{ "term": { "text": "dog" }},
{
"bool":{
"should":[
{ "term": { "text": "brown" }},
{ "term": { "text": "brown" }},
]
}
}
]
}
}
}
DELETE blogs
POST /blogs/_bulk
{ "index": { "_id": 1 }}
{"title":"Apple iPad", "content":"Apple iPad,Apple iPad" }
{ "index": { "_id": 2 }}
{"title":"Apple iPad,Apple iPad", "content":"Apple iPad" }
POST blogs/_search
{
"query": {
"bool": {
"should": [
{"match": {
"title": {
"query": "apple,ipad",
"boost": 1.1
}
}},
{"match": {
"content": {
"query": "apple,ipad",
"boost":
}
}}
]
}
}
}
DELETE news
POST /news/_bulk
{ "index": { "_id": 1 }}
{ "content":"Apple Mac" }
{ "index": { "_id": 2 }}
{ "content":"Apple iPad" }
{ "index": { "_id": 3 }}
{ "content":"Apple employee like Apple Pie and Apple Juice" }
POST news/_search
{
"query": {
"bool": {
"must": {
"match":{"content":"apple"}
}
}
}
}
POST news/_search
{
"query": {
"bool": {
"must": {
"match":{"content":"apple"}
},
"must_not": {
"match":{"content":"pie"}
}
}
}
}
POST news/_search
{
"query": {
"boosting": {
"positive": {
"match": {
"content": "apple"
}
},
"negative": {
"match": {
"content": "pie"
}
},
"negative_boost": 0.5
}
}
}
单字符串多字段查询 - DisMaxQuery
Disjunction Max Query - 将评分最高的字符评分作为结果返回,满足多个字段是竞争关系的场景
对最佳字段查询进行调优:通过控制 tie_breaker 参数,引入其他字段对计算的一些影响
【示例】
PUT /blogs/_doc/1
{
"title": "Quick brown rabbits",
"body": "Brown rabbits are commonly seen."
}
PUT /blogs/_doc/2
{
"title": "Keeping pets healthy",
"body": "My quick brown fox eats rabbits on a regular basis."
}
POST /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
]
}
}
}
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{
"match": {
"title": "Quick pets"
}
},
{
"match": {
"body": "Quick pets"
}
}
],
"tie_breaker": 0.2
}
}
}
单字符串多字段查询 - Multi Match
场景:最佳字段、多数字段、混合字段
multi_match
best_fields 是默认类型,可以不指定
minimum_should_match 等参数可以传递到生成的 query 中
【示例】
POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.2
}
}
}
POST blogs/_search
{
"query": {
"multi_match": {
"type": "best_fields",
"query": "Quick pets",
"fields": ["title","body"],
"tie_breaker": 0.2,
"minimum_should_match": "20%"
}
}
}
POST books/_search
{
"multi_match": {
"query": "Quick brown fox",
"fields": "*_title"
}
}
POST books/_search
{
"multi_match": {
"query": "Quick brown fox",
"fields": [ "*_title", "chapter_title^2" ]
}
}
DELETE /titles
PUT /titles
{
"settings": { "number_of_shards": 1 },
"mappings": {
"my_type": {
"properties": {
"title": {
"type": "string",
"analyzer": "english",
"fields": {
"std": {
"type": "string",
"analyzer": "standard"
}
}
}
}
}
}
}
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english"
}
}
}
}
POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }
GET titles/_search
{
"query": {
"match": {
"title": "barking dogs"
}
}
}
DELETE /titles
PUT /titles
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "english",
"fields": {"std": {"type": "text","analyzer": "standard"}}
}
}
}
}
POST titles/_bulk
{ "index": { "_id": 1 }}
{ "title": "My dog barks" }
{ "index": { "_id": 2 }}
{ "title": "I see a lot of barking dogs on the road " }
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}
GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [
"title^10",
"title.std"
]
}
}
}
多语言及中文分词与检索
自然语言与查询 recall
处理人类自然语言时,有些情况下,尽管搜索和原文不完全匹配,但希望搜到一些内容。
可采取的优化:
- 归一化词元
- 抽取词根
- 包含同义词
- 拼写错误
混合语言、中文的分词都存在一些挑战
- 词干提取
- 不正确的文档频率
- 语言识别
- 歧义
中文分析器
- elasticsearch-analysis-hanlp
- elasticsearch-analysis-ik
- elasticsearch-analysis-pinyin
SpaceJam 一个全文搜索的实例
使用 SearchTemplate 和 IndexAlias 进行查询
综合排序:Function Score Query 优化算分
ES 默认会以文档的相关度算分进行排序
可以指定一个或多个字段进行排序
使用相关度算分排序,不能满足某些特定条件
function_score 可以在查询结束后,对每一个匹配的文档进行一系列的重新算分,根据新生成的分数进行排序。提供了几种默认的计算分值的函数:
- weight - 为每一个文档设置一个简单而不被规范化的权重
- field_value_factor - 使用该数值来修改
_score
- random_score - 为每一个用户使用一个不同的,随机算分结果
- 衰减函数 - 以某个字段的值为标准,距离某个值越近,得分越高
- script_score - 自定义脚本完全控制所需逻辑
Boost Mode
- multiply
- sum
- min / max
- replace
Max Boost 可以将算分控制在一个最大值
【示例】
DELETE blogs
PUT /blogs/_doc/1
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 0
}
PUT /blogs/_doc/2
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 100
}
PUT /blogs/_doc/3
{
"title": "About popularity",
"content": "In this post we will talk about...",
"votes": 1000000
}
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes"
}
}
}
}
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p"
}
}
}
}
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
}
}
}
}
POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
},
"boost_mode": "sum",
"max_boost": 3
}
}
}
POST /blogs/_search
{
"query": {
"function_score": {
"random_score": {
"seed": 911119
}
}
}
}
Term&PhraseSuggester
自动补全与基于上下文的提示
Completion Suggester,对性能要求比较苛刻。采用了不同的数据结构,并非通过倒排索引来完成。而是将 Analyze 的数据编码成 FST 和索引一起存放。FST 会被 ES 整个加载进内存,速度很快。
精准度:completion > Phrase > Term
召回率:Term > Phrase > Completion
性能:Completion > Phrase > Term
跨集群搜索
早期版本,通过 Tribe Node 可以实现多集群访问的需求,但是还存在一定的问题,现已废弃。
ES 5.3 引入了跨集群搜索的功能。
- 允许任何节点扮演 federated 节点,以轻量的方式,将搜索请求进行代理
- 不需要以 Client Node 形式加入其他集群
第五章:分布式特性及分布式搜索的机制
集群分布式模型及选主与脑裂问题
分布式特性:高可用、易扩展(水平扩展,支持 PB 级数据)
ES 集群名称可以通过配置或 -E cluster.name=xxx 来设定。
ES 节点本质上就是一个 JAVA 进程。一台机器上可以运行多个 ES 进程。
每个 ES 节点都有名字,可以通过配置文件或 -E node.name=xxx 来设定。
每个 ES 节点在启动后,会分片一个 UID,保存在 data 目录下。
- Coordinating Node - 处理请求的节点,叫 Coordinating Node(协调节点),每个节点默认都是协调节点。
- Data Node - 保存分片数据的节点。默认就是 data node,可以设置
node.data: false
禁止成为数据节点。 - Master Node - 负责处理创建、删除索引等请求;决定分片被分配到哪个节点;负责索引的创建与删除;维护并更新集群状态。
- 节点启动后,默认为主节点的候选节点,可以在必要时参与选主,成为 master node。可以设置 node.master: false 禁止成为主节点候选节点。
- 集群状态
- 所有的节点信息
- 所有的索引和其相关的 mapping、setting
- 分片的路由信息
- 每个节点上都保存了集群的状态信息
- 只有 master 节点才能修改集群的状态信息,并负责同步给其他节点
选主过程
集群中的节点互 ping,node id 最小的会成为被选举的节点。
其他节点会加入集群,但是不承担 master 节点的角色,一旦发现被选中的主节点丢失,就会选举出新的 master。
避免脑裂
7.0 之前,minimum_master_nodes 设为 N / 2 + 1
7.0 开始,ES 自动选择以形成仲裁的节点。
分片与集群的故障转移
主分片 - 水平扩展
副本分片 - 高可用:冗余、故障转移
分片数过小:无法通过增加节点实现扩展;分片数过大:使得单个分片容量很小,导致一个节点上有过多分片,影响性能。
文档分布式存储
文档到分配的路由
shard = hash(_routing) % number_of_primary_shards
hash 算法确保离散
默认的 _routing 值是文档 id,可以定制 routing 数值
这也是设置 setting 中主分片数后,不能随意修改的根本原因。
分片及其生命周期
分片是 ES 中的最小工作单元。分片是一个 Lucene 的索引。
倒排索引不可变性
无需考虑并发写文件的问题,避免了锁机制带来的性能问题
一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能
如果需要让一个新的文档可以被搜索,需要重建整个索引。
Lucene Index
在 Lucene 中,单个倒排索引文件被称为 Segment。Segment 是自包闭的,不可变更的。多个 Segment 汇总在一起,称为 Lucene 的 Index,其对应的就是 ES 中的 shard
当有新文档写入时,会生成新 Segment,查询时会同时查询所有 Segment,并且对结果汇总。Lucene 中有一个文件,用来记录所有 Segment 信息,叫做 Commit Point。
删除的文档信息,保存在 .del 文件中。
什么是 Refresh
将 Index buffer 写入 Segment 的过程叫 refresh。refresh 不执行 fsync 操作。
refresh 默认 1 秒发生一次,refresh 后,数据就可以被搜索到了。
如果系统有大量的数据写入,就会产生很多的 Segment
index buffer 被占满时,会触发 refresh,默认是 JVM 的 10%
什么是事务日志
segment 写入磁盘的过程相对耗时,借助文件系统缓存,refresh 时,现将 segment 写入缓存以开放查询
为了保证数据不丢失,所以在 index 文档时,同时写事务日志,高版本开始,事务日志默认落盘。每个分片有一个事务日志。
在 ES refresh 时,index buffer 被清空,事务日志不会清空
什么是 flush
调用 refresh,index buffer 清空并 refresh
调用 fsync,将缓存中的 segments 写入磁盘
清空事务日志
默认 30 分钟调用一次
事务日志满(512MB)
Merge
Segment 很多,需要被定期合并
ES 和 Lucene 会自动进行 Merge 操作
剖析分布式查询及相关性评分
ES 搜索分为两阶段:
- Query
- Fetch
Query 阶段
用户发出搜索请求到 ES 节点。节点收到请求后,会以协调节点的身份,在所有主副本分片中随机选择主分片数个分片,发送查询请求。
被选中的分片执行查询,进行排序。然后,每个分片都会返回 from +size 个排序后的文档 id 和排序值给协调节点。
Fetch 阶段
协调节点会将 Query 阶段从每个分片获取的排序后的文档 id 列表,进行重排序,选取 from 到 from +size 个文档的 id
以 multi get 请求的方式,到相应的分片获取详细的文档数据
潜在问题
性能问题
- 每个分片上需要查的文档数 = from + size
- 最终协调节点需要处理 = 主分片数 * ( from + size)
- 深度分页
相关性算分
每个分片都要基于自己分片上的数据进行相关度计算。这会导致打分偏离的情况,尤其是数据量很少时。当文档总数很少的情况下,主分片数越多,相关性计算会越不准。
解决算分不准的方法
将主分片数设为 1;
使用 _search?search_type=dfs_query_then_fetch
,消耗更多 CPU 和内存,执行性能低下
DELETE message
PUT message
{
"settings": {
"number_of_shards": 20
}
}
GET message
POST message/_doc?routing=1
{
"content":"good"
}
POST message/_doc?routing=2
{
"content":"good morning"
}
POST message/_doc?routing=3
{
"content":"good morning everyone"
}
POST message/_search
{
"explain": true,
"query": {
"match_all": {}
}
}
POST message/_search
{
"explain": true,
"query": {
"term": {
"content": {
"value": "good"
}
}
}
}
POST message/_search?search_type=dfs_query_then_fetch
{
"query": {
"term": {
"content": {
"value": "good"
}
}
}
}
排序及 DocValues&Fielddata
默认采用相关性算分对结果进行降序排序
可以通过设定 sorting 参数,自行设定排序
如果不指定 _score,算分为 null
#单字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {
}
},
"sort": [
{"order_date": {"order": "desc"}}
]
}
#多字段排序
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {
}
},
"sort": [
{"order_date": {"order": "desc"}},
{"_doc":{"order": "asc"}},
{"_score":{ "order": "desc"}}
]
}
GET kibana_sample_data_ecommerce/_mapping
#对 text 字段进行排序。默认会报错,需打开 fielddata
POST /kibana_sample_data_ecommerce/_search
{
"size": 5,
"query": {
"match_all": {
}
},
"sort": [
{"customer_full_name": {"order": "desc"}}
]
}
#打开 text 的 fielddata
PUT kibana_sample_data_ecommerce/_mapping
{
"properties": {
"customer_full_name" : {
"type" : "text",
"fielddata": true,
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
#关闭 keyword 的 doc values
PUT test_keyword
PUT test_keyword/_mapping
{
"properties": {
"user_name":{
"type": "keyword",
"doc_values":false
}
}
}
DELETE test_keyword
PUT test_text
PUT test_text/_mapping
{
"properties": {
"intro":{
"type": "text",
"doc_values":true
}
}
}
DELETE test_text
DELETE temp_users
PUT temp_users
PUT temp_users/_mapping
{
"properties": {
"name":{"type": "text","fielddata": true},
"desc":{"type": "text","fielddata": true}
}
}
Post temp_users/_doc
{"name":"Jack","desc":"Jack is a good boy!","age":10}
#打开 fielddata 后,查看 docvalue_fields 数据
POST temp_users/_search
{
"docvalue_fields": [
"name","desc"
]
}
#查看整型字段的 docvalues
POST temp_users/_search
{
"docvalue_fields": [
"age"
]
}
分页与遍历-FromSize&SearchAfter&ScrollAPI
from + size
当一个查询:from = 990, size = 10,会在每个分片上先获取 1000 个文档。然后,通过协调节点聚合所有结果。最后,再通过排序选取前 1000 个文档。
页数越深,占用内存越多。为了避免深分页问题,ES 默认限定到 10000 个文档。
search after
实时获取下一页文档信息,不支持指定页数,只能向下翻页。
需要指定 sort,并保证值是唯一的
然后,可以反复使用上次结果中最后一个文档的 sort 值进行查询
scroll
创建一个快照,有新的数据写入以后,无法被查到。
每次持续后,输入上一次的 scroll id
POST tmdb/_search
{
"from": 10000,
"size": 1,
"query": {
"match_all": {
}
}
}
#Scroll API
DELETE users
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":11}
POST users/_doc
{"name":"user2","age":12}
POST users/_doc
{"name":"user2","age":13}
POST users/_count
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}
POST users/_search
{
"size": 1,
"query": {
"match_all": {}
},
"search_after":
[
10,
"ZQ0vYGsBrR8X3IP75QqX"],
"sort": [
{"age": "desc"} ,
{"_id": "asc"}
]
}
#Scroll API
DELETE users
POST users/_doc
{"name":"user1","age":10}
POST users/_doc
{"name":"user2","age":20}
POST users/_doc
{"name":"user3","age":30}
POST users/_doc
{"name":"user4","age":40}
POST /users/_search?scroll=5m
{
"size": 1,
"query": {
"match_all" : {
}
}
}
POST users/_doc
{"name":"user5","age":50}
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAWAWbWdoQXR2d3ZUd2kzSThwVTh4bVE0QQ=="
}
处理并发读写
采用乐观锁机制
内部版本控制:_seq_no
+ primary_term
外部版本控制:version
+ version_type=external