接下来主要介绍如果使用 REST 接口对 ElasticSearch 进行操作。 搜索引擎执行以下两个主要操作:
- 索引(indexing):用于接受文档,对其进行处理,并将其存储在一个索引中
- 搜索(searching):用于根据查询从索引中检索数据。
Kibana 是一个针对 ElasticSearch 的开源分析及可视化平台,用来搜索、查看交互存储在 Elasticsearch 索引中的数据。使用 Kibana,可以通过各种图表进行高级数据分析及展示。并且可以为 Logstash 和 ElasticSearch 提供的日志分析友好的 Web 界面,可以汇总、分析和搜索重要数据日志,还可以让海量数据更容易理解。它操作简单,基于浏览器的用户界面可以快速创建仪表板(dashboard)实时显示 Elasticsearch 查询动态。
以下操作基于 Kibana
检查 ES #
执行
GET /
得到输出
{
"name" : "search-es-demo",
"cluster_name" : "search-es-demo",
"cluster_uuid" : "xxxxxxx",
"version" : {
"number" : "7.1.1",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "xxxxxx",
"build_date" : "2024-11-15T11:58:60.123456Z",
"build_snapshot" : false,
"lucene_version" : "8.0.0",
"minimum_wire_compatibility_version" : "6.8.0",
"minimum_index_compatibility_version" : "6.0.0-beta1"
},
"tagline" : "You Know, for Search"
}
以上信息表明 ES 处于一个正常运行的状态。在这里我们可以看到 ES 的版本信息以及我们正在使用的 ES 的 Cluster 名称等信息。
创建一个索引及文档 #
接下来创建一个叫做 twitter 的索引(index),并插入一个文档(document)。
POST twitter/_doc/1
{
"user": "GB",
"uid": 1,
"city": "Beijing",
"province": "Beijing",
"country": "China"
}
得到输出
{
"_index" : "twittertest",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 2,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
在通常的情况下,新写入的文档并不能马上被用于搜索。新增的索引必须写入到 Segment 后才能被搜索到。需要等到 refresh 操作才可以。在默认的情况下每隔一秒的时间 refresh 一次。这就是我们通常所说的近实时。我们通过 REST API 时写进一个文档,在写入的时候没有强制 refresh 操作,而是立即进行搜索,可能就搜索不到刚写入的文档。
在写入文档时,如果该文档的 ID 已经存在,那么就更新现有的文档;如果该文档从来没有存在过,那么就创建新的文档。如果更新时该文档有新的字段并且这个字段在现有的 mapping 中没有出现,那么 Elasticsearch 会根据 schem on write 的策略来推测该字段的类型,并更新当前的 mapping 到最新的状态。
动态 mapping 还可能导致某些字段未映射到你的预期,从而导致索引请求失败。显式 mapping 允许更好地控制索引中的字段和数据类型。 一旦知道索引 schema,明确定义索引映射是一个好主意。我们在运行完上面的命令后,可以通过如下的命令来查看当前索引的 mapping:
GET twitter/_mapping
得到输出
{
"twitter" : {
"mappings" : {
"properties" : {
"city" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"country" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"province" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"uid" : {
"type" : "long"
},
"user" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
}
}
}
对于这个类型的字段
"city" : {
"type" : "text", // 类型定义
"fields" : {
"keyword" : {
"type" : "keyword", // 类型定义
"ignore_above" : 256
}
}
}
这个字段是 multi-field 字段。可能存在以不同方式索引同一字段的情况,比如我们定义字段 city 为 text 类型,text 类型的数据在摄入的时候会进行分词,这样可以实现搜索的功能。同时这个字段也被定义为 keyword 类型的数据,可以让我们针对它进行精确匹配(如区分大小写、空格等符号),聚合和排序。
同样,我们也可以这样定义这个字段
"city" : {
"type" : "text", // 类型定义
"fields" : {
"raw" : {
"type" : "keyword", // 类型定义
"ignore_above" : 256
}
}
}
这在早期的 ES 发行版中比较常见,一般定义它为 raw。在现在的版本中,默认情况下,ES 会自动选用 keyword 作为它的名称。在我们访问这个 multi-field 字段时,我们需要以这样的形式来进行访问:city.text 及 city.raw。
在实际的使用中,我们有时可能对一个字段需要同时进行搜索和聚合,我们可以这样定义它为 multi-field,但是如果我们仅对搜索或者聚合感兴趣,我们只需要定义其中的一种类型。这样做的好处是,它可以提高数据摄入的速度,因为不必为两个类型进行索引,同时它也可以减少磁盘的使用。
Elasticsearch 的数据类型:
- text:全文搜索字符串
- keyword:用于精确字符串匹配和聚合
- date 及 date_nanos:格式化为日期或数字日期的字符串
- byte, short, integer, long:整数类型
- boolean:布尔类型
- float,double,half_float:浮点数类型
- 分级的类型:object 及 nested。参考 “Elasticsearch: nested 对象”
默认情况下,ES 可以理解你正在索引的文档结构并自动创建映射(mapping)的定义,即**显式映射(Explicit mapping)**创建。绝大情况下可以 work well。使用显式映射可以使用 无模式(schemaless)方法快速摄取数据,而无需担心字段类型。
但是有时为了在索引中更好的性能和结果,需要手动定义映射。微调映射有一些优势:
- 减少磁盘的索引大小(金庸自定义字段的功能)
- 仅索引感兴趣的字段(一般加速)
- 快速搜索或实时分析(如聚合)
- 正确定义字段是否必须分词为多个 token 或单个 token
- 定义映射类型,如地理点、suggester、向量
假如,我们想创建一个索引 test,并且含有 id 及 message 字段。id 字段为 keyword 类型,而 message 字段为 text 类型,那么我们可以使用如下的方法来创建:
PUT test
{
"mappings": {
"properties": {
"id": {
"type": "keyword"
},
"message": {
"type": "text"
}
}
}
}
甚至可以使用如下的 API 来追加一个新的字段 age,并且它的类型为 long 类型:
PUT test/_mapping
{
"properties": {
"age": {
"type": "long"
}
}
}
最终得到索引 test 的 mapping:
GET test/_mapping
// output
{
"test" : {
"mappings" : {
"properties" : {
"age" : {
"type" : "long"
},
"id" : {
"type" : "keyword"
},
"message" : {
"type" : "text"
}
}
}
}
}
注意:我们不能为已经建立好的 index 动态修改 mapping。这是因为一旦修改,那么之前建立的索引就变成不能搜索的了。一种办法是 reindex 从而重新建立我们的索引,即删除并重新创建索引。如果在之前的 mapping 加入新的字段,那么我们可以不用重新建立索引。
refresh #
默认情况下通过以上方法写入到 ES 中的文档,不能马上进行搜索,需要进行 refresh。通常会有一个 refresh timmer 来定时完成这个操作,周期为 1s。这个周期可以爱索引的设置中进行配置。如果期望结果可以马上对搜索可见,可以加上 refresh=true
query 参数。
PUT twitter/_doc/1?refresh=true
{
"user": "GB",
"uid": 1,
"city": "Beijing",
"province": "Beijing",
"country": "China"
}
但是频繁这样操作会导致性能下降,因此不建议这样操作。另一种方法是通过设置 refresh=wait_for
,可以确保调用接口后等待下一个 refresh 周期发生完成后再返回。
更多参阅 Elasticsearch 中的 refresh 和 flush 操作指南
我们调用 POST 接口创建一个 document,如果该文档之前没有被创建过,那么它的 _version
就为 1,如果我们调用 POST 或 PUT 更改这个文档,_version
就会自动加一。如果我们不期望这个版本变化,可以使用 _create
端点接口实现
PUT twitter/_create/1
{
"user": "GB",
"uid": 1,
"city": "Shenzhen",
"province": "Guangdong",
"country": "China"
}
如果文档存在的话就会收到报错。同样可以使用 PUT twitter/_doc/1?op_type=create
实现同样的效果。
其中 op_type 存在两种值:index 和 create
同样,如果是在 linux 或 MacOS 机器上,可以使用以下指令实现
curl -XPUT 'http://localhost:9200/twitter/_doc/1?pretty' -H 'Content-Type: application/json' -d '
{
"user": "GB",
"uid": 1,
"city": "Shenzhen",
"province": "Guangdong",
"country": "China"
}'
查看文档 #
可以通过以下命令查看被修改的文档
GET twitter/_doc/1
// 响应
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "1",
"_version" : 11,
"_seq_no" : 29,
"_primary_term" : 10,
"found" : true,
"_source" : {
"user" : "GB",
"uid" : 1,
"city" : "Beijing",
"province" : "Beijing",
"country" : "China"
}
}
如果只想要得到文档的 _source 部分,则可以请求 GET twitter/_doc/1/_source
。在 ElasticSearch 7.0 之后,在 type 最终要废除的情况下,建议使用 GET twitter/_source/1
或者也可以只获取 source 的部分字段 GET twitter/_doc/1?_source=city,age,province
或者一次请求查询多个文档的部分字段
GET _mget
{
"docs": [
{
"_index": "twitter",
"_id": 1,
"_source":["age", "city"]
},
{
"_index": "twitter",
"_id": 2,
"_source":["province", "address"]
}
]
}
或者简写:
GET twitter/_doc/_mget
{
"ids": ["1", "2"]
}
自动 ID 生成 #
创建文档时,如果不指定文档的 ID,ES 会自动生成一个 ID,此时我们必须使用 POST 接口。如果像上文一样指定了 ID,ES 会检查 ID 是否存在,选择更新或创建
POST my_index/_doc
{
"content": "this is really cool"
}
// 返回
{
"_index" : "my_index",
"_type" : "_doc",
"_id" : "xxxxxx",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
分片异步 #
默认情况下,索引操作需要等待当前复制组的所有分片完成后才会返回。如果我们希望设置异步复制,以允许我们在主分片上同步执行索引操作,在副本分片上异步执行,这样 API 调用会更快返回响应操作:
POST my_index/_doc?replication=async
{
"content": "this is really cool"
}
修改文档 #
对于多个客户端同时更新一个索引的场景,可以参考 文档
上文我们可以通过 POST 修改文档,通常我们通过 POST 创建或修改。对于修改文档,我们通常使用 PUT。如果需要更新文档的部分内容,可以使用以下方法:
POST twitter/_update/1
{
"doc": {
"city": "成都",
"province": "四川"
}
}
查询后修改:
POST twitter/_update_by_query
{
"query": {
"match": {
"user": "GB"
}
},
"script": {
"source": "ctx._source.city = params.city;ctx._source.province = params.province;ctx._source.country = params.country",
"lang": "painless",
"params": {
"city": "上海",
"province": "上海",
"country": "中国"
}
}
}
对于 painless 语言,不认可使用中文字段名字,需要通过以下方式操作
POST edd/_update_by_query
{
"query": {
"match": {
"姓名": "张彬"
}
},
"script": {
"source": "ctx._source[\"签到状态\"] = params[\"签到状态\"]",
"lang": "painless",
"params" : {
"签到状态":"已签到"
}
}
}
doc_as_upsert
参数检查具有给定ID的文档是否已经存在,并将提供的 doc 与现有文档合并。 如果不存在具有给定 id 的文档,则会插入具有给定文档内容的新文档。 下面的示例使用 doc_as_upsert
合并到 id 为 3 的文档中,或者如果不存在则插入一个新文档:
POST /catalog/_update/3
{
"doc": {
"author": "Albert Paro",
"title": "Elasticsearch 5.0 Cookbook",
"description": "Elasticsearch 5.0 Cookbook Third Edition",
"price": "54.99"
},
"doc_as_upsert": true
}
删除文档 #
DELETE twitter/_doc/1
// 或者
POST twitter/_delete_by_query
{
"query": {
"match": {
"city": "上海"
}
}
}
检查文档存在 #
HEAD twitter/_doc/1
检查索引存在 #
HEAD twitter
删除索引 #
DELETE twitter
批处理命令 #
使用 _bulk 命令,需要注意 不要添加除了换行以外的空格,否则会导致错误
注意:通过 bulk API 为数据编制索引时,你不应在集群上进行任何查询/搜索。 这样做可能会导致严重的性能问题。
POST _bulk
{ "index" : { "_index" : "twitter", "_id": 1} }
{"user":"双榆树-张三","message":"今儿天气不错啊,出去转转去","uid":2,"age":20,"city":"北京","province":"北京","country":"中国","address":"中国北京市海淀区","location":{"lat":"39.970718","lon":"116.325747"}}
{ "index" : { "_index" : "twitter", "_id": 2 }}
{"user":"东城区-老刘","message":"出发,下一站云南!","uid":3,"age":30,"city":"北京","province":"北京","country":"中国","address":"中国北京市东城区台基厂三条3号","location":{"lat":"39.904313","lon":"116.412754"}}
{ "index" : { "_index" : "twitter", "_id": 3} }
{"user":"东城区-李四","message":"happy birthday!","uid":4,"age":30,"city":"北京","province":"北京","country":"中国","address":"中国北京市东城区","location":{"lat":"39.893801","lon":"116.408986"}}
{ "index" : { "_index" : "twitter", "_id": 4} }
{"user":"朝阳区-老贾","message":"123,gogogo","uid":5,"age":35,"city":"北京","province":"北京","country":"中国","address":"中国北京市朝阳区建国门","location":{"lat":"39.718256","lon":"116.367910"}}
{ "index" : { "_index" : "twitter", "_id": 5} }
{"user":"朝阳区-老王","message":"Happy BirthDay My Friend!","uid":6,"age":50,"city":"北京","province":"北京","country":"中国","address":"中国北京市朝阳区国贸","location":{"lat":"39.918256","lon":"116.467910"}}
{ "index" : { "_index" : "twitter", "_id": 6} }
{"user":"虹桥-老吴","message":"好友来了都今天我生日,好友来了,什么 birthday happy 就成!","uid":7,"age":90,"city":"上海","province":"上海","country":"中国","address":"中国上海市闵行区","location":{"lat":"31.175927","lon":"121.383328"}}
一个好的起点是批量处理 1000 到 5000 个文档,总有效负载在 5MB 到 15MB 之间。如果我们的 payload 过大,那么可能会造成请求的失败。可以在 示例数据 来做实验
查询数据量 #
GET twitter/_count
查询 #
在 Elasticsearch 中的搜索中,有两类搜索:
- queries
- aggregations
它们之间的区别在于:query 可以帮我们进行全文搜索,而 aggregation 可以帮我们对数据进行统计及分析。我们有时也可以结合 query 及 aggregation 一起使用,比如我们可以先对文档进行搜索然后在进行 aggregation:
// 先搜寻在 title 含有 community 的文档,然后再对数据进行 aggregation
GET blogs/_search
{
"query": {
"match": {
"title": "community"
}
},
"aggregations": {
"top_authors": {
"terms": {
"field": "author"
}
}
}
}
ElasticSearch 提供了一个基于 JSON 的完整的 Query DSL (Domain Specific Language) 来定义查询,将查询 DSL 视为查询的 AST(抽象语法树)。它提供:
- 全文搜索
- 聚合
- 排序,分页及操控响应
搜索示例:
// 搜索所有文档,默认返回 10 个
GET /_search
// 设置返回 size,并通过 from 进行分页
GET /_search?size=20&from=2
// 控制返回较少的字段
GET /_search?filter_path=hits.hits._score,hits.hits._source.city
// 对多个 index 搜索
POST /index1,index2,index3/_search
// 针对所有以 index 为开头的索引来进行搜索,但是排除 index3 索引。
POST /index,-index3/_search
// 搜索特定 index
GET twitter/_search
响应
{
"took" : 13,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 9,
"relation" : "eq"
},
"max_score" : 1.0,
"hits" : [
{
"_index" : "twitter",
"_type" : "_doc",
"_id" : "20",
"_score" : 1.0,
"_source" : {
...
搜索返回结果中,有一个 _score
项,表示搜索结果的相关度,值越高,相关度越高。默认情况下按照分数由大到小排序。
在上面,我们可以看到 relation 字段的值为 eq,它表明搜索的结果为 9 个文档。这也是满足条件的所有文档,但是针对许多的大数据搜索情况,有时我们的搜索结果会超过10000个,那么这个返回的字段值将会是 gte
source filtering #
可以通过 _source
来定义返回想要的字段。更多参考 文档
GET twitter/_search
{
"_source": ["user", "city"],
"query": {
"match_all": {
}
}
}
// 或者
GET twitter/_search
{
"_source": {
"includes": ["user", "city"]
},
"query": {
"match_all": {
}
}
}
// 或者使用 fields 来指定返回的字段,而不是 _scource,这样更高效
GET twitter/_search
{
"_source": false,
"fields": ["user", "city"],
"query": {
"match_all": {
}
}
}
有些时候,我们想要的 field 可能在 _source 里根本没有,那么我们可以使用 script field 来生成这些 field。允许为每个匹配返回 [script evaluation](https://www.elastic.co/guide/en/elasticsearch/reference/7.5/modules-scripting.html(基于不同的字段)。这种使用 script 的方法生成查询对于大量的文档来说可能会占用大量资源
GET twitter/_search
{
"query": {
"match_all": {}
},
"script_fields": {
"years_to_100": {
"script": {
"lang": "painless",
"source": "100-doc['age'].value"
}
},
"year_of_birth":{
"script": "2019 - doc['age'].value"
}
}
}