2023-02-09 - 3 Elasticsearch基础操作

news2024/11/25 14:48:23

本章主要介绍ES的基础操作,具体包括索引、映射和文档的相关操作。其中,在文档操作中将分别介绍单条操作和批量操作。在生产实践中经常会通过程序对文档进行操作,因此在介绍文档操作时会分别介绍DSL请求形式和Java的高级REST编码形式。

1 索引操作

本节主要介绍索引的相关操作,涉及创建、删除、关闭和打开索引,以及索引别名的操作。其中,索引别名的操作在生产环境中使用比较广泛,可以和关闭或删除索引配合使用。在生产环境中使用索引时,一定要慎重操作,因为稍有不慎就会导致数据的丢失或异常。

1.1 创建索引

使用ES构建搜索引擎的第一步是创建索引。在创建索引时,可以按照实际需求对索引进行主分片和副分片设置。ES创建索引的请求类型为PUT,其请求形式如下:

PUT /${index_name} 
{   "settings":{} 
    "mappings":{} 
}

其中:变量index_name就是创建的目标索引名称;可以在settings子句内部填写索引相关的设置项,如主分片个数和副分片个数等;可以在mappings子句内部填写数据组织结构,即数据映射

在第1章中曾介绍过创建索引hotel的语句,但是当时的主分片个数使用的是系统默认值(默认值为5),并且没有使用副分片个数(默认值为0)。假设设置主分片个数为15,副分片个数为2,则相应的DSL如下:

PUT /hotel 
{  
    "settings" : { 
        "number_of_shards" : 15,    //指定主分片个数 
        "number_of_replicas" : 2    //指定副分片个数 
    }, 
    "mappings":{ 
        "properties":{} 
    } 
}

1.2 删除索引

ES中删除索引的请求类型是DELETE,其请求形式如下:

DELETE /${index_name}

其中,${index_name}就是将要被删除的索引的名称,例如执行下面的删除命令:

DELETE /hotel

系统返回信息如下:

{ 
 "acknowledged" : true                   //操作成功,通知信息 
}

通过返回信息可知,目标索引hotel已经被删除。

1.3 关闭索引

在有些场景下,某个索引暂时不使用,但是后期可能又会使用,这里的使用是指数据写入和数据搜索。这个索引在某一时间段内属于冷数据或者归档数据,这时可以使用索引的关闭功能。索引关闭时,只能通过ES的API或者监控工具看到索引的元数据信息,但是此时该索引不能写入和搜索数据,待该索引被打开后,才能写入和搜索数据。先把索引hotel关闭,请求形式如下:

POST /hotel/_close

此时可以尝试进行数据写入:

POST /hotel/_doc/002 
{  
  "title":"阳光夏日酒店", 
  "city":"北京", 
  "price":788.00 
} "

ES返回信息如下:

{ 
  "error" : { 
    "root_cause" : [ 
      { 
        "type" : "index_closed_exception", //提示异常类型为索引已经关闭 
        "reason" : "closed", 
        "index_uuid" : "TIYkM2N5SCKlnmCOXuJPmg", 
        "index" : "hotel"                  //当前索引名称 
      } 
    ], 
    "type" : "index_closed_exception", 
    "reason" : "closed", 
    "index_uuid" : "TIYkM2N5SCKlnmCOXuJPmg", 
    "index" : "hotel" 
  }, 
  "status" : 400 
}

根据上面的信息可知,索引关闭时写入数据将会报错。下面可以尝试进行数据搜索:

GET /hotel/_search 
{ 
  "query": { 
   "match": {                      //使用match搜索 
      "title": "再来" 
    } 
  } 
}

ES返回的信息和进行写入请求时的返回信息是一样的,印证了索引在关闭时不能提供搜索服务的规定。

1.4 打开索引

索引关闭后,需要开启读写服务时可以将其设置为打开状态。下面的示例是把处于关闭状态的hotel索引设置为打开状态。

POST /hotel/_open 

1.5 索引别名

顾名思义,别名是指给一个或者多个索引定义另外一个名称,使索引别名和索引之间可以建立某种逻辑关系。

可以用别名表示别名和索引之间的包含关系。例如,我们建立了1月、2月、3月的用户入住酒店的日志索引,假设当前日期是4月1日,需要搜索过去的3个月的日志索引,如果分别去3个索引中进行搜索,这种编码方案比较低效。此时可以创建一个别名last_three_month,设置前面的3个索引的别名为last_three_month,然后在last_three_month中进行搜索即可。如图3.1所示,last_three_month包含january_log、february_log和march_log3个索引,用户请求在last_three_month中进行搜索时,ES会在上述3个索引中进行搜索。
在这里插入图片描述
下面进行演示,首先依次建立january_log、february_log和march_log3个索引。创建索引january_log的DSL如下:

PUT /january_log 
{ 
    "mappings":{ 
        "properties":{ 
            "uid":{              //用户ID字段 
                "type":"keyword" 
            }, 
            "hotel_id":{         //酒店ID字段 
                "type":"keyword" 
            }, 
            "check_in_date":{     //入住日期字段 
                "type":"keyword" 
            } 
        } 
    } 
}

因为这3个索引除了索引名称不一样,其他的参数都是一样的,所以创建索引february_log和march_log的DSL不再赘述。
下面分别在3个索引中写入同一用户在不同月份的入住记录。
在索引january_log中写入记录的DSL如下:

POST /january_log/_doc/001 
{                                 //写入的文档数据    
  "uid":"001", 
  "hotel_id":"92772", 
  "check_in_date":"2021-01-05" 
}

在索引february_log中写入记录的DSL如下:

POST /february_log/_doc/001 
{                                //写入的文档数据 
  "uid":"001", 
  "hotel_id":"33224", 
  "check_in_date":"2021-02-23" 
}

在索引march_log中写入记录的DSL如下:

POST /march_log/_doc/001 
{                               //写入的文档数据 
  "uid":"001", 
  "hotel_id":"92772", 
  "check_in_date":"2021-03-28" 
}

现在建立别名last_three_month,设置上面3个索引的别名为last_three_month,请求的DSL如下:

POST /_aliases 
{ 
  "actions": [ 
    { 
      "add": {   //为索引january_log建立别名last_three_month 
        "index": "january_log", 
        "alias": "last_three_month" 
      } 
    }, 
    { 
      "add": {   //为索引february_log建立别名last_three_month 
        "index": "february_log", 
        "alias": "last_three_month" 
      } 
    }, 
    { 
      "add": {   //为索引march_log建立别名last_three_month 
        "index": "march_log", 
        "alias": "last_three_month" 
      } 
    } 
  ] 
}

此时,请求在索引last_three_month中搜索uid为001的用户的入住记录,搜索的DSL如下:

GET /last_three_month/_search 
{ 
  "query": { 
    "term": {        //搜索uid为001的文档 
      "uid": "001" 
    } 
  } 
}

搜索结果返回的数据如下:

{"hits" : {"hits" : [ 
      {               //索引february_log中命中的文档信息 
        "_index" : "february_log",
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : 0.2876821, 
        "_source" : { 
          "uid" : "001", 
          "hotel_id" : "33224", 
          "check_in_date" : "2021-02-23" 
        } 
      }, 
     {                //索引january_log中命中的文档信息 
        "_index" : "january_log", 
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : 0.2876821, 
        "_source" : { 
          "uid" : "001", 
          "hotel_id" : "92772", 
          "check_in_date" : "2021-01-05" 
        } 
      }, 
     {                //索引march_log中命中的文档信息 
        "_index" : "march_log", 
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : 0.2876821, 
        "_source" : { 
          "uid" : "001", 
          "hotel_id" : "92772", 
          "check_in_date" : "2021-03-28" 
        } 
      } 
    ] 
  } 
}

由上面的结果可知,当请求搜索last_three_month的数据时,ES将请求转发到了january_log、february_log和march_log3个索引中。

需要指出的是,在默认情况下,当一个别名只指向一个索引时,写入数据的请求可以指向这个别名,如果这个别名指向多个索引(就像上面的例子),则写入数据的请求是不可以指向这个别名的。例如,向last_three_month中写入一条数据:

POST /last_three_month/_doc/002 
{                               //向别名索引last_three_month中写入数据 
  "uid":"002", 
  "hotel_id":"92772", 
  "check_in_date":"2021-01-28" 
}

ES返回的报错信息如下:

{ 
  "error" : { 
    "root_cause" : [ 
     {                                //无法写入数据的报错信息 
        "type" : "illegal_argument_exception", 
        "reason" : "no write index is defined for alias [last_three_month].  
The write index may be explicitly disabled using is_write_index=false or  
the alias points to multiple indices without one being designated as a write  
index" 
      } 
    ], 
    //无法写入数据的报错信息 
    "type" : "illegal_argument_exception", 
    "reason" : "no write index is defined for alias [last_three_month]. The  
write index may be explicitly disabled using is_write_index=false or the  
alias points to multiple indices without one being designated as a write  
index" 
  }, 
 "status" : 400       //返回状态码 
}

