ElasticSearch

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

    1. 源自搜索引擎,侧重搜索与全文检索。
    2. 数据规模从几百万到千万不等,数据量过亿的集群特别少。
      有可能存在个别系统数据量过亿,但这并不是普遍现象(就像Oracle的表里的数据规模有可能超过Hive里一样,但需要小型机)。
  • Hermes

    1. 一个基于大索引技术的海量数据实时检索分析平台。侧重数据分析。
    2. 数据规模从几亿到万亿不等。最小的表也是千万级别。
      在 腾讯17 台TS5机器,就可以处理每天450亿的数据(每条数据1kb左右),数据可以保存一个月之久。
  • Solr、ES区别
    全文检索、搜索、分析。基于lucene

    1. Solr 利用 Zookeeper 进行分布式管理,而 Elasticsearch 自身带有分布式协调管理功能;
    2. Solr 支持更多格式的数据,而 Elasticsearch 仅支持json文件格式;
    3. Solr 官方提供的功能更多,而 Elasticsearch 本身更注重于核心功能,高级功能多有第三方插件提供;
    4. 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_allmatchtermrange 等等
  • 查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解

查询结果:

  • 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客户端
ElasticSearch

  @Bean
    public RestHighLevelClient esRestConfig(){
        RestHighLevelClient client = new RestHighLevelClient(
                RestClient.builder(
                new HttpHost("192.168.126.129", 9200, "http")));
        return client;
    }

配置请求
ElasticSearch

    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"
			}
		}
	}
}
上一篇:nrf58122蓝牙芯片ble_app_proximity程序总结


下一篇:Appium环境搭建-精简版