0%

Elasticsearch入门及掌握其JavaAPI

环境

安装ES

ES项目结构

解压elasticsearch-6.2.1.zip,解压后得到的目录为==ES根目录==,其中各目录作用如下:

  • bin,存放启动ES等命令脚本
  • config,存放ES的配置文件,ES启动时会读取其中的内容
    • elasticsearch.yml,ES的集群信息、对外端口、内存锁定、数据目录、跨域访问等属性的配置
    • jvm.options,ES使用Java写的,此文件用于设置JVM相关参数,如最大堆、最小堆
    • log4j2.properties,ES使用log4j作为其日志框架
  • data,数据存放目录(索引数据)
  • lib,ES依赖的库
  • logs,日志存放目录
  • modules,ES的各功能模块
  • plugins,ES的可扩展插件存放目录,如可以将ik中文分词插件放入此目录,ES启动时会自动加载

属性配置

默认的elasticsearch.yml中的所有属性都被注释了,我们需要设置一些必要的属性值,在文尾添加如下内容(集群相关的配将在后文详细说明):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cluster.name: xuecheng #集群名称,默认为elasticsearch
node.name: xc_node_1 #节点名称,一个ES实例就是一个节点(通常一台机器上只部署一个ES实例)
network.host: 0.0.0.0 #IP绑定,0.0.0.0表示所有IP都可访问到此ES实例
http.port: 9200 #通过此端口以RESTful的形式访问ES
transport.tcp.port: 9300 #ES集群通信使用的端口
node.master: true #此节点是否能够作为主节点
node.data: true #此节点是否存放数据
discovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"] #集群其他节点的通信端口,ES启动时会发现这些节点
discovery.zen.minimum_master_nodes: 1 #主节点数量的最少值,此值的计算公式为(master_eligible_nodes/2)+1,即可作为主节点的节点数/2+1
node.ingest: true #此节点是否作为协调节点,当索引库具有多个分片并且各分片位于不同节点上时,如果收到查询请求的节点发现要查询的数据在另一个节点的分片上,那么作为协调节点的该节点将会转发此请求并最终响应结果数据
bootstrap.memory_lock: false #是否锁住ES占用的内存,此项涉及到OS的swap概念,当ES空闲时操作系统可能会把ES占用的内存数据暂时保存到磁盘上,当ES活动起来时再调入内存,如果要求ES时刻保持迅速响应状态则可设置为true,那么ES的运行内存永远不会被交换到磁盘以避免交换过程带来的延时
node.max_local_storage_nodes: 2 #本机上的最大存储节点数,多个ES实例可以共享一个数据目录,这一特性有利于我们在开发环境的一台机器上测试集群机制,但在生产环境下建议设置为1,并且官方也建议一台集群仅部署一个ES实例

path.data: D:\software\es\elasticsearch-6.2.1\data #ES的数据目录
path.logs: D:\software\es\elasticsearch-6.2.1\logs #ES的日志目录

http.cors.enabled: true #是否允许跨域访问,后面通过一个可视化ES管理插件时需要通过js跨域访问此ES
http.cors.allow-origin: /.*/ #设置所有域均可跨域访问此ES

JVM参数设置

默认ES启动需要分配的堆内存为1G,如果你的机器内存较小则可在jvm.options中调整为512M:

1
2
-Xms512m
-Xmx512

启动ES

双击/bin/elasticsearch.bat启动脚本即可启动ES,关闭该命令行窗口即可关闭ES。

启动后访问:http://localhost:9200 , 如果得到如下响应则ES启动成功:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
name: "xc_node_1",
cluster_name: "xuecheng",
cluster_uuid: "93K4AFOVSD-kPF2DdHlcow",
version: {
number: "6.2.1",
build_hash: "7299dc3",
build_date: "2018-02-07T19:34:26.990113Z",
build_snapshot: false,
lucene_version: "7.2.1",
minimum_wire_compatibility_version: "5.6.0",
minimum_index_compatibility_version: "5.0.0"
},
tagline: "You Know, for Search"
}

elasticsearch-head可视化插件

ES是基于Lucene开发的产品级搜索引擎,封装了很多内部细节,通过此插件我们可以通过Web的方式可视化查看其内部状态。

此插件无需放到ES的/plugins目录下,因为它是通过JS与ES进行交互的。

1
2
3
4
git clone git://github.com/mobz/elasticsearch-head.git
cd elasticsearch-head
npm install
npm run start

浏览器打开:http://localhost:9100 , 并连接通过ES提供的http端口连接ES:

image

ES快速入门

首先我们要理解几个概念:索引库(index)、文档(document)、字段(field),我们可以类比关系型数据库来理解:

ES MySQL
索引库index 数据库database
type 表table
文档document 行row
字段field 列column

但是自ES6.x开始,type的概念就慢慢被弱化了,官方将在ES9正式剔除它。因此我们可以将索引库类比为一张表。一个索引库用来存储一系列结构类似的数据。虽然可以通过多个type制造出一个索引库“多张表”的效果,但官方不建议这么做,因为这会降低索引和搜索性能,要么你就新建另外一个索引库。类比MySQL来看就是,一个库就只放一张表。

名词索引 & 动词索引

名词索引指的是索引库,一个磁盘上的文件。

一个索引库就是一张倒排索引表,将数据存入ES的过程就是先将数据分词然后添加到倒排索引表的过程。

image

以添加“中华人民共和国”、“中华上下五千年”到索引库为例,倒排索引表的逻辑结构如下:

term doc_id
中华 1、2
人民 1
共和国 1
上下 2
2
千年 2
doc_id doc
1 中华人民共和国
2 中华上下五千年

这种将数据分词并建立各分词到文档之间的关联关系的过程称为==索引==(动词)

Postman

Postman是一款HTTP客户端工具,能否方便地发送各种形式的RESTful请求。

下文将以Postman来测试ES的RESTful API,请求的根URL为:http://localhost:9200

索引库管理

创建索引库

创建一个名为“xc_course”的用于存放学成在线(教育平台)的课程数据的索引库:

  • PUT /xc_course
1
2
3
4
5
6
{
"settings":{
"number_of_shards":1, //索引库分片数量
"number_of_replicas":0 //每个分片的副本数,关于分片、集群等在后文详细介绍
}
}

image

创建成功了吗?我们可以通过elasticsearch-head来查看,刷新localhost:9100:

image

删除索引库

DELET /xc_course

查看索引信息

GET /xc_course

映射管理

创建映射

映射可以类比MySQL的表结构定义,如有哪些字段,字段是什么类型。

创建映射的请求格式为:POST /index_name/type_name/_mapping。

不是说type已经弱化了吗?为什么这里还要指定type的名称?因为在ES9才正式剔除type的概念,在此之前需要一个过渡期,因此我们可以指定一个无意义的type名,如“doc”:

POST /xc_course/doc/_mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"properties":{
"name":{
"type":"text"
},
"description":{
"type":"text"
},
"price":{
"type":"double"
}
}
}

查看映射(类比查看表结构)

GET /xc_course/doc/_mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"xc_course": {
"mappings": {
"doc": {
"properties": {
"description": {
"type": "text"
},
"name": {
"type": "text"
},
"price": {
"type": "double"
}
}
}
}
}
}

也可以通过head插件查看:

image

文档管理

添加文档

PUT /index/type/id

如果不指定id,那么ES会为我们自动生成:

PUT /xc_course/doc

1
2
3
4
5
{
"name" : "Bootstrap开发框架",
"description" : "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
"price" : 99.9
}

响应如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
{
"_index": "xc_course",
"_type": "doc",
"_id": "Hib0QmoB7xBOMrejqjF3",
"_version": 1,
"result": "created",
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}

根据id查询文档

GET /index/type/id

于是我们拿到我们刚添加数据生成的id来查询:

GET /xc_course/doc/Hib0QmoB7xBOMrejqjF3

1
2
3
4
5
6
7
8
9
10
11
12
{
"_index": "xc_course",
"_type": "doc",
"_id": "Hib0QmoB7xBOMrejqjF3",
"_version": 1,
"found": true,
"_source": {
"name": "Bootstrap开发框架",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
"price": 99.9
}
}

查询全部文档

GET /index/type/_search

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
{
"took": 64, //此次查询花费时间
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 1,
"max_score": 1,
"hits": [ //查询匹配的文档集合
{
"_index": "xc_course",
"_type": "doc",
"_id": "Hib0QmoB7xBOMrejqjF3",
"_score": 1,
"_source": {
"name": "Bootstrap开发框架",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,在行业之中使用较为广泛。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
"price": 99.9
}
}
]
}
}

IK中文分词器

ES默认情况下是不支持中文分词的,也就是说对于添加的中文数据,ES将会把每个字当做一个term(词项),这不利于中文检索。

测试ES默认情况下对中文分词的结果:

POST /_analyze

你会发现ES的固定API都会带上_前缀,如_mapping_search_analyze

1
2
3
{
"text":"中华人民共和国"
}

分词结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
{
"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
},
{
"token": "和",
"start_offset": 5,
"end_offset": 6,
"type": "<IDEOGRAPHIC>",
"position": 5
},
{
"token": "国",
"start_offset": 6,
"end_offset": 7,
"type": "<IDEOGRAPHIC>",
"position": 6
}
]
}

下载ik-6.4.0并解压到ES/plugins/目录下,并将解压后的目录改名为==ik==,==重启ES==,该插件即会被自动加载。

重启ES后再测试分词效果:

POST http://localhost:9200/_analyze

1
2
3
4
{
"text":"中华人民共和国",
"analyzer":"ik_max_word" //设置分词器为ik分词器,否则还是会采用默认分词器,可选ik_max_word和ik_smart
}

ik_max_word分词策略是尽可能的分出多的term,即细粒度分词:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
{
"tokens": [
{
"token": "中华人民共和国",
"start_offset": 0,
"end_offset": 7,
"type": "CN_WORD",
"position": 0
},
{
"token": "中华人民",
"start_offset": 0,
"end_offset": 4,
"type": "CN_WORD",
"position": 1
},
{
"token": "中华",
"start_offset": 0,
"end_offset": 2,
"type": "CN_WORD",
"position": 2
},
{
"token": "华人",
"start_offset": 1,
"end_offset": 3,
"type": "CN_WORD",
"position": 3
},
{
"token": "人民共和国",
"start_offset": 2,
"end_offset": 7,
"type": "CN_WORD",
"position": 4
},
{
"token": "人民",
"start_offset": 2,
"end_offset": 4,
"type": "CN_WORD",
"position": 5
},
{
"token": "共和国",
"start_offset": 4,
"end_offset": 7,
"type": "CN_WORD",
"position": 6
},
{
"token": "共和",
"start_offset": 4,
"end_offset": 6,
"type": "CN_WORD",
"position": 7
},
{
"token": "国",
"start_offset": 6,
"end_offset": 7,
"type": "CN_CHAR",
"position": 8
}
]
}

而ik_smart则是出粒度分词(设置"analyzer" : "ik_smart"):

1
2
3
4
5
6
7
8
9
10
11
{
"tokens": [
{
"token": "中华人民共和国",
"start_offset": 0,
"end_offset": 7,
"type": "CN_WORD",
"position": 0
}
]
}

自定义词库

ik分词器仅提供了常用中文短语的词库,而对于实时性的热门网络短语则无法识别,因此有时为了增加分词准确性,我们需要自己扩展词库。

首先我们测试ik对网络词汇“蓝瘦香菇”的分词效果:

PUT /_analyze

1
2
3
4
{
"text":"蓝瘦香菇",
"analyzer":"ik_smart"
}

分词如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"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": 4,
"type": "CN_WORD",
"position": 2
}
]
}

我们在ES的/plugins/ik/config目录下增加自定义的词库文件my.dic并添加一行“蓝瘦香菇”(词典文件的格式是每一个词项占一行),并在ik的配置文件/plugins/ik/config/IKAnalyzer.cfg.xml中引入该自定义词典:

1
2
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">my.dic</entry>

==重启ES==,ik分词器将会把我们新增的词项作为分词标准:

1
2
3
4
5
6
7
8
9
10
11
{
"tokens": [
{
"token": "蓝瘦香菇",
"start_offset": 0,
"end_offset": 4,
"type": "CN_WORD",
"position": 0
}
]
}

映射

新增字段

PUT /xc_course/doc/_mapping

1
2
3
4
5
6
7
{
"properties":{
"create_time":{
"type":"date"
}
}
}

