Work In Progress
初识 ElasticSearch
使用场景
搜索引擎:生产环境中使用最多,例如京东、淘宝、美团等使用 ES 实现高性能商品搜索,支持多条件筛选、排序和相关性排名等。
日志分析与监控:ELK 作为日志分析的行业标准,是 ES 经典的使用场景。
数据分析:大数据分析,通过 DSL 快速得到结果。
与类似组件对比
选 ES :复杂查询/分析场景
选 RDBMS :强事务需求
ES 处理搜索/日志
ClickHouse 处理深度分析
存储场景决策树
快速开始
⚠️ 注意 :自行下载 Docker 以及 docker-compose 工具
Kibana 快速开始
通过 localhost:5601
进入:
使用 Dev Tools:
Logstash 快速开始
运行 docker-compose up
命令后可以到日志中查看是否导入数据成功。
Cerebro 快速开始
通过 localhost:9000
进入:
可以看到刚导入的 movies 数据。
ElasticSearch 入门
Elasticsearch 中的最小数据单元,以 JSON 格式存储数据。
1、类比于关系型数据库中的一行数据。
2、包含实际数据字段(如 title
, content
等)。
描述文档自身属性的系统字段,用于唯一标识和管理文档。
包含以下核心元数据:
- _id
:文档唯一标识符(可自定义或自动生成)。
- _index
:文档所属的索引。
- _version
:文档版本号(支持乐观锁)。
一类结构相似文档的集合,类似于关系型数据库中的「表」。
通过 Mapping 定义字段类型和属性(如文本、数值、日期等)。支持倒排索引,优化全文搜索性能。
Elasticsearch 集群中的一个运行实例,本质是一个 Java 进程。
节点类型包括:
- 主节点 :管理集群状态。
- 数据节点 :存储数据。
- 协调节点 :处理请求路由。
索引的物理子集,用于分布式存储和计算。分片分为主分片(Primary Shard)和副本分片(Replica Shard)。
主分片 :数据存储和写入的基本单元,数量在索引创建时固定。
副本分片 :主分片的冗余拷贝,提供高可用和查询负载均衡。
与关系型数据库类比
健康状况
1、在 Kibana 中查询,使用命令 GET _cluster/health
2、在 Cerebro 中查询
测试节点下线
此时状态为 Yellow。
CRUD
仅当文档不存在时创建。需指定 ID(PUT
)或自动生成(POST
)。
若文档存在则替换(全量更新)。可指定 ID(PUT
)或自动生成(POST
)。
提供部分字段或脚本(支持 doc
或 script
)
部分更新。支持 upsert
(不存在时插入)。需启用 _source
字段。默认返回更新后的 Source。
Create
Demo1
复制 # Req
# create document 自动生成 _id
POST users/_doc
{
"user" : "LanLance"
}
# Resp
{
"_index" : "users",
"_type" : "_doc",
"_id" : "64IRqpYBLb6gKsOx04Rm",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
Demo2
复制 # Req
# create document 指定 ID
PUT users/_doc/1?op_type=create
{
"user" : "LanLance_2"
}
# Resp
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
Demo3
复制 # Req
# create document 指定 ID;如果已经存在就报错
PUT users/_create/1
{
"user" : "LanLance"
}
# Resp
{
"error" : {
"root_cause" : [
{
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, document already exists (current version [1])",
"index_uuid" : "J6rYdyr6TY-H9asFHjyTwQ",
"shard" : "0",
"index" : "users"
}
],
"type" : "version_conflict_engine_exception",
"reason" : "[1]: version conflict, document already exists (current version [1])",
"index_uuid" : "J6rYdyr6TY-H9asFHjyTwQ",
"shard" : "0",
"index" : "users"
},
"status" : 409
}
Get
Demo1
复制 # Req
# Get 指定 ID
GET users/_doc/1
# Resp
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 1,
"_primary_term" : 1,
"found" : true,
"_source" : {
"user" : "LanLance_2"
}
}
Index & Update
Demo1
复制 # Req
# Update 指定 ID (先删除,在写入)
PUT users/_doc/1
{
"user" : "LanLance"
}
# Resp
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 2,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 2,
"_primary_term" : 1
}
Demo2
复制 # Req
# Update 在原文档上增加字段
POST users/_update/1/
{
"doc":{
"message" : "trying out Elasticsearch"
}
}
# Resp
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 3,
"result" : "updated",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 3,
"_primary_term" : 1
}
Delete
Demo1
复制 # Req
# Delete 指定 ID
DELETE users/_doc/1
# Resp
{
"_index" : "users",
"_type" : "_doc",
"_id" : "1",
"_version" : 4,
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 4,
"_primary_term" : 1
}
Bulk、MGet、MSearch
批量增删改(Create/Index/Update/Delete)
功能与场景对比
响应中标记每个操作的 error
和 status
性能与限制对比
使用
复制 # bulk
## 执行第1次
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test2", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
## 执行第2次
POST _bulk
{ "index" : { "_index" : "test", "_id" : "1" } }
{ "field1" : "value1" }
{ "delete" : { "_index" : "test", "_id" : "2" } }
{ "create" : { "_index" : "test2", "_id" : "3" } }
{ "field1" : "value3" }
{ "update" : {"_id" : "1", "_index" : "test"} }
{ "doc" : {"field2" : "value2"} }
# mget
GET /_mget
{
"docs" : [
{
"_index" : "test",
"_id" : "1"
},
{
"_index" : "test",
"_id" : "2"
}
]
}
## URI 中指定 index
GET /test/_mget
{
"docs" : [
{
"_id" : "1"
},
{
"_id" : "2"
}
]
}
GET /_mget
{
"docs" : [
{
"_index" : "test",
"_id" : "1",
"_source" : false
},
{
"_index" : "test",
"_id" : "2",
"_source" : ["field3", "field4"]
},
{
"_index" : "test",
"_id" : "3",
"_source" : {
"include": ["user"],
"exclude": ["user.location"]
}
}
]
}
清除数据
复制 DELETE users
DELETE test
DELETE test2
倒排索引
倒排索引是搜索引擎和全文检索系统的核心数据结构,核心思想是通过建立「单词到文档」的映射关系从而实现 Keyword 快速定位包含该词的所有文档。例如当用户搜索「ElasticSearch」时,系统可直接通过倒排索引找到包含这一词汇的所有文档集合。
倒排索引的实现依赖于两个关键结构:单词词典 (Term Dictionary)和倒排列表 (Posting List)。
Analysis & Analyzer
Analysis 是通过 Analyzer 来实现的。
分词器
由三部分组成:
Character Filters:针对原始文本处理,例如去除 html。
Token Filter:将切分的的单词进行加工,例如小写、删除 stopwords、增加同义词等。
复制 Character Filters => Tokenizer => Token Filters
ElasticSearch 内置分词器
按 Unicode 标准分词,移除标点符号,转小写,支持多语言基础处理。
在非字母字符处分割文本,删除非字母字符,转小写(如 Hello-World
→ ["hello", "world"]
)。
按空格严格分割,保留原始格式(如代码、特定标识)。
仅按空格分割,保留大小写和标点(如 Quick-Brown
→ ["Quick-Brown"]
)。
需过滤常见停用词(如英文中的“the”、“is”)的文本。
按非字母字符分割出连续字母词条,转小写后移除停用词(如 The fox
→ ["fox"]
)。
将整个输入作为单一词条,不进行任何处理(如 Hello World
→ ["Hello World"]
)。
通过正则表达式(默认 \W+
)分割文本,转小写(可自定义正则)。
按语言规则分词,处理停用词、转小写、词干提取等(如 running
→ ["run"]
)。
analyzer API
通过 analyzer API 能够快速得到分词结果进行测试,以下提供了一些例子可以去到 Kibana 的 Dev Tools 进行使用。
复制 #standard
GET _analyze
{
"analyzer": "standard",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
#simple
GET _analyze
{
"analyzer": "simple",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
#stop
GET _analyze
{
"analyzer": "stop",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
#whitespace
GET _analyze
{
"analyzer": "whitespace",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
#keyword
GET _analyze
{
"analyzer": "keyword",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
#pattern
GET _analyze
{
"analyzer": "pattern",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
#english
GET _analyze
{
"analyzer": "english",
"text": "2 running Quick brown-foxes leap over lazy dogs in the summer evening."
}
示例:standard 分词器的分词结果
复制 {
"tokens" : [
{
"token" : "2",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<NUM>",
"position" : 0
},
{
"token" : "running",
"start_offset" : 2,
"end_offset" : 9,
"type" : "<ALPHANUM>",
"position" : 1
},
{
"token" : "quick",
"start_offset" : 10,
"end_offset" : 15,
"type" : "<ALPHANUM>",
"position" : 2
},
{
"token" : "brown",
"start_offset" : 16,
"end_offset" : 21,
"type" : "<ALPHANUM>",
"position" : 3
},
{
"token" : "foxes",
"start_offset" : 22,
"end_offset" : 27,
"type" : "<ALPHANUM>",
"position" : 4
},
{
"token" : "leap",
"start_offset" : 28,
"end_offset" : 32,
"type" : "<ALPHANUM>",
"position" : 5
},
{
"token" : "over",
"start_offset" : 33,
"end_offset" : 37,
"type" : "<ALPHANUM>",
"position" : 6
},
{
"token" : "lazy",
"start_offset" : 38,
"end_offset" : 42,
"type" : "<ALPHANUM>",
"position" : 7
},
{
"token" : "dogs",
"start_offset" : 43,
"end_offset" : 47,
"type" : "<ALPHANUM>",
"position" : 8
},
{
"token" : "in",
"start_offset" : 48,
"end_offset" : 50,
"type" : "<ALPHANUM>",
"position" : 9
},
{
"token" : "the",
"start_offset" : 51,
"end_offset" : 54,
"type" : "<ALPHANUM>",
"position" : 10
},
{
"token" : "summer",
"start_offset" : 55,
"end_offset" : 61,
"type" : "<ALPHANUM>",
"position" : 11
},
{
"token" : "evening",
"start_offset" : 62,
"end_offset" : 69,
"type" : "<ALPHANUM>",
"position" : 12
}
]
}
Search API
示例
复制 # URI Search
GET kibana_sample_data_ecommerce/_search?q=customer_first_name:Eddie
GET kibana*/_search?q=customer_first_name:Eddie
GET /_all/_search?q=customer_first_name:Eddie
# Request Body Search
POST kibana_sample_data_ecommerce/_search
{
"profile": true,
"query": {
"match_all": {}
}
}
指定查询的索引
URI Search
示例
复制 GET /movies/_search?q=2012&df=title&sort=year:desc&from=0&size=10&timeout=1s
{
"profile":"true"
}
参数:
q 指定查询语句,使用 Query String Syntax。
df 指定默认字段,不指定时会对所有字段进行查询。
泛查询
复制 GET /movies/_search?q=2012
{
"profile":"true"
}
未指定 df
参数时 ES 会搜索所有字段,可能触发跨字段匹配,性能消耗较大。分析结果如下:
复制 {
"type": "DisjunctionMaxQuery",
"description": "(title.keyword:2012 | id.keyword:2012 | year:[2012 TO 2012] | genre:2012 | @version:2012 | @version.keyword:2012 | id:2012 | genre.keyword:2012 | title:2012)"
}
可以看到在所有字段进行了匹配。
显式字段查询
复制 GET /movies/_search?q=title:2012
{
"profile":"true"
}
通过 title:2012
显式指定字段,精准限定搜索范围,比泛查询更高效。分析结果如下:
复制 {
"type": "TermQuery",
"description": "title:2012"
}
短语分割问题
复制 GET /movies/_search?q=title:Beautiful Mind
{
"profile":"true"
}
实际执行 title:Beautiful OR Mind
,空格被识别为 OR 逻辑,返回包含任意词的文档。
精确短语匹配
复制 GET /movies/_search?q=title:"Beautiful Mind"
{
"profile":"true"
}
使用双引号包裹词组,强制进行短语搜索,要求词语按顺序完整出现。
分组查询
复制 GET /movies/_search?q=title:(Beautiful Mind)
{
"profile":"true"
}
括号实现逻辑分组,等效于 title:Beautiful OR title:Mind
,优先执行组内操作。
布尔运算符
复制 GET /movies/_search?q=title:(Beautiful AND Mind)
显式布尔查询,要求同时包含两个词(AND 逻辑)。
范围查询
复制 GET /movies/_search?q=title:beautiful AND year:[2002 TO 2018%7D
[2002 TO 2018}
表示闭区间包含 2002,开区间不包含 2018(%7D
为 URL 编码的 }
符号)。
通配符搜索
复制 GET /movies/_search?q=title:b*
{
"profile":"true"
}
b*
匹配以 b 开头的任意长度字符,支持 ?
匹配单个字符,注意通配符在前端影响性能。
模糊匹配
复制 GET /movies/_search?q=title:beautiful~1
{
"profile":"true"
}
GET /movies/_search?q=title:"Lord Rings"~2
{
"profile":"true"
}
"Lord Rings"~2
表示短语中允许间隔 2 个单词。
Request Body Search & Query DSL
通常生成环境都使用这种方法,更加强大、功能更丰富。
示例
复制 POST movies/_search
{
"from":0,
"size":10,
"query": {
"match": {
"title": {
"query": "last christmas",
"operator": "and"
}
}
}
}
基本 Match 查询
复制 POST movies/_search
{
"query": {
"match": {
"title": "last christmas"
}
}
}
精确 AND 匹配
复制 POST movies/_search
{
"query": {
"match": {
"title": {
"query": "last christmas",
"operator": "and"
}
}
}
}
通过 operator:"and"
强制要求所有分词必须同时存在。
短语搜索
复制 POST movies/_search
{
"query": {
"match_phrase": {
"title": {
"query": "one love"
}
}
}
}
模糊短语匹配
复制 POST movies/_search
{
"query": {
"match_phrase": {
"title": {
"query": "one love",
"slop": 1
}
}
}
}
slop
参数允许词语间隔位置数(此处允许间隔 1 个词)。
Query String 搜索
复制 GET /movies/_search
{
"query": {
"query_string": {
"default_field": "title",
"query": "Beafiful AND Mind"
}
}
}
多字段搜索
复制 GET /movies/_search
{
"query": {
"query_string": {
"fields": ["title","year"],
"query": "2012"
}
}
}
Simple Query 搜索
复制 GET /movies/_search
{
"query": {
"simple_query_string": {
"query": "Beautiful +mind",
"fields": ["title"]
}
}
}
跨索引查询
复制 POST /movies,404_idx/_search?ignore_unavailable=true
{
"query": {
"match_all": {}
}
}
ignore_unavailable=true
忽略不存在索引。
源过滤
复制 POST kibana_sample_data_ecommerce/_search
{
"_source":["order_date"],
"query": {
"match_all": {}
}
}
_source
过滤返回字段。
脚本字段
复制 GET kibana_sample_data_ecommerce/_search
{
"script_fields": {
"new_field": {
"script": {
"lang": "painless",
"source": "doc['order_date'].value+'hello'"
}
}
}
}
动态计算返回字段。
Mapping
Mapping 类似数据库中的 schema 定义,用于定义字段名称、类型、相关配置。
字段的数据类型
倒排索引(分词后存储),支持模糊匹配和相关性评分。
Lucene 的数值索引优化(如 integer
、long
)。
"U29tZSBoZWxsbyB3b3JsZA=="
Lucene 的数值/日期范围索引,支持 >=
、<=
等操作。
{"city": "Beijing", "zip": "100000"}
独立索引的子文档,需通过 nested
查询访问。
[{"name": "book", "price": 15}]
{"lat": 40.7128, "lon": -74.0060}
多值字段(无需显式声明),底层以扁平化多值形式存储。
存储为 32 位或 128 位整数,支持 CIDR 范围查询。
Dynamic Mapping
在写入文档时如果索引不存在会自动创建索引,该机制使得我们不用手动定义 Mappings,但是通常不用这个,因为容易推算错误,并且 ES 禁止对有数据写入的字段修改定义。
示例
复制 # 插入测试数据
PUT mapping_test/_doc/1
{
"uid" : "123",
"isVip" : false,
"isAdmin": "true",
"age":19,
"heigh":180
}
# 查看字段类型
GET mapping_test/_mapping
# Resp
{
"mapping_test" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "long"
},
"heigh" : {
"type" : "long"
},
"isAdmin" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"isVip" : {
"type" : "boolean"
},
"uid" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
如上结果所示,Dynamic Mapping 机制会自动推断类型,同时 text 类型会新增一个 keyword 类型支持精确查找。
控制 Dynamic Mappings
dynamic
字段不同情况下的表现。
Index
index
用于控制字段是否被索引。
示例
复制 "mobile" : {
"type" : "text",
"index": false
}
Index Options
仅记录文档 ID,节省空间,适用于只需文档匹配的场景
文档编号(doc id)+ 词频(term frequency)
记录位置信息,支持短语查询(Phrase Query)和邻近查询(Proximity Query)
文档编号 + 词频 + 位置 + 偏移量(offset)
index_options
用于控制是否存储文档 ID、词频、位置和偏移量等,从而影响搜索效率和功能支持(如短语查询、高亮等)。同时记录的内容越多,占用存储空间越大。
null_value
只有 Keyword 类型支持设定 null_value。
示例
复制 "mobile" : {
"type" : "keyword",
"null_value": "NULL"
}
自定义 Analyzer
Character Filter
示例
复制 POST _analyze
{
"tokenizer":"keyword",
"char_filter":["html_strip"],
"text": "<b>hello world</b>"
}
能够将 html 的标签去除。
复制 POST _analyze
{
"tokenizer": "standard",
"char_filter": [
{
"type" : "mapping",
"mappings" : [ "- => _"]
}
],
"text": "123-456, I-test! test-990 650-555-1234"
}
能够将 text 中的 -
替换为 _
。
Tokenizer
示例
复制 POST _analyze
{
"tokenizer":"path_hierarchy",
"text":"/user/ymruan/a/b/c/d/e"
}
能够按照目录层级进行切分。
Token Filter
示例
复制 GET _analyze
{
"tokenizer": "whitespace",
"filter": ["lowercase","stop","snowball"],
"text": ["The girls in China are playing this game!"]
}
Index Template
用于自动设定 Mappings 和 Settings,并按照一定的规则自动匹配到新创建的索引中。
可以控制 order 的数值控制 merge 的过程。先应用 order 低的,后续高的会覆盖之前的设定。
示例
复制 PUT /_template/template_test
{
"index_patterns" : ["test*"],
"order" : 1,
"settings" : {
"number_of_shards": 1,
"number_of_replicas" : 2
},
"mappings" : {
"numeric_detection": true
}
}
表示当一个新索引以 test
开头时,会自动将索引的分片数设置为 2,同时会自动探测数字类型。
Dynamic Template
动态设定字段类型。例如:
示例
复制 PUT my_index
{
"mappings": {
"dynamic_templates": [
{
"strings_as_boolean": {
"match_mapping_type": "string",
"match": "is*",
"mapping": {
"type": "boolean"
}
}
},
{
"strings_as_keywords": {
"match_mapping_type": "string",
"mapping": {
"type": "keyword"
}
}
}
]
}
}
表示当字符串类型为 is 开头时设置为 boolean 类型,其余设置为 keyword 类型。
聚合(Aggregation)
ElasticSearch 除了提供搜索以外,还提供了针对 ES 进行同喜分析的功能。
聚合是一个分析总结全套的数据,而不是寻找单个文档。
本节示例需要在 Kibana 中添加官方提供的 Sample flight data 样例数据。
Bucket
示例
复制 GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs":{
"flight_dest":{
"terms":{
"field":"DestCountry"
}
}
}
}
# Resp
"aggregations" : {
"flight_dest" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 3187,
"buckets" : [
{
"key" : "IT",
"doc_count" : 2371
},
{
"key" : "US",
"doc_count" : 1987
},
// ...
]
}
}
将国家分成了桶。
Metric
示例
复制 GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs":{
"flight_dest":{
"terms":{
"field":"DestCountry"
},
"aggs":{
"avg_price":{
"avg":{
"field":"AvgTicketPrice"
}
},
"max_price":{
"max":{
"field":"AvgTicketPrice"
}
},
"min_price":{
"min":{
"field":"AvgTicketPrice"
}
}
}
}
}
}
# Resp
// ...
{
"key" : "IT",
"doc_count" : 2371,
"max_price" : {
"value" : 1195.3363037109375
},
"min_price" : {
"value" : 100.57646942138672
},
"avg_price" : {
"value" : 586.9627099618385
}
},
// ...
进行了平均值、最大值、最小值的计算。
嵌套
示例
复制 GET kibana_sample_data_flights/_search
{
"size": 0,
"aggs":{
"flight_dest":{
"terms":{
"field":"DestCountry"
},
"aggs":{
"stats_price":{
"stats":{
"field":"AvgTicketPrice"
}
},
"wather":{
"terms": {
"field": "DestWeather",
"size": 5
}
}
}
}
}
}
# Resp
// ...
{
"key" : "IT",
"doc_count" : 2371,
"wather" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 506,
"buckets" : [
{
"key" : "Clear",
"doc_count" : 428
},
{
"key" : "Sunny",
"doc_count" : 424
},
{
"key" : "Rain",
"doc_count" : 417
},
{
"key" : "Cloudy",
"doc_count" : 414
},
{
"key" : "Heavy Fog",
"doc_count" : 182
}
]
},
"stats_price" : {
"count" : 2371,
"min" : 100.57646942138672,
"max" : 1195.3363037109375,
"avg" : 586.9627099618385,
"sum" : 1391688.585319519
}
},
// ...
在使用国家分桶后再进行票价统计和 5 组最常见天气的分布。
深入搜索
Term 搜索与全文搜索
过滤(Filter)、聚合(Aggregation)
{"term": {"field": "value"}}
{"match": {"field": "user_input"}}
Term 搜索
示例
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" }
2、Term 查询
复制 POST /products/_search
{
"query": {
"term": {
"desc": {
// "value": "iPhone"
// "value": "iphone"
}
}
}
}
使用 iPhone
不能搜索出结果,而小写的 iphone
可以。因为是精确查询,而原始数据在分词后的结果为 iphone
。
复制 POST /products/_search
{
"query": {
"term": {
"desc.keyword": {
// "value": "iPhone"
// "value": "iphone"
}
}
}
}
此时使用 keyword 类型能成功获取数据。
3、Constant Score 转为 Filter
复制 POST /products/_search
{
"explain": true,
"query": {
"constant_score": {
"filter": {
"term": {
"productID.keyword": "XHDK-A-1293-#fJ3"
}
}
}
}
}
将 Query 转为 Filter,忽略 TF-IDF 计算,避免相关性算分的开销。
全文搜索
Match / Match Phrase / Query String
索引和搜索时都会进行分词,查询字符串先传递到一个合适的分词器,然后生成一个供查询的列表。
查询会对每个词项逐个查询再将结果进行合并,并为每个文档生成一个算分。
match
/ multi_match
,避免 query_string
结构化搜索
结构化数据是指具有固定格式和明确字段的数据,每个字段都有特定的类型(如字符串、数字、日期等),并且数据是可预测、易于解析的。结构化搜索即对结构化数据进行搜索。
示例
1、插入数据
复制 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" }
2、Bool
复制 POST products/_search
{
"query": {
"constant_score": {
"filter": {
"term": {
"avaliable": true
}
}
}
}
}
3、数字
复制 POST products/_search
{
"query": {
"term": {
"price": 30
}
}
}
4、Range
复制 POST products/_search
{
"query" : {
"constant_score" : {
"filter" : {
"range" : {
"price" : {
"gte" : 20,
"lte" : 30
}
}
}
}
}
}
搜索相关性
搜索的相关性算分,描述了一个文档和查询语句匹配的程度。ES 会对每个匹配查询条件的结果进行算分 _score。
打分的本质是排序,需要把最符合用户需求的文档排在前面。ES5 之前,默认的相关性算分采用 TF-IDF,现在采用 BM 25。
词频 TF
度量一条查询和结果文档相关性的简单方法:将搜索中的每一个词 TF 相加。
Stop Word 不应考虑,类似 「the」、「的」。
逆文档频率 IDF
Inverse Document Frequency:$log(全部文档数/检索词出现过的文档总数)$
TF-IDF 的本质就是将 TF 求和变成了加权求和:$TF(X)*IDF(X)$
BM 25
与 TF-IDF 相比,当一个词的 TF 无限增加时,BM 25 算分会趋于一个稳定值。
Boosting
Boosting 是控制相关度的一种手段。
当 boost > 1 时打分的相关度相对性提升。
当 0 < boost < 1 时打分的权重相对性降低。
多字段多字符串查询
bool 查询
一个 bool 查询是一个或者多个查询子句的组合。
Filter Context 查询字句,必须不能匹配。
Filter Context 必须匹配,不贡献算分。
1、bool 查询
复制 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
}
}
}
如果 bool 查询中没有 must 条件,那么 should 中必须至少满足一条查询。
2、boost 控制查询分数
复制 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": {
"boosting": {
"positive": {
"match": {
"content": "apple"
}
},
"negative": {
"match": {
"content": "pie"
}
},
"negative_boost": 0.5
}
}
}
此时会将苹果产品放前边,而 id 为 3 的显示在最后。
多字段单字符串查询
Disjunction Max Query
将任何与任一查询匹配的文档作为结果返回。采用字段上最匹配的评分最终评分返回。
示例
1、插入数据
复制 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."
}
2、bool 测试
复制 POST /blogs/_search
{
"query": {
"bool": {
"should": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
结果可以看到 1 号文档在前,是因为 bool 会对两个进行加和平均,不符合直觉。
2、dis_max 测试
复制 POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Brown fox" }},
{ "match": { "body": "Brown fox" }}
]
}
}
}
此时 2 号文档在前,符合直觉。
复制 POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
]
}
}
}
结果还是 1 号文档在前,同时分数相同。因为最高分数的 quick 都只出现一次。但 2 号文档中有 pet ,直觉上应该 2 号文档更高,但 dis_max 只取最大的一条。
复制 POST blogs/_search
{
"query": {
"dis_max": {
"queries": [
{ "match": { "title": "Quick pets" }},
{ "match": { "body": "Quick pets" }}
],
"tie_breaker": 0.2
}
}
}
加入 tie_breaker 后会把最匹配一条之外的分数与其值相乘,这样 2 号文档就能更高,符合直觉。
MultiMatch
multi_match
是 Elasticsearch 中一种用于在多个字段中执行全文搜索的查询方式。它扩展了 match
查询,允许你在多个字段上同时进行匹配。
姓名拆分(first_name + last_name)
示例
1、插入数据
复制 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 " }
2、使用 most_fields
复制 GET /titles/_search
{
"query": {
"multi_match": {
"query": "barking dogs",
"type": "most_fields",
"fields": [ "title", "title.std" ]
}
}
}
结果显示 2 号文档在前,符合直觉。若不使用 MultiMatch 则会 1 号文档在前,因为 English 分词器会把 barking dogs 分为 bark 和 dog,1 号文档分数更高。
自然语言与查询
当处理人类自然语言时,有时尽管搜索和原文不完全匹配,但是希望搜到一些内容。
可以采取的措施:
混合多语言的挑战
不同的索引使用不同的语言;同一个索引中,不同的字段使用不同的语言;一个文档的一个字段内混合不同的语言。
词干提取:以色列文档,包含了希伯来语,阿拉伯语,俄语和英文。
不正确的文档频率:英文为主的文章中,德文算分高(稀有)。
中文分词(IK)
由于生产环境中最常见的中文分词器为 IK,因此本篇也以 IK 为主。
安装
示例
复制 POST _analyze
{
"analyzer": "ik_smart",
"text": ["剑桥分析公司多位高管对卧底记者说,他们确保了唐纳德·特朗普在总统大选中获胜"]
}
Search Template
用于解耦程序和搜索 DSL。
示例
复制 POST _scripts/tmdb
{
"script": {
"lang": "mustache",
"source": {
"_source": [
"title",
"overview"
],
"size": 20,
"query": {
"multi_match": {
"query": "{{q}}",
"fields": [
"title",
"overview"
]
}
}
}
}
}
POST tmdb/_search/template
{
"id":"tmdb",
"params": {
"q": "basketball with cartoon aliens"
}
}```
上游可以不感知模版的变化,避免耦合。
### Index Alias
实现零停机运维。比如在进行索引重建、版本升级、滚动更新等操作时,无需中断服务。
**示例**
1、插入数据
```json
PUT movies-2019/_doc/1
{
"name":"the matrix",
"rating":5
}
PUT movies-2019/_doc/2
{
"name":"Speed",
"rating":3
}
2、设置别名
复制 POST _aliases
{
"actions": [
{
"add": {
"index": "movies-2019",
"alias": "movies-latest"
}
}
]
}
POST movies-latest/_search
{
"query": {
"match_all": {}
}
}
Function Score Query
可以在查询结束后对每一个匹配的文档进行一系列的重新算分,根据新生成的分数进行排序。
Weight:为每一个文档设置一个简单而不被规范化的权重。
Field Value Factor:使用该数值来修改 \_score
,例如将「热度」和「点赞数」作为算分的参考因素。
Random Score: 为每一个用户使用一个不同的,随机算分结果。
衰减函数:以某个字段的值为标准,距离某个值越近,得分越高。
Script Score:自定义脚本完全控制所需逻辑。
示例
1、插入数据
复制 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
}
2、使用多个参数测试
复制 POST /blogs/_search
{
"query": {
"function_score": {
"query": {
"multi_match": {
"query": "popularity",
"fields": [ "title", "content" ]
}
},
"field_value_factor": {
"field": "votes",
"modifier": "log1p" ,
"factor": 0.1
}
}
}
}
使用 modifier 平滑参数后:$新的算分 = 老的算分 * log(1 + 投票数)$
Factor 参数:$新的算分 = 老的算分 * log(1 + factor * 投票数)$
3、一致性随机函数
复制 POST /blogs/_search
{
"query": {
"function_score": {
"random_score": {
"seed": 911119
}
}
}
}
具体需求:让每个用户能看到不同的随机排名,但是也希望同一个用户访问时,结果的相对顺序保持一致。
4、Boost Mode 和 Max Boost
复制 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
}
}
}
Max Boost 可以将算分控制在一个最大值。Boost Mode 用于进行算分的数学运算。
Suggester
实现搜索引擎中纠错的功能。原理是将文本分解为 Token 然后在索引的字典中查找相似的 Term 并返回。
四种 Suggester 的不同之处:
对输入文本的每个词条进行纠错或建议,基于索引中的词典查找相似 Term。
在 Term Suggester 基础上,考虑多个词条之间的关系(如是否共同出现、相邻程度等)。
多个词组成的短语级别的纠错和建议(如句子片段的修正)。
提供前缀匹配的自动补全功能,支持快速搜索建议(如用户输入时的提示)。
基于 Completion Suggester,增加了上下文信息的支持(如地理位置、类别等过滤条件)。
需要结合上下文信息的自动补全(如特定分类下的搜索提示)。
1、插入数据
复制 DELETE articles
POST articles/_bulk
{ "index" : { } }
{ "body": "lucene is very cool"}
{ "index" : { } }
{ "body": "Elasticsearch builds on top of lucene"}
{ "index" : { } }
{ "body": "Elasticsearch rocks"}
{ "index" : { } }
{ "body": "elastic is the company behind ELK stack"}
{ "index" : { } }
{ "body": "Elk stack rocks"}
{ "index" : {} }
{ "body": "elasticsearch is rock solid"}
2、Term Suggester
几种 Suggestion Mode
Missing:如果索引中已经存在,就不提供建议。
复制 POST /articles/_search
{
"size": 1,
"query": {
"match": {
"body": "lucen rock"
}
},
"suggest": {
"term-suggestion": {
"text": "lucen rock",
"term": {
"suggest_mode": "missing",
"field": "body"
}
}
}
}
3、Phrase Suggester
在 Term Suggester 的基础上增加了一些额外的逻辑。
Max Errors:最多可以拼错的 Terms 数。
Confidence:控制返回建议的置信度阈值。只有当建议短语的原始得分加上长度归一化后 ≥confidence 时才会被返回,默认为 1。
复制 POST /articles/_search
{
"suggest": {
"my-suggestion": {
"text": "lucne and elasticsear rock hello world ",
"phrase": {
"field": "body",
"max_errors":2,
"confidence":2,
"direct_generator":[{
"field":"body",
"suggest_mode":"always"
}],
"highlight": {
"pre_tag": "<em>",
"post_tag": "</em>"
}
}
}
}
}
4、Completion Suggester
提供了自动补全功能。对性能要求比较严苛,采用了非倒排索引的数据结构,将 Analyze 数据编码成 FST 和索引一起存放。FST 会整个加载进内存,速度很快。同时 FST 仅支持前缀查找。
复制 DELETE articles
PUT articles
{
"mappings": {
"properties": {
"title_completion":{
"type": "completion"
}
}
}
}
POST articles/_bulk
{ "index" : { } }
{ "title_completion": "lucene is very cool"}
{ "index" : { } }
{ "title_completion": "Elasticsearch builds on top of lucene"}
{ "index" : { } }
{ "title_completion": "Elasticsearch rocks"}
{ "index" : { } }
{ "title_completion": "elastic is the company behind ELK stack"}
{ "index" : { } }
{ "title_completion": "Elk stack rocks"}
{ "index" : {} }
复制 POST articles/_search?pretty
{
"size": 0,
"suggest": {
"article-suggester": {
"prefix": "elk ",
"completion": {
"field": "title_completion"
}
}
}
}
5、Context Suggester
是 Completion Suggester 的拓展,能够在搜索中加入更多的上下文信息。
可以定义两种类型的 Context:
实现 Context Suggester 的具体步骤:
索引数据,并且为每个文档加入 Context 信息。
结合 Context 进行 Suggestion 查询。
复制 DELETE comments
PUT comments
PUT comments/_mapping
{
"properties": {
"comment_autocomplete":{
"type": "completion",
"contexts":[{
"type":"category",
"name":"comment_category"
}]
}
}
}
POST comments/_doc
{
"comment":"I love the star war movies",
"comment_autocomplete":{
"input":["star wars"],
"contexts":{
"comment_category":"movies"
}
}
}
POST comments/_doc
{
"comment":"Where can I find a Starbucks",
"comment_autocomplete":{
"input":["starbucks"],
"contexts":{
"comment_category":"coffee"
}
}
}
复制 POST comments/_search
{
"suggest": {
"MY_SUGGESTION": {
"prefix": "sta",
"completion":{
"field":"comment_autocomplete",
"contexts":{
"comment_category":"coffee"
}
}
}
}
}
分布式
节点
节点是一个 ElasticSearch 示例
一个机器上可以运行多个示例但生产环境推荐只运行一个
每一个节点启动后都会分配一个 UID,保存在 data 目录下
Coordinating Node
处理请求的节点,叫 Coordinating Node
路由请求到正确的节点,例如创建索引的请求,需要路由到 Master
所有节点默认都是 Coordinating Node
通过将其他类型设置成 False,使其成为 Dedicated Coordinating Node
Data Node
可以保存数据的节点,叫做 Data Node
节点启动后,默认就是数据节点。可以设置 node.data:false 禁止
Data Node 的职责
保存分片数据。在数据扩展上起到了至关重要的作用(由 Master Node 决定如何把分片分发到数据节点上)
Master Node
Master Node 的职责
处理创建,删除索引等请求/决定分片被分配到哪个节点 /负责索引的创建与删除
Master Node 的最佳实践
Master 节点非常重要,在部署上需要考虑解决单点的问题
为一个集群设置多个 Master 节点/每个节点只承担 Master 的单一角色
Master Eligible Nodes
一个集群,支持配置多个 Master Eligible 节点。这些节点可以在必要时(如 Master 节点出现故障,网络故障时)参与选主流程,成为 Master 节点
每个节点启动后,默认就是一个 Master Eligible 节点
可以设置 node.master: false 禁止
当集群内第一个 Master Eligible 节点启动时候,它会将自己选举成 Master 节点
选主过程
互相 Ping 对方,Node ld 低的会成为被选举的节点
其他节点会加入集群,但是不承担 Master 节点的角色。一旦发现被选中的主节点丢失,就会选举出新的 Master 节点
脑裂问题
Split-Brain,分布式系统的经典网络问题,当出现网络问题,一个节点和其他节点无法连接
Node 2 和 Node 3 会重新选举 Master
Node 1 自己还是作为 Master 组成一个集群,同时更新 Cluster State
导致 2 个 Master 维护不同的 Cluster State,当网络恢复时,无法选择正确恢复
解决方法
限定选举条件,设置 quorum(仲裁),只有当 Master Eligible 节点数大于 quorum 时才能进行选举
分片
Primary Shard
分片是 ElasticSearch 分布式存储的基石(主分片 / 副本分片)
通过主分片,将数据分布在所有节点上
Primary Shard 可以将一份索引的数据分散在多个 Data Node 上,实现存储的水平扩展
主分片数在索引创建时候指定,后续默认不能修改,如要修改需重建索引
Replica Shard
数据可用性
通过引入副本分片(Replica Shard)提高数据的可用性。一旦主分片丢失,副本分片可以 Promote 成主分片。副本分片数可以动态调整。每个节点上都有完备的数据。如果不设置副本分片,一旦出现节点硬件故障,就有可能造成数据丢失
提升系统的读取性能
副本分片由主分片(Primary Shard)同步。通过支持增加 Replica 个数,一定程度可以提高读取的吞吐量
分片数的设定
如何规划一个索引的主分片数和副本分片数
主分片数过小:例如创建了 1 个 Primary Shard 的 Index。如果该索引增长很快,集群无法通过增加节点实现对这个索引的数据扩展
主分片数设置过大:导致单个 Shard 容量很小,引发一个节点上有过多分片,影响性能
集群健康状态
复制 GET /_cluster/health
{
"cluster_name" : "lanlance",
"status" : "green",
"timed_out" : false,
"number_of_nodes" : 2,
"number_of_data_nodes" : 2,
"active_primary_shards" : 21,
"active_shards" : 42,
"relocating_shards" : 0,
"initializing_shards" : 0,
"unassigned_shards" : 0,
"delayed_unassigned_shards" : 0,
"number_of_pending_tasks" : 0,
"number_of_in_flight_fetch" : 0,
"task_max_waiting_in_queue_millis" : 0,
"active_shards_percent_as_number" : 100.0
}
Green:健康状态,所有的主分片和副本分片都可用
Yellow:亚健康,所有的主分片可用,部分副本分片不可用
文档到分片的路由算法
$shard = hash(routing) / 主分片数$
可以自行制定 routing 值,与业务逻辑绑定也可以
是 Primary Shard 数不能修改的根本原因
删除一个文档的流程
分片的内部原理
倒排索引的不可变性
倒排索引采用 Immutable Design,一旦生成,不可更改
不可变性,带来了的好处如下:
无需考虑并发写文件的问题,避免了锁机制带来的性能问题
一旦读入内核的文件系统缓存,便留在哪里。只要文件系统存有足够的空间,大部分请求就会直接请求内存,不会命中磁盘,提升了很大的性能
但坏处是如果需要让一个新的文档可以被搜索,需要重建整个索引。
Lucene Index
在 Lucene 中,单个倒排索引文件被称为 Segment。Segment 是自包含的,不可变更的。多个 Segments 汇总在一起称为 Lucene 的 Index,其对应的就是 ES 中的 Shard
当有新文档写入时,会生成新 Segment,查询时会同时查询所有 Segments,并且对结果汇总。Lucene 中有一个文件用来记录所有 Segments 信息,叫做 Commit Point
Refresh
将 Index buffer 写入 Segment 的过程叫 Refresh。Refresh 不执行 fsync 操作
Refresh 默认 1 秒发生一次,可通过 index.refresh_interval 配置。Refresh 后数据就可以被搜索到了。这也是为什么 ElasticSearch 被称为近实时搜索
如果系统有大量的数据写入,那就会产生很多的 Segment
Index Buffer 被占满时会触发 Refresh,默认值是 JVM 的 10%
Transaction Log
Segment 写入磁盘的过程相对耗时,借助文件系统缓存,Refresh 时先将 Segment 写入缓存以开放查询
为了保证数据不会丢失,所以在 Index 文档时同时写 Transaction Log,高版本开始 Transaction Log 默认落盘。每个分片有一个 Transaction Log
在 ES Refresh 时 Index Buffer 被清空,Transaction log 不会清空
Flush
调用 Refresh,清空 Index Buffer
调用 fsync,将缓存中的 Segments 写入磁盘
默认 30 分钟调用一次,当 Transaction Log 满时(默认 512 MB)也会调用
Merge
Segment 很多,需要被定期合并
减少 Segments / 真正删除已经删除的文档
ES 和 Lucene 会自动进行 Merge 操作
POST my_index / _forcemerge
分布式搜索的运行机制
Demo
全文搜索
tmdb-search
是电影搜索项目,主要用于索引和搜索 TMDB(The Movie Database)的电影数据。
项目组成部分
ingest_tmdb_from_file.py
: 将 TMDB 数据导入 Elasticsearch
ingest_tmdb_to_appserarch.py
: 将数据导入 AppSearch(可选功能)
mapping/english_analyzer.json
: 默认英文分析器配置
mapping/english_english_3_shards.json
: 3 分片的配置版本
多个针对 "Space Jam" 电影的查询示例,展示不同的查询策略
快速开始
确保已安装所需 Python 包和 Python3 环境:
复制 pip install -r requirements.txt
ElasticSearch 的安装详见上方快速开始章节内容。
复制 python ingest_tmdb_from_file.py
复制 # 普通搜索
python query_tmdb.py
# 带高亮显示的搜索
python query_tmdb.py highlight
整体流程
参考
https://github.com/onebirdrocks/geektime-ELK/
https://www.elastic.co/elasticsearch