根据上面的报错信息可知,在默认情况下,ES不能确定向last_three_month写入数据时的转发对象。这种情况需要在别名设置时,将目标索引的is_write_index属性值设置为true来指定该索引可用于执行数据写入操作。例如设置january_log为数据写入转发对象,对应的DSL如下:

POST /_aliases 
{ 
  "actions": [ 
    { 
      "add": {  //设置january_log为索引别名last_three_month的数据写入转发对象 
        "index": "january_log", 
        "alias": "last_three_month", 
         "is_write_index":true 
      } 
    } 
  ] 
}

此时可以再向last_three_month中写入上面的数据,不再赘述。
ES返回的结果如下:

{ 
  "_index" : "january_log",   //实际被写入的索引名称 
  "_type" : "_doc", 
 "_id" : "002",               //文档ID 
 "_version" : 1,              //文档版本 
 "result" : "created",        //写入成功标识 
  "_shards" : { 
    "total" : 2, 
   "successful" : 1,          //写入成功状态码 
    "failed" : 0 
  }, 
  "_seq_no" : 1, 
  "_primary_term" : 1 
}

根据上面的结果可知,将索引别名last_three_month的数据写入转发对象设置为索引january_log之后,再向last_three_month发起写入数据的请求时,ES会将该请求转发到索引january_log中。

引入别名之后,还可以用别名表示索引之间的替代关系。这种关系一般是在某个索引被创建后,有些参数是不能更改的(如主分片的个数),但随着业务发展,索引中的数据增多,需要更改索引参数进行优化。我们需要平滑地解决该问题,既要更改索引的设置,又不能改变索引名称,这时就可以使用索引别名。

假设一个酒店的搜索别名设置为hotel,初期创建索引hotel_1时,主分片个数设置为5,然后设置hotel_1的别名为hotel。此时客户端使用索引别名hotel进行搜索请求,该请求会转发到索引hotel_1中。假设此时酒店索引中的新增数据急剧增长,索引分片需要扩展,需要将其扩展成为10个分片的索引。但是一个索引在创建后,主分片个数已经不能更改,因此只能考虑使用索引替换来完成索引的扩展。这时可以创建一个索引hotel_2,除了将其主分片个数设置为10外,其他设置与hotel_1相同。当hotel_2的索引数据准备好后,删除hotel_1的别名hotel,同时,置hotel_2的别名为hotel。此时客户端不用进行任何改动,继续使用hotel进行搜索请求时,该请求会转发给索引hotel_2。如果服务稳定,最后将hotel_1删除即可。此时借助别名就完成了一次索引替换工作。

如图3.2所示,在左图中,hotel索引别名暂时指向hotel_1,hotel_2做好了数据准备;在右图中,hotel索引别名指向hotel_2,完成了索引的扩展切换。
在这里插入图片描述
下面进行演示。首先建立索引hotel_1,设置其主分片个数为5,其他信息与创建索引时保持一致。

PUT /hotel_1 
{  
    "settings": { 
        "number_of_shards" : 5,    //设置主分片个数为5 
        "number_of_replicas" : 2    //设置副分片个数为2 
    }, 
     "mappings":{} 
}

在数据写入端向hotel_1写入搜索数据,请求的DSL如下:

POST /hotel_1/_doc/001 
{                                                //写入的文档数据  
   "title":"好再来酒店", 
   "city":"青岛",  
   "price":578.23 
}

建立别名hotel,请求的DSL如下:

POST /_aliases 
{ 
  "actions": [ 
    { 
      "add": {                           //为索引hotel_1建立别名hotel 
        "index": "hotel_1", 
        "alias": "hotel" 
      } 
    } 
  ] 
}

在搜索请求端使用hotel进行搜索,假设在title字段中搜索“再来”,搜索的DSL如下:

GET /hotel/_search 
{ 
 "query": {                         //在hotel中搜索 
    "match": { 
      "title": "再来" 
    } 
  } 
}

ES返回的数据如下:

{"hits" : {"hits" : [                          //命中的文档信息 
      { 
        "_index" : "hotel_1",           //索引hotel_1上命中的文档 
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : 0.5753642, 
        "_source" : {                   //具体的文档信息 
          "title" : "好再来酒店", 
          "city" : "青岛", 
          "price" : 578.23 
        } 
      } 
    ] 
  } 
}

通过搜索结果可以看出,因为只有索引hotel_1的别名为hotel,所以向索引别名hotel发起搜索请求时ES会将搜索请求全部转发给索引hotel_1。

假设过一段时间后酒店索引的分片需要扩展。通过变更索引的方式可以完成扩展。建立索引hotel_2,并设置主分片个数为10,设置副分片个数为2,请求的DSL如下:

PUT /hotel_2 
{  
    "settings": { 
        "number_of_shards" : 10,  //设置主分片数为10 
        "number_of_replicas" : 2  //设置副分片数为2 
    }, 
     "mappings":{} 
}

在数据写入端向索引hotel_2中写入搜索数据,请求的DSL如下:

POST /hotel_2/_doc/001 
{                                          //向索引hotel_2中写入的文档数据  
   "title":"好再来酒店", 
   "city":"青岛", 
   "price":578.23 
}

此时hotel_2中的数据已经准备完毕,现在变更别名设置,删除hotel_1的索引别名,设置索引hotel_2的别名为hotel2,请求的DSL如下:

POST /_aliases 
{ 
  "actions": [ 
    { 
      "remove": {     //删除索引hotel_1的别名hotel 
        "index": "hotel_1", 
        "alias": "hotel" 
      } 
    }, 
    { 
     "add": {        //增加索引hotel_2的别名hotel 
        "index": "hotel_2", 
        "alias": "hotel" 
      } 
    } 
  ] 
}

再执行前面的搜索,返回结果如下:

{"hits" : {"hits" : [ 
     {                                //命中索引hotel_2中的文档 
        "_index" : "hotel_2", 
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : 0.5753642, 
        "_source" : { 
          "title" : "好再来酒店", 
          "city" : "青岛", 
          "price" : 578.23 
        } 
      } 
    ] 
  } 
}

通过搜索结果可以看出,请求hotel索引进行搜索时,搜索已经从转发给hotel_1变更为转发给hotel_2。因此,索引别名在这种需要变更索引的情况下,搜索端不需要任何变更即可完成切换,这在实际的生产环境中是非常方便的。

2 映射操作

在使用数据之前,需要构建数据的组织结构。这种组织结构在关系型数据库中叫作表结构,在ES中叫作映射。作为无模式搜索引擎,ES可以在数据写入时猜测数据类型,从而自动创建映射。但有时ES创建的映射中的数据类型和目标类型可能不一致。当需要严格控制数据类型时,还是需要用户手动创建映射。

本节首先介绍映射的创建、查看和修改操作,然后详细介绍ES中的基本数据类型和复杂的数据类型,并且会对常用的类型用法进行示范,最后介绍映射的常用参数和动态映射的使用。

2.1 查看映射

在ES中写入文档请求的类型是GET,其请求形式如下:

GET /${index_name}/_mapping  

上面的index_name就是索引名称。查看索引hotel的mappings,请求的DSL如下:

GET /hotel/_mapping  

ES的返回结果如下:

{ 
  "hotel" : { 
    "mappings" : { 
      "properties" : { 
        "city" : {      //定义city字段类型为keyword 
          "type" : "keyword" 
        }, 
        "price" : {     //定义price字段类型为double 
          "type" : "double" 
        }, 
        "title" : {     //定义title字段类型为text 
          "type" : "text" 
        } 
      } 
    } 
  } 
} 

通过返回信息可见,查看索引hotel的mappings时,返回的信息和建立该索引时的信息是一致的。

2.2 扩展映射

可能有的读者看到标题时会有疑问:映射不能修改吗?为什么是扩展呢?答案是,映射中的字段类型是不可以修改的,但是字段可以扩展。最常见的扩展方式是增加字段和为object(对象)类型的数据新增属性。下面的DSL示例为扩展hotel索引,并增加tag字段。

POST /hotel/_mapping 
{ 
  "properties": { 
    "tag": {                  //索引中新增字段tag,类型为keyword 
      "type": "keyword" 
    } 
  } 
}

查看索引hotel的mappings,返回结果如下:

{ 
  "hotel" : { 
    "mappings" : { 
      "properties" : { 
        "city" : {      //原有的city字段,类型为keyword 
          "type" : "keyword" 
        }, 
        "price" : {     //原有的price字段,类型为double 
          "type" : "double" 
        }, 
        "tag" : {       //新增tag字段,类型为keyword 
          "type" : "keyword" 
        }, 
        "title" : {     //原有的title字段,类型为text 
          "type" : "text" 
        } 
      } 
    } 
  } 
}

