Elasticsearch数据的增删改查

首先,我们创建如下索引:

{
    "settings": {
        "index": {
            "number_of_shards": 3,
            "number_of_replicas": 2
        }
    },
    "mappings": {
        "properties": {
            "brandName": {
                "type": "text"
            },
            "categoryName": {
                "type": "text"
            },
            "description": {
                "type": "text"
            },
            "id": {
                "type": "long"
            },
            "productName": {
                "type": "text",
                "fields": {
                    "raw": {
                        "type": "keyword",
                        "ignore_above": 256
                    }
                }
            },
            "utime": {
                "type": "date_nanos",
                "index": false
            }
        }
    }
}

es数据的插入

PUT /product/_doc/1

其中的product是索引名;_doc是类型(ES7后type都是_doc);1代表这条数据的主键;如果id不填也可以,ES会自动生成一条主键,不过这时就不能用PUT了,需要使用POST添加。

{
    "id": 10001,
    "productName": "牛肉片(花卉牌)",
    "brandName": "花卉牌",
    "categoryName": "零食",
    "utime": 1614666414
}

查询插入结果:

GET /product/_doc/1   //查询ID为1的数据

插入多条数据:

{
    "id": 10002,
    "productName": "森马秋冬小脚库",
    "brandName": "森马",
    "categoryName": "长裤",
    "utime": 1614667414
}
{
    "id": 10003,
    "productName": "加绒加厚打底衫",
    "brandName": "森马",
    "categoryName": "长衣",
    "utime": 1614686414
}

es数据的修改

PUT /product/_doc/1    //PUT方式进行修改,这种是把原来对应文档覆盖掉  
{
    "id": 10001,
    "productName": "牛肉片(潮汕集锦)",
    "brandName": "潮汕集锦",
    "categoryName": "零食",
    "utime": 1614666414
}
POST /product/_doc/1/_update  //POST方式修改的话,可以针对对应field来修改,比PUT要轻量

{ 
  "doc": {
    "categoryName":"小吃"
  }
}

es数据的删除

DELETE product/_doc/1  //删除一个文档
DELETE product    //删除索引

批量操作之bulk

bulk是es提供的一种批量增删改的操作API。

bulk对JSON串有着严格的要求。每个JSON串不能换行,只能放在同一行,同时,相邻的JSON串之间必须要有换行(Linux下是\n;Window下是\r\n)。bulk的每个操作必须要一对JSON串(delete语法除外)。

{ action: { metadata }}
{ request body        }
{ action: { metadata }}
{ request body        }

例如,假如现在要给example的docs中新增一个文档。其表示如下:

POST _bulk
{"create": {"_index": "example", "_type": "_doc", "_id": 11}}
{"name": "test_bulk", "counter":"100"}


#查询example所有数据,发现id为11的已经添加成功
GET example/_doc/_search
{
  "query": {
    "match_all": {}
  }
}

bulk的操作类型

  • create 如果文档不存在就创建,但如果文档存在就返回错误
  • index 如果文档不存在就创建,如果文档存在就更新
  • update 更新一个文档,如果文档不存在就返回错误
  • delete 删除一个文档,如果要删除的文档id不存在,就返回错误

其实可以看得出来index是比较常用的。还有bulk的操作,某一个操作失败,是不会影响其他文档的操作的,它会在返回结果中告诉你失败的详细的原因。

索引及mapping准备

PUT example
{
    "settings": {
        "number_of_shards": 3,
        "number_of_replicas": 2
    },
    "mappings": {
        "properties": {
            "id": {
                "type": "long"
            },
            "name": {
                "type": "text"
            },
            "counter": {
                "type": "integer"
            },
            "tags": {
                "type": "text"
            }
        }
    }
}

批量新增

注:使用Postman执行。

PUT example/_bulk
{"index": {"_id": 1}}
{"id":1, "name": "admin", "counter":"10", "tags":["red", "black"]}

{"index": {"_id": 2}}
{"id":2, "name": "张三", "counter":"20", "tags":["green", "purple"]}

{"index": {"_id": 3}}
{"id":3, "name": "李四", "counter":"30", "tags":["red", "blue"]}

{"index": {"_id": 4}}
{"id":4, "name": "tom", "counter":"40", "tags":["orange"]}
#这里也有个换行的

注意:bulk语法要求必须两行json后换行,末尾也有一个换行的。

批量修改

注:使用Postman执行。

POST example/_bulk
{"update": {"_id": 1}}
{"doc": {"id":1, "name": "admin-02", "counter":"11"}}

