Skip to main content
  1. Docs/

ElasticSearch 基础使用

·14 mins· ·
Owl Dawn
Author
Owl Dawn
Table of Contents

接下来主要介绍如果使用 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"
    }
  }
}

Related

2020
·1 min
2024
·1 min
Kubernetes Operator
·9 mins
About Me
About
摄影调色
·2 mins
Golang Channel
·5 mins