ElasticSearch 8.x 教程
ElasticSearch 8.x 教程
MySQL 和 ElasticSearch 的区别
MySQL 架构天生不适合海量数据查询,它只适合海量数据存储,但无法应对海量数据下各种复杂条件的查询。原因如下:
-
加索引确实可以提升查询速度,但是如果没有索引满足搜索条件,就会触发全表扫描。即使使用了组合索引,也要符合最左前缀原则才能命中索引。而在海量数据多种查询条件下,很有可能不符合最左前缀原则而导致索引失效。
-
存储都是需要成本的,如果针对每一种情况都加索引,以 innoDB 为例,每加一个索引,就会创建一颗 B+ 树,如果是海量数据,将会增加很大的存储成本。
-
有些查询条件是 MySQL 加索引都解决不了的,比如我要查询商品中所有 title 带有「格力空调」的关键词,如果用 MySQL 写,会写出如下代码:该代码无法命中任何索引,会触发全表扫描。一旦关键词记错,即使全表扫描也无法查询到任何商品。
SELECT * FROM product WHERE title like '%格力空调%'
术业有专攻,海量数据查询还得用专门的搜索引擎。以 ES 为例,它是基于 Lucene 引擎构建的开源分布式搜索分析引擎,可以提供针对 PB 数据的近实时查询,广泛用在全文检索、日志分析、监控分析等场景。ES 有以下三个特点:
- 轻松支持各种复杂的查询条件:它是分布式实时文件存储,会把每一个字段都编入索引 (倒排索引),利用高效的倒排索引,以及自定义打分、排序能力与丰富的分词插件等,能实现任意复杂查询条件下的全文检索需求。
- 可扩展性强:天然支持分布式存储,通过极其简单的配置实现几百上千台服务器的分布式横向扩容,轻松处理 PB 级别的结构化或非结构化数据。
- 高可用,容灾性能好:通过使用主备节点,以及故障的自动探测与恢复,有力地保障了高可用。
简而言之,MySQL 和 ElasticSearch 的更多区别可以参考:
ELK 安装
ELK 是 Elasticsearch、Logstash、Kibana 三大开源框架首字母大写简称。市面上也被成为 Elastic Stack。
- Elasticsearch 是一个基于 Lucene、分布式、通过 Restful 方式进行交互的近实时搜索平台框架。
- Logstash 是 ELK 的中央数据流引擎,用于从不同目标(文件 / 数据存储 / MQ)收集的不同格式数据,经过过滤后支持输出到不同的目的地(文件 / MQ / redis / elasticsearch / kafka 等)。
- Kibana 是一个针对 Elasticsearch 的开源分析及可视化平台,用来搜索、查看交互存储在 es 索引中的数据,提供实时分析的功能。它操作简单,基于浏览器的用户界面可以快速创建仪表板 dashboard,实时显示查询动态。
市面上很多开发只要一提到 ELK 能够一致说出它是一个日志分析架构技术栈总称,但实际上 ELK 不仅仅适用于日志分析,它还可以支持其他任何数据分析和收集的场景。
windows 下安装 Elasticsearch
- 最新版本下载地址:Download Elasticsearch | Elastic
- 熟悉目录:
bin:启动文件
config:配置文件
log4j2:日志配置文件
jvm.options:java虚拟机相关的配置
elasticsearch.yml:elasticsearch的配置文件!默认9200端口!跨域!
lib:相关jar包
logs:日志
modules:功能模块
plugins:插件!
- 启动:进入解压后的目录,运行下面的命令:
./bin/elasticsearch
# 如果这时报错 "max virtual memory areas vm.max *map*count [65530] is too low"
sudo sysctl -w vm.max_map_count=262144
- 访问
localhost:9200
curl localhost:9200
默认情况下,Elastic 只允许本机访问,如果需要远程访问,可以修改 Elastic 安装目录的
config/elasticsearch.yml
文件,去掉network.host
的注释,将它的值改成0.0.0.0
,然后重新启动 Elastic。线上服务不要这样设置,要设成具体的 IP。
Docker-Compose 安装 ELK
- 创建
docker-compose.yaml
文件:注意 ES 和 Kibana 的版本要一致
version: "3.9"
services:
elasticsearch:
image: elasticsearch:8.7.1
container_name: elasticsearch
environment:
- discovery.type=single-node
- ES_JAVA_OPTS=-Xms1g -Xmx1g
- xpack.security.enabled=false
volumes:
- es_data:/usr/share/elasticsearch/data
ports:
- target: 9200
published: 9200
networks:
- elastic
kibana:
image: kibana:8.7.1
ports:
- target: 5601
published: 5601
depends_on:
- elasticsearch
networks:
- elastic
volumes:
es_data:
driver: local
networks:
elastic:
name: elastic
driver: bridge
-
在该文件所在目录,执行:
docker-compose up
-
访问
http://localhost:9200/
,返回:
6ab0dc860d1",
"cluster_name" : "docker-cluster",
"cluster_uuid" : "YE5ZOkzbQoejhluBtOT_eQ",
"version" : {
"number" : "8.7.1",
"build_flavor" : "default",
"build_type" : "docker",
"build_hash" : "f229ed3f893a515d590d0f39b05f68913e2d9b53",
"build_date" : "2023-04-27T04:33:42.127815583Z",
"build_snapshot" : false,
"lucene_version" : "9.5.0",
"minimum_wire_compatibility_version" : "7.17.0",
"minimum_index_compatibility_version" : "7.0.0"
},
"tagline" : "You Know, for Search"
-
访问
http://localhost:5601/app/home#/
得到 Elastic 的可视化界面 -
如果容器意外关闭的话:
-
服务器版:可以使用
docker logs -f 容器id
查看日志 -
桌面版:点击
view details
进入 详情页
-
-
如果不再使用这些容器,可以删除:
docker-compose down
-
重启 windows 前请务必先关掉容器
ES 的核心概念
集群,节点,索引,类型,文档,分片,映射是什么?
基本概念
Node 与 Cluster
Elastic 本质上是一个分布式数据库,允许多台服务器协同工作,每台服务器可以运行多个 Elastic 实例。
单个 Elastic 实例称为一个节点(node)。一组节点构成一个集群(cluster)。
Index
Elastic 会索引所有字段,经过处理后写入一个反向索引(Inverted Index)。查找数据的时候,直接查找该索引。
所以,Elastic 数据管理的顶层单位就叫做 Index(索引)。它是单个数据库的同义词。每个 Index (即数据库)的名字必须是小写。
下面的命令可以查看当前节点的所有 Index:
curl -X GET 'http://localhost:9200/_cat/indices?v'
Document
Index 里面单条的记录称为 Document(文档)。许多条 Document 构成了一个 Index。
Document 使用 JSON 格式表示,下面是一个例子。
{
"user": "张三",
"title": "工程师",
"desc": "数据库管理"
}
同一个 Index 里面的 Document,不要求有相同的结构(scheme),但是最好保持相同,这样有利于提高搜索效率。
关系行数据库和 Elasticsearch 对比
Elasticsearch 面向文档:
Relational DB | ElasticSearch |
---|---|
数据库 (database) | 索引 (indices) |
表 (tables) | types(弃用) |
行 (rows) | documents |
字段 (columns) | fields |
Elasticsearch (集群) 中可以包含多个索引 (数据库),每个索引中可以包含多个类型 (表),每个类型下又包含多个文档 (行),每个文档中又包含多个字段 (列)。
物理设计
ES 在后台把每个索引划分为多个分片,每个分片可以在集群中的不同服务器间迁移。一个人就是一个集群,默认的集群名 elasticsearch。
节点和分片如何工作
一个集群至少有一个节点,而一个节点就是一个 es 进程,节点可以有多个索引默认的,如果你创建索引,那么索引将会有 5 个分片(primary shard)构成,每一个主分片会有一个副本(replica shard)
上图是一个有 3 个节点的集群,可以看到主分片和对应的复制分片都不会在同一个节点内,这样有利于某个节点挂掉了,数据也不至于丢失。实际上,一个分片是一个 Lucene
索引,一个包含倒排索引的文件目录,倒排索引的结构使得 es 在不扫描全部文档的情况下,就能告诉你哪些文档包含特定的关键字。
什么是倒排索引?
比如我们通过博客标签来搜索博客文章。那么倒排索引列表就是这样的一个结构:
如果我们需要搜索含有 python 标签的文章,那相对于查找所有原始数据而言,查找倒排索引后的数据将会快的多。只需要查看标签这一栏,然后获取相关的文章 ID 即可。
逻辑设计
一个索引类型中,包含多个文档,比如说文档 1、文档 2。当我们索引一篇文档时,可以通过这样的一个顺序找到它:索引 (数据库) \(\to\) 类型 (表)\(\to\) 文档 ID (行),通过这个组合我们就能索引到某个具体的文档。
注意:ID 不必是整数,实际上它是个字符串。
- 文档:es 是面向文档的,索引和搜索数据的最小单位是文档,文档有如下属性:
- 自我包含:一篇文档同时包含字段和对应的值,也就是同时包含
key:value
- 可以是层次型的:个文档中包含自文档,就是一个 json 对象
- 灵活的结构:文档不依赖于预先定义的模式。在关系数据库中,要提前定义字段才能用;在 es 中,字段是非常灵活的,可以动态添加一个新的字段
- 自我包含:一篇文档同时包含字段和对应的值,也就是同时包含
- 类型:类型是文档的逻辑容器,就像关系型数据库一样,表格是行的容器。类型中对于字段的定义称为映射,比如 name 映射为字符串类型。
- 索引:也就是数据库,索引是映射类型的容器,es 的索引是一个非常大的文档集合,索引存储了映射类型的字段和其他设置,然后将其存储在各个分片上
在 es 中安装插件
以 elasticsearch-analysis-ik
分词器为例:
安装
方法一:直接进入容器下载
# 1. 进入运行中的容器
docker exec -it 容器id bash
# 2. 安装插件
bin/elasticsearch-plugin install https://github.com/ElisaMin/elasticsearch-analysis-ik/releases/download/8.7.1-17/elasticsearch-analysis-ik-8.7.1.zip
# 3. 查看插件是否安装成功
bin/elasticsearch-plugin list
# 3. 退出容器
exit
# 4. 重启容器
docker restart 容器id
方法二:本地下载
# 1. 下载插件安装包,然后将其拷贝到容器中
docker cp D:\Software\Elasticsearch\Docker\plugins\elasticsearch-analysis-ik-8.7.1.zip 容器id:/tmp/
# 2. 进入运行中的容器
docker exec -it 容器id bash
# 3. 安装插件
bin/elasticsearch-plugin install file:///tmp/elasticsearch-analysis-ik-8.7.1.zip
使用
- 进入 [Console - Dev Tools - Elastic]
- 在控制台输入:其中
ik_smart
为最少切分;ik_max_word
为最细粒度划分
GET _analyze
{
"analyzer": "ik_smart",
"text": "北京邮电大学"
}
GET _analyze
{
"analyzer": "ik_max_word",
"text": "北京邮电大学"
}
restful 风格说明
restful 风格是一种软件架构风格,而不是标准,只是提供了一组设计原则和约束条件。它主要用于客户端和服务器交互类的软件。基于这个风格设计的软件可以更简洁,更有层次,更易于实现缓存等机制。
基本 rest 说明:
method | url 地址 | 描述 |
---|---|---|
PUT | localhost:9200 / 索引名称 / 类型名称 / 文档 id | 创建文档(指定文档 id) |
POST | localhost:9200 / 索引名称 / 类型名称 | 创建文档(随机文档 id) |
POST | localhost:9200 / 索引名称 / 类型名称 / 文档 id/_update | 修改文档 |
DELETE | localhost:9200 / 索引名称 / 类型名称 / 文档 id | 删除文档 |
GET | localhost:9200 / 索引名称 / 类型名称 / 文档 id | 查询文档通过文档 id |
POST | localhost:9200 / 索引名称 / 类型名称 /_search | 查询所有数据 |
关于索引的基本操作
- 创建一个索引
# ElasticSearch 的 7.x.x 版本
PUT /索引名/类型/文档id
{
请求体
}
# ElasticSearch 的 8.x.x 版本,类型已经弃用,默认写_doc
PUT /索引名/_doc/文档id
{
请求体
}
# 举个栗子
PUT /test1/_doc/1
{
"name": "locke",
"age": 3,
"birthday": "1997-04-06"
}
Kibana 返回:
{
"_index": "test1",
"_id": "1",
"_version": 1,
"result": "created",
"_shards": {
"total": 2,
"successful": 1,
"failed": 0
},
"_seq_no": 0,
"_primary_term": 1
}
# 字符串类型
text、keyword
# 数值类型
long,integer,short,byte,double,float,half float,scaled float
# 日期类型
date
# te布尔值类型
boolean·
# 二进制类型
binary
- 创建具体的索引规则:
PUT test2/
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "long"
},
"birthday": {
"type": "date"
}
}
}
}
Kibana 返回:
{
"acknowledged": true,
"shards_acknowledged": true,
"index": "test2"
}
- 获得索引规则
GET test2
如果自己的文档字段没有指定,那么 es 就会给我们默认配置字段类型!
- 扩展命令
GET _cat/health # 查看健康值
GET _cat/indices?v # 查看所有东西的版本信息
- 修改:POST
POST test1/_update/1
{
"doc": {
"name": "lockegogo"
}
}
- 删除:DELETE,根据请求来判断是删除索引还是删除文档
DELETE test1
关于文档的基本操作
基本操作
# 增
PUT /test/_doc/1
{
"name": "locke",
"age": 26,
"desc": "喜欢大熊猫花花",
"tags": ["圆滚滚","可爱","三角形"]
}
PUT /test/_doc/2
{
"name": "feifei",
"age": 28,
"desc": "喜欢大熊猫福宝",
"tags": ["活泼","调皮","花园杀手"]
}
# 查
GET /test/_doc/1
GET /test/_doc/2
# 改
POST /test/_update/1
{
"doc": {
"name": "locke"
}
}
简单操作:条件查询
如果有多条记录,匹配度越高,分数越高:
# 支持模糊查询
GET /test/_search?q=name:locke
复杂操作
- 排序 & 分页
# "_source": 过滤字段查询
# "sort": 排序
# "from": 分页查询,从第几条数据开始查询
# "size": 返回多少数据
GET test/_search
{
"query": {
"match": {
"name": "feifei"
}
},
"_source": ["name", "desc"],
"sort": [
{
"age": {
"order": "desc"
}
}
],
"from": 0,
"size": 1
}
- 布尔值查询
- must = and:既符合 A 也符合 B
- should = or:符合 A 或符合 B
- must_not = not:不符合 A 或不符合 B
GET test/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "feifei"
}
},
{
"match": {
"age": "29"
}
}
]
}
}
}
- 过滤查询:
filter
GET test/_search
{
"query": {
"bool": {
"must": [
{
"match": {
"name": "feifei"
}
}
],
"filter": [
{
"range": {
"age": {
"gte": 10,
"lte": 30
}
}
}
]
}
}
}
- 匹配多个条件:多个条件使用空格隔开,只要满足一个条件就可以被查出,可以通过分值进行基本判断
# 匹配多个条件
GET test/_search
{
"query": {
"match": {
"tags": "活泼 花园杀手 三角形"
}
}
}
- 精确查询:term 查询是直接通过倒排索引指定的词条进行精确查找
- term:直接查询精确的,两种情况:text or keyword
- match:会使用分词器解析,先分析文档,然后在通过分析的文档进行查询
# 1. 创建数据
PUT testdb
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"desc": {
"type": "keyword"
}
}
}
}
PUT testdb/_doc/1
{
"name": "java name",
"desc": "java desc"
}
PUT testdb/_doc/2
{
"name": "java name",
"desc": "java desc2"
}
# 2. 分析数据,查看结果
# 2.1 没有被拆分
GET _analyze
{
"analyzer": "keyword",
"text": "java name"
}
# 2.2 被拆分了
GET _analyze
{
"analyzer": "standard",
"text": "java name"
}
# 3. 查看数据:keyword 类型的字段不会被分词器解析
# 3.1 两条记录都可以查到
GET testdb/_search
{
"query": {
"match": {
"name": "java"
}
}
}
# 3.2 只能查到一条,因为 keyword 不可拆分
GET testdb/_search
{
"query": {
"match": {
"desc": "java desc"
}
}
}
# term
GET testdb/_search
{
"query": {
"term": {
"desc": "java desc"
}
}
}
- 高亮查询
# "pre_tags" + "post_tags": 自定义高亮条件
GET testdb/_search
{
"query": {
"match": {
"desc": "java desc"
}
},
"highlight": {
"pre_tags": "<p class='key' style='color:red'>",
"post_tags": "</p>",
"fields": {
"desc": {}
}
}
}
集成 SpringBoot
Github 地址:ElasticSearch 集成 SpringBoot
依赖导入
- 找文档
- 找到原生的依赖:注意
elasticsearch
的版本要和本地的版本一致
<project>
<dependencies>
<dependency>
<groupId>co.elastic.clients</groupId>
<artifactId>elasticsearch-java</artifactId>
<version>8.7.1</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.12.3</version>
</dependency>
</dependencies>
</project>
如果出现 ClassNotFoundException: jakarta.json.spi.JsonProvider
错误,增加:
<project>
<dependencies>
...
<dependency>
<groupId>jakarta.json</groupId>
<artifactId>jakarta.json-api</artifactId>
<version>2.0.1</version>
</dependency>
</dependencies>
</project>
注意:在构建 SpringBoot 项目时,需要添加依赖项:
NoSQL
\(\to\)Spring Data Elasticsearch (Access+Driver)
配置类
Java API 客户端围绕三个主要组件进行了结构化设计:
-
API 客户端类:它们提供了强类型的数据结构和用于 Elasticsearch API 的方法。由于 Elasticsearch API 较大,它被组织成命名空间,每个命名空间都有自己的客户端类。Elasticsearch 核心功能在 ElasticsearchClient 类中实现。
-
JSON 对象映射器:它将您的应用程序类映射到 JSON,并与 API 客户端无缝集成。
-
传输层实现:这是处理所有 HTTP 请求的地方。
在 com.locke.demos
中新建 config
文件夹,添加 ElasticSearchClientConfig
类
@Configuration
public class ElasticSearchClientConfig {
@Bean
public ElasticsearchClient elasticsearchClient() {
// Create the low-level client
RestClient restClient = RestClient.builder(
new HttpHost("localhost", 9200)).build();
// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
restClient, new JacksonJsonpMapper());
// And create the API client
ElasticsearchClient client = new ElasticsearchClient(transport);
return client;
}
}
索引 CRUD
在 test
文件夹中的 EsApiApplicationTests
类中测试 api
@SpringBootTest
class EsApiApplicationTests {
@Autowired
@Qualifier("elasticsearchClient")
private ElasticsearchClient client;
// 创建索引
@Test
public void testCreateIndex() throws IOException {
CreateIndexResponse indexResponse = client.indices().create(c -> c.index("user"));
System.out.println(indexResponse);
}
// 查询索引
@Test
public void testGetIndex() throws IOException {
GetIndexResponse getindexResponse = client.indices().get(i -> i.index("user"));
System.out.println(getindexResponse);
}
// 判断索引是否存在
@Test
public void testExistsIndex() throws IOException {
BooleanResponse booleanResponse = client.indices().exists(e -> e.index("user"));
System.out.println(booleanResponse.value());
}
// 删除索引
@Test
public void testDeleteIndex() throws IOException {
DeleteIndexResponse deleteIndexResponse = client.indices().delete(d -> d.index("user"));
System.out.println(deleteIndexResponse.acknowledged());
}
}
Document CRUD
在 com.locke.demos
中新建 pojo
文件夹,添加 User
类
@Data
@AllArgsConstructor
@NoArgsConstructor
@Component
public class User {
private String name;
private Integer age;
}
在 test
文件夹中的 EsApiApplicationTests
类中测试 api
@SpringBootTest
class EsApiApplicationTests {
@Autowired
@Qualifier("elasticsearchClient")
private ElasticsearchClient client;
// 测试添加文档:可同时创建索引
@Test
public void testAddDocument() throws IOException {
User user = new User("locke", 18);
IndexResponse indexResponse = client.index(i -> i.index("user").id("1").document(user));
}
// 更新文档
@Test
public void testUpdateDocument() throws IOException {
UpdateResponse<User> updateResponse = client.update(u -> u.index("user")
.id("1")
.doc(new User("feifei", 13)), User.class);
}
// 判断文档是否存在
@Test
public void testExistsDocument() throws IOException {
BooleanResponse booleanResponse = client.exists(e -> e.index("user").id("1"));
System.out.println(booleanResponse.value());
}
// 查询文档
@Test
public void testGetDocument() throws IOException {
GetResponse<User> getResponse = client.get(g -> g.index("user").id("1"), User.class);
System.out.println(getResponse.source());
}
// 删除文档
@Test
public void testDeleteDocument() throws IOException {
DeleteResponse deleteResponse = client.delete(d -> d.index("user").id("1"));
System.out.println(deleteResponse.id());
}
// 批量插入文档
@Test
public void bulkTest() throws IOException {
List<User> userList = new ArrayList<>();
userList.add(new User("hello world", 11));
userList.add(new User("hello java", 12));
userList.add(new User("hello es", 13));
userList.add(new User("hello spring", 14));
userList.add(new User("user", 15));
List<BulkOperation> bulkOperationArrayList = new ArrayList<>();
// 遍历添加到 bulk 中
for (User user : userList) {
bulkOperationArrayList.add(BulkOperation.of(o -> o.index(i -> i.document(user))));
}
BulkResponse bulkResponse = client.bulk(b -> b.index("user")
.operations(bulkOperationArrayList));
}
// 查询
@Test
public void searchTest() throws IOException {
SearchResponse<User> search = client.search(s -> s
.index("user")
// 查询 name 字段包含 hello 的 document (不使用分词器精确查找)
.query(q -> q
.term(t -> t
.field("name")
.value(v -> v.stringValue("hello"))
))
// 分页查询,从第 0 页开始查询 3 个 document
.from(0)
.size(3)
// 按 age 降序排序
.sort(f -> f.field(o -> o.field("age").order(SortOrder.Desc))), User.class
);
for (Hit<User> hit : search.hits().hits()) {
System.out.println(hit.source());
}
}
}
实战:京东搜索
ElasticSearch 8.x 实战 - Lockegogo - 博客园 (cnblogs.com)