{"update": {"_id": 2}}
{"script":{"lang":"painless","source":"ctx._source.counter += params.num","params": {"num":2}}}

{"update":{"_id": 3}}
{"doc": {"name": "test3333name", "counter": 999}}

{"update":{"_id": 4}}
{"doc": {"name": "test444name", "counter": 888},  "doc_as_upsert" : true}
#这里有换行符

批量删除

注:使用Postman执行。

POST example/_bulk
{"delete": {"_id": 1}}

{"delete": {"_id": 2}}

{"delete": {"_id": 3}}

{"delete": {"_id": 4}}
#这里有换行符

es数据的查询

es中的查询请求有两种方式,一种是简易版的查询,另外一种是使用JSON完整的请求体,叫做结构化查询(DSL)。

由于DSL查询更为直观也更为简易,所以大都使用这种方式。

DSL查询是POST过去一个json,由于post的请求是json格式的,所以存在很多灵活性,也有很多形式。

这里有一个地方注意的是官方文档里面给的例子的json结构只是一部分,并不是可以直接黏贴复制进去使用的。一般要在外面加个query为key的结构。

查询一条数据

GET /product/_doc/1

查询结果:

{
    "_index": "product",
    "_type": "_doc",
    "_id": "1",
    "_version": 2,
    "_seq_no": 1,
    "_primary_term": 1,
    "found": true,
    "_source": {
        "id": 10001,
        "productName": "牛肉片(花卉牌)",
        "brandName": "花卉牌",
        "categoryName": "小吃",
        "utime": 1614666414
    }
}

查询所有数据

GET /product/_doc/_search 
{
  "query": {
    "match_all": {}
  }
}

查询结果:

{
    "took": 6,
    "timed_out": false,
    "_shards": {
        "total": 3,
        "successful": 3,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": {
            "value": 2,
            "relation": "eq"
        },
        "max_score": 1,
        "hits": [
            {
                "_index": "product",
                "_type": "_doc",
                "_id": "2",
                "_score": 1,
                "_source": {
                    "id": 10002,
                    "productName": "森马秋冬小脚库",
                    "brandName": "森马",
                    "categoryName": "长裤",
                    "utime": 1614666414
                }
            },
            {
                "_index": "product",
                "_type": "_doc",
                "_id": "1",
                "_score": 1,
                "_source": {
                    "id": 10001,
                    "productName": "牛肉片(花卉牌)",
                    "brandName": "花卉牌",
                    "categoryName": "小吃",
                    "utime": 1614666414
                }
            }
        ]
    }
}

返回结果说明:

  • took字段表示该操作的耗时(单位为毫秒)。
  • timed_out字段表示是否超时。
  • hits字段表示搜到的记录,数组形式。
  • total:返回记录数,本例是2条。
  • max_score:最高的匹配程度,本例是1.0。

查询结果只返回部分属性

POST product/_doc/_search
{
  "_source": [
    "id", "productName"
  ],
   "query": {
    "match_all": {}
  }
}

通过_source 字段来指定需要返回的字段。

将 _source 设置为 false, 可以不显示原始字段,部分特殊场景下会用到。

或者

{
    "_source": {
        "includes": [
            "id"
        ],
        "excludes": [
            "utime"
        ]
    },
    "query": {
        "match_all": {}
    }
}

其中includes代表需要返回的字段,excludes代表不要返回的字段。

注:这里是POST请求。

分页查询

Elasticsearch中数据都存储在分片中,当执行搜索时每个分片独立搜索后,数据再经过整合返回。那么,如何实现分页查询呢?

按照一般的查询流程来说,如果我想查询前10条数据:

  1. 客户端请求发给某个节点
  2. 节点转发给各个分片,查询每个分片上的前10条
  3. 结果返回给节点,整合数据,提取前10条
  4. 返回给请求客户端

那么当我想要查询第10条到第20条的数据该怎么办呢?这个时候就用到分页查询了。

在ElasticSearch中实现分页查询的方式有两种,分别为深度分页(from-size)和快照分页(scroll)。

深度分页(from-size)

深度分页原理很简单,就是查询前20条数据,然后截断前10条,只返回10-20的数据。这样其实白白浪费了前10条的查询。

查询API如下:

POST product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "match_all": {}
    }
}

其中,from定义了目标数据的偏移值,size定义当前返回的事件数目。默认from为0,size为10,即所有的查询默认仅仅返回前10条数据。

