1. ElasticSearch简介
1.1. Elasticsearch
ElasticSearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式的 RESTful 风格的搜索和数据分析引擎。
Elasticsearch 是用 Java 语言开发的,并作为 Apache 许可条款下的开放源码发布,是一种流行的企业级搜索引擎。
ElasticSearch 能够达到实时搜索,稳定,可靠,快速,安装使用方便。
特性:
- 存储:分布式的文档存储引擎,支持PB级数据。
- 查询和分析:分布式的搜索引擎和分析引擎。
- 可扩展:支持一主多从且扩容简易,只要cluster.name一致且在同一个网络中就能自动加入当前集群;也支持很多开源的第三方插件,如分词插件、同步插件、Hadoop插件、可视化插件等。
- 高可用:在一个集群的多个节点中进行分布式存储,索引支持shards和复制,即使部分节点down掉,也能自动进行数据恢复和主从切换。
- RestfulAPI标准:通过http接口使用JSON格式进行操作数据。
- 数据类型丰富:数字、文本、地理位置、结构化、非结构化等。
索引(indices)----------------------Databases 数据库
类型(type)--------------------------Table 数据表(7.0版本已被弃用)
文档(Document)----------------------Row 行 表记录
字段(Field)-------------------------Columns 列
1.2. 同类产品
Solr、ElasticSearch、Hermes(腾讯)(实时检索分析)
-
Solr、ES
- 源自搜索引擎,侧重搜索与全文检索。
- 数据规模从几百万到千万不等,数据量过亿的集群特别少。
有可能存在个别系统数据量过亿,但这并不是普遍现象(就像Oracle的表里的数据规模有可能超过Hive里一样,但需要小型机)。
-
Hermes
- 一个基于大索引技术的海量数据实时检索分析平台。侧重数据分析。
- 数据规模从几亿到万亿不等。最小的表也是千万级别。
在 腾讯17 台TS5机器,就可以处理每天450亿的数据(每条数据1kb左右),数据可以保存一个月之久。
-
Solr、ES
区别
全文检索、搜索、分析。基于lucene- Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能;
- Solr 支持更多格式的数据,而 Elasticsearch 仅支持json文件格式;
- Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供;
- Solr 在传统的搜索应用中表现好于 Elasticsearch,但在处理实时搜索应用时效率明显低于 Elasticsearch-----附近的人
Lucene是一个开放源代码的全文检索引擎工具包,但它不是一个完整的全文检索引擎,而是一个全文检索引擎的架构,提供了完整的查询引擎和索引引擎,部分文本分析引擎
搜索引擎产品简介
1.3. 相关概念
1.3.1索引
索引基本概念(indices):
索引是含义相同属性的文档集合,是 ElasticSearch 的一个逻辑存储,可以理解为关系型数据库中的数据库,ElasticSearch 可以把索引数据存放到一台服务器上,也可以 sharding 后存到多台服务器上,每个索引有一个或多个分片,每个分片可以有多个副本。
索引类型(index_type):
索引可以定义一个或多个类型,文档必须属于一个类型。在 ElasticSearch 中,一个索引对象可以存储多个不同用途的对象,通过索引类型可以区分单个索引中的不同对象,可以理解为关系型数据库中的表。每个索引类型可以有不同的结构,但是不同的索引类型不能为相同的属性设置不同的类型。
1.3.2. 文档
文档(document):
文档是可以被索引的基本数据单位。存储在 ElasticSearch 中的主要实体叫文档 document,可以理解为关系型数据库中表的一行记录。每个文档由多个字段构成,ElasticSearch 是一个非结构化的数据库,每个文档可以有不同的字段,并且有一个唯一的标识符。
1.3.3. 映射
映射(mapping):
ElasticSearch 的 Mapping 非常类似于静态语言中的数据类型:声明一个变量为 int 类型的变量,以后这个变量都只能存储 int 类型的数据。同样的,一个 number 类型的 mapping 字段只能存储 number 类型的数据。
同语言的数据类型相比,Mapping 还有一些其他的含义,Mapping 不仅告诉 ElasticSearch 一个 Field 中是什么类型的值, 它还告诉 ElasticSearch 如何索引数据以及数据是否能被搜索到。
ElaticSearch 默认是动态创建索引和索引类型的 Mapping 的。这就相当于无需定义 Solr 中的 Schema,无需指定各个字段的索引规则就可以索引文件,很方便。但有时方便就代表着不灵活。比如,ElasticSearch 默认一个字段是要做分词的,但我们有时要搜索匹配整个字段却不行。如有统计工作要记录每个城市出现的次数。对于 name 字段,若记录 new york 文本,ElasticSearch 可能会把它拆分成 new 和 york 这两个词,分别计算这个两个单词的次数,而不是我们期望的 new york。
1.2. 索引操作(indeces)
1.2.1. 初步索引
GET /_cat/nodes:查看所有节点
GET /_cat/health:查看 es 健康状况
GET /_cat/master:查看主节点
GET /_cat/indices:查看所有索引 show databases;
1.2.2. 创建索引
PUT /索引名
PUT customer
返回结果:
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "customer"
}
1.2.3. 查看索引具体信息
GET /索引名
GET customer
返回结果:
{
"customer" : {
"aliases" : { },
"mappings" : { },
"settings" : {
"index" : {
"creation_date" : "1635937861845",
"number_of_shards" : "1",
"number_of_replicas" : "1",
"uuid" : "kVIb3V4yTy6M5NSgLCMGZg",
"version" : {
"created" : "7040299"
},
"provided_name" : "customer"
}
}
}
}
1.2.4. 删除索引
DELETE /索引库名
DELETE customer
返回结果:
{
"acknowledged" : true
}
1.3. 映射配置(_mapping)
Mapping 是用来定义一个文档(document),以及它所包含的属性(field)是如何存储和
索引的。比如,使用 mapping 来定义:
- 哪些字符串属性应该被看做全文本属性(full text fields)。
- 哪些属性包含数字,日期或者地理位置。
- 文档中的所有属性是否都能被索引(_all 配置)。
- 日期的格式。
- 自定义映射规则来执行动态添加属性
新版本改变
Es7 及以上移除了 type 的概念。
- 关系型数据库中两个数据表示是独立的,即使他们里面有相同名称的列也不影响使用,
但 ES 中不是这样的。elasticsearch 是基于 Lucene 开发的搜索引擎,而 ES 中不同 type
下名称相同的 filed 最终在 Lucene 中的处理方式是一样的。 - 两个不同 type 下的两个 user_name,在 ES 同一个索引下其实被认为是同一个 filed,
你必须在两个不同的 type 中定义相同的 filed 映射。否则,不同 type 中的相同字段
名称就会在处理中出现冲突的情况,导致 Lucene 处理效率下降。 - 去掉 type 就是为了提高 ES 处理数据的效率。
Elasticsearch 7.x - URL 中的 type 参数为可选。比如,索引一个文档不再要求提供文档类型。
Elasticsearch 8.x - 不再支持 URL 中的 type 参数。
解决:
1)、将索引从多类型迁移到单类型,每种类型文档一个独立索引
2)、将已存在的索引下的类型数据,全部迁移到指定位置即可。详见数据迁移
索引有了,接下来肯定是添加数据。但是,在添加数据之前必须定义映射。
1.3.1. 创建映射字段
PUT /索引库名
{
"mappings": {
"properties": {
"字段名": {
"type": "类型",
"index": true,
"store": true,
"analyzer": "分词器"
}
}
}
}
类型名称:就是前面将的type的概念,类似于数据库中的不同表
字段名:类似于列名,properties下可以指定许多字段。
每个字段可以有很多属性。例如:
-
type
:类型,可以是text、long、short、date、integer、object等 -
index
:是否索引,默认为true -
store
:是否存储,默认为false -
analyzer
:分词器,这里使用ik分词器:ik_max_word
或者ik_smart
发起请求:
PUT atguigu
{
"mappings": {
"properties": {
"title": {
"type": "text",
"analyzer": "ik_max_word"
},
"images": {
"type": "keyword",
"index": "false"
},
"price": {
"type": "long"
}
}
}
}
返回结果:
{
"acknowledged" : true,
"shards_acknowledged" : true,
"index" : "atguigu"
}
1.3.2. 查看映射关系
GET /索引库名/_mapping
GET /atguigu/_mapping
返回结果:
{
"atguigu" : {
"mappings" : {
"goods" : {
"properties" : {
"images" : {
"type" : "keyword",
"index" : false
},
"price" : {
"type" : "long"
},
"title" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}
}
}
type
:字段类型。String(text keyword) Numeric(long integer float double) date boolean
index
:是否创建索引
analyzer
:分词器(ik_max_word)
1.3.3. 更新映射
对于已经存在的映射字段,我们不能更新。更新必须创建新的索引进行数据迁移
1.3.4. 数据迁移
将旧索引的 type 下的数据进行迁移
POST _reindex
{
"source": {
"index": "旧索引名",
"type": "旧索引的类型,如果有则需写"
},
"dest": {
"index": "新索引名"
}
}
1.4. 文档操作(document)
有了索引、类型和映射,就可以对文档做增删改查操作了。
1.4.1. 新增文档
如果我们想要自己新增的时候指定id,可以这么做:
PUT/索引库名/类型/id值
{
---
}
PUT 和 POST 都可以,
POST 新增。如果不指定 id,会自动生成 id。指定 id 就会修改这个数据,并新增版本号
PUT 可以新增可以修改。PUT 必须指定 id;由于 PUT 需要指定 id,我们一般都用来做修改
操作,不指定 id 会报错
-------
PUT my-index/external/1
{
"name": "John Doe"
}
返回结果:
{
"_index" : "my-index",
"_type" : "external",
"_id" : "1",
"_version" : 1,
"result" : "created",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 0,
"_primary_term" : 1
}
1.4.2. 智能判断
事实上Elasticsearch非常智能,你不需要给索引库设置任何mapping映射,它也可以根据你输入的数据来判断类型,动态添加数据映射。
测试一下:
POST /atguigu/goods/2
{
"title":"小米手机",
"images":"http://image.jd.com/12479122.jpg",
"price":2899,
"stock": 200,
"saleable":true,
"attr": {
"category": "手机",
"brand": "小米"
}
}
我们额外添加了stock库存,saleable是否上架,attr其他属性几个字段。
来看结果:GET /atguigu/_search
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 2,
"successful" : 2,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 2,
"max_score" : 1.0,
"hits" : [
{
"_index" : "atguigu",
"_type" : "goods",
"_id" : "1",
"_score" : 1.0,
"_source" : {
"title" : "华为手机",
"images" : "http://image.jd.com/12479122.jpg",
"price" : 4288
}
},
{
"_index" : "atguigu",
"_type" : "goods",
"_id" : "2",
"_score" : 1.0,
"_source" : {
"title" : "小米手机",
"images" : "http://image.jd.com/12479122.jpg",
"price" : 2899,
"stock" : 200,
"saleable" : true,
"attr" : {
"category" : "手机",
"brand" : "小米"
}
}
}
]
}
}
再看下索引库的映射关系: GET /atguigu/_mapping
{
"atguigu" : {
"mappings" : {
"goods" : {
"properties" : {
"attr" : {
"properties" : {
"brand" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
},
"category" : {
"type" : "text",
"fields" : {
"keyword" : {
"type" : "keyword",
"ignore_above" : 256
}
}
}
}
},
"images" : {
"type" : "keyword",
"index" : false
},
"price" : {
"type" : "long"
},
"saleable" : {
"type" : "boolean"
},
"stock" : {
"type" : "long"
},
"title" : {
"type" : "text",
"analyzer" : "ik_max_word"
}
}
}
}
}
}
stock,saleable,attr都被成功映射了。
如果是字符串类型的数据,会添加两种类型:text
+ keyword
。如上例中的category 和 brand
14.3. 更新数据
POST /索引库名/类型名/id值/_update
{
---
}
或者
POST /索引库名/类型名/id值
{
---
}
不同:
POST 操作会对比源文档数据,如果相同不会有什么操作,文档 version 不增加
PUT 操作总会将数据重新保存并增加 version 版本;
带_update 对比元数据如果一样就不进行任何操作。
看场景;
对于大并发更新,不带 update;
对于大并发查询偶尔更新,带 update;对比更新,重新计算分配规则。
---
POST customer/external/1/_update
{
"name": "John Doe"
}
POST customer/external/1
{
"name": "John Doe"
}
更新时增加新属性 需用 doc
POST customer/external/1/_update
{
"doc": {
"name": "Jane Doe",
"age": 20
}
}
1.4.6. 删除数据
删除使用DELETE请求,同样,需要根据id进行删除:
DELETE /索引库名/类型名/id值
DELETE /atguigu/goods/3
返回结果:
{
"_index" : "atguigu",
"_type" : "goods",
"_id" : "3",
"_version" : 2,
"result" : "deleted",
"_shards" : {
"total" : 4,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
14.7. bulk 批量 API
POST /索引库名/类型名/_bulk
{ action: { metadata }}\n
{ request body }\n
{ action: { metadata }}\n
{ request body }\n
action: index, create, delete and update
1、index ,create 下一行需要资源。(已存在的情况下create 会失败,index 则会添加或者替换已有的文档);
{"index": {"_index": 索引名称, "_id": 文档ID1}}
{新增文档1内容}
{"creade": {"_index": 索引名称, "_id": 文档ID1}}
{新增文档1内容}
2、delete 下一行不需要资源;
{"delete": {"_index": 索引名称, "_id": 文档ID1}}
3、update 需要部分文档,upsert and script and its options 需要在下一行指明。
{"update": {"_index": 索引名称, "_id": 文档ID1}}
{"doc": {文档1内容} }
-----------------------------
POST customer/external/_bulk
{"index":{"_id":"1"}}
{"name": "John Doe" }
{"index":{"_id":"2"}}
{"name": "Jane Doe" }
复杂示例:
POST /_bulk
{ "delete": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "create": { "_index": "website", "_type": "blog", "_id": "123" }}
{ "title": "My first blog post" }
{ "index": { "_index": "website", "_type": "blog" }}
{ "title": "My second blog post" }
{ "update": { "_index": "website", "_type": "blog", "_id": "123"} }
{ "doc" : {"title" : "My updated blog post"} }
bulk API 以此按顺序执行所有的 action(动作)。如果一个动作因任何原因而失败,
它将继续处理它后面剩余的动作。当 bulk API 返回时,它将提供每个动作的状态(与发送
的顺序相同),所以您可以检查是否一个指定的动作是不是失败了。
进阶检索
2. SearchAPI
准备数据:
https://gitee.com/tax_yuan_hao/es-learning-json-data/blob/master/json%E6%95%B0%E6%8D%AE.json
ES 支持两种基本方式检索 :
- 一个是通过使用 REST request URI 发送搜索参数(uri+检索参数)
- 另一个是通过使用 REST request body 来发送它们(uri+请求体)
查询指定索引下的所有信息:
GET /{index}/_search
GET /{index}/{type}/{id} 根据id查询:
GET bank/_search 检索 bank 下所有信息,包括 type 和 docs
GET bank/_search?q=*&sort=account_number:asc 请求参数方式检索
elasticsearch作为搜索引擎,最复杂最强大的功能就是搜索查询功能。包括:匹配查询、词条查询、模糊查询、组合查询、范围查询、高亮、排序、分页等等查询功能。
基本查询语法如下:
GET /索引名/_search
{
"query":{
"查询类型":{
"查询条件":"查询条件值"
}
}
}
# 查询bank索引中年龄再10岁到20岁之间的并按降序排序,显示一条数据
GET /bank/_search
{
"query": {
"range": {
"age": {
"gte": 10,
"lte": 20
}
}
},
"sort": [
{
"age": {
"order": "desc"
}
}
],
"size": 1
}
返回结果:
{
"took" : 5,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 44,
"relation" : "eq"
},
"max_score" : null,
"hits" : [
{
"_index" : "bank",
"_type" : "account",
"_id" : "157",
"_score" : null,
"_source" : {
"account_number" : 157,
"balance" : 39868,
"firstname" : "Claudia",
"lastname" : "Terry",
"age" : 20,
"gender" : "F",
"address" : "132 Gunnison Court",
"employer" : "Lumbrex",
"email" : "claudiaterry@lumbrex.com",
"city" : "Castleton",
"state" : "MD"
},
"sort" : [
20
]
}
]
}
}
这里的query代表一个查询对象,里面可以有不同的查询属性
- 查询类型: 例如:
match_all
,match
,term
,range
等等 - 查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解
查询结果:
-
took
:查询花费时间,单位是毫秒 -
time_out
:是否超时 -
_shards
:分片信息 -
hits
:搜索结果总览对象-
total
:搜索到的总条数 -
max_score
:所有结果中文档得分的最高分 -
hits
:搜索结果的文档对象数组,每个元素是一条搜索到的文档信息-
_index
:索引库 -
_type
:文档类型 -
_id
:文档id -
_score
:文档得分 -
_source
:文档的源数据
-
-
2.1. Query DSL
POST /atguigu/goods/_bulk
{"index":{"_id":1}}
{ "title":"小米手机", "images":"http://image.jd.com/12479122.jpg", "price":1999, "stock": 200, "attr": { "category": "手机", "brand": "小米" } }
{"index":{"_id":2}}
{"title":"超米手机", "images":"http://image.jd.com/12479122.jpg", "price":2999, "stock": 300, "attr": { "category": "手机", "brand": "小米" } }
{"index":{"_id":3}}
{ "title":"小米电视", "images":"http://image.jd.com/12479122.jpg", "price":3999, "stock": 400, "attr": { "category": "电视", "brand": "小米" } }
{"index":{"_id":4}}
{ "title":"小米笔记本", "images":"http://image.jd.com/12479122.jpg", "price":4999, "stock": 200, "attr": { "category": "笔记本", "brand": "小米" } }
{"index":{"_id":5}}
{ "title":"华为手机", "images":"http://image.jd.com/12479122.jpg", "price":3999, "stock": 400, "attr": { "category": "手机", "brand": "华为" } }
{"index":{"_id":6}}
{ "title":"华为笔记本", "images":"http://image.jd.com/12479122.jpg", "price":5999, "stock": 200, "attr": { "category": "笔记本", "brand": "华为" } }
{"index":{"_id":7}}
{ "title":"荣耀手机", "images":"http://image.jd.com/12479122.jpg", "price":2999, "stock": 300, "attr": { "category": "手机", "brand": "华为" } }
{"index":{"_id":8}}
{ "title":"oppo手机", "images":"http://image.jd.com/12479122.jpg", "price":2799, "stock": 400, "attr": { "category": "手机", "brand": "oppo" } }
{"index":{"_id":9}}
{ "title":"vivo手机", "images":"http://image.jd.com/12479122.jpg", "price":2699, "stock": 300, "attr": { "category": "手机", "brand": "vivo" } }
{"index":{"_id":10}}
{ "title":"华为nova手机", "images":"http://image.jd.com/12479122.jpg", "price":2999, "stock": 300, "attr": { "category": "手机", "brand": "华为" } }
2.2. 匹配查询(match)
GET /atguigu/_search
{
"query":{
"match_all": {}
}
}
-
query
:代表查询对象 -
match_all
:代表查询所有
GET /atguigu/_search
{
"query": {
"match": {
"title": "小米手机"
}
}
}
查询出很多数据,不仅包括小米手机
,而且与小米
或者手机
相关的都会查询到,es会把数据类型为text的数据进行分词查询,但某些情况下,我们需要更精确查找,可以这样做字段名.keyword,es就不会进行分词
或者使用match_phrase
将需要匹配的值当成一个整体单词(不分词)进行检索
GET /atguigu/_search
{
"query": {
"match": {
"title.keyword": "小米手机"
}
}
}
=========或者
GET /atguigu/_search
{
"query": {
"match_phrase": {
"title": "小米手机"
}
}
}
返回结果:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 1.9924302,
"hits" : [
{
"_index" : "atguigu",
"_type" : "_doc",
"_id" : "1",
"_score" : 1.9924302,
"_source" : {
"title" : "小米手机",
"images" : "http://image.jd.com/12479122.jpg",
"price" : 1999,
"stock" : 200,
"attr" : {
"category" : "手机",
"brand" : "小米"
}
}
}
]
}
}
## 子属性匹配
GET /atguigu/_search
{
"query": {
"match": {
"attr.brand": "小米"
}
}
}
match
只能根据一个字段匹配查询,如果要根据多个字段匹配查询可以使用multi_match
## 查询title中包含小米,或attr的子属性brand为小米的所有信息
GET /atguigu/_search
{
"query":{
"multi_match": {
"query": "小米",
"fields": ["title", "attr.brand.keyword"]
}
}
}
2.3. 词条查询(term)
term
查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字符串。
GET /atguigu/_search
{
"query":{
"term":{
"price": 4999
}
}
}
2.4. 范围查询(range)
range
查询找出那些落在指定区间内的数字或者时间
GET /atguigu/_search
{
"query":{
"range": {
"price": {
"gte": 1000,
"lt": 3000
}
}
}
}
range
查询允许以下字符:
操作符 | 说明 |
---|---|
gt | 大于 |
gte | 大于等于 |
lt | 小于 |
lte | 小于等于 |
2.5. 布尔组合(bool)
布尔查询又叫组合查询
bool
把各种其它查询通过must
(与)、must_not
(非)、should
(或)的方式进行组合
- must:必须达到 must 列举的所有条件
- must_not 必须不是指定的情况
- should:应该达到 should 列举的条件,如果达到会增加相关文档的评分,并不会改变
查询的结果。如果 query 中只有 should 且只有一种匹配规则,那么 should 的条件就会
被作为默认匹配条件而去改变查询结果
注意:
一个组合查询里面只能出现一种组合,不能混用
## 查询价格在1000-3000 且在2000 到4000范围内的数据
GET /atguigu/_search
{
"query":{
"bool":{
"must": [
{
"range": {
"price": {
"gte": 1000,
"lte": 3000
}
}
},
{
"range": {
"price": {
"gte": 2000,
"lte": 4000
}
}
}
]
}
}
}
2.6. 过滤(filter)
所有的查询都会影响到文档的评分及排名。如果我们需要在查询结果中进行过滤,并且不希望过滤条件影响评分,那么就不要把过滤条件作为查询条件来用。而是使用filter
方式:
GET /atguigu/_search
{
"query": {
"bool": {
"must": {
"match": { "title": "小米手机" }
},
"filter": {
"range": {
"price": { "gt": 2000, "lt": 3000 }
}
}
}
}
}
2.7. 排序(sort)
sort
可以让我们按照不同的字段进行排序,并且通过order
指定排序的方式
GET /atguigu/_search
{
"query": {
"match": {
"title": "小米手机"
}
},
"sort": [
{
"price": { "order": "desc" }
},
{
"_score": { "order": "desc"}
}
]
}
2.8. 分页(from/size)
GET /atguigu/_search
{
"query": {
"match": {
"title": "小米手机"
}
},
"from": 2,
"size": 2
}
from
:从那一条开始
size
:取多少条
2.10. 结果过滤(_source)
默认情况下,elasticsearch在搜索的结果中,会把文档中保存在_source
的所有字段都返回。
如果我们只想获取其中的部分字段,可以添加_source
的过滤
GET /atguigu/_search
{
"_source": ["title","price"],
"query": {
"term": {
"price": 2699
}
}
}
返回结果:
{
"took" : 9,
"timed_out" : false,
"_shards" : {
"total" : 2,
"successful" : 2,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 1,
"max_score" : 1.0,
"hits" : [
{
"_index" : "atguigu",
"_type" : "goods",
"_id" : "9",
"_score" : 1.0,
"_source" : {
"price" : 2699,
"title" : "vivo手机"
}
}
]
}
}
3. 聚合(aggregations)
聚合可以让我们极其方便的实现对数据的统计、分析。例如:
- 什么品牌的手机最受欢迎?
- 这些手机的平均价格、最高价格、最低价格?
- 这些手机每月的销售情况如何?
实现这些统计功能的比数据库的sql要方便的多,而且查询速度非常快,可以实现实时搜索效果。
3.1 基本概念
Elasticsearch中的聚合,包含多种类型,最常用的两种,一个叫桶
,一个叫度量
:
桶(bucket)
桶的作用,是按照某种方式对数据进行分组,每一组数据在ES中称为一个
桶
,例如我们根据国籍对人划分,可以得到中国桶
、英国桶
,日本桶
……或者我们按照年龄段对人进行划分:010,1020,2030,3040等。
Elasticsearch中提供的划分桶的方式有很多:
- Date Histogram Aggregation:根据日期阶梯分组,例如给定阶梯为周,会自动每周分为一组
- Histogram Aggregation:根据数值阶梯分组,与日期类似
- Terms Aggregation:根据词条内容分组,词条内容完全匹配的为一组
- Range Aggregation:数值和日期的范围分组,指定开始和结束,然后按段分组
- ……
bucket aggregations 只负责对数据进行分组,并不进行计算,因此往往bucket中往往会嵌套另一种聚合:metrics aggregations即度量
度量(metrics)
分组完成以后,我们一般会对组中的数据进行聚合运算,例如求平均值、最大、最小、求和等,这些在ES中称为
度量
比较常用的一些度量聚合方式:
- Avg Aggregation:求平均值
- Max Aggregation:求最大值
- Min Aggregation:求最小值
- Percentiles Aggregation:求百分比
- Stats Aggregation:同时返回avg、max、min、sum、count等
- Sum Aggregation:求和
- Top hits Aggregation:求前几
- Value Count Aggregation:求总数
- ……
3.2 聚合为桶
首先,我们按照手机的品牌attr.brand.keyword
来划分桶
GET /atguigu/_search
{
"size" : 0,
"aggs" : {
"brands" : {
"terms" : {
"field" : "attr.brand.keyword"
}
}
}
}
-
size
: 查询条数,这里设置为0,因为我们不关心搜索到的数据,只关心聚合结果,提高效率 -
aggs
:声明这是一个聚合查询,是aggregations的缩写-
brands
:给这次聚合起一个名字,任意。-
terms
:划分桶的方式,这里是根据词条划分-
field
:划分桶的字段
-
-
-
### 结果:
{
"took" : 124,
"timed_out" : false,
"_shards" : {
"total" : 2,
"successful" : 2,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : 10,
"max_score" : 0.0,
"hits" : [ ]
},
"aggregations" : {
"brands" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "华为",
"doc_count" : 4
},
{
"key" : "小米",
"doc_count" : 4
},
{
"key" : "oppo",
"doc_count" : 1
},
{
"key" : "vivo",
"doc_count" : 1
}
]
}
}
}
-
hits
:查询结果为空,因为我们设置了size为0 -
aggregations
:聚合的结果 -
brands
:我们定义的聚合名称 -
buckets
:查找到的桶,每个不同的品牌字段值都会形成一个桶-
key
:这个桶对应的品牌字段的值 -
doc_count
:这个桶中的文档数量
-
3.3 桶内度量
前面的例子告诉我们每个桶里面的文档数量,这很有用。 但通常,我们的应用需要提供更复杂的文档度量。 例如,每种品牌手机的平均价格是多少?
因此,我们需要告诉Elasticsearch使用哪个字段
,使用何种度量方式
进行运算,这些信息要嵌套在桶
内,度量
的运算会基于桶
内的文档进行
现在,我们为刚刚的聚合结果添加 求价格平均值的度量:
## 统计attr中的(brand)品牌有多少种,再求各品牌商品的平均价格
GET /atguigu/_search
{
"aggs": {
"brands": {
"terms": {
"field": "attr.brand.keyword"
},
"aggs": {
"price_avg": {
"avg": {
"field": "price"
}
}
}
}
},
"size": 0
}
-
aggs
:我们在上一个aggs(brands)中添加新的aggs。可见度量
也是一个聚合 -
price_avg
:聚合的名称 -
avg
:度量的类型,这里是求平均值 -
field
:度量运算的字段
返回结果:
{
"took" : 19,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"brand_count" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "华为",
"doc_count" : 4,
"price_avg" : {
"value" : 3999.0
}
},
{
"key" : "小米",
"doc_count" : 4,
"price_avg" : {
"value" : 3499.0
}
},
{
"key" : "oppo",
"doc_count" : 1,
"price_avg" : {
"value" : 2799.0
}
},
{
"key" : "vivo",
"doc_count" : 1,
"price_avg" : {
"value" : 2699.0
}
}
]
}
}
}
可以看到每个桶中都有自己的price_avg
字段,这是度量聚合的结果
3.4 桶内嵌套桶
刚刚的案例中,我们在桶内嵌套度量运算。事实上桶不仅可以嵌套运算, 还可以再嵌套其它桶。也就是说在每个分组中,再分更多组。
比如:我们想统计每个品牌都生产了那些产品,按照attr.category.keyword
字段再进行分桶
## 统计attr中的(brand)品牌有多少种,再求各品牌商品的平均价格,且统计各品牌中商品的类别(category)
GET /atguigu/_search
{
"size" : 0,
"aggs" : {
"brands" : {
"terms" : {
"field" : "attr.brand.keyword"
},
"aggs":{
"avg_price": {
"avg": {
"field": "price"
}
},
"categorys": {
"terms": {
"field": "attr.category.keyword"
}
}
}
}
}
}
=======================================
##返回结果:
{
"took" : 0,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 10,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"brands" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "华为",
"doc_count" : 4,
"categorys" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "手机",
"doc_count" : 3
},
{
"key" : "笔记本",
"doc_count" : 1
}
]
},
"avg_price" : {
"value" : 3999.0
}
},
{
"key" : "小米",
"doc_count" : 4,
"categorys" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "手机",
"doc_count" : 2
},
{
"key" : "电视",
"doc_count" : 1
},
{
"key" : "笔记本",
"doc_count" : 1
}
]
},
"avg_price" : {
"value" : 3499.0
}
},
{
"key" : "oppo",
"doc_count" : 1,
"categorys" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "手机",
"doc_count" : 1
}
]
},
"avg_price" : {
"value" : 2799.0
}
},
{
"key" : "vivo",
"doc_count" : 1,
"categorys" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "手机",
"doc_count" : 1
}
]
},
"avg_price" : {
"value" : 2699.0
}
}
]
}
}
}
- 我们可以看到,新的聚合
categorys
被嵌套在原来每一个brands
的桶中。 - 每个品牌下面都根据
attr.category.keyword
字段进行了分组 - 我们能读取到的信息:
- 华为有4中产品
- 华为产品的平均售价是 3999.0美元。
- 其中3种手机产品,1种笔记本产品
练习:
1、搜索 address 中包含 mill 的所有人的年龄分布以及平均年龄,但不显示这些人的详情
GET /bank/_search
{
"query": {
"match": {
"address": "mill"
}
},
"aggs": {
"group_by_age": {
"terms": {
"field": "age"
}
},
"avg_age":{
"avg": {
"field": "age"
}
}
},
"size": 0
}
===========
返回结果:
{
"took" : 1,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 4,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"avg_age" : {
"value" : 34.0
},
"group_by_age" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : 38,
"doc_count" : 2
},
{
"key" : 28,
"doc_count" : 1
},
{
"key" : 32,
"doc_count" : 1
}
]
}
}
}
2、按照年龄聚合,并且请求这些年龄段的这些人的平均薪资
GET /bank/_search
{
"aggs": {
"group_by_age": {
"terms": {
"field": "age",
"size": 1 #表示只返回一条数据
},
"aggs": {
"avg_balance": {
"avg": {
"field": "balance"
}
}
}
}
},
"size": 0
}
========
返回结果:
{
"took" : 7,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_age" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 939,
"buckets" : [
{
"key" : 31,
"doc_count" : 61,
"avg_balance" : {
"value" : 28312.918032786885
}
}
]
}
}
}
3、查出所有年龄分布,并且这些年龄段中 男性 的平均薪资和 女性 的平均薪资以及这个年龄段的总体平均薪资
GET /bank/_search
{
"aggs": {
"group_by_age": {
"terms": {
"field": "age",
"size": 1
},
"aggs": {
"gender_agg": {
"terms": {
"field": "gender.keyword"
},
"aggs": {
"avg_balance": {
"avg": {
"field": "balance"
}
}
}
}
}
},
"avg_balance": {
"avg": {
"field": "balance"
}
}
},
"size": 0
}
================
返回结果:
{
"took" : 4,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1000,
"relation" : "eq"
},
"max_score" : null,
"hits" : [ ]
},
"aggregations" : {
"group_by_age" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 939,
"buckets" : [
{
"key" : 31,
"doc_count" : 61,
"gender_agg" : {
"doc_count_error_upper_bound" : 0,
"sum_other_doc_count" : 0,
"buckets" : [
{
"key" : "M",
"doc_count" : 35,
"avg_balance" : {
"value" : 29565.628571428573
}
},
{
"key" : "F",
"doc_count" : 26,
"avg_balance" : {
"value" : 26626.576923076922
}
}
]
}
}
]
},
"avg_balance" : {
"value" : 25714.837
}
}
}
分词
一个 tokenizer
(分词器)接收一个字符流,将之分割为独立的 tokens
(词元,通常是独立的单词),然后输出 tokens
流。
例如,whitespace tokenizer
遇到空白字符时分割文本。它会将文本 “Quick brown fox!” 分割为 [Quick, brown, fox!]。该 tokenizer
(分词器)还负责记录各个 term
(词条)的顺序或 position
位置(用于 phrase
短
语和 word proximity
词近邻查询),以及 term
(词条)所代表的原始 word
(单词)的 start
(起始)和 end
(结束)的 character offsets
(字符偏移量)(用于高亮显示搜索的内容)。
Elasticsearch 提供了很多内置的分词器,可以用来构建 custom analyzers(自定义分词器)。
安装 ik 分词器
注意:不能用默认 elasticsearch-plugin install xxx.zip 进行自动安装
下载 es 版本相同的ik版本安装
- 进入 es 的 plugins 目录 安装ES时设置了文件挂载所以在进入外部的plugins目录上传下载的zip就行
- unzip 解压下载的文件
- rm –rf *.zip
解压完后一定要删除压缩包 不然ES不能正常启动
确认是否安装好了分词器:
进入 es 容器内部 plugins 目录
- docker exec -it 容器 id /bin/bash
- cd …/bin
- elasticsearch plugin list:即可列出系统的分词器
测试分词器
1、默认分词器
POST _analyze
{
"text": "我是中国人"
}
返回结果:
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "<IDEOGRAPHIC>",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "<IDEOGRAPHIC>",
"position" : 1
},
{
"token" : "中",
"start_offset" : 2,
"end_offset" : 3,
"type" : "<IDEOGRAPHIC>",
"position" : 2
},
{
"token" : "国",
"start_offset" : 3,
"end_offset" : 4,
"type" : "<IDEOGRAPHIC>",
"position" : 3
},
{
"token" : "人",
"start_offset" : 4,
"end_offset" : 5,
"type" : "<IDEOGRAPHIC>",
"position" : 4
}
]
}
2、使用ik_smart分词器
POST _analyze
{
"analyzer": "ik_smart",
"text": "我是中国人"
}
返回结果:
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
}
]
}
3、使用ik_max_word分词器
POST _analyze
{
"analyzer": "ik_max_word",
"text": "我是中国人"
}
返回结果:
{
"tokens" : [
{
"token" : "我",
"start_offset" : 0,
"end_offset" : 1,
"type" : "CN_CHAR",
"position" : 0
},
{
"token" : "是",
"start_offset" : 1,
"end_offset" : 2,
"type" : "CN_CHAR",
"position" : 1
},
{
"token" : "中国人",
"start_offset" : 2,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "中国",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "国人",
"start_offset" : 3,
"end_offset" : 5,
"type" : "CN_WORD",
"position" : 4
}
]
}
根据返回结果可以看出,默认的分词器对中文分词不友好,只是把每次字单独拆分,还有写词语没有分出来
4. SpringData-Elasticsearch
目前市面上有两类客户端:一类是TransportClient
为代表的ES原生客户端,不能执行原生dsl语句必须使用它的Java api方法。另外一种是以Rest Api
为主的missing client
,最典型的就是jest
。 这种客户端可以直接使用dsl语句拼成的字符串,直接传给服务端,然后返回json字符串再解析。两种方式各有优劣,但是最近elasticsearch官网,宣布计划在7.0以后的版本中废除TransportClient。以RestClient为主。由于原生的Elasticsearch客户端API非常麻烦。所以这里直接学习Spring提供的套件:Spring Data Elasticsearch。
注:spring-data-Elasticsearch 使用之前,必须先确定版本,elasticsearch 对版本的要求比较高。
4.1. 创建module
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.4.2</version>
</dependency>
配置es客户端
@Bean
public RestHighLevelClient esRestConfig(){
RestHighLevelClient client = new RestHighLevelClient(
RestClient.builder(
new HttpHost("192.168.126.129", 9200, "http")));
return client;
}
配置请求
public static final RequestOptions COMMON_OPTIONS;
static {
RequestOptions.Builder builder = RequestOptions.DEFAULT.toBuilder();
// builder.addHeader("Authorization", "Bearer " + TOKEN);Bearer
// builder.setHttpAsyncResponseConsumerFactory(
// new HttpAsyncResponseConsumerFactory
// .HeapBufferedResponseConsumerFactory(30 * 1024 * 1024 * 1024));
COMMON_OPTIONS = builder.build();
}
简单测试 ,新增索引
@Autowired
private RestHighLevelClient restClient;
@Test
public void testResElastic() throws IOException {
//创建索引请求
IndexRequest request = new IndexRequest("user");
User user = new User();
user.setAge("18");
user.setName("好");
user.setSex("男");
//往请求中加入json数据
request.source(JSONObject.toJSONString(user), XContentType.JSON);
//发送给es创建
IndexResponse index = restClient.index(request, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println(index);
}
复杂测试:
@Test
public void searchData() throws IOException {
//创建索引请求
SearchRequest searchRequest = new SearchRequest("bank");
//DS构造器
SearchSourceBuilder searchBuilder = new SearchSourceBuilder();
searchBuilder.query(QueryBuilders.matchQuery("address","mill"));
//构建聚合
searchBuilder.aggregation(AggregationBuilders.terms("group_by_age").field("age"));
searchBuilder.aggregation(AggregationBuilders.avg("avg_age").field("age"));
//指定DSL,检索条件
searchRequest.source(searchBuilder);
System.out.println("builder:"+searchRequest);
//执行索引
SearchResponse search = restClient.search(searchRequest, GulimallElasticSearchConfig.COMMON_OPTIONS);
System.out.println("返回结果:"+search.toString());
//解析数据
SearchHits hits = search.getHits();
for (SearchHit hit : hits) {
String sourceAsString = hit.getSourceAsString();
count count = JSONObject.parseObject(sourceAsString, count.class);
System.out.println("数据:"+count);
}
//解析聚合数据
Aggregations aggregations = search.getAggregations();
System.out.println("001"+aggregations);
Terms terms = aggregations.get("group_by_age");
for (Terms.Bucket bucket : terms.getBuckets()) {
System.out.println("年龄:"+bucket.getKey()+"===共:"+bucket.getDocCount());
}
}
es语句
GET bank/_search
{
"query": {
"match": {
"address": {
"query": "mill"
}
}
},
"aggs": {
"group_by_age": {
"terms": {
"field": "age",
"size": 10
}
},
"avg_age": {
"avg": {
"field": "age"
}
}
}
}