由返回结果可知,tag字段已经被添加到索引hotel中。

2.3 基本的数据类型

1 keyword类型

keyword类型是不进行切分的字符串类型。这里的“不进行切分”指的是:在索引时,对keyword类型的数据不进行切分,直接构建倒排索引;在搜索时,对该类型的查询字符串不进行切分后的部分匹配。keyword类型数据一般用于对文档的过滤、排序和聚合
在现实场景中,keyword经常用于描述姓名、产品类型、用户ID、URL和状态码等。keyword类型数据一般用于比较字符串是否相等,不对数据进行部分匹配,因此一般查询这种类型的数据时使用term查询。

例如,建立一个人名索引,可以设定姓名字段为keyword字段:

PUT /user 
{ 
  "mappings": { 
    "properties": { 
      "user_name":{                 //定义user_name字段类型为keyword 
        "type": "keyword" 
      } 
    } 
  } 
}

现在写入一条数据,请求的DSL如下:

POST /user/_doc/001 
{                                     //写入数据 
  "user_name":"张三" 
}

下面查询刚刚写入的数据,请求的DSL如下:

GET /user/_search 
{ 
  "query": { 
   "term": {                         //term查询 
      "user_name": { 
        "value": "张三" 
      } 
    }  
  } 
}

返回的结果信息如下:

{"hits" : [ 
     {                                 //返回命中的文档 
        "_index" : "user", 
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : 0.2876821, 
        "_source" : { 
          "user_name" : "张三" 
        } 
      } 
    ] 
  } 
}

由搜索结果可以看出,使用term进行全字符串匹配“张三”可以搜索到命中文档。

下面的DSL使用match搜索姓名中带有“张”的记录:

GET /user/_search 
{ 
  "query": { 
   "match": {                       //使用match搜索 
      "user_name": "张" 
    } 
  }  
}

返回结果如下:

{"hits" : { 
   "total" : {                     //没有命中文档 
      "value" : 0, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ]                 //命中文档的集合为空 
  } 
} 

由搜索结果可见,对keyword类型使用match搜索进行匹配是不会命中文档的。

2 text类型

text类型是可进行切分的字符串类型。这里的“可切分”指的是:在索引时,可按照相应的切词算法对文本内容进行切分,然后构建倒排索引;在搜索时,对该类型的查询字符串按照用户的切词算法进行切分,然后对切分后的部分匹配打分。

例如,一个酒店搜索项目,我们希望可以根据酒店名称即title字段进行模糊匹配,因此可以设定title字段为text字段,建立酒店索引的DSL如下:

PUT /hotel 
{ 
    "mappings":{ 
        "properties":{ 
            "title":{                          //定义title字段类型为text 
                "type":"text" 
            }} 
    } 
}

现在写入一条数据:

POST /hotel/_doc/001 
{   //写入数据  
   "title":"文雅酒店" 
}

下面先按照普通的term进行搜索,观察能否搜索到刚刚写入的文档,请求的DSL如下:

GET /hotel/_search 
{ 
  "query": { 
   "term": {                     //使用term搜索 
      "title": { 
        "value": "文雅酒店" 
      } 
    } 
  } 
}

返回结果如下:

{"hits" : { 
   "total" : {                //使用term搜索text类型数据时没有命中文档 
      "value" : 0, 
      "relation" : "eq" 
    }, 
    "max_score" : null, 
    "hits" : [ ]              //命中的文档集合为空 
  } 
}

根据返回结果可知,上面的请求并没有搜索到文档。term搜索用于搜索值和文档对应的字段是否完全相等,而对于text类型的数据,在建立索引时ES已经进行了切分并建立了倒排索引,因此使用term没有搜索到数据。一般情况下,搜索text类型的数据时应使用match搜索。关于match搜索的具体使用,后面的章节会详细介绍,本节仅进行简单的使用:

GET /hotel/_search 
{ 
  "query": { 
   "match": {                      //使用match搜索 
      "title": "文雅" 
    } 
  } 
}

返回结果如下:

{"hits" : { 
    "total" : { 
      "value" : 1,               //有一个文档被命中 
      "relation" : "eq" 
    }, 
    "max_score" : 0.5753642, 
   "hits" : [                    //命中的文档数据 
      { 
        "_index" : "hotel", 
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : 0.5753642, 
        "_source" : { 
          "title" : "文雅酒店" 
        } 
      } 
    ] 
  } 
}

3 数值类型

ES支持的数值类型有long、integer、short、byte、double、float、half_float、scaled_float和unsigned_long等。各类型所表达的数值范围可以参考官方文档,网址为https://www.elastic.co/guide/en/elasticsearch/reference/current/number.html。为节约存储空间并提升搜索和索引的效率,在实际应用中,在满足需求的情况下应尽可能选择范围小的数据类型。比如,年龄字段的取值最大值不会超过200,因此选择byte类型即可。数值类型的数据也可用于对文档进行过滤、排序和聚合。以酒店搜索为例,酒店的索引除了包含酒店名称和城市之外,还需要定义价格、星级和评论数等,创建索引的DSL如下:

PUT /hotel 
{ 
    "mappings":{ 
        "properties":{ 
            "title":{ 
                "type":"text" 
            }, 
            "city":{ 
                "type":"keyword" 
            }, 
            "price":{               //定义价格字段,类型为double 
                "type":"double" 
            }, 
            "star":{                //定义星级字段,类型为byte 
                "type":"byte" 
            }, 
            "comment_count":{       //定义评论数字段,类型为integer 
                "type":"integer" 
            } 
        } 
    } 
}

对于数值型数据,一般使用term搜索或者范围搜索。例如,搜索价格为350~400(包含350和400)元的酒店,搜索的DSL如下:

GET /hotel/_search 
{ 
  "query": { 
   "range": {                     //定义range查询 
      "price": { 
        "gte": 350, 
        "lte": 400 
      } 
    } 
  } 
}

4 布尔类型

布尔类型使用boolean定义,用于业务中的二值表示,如商品是否售罄,房屋是否已租,酒店房间是否满房等。写入或者查询该类型的数据时,其值可以使用true和false,或者使用字符串形式的"true"和"false"。下面的DSL定义索引中“是否满房”的字段为布尔类型:

PUT /hotel 
{ 
    "mappings":{ 
        "properties":{ 
            "title":{ 
                "type":"text" 
            }, 
            "city":{ 
                "type":"keyword" 
            }, 
            "price":{ 
                "type":"double" 
            }, 
            "full_room":{             //定义是否满房的字段,类型为boolean 
              "type": "boolean" 
            } 
        } 
    } 
}

下面的DSL将查询满房的酒店:

GET /hotel/_search 
{ 
  "query": { 
    "term": { 
      "full_room": {              //使用term查询boolean类型的数据 
        "value": "true" 
      } 
    } 
  } 
}

5 日期类型

在ES中,日期类型的名称为date。ES中存储的日期是标准的UTC格式。下面定义索引hotel,该索引有一个create_time字段,现在把它定义成date类型。定义date类型请求的DSL如下:

PUT /hotel 
{ 
    "mappings":{ 
        "properties":{ 
            "title":{ 
                "type":"text" 
            }, 
            "city":{ 
                "type":"keyword" 
            }, 
            "price":{ 
                "type":"double" 
            }, 
            "create_time":{              //定义create_time字段,类型为date 
                "type":"date" 
            } 
        } 
    } 
}

一般使用如下形式表示日期类型数据:

  • 格式化的日期字符串。
  • 毫秒级的长整型,表示从1970年1月1日0点到现在的毫秒数。
  • 秒级别的整型,表示从1970年1月1日0点到现在的秒数。

日期类型的默认格式为strict_date_optional_time||epoch_millis。其中,strict_date_optional_time的含义是严格的时间类型,支持yyyy-MM-dd、yyyyMMdd、yyyyMMddHHmmss、yyyy-MM-ddTHH:mm:ss、yyyy-MM-ddTHH:mm:ss.SSS和yyyy-MM-ddTHH:mm:ss.SSSZ等格式,epoch_millis的含义是从1970年1月1日0点到现在的毫秒数。

下面写入索引的文档中有一个create_time字段是日期格式的字符串,请求的DSL如下:

POST /hotel/_doc/001 
{                                       //写入数据 
  "title":"好再来酒店",  
  "city":"青岛", 
  "price":578.23, 
  "create_time":"20210115" 
}

搜索日期型数据时,一般使用ranges查询。例如,搜索创建日期为2015年的酒店,请求的DSL如下:

GET /hotel/_search 
{ 
  "query": { 
   "range": {                          //使用range搜索日期型数据 
      "create_time": { 
        "gte": "20150101", 
        "lt": "20160101" 
      } 
    } 
  } 
}