在实际测试过程中,当此方式的访问页码越高,其执行的查询效率就越低。假设我们现在需要获取第20页的数据,ElasticSearch不得不取出所有分片上的第1页到第20页的所有文档,并对其进行合并排序,最终再取出from后的size条作为最终的返回结果;假设我们现在服务器上有16个分片,则我们需要汇总到shards*(from+size)条记录,即需要16*(20+10)条记录后,对其进行整合再做一次全局排序。因此,当索引非常大时,我们是无法使用from+size方式做深分页的,分页越深越容易OOM或者消耗内存,所以ES使用index.max_result_window:10000作为保护措施来避免这种情况的发生。但实际上当访问数据非常大时,我们采用scroll游标的方式来获取数据是更好地一种选择

  • 优点:数据量小的情况使用最方便,灵活性好,实现简单
  • 缺点:内存消耗大,速度一般,数据量大的情况面临深度分页问题

大数据量的快照分页(scroll)

相对于from&size的分页来说,使用scroll可以模拟一个传统的游标来记录当前读取的文档信息位置。采用此分页方法,不是为了实时查询数据,而是为了查询大量甚至全部的数据。此方式相当于维护了一份当前索引的快照信息,在执行数据查询时,scroll将会从这个快照信息中获取数据。它相对于传统的分页方式来说,不是查询所有数据再剔除掉不需要的部分,而是记录一个读取的位置来保证下次对数据的继续获取。

scroll是一种快照的查询形式,快照一旦形成,本次滚动查询内便无法查出来新增的那些数据,而且scroll是无法进行排序的,也无法指定from,那么我们想查看指定页码的数据就必须将该页数据之前的全部数据取出来再进行丢弃,所以scroll一般用于导出全量数据。

可以把 scroll 分为初始化和遍历两步:

(1)初始化时将所有符合搜索条件的搜索结果缓存起来,可以想象成快照;

POST /product/_doc/_search?scroll=3m
{
  "query": {"match_all": {}},
  "size": 100
}

初始化的时候就像是普通的search一样,其中的scroll=3m代表当前查询的数据缓存3分钟。

size:100 代表当前查询100条数据

(2)遍历时,从这个快照里取数据;

在遍历时候,拿到上一次遍历中的_scroll_id,然后带scroll参数,重复上一次的遍历步骤,直到返回的数据为空,表示遍历完成。

每次都要传参数scroll,刷新搜索结果的缓存时间,另外不需要指定index和type(不要把缓存的时时间设置太长,占用内存)。

POST /product/_doc/_search?scroll=3m
{
  "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}
  • 优点:导出全量数据时性能最高
  • 缺点:无法反应数据的实时性(快照版本),维护成本高,需要维护一个 scroll_id,并且不支持排序,只能按照文档id排序

search_after

search_after有点类似scroll,但是和scroll又不一样,它提供一个活动的游标,通过上一次查询最后一条数据来进行下一次查询同时可以实时查询出来新增的数据且支持排序。但是不支持跳页查询,即每一次的查询都需要依赖上一次的查询结果,只能一页一页的往下翻。

  • 优点:查询性能最好,不存在深度分页问题,能够反映数据的实时变更
  • 缺点:实现复杂,需要有一个全局唯一的字段,连续分页的实现会比较复杂,因为每一次查询都需要上次查询的结果。

小结

在这几种方式中,scroll方式适用于ES中索引数据很大的情况,因为scroll第一次请求数据时的时间相对于后面请求size大小的时间大得多(原因是因为此种方式会将满足条件的所有索引数据都以快照的方式保存在内存中,然后后续的数据请求都直接可以获取,因此第一次和之后的请求时间会差别比较大);当数据量比较小时,采用传统的from&size方式的效率就会比较高。

无论是哪种方式,都要避免深分页查询。

排序

POST product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "match_all": {}
    },
    "sort": {
        "id": {
            "order": "desc"
        }
    }
}

上面按id降序排序。

假定我们想要结合使用id和 _score进行查询,并且匹配的结果首先按照id排序,然后按照相关性得分排序。

POST product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "match_all": {}
    },
    "sort": [
        {
            "id": {
                "order": "desc"
            }
        },
        {
            "_score": {
                "order": "desc"
            }
        }
    ]
}

term查询

精确查询,不会对输入做分词,如果输入的是"某某人",则直接查询"某某人",如果输入的是"某某事",则直接查询"某某事"。

POST product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "term": {
            "id": 10002
        }
    }
}

text类型term查询不到

