文章目录

第 3 章 搜索解决方案 - elasticsearch

学习目标:

理解 elasticsearch 索引结构和数据类型,掌握 IK 分词器的使用
掌握索引的常用操作(使用 Kibana 工具)
掌握 javaRest 高级 api
完成数据批量导入
项目序列 - 11:https://github.com/Jonekaka/javaweb-qingcheng-11-86

1 走进 ElasticSearch

1.1 全文检索

1.1.1 为什么要使用全文检索

用户访问我们的首页,一般都会直接搜索来寻找自己想要购买的商品。
需要保证迅速而正确,但是传统数据库数目众多而无分词功能,

常见的全文检索技术有 Lucene、solr 、elasticsearch 等。

1.1.2 理解索引结构

索引是全文检索的核心与原理

下图是索引结构,就像一本书的目录一样
下边是物理结构,上边是逻辑结构

逻辑结构部分是一个倒排索引表:
1、将要搜索的文档内容分词,所有不重复的词组成分词列表。
2、将搜索的文档最终以 Document 方式存储起来。
3、每个词和 docment 都有关联。
如下:

现在,如果我们想搜索 quick brown ,我们只需要查找包含每个词条的文档:

两个文档都匹配,但是第一个文档比第二个匹配度更高。如果我们使用仅计算匹配
词条数量的简单 相似性算法 ,那么,我们可以说,对于我们查询的相关性来讲,第一个
文档比第二个文档更佳。

1.2 Elasticsearch

1.2.1 Elasticsearch 简介

ElasticSearch 是一个基于 Lucene 的搜索服务器。它提供了一个分布式多用户能力的全文搜索引擎,基于 RESTful web 接口。Elasticsearch 是用 Java 开发的,并作为 Apache 许可条款下的开放源码发布,是当前流行的企业级搜索引擎。设计用于云计算中,能够达到实时搜索,稳定,可靠,快速,安装使用方便。

我们建立一个网站或应用程序,并要添加搜索功能,但是想要完成搜索工作的创建是非常困难的。我们希望搜索解决方案要运行速度快,我们希望能有一个零配置和一个完全免费的搜索模式,我们希望能够简单地使用 JSON 通过 HTTP 来索引数据,我们希望我们的搜索服务器始终可用,我们希望能够从一台开始并扩展到数百台,我们要实时搜索,我们要简单的多租户,我们希望建立一个云的解决方案。因此我们利用 Elasticsearch 来解决所有这些问题及可能出现的更多其它问题。
官方网址:https://www.elastic.co/cn/products/elasticsearch
Github:https://github.com/elastic/elasticsearch
优点:
(1)可以作为一个大型分布式集群(数百台服务器)技术,处理 PB 级数据,服务大公
司;也可以运行在单机上
(2)将全文检索、数据分析以及分布式技术,合并在了一起,才形成了独一无二的 ES;
(3)开箱即用的,部署简单
(4)全文检索,同义词处理,相关度排名,复杂数据分析,海量数据的近实时处理
下表是 Elasticsearch 与 MySQL 数据库逻辑结构概念的对比
可以增删改查但是具备 nosql 的特性

1.2.2 安装与启动

下载 ElasticSearch 6.5.2 版本
https://www.elastic.co/downloads/past-releases/elasticsearch-6.5.2
无需安装,解压安装包后即可使用

配置,依赖包,日志,模块,插件
在命令提示符下,进入 ElasticSearch 安装目录下的 bin 目录, 执行命令

即可启动。
我们打开浏览器,在地址栏输入 http://127.0.0.1:9200/ 即可看到输出结果
版本 “number” : “6.5.2”, 所以来的 lucence 版本 “lucene_version” : “7.5.0”,

{
  "name" : "DpUGo1b",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "1KYrJiyjRX2EjnS4y0157A",
  "version" : {
    "number" : "6.5.2",
    "build_flavor" : "default",
    "build_type" : "zip",
    "build_hash" : "9434bed",
    "build_date" : "2018-11-29T23:58:20.891072Z",
    "build_snapshot" : false,
    "lucene_version" : "7.5.0",
    "minimum_wire_compatibility_version" : "5.6.0",
    "minimum_index_compatibility_version" : "5.0.0"
  },
  "tagline" : "You Know, for Search"
}
 

1.3 使用 Postman 操作索引库

elasticsearch 提供了很多接口
可以在不建立结构的情况下假如数据,nosql, 添加数据后自动创建类型,结构,索引

1.3.1 新建文档

/testindex 索引如果存在就查询,不存在就创建
/doc 索引下的类型,不存在则创建
使用 postman 测试:以 post 方式提交 http://127.0.0.1:9200/testindex/doc
body:

{
        "name":"测试商品",
        "price":123
        }
 

返回结果如下:
索引建立,类型建立,id 自动创建,版本建立(如果修改则自动增加),结果类型:创建

{
    "_index": "testindex",
    "_type": "doc",
    "_id": "HIFYSnkBFyCwHiSwwdpJ",
    "_version": 1,
    "result": "created",
    "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
    },
    "_seq_no": 0,
    "_primary_term": 1
}
 

_id 是由系统自动生成的。 为了方便之后的演示,我们再次录入几条测试数据。
输入数据如下:

{
        "name":"苹果手机",
        "price":789
        }
 
{
        "name":"华为手机",
        "price":456
        }
 
{
        "name":"苹果电脑",
        "price":1000
        }
 

1.3.2 查询文档

查询某索引某类型的全部数据,以 get 方式请求
http://127.0.0.1:9200/testindex/doc/_search 返回结果如下:

“total”: 5, 总数
“max_score”: 1, 最大分值,那个数据与期望匹配程度最高
“_score”: 1, 代表搜索的匹配程度,关键字搜索使用

{
    "took": 10,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
    },
    "hits": {
        "total": 5,
        "max_score": 1,
        "hits": [
            {
                "_index": "testindex",
                "_type": "doc",
                "_id": "HoFbSnkBFyCwHiSwHdq7",
                "_score": 1,
                "_source": {
                    "name": "苹果手机",
                    "price": 789
                }
            },
            {
                "_index": "testindex",
                "_type": "doc",
                "_id": "HIFYSnkBFyCwHiSwwdpJ",
                "_score": 1,
                "_source": {
                    "name": "测试商品",
                    "price": 123
                }
            },
            {
                "_index": "testindex",
                "_type": "doc",
                "_id": "IIFfSnkBFyCwHiSwaNpJ",
                "_score": 1,
                "_source": {
                    "name": "小米电脑",
                    "price": 1111
                }
            },
            {
                "_index": "testindex",
                "_type": "doc",
                "_id": "HYFaSnkBFyCwHiSw4Nrz",
                "_score": 1,
                "_source": {
                    "name": "华为手机",
                    "price": 456
                }
            },
            {
                "_index": "testindex",
                "_type": "doc",
                "_id": "H4FcSnkBFyCwHiSwHNrT",
                "_score": 1,
                "_source": {
                    "name": "苹果电脑",
                    "price": 1000
                }
            }
        ]
    }
}
 

1.4 映射与数据类型

映射 (Mapping) 相当于数据表的表结构。
ElasticSearch 中的映射(Mapping)用来定义一个文档,可以定义所包含的字段以及字段的类型、分词器及属性等等。
映射可以分为动态映射和静态映射。
动态映射 (dynamic mapping):在关系数据库中,需要事先创建数据库,然后在该数据库实例下创建数据表,然后才能在该数据表中插入数据。而 ElasticSearch 中不需要事先定义映射(Mapping),文档写入 ElasticSearch 时,会根据文档字段自动识别类型,这种机制称之为动态映射。
静态映射 :在 ElasticSearch 中也可以事先定义好映射,包含文档的各个字段及其类型等,这种方式称之为静态映射。
常用类型如下:

1.4.1 字符串类型

两者都是 用来存储文本的,但是作用不同
text 需要被分析,而 keyword 已经属于原子类型

1.4.2 整数类型

1.4.3 浮点类型

1.4.4 date 类型

日期类型表示格式可以是以下几种:
(1)日期格式的字符串,比如 “2018-01-13” 或 “2018-01-13 12:10:30”
(2)long 类型的毫秒数 ( milliseconds-since-the-epoch,epoch 就是指 UNIX 诞生的 UTC
时间 1970 年 1 月 1 日 0 时 0 分 0 秒)
(3)integer 的秒数 (seconds-since-the-epoch)

1.4.5 boolean 类型

逻辑类型(布尔类型)可以接受 true/false

1.4.6 binary 类型

二进制字段是指用 base64 来表示索引中存储的二进制数据,可用来存储二进制形式
的数据,例如图像。默认情况下,该类型的字段只存储不索引。二进制类型只支持
index_name 属性。

1.4.7 array 类型

