1. 前言
Elasticsearch索引是一组相关文档的集合,文档在Elasticsearch中用JSON来表示,每个文档都有一个唯一的”_id“字段来标识。每个文档又是一组字段的集合,字段可以有自己的数据类型,可以是数字、字符串、日期、布尔类型等,Elasticsearch可以索引文档,并对索引的文档做检索和数据分析。
2. 新增文档
我们已经知道,每个文档都有一个唯一的”_id“字段标识。新增文档时,我们既可以指定ID,也可以不指定ID,不指定ID时Elasticsearch会自动生成基于Base64编码长度为20的GUID字符串。
下面是指定ID索引文档的请求:
PUT items/_doc/1
{"title":"苹果","price":5
}
下面是不指定ID索引文档的请求:
POST items/_doc
{"title":"香蕉","price":3
}
可以看到,系统自动生成的ID值为”h1e65Y4BXAgLe9UU1-xS“
{"_index": "items","_id": "h1e65Y4BXAgLe9UU1-xS","_score": 1,"_source": {"title": "香蕉","price": 3}
}
你也许会好奇,items索引压根就不存在,为什么文档可以索引成功呢?这是因为Elasticsearch默认会自动创建索引,可以配置action.auto_create_index
来关闭,如下所示:
PUT _cluster/settings
{"persistent": {"action.auto_create_index":false}
}
再次发起一个索引不存在的文档索引请求,会得到一个异常
{"error": {"root_cause": [{"type": "index_not_found_exception","reason": "no such index [items-1] and [action.auto_create_index] is [false]","index_uuid": "_na_","index": "items-1"}],"type": "index_not_found_exception","reason": "no such index [items-1] and [action.auto_create_index] is [false]","index_uuid": "_na_","index": "items-1"},"status": 404
}
Elasticsearch默认会自动创建不存在的索引,并根据索引的文档数据动态映射字段类型,在这个例子中,price被映射为long类型,title被映射为text和keyword多数据类型。
GET items/_mapping{"items": {"mappings": {"properties": {"price": {"type": "long"},"title": {"type": "text","fields": {"keyword": {"type": "keyword","ignore_above": 256}}}}}}
}
除了单文档的添加,Elasticsearch还支持批量添加文档,对应的API是 _bulk,对于大量文档的写入,批量操作可以显著提升性能。如下示例,批量写入两个文档:
POST items/_bulk
{"index":{"_id":"1"}}
{"title":"苹果", "price":5}
{"index":{"_id":"2"}}
{"title":"香蕉", "price":3}
文档与文档之间必须通过换行符来分割,除了索引请求,还可以批量处理删除、更新请求,如下请求,它在items索引下添加了两篇文档、删除了一篇文档、同时更新了orders索引下的一篇文档。
POST _bulk
{"index":{"_index":"items","_id":"1"}}
{"title":"苹果", "price":5}
{"create":{"_index":"items","_id":"2"}}
{"title":"香蕉", "price":3}
{"update":{"_index":"orders","_id":"1"}}
{"doc":{"order_amount":600}}
{"delete":{"_index":"items","_id":"3"}}
关于create和index的区别是:对于相同的文档,create请求会失败,而index总是会成功,已存在的文档index请求会更新文档。
3. 更新文档
更新文档操作分为:部分更新和全量更新,部分更新即更新文档的部分字段,全量更新则是直接替换整个文档。
部分更新操作,如下示例,把1号文档的title改为”新鲜苹果“,price字段仍然会保留
POST items/_update/1
{"doc": {"title":"新鲜苹果"}
}GET items/_doc/1{"_index": "items","_id": "1","_version": 2,"_seq_no": 2,"_primary_term": 1,"found": true,"_source": {"title": "新鲜苹果","price": 5}
}
全量更新操作会直接覆盖原有文档,本质上是通过index API来完成的,如下示例,price字段会丢失:
PUT items/_doc/1
{"title":"红富士苹果"
}GET items/_doc/1{"_index": "items","_id": "1","_version": 3,"_seq_no": 3,"_primary_term": 1,"found": true,"_source": {"title": "红富士苹果"}
}
除此之外,Elasticsearch还支持upsert操作,它类似于关系数据库中的insertOrUpdate,如果文档存在则执行更新操作,不存在则执行写入操作。
如下示例,如果不存在ID=1的文档,则写入upsert部分内容,如果存在ID=1的文档,则更新doc部分的内容。
POST items/_update/1
{"doc": {"place_of_production": "山东烟台"},"upsert": {"title": "苹果","price": 5}
}
第一次请求时没有place_of_production字段,第二次请求时,因为文档已经存在,则会写入place_of_production。
除了根据指定ID更新单个文档,Elasticsearch同样支持根据搜索条件批量更新文档,对应的API是_update_by_query。批量更新文档首先需要提供“query”部分设定要更新文档的匹配条件,再提供"script"部分即文档更新的脚本。
如下示例,“match_all”匹配所有文档,将所有商品的价格上调1元钱
POST items/_update_by_query
{"query": {"match_all": {}},"script": {"lang": "painless","source": "ctx._source.price+=1"}
}
文档更新结果:
[{"_index": "items","_id": "1","_score": 1,"_source": {"price": 6,"title": "苹果"}},{"_index": "items","_id": "2","_score": 1,"_source": {"price": 4,"title": "香蕉"}}
]
4. 删除文档
删除文档同样分为根据ID单个删除和批量删除文档。需要说明的是,执行删除操作后,Elasticsearch并不会立即将文档从磁盘中物理删除掉,这是因为Elasticsearch采用段的设计来存储文档,每个段都是独立且不可变的索引结构,这么做可以提高查询性能,删除操作仅仅是将文档标记为“已删除”,在后续“段合并”阶段Elasticsearch才会真正将这些被标记为已删除的文档真正的删除并释放存储空间。
如下示例,删除items索引下的1号文档:
DELETE items/_doc/1
返回结果中,result=deleted代表删除成功,同时_version会自增1。
{"_index": "items","_id": "1","_version": 2,"result": "deleted","_shards": {"total": 2,"successful": 1,"failed": 0},"_seq_no": 1,"_primary_term": 1
}
还可以根据条件来批量删除文档,对应的API是_delete_by_query,如下示例,将价格大于等于4元的商品全部删除:
POST items/_delete_by_query
{"query": {"range": {"price": {"gte": 4}}}
}
返回结果中,deleted=2代表删除了两篇符合要求的文档
{"took": 37,"timed_out": false,"total": 2,"deleted": 2,"batches": 1,"version_conflicts": 0,"noops": 0,"retries": {"bulk": 0,"search": 0},"throttled_millis": 0,"requests_per_second": -1,"throttled_until_millis": 0,"failures": []
}
5. 迁移文档
除了常规的增删改查,Elasticsearch还支持将一个索引的文档迁移到另一个索引中,可以是集群内迁移,也支持跨集群迁移。
对应的API是_reindex,什么时候需要用到reindex呢?索引的一部分配置是静态的,一旦创建好就不允许更改了,比如:主分片的数量,字段的数据类型等,要更改这些静态配置就需要reindex重新构建索引。
举个例子,现在有一个news索引存储新闻数据,包含新闻的标题和评论:
POST news/_doc
{"title": "女子在小区车库遇1米多高阿富汗猎犬","comments": [{"user":"张三","content":"遛狗不拴绳,等于狗遛人"},{"user":"李四","content":"那最后是什么处理结果呢"},{"user":"王五","content":"大型犬太危险了"}]
}
默认情况下,Elasticsearch会把comments字段动态映射为Object类型,comments数组会被构建成一个扁平的键值对数组,从而丢失了单个对象之间的关系,如下所示:
{"comments.user":["张三","李四","王五"],"comments.content":["遛狗不拴绳,等于狗遛人","那最后是什么处理结果呢","大型犬太危险了"]
}
现在我们要搜索:张三评论了包含“危险”词语的新闻,搜索条件如下所示:
GET news/_search
{"query": {"bool": {"must": [{"match": {"comments.user": "张三"}},{"match": {"comments.content": "危险"}}]}}
}
因为Object类型的关系,文档竟然被意外的返回了,这明显与我们的需求不符,这个时候就需要把comments字段更改为nested数据类型,让comments数组的每个对象都单独存储。这种操作不能在news索引上直接操作,所以我们需要创建一个新的索引,然后通过reindex操作来做数据迁移。
新索引news-nested创建命令如下所示,comments类型改为nested,同时新增一个comments_count字段记录评论数:
PUT news-nested
{"mappings": {"properties": {"title":{"type": "text"},"comments":{"type": "nested"},"comments_count":{"type": "integer"}}}
}
接着,调用reindex API完成数据迁移,指定源索引和目标索引,以及迁移过程中要执行的脚本:
POST _reindex
{"source": {"index": "news"},"dest": {"index": "news-nested"},"script": {"source": "ctx._source.comments_count = ctx._source.comments.length"}
}
查看目标索引的数据,发现文档迁移成功,且comments_count字段有了
[{"_index": "news-nested","_id": "lFcm5o4BXAgLe9UUo-wj","_score": 1,"_source": {"comments": [{"user": "张三","content": "遛狗不拴绳,等于狗遛人"},{"user": "李四","content": "那最后是什么处理结果呢"},{"user": "王五","content": "大型犬太危险了"}],"comments_count": 3,"title": "女子在小区车库遇1米多高阿富汗猎犬"}}
]
再次执行nested搜索,结果没有召回任何文档,符合需求。
GET news-nested/_search
{"profile": false,"query": {"bool": {"must": [{"nested": {"path": "comments","query": {"bool": {"must": [{"match": {"comments.user": "张三"}},{"match": {"comments.content": "危险"}}]}}}}]}}
}
6. 尾巴
Elasticsearch索引是一系列相关文档的集合,文档又是一组字段的集合,每个字段有自己的数据类型,Elasticsearch可以索引并检索和分析文档数据。文档的增删改操作即可以针对单个文档进行,也可以批量处理,当面临大量操作时,使用批量API可以大幅提升性能。最后,当需要修改索引的静态配置时,可以通过reindex API在索引间迁移数据。