① 我想要进行查询的字段在创建mapping时使用的“text”数据类型进行创建。

② 众所周知text类型的数据在elasticsearch中会进行分词并建立倒排索引,因此它会对每个词进行索引,而不会建立整个句子的索引。

③ term搜索时会对整个句子作为关键词进行搜索,由于没有建立整个句子的关键词索引,因此无法查找到东西。

match查询

match搜索会先对搜索词进行分词,对于最基本的match搜索来说,只要搜索词的分词集合中的一个或多个存在于文档中即可,例如,当我们搜索中国杭州,搜索词会先分词为中国和杭州,只要文档中包含搜索和杭州任意一个词,都会被搜索到。

POST /product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "match": {
            "productName": "森马"
        }
    }
}

match单字段多条件or/and查询

我们知道match查询会对输入进行分词,我们可以指定对分词后的结果进行and和or查询。

and:代表分词后的所有结果都得匹配,or:代表分词后只要有一个结果匹配就行(默认是or)

POST /product/_doc/_search
-- or查询
{
    "from": 0,
    "size": 2,
    "query": {
        "match": {
            "productName": {
                "query": "森马牛肉",
                "operator": "or"
            }
        }
    }
}

-- and查询
{
    "from": 0,
    "size": 2,
    "query": {
        "match": {
            "productName": {
                "query": "森马牛肉",
                "operator": "and"
            }
        }
    }
}

match_phrase短语查询

match_phrase为按短语搜索,比如根据一个文本搜索:“我的宝马多少马力”,这个文本可能会被分词成宝马、多少、马力三个短语,只有同时满足这三个才能被搜索出来。

POST /product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "match_phrase": {
            "productName": {
                "query": "森马秋冬"
            }
        }
    }
}

完全匹配可能比较严,我们会希望有个可调节因子,少匹配一个也满足,那就需要使用到slop。

POST /product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "match_phrase": {
            "productName": {
                "query": "森马",
		"slop" : "1"
            }
        }
    }
}

multi_match查询

多字段模糊查询,和match类似都是模糊查询,但multi_match可以指定多字段进行模糊查询。

{
    "from": 0,
    "size": 2,
    "query": {
        "multi_match": {
            "query": "森马",
            "fields": [
                "brandName",
                "categoryName"
            ]
        }
    }
}

这里根据brandName和categoryName字段进行模糊查询。

但是multi_match就涉及到匹配评分的问题了。

我们希望完全匹配的文档占的评分比较高,则需要使用best_fields

POST /product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "multi_match": {
            "query": "森马花卉牌",
            "fields": [
                "brandName",
                "categoryName"
            ],
            "type": "best_fields",
            "tie_breaker": 0.3
        }
    }
}

意思就是完全匹配"森马花卉牌"的文档评分会比较靠前,如果只匹配森马的文档评分乘以0.3的系数。

我们希望越多字段匹配的文档评分越高,就要使用most_fields。

POST /product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "multi_match": {
            "query": "森马花卉牌",
            "fields": [
                "brandName",
                "categoryName"
            ],
            "type": "most_fields"
        }
    }
}

我们会希望这个词条的分词词汇是分配到不同字段中的,那么就使用cross_fields

POST /product/_doc/_search
{
    "from": 0,
    "size": 2,
    "query": {
        "multi_match": {
            "query": "森马花卉牌",
            "fields": [
                "brandName",
                "categoryName"
            ],
            "type": "cross_fields"
        }
    }
}

query_string查询

query_string和match类似,但是match需要指定字段名,query_string是在所有字段中搜索,范围更广泛(当然query_string也支持指定字段查询)。

POST /product/_doc/_search
{
    "from": 0,
    "size": 20,
    "query": {
        "query_string": {
            "query": "森马"
        }
    }
}

-- 指定字段
{
    "from": 0,
    "size": 20,
    "query": {
        "query_string": {
            "query": "森马",
            "fields": [
                "brandName",
                "categoryName"
            ]
        }
    }
}

类似mysql的like查询

创建一个索引:

PUT testEs
{
    "mappings" : {
      "properties" : {
        "num" : {
          "type" : "keyword"
        },
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
         "englishName" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          }
        },
         "msg" : {
          "type" : "text",
            "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
   	     }
        }
      }
    }
}

插入一条数据:

PUT  /testEs/_doc/1
{
  "num": "123456",
  "name": "小明",
  "englishName": "xiaoMing",
  "msg": "我爱学习"
}

使用Wildcard Query的通配符进行查询