在 ElasticSearch 中,没有专门的数组(Array)数据类型,但是,在默认情况下,任
意一个字段都可以包含 0 或多个值,这意味着每个字段默认都是数组类型,只不过,数组
类型的各个元素值的数据类型必须相同。在 ElasticSearch 中,数组是开箱即用的(out of
box),不需要进行任何配置,就可以直接使用。
在同一个数组中,数组元素的数据类型是相同的,ElasticSearch 不支持元素为多个
数据类型:[10, “some string” ],常用的数组类型是:
(1)字符数组: [“one”, “two” ]
(2)整数数组: productid:[1, 2]
(3)对象(文档)数组: “user”:[ { “name”: “Mary”, “age”: 12 }, { “name”: “John”, “age”:10 }],ElasticSearch 内部把对象数组展开为 {“user.name”: [“Mary”, “John”], “user.age”:[12,10]}

1.4.8 object 类型

JSON 天生具有层级关系,文档会包含嵌套的对象
比如上次的菜单,可以直接存储为树状 json

1.5 IK 分词器

1.5.1 什么是 IK 分词器

使用 postman 测试 post 方式提交 http://127.0.0.1:9200/testindex/_analyze

{"analyzer": "chinese", "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
        }
    ]
}
 

默认的中文分词是将每个字看成一个词,这显然是不符合要求的,所以我们需要安装中
文分词器来解决这个问题。
IK 分词是一款国人开发的相对简单的中文分词器。虽然开发者自 2012 年之后就不在维护
了,但在工程应用中 IK 算是比较流行的一款!我们今天就介绍一下 IK 中文分词器的使用。

1.5.2 IK 分词器安装

下载地址:https://github.com/medcl/elasticsearch-analysis-ik/releases 下载 6.5.2 版
(1)先将其解压,将解压后的 elasticsearch 文件夹重命名文件夹为 ik
(2)将 ik 文件夹拷贝到 elasticsearch/plugins 目录下。
(3)重新启动,即可加载 IK 分词器

1.5.3 IK 分词器测试

IK 提供了两个分词算法 ik_smart 和 ik_max_word
其中 ik_smart 为最少切分,ik_max_word 为最细粒度划分
我们分别来试一下
(1)最小切分:
使用 postman 测试 post 方式提交 http://127.0.0.1:9200/testindex/_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
        }
    ]
}
 
 

(2)最细切分:
使用 postman 测试 post 方式提交 http://127.0.0.1:9200/testindex/_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
        }
    ]
}
 

1.5.4 自定义词库

我们现在测试 “龙云播客”,结果如下:
{“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": 3,
            "type": "CN_CHAR",
            "position": 2
        },
        {
            "token": "客",
            "start_offset": 3,
            "end_offset": 4,
            "type": "CN_CHAR",
            "position": 3
        }
    ]
}
 

默认的分词并没有识别 “龙云播客” 是一个词。如果我们想让系统识别 “龙云播客” 是一个
词,需要编辑自定义词库。
步骤:
(1)进入 elasticsearch/plugins/ik/config 目录
(2)新建一个 my.dic 文件,格式另存为 UTF-8, 编辑内容:
龙云播客
修改 IKAnalyzer.cfg.xml(在 ik/config 目录下)

<properties>
<comment>IK Analyzer 扩展配置</comment>
<!‐‐用户可以在这里配置自己的扩展字典 ‐‐>
<entry key="ext_dict">my.dic</entry>
<!‐‐用户可以在这里配置自己的扩展停止词字典‐‐>
<entry key="ext_stopwords"></entry>
</properties>
 

重新启动 elasticsearch,通过浏览器测试分词效果

{
    "tokens": [
        {
            "token": "龙云播客",
            "start_offset": 0,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 0
        }
    ]
}
 

1.6 Kibana

1.6.1 Kibana 简介

Kibana 是一个开源的分析和可视化平台,旨在与 Elasticsearch 合作。
Kibana 提供搜索、查看和与存储在 Elasticsearch 索引中的数据进行交互的功能。开发者或运维人员可以轻松地执行高级数据分析,并在各种图表、表格和地图中可视化数据。

1.6.2 Kibana 安装与启动

(1)解压 资料 \ 配套软件 \ elasticsearch\kibana-6.5.2-windows-x86_64.zip
(2)如果 Kibana 远程连接 Elasticsearch ,可以修改 config\kibana.yml
(3)执行 bin\kibana.bat
(4)打开浏览器,键入 http://localhost:5601 访问 Kibana
我们这里使用 Kibana 进行索引操作,Kibana 与 Postman 相比省略了服务地址,并且有语
法提示,非常便捷。

2 索引操作

这里是静态映射,先创建表结构
为什么要静态映射呢?
因为为索引字段可以自定义功能,比如分词器,动态映射不具备此功能

2.1 创建索引与映射字段