GET /xc_course/doc/_mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
{
"xc_course": {
"mappings": {
"doc": {
"properties": {
"create_time": {
"type": "date"
},
"description": {
"type": "text"
},
"name": {
"type": "text"
},
"price": {
"type": "double"
}
}
}
}
}
}

已有的映射可以新增字段但不可以更改已有字段的定义!

PUT /xc_course/doc/_mapping

1
2
3
4
5
6
7
{
"properties":{
"price":{
"type":"integer"
}
}
}

报错:已定义的price不能从double类型更改为integer类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"error": {
"root_cause": [
{
"type": "illegal_argument_exception",
"reason": "mapper [price] cannot be changed from type [double] to [integer]"
}
],
"type": "illegal_argument_exception",
"reason": "mapper [price] cannot be changed from type [double] to [integer]"
},
"status": 400
}

如果一定要更改某字段的定义(包括类型、分词器、是否索引等),那么只有删除此索引库重新建立索引并定义好各字段,再迁入数据。因此在索引库创建时要考虑好映射的定义,因为仅可扩展字段但不可重新定义字段。

常用的映射类型——type

ES6.2的核心数据类型如下:

image

keyword

此类型的字段不会被分词,该字段内容被表示为就是一个短语不可分割。如各大商标和品牌名可使用此类型。并且在该字段查询内容时是精确匹配,如在type为keyword的brand字段搜索“华为”不会搜出字段值为“华为荣耀”的文档。

date

type为date的字段还可以额外指定一个format,如

1
2
3
4
5
6
7
8
{
"properties":{
"create_time":{
"type":"date",
"format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
}
}
}

新增文档的create_time字段值可以是日期+时间或仅日期

数值类型

image

1、尽量选择范围小的类型,提高搜索效率
2、对于浮点数尽量用比例因子,比如一个价格字段,单位为元,我们将比例因子设置为100这在ES中会按==分==存
储,映射如下:

1
2
3
4
"price": {
"type": "scaled_float",       
"scaling_factor": 100
}

由于比例因子为100,如果我们输入的价格是23.45则ES中会将23.45乘以100存储在ES中。如果输入的价格是23.456,ES会将23.456乘以100再取一个接近原始值的数,得出2346。

使用比例因子的好处是整型比浮点型更易压缩,节省磁盘空间。

是否建立索引——index

index默认为true,即需要分词并根据分词所得词项建立倒排索引(词项到文档的关联关系)。有些字段的数据是无实际意义的,如课程图片的url仅作展示图片之用,不需要分词建立索引,那么可以设置为false:

PUT /xc_course/doc/_mapping

1
2
3
4
5
6
7
8
{
"properties":{
"pic":{
"type":"text"
"index":"false"
}
}
}

索引分词器 & 搜索分词器

索引分词器——analyzer

将数据添加到索引库时使用的分词器,建议使用ik_max_word,比如“中华人民共和国”,如果使用ik_smart,那么整个“中华人民共和国”将被作为一个term(此项)存入倒排索引表,那么在搜索“共和国”时就搜不到此数据(词项与词项之间是精确匹配的)。

搜索分词器——search_analyzer

搜索分词器则是用于将用户的检索输入分词的分词器。

建议使用ik_smart,比如搜索“中华人民共和国”,不应该出现“喜马拉雅共和国”的内容。

是否额外存储——store

是否在source之外存储,每个文档索引后会在 ES中保存一份原始文档,存放在_source中,一般情况下不需要设置
store为true,因为在_source中已经有一份原始文档了。

综合实战

创建一个课程集合的映射:

  1. 首先删除已建立映射的索引

    DELET /xc_course

  2. 新增索引

    PUT /xc_course

  3. 创建映射

    PUT /xc_course/doc/_mapping

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    {
    "properties": {
    "name": {
    "type": "text",
    "analyzer": "ik_max_word",
    "search_analyzer": "ik_smart"
    },
    "description": {
    "type": "text",
    "analyzer": "ik_max_word",
    "search_analyzer": "ik_smart"
    },
    "price": {
    "type": "scaled_float",
    "scaling_factor": 100
    },
    "studypattern": {
    "type": "keyword"
    },
    "pic": {
    "type": "text",
    "index": false
    },
    "timestamp": {
    "type": "date",
    "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
    }
    }
    }
  4. 添加文档

    POST /xc_course/doc

    1
    2
    3
    4
    5
    6
    7
    8
    {
    "name": "Java核心技术",
    "description": "深入浅出讲解Java核心技术以及相关原理",
    "price": 99.9,
    "studypattern": "20101",
    "pic": "http://xxx.xxx.xxx/skllsdfsdflsdfk.img",
    "timestamp": "2019-4-1 13:16:00"
    }
  5. 检索Java

    GET http://localhost:9200/xc_course/doc/_search?q=name:java

  6. 检索学习模式

    GET http://localhost:9200/xc_course/doc/_search?q=studypattern:20101

索引管理和Java客户端

从此章节开始我们将对ES的每个RESTful API实现配套的Java代码。毕竟虽然前端可以通过HTTP访问ES,但是ES的管理和定制化业务还是需要一个后端作为枢纽。

ES提供的Java客户端——RestClient

RestClient是官方推荐使用的,它包括两种:Java Low Level REST Client和 Java High Level REST Client。ES在6.0之后提供 Java High Level REST Client, 两种客户端官方更推荐使用 Java High Level REST Client,不过当前它还处于完善中,有些功能还没有(如果它有不支持的功能,则使用Java Low Level REST Client。)。

依赖如下:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.2.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.2.1</version>
</dependency>

Spring整合ES

依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>6.2.1</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>6.2.1</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
</dependency>
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-io</artifactId>
</dependency>

配置文件

application.yml

1
2
3
4
5
6
7
8
server:
port: ${port:40100}
spring:
application:
name: xc-service-search
xuecheng: #自定义属性项
elasticsearch:
host-list: ${eshostlist:127.0.0.1:9200} #多个节点中间用逗号分隔

启动类

1
2
3
4
5
6
@SpringBootApplication
public class SearchApplication {
public static void main(String[] args){
SpringApplication.run(SearchApplication.class, args);
}
}

ES配置类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
package com.xuecheng.search.config;

