Elasticsearch

ElasticSearch 7.14-分布式搜索引擎

  1. 全文检索
  2. 简介
  3. 安装
  4. kibana
  5. 核心概念 索引 映射 文档
  6. 高级查询 Query DSL
  7. 索引原理
  8. 分词器
  9. 过滤查询
  10. 聚合查询
  11. 整合应用
  12. 集群

全文检索

全文检索是计算机程序通过扫描文章中的每一个词,对每一个词建立一个索引,指明该词在文章中出现的次数和位置。当用户查询时根据建立的索引查找,类似于通过字典的检索字表查字的过程。

索: 建立索引 文本---->切分 —> 词 文章出现过 出现多少次

检索: 查询 关键词—> 索引中–> 符合条件文章 相关度排序

全文检索(Full-Text Retrieval)以文本作为检索对象,找出含有指定词汇的文本。全面、准确和快速是衡量全文检索系统的关键指标。

  • 只处理文本、不处理语义
  • 搜索时英文不区分大小写
  • 结果列表有相关度排序

简介

什么是ElasticSearch

ElasticSearch 简称 ES ,是基于Apache Lucene构建的开源搜索引擎,是当前最流行的企业级搜索引擎Lucene本身就可以被认为迄今为止性能最好的一款开源搜索引擎工具包,但是lucene的API相对复杂,需要深厚的搜索理论。很难集成到实际的应用中去。ES是采用java语言编写,提供了简单易用的RestFul API,开发者可以使用其简单的RestFul API,开发相关的搜索功能,从而避免lucene的复杂性

ElasticSearch诞生

​ 多年前,一个叫做Shay Banon的刚结婚不久的失业开发者,由于妻子要去伦敦学习厨师,他便跟着也去了。在他找工作的过程中,为了给妻子构建一个食谱的搜索引擎,他开始构建一个早期版本的Lucene

直接基于Lucene工作会比较困难,所以Shay开始抽象Lucene代码以便Java程序员可以在应用中添加搜索功能。他发布了他的第一个开源项目,叫做“Compass”。

后来Shay找到一份工作,这份工作处在高性能和内存数据网格的分布式环境中,因此高性能的、实时的、分布式的搜索引擎也是理所当然需要的。然后他决定重写Compass库使其成为一个独立的服务叫做Elasticsearch

第一个公开版本出现在2010年2月,在那之后Elasticsearch已经成为Github上最受欢迎的项目之一,代码贡献者超过300人。一家主营Elasticsearch的公司就此成立,他们一边提供商业支持一边开发新功能,不过Elasticsearch将永远开源且对所有人可用。

Shay的妻子依旧等待着她的食谱搜索……

目前国内大厂几乎无一不用Elasticsearch,阿里,腾讯,京东,美团 等等 …

安装

  • 传统方式安装 下载安装包—> 平台 window macos linux(ubuntu)
  • Docker 方式安装 推荐

传统方式安装[只能在普通用户下启动es]

# 0.环境准备
- centos7.x+、ubuntu、windows、macos
- 安装jdk11.0+ 并配置环境变量 jdk11   要求安装jdk11+以上版本的java环境 

# 0.1 安装jdk11.0+以上版本的java环境

# 1.下载ES
- https://www.elastic.co/cn/start

image-20210816103401531

# 2.安装ES不能使用root用户,创建普通用户,使用普通用户安装
# 添加用户名
$ useradd lb
# 修改密码
$ passwd lb

# 之后用ssh工具使用普通用户登录并将elasticsearch安装包上传到linux中

image-20220920081009466

# 3.解压缩ES安装包
$ tar -zxvf elasticsearch-7.14.0-linux-x86_64.tar.gz 
$ ll
总用量 650168
drwxr-xr-x. 10 chenyn chenyn       167 816 11:07 elasticsearch-7.14.0
# 4.查看ES解压包中目录结构
[chenyn@localhost elasticsearch-7.14.0]$ ll
- bin 		启动ES服务脚本目录
- config  ES配置文件的目录
- data    ES的数据存放目录
- jdk     ES提供需要指定的jdk目录
- lib     ES依赖第三方库的目录
- logs    ES的日志目录
- modules 模块的目录
- plugins 插件目录

image-20220920081600290

# 5.启动ES服务  
# 前台方式启动elasticsearch
./elasticsearch-7.14.0/bin/elasticsearch

# 后台方式启动elasticsearch,不占用窗口
 ./elasticsearch-7.14.0/bin/elasticsearch -d
 
# 后台启动如何停止elasticsearch服务
 ps aux|grep elasticsearch                -- 搜索elasticsearch进程id
 kill -9 es的进程id											  -- 杀死es进程

image-20210816132834613

image-20220920085731839

- 这个错误是系统jdk版本与es要求jdk版本不一致,es默认需要jdk11以上版本,当前系统使用的jdk8,需要从新安装jdk11才行!
- 解决方案:
	1.安装jdk11+ 配置环境变量、
	2.ES包中jdk目录就是es需要的jdk,只需要将这个目录配置到ES_JAVA_HOME环境变即可、
# 6.配置环境变量
$ vim /etc/profile
- export ES_JAVA_HOME=指定为ES安装目录中jdk目录
- source /etc/profile

image-20210816135925943

# 7.重新启动ES服务 

注:前台方式启动的话,如果想停止直接 ctrl + c

image-20220920084057146

# 8.ES启动默认监听9200端口,访问9200
$ curl http://localhost:9200

image-20220920084233987

开启远程访问
# 0. 注:开启elasticsearch远程访问权限时需要提前关闭elasticsearch服务

# 1.默认ES无法使用主机ip进行远程连接,需要开启远程连接权限
- 修改ES安装包中config/elasticsearch.yml配置文件
$ cd elasticsearch-7.14.0/config
$ vim elasticsearch.yml

image-20220920104348395