语法
请求方式依然是 PUT
类型名称:就是前面将的 type 的概念,类似于数据库中的不同表
字段名:任意命名,可以指定许多属性,来表示字段,如:
type:类型,可以是 text、long、short、date、integer、object 等
index:是否索引,默认为 true,需要排序或者查询可以设置。图片不需要查询,存储就行
store:是否单独存储,默认为 false ,一般内容比较多的字段设置成 true,可提升查询性能
analyzer:指定分词器

PUT /索引库名
        {
        "mappings": {
        "类型名称":{
        "properties": {
        "字段名": {
        "type": "类型",
        "index": true
        "store": true
        "analyzer": "分词器"
        }
        }
        }
        }
 

分析需求:
对商品页分析需要查询的参数

这里的 id 就用 sku 的 id 代替
示例
发起请求:

#创建索引结构
PUT sku
{
  "mappings":{
    "doc":{
      "properties":{
        "name":{
          "type":"text",
          "analyzer":"ik_smart"
        },
        "price":{
          "type":"integer"
        },
        "image":{
          "type":"text"
        },
        "createTime":{
          "type":"date"
        },
        "spuId":{
          "type":"text"
        },
        "categoryName":{
          "type":"keyword"
        },
        "brandName":{
          "type":"object"
        },
        "saleNum":{
          "type":"integer"
        },
        "commentNum":{
          "type":"integer"
        }
      }
    }
  }
}
 

响应结果:

{
        "acknowledged": true,
        "shards_acknowledged": true,
        "index": "sku"
        }
 

2.2 文档增加与修改

2.2.1 增加文档自动生成 ID

通过 POST 请求,可以向一个已经存在的索引库中添加数据。
语法:

POST 索引库名/类型名
        {
        "key":"value"
        }
 
 

示例:

POST sku/doc
        {
        "name":"小米手机",
        "price":1000,
        "spuId":"101",
        "createTime":"2019‐03‐01",
        "categoryName":"手机",
        "brandName":"小米",
        "saleNum":10102,
        "commentNum":1331,
        "spec":{
        "网络制式":"移动4G",
        "屏幕尺寸":"4.5"
        }
        }
 
 

响应:

{
        "_index": "sku",
        "_type": "doc",
        "_id": "hyjXKWkBtgZXp‐‐BshX_",
        "_version": 1,
        "result": "created",
        "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
        },
        "_seq_no": 0,
        "_primary_term": 1
        }
 

通过以下命令查询 sku 索引的数据
GET sku/_search
响应结果如下:

{
        "took": 2,
        "timed_out": false,
        "_shards": {
        "total": 5,
        "successful": 5,
        "skipped": 0,
        "failed": 0
        },
        "hits": {
        "total": 1,
        "max_score": 1,
        "hits": [
        {
        "_index": "sku",
        "_type": "doc",
        "_id": "hyjXKWkBtgZXp‐‐BshX_",
        "_score": 1,
        "_source": {
        "name": "小米手机",
        "price": 1000,
        "spuId": 1010101011,
        "createTime": "2019‐03‐01",
        "categoryName": "手机",
        "brandName": "小米",
        "saleNum": 10102,
        "commentNum": 1331,
        "spec": {
        "网络制式": "移动4G",
        "屏幕尺寸": "4.5"
        }
        }
        }
        ]
        }
        }
 

2.2.2 新增文档指定 ID

如果我们想要自己新增的时候指定 id,可以这么做:
语法

PUT /索引库名/类型/id值
        {
        ...
        }
 

示例:

PUT sku/doc/1
{
  "name": "小米电视",
  "price": 1000,
  "spuId": 1010101011,
  "createTime": "2019‐03‐01",
  "categoryName": "电视",
  "brandName": "小米",
  "saleNum": 10102,
  "commentNum": 1331,
  "spec": {
    "网络制式": "移动4G",
    "屏幕尺寸": "4.5"
  }
}
 

响应:

{
        "_index": "sku",
        "_type": "doc",
        "_id": "1",
        "_version": 1,
        "result": "created",
        "_shards": {
        "total": 2,
        "successful": 1,
        "failed": 0
        },
        "_seq_no": 0,
        "_primary_term": 1
        }
 
 

再次查询,观察返回的结果。

2.3 索引查询

基本语法

GET /索引库名/_search
        {
        "query":{
        "查询类型":{
        "查询条件":"查询条件值"
        }
        }
        }
 

其实修改也一样, 只需要修改其中的某个数据, 自动识别为 update
不写条件, 默认查询全部
这里的 query 代表一个查询对象,里面可以有不同的查询属性
查询类型:
例如: match_all , match , term , range 等等
查询条件:查询条件会根据类型的不同,写法也有差异,后面详细讲解
为了看到清晰的测试效果,我们再录入两条数据:三星手机和三星电视

2.3.1 查询所有数据(match_all)