import org.apache.http.HttpHost;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* ElasticsearchConfig class
*
* @author : zaw
* @date : 2019/4/22
*/
@Configuration
public class ElasticsearchConfig {

@Value("${xuecheng.elasticsearch.host-list}")
private String hostList;

@Bean
public RestHighLevelClient restHighLevelClient() {
return new RestHighLevelClient(RestClient.builder(getHttpHostList(hostList)));
}

private HttpHost[] getHttpHostList(String hostList) {
String[] hosts = hostList.split(",");
HttpHost[] httpHostArr = new HttpHost[hosts.length];
for (int i = 0; i < hosts.length; i++) {
String[] items = hosts[i].split(":");
httpHostArr[i] = new HttpHost(items[0], Integer.parseInt(items[1]), "http");
}
return httpHostArr;
}

// rest low level client
@Bean
public RestClient restClient() {
return RestClient.builder(getHttpHostList(hostList)).build();
}
}

测试类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
package com.xuecheng.search;

import org.elasticsearch.action.admin.indices.create.CreateIndexRequest;
import org.elasticsearch.action.admin.indices.create.CreateIndexResponse;
import org.elasticsearch.client.IndicesClient;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.xcontent.XContentType;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.io.IOException;

/**
* TestES class
*
* @author : zaw
* @date : 2019/4/22
*/
@SpringBootTest
@RunWith(SpringRunner.class)
public class TestESRestClient {

@Autowired
RestHighLevelClient restHighLevelClient; //ES连接对象

@Autowired
RestClient restClient;
}

ES客户端API

首先我们将之前创建的索引库删除:

DELETE /xc_course

然后回顾一下创建索引库的RESTful形式:

PUT /xc_course

1
2
3
4
5
6
7
8
{
"settings":{
"index":{
"number_of_shards":1,
"number_of_replicas":0
}
}
}

创建索引库

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Test
public void testCreateIndex() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("xc_course");
/**
* {
* "settings":{
* "index":{
* "number_of_shards":1,
* "number_of_replicas":0
* }
* }
* }
*/
request.settings(Settings.builder().put("number_of_shards", 1).put("number_of_replicas", 0));
IndicesClient indicesClient = restHighLevelClient.indices(); //通过ES连接对象获取索引库管理对象
CreateIndexResponse response = indicesClient.create(request);
System.out.println(response.isAcknowledged()); //操作是否成功
}

对比RESTful形式,通过CreateIndexRequest方式发起此次请求,第3行通过构造函数指明了要创建的索引库名(对应URI /xc_course),第14行构造了请求体(你会发现settings方法和JSON请求格式很相似)。

操作索引库需要使用IndicesClient对象。

删除索引库

1
2
3
4
5
6
7
@Test
public void testDeleteIndex() throws IOException {
DeleteIndexRequest request = new DeleteIndexRequest("xc_course");
IndicesClient indicesClient = restHighLevelClient.indices();
DeleteIndexResponse response = indicesClient.delete(request);
System.out.println(response.isAcknowledged());
}

创建索引库时指定映射

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testCreateIndexWithMapping() throws IOException {
CreateIndexRequest request = new CreateIndexRequest("xc_course");
request.settings(Settings.builder().put("number_of_shards", 1).put("number_of_replicas", 0));
request.mapping("doc", "{\n" +
" \"properties\": {\n" +
" \"name\": {\n" +
" \"type\": \"text\",\n" +
" \"analyzer\": \"ik_max_word\",\n" +
" \"search_analyzer\": \"ik_smart\"\n" +
" },\n" +
" \"price\": {\n" +
" \"type\": \"scaled_float\",\n" +
" \"scaling_factor\": 100\n" +
" },\n" +
" \"timestamp\": {\n" +
" \"type\": \"date\",\n" +
" \"format\": \"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd\"\n" +
" }\n" +
" }\n" +
"}", XContentType.JSON);
IndicesClient indicesClient = restHighLevelClient.indices();
CreateIndexResponse response = indicesClient.create(request);
System.out.println(response.isAcknowledged());
}

添加文档

添加文档的过程就是“索引”(动词)。需要使用IndexRequest对象进行索引操作。

1
2
3
4
5
6
7
8
9
10
11
12
public static final SimpleDateFormat FORMAT = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
@Test
public void testAddDocument() throws IOException {
Map<String, Object> jsonMap = new HashMap<>();
jsonMap.put("name", "Java核心技术");
jsonMap.put("price", 66.6);
jsonMap.put("timestamp", FORMAT.format(new Date(System.currentTimeMillis())));
IndexRequest request = new IndexRequest("xc_course", "doc");
request.source(jsonMap);
IndexResponse response = restHighLevelClient.index(request);
System.out.println(response);
}

响应结果包含了ES为我们生成的文档id,这里我测试得到的id为fHh6RWoBduPBueXKl_tz

根据id查询文档

1
2
3
4
5
6
@Test
public void testFindById() throws IOException {
GetRequest request = new GetRequest("xc_course", "doc", "fHh6RWoBduPBueXKl_tz");
GetResponse response = restHighLevelClient.get(request);
System.out.println(response);
}

根据id更新文档

ES更新文档有两种方式:全量替换和局部更新

全量替换:ES首先会根据id查询文档并删除然后将该id作为新文档的id插入。

局部更新:只会更新相应字段

全量替换:

POST /index/type/id

局部更新:

POST /index/type/_update

Java客户端提供的是局部更新,即仅对提交的字段进行更新而其他字段值不变

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testUpdateDoc() throws IOException {
UpdateRequest request = new UpdateRequest("xc_course", "doc", "fHh6RWoBduPBueXKl_tz");
Map<String, Object> docMap = new HashMap<>();
docMap.put("name", "Spring核心技术");
docMap.put("price", 99.8);
docMap.put("timestamp", FORMAT.format(new Date(System.currentTimeMillis())));
request.doc(docMap);
UpdateResponse response = restHighLevelClient.update(request);
System.out.println(response);
testFindById();
}

根据id删除文档

1
2
3
4
5
6
@Test
public void testDeleteDoc() throws IOException {
DeleteRequest request = new DeleteRequest("xc_course", "doc", "fHh6RWoBduPBueXKl_tz");
DeleteResponse response = restHighLevelClient.delete(request);
System.out.println(response);
}

搜索管理

准备环境

为了有数据可搜,我们重新创建映射并添加一些测试数据

创建映射

DELETE /xc_course

PUT /xc_course

1
2
3
4
5
6
{
"settings":{
"number_of_shards":1,
"number_of_replicas":0
}
}

