Elasticsearch 入门及掌握其 JavaAPI

本贴最后更新于 1679 天前,其中的信息可能已经事过景迁

环境

安装 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 中的所有属性都被注释了,我们需要设置一些必要的属性值,在文尾添加如下内容(集群相关的配将在后文详细说明):

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:

-Xms512m
-Xmx512

启动 ES

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

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

{
    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 进行交互的。

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
{
	"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

{
	"properties":{
		"name":{
			"type":"text"
		},
		"description":{
			"type":"text"
		},
		"price":{
			"type":"double"
		}
	}
}

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

GET /xc_course/doc/_mapping

{
    "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

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

响应如下:

{
    "_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

{
    "_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

{
    "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

{
	"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
        },
        {
            "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

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

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

{
    "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" ):

{
    "tokens": [
        {
            "token": "中华人民共和国",
            "start_offset": 0,
            "end_offset": 7,
            "type": "CN_WORD",
            "position": 0
        }
    ]
}

自定义词库

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

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

PUT /_analyze

{
    "text":"蓝瘦香菇",
    "analyzer":"ik_smart"
}

分词如下:

{
    "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 中引入该自定义词典:

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

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

{
    "tokens": [
        {
            "token": "蓝瘦香菇",
            "start_offset": 0,
            "end_offset": 4,
            "type": "CN_WORD",
            "position": 0
        }
    ]
}

映射

新增字段

PUT /xc_course/doc/_mapping

{
    "properties":{
        "create_time":{
            "type":"date"
        }
    }
}

GET /xc_course/doc/_mapping

{
    "xc_course": {
        "mappings": {
            "doc": {
                "properties": {
                    "create_time": {
                        "type": "date"
                    },
                    "description": {
                        "type": "text"
                    },
                    "name": {
                        "type": "text"
                    },
                    "price": {
                        "type": "double"
                    }
                }
            }
        }
    }
}

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

PUT /xc_course/doc/_mapping

{
    "properties":{
        "price":{
            "type":"integer"
        }
    }
}

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

{
    "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,如

{
    "properties":{
        "create_time":{	
            "type":"date",
            "format":"yyyy-MM-dd HH:mm:ss||yyyy-MM-dd"	
        }
    }
}

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

数值类型

image

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

"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

{
    "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

    {
        "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

    {
        "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。)。

依赖如下:

<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

依赖

<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

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

启动类

@SpringBootApplication
public class SearchApplication {
    public static void main(String[] args){
        SpringApplication.run(SearchApplication.class, args);
    }
}

ES 配置类

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();
    }
}

测试类

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

{
	"settings":{
		"index":{
			"number_of_shards":1,
			"number_of_replicas":0
		}
	}
}

创建索引库

@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 对象。

删除索引库

@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());
}

创建索引库时指定映射

@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 对象进行索引操作。

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 查询文档

@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 客户端提供的是局部更新,即仅对提交的字段进行更新而其他字段值不变

@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 删除文档

@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

{
    "settings":{
        "number_of_shards":1,
        "number_of_replicas":0
    }
}

PUT /xc_course/doc/_mapping

{
    "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

{
    "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

{
    "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

{
    "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

{
	"query":{
		"match_all":{}
	},
	"_source":["name","studymodel"]
}

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

结果说明:

  • took,本次操作花费的时间,单位毫秒
  • time_out,请求是否超时(ES 不可用或网络故障时会超时)
  • _shard,本次操作共搜索了哪些分片
  • hits,命中的结果
  • hits.total,符合条件的文档数
  • hits.hits,命中的文档集
  • hits.max_score,hits.hits 中各文档得分的最高分,文档得分即查询相关度
  • _source,文档源数据
{
    "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 代码实现:

@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

{
	"from":0,"size":1,
	"query":{
		"match_all":{
			
		}
	},
	"_source":["name","studymodel"]
}

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

{
    "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 代码实现

@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);
}

提取结果集中的文档

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 词项的文档

{
	"from":0,"size":1,
	"query":{
		"term":{ "name":"java" }
	},
	"_source":["name","studymodel"]
}

结果如下:

"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编程" } 就搜索不到了:

"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

{
	"query":{
		"ids":{
			"values":["1","3"]
		}
	}
}

Java 实现

@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

{
	"query":{
		"match":{
			"name":"bootstrap基础"
		}
	}
}

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

@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

上述查询等同于:

{
    "query": {
        "match": {
            "name": {
                "query": "bootstrap基础",
                "operator": "or"
            }
        }
    }
}

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

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

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

{
    "query": {
        "match": {
            "name": {
                "query": "java基础",
                "operator": "and"
            }
        }
    }
}

Java 代码

@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 可以指定文档匹配词的占比,比如搜索语句如下:

{
    "query": {
        "match": {
            "name": {
                "query": "spring开发框架",
                "minimum_should_match":"80%"
            }
        }
    }
}

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

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

@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”的文档:

{
    "query": {
        "multi_match": {
            "query": "spring css",
            "minimum_should_match": "50%",
            "fields": [
                "name",
                "description"
            ]
        }
    },
    "_source":["name","description"]
}

Java:

@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 权重

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

"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:

@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 的得分果然变高了:

"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):

{
    "query": {
        "multi_match": {
            "query": "spring css",
            "minimum_should_match": "50%",
            "fields": [
                "name^10",
                "description"
            ]
        }
    },
    "_source":["name","description"]
}

Java:

@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”==且==课程名或课程描述跟“开发框架”有关的:

{
    "query": {
        "bool":{
            "must":[
                {
                    "term":{
                        "name":"spring"
                    }
                },
                {
                    "multi_match":{
                        "query":"开发框架",
                        "fields":["name","description"]
                    }
                }
            ]
        }
    },
    "_source":["name"]
}

Java

@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”的课程:

{
    "query": {
       "bool":{
       		"must":[
				{
					"term":{
						"name":"开发"
					}
				}
       		],
       		"must_not":[
				{
					"term":{
						"name":"java"
					}
				}
       		],
       		"should":[
				{
					"term":{
						"name":"bootstrap"
					}
				},
				{
					"term":{
						"name":"spring"
					}
				}
       		]
       }
    },
    "_source":["name"]
}

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

Java

@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 之间的

{
    "query": {
        "bool": {
            "must": [
                {
                    "multi_match": {
                        "query": "spring框架",
                        "fields": [
                            "name",
                            "description"
                        ]
                    }
                }
            ],
            "filter": [
                {
                    "term": {
                        "studymodel": "201001"
                    }
                },
                {
                    "range": {
                        "price": {
                            "gte": "10",
                            "lte": "100"
                        }
                    }
                }
            ]
        }
    }
}

Java

@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 之间的,并按照价格升序排列,当价格相同时再按照时间戳降序排列

{
    "query": {
        "bool": {
            "filter": [
                {
                    "range": {
                        "price": {
                            "gte": "10",
                            "lte": "100"
                        }
                    }
                }
            ]
        }
    },
    "sort": [
        {
            "price": "asc"
        },
        {
            "timestamp": "desc"
        }
    ],
    "_source": [
        "name",
        "price",
        "timestamp"
    ]
}

Java

@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);
}

高亮

{
    "query": {
        "bool": {
            "filter": [
                {
                    "multi_match": {
                        "query": "bootstrap",
                        "fields": [
                            "name",
                            "description"
                        ]
                    }
                }
            ]
        }
    },
    "highlight":{
        "pre_tags":["<tag>"],
        "post_tags":["</tag>"],
        "fields":{
            "name":{},
            "description":{}
        }
    }
}

结果:

"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

@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:

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:

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

{
    "settings": {
        "number_of_shards": 2,
        "number_of_replicas": 1
    }
}

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

image

测试主从复制

写入数据

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

{
	"name":"java编程基础"
}

两个结点均有数据:

image

image

集群的健康

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

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

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

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

    116 引用 • 99 回帖 • 275 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
zanwen
男儿何不带吴钩,收取关山五十州 武汉