示例:

#查询所有
GET /sku/_search
{
  "query": {
    "match_all": {}
  }
}
 
 
query :代表查询对象  
match_all :代表查询所有
 
took:查询花费时间,单位是毫秒  
time_out:是否超时  
_shards:分片信息  
hits:搜索结果总览对象  
total:搜索到的总条数  
max_score:所有结果中文档得分的最高分  
hits:搜索结果的文档对象数组,每个元素是一条搜索到的文档信息  
_index:索引库  
_type:文档类型  
_id:文档 id  
_score:文档得分  
_source:文档的源数据
 

2.3.2 匹配查询(match)

示例:查询名称包含手机的记录

GET /sku/doc/_search
{
  "query": {
    "match": {
      "name": "手机"
    }
  }
}
 
 

结果:查询出三星手机和小米手机两条结果

如果查询名称包含电视的,结果为三星电视和小米电视两条结果

如果我们查询 “小米电视” 会有几条记录被查询出来呢?
猜测有一条,但结果为小米电视、三星电视、小米手机三条结果,

这是为什么呢?这是因为在查询时,会先将搜索关键字进行分词,对分词后的字符串进行查询,只要是包含这些字符串的都是要被查询出来的,多个词之间是 or 的关系。
返回的结果中_score 是对这条记录的评分,评分代表这条记录与搜索关键字的匹配度,
查询结果按评分进行降序排序。 比如我们刚才搜索 “小米电视” ,那小米电视这条记录的
评分是最高的,排列在最前面。
往往在查询时用户需要更多的结果, 符合人性化

如果我们需要精确查找,也就是同时包含小米和电视两个词的才可以查询出来,这就需
要将操作改为 and 关系:

GET /sku/doc/_search
{
  "query": {
    "match": {
      "name": {
        "query": "小米电视",
        "operator": "and"
      }
    }
  }
}
 

查询结果为小米电视一条记录了。

2.3.3 多字段查询(multi_match)

multi_match 与 match 类似,不同的是它可以在多个字段中查询

GET /sku/_search
{
  "query": {
    "multi_match": {
      "query": "小米",
      "fields": [
        "name",
        "brandName",
        "categoryName"
      ]
    }
  }
}
 
 

本例中,我们会在 name、brandName 、categoryName 字段中查询 小米 这个词

2.3.4 词条匹配 (term)

term 查询被用于精确值 匹配,这些精确值可能是数字、时间、布尔或者那些未分词的字
符串

GET /sku/_search
{
  "query": {
    "term": {
      "price": 1000
    }
  }
}
 
 

2.3.5 多词条匹配 (terms)

terms 查询和 term 查询一样,但它允许你指定多值进行匹配。如果这个字段包含了指定
值中的任何一个值,那么这个文档满足条件:

GET /sku/_search
{
  "query": {
    "terms": {
      "price": [
        1000,
        2000,
        3000
      ]
    }
  }
}
 

2.3.6 布尔组合(bool)

bool 把各种其它查询通过 must (与)、 must_not (非)、 should (或)的方式进行
组合
示例:查询名称包含手机的,并且品牌为小米的。

GET /sku/_search
{
  "query": {
    "bool": {
      "must": [
        {
          "match": {
            "name": "手机"
          }
        },
        {
          "term": {
            "brandName": "小米"
          }
        }
      ]
    }
  }
}
 
 

示例:查询名称包含手机的,或者品牌为小米的。

GET /sku/_search
{
  "query": {
    "bool": {
      "should": [
        {
          "match": {
            "name": "手机"
          }
        },
        {
          "term": {
            "brandName": "小米"
          }
        }
      ]
    }
  }
}
 

2.3.7 过滤查询

过滤是针对搜索的结果进行过滤,过滤器主要判断的是文档是否匹配,不去计算和判断文档的匹配度得分,所以过滤器性能比查询要高,且方便缓存,推荐尽量使用过滤器去实现查询或者过滤器和查询共同使用。
示例:过滤品牌为小米的记录

GET /sku/_search
{
  "query": {
    "bool": {
      "filter": [
        {
          "match": {
            "brandName": "小米"
          }
        }
      ]
    }
  }
}
 
 

2.3.8 分组查询

示例:按分组名称聚合查询,统计每个分组的数量
aggregation 聚合
terms 查询的类型
sku_category 自定义分组结果集的名称
categoryName 按照其分组

GET /sku/_search
{
  "size": 0,
  "aggs": {
    "sku_category": {
      "terms": {
        "field": "categoryName"
        
      }
    }
  }
}
 
 

size 为 0 不会将数据查询出来,目的是让查询更快。

我们也可以一次查询两种分组统计结果:

#分组查询
GET sku/_search
{
  "size": 0,
  "aggs": {
    "sku_category": {
      "terms": {
        "field": "categoryName"
      }
    },
    "sku_brand": {
      "terms": {
        "field": "brandName"
      }
    }
  }
}
 

3 JavaRest 高级客户端入门

3.1 JavaRest 高级客户端简介

将界面操作, 转化为代码自动操作
elasticsearch 存在三种 Java 客户端。

  1. Transport Client
  2. Java Low Level Rest Client(低级 rest 客户端)
  3. Java High Level REST Client(高级 rest 客户端)
    这三者的区别是:

TransportClient 没有使用 RESTful 风格的接口,而是二进制的方式传输数据。tcp/ip, 废弃

ES 官方推出了 Java Low Level REST Client, 它支持 RESTful。但是缺点是因为 TransportClient 的使用者把代码迁移到 Low Level REST Client 的工作量比较大。
ES 官方推出 Java High Level REST Client, 它是基于 Java Low Level REST Client 的封装,并且 API 接收参数和返回值和 TransportClient 是一样的,使得代码迁移变得容易
并且支持了 RESTful 的风格,兼容了这两种客户端的优点。强烈建议 ES5 及其以后的版本使用 Java High Level REST Client。
准备工作:新建工程,引入依赖

<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch‐rest‐high‐level‐client</artifactId>
<version>6.5.3</version>
</dependency>
 

3.2 快速入门

3.2.1 新增和修改数据

插入单条数据:
HttpHost : url 地址封装
RestClientBuilder: rest 客户端构建器
RestHighLevelClient: rest 高级客户端
IndexRequest: 新增或修改请求
IndexResponse:新增或修改的响应结果
批处理请求:可以 for 循环, 但是需要和服务器多次交互, 没有必要

BulkRequest: 批量请求(用于增删改操作)使用, 和查询区别开
BulkResponse:批量请求(用于增删改操作)

public class Test1 {
    public static void main(String[] args) throws IOException {
        /*连接rest接口*/
        /*获得需要连接的地址*/
        HttpHost http = new HttpHost("localhost", 9200, "http");
        /*创建rest客户端构建器*/
        RestClientBuilder restClientBuilder = RestClient.builder(http);
        /*创建高级客户端rest*/
        RestHighLevelClient client = new RestHighLevelClient(restClientBuilder);
 
        /*封装请求对象*/
        /*索引,文档名,id*/
        BulkRequest bulkRequest=new BulkRequest();
        IndexRequest indexRequest = new IndexRequest("sku", "doc", "100");
        Map skuMap=new HashMap();
        skuMap.put("name","华为mete20 pro");
        skuMap.put("brandName","华为");
        skuMap.put("categoryName","手机");
        skuMap.put("price",1010221);
        skuMap.put("createTime","2019-05-01");
        skuMap.put("saleNum",101021);
        skuMap.put("commentNum",10102321);
        Map spec=new HashMap();
        spec.put("网络制式","移动4G");
        spec.put("屏幕尺寸","5");
        skuMap.put("spec",spec);
        indexRequest.source(skuMap);
        /*获得执行结果*/
        /*有一个默认参数,暂时没用,只是为了以后扩展*/
        bulkRequest.add(indexRequest);
          //IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        BulkResponse bulkResponse = client.bulk(bulkRequest, RequestOptions.DEFAULT);
        int status = bulkResponse.status().getStatus();
        System.out.println(status);
        /*客户端有始有终*/
        client.close();
    }
}
 
 

如果 ID 不存在则新增,如果 ID 存在则修改。

3.2.2 匹配查询

SearchRequest: 查询请求对象
SearchResponse:查询响应对象
SearchSourceBuilder:查询源构建器, 将携带参数的查询请求对象构建, 就是吸收上述请求与相应对象的数据, 构建 json 数据对象
MatchQueryBuilder:匹配查询构建器
示例:查询商品名称包含电视的记录。