# 2.重新启动ES服务
- ./elasticsearch
- 启动出现如下错误:
	`bootstrap check failure [1] of [4]: max file descriptors [4096] for elasticsearch process is too low, increase to at least [65535]
	`bootstrap check failure [2] of [4]: max number of threads [3802] for user [chenyn] is too low, increase to at least [4096]
	`bootstrap check failure [3] of [4]: max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]
	`bootstrap check failure [4] of [4]: the default discovery settings are unsuitable for production use; at least one of [discovery.seed_hosts, discovery.seed_providers

image-20220920111107918

# 3.解决错误 1   [最大虚拟内存]  注:需要在root用户下修改错误,因为普通用户不允许修改
$ vim /etc/security/limits.conf
# 在最后面追加下面内容
*               soft    nofile          65536
*               hard    nofile          65536
*               soft    nproc           4096
*               hard    nproc           4096
# 退出重新登录检测配置是否生效:
ulimit -Hn
ulimit -Sn
ulimit -Hu
ulimit -Su

image-20220920114226482

# 3.解决错误 2 									注:需要在root用户下修改错误,因为普通用户不允许修改
#进入limits.d目录下修改配置文件。
$ vim /etc/security/limits.d/20-nproc.conf
# 修改为 
启动ES用户名 soft nproc 4096

例: lb soft nproc 4096

image-20220920114108204

# 3.解决错误 3									注:需要在root用户下修改错误,因为普通用户不允许修改
# 编辑sysctl.conf文件
$ vim /etc/sysctl.conf
vm.max_map_count=655360 		#centos7 系统
// vm.max_map_count=262144     #ubuntu 系统
# 执行以下命令生效:
$ sysctl -p

image-20220920114019894

# 3.解决错误 4									注: 在普通用户下修改
# 编辑elasticsearch.yml配置文件 (es默认是以集群方式启动,修改配置文件使其以单节点方式启动)
$ vim elasticsearch-7.14.0/config/elasticsearch.yml 
cluster.initial_master_nodes: ["node-1"]

image-20220920113745016

# 4.重启启动ES服务,并通过浏览器访问   注:只有普通用户才能启动es,root用户不能启动es
192.168.204.137:9200
{
  "name" : "localhost.localdomain",
  "cluster_name" : "elasticsearch",
  "cluster_uuid" : "GgxAPEXTSWCZN6vES6uvvQ",
  "version" : {
    "number" : "7.14.0",
    "build_flavor" : "default",
    "build_type" : "tar",
    "build_hash" : "dd5a0a2acaa2045ff9624f3729fc8a6f40835aa1",
    "build_date" : "2021-07-29T20:49:32.864135063Z",
    "build_snapshot" : false,
    "lucene_version" : "8.9.0",
    "minimum_wire_compatibility_version" : "6.8.0",
    "minimum_index_compatibility_version" : "6.0.0-beta1"
  },
  "tagline" : "You Know, for Search"
}

image-20220920115253378

Docker方式安装 [可以在root用户下启动es]

# 1.获取镜像
- docker pull elasticsearch:7.14.0

# 2.运行es
- docker run -d -p 9200:9200 -p 9300:9300  -e "discovery.type=single-node"  elasticsearch:7.14.0

# 3.访问ES
- http://192.168.204.137:9200

image-20220920121402468

Kibana

简介

Kibana Navicat是一个针对Elasticsearch mysql开源分析及可视化平台,使用Kibana可以查询、查看并与存储在ES索引的数据进行交互操作,使用Kibana能执行高级的数据分析,并能以图表、表格和地图的形式查看数据。

安装

Kibana的版本一定要和Elasticsearch版本一致, 比如 ES 版本是 7.14.0,Kibana的版本一定也要是 7.14.0 ,与其对应。

Kibana的安装要求ES必须先启动起来,因为ES只能在普通用户启动,所以Kibana也只能在普通用户启动(传统方式,Kibbna只能以普通用户启动,如果使用docker方式安装可以使用root用户启动)。

传统方式安装

# 1. 下载Kibana并上传到Linux中
- https://www.elastic.co/downloads/kibana

# 2. 安装下载的kibana
- $ tar -zxvf kibana-7.14.0-linux-x86_64.tar.gz 
  
# 3. 编辑kibana配置文件
- $ cd kibana-7.14.0-linux-x86_64/config/								-- 进入安装目录中的配置目录
- $ vim kibana.yml																	    -- 编辑配置文件
# 4. 修改如下配置
- server.host: "0.0.0.0"                								 -- 开启kibana远程访问
- elasticsearch.hosts: ["http://localhost:9200"]   			 -- ES服务器地址

image-20220920142532962

# 5. 启动kibana
- cd kibana-7.14.0-linux-x86_64/bin
- ./kibana

# 6. 访问kibana的web界面  
- http://192.168.204.137:5601/         # kibana默认端口为5601,使用云服务器的话记得开启5601端口  
Docker方式安装
# 1.获取镜像
- docker pull kibana:7.14.0

# 2.运行kibana
- docker run -d  --name kibana -p 5601:5601 kibana:7.14.0

# 3.进入容器连接到ES,重启kibana容器,访问
- docker exec -it kibbna的容器id bash					-- 进入Kibbna容器内部
- cd config   																-- 进入Kibbna容器内部的配置文件目录
- vi kibana.yml															-- 编辑配置文件

修改配置文件,指明elasticsearch服务所在的地址

image-20220920153216956

# 4. 重启Kibbna
docker restart Kibbna容器的id

# 5. 浏览器访问查看是否启动成功
- http://192.168.204.137:5601


注
# 6.基于数据卷加载配置文件方式运行[启动Kibbna之后无需再修改配置文件再重启]
- a.从容器复制kibana配置文件出来
- b.修改配置文件为对应ES服务器地址
- c.通过数据卷加载配置文件方式启动
  `docker run -d -v /root/kibana.yml:/usr/share/kibana/config/kibana.yml  --name kibana -p 5601:5601 kibana:7.14.0
compose方式安装
# 创建 docker-compose.yml 配置文件
vim docker-compose.yml
version: "3.8"
volumes:
  data:
  config:
  plugin:
networks:
  es:
services:
  elasticsearch:
    image: elasticsearch:7.14.0
    ports:
      - "9200:9200"
      - "9300:9300"
    networks:
      - "es"
    environment:
      - "discovery.type=single-node"
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - data:/usr/share/elasticsearch/data
      - config:/usr/share/elasticsearch/config
      - plugin:/usr/share/elasticsearch/plugins

  kibana:
    image: kibana:7.14.0
    ports:
      - "5601:5601"
    networks:
      - "es"
    volumes:
      - ./kibana.yml:/usr/share/kibana/config/kibana.yml
# kibana配置文件 kibana.yml ,连接到ES
vim kibana.yml


server.host: "0"
server.shutdownTimeout: "5s"
elasticsearch.hosts: [ "http://elasticsearch:9200" ]
monitoring.ui.container.elasticsearch.enabled: true


#	注:因为docker-compose里有elasticsearch和Kibbna两个服务,两个服务之间可以使用服务名表示服务的ip地址,所以上面配置文件中的elasticsearch在启动时候会转化为elasticsearch所在服务的地址.

# 使用 docker-compose up -d   启动当前目录的docker-compose.yml配置的docker-compose

核心概念

索引

一个索引就是一个拥有几分相似特征的文档的集合。比如说,你可以有一个商品数据的索引,一个订单数据的索引,还有一个用户数据的索引。一个索引由一个名字来标识(必须全部是小写字母的)**,**并且当我们要对这个索引中的文档进行索引、搜索、更新和删除的时候,都要使用到这个名字。

映射

映射是定义一个文档和它所包含的字段如何被存储和索引的过程。在默认配置下,ES可以根据插入的数据自动地创建mapping,也可以手动创建mapping。 mapping中主要包括字段名字段类型

文档

文档是索引中存储的一条条数据。一条文档是一个可被索引的最小单元。ES中的文档采用了轻量级的JSON格式数据来表示。


基本操作

索引

创建
# 1.创建索引
- PUT /索引名 ====> PUT /products
- 注意: 
		1.ES中索引健康转态  red(索引不可用) 、yellwo(索引可用,存在风险)、green(健康)
		2.默认ES在创建索引时会为索引创建1个备份索引和一个primary索引
		
# 2.创建索引 进行索引分片配置
PUT /orders
{
  "settings": {
    "number_of_shards": 1, #指定主分片的数量
    "number_of_replicas": 0 #指定副本分片的数量
  }
}


注:
/*

"number_of_shards": 1,  	 	表示主分片的数量
"number_of_replicas": 0    	表示指定副本分片的数量

*/

image-20211020205824120

image-20220921072849190

查询
# 查看es中的索引
GET /_cat/indices

# 查询索引(显示返回结果的标题)
- GET /_cat/indices?v

image-20211020210043010

删除
# 3.删除索引
- DELETE /索引名 =====> DELETE /products
- DELETE /*     `*代表通配符,代表所有索引`

image-20211020205934645

注:索引只能被创建、查询、删除,不能被修改

映射

注:映射是不允许删除和修改的,如果创建的时候出现问题,那就只能删除索引之后重新创建

创建

字符串类型: keyword 关键字 关键词 、text 一段文本

数字类型:integer 、long

小数类型:float 、double

布尔类型:boolean

日期类型:date

# 1.创建索引&映射
PUT /products
{ 
  "settings": {
    "number_of_shards": 1,
    "number_of_replicas": 0
  }, 
  "mappings": {
    "properties": {
      "title":{
        "type": "keyword"
      },
      "price":{
        "type": "double"
      },
      "created_at":{
        "type": "date"
      },
      "description":{
        "type": "text"
      }
    }
  }
}

image-20211020211425811

说明: ES中支持字段类型非常丰富,如:text、keyword、integer、long、ip 等。更多参见https://www.elastic.co/guide/en/elasticsearch/reference/7.15/mapping-types.html

查询
# 1.查看某个索引的映射
- GET /索引名/_mapping =====> GET /products/_mapping

image-20211020211531271

文档

添加文档
# 添加文档操作,手动指定文档_id
POST /products/_doc/1  
{
  "title":"iphone13",
  "price":8999.99,
  "created_at":"2021-09-15",
  "description":"iPhone 13屏幕采用6.1英寸OLED屏幕。"
}
# 添加文档操作,自动生成文档_id
POST /products/_doc/ 
{
  "title":"iphone14",
  "price":8999.99,
  "created_at":"2021-09-15",
  "description":"iPhone 13屏幕采用6.8英寸OLED屏幕"
}
# 添加文档之后返回的结果
{
  "_index" : "products",
  "_type" : "_doc",
  "_id" : "sjfYnXwBVVbJgt24PlVU",
  "_version" : 1,
  "result" : "created",
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 3,
  "_primary_term" : 1
}
查询文档
GET /products/_doc/1
{
  "_index" : "products",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 1,
  "_seq_no" : 0,
  "_primary_term" : 1,
  "found" : true,
  "_source" : {
    "title" : "iphone13",
    "price" : 8999.99,
    "created_at" : "2021-09-15",
    "description" : "iPhone 13屏幕采用6.1英寸OLED屏幕"
  }
}
删除文档
DELETE /products/_doc/1
{
  "_index" : "products",
  "_type" : "_doc",
  "_id" : "1",
  "_version" : 2,
  "result" : "deleted",
  "_shards" : {
    "total" : 1,
    "successful" : 1,
    "failed" : 0
  },
  "_seq_no" : 2,
  "_primary_term" : 1
}
更新文档

说明: 这种更新方式是先删除原始文档,再将更新文档以新的内容插入,所以再查询时其他没更新的字段会变为空。

PUT /products/_doc/sjfYnXwBVVbJgt24PlVU
{
  "title":"iphon15"
}


/*
sjfYnXwBVVbJgt24PlVU 表示文档的id
*/

说明: 这种方式会将数据原始内容保存,并在此基础上更新,也就是说其他字段会保留。


POST /products/_doc/sjfYnXwBVVbJgt24PlVU/_update
{
    "doc" : {
        "title" : "iphon15"
    }
}

/*
sjfYnXwBVVbJgt24PlVU 表示文档的id
*/
批量操作

所谓批量操作顾名思义就是把一系列数据一次性的放入es中,我们可以对一批数据进行相应的添加、删除、或者更新操作。

在进行批量操作时,会涉及到一些含义

  • index :表示添加
  • update:表示更新
  • delte:表示删除
# 批量添加两条文档
POST /products/_doc/_bulk 
 	{"index":{"_id":"1"}}
  		{"title":"iphone14","price":8999.99,"created_at":"2021-09-15","description":"iPhone 13屏幕采用6.8英寸OLED屏幕"}
	{"index":{"_id":"2"}}
  		{"title":"iphone15","price":8999.99,"created_at":"2021-09-15","description":"iPhone 15屏幕采用10.8英寸OLED屏幕"}
# 更新文档并删除另一个文档
POST /products/_doc/_bulk 
	{"update":{"_id":"1"}}
		{"doc":{"title":"iphone17"}}
	{"delete":{"_id":2}}
	{"index":{}}
		{"title":"iphone19","price":8999.99,"created_at":"2021-09-15","description":"iPhone 19屏幕采用61.8英寸OLED屏幕"}

说明:批量时不会因为一个失败而全部失败,而是继续执行后续操作,在返回时按照执行的状态返回。每一个操作之间是互不影响的,也就是说一个操作失败了,并不会影响其他的操作!

注:es文档的批量操作中涉及一个文档的内容不能换行

image-20220921090520452

高级查询 Query DSL

说明

​ ES中提供了一种强大的检索数据方式,这种检索方式称之为Query DSL ,Query DSL是利用Rest API传递JSON格式的请求体(Request Body)数据与ES进行交互,这种方式的丰富查询语法让ES检索变得更强大,更简洁

语法

# GET /索引名/_doc/_search {json格式请求体数据}
# GET /索引名/_search {json格式请求体数据}					    // 简写
  • 测试数据
# 1.创建索引 映射
PUT /products/
{
  "mappings": {
    "properties": {
      "title":{
        "type": "keyword"
      },
      "price":{
        "type": "double"
      },
      "created_at":{
        "type":"date"
      },
      "description":{
        "type":"text"
      }
    }
  }
}
# 2.插入数据
PUT /products/_doc/_bulk
{"index":{}}
  {"title":"iphone12 pro","price":8999,"created_at":"2020-10-23","description":"iPhone 12 Pro采用超瓷晶面板和亚光质感玻璃背板,搭配不锈钢边框,有银色、石墨色、金色、海蓝色四种颜色。宽度:71.5毫米,高度:146.7毫米,厚度:7.4毫米,重量:187克"}
{"index":{}}
  {"title":"iphone12","price":4999,"created_at":"2020-10-23","description":"iPhone 12 高度:146.7毫米;宽度:71.5毫米;厚度:7.4毫米;重量:162克(5.73盎司) [5]  。iPhone 12设计采用了离子玻璃,以及7000系列铝金属外壳。"}
{"index":{}}
  {"title":"iphone13","price":6000,"created_at":"2021-09-15","description":"iPhone 13屏幕采用6.1英寸OLED屏幕;高度约146.7毫米,宽度约71.5毫米,厚度约7.65毫米,重量约173克。"}
{"index":{}}
  {"title":"iphone13 pro","price":8999,"created_at":"2021-09-15","description":"iPhone 13Pro搭载A15 Bionic芯片,拥有四种配色,支持5G。有128G、256G、512G、1T可选,售价为999美元起。"}

常见检索

查询所有[match_all]

match_all关键字: 返回索引中的全部文档

GET /products/_search
{
  "query": {
    "match_all": {}
  }
}

/*
products 是索引的名字,其它固定
*/
关键词查询(term)

term 关键字: 用来使用关键词查询

GET /products/_search
{
 "query": {
   "term": {
     "price": {
       "value": 4999
     }
   }
 }
}

/*
products表示索引名
price表示是哪个字段的名字
value后面的值表示这个字段的值是什么

比如有的文档字段值为 小浣熊、小小浣熊,我们可以试着搜一搜 浣熊 这个词
*/

NOTE0: 在ES中只有text类型分词,其余类型均不分词。分词的类型可以根据分词查询,不分词的类型只能通过全部内容查询。

NOTE1: 通过使用term查询得知ES中默认使用分词器为标准分词器(StandardAnalyzer),标准分词器对于英文单词分词,对于中文单字分词。 (text 类型)

NOTE2: 通过使用term查询得知,在ES的Mapping Type 中 keyword , date ,integer, long , double , boolean or ip 这些类型不分词,搜索的时候要使用全部内容搜索,只有text类型分词

范围查询[range]

range 关键字: 用来指定查询指定范围内的文档

GET /products/_search
{
  "query": {
    "range": {
      "price": {
        "gte": 5,
        "lte": 7.8
      }
    }
  }
}

/*
price 表示搜索的字段,一般是数字类型,因为这样的类型会有一个范围。
gte 表示大于等于
lte 表示小于等于
*/
前缀查询[prefix]

prefix 关键字: 用来检索含有指定前缀的关键词的相关文档

GET /products/_search
{
  "query": {
    "prefix": {
      "title": {
        "value": "ipho"
      }
    }
  }
}
通配符查询[wildcard]

wildcard 关键字: 通配符查询 ? 用来匹配一个任意字符 * 用来匹配多个任意字符

GET /products/_search
{
  "query": {
    "wildcard": {
      "description": {
        "value": "iphon*"
      }
    }
  }
}
多id查询[ids]

ids 关键字 : 值为数组类型,用来根据一组id获取多个对应的文档

GET /products/_search
{
  "query": {
    "ids": {
      "values": [1,2,4]
    }
  }
}
模糊查询[fuzzy]

fuzzy 关键字: 用来模糊查询含有指定关键字的文档

GET /products/_search
{
  "query": {
    "fuzzy": {
      "description": "小浣猫"
    }
  }
}

注意: fuzzy 模糊查询 最大模糊错误 必须在0-2之间

  • 搜索关键词长度小于等于 2 不允许存在模糊

  • 搜索关键词长度为3-5 允许一次模糊

  • 搜索关键词长度大于5 允许最大2模糊

布尔查询[bool]

bool 关键字: 用来组合多个条件实现复杂查询

must: 相当于&& 同时成立

should: 相当于|| 成立一个就行

must_not: 相当于! 不能满足任何一个

GET /products/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "price": {
            "value": 4999
          }
        }},{
        	"ids":{
        		"value": [1]
        	}
        }
      ]
    }
  }
}
多字段查询[multi_match]
GET /products/_search
{
  "query": {
    "multi_match": {
      "query": "iphone13 毫",
      "fields": ["title","description"]
    }
  }
}

query  后面的内容表示检索的关键词
fields 后面的内容表示检索的字段

注意: 如果字段类型分词,将查询条件分词之后再查询该字段  如果该字段不分词就会将查询条件作为整体进行查询
默认字段分词查询[query_string]
GET /products/_search
{
  "query": {
    "query_string": {
      "default_field": "description",
      "query": "屏幕真的非常不错"
    }
  }
}

default_field 后面写查询字段的类型
query					后面写查询的关键词

注意: 查询字段分词就将查询条件分词查询  查询字段不分词将查询条件不分词查询
高亮查询[highlight]

注:在ES7中只有能分词的字段才可以使用高亮,即text类型

高亮并没有直接修改原始文档,而是单独的把高亮的结果又放到了一个highlight里

highlight 关键字: 可以让符合条件的文档中的关键词高亮

GET /products/_search
{
  "query": {
    "term": {
      "description": {
        "value": "iphone"
      }
    }
  },
  "highlight": {
    "fields": {
      "*":{}
    }
  }
}

自定义高亮html标签: 可以在highlight中使用pre_tagspost_tags

GET /products/_search
{
  "query": {
    "term": {
      "description": {
        "value": "iphone"
      }
    }
  },
  "highlight": {
    "post_tags": ["</span>"], 
    "pre_tags": ["<span style='color:red'>"],
    "fields": {
      "*":{}
    }
  }
}

多字段高亮 使用require_field_match开启多个字段高亮

GET /products/_search
{
  "query": {
    "term": {
      "description": {
        "value": "iphone"
      }
    }
  },
  "highlight": {
    "require_field_match": "false",
    "post_tags": ["</span>"], 
    "pre_tags": ["<span style='color:red'>"],
    "fields": {
      "*":{}
    }
  }
}
返回指定条数[size]

size 关键字: 指定查询结果中返回指定条数。 默认返回值10条

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 5
}
分页查询[form]

from 关键字: 用来指定起始返回位置,和size关键字连用可实现分页效果

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "size": 5,
  "from": 0
}
指定字段排序[sort]
GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "sort": [
    {
      "price": {
        "order": "desc"
      }
    }
  ]
}

/*
desc		降序,默认就是降序
asc			升序
*/
返回指定字段[_source]

_source 关键字: 是一个数组,在数组中用来指定展示那些字段

GET /products/_search
{
  "query": {
    "match_all": {}
  },
  "_source": ["title","description"]
}

索引原理

倒排索引

倒排索引(Inverted Index)也叫反向索引,有反向索引必有正向索引。通俗地来讲,正向索引是通过key找value,反向索引则是通过value找key。ES底层在检索时底层使用的就是倒排索引。

ES的索引区部件存储了value-key这样的映射,还会存储每个字出现的次数以及文章的长度用来计算相关度的得分.

ES的元数据区用来存储一条一条完整的文档.

索引模型

现有索引和映射如下:

{
  "products" : {
    "mappings" : {
      "properties" : {
        "description" : {
          "type" : "text"
        },
        "price" : {
          "type" : "float"
        },
        "title" : {
          "type" : "keyword"
        }
      }
    }
  }
}

先录入如下数据,有三个字段title、price、description等

_idtitlepricedescription
1蓝月亮洗衣液19.9蓝月亮洗衣液高效
2iphone1319.9不错的手机
3小浣熊干脆面1.5小浣熊好吃

在ES中除了text类型分词,其他类型不分词,因此根据不同字段创建索引如下:

  • title字段:

    term_id(文档id)
    蓝月亮洗衣液1
    iphone132
    小浣熊干脆面3
  • price字段

    term_id(文档id)
    19.9[1,2]
    1.53
  • description字段

    term_idterm_idterm_id
    123
    123
    123
    123
    123
    1
    [1,2,3]
    1
    1

注意: Elasticsearch分别为每个字段都建立了一个倒排索引。因此查询时查询字段的term,就能知道文档ID,就能快速找到文档。

image-20220922071700110

分词器

Analysis(文本分析) 和 Analyzer(分词器)

Analysis: 文本分析是把全文本转换一系列单词(term/token)的过程,也叫分词(Analyzer)。Analysis是通过Analyzer来实现的分词就是将文档通过Analyzer分成一个一个的Term(关键词),每一个Term都指向包含这个Term的文档

Analyzer 组成

  • ​ 注意: 在ES中默认使用标准分词器: StandardAnalyzer 特点: 中文单字分词 单词分词

    我是中国人 this is good man----> analyzer----> 我 是 中 国 人 this is good man

分析器(analyzer)都由三种构件组成的:character filterstokenizerstoken filters

  • character filter 字符过滤器

    • 在一段文本进行分词之前,先进行预处理,比如说最常见的就是,过滤html标签(hello --> hello),& --> and(I&you --> I and you)
  • tokenizers 分词器

    • 英文分词可以根据空格将单词分开,中文分词比较复杂,可以采用机器学习算法来分词。
  • Token filters Token过滤器

    • 将切分的单词进行加工。大小写转换(例将“Quick”转为小写),去掉停用词(例如停用词像“a”、“and”、“the”等等),加入同义词(例如同义词像“jump”和“leap”)。

注意:

  • 三者顺序: Character Filters—>Tokenizer—>Token Filter
  • 三者个数:Character Filters(0个或多个) + Tokenizer + Token Filters(0个或多个)

内置分词器

  • Standard Analyzer - 默认分词器,英文按单词词切分,并小写处理
  • Simple Analyzer - 按照单词切分(标点符号被过滤), 小写处理
  • Stop Analyzer - 小写处理,停用词过滤(the,a,is)
  • Whitespace Analyzer - 按照空格切分,不转小写
  • Keyword Analyzer - 不分词,直接将输入当作输出

内置分词器测试

  • 标准分词器
    • 特点: 按照单词分词 英文统一转为小写 过滤标点符号 中文单字分词
POST /_analyze
{
  "analyzer": "standard",
  "text": "this is a , good Man 中华人民共和国"
}
  • Simple 分词器
    • 特点: 英文按照单词分词 英文统一转为小写 去掉符号 中文按照空格进行分词
POST /_analyze
{
  "analyzer": "simple",
  "text": "this is a , good Man 中华人民共和国"
}
  • Whitespace 分词器
    • 特点: 中文 英文 按照空格分词 英文不会转为小写 不去掉标点符号
POST /_analyze
{
  "analyzer": "whitespace",
  "text": "this is a , good Man 中华 人民共和国"
}

创建索引并设置分词器

PUT /索引名
{
  "settings": {},
  "mappings": {
    "properties": {
      "title":{
        "type": "text",
        "analyzer": "standard" //显示指定分词器
      }
    }
  }
}

中文分词器

在ES中支持中文分词器非常多 如 smartCNIK 等,推荐的就是 IK分词器

安装IK

开源分词器 Ik 的github:https://github.com/medcl/elasticsearch-analysis-ik

  • 注意 IK分词器的版本要与你安装ES的版本一致
  • 注意 Docker 容器运行 ES 安装插件目录为 /usr/share/elasticsearch/plugins
# 一、普通方式安装
# 1. 下载对应版本
- [es@linux ~]$ wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.14.0/elasticsearch-analysis-ik-7.14.0.zip

# 2. 创建一个文件夹ik-7.14.0并将ik分词器压缩包解压到这个文件夹,然后上传到linux中

# 3. 移动到es安装目录的plugins目录中
- [es@linux ~]$ ls elasticsearch-7.14.0/plugins/
	[es@linux ~]$ mv elasticsearch elasticsearch-7.14.0/plugins/
	[es@linux ~]$ ls elasticsearch-7.14.0/plugins/
	 	elasticsearch
	[es@linux ~]$ ls elasticsearch-7.14.0/plugins/elasticsearch/
		commons-codec-1.9.jar    config                               httpclient-4.5.2.jar  		plugin-descriptor.properties
		commons-logging-1.2.jar  elasticsearch-analysis-ik-6.2.4.jar  httpcore-4.4.4.jar
		
# 4. 重启es生效

# 5. 本地安装ik配置目录为  
- es安装目录中/plugins/analysis-ik/config/IKAnalyzer.cfg.xml
# 二、docker方式安装
# 1. 下载对应版本

# 2. 创建一个文件夹ik-7.14.0并将ik分词器压缩包解压到这个文件夹,然后上传到linux中docker-compose同目录中
			
# 3. 编写docker-compose采用数据卷映射方式将ik分词器映射到容器内部
version: "3.8"
volumes:
  data:
  config:
networks:
  es:
services:
  elasticsearch:
    image: elasticsearch:7.14.0
    ports:
      - "9200:9200"
      - "9300:9300"
    networks:
      - "es"
    environment:
      - "discovery.type=single-node"
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    volumes:
      - data:/usr/share/elasticsearch/data
      - config:/usr/share/elasticsearch/config
      - ./ik-7.14.0:/usr/share/elasticsearch/plugins/ik-7.14.0

  kibana:
    image: kibana:7.14.0
    ports:
      - "5601:5601"
    networks:
      - "es"
    volumes:
      - ./kibana.yml:/usr/share/kibana/config/kibana.yml
# 4. 重新启动
docker-compose up -d
IK使用

IK有两种颗粒度的拆分:

  • ik_smart: 会做最粗粒度的拆分

  • ik_max_word: 会将文本做最细粒度的拆分

POST /_analyze
{
  "analyzer": "ik_smart",
  "text": "中华人民共和国国歌"
}

image-20211024212944413

POST /_analyze
{
  "analyzer": "ik_max_word",
  "text": "中华人民"
}

image-20211024213133835

扩展词、停用词配置

IK支持自定义扩展词典停用词典

  • **扩展词典**就是有些词并不是关键词,但是也希望被ES用来作为检索的关键词,可以将这些词加入扩展词典。
  • **停用词典**就是有些词是关键词,但是出于业务场景不想使用这些关键词被检索到,可以将这些词放入停用词典。

定义扩展词典和停用词典可以修改IK分词器中config目录中IKAnalyzer.cfg.xml这个文件。

1. 编辑vim 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">extra_main.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典-->
        <entry key="ext_stopwords">extra_stopword.dic</entry>
        <!--用户可以在这里配置远程扩展字典 -->
        <!-- <entry key="remote_ext_dict">words_location</entry> -->
        <!--用户可以在这里配置远程扩展停止词字典-->
        <!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>

2. 编辑ik分词器目录下config目录中的extra_main.dic文件   编码一定要为UTF-8才能生效
	vim extra_main.dic 			在文章末尾加入扩展词即可,每一行一个词

3. 编辑ik分词器目录下config目录中的extra_stopword.dic文件 
	vim extra_stopword.dic 	在文章末尾加入停用词即可,每一行一个词

4. 重启es生效(如果是docker的话建议删除容器重新启动,因为可能会有缓存)

/*

扩展词的另一个方法是自己在 config 目录下创建一个 myWord.dic 扩展词典,然后在里面加上扩展词,一行一
个,然后修改 IKAnalyzer.cfg.xml 配置文件,添加上自己的扩展词典.
例:
        <!--用户可以在这里配置自己的扩展字典 -->
        <entry key="ext_dict">extra_main.dic;myWord.dic</entry>

*/

注意: 词典的编码必须为UTF-8,否则无法生效!


过滤查询

过滤查询

过滤查询,其实准确来说,ES中的查询操作分为2种: 查询(query)过滤(filter)。查询即是之前提到的query查询,它 (查询)默认会计算每个返回文档的得分,然后根据得分排序。而过滤(filter)只会筛选出符合的文档,并不计算 得分,而且它可以缓存文档 。所以,单从性能考虑,过滤比查询更快。 换句话说**过滤适合在大范围筛选数据,而查询则适合精确匹配数据。一般应用时, 应先使用过滤操作过滤数据, 然后使用查询匹配数据。**

image-20211101113527794

执行顺序:

过滤 --> 查询 --> ES服务

过滤的速度很快,所以我们一般在查询之前都要进行一下过滤,然后再查询,这样会优化许多.

使用

GET /ems/emp/_search
{
  "query": {
    "bool": {
      "must": [
        {"match_all": {}} //查询条件
      ],
      "filter": {....} //过滤条件
  }
}
  • 注意:
    • 在执行 filter 和 query 时,先执行 filter 在执行 query
    • Elasticsearch会自动缓存经常使用的过滤器,以加快性能。

类型

常见过滤类型有: term 、 terms 、ranage、exists、ids等filter。

term 、 terms Filter
# 使用term过滤
GET /ems/emp/_search   
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "name": {
            "value": "小黑"
          }
        }}
      ],
      "filter": {
        "term": {
          "content":"框架"
        }
      }
    }
  }
}
#使用terms过滤
GET /dangdang/book/_search  
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "name": {
            "value": "中国"
          }
        }}
      ],
      "filter": {
        "terms": {
          "content":[
              "科技",
              "声音"
            ]
        }
      }
    }
  }
}
ranage filter
GET /ems/emp/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "name": {
            "value": "中国"
          }
        }}
      ],
      "filter": {
        "range": {
          "age": {
            "gte": 7,
            "lte": 20
          }
        }
      }
    }
  }
}
exists filter

过滤存在指定字段,获取字段不为空的索引记录使用,获取记录中带有指定字段的,比如name、age

GET /ems/emp/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "name": {
            "value": "中国"
          }
        }}
      ],
      "filter": {
        "exists": {
          "field":"description"
        }
      }
    }
  }
}
ids filter

过滤指定 id 的索引记录

GET /ems/emp/_search
{
  "query": {
    "bool": {
      "must": [
        {"term": {
          "name": {
            "value": "中国"
          }
        }}
      ],
      "filter": {
        "ids": {
          "values": ["1","2","3"]
        }
      }
    }
  }
}

整合应用

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

配置客户端

在工厂中创建客户端工具

下面这种方式是将es的主机和端口写死在代码里,如果要修改的话还得找到确切的位置,不利于维护

@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {
    @Override
    @Bean
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo("192.168.204.137:9200")    // es服务的ip和端口
                .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

为了方便以后修改,我们还是建议将es的主机和端口配置到配置文件中,日后注入即可,所以如下:

# 配置文件
# es 主机和端口
elasticsearch.host=192.168.204.137:9200
# 创建客户端
@Configuration
public class RestClientConfig extends AbstractElasticsearchConfiguration {

    @Value("${elasticsearch.host}")
    private String host;

    @Override
    @Bean   
    public RestHighLevelClient elasticsearchClient() {
        final ClientConfiguration clientConfiguration = ClientConfiguration.builder()
                .connectedTo(host)    // es服务的ip和端口
                .build();
        return RestClients.create(clientConfiguration).rest();
    }
}

客户端对象

  • ElasticsearchOperations
  • RestHighLevelClient 推荐
ElasticsearchOperations
  • 特点: 始终使用面向对象方式操作 ES
    • 索引: 用来存放相似文档集合
    • 映射: 用来决定放入文档的每个字段以什么样方式录入到 ES 中 字段类型 分词器…
    • 文档: 可以被索引最小单元 json 数据格式
相关注解
@Document(indexName = "products", createIndex = true)
public class Product {
		@Id
    private Integer id;
    @Field(type = FieldType.Keyword)
    private String title;
    @Field(type = FieldType.Float)
    private Double price;
    @Field(type = FieldType.Text,analyzer="ik_max_word")
    private String description;
  	//get set ...
}
//1. @Document(indexName = "products", createIndex = true) 用在类上 作用:代表一个对象为一个文档
		-- indexName属性: 创建索引的名称
    -- createIndex属性: 是否创建索引
//2. @Id 用在属性上  作用:将对象id字段与ES中文档的_id对应
//3. @Field(type = FieldType.Keyword) 用在属性上 作用:用来描述属性在ES中存储类型以及分词情况
    -- type: 用来指定字段类型
创建 | 更新 文档

save方法当文档id不存在时会添加文档,文档id存在的时候会更新文档.

@Autowired
private ElasticsearchOperations elasticsearchOperations;

@Test
public void testCreate() throws IOException {
  Product product = new Product();
  product.setId(1); //指定id则_id为id(属性上有@Id注解)  不存在id自动生成_id
  product.setTitle("小浣熊干吃面");
  product.setPrice(129.11);
  product.setDescription("小浣熊干吃面真好吃,曾经非常爱吃!");
  elasticsearchOperations.save(product);
}
删除文档
@Test
public void testDelete() {
  Product product = new Product();
  product.setId(1);
  String delete = elasticsearchOperations.delete(product);
  System.out.println(delete);
}
查询文档
@Test
public void testGet() {
  Product product = elasticsearchOperations.get("1", Product.class); // 文档id   文档转换为什么类型
  System.out.println(product);
}
更新文档
 @Test
public void testUpdate() {
  Product product = new Product();
  product.setId(1);
  product.setTitle("怡宝矿泉水");
  product.setPrice(129.11);
  product.setDescription("我们喜欢喝矿泉水,你们喜欢吗....");
  elasticsearchOperations.save(product);//文档不存在添加,存在更新
}
删除所有
@Test
public void testDeleteAll() {
  elasticsearchOperations.delete(Query.findAll(), Product.class);
}
查询所有
@Test
public void testFindAll() {
  SearchHits<Product> productSearchHits = elasticsearchOperations.search(Query.findAll(), Product.class);
  productSearchHits.forEach(productSearchHit -> {
    System.out.println("id: " + productSearchHit.getId());
    System.out.println("score: " + productSearchHit.getScore());
    Product product = productSearchHit.getContent();
    System.out.println("product: " + product);
  });
}

RestHighLevelClient

之前配置了 RestClientConfig , 所以 RestHighLevelClient 会在工厂中注册

注:RestHighLevelClient 在使用的时候可能会出现 java.net.SocketTimeoutException: 5,000 milliseconds timeout on connection http-outgoing-0 [ACTIVE] 异常, 在工厂中创建下面的类即可.

import org.apache.http.HttpResponse;
import org.apache.http.impl.client.DefaultConnectionKeepAliveStrategy;
import org.apache.http.protocol.HttpContext;
import org.elasticsearch.client.RestClientBuilder;
import org.springframework.boot.autoconfigure.elasticsearch.RestClientBuilderCustomizer;
import org.springframework.stereotype.Component;
 
import java.util.concurrent.TimeUnit;
 
@Component
public class MyRestClientBuilderCustomizer implements RestClientBuilderCustomizer {
 
    @Override
    public void customize(RestClientBuilder builder) {
        // keep alive策略
        builder.setHttpClientConfigCallback(httpClientBuilder -> httpClientBuilder.setKeepAliveStrategy(CustomConnectionKeepAliveStrategy.INSTANCE));
    }
 
    public static class CustomConnectionKeepAliveStrategy extends DefaultConnectionKeepAliveStrategy {
 
        public static final CustomConnectionKeepAliveStrategy INSTANCE = new CustomConnectionKeepAliveStrategy();
 
        private CustomConnectionKeepAliveStrategy() {
            super();
        }
 
        /**
         * 最大keep alive的时间(分钟)
         * 这里默认为10分钟,可以根据实际情况设置。可以观察客户端机器状态为TIME_WAIT的TCP连接数,如果太多,可以增大此值。
         */
        private final long MAX_KEEP_ALIVE_MINUTES = 10;
 
        @Override
        public long getKeepAliveDuration(HttpResponse response, HttpContext context) {
            long keepAliveDuration = super.getKeepAliveDuration(response, context);
            // <0 为无限期keepalive
            // 将无限期替换成一个默认的时间
            if(keepAliveDuration < 0){
                return TimeUnit.MINUTES.toMillis(MAX_KEEP_ALIVE_MINUTES);
            }
            return keepAliveDuration;
        }
    }
}
创建索引、映射

@Autowired
private RestHighLevelClient restHighLevelClient;

@Test
public void testCreateIndex() throws IOException {
  CreateIndexRequest createIndexRequest = new CreateIndexRequest("products");    // 索引名字
  // 参数1:指定映射的结构(在kibana写好复制过来)  参数2:指定数据类型
  createIndexRequest.mapping("{\n" +                           // 指定映射
                             "    \"properties\": {\n" +
                             "      \"title\":{\n" +
                             "        \"type\": \"keyword\"\n" +
                             "      },\n" +
                             "      \"price\":{\n" +
                             "        \"type\": \"double\"\n" +
                             "      },\n" +
                             "      \"created_at\":{\n" +
                             "        \"type\": \"date\"\n" +
                             "      },\n" +
                             "      \"description\":{\n" +
                             "        \"type\": \"text\"\n" +
                             "      }\n" +
                             "    }\n" +
                             "  }\n" , XContentType.JSON);
  // 参数1:创建索引的请求对象  参数2:请求的配置对象(一般直接使用默认配置)
  CreateIndexResponse createIndexResponse = restHighLevelClient.indices().create(createIndexRequest, RequestOptions.DEFAULT);
  System.out.println("创建状态:" + createIndexResponse.isAcknowledged());
  restHighLevelClient.close();
}
删除索引
@Test
public void testDeleteIndex() throws IOException {
    // 参数1: 删除的索引     参数2: 请求配置对象
    AcknowledgedResponse acknowledgedResponse = restHighLevelClient.indices().delete(new DeleteIndexRequest("products"), RequestOptions.DEFAULT);
    System.out.println(acknowledgedResponse.isAcknowledged());
}
创建文档
@Autowired
private RestHighLevelClient restHighLevelClient;

@Test
public void testCreate() throws IOException {
  IndexRequest indexRequest = new IndexRequest("products");   // 索引的名称
  indexRequest
    .id("1")                 // .id("1") 表示指定_id
    .source("{\n" +
            "          \"id\" : 1,\n" +
            "          \"title\" : \"蓝月亮\",\n" +
            "          \"price\" : 123.23,\n" +
            "          \"description\" : \"这个洗衣液非常不错哦!\"\n" +
            "        }",XContentType.JSON);
  // 参数1:索引的请求对象  参数2:请求的配置对象
  IndexResponse index = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
  System.out.println(index.status());     // 创建的状态
}
更新文档
@Test
public void testUpdate() throws IOException {
  // 参数1:去哪个索引中更新  参数2:需要更新的文档的_id
  UpdateRequest updateRequest = new UpdateRequest("products","1");
  updateRequest.doc("{\"title\":\"好月亮\"}",XContentType.JSON); // 保留原始数据的基础之上再做更新
  // 参数1:更新的请求对象  参数2:请求的配置对象
  UpdateResponse update = restHighLevelClient.update(updateRequest, RequestOptions.DEFAULT);
  System.out.println(update.status());
}
删除文档
@Test
public void testDelete() throws IOException {
  // 参数1:删除哪个索引中的文档  参数2:删除的文档的id
  DeleteRequest deleteRequest = new DeleteRequest("products","t8KzZ4MBTPbYjF27ZPRx");
  // 参数1:删除的请求对象  参数2:请求的配置对象
  DeleteResponse delete = restHighLevelClient.delete(deleteRequest, RequestOptions.DEFAULT);
  System.out.println(delete.status());
}
基于 id 查询文档
@Test
public void testQueryById() throws IOException {
  // 参数1:查询哪个索引中的文档  参数2:删除的文档的id
  GetRequest getRequest = new GetRequest("products","1");
  // 参数1:查询的请求对象        参数2:请求的配置对象     返回值:查询的响应对象
  GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
  System.out.println(getResponse.getSourceAsString());
}
查询所有
@Test
public void testSearchAll() throws IOException {
  // 搜索哪个索引的文档
  SearchRequest searchRequest = new SearchRequest("products");
  // 指定条件对象
  SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  // 为条件对象指定条件           查询所有
  sourceBuilder.query(QueryBuilders.matchAllQuery());
  // 指定查询条件
  searchRequest.source(sourceBuilder);
  // 参数1:搜索的请求对象   参数2:请求的配置
  SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
  System.out.println(searchResponse.getHits().getTotalHits().value); // 获取总条数
  SearchHit[] hits = searchResponse.getHits().getHits();    // 获取结果
  for (SearchHit hit : hits) {
    System.out.println(hit.getSourceAsString());
  }
}
不同条件查询
public void testQuery() throws IOException {
    // 搜索哪个索引的文档
    SearchRequest searchRequest = new SearchRequest("products");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    // 1.term 查询(关键词查询)
    //sourceBuilder.query(QueryBuilders.termQuery("description","衣"));  // 基于关键词查询
    // 2. range 查询(范围查询) 
    //sourceBuilder.query(QueryBuilders.rangeQuery("price").gt(0).lte(10));  
    // 3. prefix 前缀查询
    //sourceBuilder.query(QueryBuilders.prefixQuery("title", "蓝"));
    // 4. wildcard 通配符查询  ? : 一个字符    * : 任意多个字符
    //sourceBuilder.query(QueryBuilders.wildcardQuery("title", "蓝*"));
    // 5. ids 查询(多id查询)
    //sourceBuilder.query(QueryBuilders.idsQuery().addIds("1").addIds("2"));
    // 6. multi_match 查询(多字段查询)    在什么字段
    sourceBuilder.query(QueryBuilders.multiMatchQuery("非常", "title", "description"));
    searchRequest.source(sourceBuilder);
    // 参数1:搜索的请求对象   参数2:请求的配置对象
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    System.out.println("符合条件的总条数: " + searchResponse.getHits().getTotalHits().value);
    System.out.println("符合查询到的文档中的最大分数: " + searchResponse.getHits().getMaxScore());
    SearchHit[] hits = searchResponse.getHits().getHits();     // 获取所有符合条件的文档
    for (SearchHit hit : hits) {
        System.out.println(hit.getSourceAsString());
    }
}
分页查询 & 排序 & 返回指定字段 & 高亮
/*
分页查询        from: 起始位置  size: 展示的记录数
排序            sort
返回指定的字段   _source
高亮查询        highlighter
 */
@Test
public void testSearchPage() throws IOException {
    // 搜索哪个索引的文档
    SearchRequest searchRequest = new SearchRequest("products");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    // 创建高亮器
    HighlightBuilder highlightBuilder = new HighlightBuilder();
    highlightBuilder.requireFieldMatch(false).field("description").field("title").preTags("<span style='color:red'>").postTags("</span>");     // 关闭字段匹配,指定哪些字段高亮,使用什么字段高亮
    sourceBuilder
            .query(QueryBuilders.termQuery("description", "不"))    // 基于关键词查询
            .from(0)     // 起始位置
            .size(2)     // 每页显示的条数
            .sort("price", SortOrder.DESC)     // 参数1:根据哪个字段排序  参数2:排序方式
            //.fetchSource(new String[]{}, new String[]{"price"})  // 参数1:包含字段数组   参数2:排除字段数组 包含和排除一般不同时使用,根据需要选择一个
            .highlighter(highlightBuilder);           // 高亮查询
    // 默认全部返回
    searchRequest.source(sourceBuilder);
    // 参数1:搜索的请求对象   参数2:请求的配置对象
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    System.out.println("符合条件的总条数: " + searchResponse.getHits().getTotalHits().value);
    System.out.println("符合查询到的文档中的最大分数: " + searchResponse.getHits().getMaxScore());
    SearchHit[] hits = searchResponse.getHits().getHits();     // 获取所有符合条件的文档
    for (SearchHit hit : hits) {
        System.out.println(hit.getSourceAsString());
        Map<String, HighlightField> highlightFields = hit.getHighlightFields();
        if (highlightFields.containsKey("description")){
            System.out.println("description高亮结果:" + highlightFields.get("description").fragments()[0]);
        }
        if (highlightFields.containsKey("title")){
            System.out.println("title高亮结果:" + highlightFields.get("title").fragments()[0]);
        }
    }
}
过滤查询

一旦使用了过滤,一定是先进行过滤,然后在过滤的基础之上进行查询.

使用过滤可以一定程度上加快查询的效率.

@Test
public void testFilter() throws IOException {
    // 搜索哪个索引的文档
    SearchRequest searchRequest = new SearchRequest("products");
    SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
    // 创建高亮器
    sourceBuilder
            .postFilter(QueryBuilders.rangeQuery("price").gte(10))  // 过滤
            .query(QueryBuilders.termQuery("description", "不"));
    // 默认全部返回
    searchRequest.source(sourceBuilder);
    // 参数1:搜索的请求对象   参数2:请求的配置对象
    SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
    System.out.println("符合条件的总条数: " + searchResponse.getHits().getTotalHits().value);
    System.out.println("符合查询到的文档中的最大分数: " + searchResponse.getHits().getMaxScore());
    SearchHit[] hits = searchResponse.getHits().getHits();     // 获取所有符合条件的文档
    for (SearchHit hit : hits) {
        System.out.println(hit.getSourceAsString());
    }
}
RestHighLevelClient 如何实现对象与文档之间转换

先在 java 中创建一个实体类 Product,然后使用kibana在es中创建一个与之对应的索引和映射.

实体类Product

public class Product {

    private Integer id;
    private String title;
    private Double price;
    private String description;
  
    //get set ...
}

测试类

@SpringBootTest
public class RestHighLevelClientForObject{

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     * 将对象放入es中
     */
    @Test
    public void testIndex() throws IOException {
        Product product = new Product();
        product.setId(1);
        product.setTitle("小浣熊干吃面");
        product.setPrice(1.5);
        product.setDescription("小浣熊真好吃!");

        // 录入es中
        IndexRequest indexRequest = new IndexRequest("products");
        indexRequest.id(product.getId().toString())       // 设置文档的_id
                    .source(new ObjectMapper().writeValueAsString(product), XContentType.JSON);  // 将对象转换为json
        IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
        System.out.println(indexResponse.status());
    }

    /*
    从es中查询数据并转换为对象
     */
    @Test
    public void testSearch() throws IOException {
        SearchRequest searchRequest = new SearchRequest("products");
        SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.requireFieldMatch(false).field("description").preTags("<span style='color:red;'>").postTags("</span>"); // 设置高亮
        sourceBuilder
                .query(QueryBuilders.termQuery("description", "浣熊"))     // 查询所有
                .from(0)
                .size(30)
                .highlighter(highlightBuilder);
        searchRequest.source(sourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        System.out.println("符合条件的文档条数: " + searchResponse.getHits().getTotalHits().value);
        System.out.println(searchResponse.getHits().getMaxScore());
        List<Product> productList = new ArrayList<>();
        SearchHit[] hits = searchResponse.getHits().getHits();
        for (SearchHit hit : hits) {
            //System.out.println(hit.getSourceAsString());    // json
            Product product = new ObjectMapper().readValue(hit.getSourceAsString(), Product.class);// 将json转为对象
            // 处理高亮
            Map<String, HighlightField> highlightFields = hit.getHighlightFields();
            if(highlightFields.containsKey("description")){
                product.setDescription(highlightFields.get("description").fragments()[0].toString());
            }
            productList.add(product);
        }
        productList.forEach(product -> System.out.println(product));   // 遍历
    }
}

聚合查询

简介

聚合:英文为Aggregation,是es除搜索功能外提供的针对es数据做统计分析的功能。聚合有助于根据搜索查询提供聚合数据。聚合查询是数据库中重要的功能特性,ES作为搜索引擎兼数据库,同样提供了强大的聚合分析能力。它基于查询条件来对数据进行分桶、计算的方法。有点类似于 SQL 中的 group by 再加一些函数方法的操作。

注意事项:text类型是不支持聚合的。

测试数据

# 创建索引 index 和映射 mapping
PUT /fruit
{
  "mappings": {
    "properties": {
      "title":{
        "type": "keyword"
      },
      "price":{
        "type":"double"
      },
      "description":{
        "type": "text",
        "analyzer": "ik_max_word"
      }
    }
  }
}
# 放入测试数据
PUT /fruit/_bulk
{"index":{}}
  {"title" : "面包","price" : 19.9,"description" : "小面包非常好吃"}
{"index":{}}
  {"title" : "旺仔牛奶","price" : 29.9,"description" : "非常好喝"}
{"index":{}}
  {"title" : "日本豆","price" : 19.9,"description" : "日本豆非常好吃"}
{"index":{}}
  {"title" : "小馒头","price" : 19.9,"description" : "小馒头非常好吃"}
{"index":{}}
  {"title" : "大辣片","price" : 39.9,"description" : "大辣片非常好吃"}
{"index":{}}
  {"title" : "透心凉","price" : 9.9,"description" : "透心凉非常好喝"}
{"index":{}}
  {"title" : "小浣熊","price" : 19.9,"description" : "童年的味道"}
{"index":{}}
  {"title" : "海苔","price" : 19.9,"description" : "海的味道"}

使用

根据某个字段分组
# 根据某个字段进行分组 统计数量
GET /fruit/_search
{
  "query": {
    "match_all": {}
  }, 
  "size": 0, 
  "aggs": {
    "price_group": {	
      "terms": {
        "field": "price"
      }
    }
  }
}

/*
"size": 0  表示只想要聚合的结果,不想要查询的数据
price_group 聚合的名字,自己随便写
price 是对哪个字段进行分组
*/
求最大值
# 求最大值 
GET /fruit/_search
{
  "query": {
    "match_all": {}
  }, 
  "size": 0, 
  "aggs": {
    "price_max": {
      "max": {
        "field": "price"
      }
    }
  }
}

/*
"size": 0  表示只想要聚合的结果,不想要查询的数据
price_max 表示这个聚合查询的名字,自己随便写
price 表示对哪个字段求最大值
*/
求最小值
# 求最小值
GET /fruit/_search
{
  "query": {
    "match_all": {}
  }, 
  "size": 0, 
  "aggs": {
    "price_min": {
      "min": {
        "field": "price"
      }
    }
  }
}

/*
"size": 0  表示只想要聚合的结果,不想要查询的数据
price_min  表示聚合名字,自己随便写
price  表示对哪个字段求最小值
*/
求平均值
# 求平均值
GET /fruit/_search
{
  "query": {
    "match_all": {}
  }, 
  "size": 0, 
  "aggs": {
    "price_avg": {
      "avg": {
        "field": "price"
      }
    }
  }
}

/*
"size": 0  表示只想要聚合的结果,不想要查询的数据
price_avg  表示聚合名字,自己随便写
price  表示对哪个字段求平均值
*/
求和
# 求和
GET /fruit/_search
{
  "query": {
    "match_all": {}
  }, 
  "size": 0, 
  "aggs": {
    "price_sum": {
      "sum": {
        "field": "price"
      }
    }
  }
}

/*
"size": 0  表示只想要聚合的结果,不想要查询的数据
price_sum  表示聚合名字,自己随便写
price  表示对哪个字段求和
*/
RestHighLevelClient 实现聚合查询

测试类

@SpringBootTest
public class RestHighLevelClientForAggs {

    @Autowired
    private RestHighLevelClient restHighLevelClient;

    /**
     * 基于 term 类型进行聚合   基于字段进行分组聚合
     */
    @Test
    public void testAggs() throws IOException {
        SearchRequest searchRequest = new SearchRequest("fruit");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder
                .query(QueryBuilders.matchAllQuery())     // 设置查询条件
                // 聚合名字(自己随便起)    对哪个字段进行聚合       下面这段意思是对指定字段分组
                .aggregation(AggregationBuilders.terms("price_group").field("price"))  // 设置聚合处理
                .size(0);       // 只要聚合的结果,不要查询到的文档
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 处理聚合的结果
        Aggregations aggregations = searchResponse.getAggregations();
        // 由聚合的名字继续处理
        ParsedDoubleTerms aggregation = aggregations.get("price_group"); //price是double类型,所以用ParsedDoubleTerms

        List<? extends Terms.Bucket> buckets = aggregation.getBuckets();
        for (Terms.Bucket bucket : buckets) {
            System.out.println(bucket.getKey() + " " + bucket.getDocCount());
        }
    }

    /**
     * max(ParsedMax接收) min(ParsedMin接收) sum(ParsedSum接收) avg(ParsedAvg接收) 聚合查询    桶中只有一个返回值
     */
    @Test
    public void testAggsFunction() throws IOException {
        SearchRequest searchRequest = new SearchRequest("fruit");
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
        searchSourceBuilder
                .query(QueryBuilders.matchAllQuery())     // 设置查询条件
                // 聚合名字(自己随便起)    对哪个字段进行聚合       下面这段意思是求哪个字段的和
                .aggregation(AggregationBuilders.sum("sum_price").field("price"))  // 设置聚合处理
                .size(0);       // 只要聚合的结果,不要查询到的文档
        searchRequest.source(searchSourceBuilder);
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);

        // 处理聚合的结果
        Aggregations aggregations = searchResponse.getAggregations();
        // 由聚合的名字继续处理
        ParsedSum parsedSum = aggregations.get("sum_price");//返回的类型是求和数据, 所以使用ParsedSum
        System.out.println(parsedSum.getValue());
    }
}

求不同价格的数量

// 求不同价格的数量
@Test
public void testAggsPrice() throws IOException {
  SearchRequest searchRequest = new SearchRequest("fruit");
  SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  sourceBuilder.aggregation(AggregationBuilders.terms("group_price").field("price"));
  searchRequest.source(sourceBuilder);
  SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
  Aggregations aggregations = searchResponse.getAggregations();
  ParsedDoubleTerms terms = aggregations.get("group_price");
  List<? extends Terms.Bucket> buckets = terms.getBuckets();
  for (Terms.Bucket bucket : buckets) {
    System.out.println(bucket.getKey() + ", "+ bucket.getDocCount());
  }
}

求不同名称的数量

// 求不同名称的数量
@Test
public void testAggsTitle() throws IOException {
  SearchRequest searchRequest = new SearchRequest("fruit");
  SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  sourceBuilder.aggregation(AggregationBuilders.terms("group_title").field("title"));
  searchRequest.source(sourceBuilder);
  SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
  Aggregations aggregations = searchResponse.getAggregations();
  ParsedStringTerms terms = aggregations.get("group_title");
  List<? extends Terms.Bucket> buckets = terms.getBuckets();
  for (Terms.Bucket bucket : buckets) {
  	System.out.println(bucket.getKey() + ", "+ bucket.getDocCount());
  }
}

求和

// 求和
@Test
public void testAggsSum() throws IOException {
  SearchRequest searchRequest = new SearchRequest("fruit");
  SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
  sourceBuilder.aggregation(AggregationBuilders.sum("sum_price").field("price"));
  searchRequest.source(sourceBuilder);
  SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
  ParsedSum parsedSum = searchResponse.getAggregations().get("sum_price");
  System.out.println(parsedSum.getValue());
}

集群 Cluster

相关概念

集群

一个集群就是由一个或多个节点组织在一起,它们共同持有你整个的数据,并一起提供索引和搜索功能。一个集群由一个唯一的名字标识,这个名字默认就是elasticsearch。这个名字是重要的,因为一个节点只能通过指定某个集群的名字,来加入这个集群。

节点

一个节点是你集群中的一个服务器,作为集群的一部分,它存储你的数据,参与集群的索引和搜索功能。和集群类似,一个节点也是由一个名字来标识的,默认情况下,这个名字是一个随机的漫威漫画角色的名字,这个名字会在启动的时候赋予节点。

索引

一组相似文档的集合

映射

用来定义索引存储文档的结构如:字段、类型等。

文档

索引中一条记录,可以被索引的最小单元

分片

Elasticsearch提供了将索引划分成多份的能力,这些份就叫做分片。当你创建一个索引的时候,你可以指定你想要的分片的数量。每个分片本身也是一个功能完善并且独立的“索引”,这个“索引”可以被放置 到集群中的任何节点上。

复制

Index的分片中一份或多份副本。

搭建集群

集群规划

因为没有很多的机器,这里我们在一台机器上启动三个es服务,做一个伪分布式.

# 1.准备3个ES节点和一个kibana 节点  ES 9200 9300 
- web: 9201 tcp:9301  node-1  elasticsearch.yml   
- web: 9202 tcp:9302  node-2  elasticsearch.yml
- web: 9203 tcp:9303  node-3  elasticsearch.yml
- kibana: 5602
  • 注意
    • 所有节点集群名称必须一致 cluster.name
    • 每个节点必须有一个唯一名字 node.name
    • 开启每个节点远程连接 network.host: 0.0.0.0
    • 指定使用 IP地址进行集群节点通信 network.publish_host:
    • 修改 web 端口 tcp 端口 http.port: transport.tcp.port
    • 指定集群中所有节点通信列表 discovery.seed_hosts: node-1 node-2 node-3 相同
    • 允许集群初始化 master 节点节点数: cluster.initial_master_nodes: [“node-1”, “node-2”,“node-3”]
    • 集群最少几个节点可用 gateway.recover_after_nodes: 2
    • 开启每个节点跨域访问http.cors.enabled: true http.cors.allow-origin: “*”

image-20220924072829246

配置文件
# node-1 配置文件
# 指定集群名称 3个节点必须一致
cluster.name: es-cluster
# 指定节点名称 每个节点名字唯一
node.name: node-1
# 开放远程链接
network.host: 0.0.0.0
# 指定使用发布地址进行集群间通信
network.publish_host: 192.168.204.137
# 指定 web 端口
http.port: 9201
# 指定 tcp 端口
transport.tcp.port: 9301
# 指定所有节点的 tcp 通信
discovery.seed_hosts: ["192.168.204.137:9301", "192.168.204.137:9302","192.168.204.137:9303"]
# 指定可以初始化集群的节点名称
cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
# 集群最少几个几点可用
gateway.recover_after_nodes: 2
# 解决跨域问题
http.cors.enabled: true
http.cors.allow-origin: "*"
# node-2 配置文件
# 指定集群名称 3个节点必须一致
cluster.name: es-cluster
# 指定节点名称 每个节点名字唯一
node.name: node-2
# 开放远程链接
network.host: 0.0.0.0
# 指定使用发布地址进行集群间通信
network.publish_host: 192.168.204.137
# 指定 web 端口
http.port: 9202
# 指定 tcp 端口
transport.tcp.port: 9302
# 指定所有节点的 tcp 通信
discovery.seed_hosts: ["192.168.204.137:9301", "192.168.204.137:9302","192.168.204.137:9303"]
# 指定可以初始化集群的节点名称
cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
# 集群最少几个几点可用
gateway.recover_after_nodes: 2
# 解决跨域问题
http.cors.enabled: true
http.cors.allow-origin: "*"
# node-3 配置文件
# 指定集群名称 3个节点必须一致
cluster.name: es-cluster
# 指定节点名称 每个节点名字唯一
node.name: node-2
# 开放远程链接
network.host: 0.0.0.0
# 指定使用发布地址进行集群间通信
network.publish_host: 192.168.204.137.3
# 指定 web 端口
http.port: 9202
# 指定 tcp 端口
transport.tcp.port: 9302
# 指定所有节点的 tcp 通信
discovery.seed_hosts: ["192.168.204.137:9301", "192.168.204.137:9302","192.168.204.137:9303"]
# 指定可以初始化集群的节点名称
cluster.initial_master_nodes: ["node-1", "node-2","node-3"]
# 集群最少几个几点可用
gateway.recover_after_nodes: 2
# 解决跨域问题
http.cors.enabled: true
http.cors.allow-origin: "*"
编写 compose 文件
version: "3.8"
networks:
  escluster:
services:
  es01:
    image: elasticsearch:7.14.0
    ports:
      - "9201:9201"
      - "9301:9301"
    networks:
      - "escluster"
    volumes:
      - ./node-1/data:/usr/share/elasticsearch/data
      - ./node-1/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - ./node-1/plugins/ik:/usr/share/elasticsearch/plugins/ik
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"

  es02:
    image: elasticsearch:7.14.0
    ports:
      - "9202:9202"
      - "9302:9302"
    networks:
      - "escluster"
    volumes:
      - ./node-2/data:/usr/share/elasticsearch/data
      - ./node-2/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - ./node-2/plugins/ik:/usr/share/elasticsearch/plugins/ik
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"

  es03:
    image: elasticsearch:7.14.0
    ports:
      - "9203:9203"
      - "9303:9303"
    networks:
      - "escluster"
    volumes:
      - ./node-3/data:/usr/share/elasticsearch/data
      - ./node-3/config/elasticsearch.yml:/usr/share/elasticsearch/config/elasticsearch.yml
      - ./node-3/plugins/ik:/usr/share/elasticsearch/plugins/ik
    environment:
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"

  kibana:
    image: kibana:7.14.0
    ports:
      - "5602:5601"
    networks:
      - "escluster"
    volumes:
      - ./kibana.yml:/usr/share/kibana/config/kibana.yml
kibana 配置文件
# kibana配置文件 连接到ES
server.host: "0"
server.shutdownTimeout: "5s"
elasticsearch.hosts: [ "http://192.168.204.137:9201" ] #链接任意节点即可
monitoring.ui.container.elasticsearch.enabled: true
查看集群状态
http://192.168.204.137:9201/_cat/health?v

安装head插件

1. 访问github网站
	搜索: elasticsearch-head 插件
	
2. 安装git
	yum install git
	
3. 将elasticsearch-head下载到本地
	git clone git://github.com/mobz/elasticsearch-head.git

4. 安装nodejs
	#注意: 没有wget的请先安装 yum install -y wget
	wget http://cdn.npm.taobao.org/dist/node/latest-v8.x/node-v8.1.2-linux-x64.tar.xz
 
5. 解压缩nodejs
	xz -d node-v10.15.3-linux-arm64.tar.xz
	tar -xvf node-v10.15.3-linux-arm64.tar

6. 配置环境变量
	mv node-v10.15.3-linux-arm64 nodejs
	mv nodejs /usr/nodejs
	vim /etc/profile
		export NODE_HOME=/usr/nodejs
		export PATH=$PATH:$JAVA_HOME/bin:$NODE_HOME/bin

7.	进入elasticsearch-head的目录
	npm config set registry https://registry.npm.taobao.org
	npm install
	npm run start


8.  启动访问head插件 默认端口9100
	http://ip:9100  查看集群状态
  • 1
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值