PUT /xc_course/doc/_mapping

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
"properties": {
"name": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"description": {
"type": "text",
"analyzer": "ik_max_word",
"search_analyzer": "ik_smart"
},
"studymodel":{
"type":"keyword" //授课模式,值为数据字典代号
},
"pic": {
"type": "text",
"index": false
},
"price": {
"type": "float"
},
"timestamp": {
"type": "date",
"format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"
}
}
}

添加测试数据

PUT /xc_course/doc/1

1
2
3
4
5
6
7
8
{
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
"studymodel": "201002",
"price": 38.6,
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"timestamp": "2018-04-25 19:11:35"
}

PUT /xc_course/doc/2

1
2
3
4
5
6
7
8
{
"name": "java编程基础",
"description": "java语言是世界第一编程语言,在软件开发领域使用人数最多。",
"studymodel": "201001",
"price": 68.6,
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"timestamp": "2018-03-25 19:11:35"
}

PUT /xc_course/doc/3

1
2
3
4
5
6
7
8
{
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。",
"studymodel": "201001",
"price": 88.6,
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"timestamp": "2018-02-24 19:11:35"
}

简单搜索

  • 搜索指定索引库中的所有文档

    GET /xc_course/_search

  • 搜索指定type中的所有文档

    GET /xc_course/doc/_search

DSL搜索

DSL(Domain Specific Language)是ES提出的基于json的搜索方式,在搜索时传入特定的json格式的数据来完成不同的搜索需求。

DSL比URI搜索方式功能强大,在项目中建议使用DSL方式来完成搜索。

DSL搜索方式是使用POST提交,URI为以_search结尾(在某index或某type范围内搜索),而在JSON请求体中定义搜索条件。

查询所有文档——matchAllQuery

POST /xc_course/doc/_search

1
2
3
4
5
6
{
"query":{
"match_all":{}
},
"_source":["name","studymodel"]
}

query用来定义搜索条件,_source用来指定返回的结果集中需要包含哪些字段。这在文档本身数据量较大但我们只想获取其中特定几个字段数据时有用(既可过滤掉不必要字段,又可提高传输效率)。

结果说明:

  • took,本次操作花费的时间,单位毫秒
  • time_out,请求是否超时(ES不可用或网络故障时会超时)
  • _shard,本次操作共搜索了哪些分片
  • hits,命中的结果
  • hits.total,符合条件的文档数
  • hits.hits,命中的文档集
  • hits.max_score,hits.hits中各文档得分的最高分,文档得分即查询相关度
  • _source,文档源数据
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
{
"took": 57,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 1,
"hits": [
{
"_index": "xc_course",
"_type": "doc",
"_id": "1",
"_score": 1,
"_source": {
"studymodel": "201002",
"name": "Bootstrap开发"
}
},
{
"_index": "xc_course",
"_type": "doc",
"_id": "2",
"_score": 1,
"_source": {
"studymodel": "201001",
"name": "java编程基础"
}
},
{
"_index": "xc_course",
"_type": "doc",
"_id": "3",
"_score": 1,
"_source": {
"studymodel": "201001",
"name": "spring开发基础"
}
}
]
}
}

Java代码实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
@Test
public void testMatchAll() throws IOException {
// POST /xc_course/doc
SearchRequest request = new SearchRequest("xc_course"); //DSL搜索请求对象
request.types("doc");
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder(); //DSL请求体构造对象
/**
* {
* "from":2,"size":1,
* "query":{
* "match_all":{
*
* }* },
* "_source":["name","studymodel"]
* }
*/
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
//参数1:要返回哪些字段 参数2:不要返回哪些字段 两者通常指定其一
searchSourceBuilder.fetchSource(new String[]{"name", "studymodel"}, null);
//将请求体设置到请求对象中
request.source(searchSourceBuilder);
//发起DSL请求
SearchResponse response = restHighLevelClient.search(request);
System.out.println(response);
}

DSL核心API

  • new SearchRequest(index),指定要搜索的索引库
  • searchRequest.type(type),指定要搜索的type
  • SearchSourceBuilder,构建DSL请求体
  • searchSourceBuilder.query(queryBuilder),构造请求体中“query”:{}部分的内容
  • QueryBuilders,静态工厂类,方便构造queryBuilder,如searchSourceBuilder.query(QueryBuilders.matchAllQuery())就相当于构造了“query”:{ "match_all":{} }
  • searchRequest.source(),将构造好的请求体设置到请求对象中

分页查询

PUT http://localhost:9200/xc_course/doc/_search

1
2
3
4
5
6
7
8
9
{
"from":0,"size":1,
"query":{
"match_all":{

}
},
"_source":["name","studymodel"]
}

其中from的含义是结果集偏移,而size则是从偏移位置开始之后的size条结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
{
"took": 80,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"skipped": 0,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 1,
"hits": [
{
"_index": "xc_course",
"_type": "doc",
"_id": "1",
"_score": 1,
"_source": {
"studymodel": "201002",
"name": "Bootstrap开发"
}
}
]
}
}

这里虽然hits.total为3,但是只返回了第一条记录。因此我们在做分页功能时需要用到一个公式:from = (page-1)*size

Java代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testPaginating() throws IOException {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

int page = 1, size = 1;
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.from((page - 1) * size);
searchSourceBuilder.size(size);
searchSourceBuilder.query(QueryBuilders.matchAllQuery());
searchSourceBuilder.fetchSource(new String[]{"name", "studymodel"}, null);

request.source(searchSourceBuilder);
SearchResponse response = restHighLevelClient.search(request);
System.out.println(response);
}

提取结果集中的文档

1
2
3
4
5
6
7
8
SearchResponse response = restHighLevelClient.search(request);
SearchHits hits = response.getHits(); //hits
if (hits != null) {
SearchHit[] results = hits.getHits(); //hits.hits
for (SearchHit result : results) {
System.out.println(result.getSourceAsMap()); //hits.hits._source
}
}

词项匹配——termQuery

词项匹配是==精确匹配==,只有当倒排索引表中存在我们指定的词项时才会返回该词项关联的文档集。

如搜索课程名包含java词项的文档

1
2
3
4
5
6
7
{
"from":0,"size":1,
"query":{
"term":{ "name":"java" }
},
"_source":["name","studymodel"]
}