前提是查询的字段类型是string类型,对应ES中的text,keyword(这种查询方式会慢,查询不进行分词处理)

GET /testEs/_search
{
  "query": {
    "wildcard": {
      "msg.keyword": "*爱学*"
    }
  }
}

使用match_phrase进行查询

GET /testEs/_search
{
  "query": {
    "match_phrase": {
      "msg": "爱学"
    }
  }
}

GET /testEs/_search
{
  "query": {
    "match_phrase": {
      "num": "23"
    }
  }
}

查询name和msg字段发现可以查询出来,但是查询num和englishName会发现查询不出来数据。

这是因为ES默认会把中文进行单个字的分词拆分,而对于英文和数字是基于空格进行拆分的,这显然不符合我们对于mysql的like查询,对此我们可以建立一个分词器来解决。

建立一个带分词的索引

PUT /testEs2
{
    "mappings" : {
      "properties" : {
        "num" : {
          "type" : "text",
          "analyzer": "my_analyzer"
        },
        "name" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          },
          "analyzer": "my_analyzer"
        },
         "englishName" : {
          "type" : "text",
          "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          },
          "analyzer": "my_analyzer"
        },
         "msg" : {
          "type" : "text",
            "fields" : {
            "keyword" : {
              "type" : "keyword",
              "ignore_above" : 256
            }
          },
          "analyzer": "my_analyzer"
        }
      }
    },
    "settings": {
    "index": {
      "analysis": {
        "analyzer": {
          "my_analyzer": {
            "type": "custom",
            "tokenizer": "my_tokenizer"
          }
        },
        "tokenizer": {
          "my_tokenizer": {
            "type": "ngram",
            "min_gram": "1",
            "max_gram": "2"
          }
        }
      }
    }
  }
}

插入数据

PUT  /testEs2/_doc/1
{
  "num": "123456",
  "name": "小明",
  "englishName": "xiaoMing",
  "msg": "我爱学习"
}

再进行测试查询,发现可以查询了

GET /testEs2/_search
{
  "query": {
    "match_phrase": {
      "englishName": "in"
    }
  }
}

bool联合查询

如果我们想要请求"productName中带宝马,但是brandName中不带宝马"这样类似的需求,就需要用到bool联合查询。

bool查询包括四种子句:

  • must:文档必须完全匹配条件
  • should:只要有一个或部分条件满足
  • must_not:文档必须不匹配条件
  • filter:返回的文档必须满足filter子句的条件。但是跟must不一样的是,不会计算分值, 并且可以使用缓存

比如上面那个需求:

POST /product/_doc/_search
{
  "query": {
    "bool": {
      "must": {
        "term": {
          "productName": "宝马"
        }
      },
      "must_not": {
        "term": {
          "brandName": "宝马"
        }
      }
    }
  }
}

我们再来说filter过滤条件。从上面的描述来看,你应该已经知道,如果只看查询的结果,must和filter是一样的。区别是场景不一样。如果结果需要算分就使用must,否则可以考虑使用filter。

使用filter过滤时间范围:

{
  "size": 1000, 
  "query": {
    "bool": {
      "must": [
        {"term": {
          "currency": "EUR"
        }}
      ],
      "filter": {
        "range": {
          "order_date": {
            "gte": "2020-01-25T23:45:36.000+00:00",
            "lte": "2020-02-01T23:45:36.000+00:00"
          }
        }
      }
    }
  }
}

使用must过滤时间范围

{
  "size": 1000, 
  "query": {
    "bool": {
      "must": [
        {"term": {
          "currency": "EUR"
        }},
        {"range": {
          "order_date": {
            "gte": "2020-01-25T23:45:36.000+00:00",
            "lte": "2020-02-01T23:45:36.000+00:00"
          }
        }}
      ]
    }
  }
}

filter比较高效的原理

为了说明filter查询高效的原因,我们需要引入ES的一个概念 query context和 filter context。

query context关注的是,文档到底有多匹配查询的条件,这个匹配的程度是由相关性分数决定的,分数越高自然就越匹配。所以这种查询除了关注文档是否满足查询条件,还需要额外的计算相关性分数。

filter context关注的是,文档是否匹配查询条件,结果只有两个,是和否。没有其它额外的计算。它常用的一个场景就是过滤时间范围。并且filter context会自动被ES缓存结果,效率进一步提高。

对于bool查询,must使用的就是query context,而filter使用的就是filter context。

 

posted @ 2022-02-13 18:46  残城碎梦  阅读(1436)  评论(0编辑  收藏  举报