public class Test2 {
    public static void main(String[] args) throws IOException {
        /*连接rest接口*/
        /*获得需要连接的地址*/
        HttpHost http = new HttpHost("localhost", 9200, "http");
        /*创建rest客户端构建器*/
        RestClientBuilder restClientBuilder = RestClient.builder(http);
        /*创建高级客户端rest*/
        RestHighLevelClient client = new RestHighLevelClient(restClientBuilder);
 
        /*封装请求对象*/
        /*GET sku/doc/_search
        {
          "query":
          {
            "match":{
              "name":{
                "query":"电视"
              }
 
            }
          }
        }*/
        /*查询请求,get sku*/
        SearchRequest searchRequest = new SearchRequest("sku");
        /*get sku/doc,不写默认查询sku的全部索引*/
        searchRequest.types("doc");
        /*json信息,query,查询源构建器"query":*/
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        /*json,match属性"match":*/
        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", "电视");
        /*match在query内*/
        searchSourceBuilder.query(matchQueryBuilder);
        /*query在打括号内,作为一个完整的请求体,模拟手写json的功能*/
        searchRequest.source(searchSourceBuilder);
        /*使用高级客户端获取查询结果*/
        /*开头的命令/_search,将大括号里面的json装载进入*/
        /*RequestOptions.DEFAULT,只有一个选项,并没有特殊的意义,只是为了以后的扩展*/
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
 
        /*查询结果都在hits里面*/
        /*{
          "took" : 42,
          "timed_out" : false,
          "_shards" : {
            "total" : 5,
            "successful" : 5,
            "skipped" : 0,
            "failed" : 0
          },
          "hits" : {
            "total" : 1,
            "max_score" : 0.5753642,
            "hits" : [
              {
                "_index" : "sku",
                "_type" : "doc",
                "_id" : "1",
                "_score" : 0.5753642,
                "_source" : {
                  "name" : "小米电视",
                  "price" : 1000,
                  "spuId" : 1010101011,
                  "createTime" : "2019‐03‐01",
                  "categoryName" : "电视",
                  "brandName" : "小米",
                  "saleNum" : 10102,
                  "commentNum" : 1331,
                  "spec" : {
                    "网络制式" : "移动4G",
                    "屏幕尺寸" : "4.5"
                  }
                }
              }
            ]
          }
        }*/
        /*然而hits有两层,因此继续寻找*/
        SearchHits searchHits = searchResponse.getHits();
        long totalHits = searchHits.getTotalHits();
        System.out.println("记录数" + totalHits);
        /*得到内容hits列*/
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            String source = hit.getSourceAsString();
            System.out.println(source);
        }
        /*客户端有始有终*/
        client.close();
    }
}
 
 

3.2.3 布尔与词条查询

BoolQueryBuilder:布尔查询构建器
TermQueryBuilder:词条查询构建器
QueryBuilders:查询构建器工厂
示例:查询名称包含手机的,并且品牌为小米的记录

//1.连接rest接口 同上......
//2.封装查询请求
//3.获取查询结果 同上......
public class Test3 {
    public static void main(String[] args) throws IOException {
        /*连接rest接口*/
        /*获得需要连接的地址*/
        HttpHost http = new HttpHost("localhost", 9200, "http");
        /*创建rest客户端构建器*/
        RestClientBuilder restClientBuilder = RestClient.builder(http);
        /*创建高级客户端rest*/
        RestHighLevelClient client = new RestHighLevelClient(restClientBuilder);
        /*GET /sku/_search
        {
          "query": {
            "bool": {
              "must": [
                {
                  "match": {
                    "name": "电视"
                  }
                },
                {
                  "term": {
                    "brandName": "小米"
                  }
                }
              ]
            }
          }
        }*/
        /*封装请求对象*/
        SearchRequest searchRequest = new SearchRequest("sku");
        searchRequest.types("doc");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        /*"bool": {*/
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        /*match": {
            "name": "电视"
          }*/
        MatchQueryBuilder matchQueryBuilder = QueryBuilders.matchQuery("name", "电视");
        /*"must": [
                {
                  "match": {
                    "name": "电视"
                  }
                },*/
        boolQueryBuilder.must(matchQueryBuilder);
        /*term查询,类似*/
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brandName", "小米");
        /*加入布尔,must*/
        boolQueryBuilder.must(termQueryBuilder);
        /*bool加入query*/
        searchSourceBuilder.query(boolQueryBuilder);
        /*query加入search*/
        searchRequest.source(searchSourceBuilder);
        /*获得请求对象*/
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
        SearchHits searchHits = searchResponse.getHits();
        long totalHits = searchHits.getTotalHits();
        System.out.println("记录数" + totalHits);
        /*得到内容hits列*/
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            String source = hit.getSourceAsString();
            System.out.println(source);
        }
        /*客户端有始有终*/
        client.close();
    }
}
 
 

3.2.4 过滤查询

示例:筛选品牌为小米的记录

//1.连接rest接口 同上.....
//2.封装查询请求
 
//设置查询的类型
 
//布尔查询构建器
 
//3.获取查询结果 同上....
 