结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
"hits": {
"total": 1,
"max_score": 0.9331132,
"hits": [
{
"_index": "xc_course",
"_type": "doc",
"_id": "2",
"_score": 0.9331132,
"_source": {
"studymodel": "201001",
"name": "java编程基础"
}
}
]
}

但如果你指定"term"{ "name":"java编程" }就搜索不到了:

1
2
3
4
5
"hits": {
"total": 0,
"max_score": null,
"hits": []
}

因为“java编程基础”在索引时会被分为“java”、“编程”、“基础”三个词项添加到倒排索引表中,因此没有一个叫“java编程”的词项和此次查询匹配。

term查询是精确匹配,term.name不会被search_analyzer分词,而是会作为一个整体和倒排索引表中的词项进行匹配。

根据id精确匹配——termsQuery

查询id为1和3的文档

POST http://localhost:9200/xc_course/doc/_search

1
2
3
4
5
6
7
{
"query":{
"ids":{
"values":["1","3"]
}
}
}

Java实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Test
public void testQueryByIds(){
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
List<String> ids = Arrays.asList(new String[]{"1", "3"});
sourceBuilder.query(QueryBuilders.termsQuery("_id", ids));

printResult(request, sourceBuilder);
}

private void printResult(SearchRequest request,SearchSourceBuilder sourceBuilder) {
request.source(sourceBuilder);
SearchResponse response = null;
try {
response = restHighLevelClient.search(request);
} catch (IOException e) {
e.printStackTrace();
}
SearchHits hits = response.getHits();
if (hits != null) {
SearchHit[] results = hits.getHits();
for (SearchHit result : results) {
System.out.println(result.getSourceAsMap());
}
}
}

==大坑==

根据id精确匹配也是term查询的一种,但是调用的API是termsQuery("_id", ids),注意是termsQuery而不是termQuery

全文检索—— matchQuery

输入的关键词会被search_analyzer指定的分词器分词,然后根据所得词项到倒排索引表中查找文档集合,每个词项关联的文档集合都会被查出来,如查“bootstrap基础”会查出“java编程基础”:

POST

1
2
3
4
5
6
7
{
"query":{
"match":{
"name":"bootstrap基础"
}
}
}

因为“bootstrap基础”会被分为“bootstrap”和“基础”两个词项,而词项“基础”关联文档“java编程基础”。

1
2
3
4
5
6
7
8
9
10
11
@Test
public void testMatchQuery() {

SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("name", "bootstrap基础"));

printResult(request, sourceBuilder);
}

operator

上述查询等同于:

1
2
3
4
5
6
7
8
9
10
{
"query": {
"match": {
"name": {
"query": "bootstrap基础",
"operator": "or"
}
}
}
}

即对检索关键词分词后每个词项的查询结果取并集。

operator可取值orand,分别对应取并集和取交集。

如下查询就只有一结果(课程名既包含“java”又包含“基础”的只有“java编程基础”):

1
2
3
4
5
6
7
8
9
10
{
"query": {
"match": {
"name": {
"query": "java基础",
"operator": "and"
}
}
}
}

Java代码

1
2
3
4
5
6
7
8
9
10
@Test
public void testMatchQuery2() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("name", "java基础").operator(Operator.AND));

printResult(request, sourceBuilder);
}

minimum_should_match

上边使用的operator = or表示只要有一个词匹配上就得分,如果实现三个词至少有两个词匹配如何实现?

使用minimum_should_match可以指定文档匹配词的占比,比如搜索语句如下:

1
2
3
4
5
6
7
8
9
10
{
"query": {
"match": {
"name": {
"query": "spring开发框架",
"minimum_should_match":"80%"
}
}
}
}

“spring开发框架”会被分为三个词:spring、开发、框架。

设置"minimum_should_match":"80%"表示,三个词在文档的匹配占比为80%,即3*0.8=2.4,向上取整得2,表
示至少有两个词在文档中才算匹配成功。

1
2
3
4
5
6
7
8
9
10
@Test
public void testMatchQuery3() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("name", "spring开发指南").minimumShouldMatch("70%")); //3*0.7 -> 2

printResult(request, sourceBuilder);
}

多域检索——multiMatchQuery

上边学习的termQuerymatchQuery一次只能匹配一个Field,本节学习multiQuery,一次可以匹配多个字段(即扩大了检索范围,之前一直都是在name字段中检索)。

如检索课程名或课程描述中包含“spring”或“css”的文档:

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"query": {
"multi_match": {
"query": "spring css",
"minimum_should_match": "50%",
"fields": [
"name",
"description"
]
}
},
"_source":["name","description"]
}

Java:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testMultiMatchQuery() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.
multiMatchQuery("spring css","name","description").
minimumShouldMatch("50%"));

printResult(request, sourceBuilder);
}

boost权重

观察上述查出的文档得分:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"hits": [
{
"_index": "xc_course",
"_type": "doc",
"_id": "3",
"_score": 1.3339276,
"_source": {
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。"
}
},
{
"_index": "xc_course",
"_type": "doc",
"_id": "1",
"_score": 0.69607234,
"_source": {
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个前台页面开发框架,是一个非常流行的开发框架,此框架集成了多种页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助开发者(尤其是不擅长页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。"
}
}
]

你会发现文档3spring词项在文档出现的次数占文档词项总数的比例较高因此得分(_score)较高。那我们猜想,是不是我们在文档1的课程描述中多添加几个css能否提升其_score呢?

于是我们更新一下文档1:

1
2
3
4
5
6
7
8
9
10
@Test
public void testUpdateDoc() throws IOException {
UpdateRequest request = new UpdateRequest("xc_course", "doc", "1");
Map<String, Object> docMap = new HashMap<>();
docMap.put("description", "Bootstrap是由Twitter推出的一个css前台页面开发框架,是一个非常流行的css开发框架,此框架集成了多种css页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助css开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。");
request.doc(docMap);
UpdateResponse response = restHighLevelClient.update(request);
System.out.println(response);
testFindById();
}

再次查询发现文档1的得分果然变高了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
"hits": [
{
"_index": "xc_course",
"_type": "doc",
"_id": "1",
"_score": 1.575484,
"_source": {
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个css前台页面开发框架,是一个非常流行的css开发框架,此框架集成了多种css页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助css开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。"
}
},
{
"_index": "xc_course",
"_type": "doc",
"_id": "3",
"_score": 1.346281,
"_source": {
"name": "spring开发基础",
"description": "spring 在java领域非常流行,java程序员都在用。"
}
}
]

那我们有这样一个业务需求:课程出现spring或css肯定是与spring或css相关度更大的课程,而课程描述出现则不一定。因此我们想提高课程出现关键词项的得分权重,我们可以这么办(在name字段后追加一个^符号并指定权重,默认为1):

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"query": {
"multi_match": {
"query": "spring css",
"minimum_should_match": "50%",
"fields": [
"name^10",
"description"
]
}
},
"_source":["name","description"]
}