日期类型默认不支持yyyy-MM-dd HH:mm:ss格式,如果经常使用这种格式,可以在索引的mapping中设置日期字段的format属性为自定义格式。下面的示例将设置create_time字段的格式为yyyy-MM-dd HH:mm:ss:

PUT /hotel 
{ 
    "mappings":{ 
        "properties":{ 
            "title":{ 
                "type":"text" 
            }, 
            "city":{ 
                "type":"keyword" 
            }, 
            "price":{ 
                "type":"double" 
            }, 
            "create_time":{                     //指定日期型字段的格式 
                "type":"date",  
                "format": "yyyy-MM-dd HH:mm:ss" 
            } 
        } 
    } 
}

此时如果再写入以前的数据,系统将报错。报错信息如下:

{ 
  "error" : { 
    "root_cause" : [ 
     {                                               //日期格式解析异常 
        "type" : "mapper_parsing_exception", 
        "reason" : "failed to parse field [create_time] of type [date] in
document with id '001'. Preview of field's value: '20210115'" 
      } 
    ], 
    "type" : "mapper_parsing_exception", 
    "reason" : "failed to parse field [create_time] of type [date] in document
with id '001'. Preview of field's value: '20210115'", //日期格式解析异常 
    "caused_by" : { 
      "type" : "illegal_argument_exception", 
      "reason" : "failed to parse date field [20210115] with format [yyyy-
MM-dd HH:mm:ss]", 
      "caused_by" : { 
        "type" : "date_time_parse_exception", 
        "reason" : "Text '20210115' could not be parsed at index 0" 
      } 
    } 
  }, 
  "status" : 400 
}

根据错误信息可知,错误的原因是写入的数据格式和定义的数据格式不同。此时需要写入的格式为yyyy-MM-dd HH:mm:ss的文档,请求的DSL如下:

POST /hotel/_doc/001 
{                                       //写入数据 
  "title":"好再来酒店",  
  "city":"青岛", 
  "price":578.23, 
  "create_time":"2021-01-15 01:23:30"  //写入符合自定义格式的日期数据 
}

2.4 复杂的数据类型

1 数组类型

ES数组没有定义方式,其使用方式是开箱即用的,即无须事先声明,在写入时把数据用中括号[]括起来,由ES对该字段完成定义。当然,如果事先已经定义了字段类型,在写数据时以数组形式写入,ES也会将该类型转为数组。例如,为hotel索引增加一个标签字段,名称为tag,请求的DSL如下:

PUT /hotel/_mapping 
{ 
  "properties": { 
    "tag": {                      //增加tag字段,类型为keyword 
      "type": "keyword" 
    } 
  } 
}

查看一下索引hotel的mapping:

{ 
  "hotel" : { 
    "mappings" : { 
      "properties" : { 
        "city" : {                //原有的city字段,类型为keyword 
          "type" : "keyword" 
        }, 
        "price" : {               //原有的price字段,类型为double 
          "type" : "double" 
        }, 
        "tag" : {                 //增加tag字段,类型为keyword 
          "type" : "keyword" 
        }, 
        "title" : {               //原有的title字段,类型为text 
          "type" : "text" 
        } 
      } 
    } 
  } 
}

通过返回的mapping信息来看,新增的tag字段与普通的keyword类型字段没什么区别,现在写入一条数据:

POST /hotel/_doc/001 
{                            //写入数据  
  "title":"好再来酒店", 
  "city":"青岛", 
  "price":578.23, 
  "tag":["有车位","免费WIFI"] //写入字符串数组数据 
}

查看一下写入的数据,ES返回的信息如下:

{"_source" : { 
    "title" : "好再来酒店", 
    "city" : "青岛", 
    "price" : 578.23, 
   "tag" : [                 //tag字段自动转换为字符串数组类型 
      "有车位", 
      "免费WIFI" 
    ] 
  } 
}

通过以上信息可以看到,写入的数据的tag字段已经是数组类型了。那么,数组类型的数据如何搜索呢?

数组类型的字段适用于元素类型的搜索方式,也就是说,数组元素适用于什么搜索,数组字段就适用于什么搜索。例如,在上面的示例中,数组元素类型是keyword,该类型可以适用于term搜索,则tag字段也可以适用于term搜索,搜索的DSL如下:

GET /hotel/_search 
{ 
  "query": { 
   "term": {             //使用term搜索数组类型的数据 
      "tag": { 
        "value": "有车位" 
      } 
    } 
  } 
}

ES中的空数组可以作为missing field,即没有值的字段,下面的DSL将插入一条tag为空的数组:

POST /hotel/_doc/002 
{     
    "title":"环球酒店", 
    "city":"青岛", 
    "price":530.00, 
    "tag":[]              //写入空数组 
} 

2 对象类型

在实际业务中,一个文档需要包含其他内部对象。例如,在酒店搜索需求中,用户希望酒店信息中包含评论数据。评论数据分为好评数量和差评数量。为了支持这种业务,在ES中可以使用对象类型。和数组类型一样,对象类型也不用事先定义,在写入文档的时候ES会自动识别并转换为对象类型。

下面将在hotel索引中添加一条记录,请求的DSL如下:

 
POST /hotel/_doc/001 
{                                 //写入数据 
  "title": "好再来酒店", 
  "city": "青岛", 
  "price": 578.23, 
      "comment_info": {           //评论数据 
      "properties": { 
        "favourable_comment":199, //好评数据 
        "negative_comment": 68    //差评数据 
      } 
    } 
}   

执行以上DSL后,索引hotel增加了一个字段comment_info,它有两个属性,分别是favourable_comment和negative_comment,二者的类型都是long。下面查看mapping进行验证:

{ 
  "hotel" : { 
    "mappings" : { 
      "properties" : { 
        "city" : { 
          "type" : "keyword" 
        }, 
        "comment_info" : {                 //评论数据 
          "properties" : { 
            "properties" : { 
              "properties" : { 
                "favourable_comment" : {   //好评数据,类型为long 
                  "type" : "long" 
                }, 
                "negative_comment" : {     //差评数据,类型为long 
                  "type" : "long" 
                } 
              } 
            } 
          } 
        }, 
        "create_time" : { 
          "type" : "date", 
          "format" : "yyyy-MM-dd HH" 
        }, 
        "price" : { 
          "type" : "double" 
        }, 
        "title" : { 
          "type" : "text" 
        } 
      } 
    } 
  } 
}   

根据对象类型中的属性进行搜索,可以直接用“.”操作符进行指向。例如,搜索hotel索引中好评数大于200的文档,请求的DSL如下:

GET /hotel/_search 
{ 
  "query": { 
   "range": {                                 //使用range搜索对象类型数据 
      "comment_info.properties.favourable_comment": { 
        "gte": 209 
      } 
    } 
  } 
}  

当然,对象内部还可以包含对象。例如,评论信息字段comment_info可以增加前3条好评数据,请求的DSL如下:

POST /hotel/_doc/001 
{                                    //写入数据 
  "title": "好再来酒店", 
  "city": "青岛", 
  "price": 578.23,
  "comment_info": { 
    "properties": { 
      "favourable_comment": 199, 
      "negative_comment": 68, 
      "top3_favourable_comment": {   //新增字段 
       "top1": {                     //增加的第一条评论数据 
          "content": "干净整洁的一家酒店", 
          "score": 87 
        }, 
       "top2": {                     //增加的第二条评论数据 
          "content": "服务周到,停车方便", 
          "score": 89 
        }, 
        "top3":  {                  //增加的第三条评论数据 
          "content": "闹中取静,环境优美", 
          "score": 90 
        } 
      } 
    } 
  } 
}    

以上请求,对文档的comment_info字段增加了前3条评论的内容和评分数据。

3 地理类型

在移动互联网时代,用户借助移动设备产生的消费也越来越多。例如,用户需要根据某个地理位置来搜索酒店,此时可以把酒店的经纬度数据设置为地理数据类型。该类型的定义需要在mapping中指定目标字段的数据类型为geo_point类型,示例如下:

PUT /hotel 
{ 
  "mappings": { 
    "properties": { 
      "title": { 
        "type": "text" 
      }, 
      "city": { 
        "type": "keyword" 
      }, 
      "price": { 
        "type": "double" 
      }, 
      "create_time": { 
        "type": "date" 
      }, 
      "location": {                 //定义字段location,类型为geo_point 
        "type": "geo_point" 
      } 
    } 
  } 
}  

其中,location字段定义为地理类型,现在向索引中写入一条酒店文档,DSL如下:

POST /hotel/_doc/001 
{ 
  "title": "文雅酒店", 
  "city": "北京", 
  "price": 556, 
  "create_time": "2021-01-15", 
 "location": {                      //写入geo_point类型数据,lat为纬度,lon为经度 
    "lat": 40.012134, 
    "lon": 116.497553 
  } 
}  

2.5 动态映射

