ElasticSearch 基本使用
简介
ElasticSearch 简介
elasticsearch是一款非常强大的开源搜索引擎,可以帮助我们从海量数据中快速找到需要的内容。
搜索引擎是一个技术集合,其中其中Kibana是负责基于ES之上的可视化应用,而ES是核心应用,负责数据的存储与计算。
ElasticSearch 与 MySQL 的区别
MySQL 是正向索引查询方式,ElasticSearch 是倒序索引查询方式。
正向索引:
当我需要查询name包含tzming的数据时,MySQL会在表中从头到属遍历地查询name中为包含tzming的行数据,并把这行数据东输出,这种叫正向索引。
严格来说,当查询的数据中只有部分,比如使用 like %xx% 的方式查询时,MySQL 只能在每一行中一行行遍历具有该关键字的行数据,这叫正向索引。
倒序索引:
当我们需要查询关键字的数据时,ElasticSearch 会事先对有该关键字的所有行数据的id整理成一行记录,与MySQL不一样的是,ElasticSearch 存储的是关键字对应的行id,MySQL是存储这个id对应的行数据,只是行数据中有对应的关键字。
所以倒序索引更适合关键词搜索。
索引 Index
针对不同的数据内容,ElasticSearch 可以把它们存放到不同的表格中,每个表格中的行记录所记录的数据结构都是一样的,就和MySQL中的不同的表意思一样。ElasticSearch 与MySQL的概念对比如下:
MySQL | Elasticsearch | 说明 |
Table | Index | 索引(index),就是文档的集合,类似数据库的表(table) |
Row | Document | 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式 |
Column | Field | 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column) |
Schema | Mapping | Mapping(映射)是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema) |
SQL | DSL | DSL是elasticsearch提供的JSON风格的请求语句,用来操作elasticsearch,实现CRUD |
Kibana 简介
Kibana 是基于 ElasticSearch 做可视化数据展示与管理的,本身ElasticSearch是通过RESTFul进行数据操作的,对于数据查询、分析等后台操作来说非常不方便,Kibana 能可视化管理和分析ElasticSearch数据的辅工具
IK 分词器 简介
根据 ElasticSearch 的原理,我们需要对数据行中的可能存在的搜索文本进行分关键词并记录具有该关键词的行id,那么这些文本中的词如何去切割,需要一个非常复杂的算法进行,IK分词器就是负责对文本分词的插件。
IK 有两种分词策略:
ik_smart : 最少切分,粗粒度,表示在一些词上面,不会太过细分,如“Java程序员”这个词,除了“程序员”是一个词,“程序”也是一个词,但粗粒度下,“程序”可能不会作为一个词。
ik_max_word : 最细切分,细粒度,表示一些词上面,会把所有可能的词都进行切割,如“程序员”会把“程序”和“员”切割。
ElasticSearch 安装
本次安装使用Docker容器的方式安装,注意,考虑到版本统一,ES版本和Kibana与IK分词器的版本必须一致,但是IK或Kibana的版本有可能与ES的版本出现断层,如ES有7.17.13版本,而IK却没有,这时ES的7.17.13版本无法安装IK.
安装步骤:
1.拉取Docker镜像,本次使用版本是 7.17.6,该版本有Kibana和IK对应的版本
docker pull elasticsearch:7.17.6
2.运行ES镜像:
在运行ES镜像之前,需要特别注意,在后面的Kibana系统启动时,需要把ES的ip地址加上去,但是本次使用的是Docker单独容器启动,非DockerComponse启动,因此容器之间会隔离,为了让Kibana能顺利连接上ES系统,我们可以创建一个网络,把Kibana与ES系统的网络都使用同一个网络环境,这样就可以直接使用容器名称代替ip地址了:
docker network create es-net
es-net 是网络名称
接下来就可以启动ES的镜像了,并把该镜像的网络设置为 es-net
docker run -d \ # 以后台方式启动
--name es \ # 容器的名称
-e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
-e "discovery.type=single-node" \
-v es-data:/usr/share/elasticsearch/data \
-v es-plugins:/usr/share/elasticsearch/plugins \
--privileged \
--network es-net \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.17.6
命令参数说明如下:
-e "cluster.name=es-docker-cluster"
:设置集群名称-e "http.host=0.0.0.0"
:监听的地址,可以外网访问-e "ES_JAVA_OPTS=-Xms512m -Xmx512m"
:内存大小-e "discovery.type=single-node"
:非集群模式-v es-data:/usr/share/elasticsearch/data
:挂载逻辑卷,绑定es的数据目录-v es-logs:/usr/share/elasticsearch/logs
:挂载逻辑卷,绑定es的日志目录-v es-plugins:/usr/share/elasticsearch/plugins
:挂载逻辑卷,绑定es的插件目录--privileged
:授予逻辑卷访问权--network es-net
:加入一个名为es-net的网络中-p 9200:9200
:端口映射配置
es-data 和 es-plugins 数据卷是Docker 自动创建的,可以通过 docker volume 命令获取直实存放地址。
至此,ES完成安装与运行。
Kibana 安装
操作步骤:
1.拉取镜像
docker pull kibana:7.17.6
2.运行kibana镜像:
docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601 \
kibana:7.17.6
注意:因为加入到 es-net 的网络中,所以可以直接使用容器名代替ES容器的ip
--network es-net
:加入一个名为es-net的网络中,与elasticsearch在同一个网络中-e ELASTICSEARCH_HOSTS=http://es:9200"
:设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch-p 5601:5601
:端口映射配置
IK分词器安装
下载对应版本的IK分词器,本次运行的ES使用的是7.17.6版本,因此我们也需要下载7.17.6版本的IK分词器
操作步骤:
1.下载对应版本的IK分词器
https://github.com/medcl/elasticsearch-analysis-ik
2.把下载下来的IK分词器放到一个文件夹中,并把这个文件夹放到 es-plugins 数据卷中。
3.重启ES
docker restart es
IK 分词器自定义字典
IK 的分词功能,实际上是通过自带的字典进行查询,通过查询字典得知哪些字能组成一个词语,那么当我们有一些流行词时,默认的IK字典肯定无法识别,这时就可以使用扩展字典
IK的扩展字典存放在IK插件文件夹的config文件夹中,需要配置 IKAnalyzer.cfg.xml 文件进行字典的配置:
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
<comment>IK Analyzer 扩展配置</comment>
<!--用户可以在这里配置自己的扩展字典 -->
<entry key="ext_dict">ext.dic</entry>
<!--用户可以在这里配置自己的扩展停止词字典-->
<entry key="ext_stopwords">stopwords.dic</entry>
<!--用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!--用户可以在这里配置远程扩展停止词字典-->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
我们可以自己创建一个新的字典,用于定义流行词,并在IKAnalyzer配置文件中定义字典,这样IK就能识别流行词。
同时 IKAnalyzer 还能配置忽略词,比如“的”,“啊”这些无意义的语气词,或一些违禁词,我们可以设置去掉,这样IK在分词时则会对这些词进行过滤不加入到数据库索引中。
<entry key="ext_stopwords">stopwords.dic</entry>
ElasticSearch 索引库操作
Mapping 属性
Mapping 相当于数据库中的表,那么要把数据存储,ElasticSearch 需要创建表,又称Mapping,在ElasticSearch中,Mapping 有以下几种属性:
type: 指定的搜索列的数据类型
- 字符串分为两种:text和keyword
- text 是指一串文本,这类文本是可以进行分词的,如“Java程序员”
- keyword 是指一个关键词,它是不能进行分词的,这个词是整体的,如“中国”、“美国”
- 数字分为:long、integer、short、byte、double、float
- 布尔型:boolean
- 日期:date
- 对象:object
index: 指定的数据是否加入搜索索引,参与搜索,加入索引后,才可以被搜索,默认为 true
analyzer: 指定的分词器,这个属性通常只对 text 有用,因为只有text 才需要分词
properties: 指定的数据如果是个对象,那么它的对象内的成员变量名有哪些。
创建索引库
ES采用JSON通过RESTful进行操作索引库、文档。
我们可以按照上面Mapping属性规律,创建一个对应的索引库
请求ES创建索引库
我们可以参照以下规则对ES进行创建索引库
PUT /索引库名称
{
"mappings": {
"properties": {
"字段名":{
"type": "text",
"analyzer": "ik_smart"
},
"字段名2":{
"type": "keyword",
"index": "false"
},
"字段名3":{
"properties": {
"子字段": {
"type": "keyword"
}
}
},
// ...略
}
}
}
创建索引库案例:
具体数据如下:
{
"age": 21,
"weight": 52.1,
"isMarried": false,
"info": "SpringBoot程序员讲师",
"email": "zy@itcast.cn",
"score": [99.1, 99.5, 98.9],
"name": {
"firstName": "云",
"lastName": "赵"
}
}
分析说明:
1.对于 age , weight 这些需要数字类型,而且我们认为这些不需要做搜索索引,所以不对这些字段进行index操作
2.对于 info 这类字串,我们希望它能做搜索,那么我们就可以让它开启index,且该字串具有可分词的条件,因此也可做分词。
3.name 是一个对象,它之下还有成员变量,则我们也需要对其进行搜索和定义。
得出以下创建库实例:
PUT /unsoft # 创建一个库的库名
{
"mappings": {
"properties": {
"age":{ # age 类型为整数,不索引
"type": "integer",
"index": false
},
"weight":{ # weight 类型为浮点,不索引
"type": "float",
"index": false
},
"isMarried":{ # isMarried 类型为布尔,不索引
"type": "boolean",
"index": false
},
"info":{ # info 类型为text,加入索引,使用IK进行粗粒度分词
"type": "text",
"index": true,
"analyzer": "ik_smart",
"copy_to": "all"
},
"email":{ # email类型为keyword,不索引,也不分词
"type": "keyword",
"index": false
},
"name":{ # name 类型为object对象,索引里面的成员
"properties": {
"firstName":{
"type":"keyword",
"index": true # firstName 类型为 keyword,索引
"copy_to": "all"
},
"lastName":{
"type":"keyword", # lastName 类型为 keyword,索引
"index": true
"copy_to": "all"
}
},
"all": {
"type":"text",
"analyzer": "ik_max_word"
}
}
}
}
}
小技巧:
使用 copy_to 属性,可以把这个属性的值引用到指定的字段,比如上面的索引属性all,是用于方便集合所有应该被检索的字段,然后所有需要检索的字段可以使用 copy_to 指定到 all 字段中,以后我们直接对all字段进行检索,ES就会自动检索被 copy_to 的字段。 在实际原理中,copy_to 只是一个引用,并非真的把字段值统统都复制到all字段中。
查询索引库
要查询索引库,使用GET 请求即可,如查询上面创建的索引库
GET /unsoft
删除索引库
删除上面创建的索引库
DELETE /unsoft
修改索引库
索引库按道理来说,是不能修改的,因为索引库创建后,就会对库里做倒排索引,一旦修改那么整个库都会无法使用,所以ES默认是不允许修改的,但是允许往库里增加字段,使用 PUT /库名/_mapping 来对索引库进行修改
PUT /unsoft/_mapping
{
"properties":{
"aaa":{ # 增加的字段
"type": "text",
"index": true
}
}
}
ElasticSearch 文档操作
添加(插入)文档
插入文档基本和数据本身相当了
使用 POST /索引库/_doc/文档id
进行插入
POST /unsoft/_doc/1
{
"age":18,
"weight":99.5,
"isMarried": false,
"info": "Java程序员",
"email": "111@qq.com",
"name":{
"firstName":"云",
"lastName":"赵"
}
}
查询文档
使用 GET /索引库名/_doc/文档id
就能获取到查询数据
{
"_index" : "unsoft",
"_type" : "_doc",
"_id" : "1",
"_version" : 1,
"_seq_no" : 0,
"_primary_term" : 1,
"found" : true,
"_source" : {
"age" : 18,
"weight" : 99.5,
"isMarried" : false,
"info" : "Java程序员",
"email" : "111@qq.com",
"name" : {
"firstName" : "云",
"lastName" : "赵"
}
}
}
删除文档
使用 DELETE /索引库名/_doc/文档id
就能删除数据
{
"_index" : "unsoft",
"_type" : "_doc",
"_id" : "1",
"_version" : 2, # 每做一次操作,版本会加1
"result" : "deleted",
"_shards" : {
"total" : 2,
"successful" : 1,
"failed" : 0
},
"_seq_no" : 1,
"_primary_term" : 1
}
修改文档
修改文档有两种方式,一种是全量修改,一种是增量修改
全量修改
全量修改是指,把原有的文档全部删除,并新增修改的文档内容,如果id值存在,则先删除该id的文档,再对该文档进行新增操作,如果id值不存,则直接新增文档,因此全量修改既包含修改也能插入功能。
使用 PUT /索引库/_doc/文档id
进行全量修改
PUT /unsoft/_doc/1 # 会把原有的文档删除,再添加新的,如果没有原有文档则直接新增
{
"age":19,
"weight":100.5,
"isMarried": false,
"info": "Java程序员111",
"email": "111@qq.com",
"name":{
"firstName":"云",
"lastName":"赵"
}
}
增量修改
增量修改则是在不删除原有的文档内容情况下,仅对个别字段数据进行修改
使用 POST /索引库/_update/文档id
进行修改
POST /unsoft/_update/1
{
"doc": {
"age": 22
}
}
注意:要在json中加上 doc 字段
案例
# 创建索引库
PUT /unsoft
{
"mappings": {
"properties": {
"id": {
"type": "text",
"index": false
},
"username": {
"type": "keyword"
},
"goods_title": {
"type": "text",
"analyzer": "ik_smart"
},
"shop": {
"type": "text",
"analyzer": "ik_max_word"
},
"other":{
"type": "text",
"analyzer": "ik_smart"
}
}
}
}
# 删除索引库
DELETE /unsoft
# 插入一条数据
POST /unsoft/_doc/1
{
"id":"10086",
"username":"TZMing",
"goods_title":"劲鲨 X79迷你17*19cm小板2011针台式e5至强CPU十核游戏电脑M2主板",
"shop":"中弘科技 惠民店",
"other":"赠品五元红包现金秀图"
}
# 全量修改或插入一条数据
PUT /unsoft/_doc/10086
{
"id":"10086",
"username":"TZMing",
"goods_title":"金牌全模组小1U FLEX 7660B 450 500 600 700W 益衡方案 itx电源",
"shop":"电竞艾派",
"other":"电竞艾派"
}
# 查询一条数据
GET /unsoft/_doc/10086
# 查询索引库结构
GET /unsoft
索引库操作 新版 Java Client
Java Client 是在 7.17 版本后推出的,在其之后不再支持 Rest Client了,如果要使用 Rest Client 请使用低于 7.15版本的ES
https://www.elastic.co/guide/en/elasticsearch/client/index.html
本次我们使用的是 Java 语言的客户端
1.引入依赖,注意,依赖版本需要和服务器版本一致
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>7.17.6</version>
</dependency>
注意:在7.17之后版本,ES放弃了 RestClient ,取而代之的是 Java Client
连接ES服务器
使用Java Client 连接ES服务器如下代码
// 1.准备Request PUT /xxx
RestClient request = RestClient.builder(
new HttpHost("127.0.0.1", 9200)
).build();
// 2.创建一个Rest客户端转换通道,使用json转换,这里使用的是 jackson 依赖的json转换
ElasticsearchTransport elasticsearchTransport = new RestClientTransport(request, new JacksonJsonpMapper());
// 3.1.创建一个ES客户端
ElasticsearchClient elasticsearchClient = new ElasticsearchClient(elasticsearchTransport);
// 3.2.也可以创建一个ES的异步客户端
ElasticsearchAsyncClient elasticsearchAsyncClient = new ElasticsearchAsyncClient(elasticsearchTransport);
创建索引库
使用Java Client 创建索引库,需要先连接ES服务器,其代码如下:
方式一:直接使用 json 代码创建,json 代码建议先在Kibana写好再放到代码中
// 1. 封装json文本到StringReader
StringReader json = new StringReader(MAPPING_TEMPLATE);
// 2.创建索引对象,使用Lambda创建索引
CreateIndexResponse createIndexResponse = elasticsearchClient.indices().create(create -> create.withJson(json));
// 3.判断是否创建成功
System.out.println(createIndexResponse.acknowledged()?"创建索引成功" : "创建索引失败");
方式二:使用Java的Lambda方式进行构建
elasticsearchClient.indices().create(
create -> create.index("hotel")
.mappings(
mapping -> mapping.properties("info", p -> p.text(analyzer -> analyzer.analyzer("ik_smart")))
.properties("age", prop -> prop.integer(i -> i.index(false)))
.properties("weigth", prop -> prop.float_(f -> f.index(false)))
.properties("business", prop -> prop.keyword(k -> k.index(true)))
)
);
说明:
1.使用 create 参数创建一个 mapping
2.使用 mapping 参数创建一个 properties
3.使用properties 指定这个字段名, Lambda 中的 prop 参数创建字段的各种属性
查询索引
删除索引
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)
).build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);
//删除一个索引
DeleteIndexResponse delete = client.indices().delete(f ->
f.index("indexName")
);
System.out.println("delete.acknowledged() = " + delete.acknowledged());
文档操作 新版 Java Client
插入文档
同步插入:
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)).build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);
Book book = new Book();
book.setId(890);
book.setName("深入理解Java虚拟机");
book.setAuthor("xxx");
//添加一个文档
//这是一个同步请求,请求会卡在这里
IndexResponse response = client.index(i -> i.index("books")
.document(book)
.id("890")
.refresh(Refresh.True)); // 设置为搜索时可见
System.out.println("response.result() = " + response.result());
System.out.println("response.id() = " + response.id());
System.out.println("response.seqNo() = " + response.seqNo());
System.out.println("response.index() = " + response.index());
System.out.println("response.shards() = " + response.shards());
异步插入:
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)).build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
ElasticsearchAsyncClient client = new ElasticsearchAsyncClient(transport);
Book book = new Book();
book.setId(890);
book.setName("深入理解Java虚拟机");
book.setAuthor("xxx");
// 异步添加一个文档
client.index(i -> i.index("books").document(book).id("890"))
.whenComplete((resp, exception) -> {
System.out.println("response.result() = " + response.result());
System.out.println("response.id() = " + response.id());
System.out.println("response.seqNo() = " + response.seqNo());
System.out.println("response.index() = " + response.index());
System.out.println("response.shards() = " + response.shards());
});
说明:
异步Lambda 中的参数 resp 是指成功添加文档后的 IndexResponse 对象,其实只是服务器返回1的Jdon封装对象
exception 则是添加不成功后的异常对象
查询文档
删除文档
同步删除:
方式一:
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)).build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
ElasticsearchClient client = new ElasticsearchClient(transport);
client.delete(d -> d.index("books").id("891"));
方式二
DeleteRequest deleteRequest = DeleteRequest.of(s -> s
.index(MY_INDEX)
.id(id));
elasticsearchClient.delete(deleteRequest);
异步删除:
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200))
.build();
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
ElasticsearchAsyncClient client = new ElasticsearchAsyncClient(transport);
client.delete(d -> d.index("books").id("891")).whenComplete((resp, e) -> {
System.out.println("resp.result() = " + resp.result());
}
修改文档
索引库操作 旧版 Rest Client
RestgClient 是 ES 官方提供的一类通过代码操作ES的库,我们可以通过查看下面地址来找到适合当前开发语言的RestClient客户端类库。
https://www.elastic.co/guide/en/elasticsearch/client/index.html
引入依赖
引入 Rest Client 依赖,在SpringBoot项目中,要跟ES服务器版本一致,因为 SpringBoot 集成了版本,有可能会使用SpringBoot提供的版本,所以我们需要在引入时,覆盖Rest Client 的版本。
引入es的RestHighLevelClient依赖:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>
覆盖SpringBoot 集成的版本,实现与当前服务器版本一致
<properties>
<java.version>1.8</java.version>
<elasticsearch.version>7.15</elasticsearch.version>
</properties>
连接ES服务器
使用 RestHighLevelClient 连接服务器
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder(
HttpHost.create("http://192.168.150.101:9200")
));
client 是 ES的客户端对象,我们可以用client做增删改查、索引库等的操作
创建索引库
使用 CreateIndexRequest 对象进行创建索引库,RestClient只能使用json文本代码的方式进行创建索引库
// 1. 创建一个索引对象,索引名叫 hotel
CreateIndexRequest request = new CreateIndexRequest("hotel");
// 2.请求参数,MAPPING_TEMPLATE是静态常量字符串,内容是创建索引库的DSL语句
request.source(MAPPING_TEMPLATE, XContentType.JSON);
// 3.发起请求
client.indices().create(request, RequestOptions.DEFAULT);
MAPPING_TEMPLATE 是存在创建索引的json文本变量。
代码对照图如下:
获取索引库
// 1.创建Request对象
GetIndexRequest request = new GetIndexRequest("hotel");
// 2.发起请求
client.indices().get(request, RequestOptions.DEFAULT);
删除索引库
// 1.创建Request对象
DeleteIndexRequest request = new DeleteIndexRequest("hotel");
// 2.发起请求
client.indices().delete(request, RequestOptions.DEFAULT);
文档操作 旧版 Rest Client
插入文档
我们可以用 ES的 client进行做 index 插入操作,使用 IndexRequest 对象进行包装文档数据
// 1.创建request对象
IndexRequest request = new IndexRequest("indexName").id("1");
// 2.准备JSON文档
request.source("json 格式的对象数据", XContentType.JSON);
// 3.发送请求
client.index(request, RequestOptions.DEFAULT);
查询单条文档
我们可以用 ES的 client进行做 get 查询操作,使用 GetRequest 对象进行包装查询数据
// 1.创建request对象
GetRequest request = new GetRequest("indexName", "1");
// 2.发送请求,得到结果
GetResponse response = client.get(request, RequestOptions.DEFAULT);
// 3.解析结果
String json = response.getSourceAsString();
System.out.println(json);
因为查询文档本身不需要提供内容体,只需要提供索引和id即可。
删除文档
我们可以用 ES的 client进行做 delete 查询操作,使用 DeleteRequest 对象进行包装删除数据
// 1.创建request对象
DeleteRequest request = new DeleteRequest("indexName", "1");
// 2.删除文档
client.delete(request, RequestOptions.DEFAULT);
修改文档
我们可以用 ES的 client进行做 update 查询操作,使用UpdateRequest对象进行包装要修改的数据
// 1.创建request对象
UpdateRequest request = new UpdateRequest("indexName", "1");
// 2.准备参数,每2个参数为一对 key value
request.doc(
"age", 18,
"name", "Rose"
);
// 3.更新文档
client.update(request, RequestOptions.DEFAULT);
注意:
使用 request.doc() 来定义要修改的数据和内容,doc(Object...obj) 可传入一个数组,它的成员以 key,value,key,value... 的方式进行存储。
批量插入文档
我们可以对数据库查出来的文档做批量插入,使用 BulkRequest 进行批量插入文档
// 1.创建Bulk请求
BulkRequest request = new BulkRequest();
// 2.添加要批量提交的请求:这里添加了两个新增文档的请求
request.add(new IndexRequest("hotel")
.id("101").source("json source", XContentType.JSON));
request.add(new IndexRequest("hotel")
.id("102").source("json source2", XContentType.JSON));
// 3.发起bulk请求
client.bulk(request, RequestOptions.DEFAULT);
查询搜索文档
ES 的查询方式十分之多,我们可以通过下面网页查询所有的搜索方式。
https://www.elastic.co/guide/en/elasticsearch/reference/current/query-dsl.html
Match all 查询所有
基本语法:
GET /索引库/_search
{
查询类型: {
查询条件: 查询值
}
}
Match all 语法:
# 查询所有
GET /unsoft/_search
{
"query": {
"match_all": {}
}
}
因为 match_all 没有查询条件,所以直接定义 match_all 即可。
全文检索查询
全文检索查询,意思则是会对用户输入内容分词,常用于搜索框搜索
match查询:全文检索的一个,会对用户输入内容先进行分词,然后去倒排索引库检索
语法:
# 查询所有
GET /unsoft/_search
{
"query": {
"match": {
"FIELD名称": "要查询的值"
}
}
}
比如我们之前是把所有查询的字段加到all字段中,那么我们就可以这样
# 查询所有
GET /unsoft/_search
{
"query": {
"match": {
"all": "西门子冰箱"
}
}
}
这时ES会对“西门子冰箱”分割成“西门子”和“冰箱”对文档进行检索,针对两个词的匹配度越高,检索的文档越靠前。
multi_match查询:支持多个字段的查询,会在指定的字段中检索包含关键词的文档
# 多字段检索
GET /unsoft/_search
{
"query": {
"multi_match": {
"query": "西门子冰箱",
"fields": ["username","goods_title","other"] # 会在这三个字段中检索分词,匹配度越高越靠前
}
}
}
精确查询
精确查询一般是查找keyword、数值、日期、boolean等类型字段。所以不会对搜索词进行分词
term : 根据词条精确值查询
语法:
# 精确查询
GET /unsoft/_search
{
"query": {
"term": {
"字段名": {
"value": "检索值"
}
}
}
}
示例:
# 精确查询
GET /unsoft/_search
{
"query": {
"term": {
"city": {
"value": "广东"
}
}
}
}
会对字段为 city 精确查询“广东”,只有完全匹配的才会查询到,ES不会对检索值做分词
range : 根据值的范围查询
语法:
# 范围查询
GET /unsoft/_search
{
"query": {
"range": {
"字段名": {
"gte": , # 大于等于
"lte": # 小于等于
"gt": # 大于
"lt": # 小于
}
}
}
}
示例:
# 范围查询
GET /unsoft/_search
{
"query": {
"range": {
"price": {
"gte": 10,
"lte": 20
}
}
}
}
price 大于等于10小于等于20
地理查询
根据经纬度查询,通常使用在地图附近的位置范围检索。
geo_bounding_box : 查询 geo_point 值落在某个矩形范围的所有文档
语法:
# 地理查询
GET /unsoft/_search
{
"query": {
"geo_bounding_box":{
"字段名":{
"top_left":{ # 左上角的经纬度
"lat":31.5,
"lon":121.5
},
"bottom_right":{ # 右下角的经纬度
"lat":30.9,
"lon":121.7
}
}
}
}
}
geo_distance : 查询到指定中心点小于某个距离值的所有文档
语法:
# 地理中心点查询
GET /unsoft/_search
{
"query": {
"geo_distance":{
"distance":"15km", # 附近的15km范围
"字段名":"31.21,121.5" # 中心点经纬度
}
}
}
复合查询
复合查询(compound),可以将其它简单查询组合起来,实现更复杂的搜索逻辑
function score : 算分函数查询,可以控制文档相关性算分,人工对搜索排名的干预,控制文档排名。
算分算法基础概念:
1.对文档中的关键词进行简单计算,文档中所包含的关键词越多,越排前。
2.当每一条文档中都有相同的关键词时,我们认为这个关键词是没有权重的。
3.BM25算法。目前使用最公平的排名算法。
Function score query 算分检索
可以修改文档的相关性算分,根据新得到的算分排序,通过算分的多少来判断文档的排名。
慨念:
算分检索分为三个步骤:
1.过滤条件,需要对哪些文档进行算分操作,比如我希望把“广东月饼”相关的文档提高分值。
- 过滤条件与正常的 query 匹配文档一样,目的是要查询出要加值的文档
-
{ "query": { "match": { "goods_title": "广东月饼" } } }
2.算分函数,我们需要如何计算分值,是固定值、随机值、自定义值?
- 算分函数有以下方式:
- weigth : 给一个常量值,固定算分值。
- field_value_factor : 用文档中的某个字段值作为算分值
- random_score : 随机生成一个值,作为算分值
- script_score : 自定义计算公式,得到的结果作为算分值
3.加权方式,当我们算出分值后,这些分值要如何作用于文档的原分值,是乘、加、平均、还是替换。
- 当我们算出算分值后,我们要把算分值和文档原有的分值如何计算
- multiply : 两者相乘,结果为最终的分值,默认计算方式
- replace : 丢弃文档原有的分值,用算分函数的分值作为最终分值
- sum : 与原文档分值相加,结果为最终分值
- avg : 与原文档分值相加后平均,结果为最终分值
- max : 与原文档分值对比,谁的值大谁作为最终分值
- min : 与原文档分值对比,谁的值小谁作为最终分值。
示例:
GET /unsoft/_search
{
"query": {
"function_score": { # 进行算分排序
"query": {
"match": {
"goods_title": "金牌" # 先检索出所有符合的文档
}
},
"functions": [ # 在检索出的文档中对检索结果做分值运算
{
"filter": { # 在检索出来的结果中进行过滤,只有匹配的才会做算分操作
"term": {
"id": "1" # 精确查找 id 为 1 的文档进行分值运算
}
},
"weight": 10 # 固定分值为 10
}
],
"boost_mode": "multiply" # 对 id 为 1 的文档的分值与 10 相乘,结果为最终的分值
}
}
}
Boolean Query 布尔检索
布尔查询是一个或多个查询子句的组合。
- must : 必须匹配每个子查询,类似“与”,参与算分。
- should : 选择性匹配子查询,类似“或”,参与算分
- must_not : 必须不匹配,类以“非”,不参与算分
- filter : 必须匹配,但不参与算分
示例:
# 搜索名字包含“如家”,价格不高于 400,在坐标 31.21,121.5周围10km范围内的酒店
GET /unsoft/_search
{
"query": {
"bool": {
"must": [ # 必须查询
{
"match": {
"hotel_name": "如家" # 参与算分的关键词
}
},
],
"must_not": [ # 必须不查询
{
"range": {
"price": {
"gt": 400, # 范围是大于 400 ,必须不查询后就是查询低于400的
}
}
}],
"filter": [
{
"geo_distance": { # 不关键数据,应不参与算分,所以计入 filter
"distance": "10km",
"location": {
"lat": 31.21,
"lon": 121.5
}
}
}
]
}
}
}
搜索结果处理
ES 支持对搜索结果进行排序,默认是根据相关度算分来排序的。当然也可以使用字段进行排序,使用字段排序后ES将不使用算分来排序,可排序的类型有:“keyword”(以字母排序)、数值类型、地理坐标类型、日期类型等
排序
可以对检索结果提出排序
对于keyword、数值、日期等可以使用以下排序方法
语法:
# 排序查询
GET /unsoft/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"字段名": {
"order": "desc" # 排序方案 ASC 和 DESC
}
}
]
}
对于地理坐标可以使用以下方式排序
语法:
# 地理坐标排序查询
GET /unsoft/_search
{
"query": {
"match_all": {}
},
"_geo_distance":{
"地理坐标字段名": {"lat":xx , "lon": xx} 或使用 "纬度,经度",
"order": "asc",
"unit" : "km" # 与坐标的距离单位,检索出来的距离则为 km
}
}
分页
ES 支持分页,而且ES 查询时默认都会给我们做了分页。
语法:使用 from + size 进行分页检索
# 分页查询
GET /unsoft/_search
{
"query": {
"match_all": {}
},
"from": 10, # 从第几条文档开始
"size": 20, # 获取多少条文档
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
关于性能的问题
分页对于ES这种分布式倒排检索类型数据库而言,性能消耗是致命的
ES 只能通过检索全部文档,再从文档中截取需要的文档进行分页,也就是说假如需要从990条文档中获取10条文档,那么ES 需要检索 1000 条文档排序后,再从990条文档当中截取10条。
当 ES 作为分布式系统时,这种分页性能消耗尤其明显,如下图所示
当ES是一个集群时,索引数据都是分片存储在多个ES系统当中,数据库也是分开的,当我们需要检索1000条数据时,ES不得不向所有的集群同时索引自身库的前1000条数据,并聚合一起后重新排序获得前1000条数据。
打个比喻:10个班每个班100人,获取学校前10名。不能通过10个班获取它的第一名组合成10名,这样数据将不准确。只能10个班都选出前10名聚合成100名后,再排序截取前10名。
因此ES本身就限制了分页查询数量必须少于10000条,超出会报错。
超出一万条解决方案
search after
分页时需要排序,原理是从上一次的排序值开始,查询下一页数据。
详细:https://www.elastic.co/guide/en/elasticsearch/reference/current/paginate-search-results.html
高亮
就是在搜索结果中把搜索关键字突出显示
# 多字段检索
GET /unsoft/_search
{
"query": {
"multi_match": {
"query": "17",
"fields": ["username","goods_title","other"]
# "fields": ["all"]
}
},
"highlight": { # 设置高亮
"fields": { # 可以设置多个字段的高亮
"goods_title":{ # 这里仅设置goods_title字段的高亮
"pre_tags": "<em>", # 自动在高亮检索文字中加上标签
"post_tags": "</em>.", # 后标签
"require_field_match": "false" # 不开启对应字段检测
}
}
}
}
注意:ES的高亮默认有一个要求,就是高亮的字段,必须要和检索的字段一致,
如上面代码中的 "fields": ["all"] 检索的是 all 字段,但高亮时却指定了 goods_title 字段,默认ES是不会对goods_title生效高亮的,除非检索字段中有goods_title。
那如果我希望使用一个 all 字段同时,又想让 goods_title 字段高亮,就可以增加一个属性 "require_field_match": "false".使其不与检索字段做检测即可。
RestClient (旧版) 查询文档
基础入门 (Match_all)
本章节通过使用 RestClient 查询最简单的 Match_all 的检索。
1.我们回顾一下使用match_all 检索所有文档的方式,json检索代码:
GET /unsoft/_search
{
"query": {
"match_all": {}
}
}
2.接下来我们就使用 RestClient 进行组建上面的json代码
- 1.连接ES服务器,得到client,请查看之前连接RestClient的章节
- 2.创建一个 SearchRequest 对象,该对象用于构建检索结构
-
SearchRequest request = new SearchRequest("hotel");
- 3.使用 request.source() 进行创建检索,构造检索结构:
-
request.source() .query(QueryBuilders.matchAllQuery());
QueryBuilders 是一个非常强大的结构构建器,可以满足我们各种情况下的检索结构
- 4.提交请求给 client
-
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
实际上返回的是json结果打包好的对象
- 5.返回结果一览:
-
{ "took" : 0, "timed_out" : false, "_shards" : { "total" : 1, "successful" : 1, "skipped" : 0, "failed" : 0 }, "hits" : { "total" : { "value" : 2, "relation" : "eq" }, "max_score" : 1.0, "hits" : [ { "_index" : "unsoft", "_type" : "_doc", "_id" : "10086", "_score" : 1.0, "_source" : { "id" : "10086", "username" : "TZMing", "goods_title" : "金牌全模组小1U FLEX 7660B 450 500 600 700W 益衡方案 itx电源", "shop" : "电竞艾派", "other" : "电竞艾派" } }, ] } }
通过结构我们可以看到,我们查询出来的数据位置的 json.hits.hits._source ,那么我们也可以以这样的路径获取到数据
-
// 略掉 response 中的代码 SearchHits searchHits = response.getHits(); // 4.1.查询的总条数 long total = searchHits.getTotalHits().value; // 4.2.查询的结果数组 SearchHit[] hits = searchHits.getHits(); for (SearchHit hit : hits) { // 4.3.得到source String json = hit.getSourceAsString(); // 4.4.打印 System.out.println(json); }
全文检索查询
全文检索的查询基本都是使用 SearchRequest 对象来查询,唯一不一样的,是查询条件的构建,具体不同的查询条件,可以看下面的案例示例。
match 单字段查询
# 单字段分词查询
GET /hotel/_search
{
"query": {
"match": {
"all": "如家"
}
}
}
/* 对应的具体查询代码 */
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.组织DSL参数 先字段 后value
request.source().query(QueryBuilders.matchQuery("all", "如家"));
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
multi_match 多字段查询
# 多字段分词查询
GET /hotel/_search
{
"query": {
"multi_match": {
"query": "如家",
"fields": ["brand", "name"]
}
}
}
/* 对应的具体查询代码 */
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.组织DSL参数 先value 后字段
request.source().query(QueryBuilders.multiMatchQuery("如家", "name", "business"));
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
term 精确查询
# 精确检索 city 字段中包含 杭州的文档
GET /hotel/_search
{
"query": {
"term": {
"city": "杭州"
}
}
}
/* 对应的具体查询代码 */
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.组织DSL参数
request.source().query(QueryBuilders.termQuery("city", "杭州"));
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
range 范围查询
# 范围检索,对 price 字段大于100小于150的文档
GET /hotel/_search
{
"query": {
"range": {
"price": { "gte": 100, "lte": 150 }
}
}
}
/* 对应的具体查询代码 */
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.组织DSL参数
request.source().query(QueryBuilders.rangeQuery("price").gte(100).lte(150));
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
Bool 复合查询
复合查询使用的就不是 SearchRequest 了,而是 BoolRequest 对象了,可以构建 must、must_not、filter 等条件
# 布尔复合检索,必须精确查询 city 为杭州的,price小于250的文档
GET /hotel/_search
{
"query": {
"bool": {
"must": [
{
"term": { "city": "杭州" }
}
],
"filter": [
{
"range": {
"price": { "lte": 250 }
}
}
]
}
}
}
/* 对应的具体查询代码 */
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 2.组织DSL参数
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
// 添加must条件
boolQuery.must(QueryBuilders.termQuery("city", "杭州"));
// 添加filter条件
boolQuery.filter(QueryBuilders.rangeQuery("price").lte(250));
request.source().query(boolQuery);
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
排序
我们先来看看排序的json代码是如何:
# 排序查询
GET /unsoft/_search
{
"query": {
"match_all": {}
},
"sort": [
{
"FIELD": {
"order": "desc"
}
}
]
}
我们发现,sort 排序是在 query 之外的另一种辅助属性,所以我们就不能在 query() 方法内构建了,而是在 source() 方法上构建,具体代码:
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
request.source().sort("price", SortOrder.ASC); // SortOrder 是一个枚举类型
request.source().query(QueryBuilders.matchAllQuery());
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
地理位置排序
我们先来看看单只是用地理位置排序的json代码是如何:
# 地理坐标排序查询
GET /unsoft/_search
{
"query": {
"match_all": {}
},
"_geo_distance":{
"地理坐标字段名": {"lat":xx , "lon": xx} 或使用 "纬度,经度",
"order": "asc",
"unit" : "km" # 与坐标的距离单位,检索出来的距离则为 km
}
}
可以看到坐标的排序 使用的是 geo_distance,和 query 同级,所以我们应该在 source 后进行定义
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 对坐标的排序
request.source().sort(SortBuilders
.geoDistanceSort("location", new GeoPoint("31.21, 121.5")) // 也可以使用 String 的 "经度,纬度" 格式
.order(SortOrder.ASC)
.unit(DistanceUnit.KILOMETERS) //
);
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
说明:sort 排序除了可以直接设置字段排序,也可以设置 SortBuilders 对象的排序
创建geoDistanceSort("坐标字段名", 坐标对象或坐标格式文本)
ES会对当前设置的坐标,与字段中的坐标做范围对比。
分页
我们先来看看分页的json代码是如何:
# 分页查询
GET /unsoft/_search
{
"query": {
"match_all": {}
},
"from": 10,
"size": 20,
"sort": [
{
"price": {
"order": "desc"
}
}
]
}
我们发现,分页的 size 和 from 是在 query 之外的另一种辅助属性,所以我们就不能在 query() 方法内构建了,而是在 source() 方法上构建,具体代码:
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
// 设置分页
request.source().from(0).size(5);
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
注意:
在分页中,提供的from 是定位在第多少条,size 是取多少条
在 from 的数据问题上,我们可以通过 (currentPage - 1) * size 来获得
高亮
我们先来看看高亮的json代码是如何:
# 高亮显示多字段检索
GET /unsoft/_search
{
"query": {
"multi_match": {
"query": "金牌",
"fields": [
"username",
"goods_title",
"other"
]
}
},
"highlight": {
"fields": {
"goods_title": {
"pre_tags": "<em>",
"post_tags": "</em>",
"require_field_match": "false"
}
}
}
}
我们发现,高亮的 highlight 是在 query 之外的另一种辅助属性,所以我们就不能在 query() 方法内构建了,而是在 source() 方法上构建,具体代码:
// 1.准备Request
SearchRequest request = new SearchRequest("hotel");
request.source().highlighter(new HighlightBuilder()
.field("name")
// 是否需要与查询字段匹配
.requireFieldMatch(false)
);
// 3.发送请求,得到响应结果
SearchResponse response = client.search(request, RequestOptions.DEFAULT);
在 response 中要获取经过高亮处理的数据,我们不能通过 sourse 中获取了,而是需要获取 hits.hits.highlight 中获取
如下是一个 response 的 json 数据:
{
"took" : 2,
"timed_out" : false,
"_shards" : {
"total" : 1,
"successful" : 1,
"skipped" : 0,
"failed" : 0
},
"hits" : {
"total" : {
"value" : 1,
"relation" : "eq"
},
"max_score" : 0.7102385,
"hits" : [
{
"_index" : "unsoft",
"_type" : "_doc",
"_id" : "10086",
"_score" : 0.7102385,
"_source" : {
"id" : "10086",
"username" : "TZMing",
"goods_title" : "金牌全模组小1U FLEX 7660B 450 500 600 700W 益衡方案 itx电源",
"shop" : "电竞艾派",
"other" : "电竞艾派"
},
"highlight" : {
"goods_title" : [
"<em>金牌</em>全模组小1U FLEX 7660B 450 500 600 700W 益衡方案 itx电源"
]
}
}
]
}
}
在代码中如下操作获取:
// 获取 hit 字段数据
hit = response.getHits().getHits()
// 处理高亮,获取 highlight 中的字段,是一个Map
Map<String, HighlightField> highlightFields = hit.getHighlightFields();
if (!CollectionUtils.isEmpty(highlightFields)) {
// 获取高亮字段结果
HighlightField highlightField = highlightFields.get("name"); // name 为设置了高亮的字段名
if (highlightField != null) {
// 取出高亮结果数组中的第一个,就是酒店名称
String name = highlightField.getFragments()[0].string();
hotelDoc.setName(name);
}
}
算分检索
我们先来看看使用算分检索的json代码:
GET /unsoft/_search
{
"query": {
"function_score": {
"query": { # 设置检索区
"match": {
"goods_title": "金牌"
}
},
"functions": [
{
"filter": { # 设置过滤区
"term": {
"goods_title": "电源"
}
},
"weight": 10 # 设置算分的值
}
],
"boost_mode": "multiply" # 设置算分处理区
}
}
}
可以看到 算分方式分为三个区域,分别是文档检索区、过滤区、算分处理区,这三个区都处在 query 中
在代码中,我们需要用 QueryBuilder 对象创建 functionScoreQuery 包裹这三个区域
FunctionScoreQueryBuilder functionScoreQueryBuilder =
// 使用QueryBuilder.functionScoreQuery 创建一个查询区
QueryBuilders.functionScoreQuery(
// 第一个区域,文档检索区,用于检索匹配的文档
QueryBuilders.matchQuery("goods_title", "金牌"),
// 第二个区域,过滤区,用于定义检索出来的文档中哪些适合算分操作
new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
// 配置一个 过滤对象
new FunctionScoreQueryBuilder.FilterFunctionBuilder(
QueryBuilders.termQuery("goods_title", "电源"),
ScoreFunctionBuilders.weightFactorFunction(10)
)
}
);
sourceBuilder.query(functionScoreQueryBuilder);
共有 0 条评论