Java:

1
2
3
4
5
6
7
8
9
10
11
12
@Test
public void testMultiMatchQuery2() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("spring css", "name", "description").minimumShouldMatch("50%");
multiMatchQueryBuilder.field("name", 10);
sourceBuilder.query(multiMatchQueryBuilder);

printResult(request, sourceBuilder);
}

布尔查询——boolQuery

布尔查询对应于Lucene的BooleanQuery查询,实现==将多个查询组合起来==。

三个参数

  • must:文档必须匹配must所包括的查询条件,相当于 “AND”
  • should:文档应该匹配should所包括的查询条件其中的一个或多个,相当于 “OR”
  • must_not:文档不能匹配must_not所包括的该查询条件,相当于“NOT”

如查询课程名包含“spring”==且==课程名或课程描述跟“开发框架”有关的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
{
"query": {
"bool":{
"must":[
{
"term":{
"name":"spring"
}
},
{
"multi_match":{
"query":"开发框架",
"fields":["name","description"]
}
}
]
}
},
"_source":["name"]
}

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testBoolQuery() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder(); //query
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder(); //query.bool

TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("name", "spring");
MultiMatchQueryBuilder multiMatchQueryBuilder = QueryBuilders.multiMatchQuery("开发框架", "name", "description");

boolQueryBuilder.must(termQueryBuilder); //query.bool.must
boolQueryBuilder.must(multiMatchQueryBuilder);
sourceBuilder.query(boolQueryBuilder);

printResult(request, sourceBuilder);
}

必须满足的条件放到must中(boolQueryBuilder.must(条件)),必须排斥的条件放到must_not中,只需满足其一的条件放到should中。

查询课程名必须包含“开发”但不包含“java”的,且包含“spring”或“boostrap”的课程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
{
"query": {
"bool":{
"must":[
{
"term":{
"name":"开发"
}
}
],
"must_not":[
{
"term":{
"name":"java"
}
}
],
"should":[
{
"term":{
"name":"bootstrap"
}
},
{
"term":{
"name":"spring"
}
}
]
}
},
"_source":["name"]
}

当然实际项目不会这么设置条件,这里只是为了演示效果,这里为了演示方便用的都是termQuery,事实可用前面任意一种Query

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Test
public void testBoolQuery2() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

boolQueryBuilder.must(QueryBuilders.termQuery("name","开发"));
boolQueryBuilder.mustNot(QueryBuilders.termQuery("name", "java"));
boolQueryBuilder.should(QueryBuilders.termQuery("name","spring"));
boolQueryBuilder.should(QueryBuilders.termQuery("name","bootstrap"));

sourceBuilder.query(boolQueryBuilder);

printResult(request, sourceBuilder);
}

过滤器——filter

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

过滤器仅能在布尔查询中使用。

全文检索“spring框架”,并过滤掉学习模式代号不是“201001”和课程价格不在10~100之间的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
{
"query": {
"bool": {
"must": [
{
"multi_match": {
"query": "spring框架",
"fields": [
"name",
"description"
]
}
}
],
"filter": [
{
"term": {
"studymodel": "201001"
}
},
{
"range": {
"price": {
"gte": "10",
"lte": "100"
}
}
}
]
}
}
}

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testBoolQuery3() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();

boolQueryBuilder.must(QueryBuilders.multiMatchQuery("spring框架", "name", "description"));
boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel", "201001"));
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(10).lte(100));

sourceBuilder.query(boolQueryBuilder);

printResult(request, sourceBuilder);
}

排序

查询课程价格在10~100之间的,并按照价格升序排列,当价格相同时再按照时间戳降序排列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{
"query": {
"bool": {
"filter": [
{
"range": {
"price": {
"gte": "10",
"lte": "100"
}
}
}
]
}
},
"sort": [
{
"price": "asc"
},
{
"timestamp": "desc"
}
],
"_source": [
"name",
"price",
"timestamp"
]
}

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Test
public void testSort() {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");

SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(10).lte(100));

sourceBuilder.sort("price", SortOrder.ASC);
sourceBuilder.sort("timestamp", SortOrder.DESC);

sourceBuilder.query(boolQueryBuilder);
printResult(request, sourceBuilder);
}

高亮

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
{
"query": {
"bool": {
"filter": [
{
"multi_match": {
"query": "bootstrap",
"fields": [
"name",
"description"
]
}
}
]
}
},
"highlight":{
"pre_tags":["<tag>"],
"post_tags":["</tag>"],
"fields":{
"name":{},
"description":{}
}
}
}

结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
"hits": [
{
"_index": "xc_course",
"_type": "doc",
"_id": "1",
"_score": 0,
"_source": {
"name": "Bootstrap开发",
"description": "Bootstrap是由Twitter推出的一个css前台页面开发框架,是一个非常流行的css开发框架,此框架集成了多种css页面效果。此开发框架包含了大量的CSS、JS程序代码,可以帮助css开发者(尤其是不擅长css页面开发的程序人员)轻松的实现一个不受浏览器限制的精美界面效果。",
"studymodel": "201002",
"price": 38.6,
"pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg",
"timestamp": "2018-04-25 19:11:35"
},
"highlight": {
"name": [
"<tag>Bootstrap</tag>开发"
],
"description": [
"<tag>Bootstrap</tag>是由Twitter推出的一个css前台页面开发框架,是一个非常流行的css开发框架,此框架集成了多种css页面效果。"
]
}
}
]