public class Test4 {
    public static void main(String[] args) throws IOException {
        /*连接rest接口*/
        /*获得需要连接的地址*/
        HttpHost http = new HttpHost("localhost", 9200, "http");
        /*创建rest客户端构建器*/
        RestClientBuilder restClientBuilder = RestClient.builder(http);
        /*创建高级客户端rest*/
        RestHighLevelClient client = new RestHighLevelClient(restClientBuilder);
 
        /*封装请求对象*/
        /*GET sku/_search
        {
          "query":{
            "bool":{
              "filter": [
                {
                  "match":
                  {
                    "brandName":"小米"
                  }
                }
              ]
            }
          }
        }*/
        SearchRequest searchRequest = new SearchRequest("sku");
        searchRequest.types("doc");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
        /*构建match条件*/
        TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("brandName", "小米");
        /*bool的filter吸收match,可以吸收多个*/
        boolQueryBuilder.filter(termQueryBuilder);
        /*query吸收bool*/
        searchSourceBuilder.query(boolQueryBuilder);
        /*_search吸收query*/
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
 
        /*然而hits有两层,因此继续寻找*/
        SearchHits searchHits = searchResponse.getHits();
        long totalHits = searchHits.getTotalHits();
        System.out.println("记录数" + totalHits);
        /*得到内容hits列*/
        SearchHit[] hits = searchHits.getHits();
        for (SearchHit hit : hits) {
            String source = hit.getSourceAsString();
            System.out.println(source);
        }
        /*客户端有始有终*/
        client.close();
    }
}
 

3.2.5 分组(聚合)查询

AggregationBuilders:聚合构建器工厂
TermsAggregationBuilder:词条聚合构建器
Aggregations:分组结果封装
Terms.Bucket: 桶
示例:按商品分类分组查询,求出每个分类的文档数
其实就是模拟 json, 得到数据 json, 再从 json 中提取值

//rest构建器
     //高级客户端对象
//2.封装查询请求
        //设置查询的类型
       
//3.获取查询结果
       public class Test5 {
    public static void main(String[] args) throws IOException {
        /*连接rest接口*/
        /*获得需要连接的地址*/
        HttpHost http = new HttpHost("localhost", 9200, "http");
        /*创建rest客户端构建器*/
        RestClientBuilder restClientBuilder = RestClient.builder(http);
        /*创建高级客户端rest*/
        RestHighLevelClient client = new RestHighLevelClient(restClientBuilder);
 
        /*封装请求对象*/
        /*GET sku/_search
        {
          "size": 0,
          "aggs": {
            "sku_category": {
              "terms": {
                "field": "categoryName"
              }
            },
            "sku_brand": {
              "terms": {
                "field": "brandName"
              }
            }
          }
        }*/
        SearchRequest searchRequest = new SearchRequest("sku");
        searchRequest.types("doc");
        //query
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        //分组
        TermsAggregationBuilder termsAggregationBuilder =
                AggregationBuilders.terms("sku_category").field("categoryName");
       //agg         
        searchSourceBuilder.aggregation(termsAggregationBuilder);
        //不输出全部的结果集
        searchSourceBuilder.size(0);
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = client.search(searchRequest, RequestOptions.DEFAULT);
 
        /*从json返回值提取数据,因此继续寻找*/
        Aggregations aggregations = searchResponse.getAggregations();
        //返回map集合
        Map<String, Aggregation> asMap = aggregations.getAsMap();
        //得到map中sku_category键值对
        Terms terms = (Terms) asMap.get("sku_category");
        //得到桶中的内容
        List<? extends Terms.Bucket> buckets = terms.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            System.out.println(bucket.getKeyAsString()+":"+bucket.getDocCount());
        }
        /*客户端有始有终*/
        client.close();
    }
}
 
 

原始 json

{
  "took" : 16,
  "timed_out" : false,
  "_shards" : {
    "total" : 5,
    "successful" : 5,
    "skipped" : 0,
    "failed" : 0
  },
  "hits" : {
    "total" : 3,
    "max_score" : 0.0,
    "hits" : [ ]
  },
  "aggregations" : {
    "sku_category" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "电视",
          "doc_count" : 2
        },
        {
          "key" : "手机",
          "doc_count" : 1
        }
      ]
    },
    "sku_brand" : {
      "doc_count_error_upper_bound" : 0,
      "sum_other_doc_count" : 0,
      "buckets" : [
        {
          "key" : "小米",
          "doc_count" : 2
        },
        {
          "key" : "华为",
          "doc_count" : 1
        }
      ]
    }
  }
}
 

4 批量数据导入

4.1 需求分析

单独建立工程,实现青橙商品索引数据的批量导入。

4.2 实现思路

(1)建立工程,引入通用 mapper 和 elasticsearch 依赖
(2)查询 sku 表数据,将集合数据循环插入到 elasticsearch 中