当字段没有定义时,ES可以根据写入的数据自动定义该字段的类型,这种机制叫作动态映射。其实,在前面的章节中我们已经用到了ES的动态映射机制。在介绍数组类型和对象类型时提到,这两种类型都不需要用户提前定义,ES将根据写入的数据自动创建mapping中对应的字段并指定类型。对于基本类型,如果字段没有定义,ES在将数据存储到索引时会进行自动映射,如表3.1所示为自动映射时的JSON类型和索引数据类型的对应关系。
在这里插入图片描述
在一般情况下,如果使用基本类型数据,最好先把数据类型定义好,因为ES的动态映射生成的字段类型可能会与用户的预期有差别。例如,写入数据时,由于ES对于未定义的字段没有类型约束,如果同一字段的数据形式不同(有的是字符型,有的是数值型),则ES动态映射生成的字段类型和用户的预期可能会有偏差。提前定义好数据类型并将索引创建语句纳入SVN或Git管理范围是良好的编程习惯,同时还能增强项目代码的连贯性和可读性。

2.6 多字段

针对同一个字段,有时需要不同的数据类型,这通常表现在为了不同的目的以不同的方式索引相同的字段。例如,在订单搜索系统中,既希望能够按照用户姓名进行搜索,又希望按照姓氏进行排列,可以在mapping定义中将姓名字段先后定义为text类型和keyword类型,其中,keyword类型的字段叫作子字段,这样ES在建立索引时会将姓名字段建立两份索引,即text类型的索引和keyword类型的索引。订单搜索索引的定义如下:

PUT /hotel_order 
{ 
  "mappings": { 
    "properties": { 
      "order_id": {     //定义order_id字段类型为keyword 
        "type": "keyword" 
      }, 
      "user_id": {     //定义user_id字段类型为keyword 
        "type": "keyword" 
      }, 
      "user_name": {    //定义user_name字段类型为text 
        "type": "text", 
        "fields": {     //定义user_name多字段 
          //定义user_name字段的子字段user_name_keyword,并定义其类型为keyword 
          "user_name_keyword": { 
            "type": "keyword" 
          } 
        } 
      }, 
      "hotel_id": {     //定义hotel_id字段类型为keyword 
        "type": "keyword" 
      } 
    } 
  } 
} 

可以看出,正常定义user_name字段之后,使用fields定义其子字段的定义方式和普通字段的定义方式相同。
为方便演示,写入如下数据:

POST /_bulk
{"index":{"_index":"hotel_order","_id":"001"}}
{"order_id":"001","user_id":"user_00x","user_name":"Michael Jordan","hotel_id":"h001"}
{"index":{"_index":"hotel_order","_id":"002"}}
{"order_id":"002","user_id":"user_00a","user_name":"Stephen Jordan","hotel_id":"h0500"}
{"index":{"_index":"hotel_order","_id":"003"}}
{"order_id":"003","user_id":"user_30e","user_name":"Tim Jordan","hotel_id":"h0520"}
{"index":{"_index":"hotel_order","_id":"004"}}
{"order_id":"004","user_id":"user_430","user_name":"Kobe Jordan","hotel_id":"h0600"} 

可以在普通搜索中使用user_name字段,DSL如下:

GET /hotel_order/_search 
{ 
  "query": { 
    "match": {     //match搜索使用text类型的字段 
      "user_name": "Jordan" 
    } 
  }, 
  "sort": {      //排序使用子字段 
    "user_name.user_name_keyword": "asc" 
  } 
}  

搜索结果如下:

{"hits" : {"max_score" : null, 
   "hits" : [          //命中文档列表 
      {"_id" : "004", 
        "_score" : null, 
        "_source" : {"user_name" : "Kobe Jordan" 
        }, 
        "sort" : [     //按照姓名全称排序 
          "Kobe Jordan" 
        ] 
      }, 
      { 
        "_index" : "hotel_order", 
        "_type" : "_doc", 
        "_id" : "001", 
        "_score" : null, 
        "_source" : {"user_name" : "Michael Jordan" 
        }, 
        "sort" : [     //按照姓名全称排序 
          "Michael Jordan" 
        ] 
      }, 
      { 
        "_index" : "hotel_order", 
        "_type" : "_doc", 
        "_id" : "002", 
        "_score" : null, 
        "_source" : {"user_name" : "Stephen Jordan" 
        }, 
        "sort" : [     //按照姓名全称排序 
          "Stephen Jordan" 
        ] 
      },] 
  } 
} 

根据以上结果可知,搜索Jordan之后,添加的4个文档都命中并且排序时是按照用户姓名的全称进行排序的。

3 文档操作

使用ES构建搜索引擎时需要经常对文档进行操作。除了简单的单条文档操作,有时还需要进行批量操作。本节将介绍文档的各种日常操作。此外,在生产环境中,对文档的批量操作一般需要借助编程语言来完成,因此在介绍DSL的同时本节还将演示Java客户端的使用。

3.1 单条写入文档

在ES中写入文档请求的类型是POST,其请求形式如下:

POST /${index_name}/_doc/${_id} 
{   //写入的文档数据 } 

上面的_id就是ES中的文档_id,这种请求方式是用户直接定义_id值,不使用ES生成的_id。请求的数据体即为写入的文档数据,格式是JSON形式。例如,在目标索引中写入下面数据:

POST /hotel/_doc/001 
{ //写入的文档数据在URL中已经指定_id,因此在数据中需要指定_id 
  "title":"好再来酒店", 
  "city":"青岛", 
  "price":578.23 
}  

ES返回的结果如下:

{ 
 "_index" : "hotel",    //当前文档对应的索引 
  "_type" : "_doc", 
 "_id" : "001",         //文档的_id 
  "_version" : 1, 
  "result" : "created", 
 "_shards" : {          //写入影响的分片信息 
    "total" : 2, 
    "successful" : 1, 
    "failed" : 0 
  }, 
  "_seq_no" : 2, 
  "_primary_term" : 9 
} 

由以上结果可知,向hotel索引中写入文档成功。另外,ES在返回结果中还会显示文档的版本,这里因为文档刚刚建立,所以当前值为1。
当然,用户也可以不指定文档_id,该_id值将由ES自动生成,其请求形式如下:

POST /${index_name}/_doc 
{ //写入的文档数据 } 

例如,写入上面的文档时不指定文档_id,请求的DSL如下:

POST /hotel/_doc 
{ //写入的文档数据,url中不指定_id 
  "title":"好再来酒店", 
  "city":"青岛", 
  "price":578.23 
}  

在Java高级REST客户端中,单条写入文档需要创建IndexRequest对象并设置对应的索引和_id字段名称,执行时调用客户端的index()方法并把IndexRequest对象传入即可。index()方法返回IndexResponse对象,通过该对象可以获取当前请求的索引名称、文档_id和版本等。下面的代码演示了向索引中添加单条文档的方法。

       public void singleIndexDoc(Map<String, Object> dataMap, String  
indexName, String indexId) { 
        IndexRequest indexRequest = new IndexRequest(indexName).id(indexId). 
source(dataMap);         //构建IndexRequest对象并设置对应的索引和_id字段名称 
        try { 
            IndexResponse indexResponse = client.index(indexRequest,  
RequestOptions.DEFAULT); //执行写入 
            //通过IndexResponse获取索引名称 
            String index = indexResponse.getIndex(); 
            String id = indexResponse.getId();//通过IndexResponse获取文档ID 
            //通过IndexResponse获取文档版本 
            Long version = indexResponse.getVersion(); 
            System.out.println("index=" + index + ",id=" + id + ",version="  
+ version ); 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
    } 

3.2 批量写入文档

在ES中批量写入文档请求的类型是POST,其请求形式如下:

POST /_bulk                           //批量请求 
{"index":{"_index":"${index_name}"}}  //指定批量写入的索引 
{}                                  //设定写入的文档内容 
{"index":{"_index":"${index_name}"}} 
{}                                  //设定写入的文档内容 

请求体的第一行表示写入的第一条文档对应的元数据,其中,index_name表示写入的目标索引,第2行表示数据体,第3行表示写入的第二条文档对应的元数据,第4行表示数据体。以此类推,在一次请求里可以写入多条数据。下面将向hotel索引中批量写入两条酒店数据:

POST /_bulk                                               //批量请求 
{"index":{"_index":"hotel"}}   //指定批量请求的索引,不指定文档_id 
{"title": "文雅酒店","city": "北京","price": 556.00}      //写入的数据 
{"index":{"_index":"hotel"}}   //指定批量请求的索引,不指定文档_id 
{"title": "嘉怡假日酒店","city": "北京","price": 337.00}  //写入的数据  

上面的DSL写入索引中的文档_id是ES自动生成的。如果需要指定_id,则应该在元数据中添加_id。例如,下面的DSL将向酒店索引中添加文档_id为001和002两条文档:

POST /_bulk                              //批量请求 
{"index":{"_index":"hotel","_id":"001"}} //指定批量请求的索引 
{"title": "文雅酒店","city": "北京","price": 556.00} //写入的数据,指定文档_id 
{"index":{"_index":"hotel","_id":"002"}} //指定批量请求的索引 
//写入的数据,指定文档_id 
{"title": "嘉怡假日酒店","city": "北京","price": 337.00} 

在实际使用过程中需要批量写入的文档比较多,有时甚至上千条或者上万条,这时如果使用Kibana的请求页面就很不方便了。一般使用Linux系统中的curl命令进行数据的批量写入。curl命令支持上传文件,用户可以将批量写入的JSON数据保存到文件中,然后使用curl命令进行提交。

在ES单机环境下登录服务器,然后执行curl命令将上述两个文档批量写入hotel索引中:

curl  -s  –XPOST  '127.0.0.1:9200/_bulk?preety' --data-binary "@bulk_doc.json"  

其中,bulk_doc.json是文件名称,文件内容如下:

{"index":{"_index":"hotel","_id":"001"}}             //指定批量请求的索引 
{"title": "文雅酒店","city": "北京","price": 556.00} //写入的数据,指定文档_id 
{"index":{"_index":"hotel","_id":"002"}}             //指定批量请求的索引 
//写入的数据,指定文档_id 
{"title": "嘉怡假日酒店","city": "北京","price": 337.00} 

在Java高级REST客户端中,批量写入文档需要创建BulkRequest对象并设置对应的索引名称。对于多条预写入的文档,可构建多个IndexRequest对象并调用BulkRequest.add()方法添加这些IndexRequest对象,执行时调用客户端的bulk()方法并把BulkRequest对象传入即可。bulk()方法返回BulkResponse对象,通过该对象可以获取当前请求的状态。以下代码演示了向索引中批量添加文档的方法:

    public void bulkIndexDoc(String indexName, String docIdKey, List<Map<String, Object>> recordMapList) {
        //构建批量操作BulkRequest对象 
        BulkRequest bulkRequest = new BulkRequest(indexName);
        for (Map<String, Object> dataMap : recordMapList) { //遍历数据 
            //获取主键作为ES索引的主键 
            String docId = dataMap.get(docIdKey).toString();
            IndexRequest indexRequest = new IndexRequest().id(docId).source(dataMap); //构建IndexRequest对象 
            bulkRequest.add(indexRequest); //添加IndexRequest 
        }
        bulkRequest.timeout(TimeValue.timeValueSeconds(5)); //设置超时时间 
        try {
            BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);  //执行批量写入 
            if (bulkResponse.hasFailures()) { //判断执行状态 
                System.out.println("bulk fail,message:" + bulkResponse.buildFailureMessage());
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    @GetMapping("/bulkIndex")
    public void bulkIndex() {
        String indexName = "hotel_oder_1";
        String docIdKey = "id";
        List<Map<String, Object>> list = Lists.newArrayList();
        Map<String, Object> r_map = Maps.newHashMap();
        r_map.put("id", "001");
        r_map.put("title", "如家酒店");
        r_map.put("price", 10000);
        r_map.put("address", "济南");
        list.add(r_map);
        Map<String, Object> bj_map = Maps.newHashMap();
        bj_map.put("id", "002");
        bj_map.put("title", "如家酒店");
        bj_map.put("price", 10000);
        bj_map.put("address", "北京");
        list.add(bj_map);
        esService.bulkIndexDoc(indexName, docIdKey, list);
    }

3.3 更新单条文档

在ES中更新索引的请求类型是POST,其请求形式如下:

POST /${index_name}/_update/${_id} 
{ //需要更新的数据,在URL中指定文档_id }

上面的_id就是将要修改的ES文档中的_id,修改后的字段和值将会填写到大括号中,其格式是JSON形式。例如把_id为001的文档修改成下面的数据:

POST /hotel/_update/001 
{//需要更新的数据,在url中指定文档_id 
 "doc": {                                  //更新后的数据 
    "title": "好再来酒店", 
    "city": "北京", 
    "price": 659.45 
  } 
}

执行上述命令后,ES返回的结果如下:

{ 
 "_index" : "hotel",                //更新的索引名称 
  "_type" : "_doc", 
   "_id" : "001",                   //更新的文档_id 
  "_version" : 2, 
 "result" : "updated",              //更新成功信息 
 "_shards" : {                      //更新影响的分片信息 
    "total" : 2, 
    "successful" : 1, 
    "failed" : 0 
  }, 
  "_seq_no" : 3, 
  "_primary_term" : 9 
}

通过结果可知,已经成功更新文档信息,并且本次修改后文档的版本变为2。下面根据_id搜索文档的命令进行验证:

 GET /hotel/_doc/001
{ 
  "_index" : "hotel", 
  "_type" : "_doc", 
  "_id" : "001", 
  "_version" : 2, 
  "_seq_no" : 3, 
  "_primary_term" : 9, 
  "found" : true, 
  "_source" : { 
    "title" : "好再来酒店", 
    "city" : "北京", 
    "price" : 659.45 
  } 
} 

通过返回的结果可知,文档001的对应字段已经被修改成目标数据,并且文档的版本已经更新为2。

在Java高级REST客户端中,更新单条文档需要创建UpdateRequest对象并设置对应的索引和_id字段名称,执行时,调用客户端的update()方法并把UpdateRequest对象传入即可。index()方法返回UpdateResponse对象,通过该对象可以获取当前请求的索引名称、文档_id和版本等。以下代码演示了向索引中添加单条文档的方法:

   public void singleUpdate(String indexName, String docIdKey, Map<String,  
Object> recordMap) { 
        UpdateRequest updateRequest = new UpdateRequest(indexName, docIdKey); 
        updateRequest.doc(recordMap); 
        try { 
            UpdateResponse updateResponse=client.update(updateRequest,  
RequestOptions.DEFAULT); 
            //通过IndexResponse获取索引名称 
            String index = updateResponse.getIndex(); 
            //通过IndexResponse获取文档ID 
            String id = updateResponse.getId(); 
            //通过IndexResponse获取文档版本 
            Long version = updateResponse.getVersion(); 
            System.out.println("index=" + index + ",id=" + id + ",version=" 
+version); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    }
    @GetMapping("/singleUpdate")
    public void singleUpdate() {
        String indexName = "hotel_oder_1";
        String docIdKey = "001";
        Map<String, Object> recordMap = Maps.newHashMap();
        recordMap.put("title", "_如家酒店");
        esService.singleUpdate(indexName, docIdKey, recordMap);
    }

除了普通的update功能,ES还提供了upsert。upsert即是update和insert的合体字,表示更新/插入数据。如果目标文档存在,则执行更新逻辑;否则执行插入逻辑。以下DSL演示了upsert的应用:

POST /hotel/_update/001 
{ 
  "doc": { 
    "title": "好再来酒店", 
    "city": "北京", 
    "price": 659.45 
  }, 
  "upsert": { 
    "title": "好再来酒店", 
    "city": "北京", 
    "price": 659.45 
  } 
}

执行以上DSL后,如果文档001存在,则执行更新逻辑,将doc内容更新到文档中;否则执行插入逻辑,将upsert的内容写入文档中。

编写Java代码时执行upsert的逻辑和update相似,不同的是构建完成后需调用UpdateRequest.upsert()将插入的对象传入。下面的代码演示了使用upsert向索引中更新/插入数据的方法。

   public void singleUpsert(String index, String docIdKey, Map<String,  
Object> recordMap,Map<String, Object> upRecordMap) { 
        //构建UpdateRequest 
        UpdateRequest updateRequest = new UpdateRequest(index, docIdKey); 
        updateRequest.doc(recordMap);        //设置更新逻辑 
        updateRequest.upsert(upRecordMap);   //设置插入逻辑 
        try { 
            //执行upsert命令 
            client.update(updateRequest, RequestOptions.DEFAULT); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    }

3.4 批量更新文档

与批量写入文档相似,批量更新文档的请求形式如下:

POST /_bulk                //批量请求 
//指定批量更新的索引和文档_id 
{"update":{"_index":"${index_name}","_id":"${_id}"}} 
{"doc":{}#JSON数据}      //设定更新的文档内容 
//指定批量更新的索引和文档_id  
{"update":{"_index":"${index_name}","_id":"${_id}"}} 
{"doc":{}#JSON数据}      //设定更新的文档内容

注意,与批量写入文档不同的是,批量更新文档必须在元数据中填写需要更新的文档_id。下面的DSL将批量更新_id为001和002的文档:

POST /_bulk                               //批量请求 
//指定批量更新的索引和文档_id 
{"update":{"_index":"hotel","_id":"001"}} 
//设定更新的文档内容 
{"doc":{"title": "文雅豪情酒店","city": "北京","price": 556.00}} 
{"update":{"_index":"hotel","_id":"002"}} //指定批量更新的索引和文档_id 
//设定更新的文档内容 
{"doc":{"title": "嘉怡七天酒店","city": "北京","price": 337.00}}

在Java客户端接口中,批量更新文档需要创建BulkRequest对象并设置对应的索引名称。对于多条需要更新的文档,可构建多个UpdateRequest对象并调用BulkRequest.add()方法添加这些UpdateRequest对象,执行时,调用客户端的bulk()方法并把BulkRequest对象传入即可。以下代码演示了向索引中批量更新文档的方法:

    public void bulkUpdate(String index, String docIdKey, List<Map<String,  
Object>> recordMapList) { 
        BulkRequest bulkRequest = new BulkRequest();//构建BulkRequest对象 
        for (Map<String, Object> dataMap : recordMapList) {//遍历数据列表 
            String docId = dataMap.get(docIdKey).toString(); 
            dataMap.remove(docId);            //将ID字段从map中删除 
            //创建UpdateRequest对象 
            bulkRequest.add(new UpdateRequest(index, docId).doc(dataMap)); 
        } 
        try { 
            BulkResponse bulkResponse = client.bulk(bulkRequest, Request 
Options.DEFAULT);                             //执行批量更新 
            if (bulkResponse.hasFailures()) { //判断状态 
                System.out.println("bulk fail,message:" + bulkResponse. 
buildFailureMessage()); 
            } 
        } catch (Exception e) { 
            e.printStackTrace(); 
        } 
    }
    @GetMapping("/bulkUpdate")
    public void bulkUpdate() {
        String indexName = "hotel_order";
        String docIdKey = "id";
        List<Map<String, Object>> recordMapList = Lists.newArrayList();
        Map<String, Object> r_recordMap = Maps.newHashMap();
        r_recordMap.put("user_name", "_如家酒店");
        r_recordMap.put("id", "001");

        Map<String, Object> bj_recordMap = Maps.newHashMap();
        bj_recordMap.put("user_name", "__如家酒店");
        bj_recordMap.put("id", "002");

        recordMapList.add(r_recordMap);
        recordMapList.add(bj_recordMap);
        esService.bulkUpdate(indexName, docIdKey, recordMapList);
    }

3.5 根据条件更新文档

在索引数据的更新操作中,有些场景需要根据某些条件同时更新多条数据,类似于在RDBMS中使用update table table_name set…where…更新一批数据。为了满足这样的需求,ES为用户提供了_update_by_query功能,其请求形式如下:

POST /${index_name}/_update_by_query 
{ 
 "query": {                   //条件更新的查询条件 }, 
 "script": {                 //条件更新的具体更新脚本代码 } 
}

上面的query用于指定更新数据的匹配条件,相当于SQL中的where语句;script用于指定具体的更新操作,相当于SQL的set内容。script的知识点将在后面的章节中进行介绍,这里仅简单应用一下,请求的DSL如下:

POST  /hotel/_update_by_query 
{ 
 "query": {                //更新文档的查询条件:城市为北京的文档 
    "term": { 
      "city": { 
        "value": "北京" 
      } 
    } 
  }, 
 "script": {              //条件更新的更新脚本,将城市改为“上海” 
    "source": "ctx._source['city']='上海'", 
    "lang": "painless" 
  } 
}

执行以上DSL后,ES将先搜索城市为“北京”的酒店,然后把这些酒店的城市字段的值改为“上海”。

在Java高级REST客户端中,执行根据条件更新文档,需要创建UpdateByQueryRequest对象并设置对应的索引名称,类似于DSL中的query子句,通过调用UpdateByQuery Request.setQuery()方法设置查询逻辑,script子句通过调用UpdateByQueryRequest.setScript()方法设置更新逻辑,然后执行客户端的updateByQuery()方法并把UpdateByQueryRequest对象传入即可。以下代码演示了根据城市字段查找文档然后更新城市字段的方法:

    public void updateCityByQuery(String index,String oldCity,String  
newCity) { 
        UpdateByQueryRequest updateByQueryRequest=new UpdateByQueryRequest 
(index);                //构建UpdateByQueryRequest对象 
        //设置按照城市查找文档的query 
        updateByQueryRequest.setQuery(new TermQueryBuilder("city",oldCity)); 
        updateByQueryRequest.setScript(new Script("ctx._source['city']= 
'"+newCity+"';"));      //设置更新城市字段的脚本逻辑 
        try { 
            client.updateByQuery(updateByQueryRequest,RequestOptions. 
DEFAULT);              //执行更新 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 
    @GetMapping("/updateHotelByQuery")
    public void updateHotelByQuery() {
        String indexName = "hotel_order";
        esService.updateHotelByQuery(indexName, "001", "_BJ如家酒店");
    }

如果更新所有文档中的某个字段应该如何操作呢?其实,_update_by_query中的query子句可以不定义,这种情况下ES会选中所有的文档执行script中的内容。以下为修改所有酒店中城市为“上海”的DSL:

POST /hotel/_update_by_query 
{ 
 "script": {                        //更新所有文档中的城市为“上海” 
    "source": "ctx._source['city']='上海'", 
    "lang": "painless" 
  } 
} 

3.6删除单条文档

在ES中删除文档的请求的类型是DELETE,其请求形式如下:

DELETE /${index_name}/_doc/${_id}

上面的_id就是将要删除的ES文档的_id。执行下面的删除命令:

DELETE /hotel/_doc/001 

返回的结果如下:

{ 
  "_index" : "hotel", 
  "_type" : "_doc", 
 "_id" : "001",            //删除的文档_id 
  "_version" : 1, 
 "result" : "deleted",    //删除成功 
 "_shards" : {            //删除请求影响的分片信息 
    "total" : 2, 
    "successful" : 1, 
    "failed" : 0 
  }, 
  "_seq_no" : 1, 
  "_primary_term" : 9 
} 

在Java高级REST客户端中,执行删除文档需要创建DeleteRequest对象并设置对应的索引名称与删除文档的_id,然后执行客户端的delete()方法并把DeleteRequest对象传入即可。以下代码演示了根据_id删除文档的方法:

    public void singleDelete(String index, String docId) { 
        //构建删除请求 
        DeleteRequest deleteRequest=new DeleteRequest(index,docId); 
        try { 
            client.delete(deleteRequest, RequestOptions.DEFAULT);//执行删除 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
    } 

3.7 批量删除文档

与批量写入和更新文档不同的是,批量删除文档不需要提供JSON数据,其请求形式如下:

POST /_bulk 
//批量删除文档,指定文档_id 
{"delete":{"_index":"${index_name}","_id":"${_id}"}} 
//批量删除文档,指定文档_id 
{"update":{"_index":"${index_name}","_id":"${_id}"}}  

例如,下面的DSL将批量删除_id为001和002的文档:

POST /_bulk 
{"delete":{"_index":"hotel","_id":"001"}} //批量删除文档,指定文档_id为“001” 
{" delete ":{"_index":"hotel","_id":"002"}} //批量删除文档,指定文档_id为“002” 

和批量写入文档相似,在Java客户端接口中,批量删除文档需要创建BulkRequest对象并设置对应的索引名称。对于多条需要删除的文档,可构建多个DeleteRequest对象并调用BulkRequest.add()方法添加这些DeleteRequest对象,执行时,调用客户端的bulk()方法并把BulkRequest对象传入即可。示例如下:

public void bulkDelete(String index, String docIdKey, List<String>  
docIdList) {  
    BulkRequest bulkRequest = new BulkRequest();    //构建BulkRequest对象 
    for (String docId : docIdList) {            //遍历文档_id列表 
        //构建删除请求 
        DeleteRequest deleteRequest=new DeleteRequest(index,docId); 
        bulkRequest.add(deleteRequest);        //创建UpdateRequest对象 
    } 
    try { 
        BulkResponse bulkResponse = client.bulk(bulkRequest,RequestOptions. 
DEFAULT);                                      //执行批量删除 
        if (bulkResponse.hasFailures()) {      //判断状态 
            System.out.println("bulk fail,message:" + bulkResponse.build 
FailureMessage()); 
        } 
    } catch (Exception e) { 
        e.printStackTrace(); 
    } 
} 

3.8 根据条件删除文档

和条件更新操作类似,有些场景需要根据某些条件同时删除多条数据,类似于在RDBMS中使用delete table_name where…删除一批数据。为了满足这样的需求,ES为用户提供了_delete_by_query功能,其请求形式如下:

POST /${index_name}/_delete_by_query 
{                          //删除文档的查询条件 
  "query": {} 
} 

query子句用于指定删除数据的匹配条件,相当于SQL中的where语句。下面的DSL将把city为北京的文档删除

POST /hotel/_delete_by_query 
{ 
 "query": {                //条件删除文档的查询条件:城市为“北京”的文档 
    "term": { 
      "city": { 
        "value": "北京" 
      } 
    } 
  } 
} 

在Java高级REST客户端中,执行条件删除文档需要创建DeleteByQueryRequest对象并设置对应的索引名称类似于DSL中的query子句,通过调用DeleteByQueryRequest.setQuery()方法设置查询逻辑,然后执行客户端的deleteByQuery()方法并把DeleteByQueryRequest对象传入即可。以下代码演示了根据城市删除文档的方法:

public void deleteByQuery(String index,String city) {  
    //构建DeleteByQueryRequest对象 
    DeleteByQueryRequest deleteByQueryRequest=new DeleteByQueryRequest(index); 
    //设置按照城市查找文档的query 
    deleteByQueryRequest.setQuery(new TermQueryBuilder("city",city)); 
    try { 
        //执行删除命令 
        client.deleteByQuery(deleteByQueryRequest,RequestOptions.DEFAULT); 
    } catch (IOException e) { 
        e.printStackTrace(); 
    } 
} 
 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/336663.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

VIF原理

文章目录一、VIF公式和原理对于R方一般回归模型皮尔逊相关系数中的方差VIF原理&#xff1a;一、VIF公式和原理 所谓VIF方法&#xff0c;计算难度并不高。在线性回归方法里&#xff0c;应用最广泛的就是最小二乘法&#xff08;OLS&#xff09;&#xff0c;只不过我们对每个因子…

锁的优化机制了解嘛?请进!

点个关注&#xff0c;必回关 文章目录自旋锁&#xff1a;自适应锁&#xff1a;锁消除&#xff1a;锁粗化&#xff1a;偏向锁&#xff1a;轻量级锁&#xff1a;从JDK1.6版本之后&#xff0c;synchronized本身也在不断优化锁的机制&#xff0c;有些情况下他并不会是一个很重量级的…

Open AI登录,可以使用ChatGPT!

目录 第一步&#xff1a;准备好上网工具 1、确认自己的IP 二、登录 三、无法登录的问题 第一步&#xff1a;准备好上网工具 1、确认自己的IP 先查看自己的电脑的IP&#xff0c;使用https://www.ip138.com/,看看&#xff0c;然后就可以将自己电脑的IP改成国外的IP&#x…

Word处理控件Aspose.Words功能演示:使用 C++ 拆分 MS Word 文档

Aspose.Words 是一种高级Word文档处理API&#xff0c;用于执行各种文档管理和操作任务。API支持生成&#xff0c;修改&#xff0c;转换&#xff0c;呈现和打印文档&#xff0c;而无需在跨平台应用程序中直接使用Microsoft Word。此外&#xff0c;API支持所有流行的Word处理文件…

利用 ChatGPT 回答 R 相关问题

最近小编也尝试使用了 ChatGPT&#xff0c;下面给出一些知乎上常见 R 语言相关问题的回答。供大家参考&#xff1a; 目录 文章目录目录经典问题预测数据科学软件排名数据科学的 10 个常用 R 包R 语言入门书籍和网站推荐回答专业问题绘图绘制正负柱状图动态散点图构建 Shiny 程…

Java学习笔记-03(API阶段)

前言 目前我们看到的是Java基础部分的一个新的部分API,这是个啥,又能做啥呢? 其实可以概括成一句话:帮助我们站在巨人的肩膀上,实现更加高效的开发,那么我们来一探究竟吧~ API API&#xff08;Application Programming Interface&#xff0c;应用程序接口&#xff09;是一些预…

如何使用HTTP远程连接DOCKER?

/etc/systemd/system/docker.service.d/override.conf 写入文件 ##Add this to the file for the docker daemon to use different ExecStart parameters (more things can be added here) [Service] ExecStart ExecStart/usr/bin/dockerd默认情况下使用 systemd 时&#xff0…

报表生成器 FastReport .Net 用户指南 2023(十):Band的属性

FastReport .Net是一款全功能的Windows Forms、ASP.NET和MVC报表分析解决方案&#xff0c;使用FastReport .NET可以创建独立于应用程序的.NET报表&#xff0c;同时FastReport .Net支持中文、英语等14种语言&#xff0c;可以让你的产品保证真正的国际性。 FastReport.NET官方版…

HTTP协议基础知识(URL,请求响应格式,状态码,方法,cookie和session等)

目录 URL urlencode和urldecode http请求格式 http响应格式 http请求解析 http响应发送 http的方法 GET vs POST http的状态码 重定向 http常见报头属性 cookie和session URL 首先我们需要知道以下几点&#xff0c; 1.我们请求的图片&#xff0c;html&#xff0c;…

spring-webflux5 使用websocket

换做平常springboot程序中使用websocket的话是很简单的&#xff0c;只需要三步就能实现前后端的实时通讯。而在spring5中则更简单了&#xff0c;并且支持定点推送与全推送的灵活运用。在这里就分常规编程与响应式编程两种使用&#xff0c;进行记录下。一、非响应式编码1、引入W…

第十节 使用设备树插件实现RGB 灯驱动

Linux4.4 以后引入了动态设备树&#xff08;Dynamic DeviceTree&#xff09;&#xff0c;我们这里翻译为“设备树插件”。设备树插件可以理解为主设备树的“补丁”它动态的加载到系统中&#xff0c;并被内核识别。例如我们要在系统中增加RGB 驱动&#xff0c;那么我们可以针对R…

深入理解Android图形系统

一、图形系统简介图形系统是计算机中最重要的子系统之一。我们平时使用的电脑、手机都是图形界面的。对于普通人来说&#xff0c;没有图形界面的计算机几乎是没法用的&#xff0c;今天我们就来讲一讲图形系统背后的原理。1.1 图形系统的诞生早期的计算机是没有图形界面的&#…

一种基于深度学习的单导联心电信号睡眠呼吸暂停检测方法

在R峰识别的基础上&#xff0c;加入S峰的识别&#xff0c;并论正了该策略对检测结果的有效性。 1、大致方法 将数据集&#xff08;ECG信号&#xff09;划分为每五分钟的一个片段&#xff0c;为了减少噪声和信号伪影&#xff0c;首先对信号应用了一个有限脉冲响应&#xff08;…

使用python加密主机文件几种方法实现

本文主要介绍了使用python加密主机文件几种方法实现&#xff0c;文中通过示例代码介绍的非常详细&#xff0c;对大家的学习或者工作具有一定的参考学习价值&#xff0c;需要的朋友们下面随着小编来一起学习学习吧数据加密是一种保护数据安全的技术&#xff0c;通过对数据进行编…

你所不知道的Number()、parseInt()、parseFloat()转换细则

为什么要写这篇文章 最近在阅读《javascript高级程序设计》&#xff08;第6版&#xff09;这本书&#xff0c;以写代记。同时刚刚入职新公司&#xff0c;发现自己对于number类型转换的细节掌握不足 使用场景 我们常常对于很多字符串类型的数字需要转换成Number&#xff0c;一…

2023春节,蔚来高速免费换电累计达37万余次

2023年春节放假前&#xff0c;蔚来官方宣布高速换电免费消息&#xff1a; 在高速公路上&#xff08;含高速出入口&#xff09;的所有蔚来汽车换电站将为蔚来用户&#xff0c;提供不限次免费换电服务。 消息发布后&#xff0c;部分电动车企也进行了跟进&#xff0c;提供了特定区…

二十四、Gtk4-GtkExpression

GtkExpression是一种基本类型。它不是GObject的后代。GtkExpression提供了一种描述对值的引用的方法。GtkExpression需要求值才能获得值。 它类似于算术计算。 1 2 3 12是一个表达式。给出了计算的方法。3是来自表达式的值。求值是计算表达式并得到值。 GtkExpression是一种…

【大厂高频真题100题】《有效的井字游戏》 真题练习第26题 持续更新~

有效的井字游戏 给你一个字符串数组 board 表示井字游戏的棋盘。当且仅当在井字游戏过程中,棋盘有可能达到 board 所显示的状态时,才返回 true 。 井字游戏的棋盘是一个 3 x 3 数组,由字符 ,X 和 O 组成。字符 代表一个空位。 以下是井字游戏的规则: 玩家轮流将字符…

CODESYS开发教程10-文件读写(SysFile库)

今天继续我们的小白教程&#xff0c;老鸟就不要在这浪费时间了&#x1f60a;。 前面一期我们介绍了CODESYS的文件操作库CAA File。这一期主要介绍CODESYS的SysFile库所包含的文件读写功能块&#xff0c;主要包括文件路径、名称、大小的获取以及文件的创建、打开、读、写、拷贝…

mysql数据库安装

三种安装方式 tar -xf mysql-5.7.37-linux-glibc2.12-x86_64.tar.gz [rootsc-mysql opt]# tar -xf mysql-5.7.37-linux-glibc2.12-x86_64.tar.gz [rootsc-mysql opt]# ls apache-zookeeper-3.6.3-bin kafka_2.12-2.8.1.tgz apache-zookeeper-3.6.3-bin.tar.gz mysql…