hits结果集中的每个结果出了给出源文档_source之外,还给出了相应的高亮结果highlight

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
@Test
public void testHighlight() throws IOException {
SearchRequest request = new SearchRequest("xc_course");
request.types("doc");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();

BoolQueryBuilder boolQueryBuilder = new BoolQueryBuilder();
boolQueryBuilder.filter(QueryBuilders.multiMatchQuery("bootstrap","name","description"));

HighlightBuilder highlightBuilder = new HighlightBuilder();
highlightBuilder.preTags("<tag>");
highlightBuilder.postTags("</tag>");
highlightBuilder.field("name").field("description");

sourceBuilder.query(boolQueryBuilder);
sourceBuilder.highlighter(highlightBuilder);
request.source(sourceBuilder);

SearchResponse response = restHighLevelClient.search(request);
SearchHits hits = response.getHits(); //hits
if (hits != null) {
SearchHit[] results = hits.getHits(); //hits.hits
if (results != null) {
for (SearchHit result : results) {
Map<String, Object> source = result.getSourceAsMap(); //_source
String name = (String) source.get("name");
Map<String, HighlightField> highlightFields = result.getHighlightFields(); //highlight
HighlightField highlightField = highlightFields.get("name");
if (highlightField != null) {
Text[] fragments = highlightField.getFragments();
StringBuilder stringBuilder = new StringBuilder();
for (Text text : fragments) {
stringBuilder.append(text.string());
}
name = stringBuilder.toString();
}
System.out.println(name);

String description = (String) source.get("description");
HighlightField highlightField2 = highlightFields.get("description");
if (highlightField2 != null) {
Text[] fragments = highlightField2.getFragments();
StringBuilder stringBuilder = new StringBuilder();
for (Text text : fragments) {
stringBuilder.append(text.string());
}
description = stringBuilder.toString();
}
System.out.println(description);
}
}
}
}

比较难理解的API是HighlightFieldshighlightField.getFragments(),我们需要对比响应JSO的结构来类比理解。

image

我们可以通过highlightFields.get()来获取highlight.namehighlight.description对应的highlightField,但是为什么hightField.getFragment返回的是一个Text[]而不是Text呢。我们猜测ES将文档按照句子分成了多个段,仅对出现关键词项的段进行高亮并返回,于是我们检索css测试一下果然如此:

image

因此你需要注意返回的highlight可能并不包含所有原字段内容

image

集群管理

ES通常以集群方式工作,这样做不仅能够提高 ES的搜索能力还可以处理大数据搜索的能力,同时也增加了系统的容错能力及高可用,ES可以实现PB级数据的搜索。

下图是ES集群结构的示意图:

image

集群相关概念

节点

ES集群由多个服务器组成,每个服务器即为一个Node节点(该服务只部署了一个ES进程)。

分片

当我们的文档量很大时,由于内存和硬盘的限制,同时也为了提高ES的处理能力、容错能力及高可用能力,我们将索引分成若干分片(可以类比MySQL中的分区来看,一个表分成多个文件),每个分片可以放在不同的服务器,这样就实现了多个服务器共同对外提供索引及搜索服务。

一个搜索请求过来,会分别从各各分片去查询,最后将查询到的数据合并返回给用户。

副本

为了提高ES的高可用同时也为了提高搜索的吞吐量,我们将分片复制一份或多份存储在其它的服务器,这样即使当前的服务器挂掉了,拥有副本的服务器照常可以提供服务。

主节点

一个集群中会有一个或多个主节点,主节点的作用是集群管理,比如增加节点,移除节点等,主节点挂掉后ES会重新选一个主节点。

节点转发

每个节点都知道其它节点的信息,我们可以对任意一个v发起请求,接收请求的节点会转发给其它节点查询数据。

节点的三个角色

主节点

master节点主要用于集群的管理及索引 比如新增节点、分片分配、索引的新增和删除等。

数据节点

data 节点上保存了数据分片,它负责索引和搜索操作。

客户端节点

client 节点仅作为请求客户端存在,client的作用也作为负载均衡器,client 节点不存数据,只是将请求均衡转发到其它节点。

配置

可在/config/elasticsearch.yml中配置节点的功能:

  • node.master: #是否允许为主节点
  • node.data: #允许存储数据作为数据节点
  • node.ingest: #是否允许成为协调节点(数据不在当前ES实例上时转发请求)

四种组合方式:

  • master=true,data=true:即是主节点又是数据节点
  • master=false,data=true:仅是数据节点
  • master=true,data=false:仅是主节点,不存储数据
  • master=false,data=false:即不是主节点也不是数据节点,此时可设置ingest为true表示它是一个客户端。

搭建集群

下我们来实现创建一个2节点的集群,并且索引的分片我们设置2片,每片一个副本。

解压elasticsearch-6.2.1.zip两份为es-1es-2

配置文件elasticsearch.yml

节点1:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cluster.name: xuecheng
node.name: xc_node_1
network.host: 0.0.0.0
http.port: 9200
transport.tcp.port: 9300
node.master: true
node.data: true
discovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]
discovery.zen.minimum_master_nodes: 1
node.ingest: true
bootstrap.memory_lock: false
node.max_local_storage_nodes: 2

path.data: D:\software\es\cluster\es-1\data
path.logs: D:\software\es\cluster\es-1\logs

http.cors.enabled: true
http.cors.allow-origin: /.*/

节点2:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
cluster.name: xuecheng
node.name: xc_node_2
network.host: 0.0.0.0
http.port: 9201
transport.tcp.port: 9301
node.master: true
node.data: true
discovery.zen.ping.unicast.hosts: ["0.0.0.0:9300", "0.0.0.0:9301"]
discovery.zen.minimum_master_nodes: 1
node.ingest: true
bootstrap.memory_lock: false
node.max_local_storage_nodes: 2

path.data: D:\software\es\cluster\es-2\data
path.logs: D:\software\es\cluster\es-2\logs

http.cors.enabled: true
http.cors.allow-origin: /.*/

测试分片

创建索引,索引分两片,每片有一个副本:

PUT http://localhost:9200/xc_course

1
2
3
4
5
6
{
"settings": {
"number_of_shards": 2,
"number_of_replicas": 1
}
}

通过head插件查看索引状态:

image

测试主从复制

写入数据

POST http://localhost:9200/xc_course/doc

1
2
3
{
"name":"java编程基础"
}

两个结点均有数据:

image

image

集群的健康

通过访问 GET /_cluster/health 来查看Elasticsearch 的集群健康情况。

用三种颜色来展示健康状态: green 、 yellow 或者 red 。

  • green:所有的主分片和副本分片都正常运行。
  • yellow:所有的主分片都正常运行,但有些副本分片运行不正常。
  • red:存在主分片运行不正常。
鼓励一下~