Elastic Search 入门级别教程
官网资料: https://www.elastic.co/guide/index.html
# 1. 初识 Elastic Search
Elastic Search(以下简称 ES)是常用的分布式搜索服务中的中间件,是一款强大的开源搜索引擎,能在海量数据中快速找到目标内容。
ES 结合 kibana、Logstash、Beats,也就是 elastic stack(ELK 技术栈)。被广泛应用在日志分析、实时监控等领域。
ES 是 Elastic Stack 的核心,复杂存储、搜索、分析数据。
ES 底层实现是 Lucene,Lucene 是一个 Java 语言的搜索引擎类库,是 Apache 公司的顶级项目,由 DougCutting 于 1999 年研发。
官网地址:https://lucene.apache.org/
Lucene 的优势:
- 易扩展
- 高性能(基于倒排索引)
Lucene 缺点:
- 只限于 Java 语言开发
- 学习曲线狭窄
- 不支持水平扩展
ES 的发展
- 2004 年 shay Banon 基于 Lucene 开发了 Compass
- 2010 年 Shay Banon 重写了 Compass,取名 Elastic Search。
- 官网地址:https://www.elastic.co/cn/,目前最新版本 8.6
相比于 Lucene,ES 具备:
- 支持分布式,可水平扩展
- 提供 RestAPI 接口,可被任何语言调用
为什么要选 Elastic Search
来源:https://db-engines.com/en/ranking/search+engine
# 1.1 倒排索引
# 1.1.1 正向索引和倒排索引
传统数据库(如 MySQL)采用正向索引,例如当不是根据主键搜索索引失效时,正向索引往往需要全表逐条异议对比才能得到结果集。
ES 实现上述查询使用倒排索引:
文档(document):每条数据就是一个文档
词条(term):文档按照语义分成的词语
搜索时直接找唯一词条索引即可:
因此。倒排索引更适用于根据文档内容查找,故在搜索引擎中广泛使用。
# 1.2 ES 的一些概念
# 1.2.1 文档
ES 是面向文档存储的,可以使数据库中的一条商品数据,一个订单信息。文档数据灰分序列化为 json 格式后,存储在 ES 中。
# 1.2.2 字段
Json 文档中的字段(key)
# 1.2.3 索引和映射
索引(index):相同类型文档的集合
映射(mapping):索引中文档字段约束信息,类似表的结构约束
比如:
# 1.2.4 概念对比
然而,并不是说 ES 可以完全替代 MySQL,而是在某些方面比 MySQL 做得更好,且 MySQL 也有自己的优势,一般在优秀的架构中,两者都会存在,各自负责自己擅长的部分,相辅相成。
MySQL:擅长 ** 事务(定义了 ACID)** 类型操作,可以确保数据的安全和一致性
ES:擅长海量数据搜索、分析、计算
# 1.3 安装 ES、kibana
以下演示 Docker 上安装单节点的 ES
ES:7.16.3
Kibana:7.16.3
ES 和 Kibana 的版本一定要一致!!!
# 1.3.1 下载
Elastic Search:https://www.elastic.co/cn/downloads/past-releases/elasticsearch-7-16-3
Kibana:https://www.elastic.co/cn/downloads/past-releases/kibana-7-16-3
docker pull elasticsearch:7.16.3 | |
docker pull kibana:7.16.3 |
# 1.3.2 安装
创建一个虚拟网络,因为 Kibana 要与 ES 在同一个网络
h docker network create es-network
# 1.3.2.1 安装和部署 ES
运行 docker 命令,部署单点 ES:
h 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-network \
-p 9200:9200 \
-p 9300:9300 \
elasticsearch:7.16.3
命令解释:
-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
:端口映射配置
访问出现以下,就说明启动成功:
# 1.3.2.1 安装和部署 Kibana
kibana 可以给提供一个 elasticsearch 的可视化界面。
运行 docker 命令,部署 Kibana
h docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-network \
-p 5601:5601 \
kibana:7.16.3
--network es-network
:加入一个名为 es-network 的网络中,与 ES 在同一个网络中-e "ELASTICSEARCH_HOSTS=http://es:9200"
:设置 ES 的地址,因为 kibana 已经与 ES 在一个网络,因此可以用容器名直接访问 ES-p 5601:5601
:端口映射配置
访问 5601 端口能进去以下界面,说明 Kibana 暂时安装没问题:
打开左侧菜单栏,找到 Management -> Dev Tools,这是 Kibana 中提供了一个 DevTools 界面:
这个界面中可以编写 DSL 来操作 ES。并且对 DSL 语句有自动补全功能。
# 1.4 分词器
ES 在创建倒排索引时需要对文档先进行分词;在搜索时,需要对用户输入内容分词。但默认的分词规则对中文处理不友好。在 Kibana 中的 DevTools 测试:
由上述测试可以看出,对中文分词是单个字符进行分组,这显然不是按词语分词,不友好!
# 1.4.1 IK 分词器
处理中文分词一般使用 IK 分词器(https://github.com/medcl/elasticsearch-analysis-ik),同时 github 上的一个开源项目,通常采用离线安装方式。
安装插件需要知道 ES 的 plugins 目录位置,由于先前用了数据卷挂载,因此可以通过查看 ES 的数据卷目录,找到本地的插件映射目录:
l docker volume inspect es-plugins
上传解压后的分词插件到上述
/var/lib/docker/volumes/es-plugins/_data
目录下重启 ES
l docker restart es
# 查看日志输出
docker logs -f es
发现版本不匹配,ES 是 7.16.3,而 ik 是 8.x,因此分词器需要装回 7.16.3,再次测试:
测试,IK 包含两中分词模式:
- ik_smart:最少切分
- ik_max_word:最细切分
以搜索【Java 程序员就业情况】为例,测试结果如下:
可见:
ik_max_word细粒度更高,同时耗内存资源也越高,而ik_smart细粒度较低,但耗内存资源也偏低
;因此实践中应该根据具体需求进行选择。
# 1.4.2 IK 分词器的扩展和停用词典
随着互联网的发展,“造词运动” 也越发的频繁。出现了很多新的词语,比如” 奥利给 “,” 皇第的新衣 “等一些网络词语,而这些词语在现有的词典中并不存在,因此需要扩展;当然也有很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么在搜索时也应该忽略当前词汇。
IK 分词器强大的扩展词典和停用词典功能,如此可以在建索引时就直接补充扩展词典中的内容或者忽略停用词汇表中的内容。
添加 IK 分词器插件中的
config/KAnalyzer.cfg.xml
配置文件内容如下:l <?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">stopword.dic</entry>
<!-- 用户可以在这里配置远程扩展字典 -->
<!-- <entry key="remote_ext_dict">words_location</entry> -->
<!-- 用户可以在这里配置远程扩展停止词字典 -->
<!-- <entry key="remote_ext_stopwords">words_location</entry> -->
</properties>
如上,增加了两个词典:
- 扩展词典:ext.dic
- 停用词典:stopword.dic
同时要在与配置文件同一目录下新建上述两个词典文件,且文件编码必须是 UTF-8,里面填写你要扩展或禁止的词语即可:
测试如下:
# 2. 索引库操作
索引就像是关系型数据库中的表。
# 2.1 Mapping 映射索引
详细 mapping 属性请查看:https://www.elastic.co/guide/en/elasticsearch/reference/8.6/mapping-params.html
mapping 是对索引库中文档的约束,常见的 mapping 属性包括:
- type:字段数据类型,常见的简单类型有:
- 字符串:text(可分词的文本)、keyword(精确值,不可拆分,例如:国家,奥利给,IP 地址)
- 数值:long、integer、short、byte、double、float(与 Java 相似,因为 ES 底层就是 Java 实现的)
- 布尔:boolean
- 日期:date
- 对象:object
- index:是否创建索引,默认为 true(如果为 false,那么就不会创建该字段的倒排索引)
- analyzer:使用那种分词器(结合 text 一起使用)
- properties:该字段的子字段
ES 中没有数组类型,但是允许一个字段有多个值
# 2.2 索引库的 CRUD
# 2.2.1 创建索引库
ES 中通过 Restful 请求操作索引库、文档。请求内容用 DSL 语句来表示。创建索引库和 mapping 的 DSL 语法如下:
案例:
# 2.2.2 查看索引库
查看索引库语法:
GET /索引库名称 |
示例:GET /dubulingbo
{ | |
"dubulingbo" : { | |
"aliases" : { }, | |
"mappings" : { | |
"properties" : { | |
"email" : { | |
"type" : "keyword", | |
"index" : false | |
}, | |
"info" : { | |
"type" : "text", | |
"analyzer" : "ik_smart" | |
}, | |
"name" : { | |
"properties" : { | |
"firstName" : { | |
"type" : "keyword" | |
}, | |
"lastName" : { | |
"type" : "keyword" | |
} | |
} | |
} | |
} | |
}, | |
"settings" : { | |
"index" : { | |
"routing" : { | |
"allocation" : { | |
"include" : { | |
"_tier_preference" : "data_content" | |
} | |
} | |
}, | |
"number_of_shards" : "1", | |
"provided_name" : "dubulingbo", | |
"creation_date" : "1680087689422", | |
"number_of_replicas" : "1", | |
"uuid" : "v1VM4wJ5S_2LI8AtTxtDQA", | |
"version" : { | |
"created" : "7160399" | |
} | |
} | |
} | |
} | |
} |
# 2.2.3 修改索引库
一般而言,ES 不允许修改索引库,因为会导致建立的倒排索引失效,从而导致查询时,大大降低了性能。但是 ES 允许添加新的字段,语法如下:
PUT /索引库名称/_mapping | |
{ | |
"properties": { | |
"新字段名": { | |
"type": 字段类型值 | |
...... | |
}, | |
...... | |
} | |
} |
示例 1:为 dubulingbo 索引库添加一个 age 字段,类型为 integer
再次查询,已存在 age,说明添加成功:
示例 2:试图修改 age 字段类型为 long
需要注意的是,新添加的字段必须是原来索引库中不存在的字段,否则会报错,即添加失败!
# 2.2.4 删除索引库
DELETE /索引库名称 |
示例:删除 dubulingbo 索引库
# 3. 文档操作
文档就是数据,就是向已存在的索引库中增加数据(记录)。常见的文档操作有:新增、修改、查询、删除操作。
# 3.1 新增文档
DSL 语法如下:
POST /索引库名/_doc/文档id | |
{ | |
"字段1": "值1", | |
"字段2": "值2", | |
"字段3": { | |
"子属性1": "子属性值1", | |
"子属性1": "子属性值1", | |
// ...... | |
} | |
// ...... | |
} |
示例:
# 3.2 查询文档
DSL 语法:
GET /索引库名/_doc/要查询的文档id
示例:
不填 id 测试:
# 3.3 删除文档
DSL 语法:
DELETE /索引库名称/_doc/文档id |
- 示例:
- 若不携带 id 删除:
# 3.4 修改文档
# 3.4.1 全量修改
删除旧文档,添加新文档
- DSL 语法:
PUT /索引库名/_doc/文档id | |
{ | |
"字段1": "值1", | |
"字段1": "值1", | |
// ...... | |
} |
- 示例:
修改同一个文档,但是字段值不一样:
再次查询:
值得注意的是:先前的 info 字段已经被删除,然后新添加的 name 字段,这就是全量更新!
# 3.4.2 增量修改
只修改已存在的指定的字段值。
- DSL 语法为:
POST /索引库名/_update/文档id | |
{ | |
"doc": { | |
"字段名1": "字段值1", | |
"字段名1": "字段值1", | |
// ...... | |
} | |
} |
- 示例:
修改不存在的字段 age 为 18:
# 4. RestClient 操作索引库
创建、删除、判断索引库是否存在
# 4.1 RestClient
ES 官方提供了各种不同语言的客户端,用来操作 ES。这些客户端的本质就是组装 DSL 语句,通过 http 请求发送给 ES 执行,官方文档地址:https://www.elastic.co/guide/en/elasticsearch/client/index.html
以下以 Java 客户端的 RestClient 为例。参考文档:https://www.elastic.co/guide/en/elasticsearch/client/java-api-client/current/index.html
- 创建 mapping 需要考虑的问题:
- 字段名是什么,数据类型怎么选
- 是否参与搜索(决定字段的 index 为 true 还是 false)
- 是否分词(决定是 type 是 keyword 还是 text)
- 如果分词,分词器怎么选择
ES 默认是搜索某个字段的内容,要想同时搜索多个字段内容,可以使用 copy_to
属性:
示例:
"all": { | |
"type": "text", | |
"analyzer": "ik_max_word" | |
} | |
"brand": { | |
"type": "keyword", | |
"copy_to": "all" // 表示将当前字段的值拷贝到 all 字段,如此,直接搜索 all 字段就能解决 | |
} |
- 案例:将 hotel 表转化成 mapping
转成 mapping 如下:
// 创建酒店的 mapping | |
PUT /hotel | |
{ | |
"mappings": { | |
"properties": { | |
"id": { | |
"type": "keyword" | |
}, | |
"name": { | |
"type": "text", | |
"analyzer": "ik_max_word", | |
"copy_to": "all" | |
}, | |
"address": { | |
"type": "keyword", | |
"index": false | |
}, | |
"price": { | |
"type": "integer" | |
}, | |
"score": { | |
"type": "integer" | |
}, | |
"brand": { | |
"type": "keyword", | |
"copy_to": "all" | |
}, | |
"city": { | |
"type": "keyword" | |
}, | |
"starName": { | |
"type": "keyword" | |
}, | |
"business": { | |
"type": "keyword", | |
"copy_to": "all" | |
}, | |
"location": { | |
"type": "geo_point" // 地理坐标点 | |
}, | |
"pic": { | |
"type": "keyword", | |
"index": false | |
}, | |
"all": { | |
"type": "text", | |
"analyzer": "ik_max_word" | |
} | |
} | |
} | |
} |
# 4.2 初始化 JavaRestClient
- 引入 ES 的 RestHighLevelClient 依赖:
<dependency> | |
<groupId>org.elasticsearch.client</groupId> | |
<artifactId>elasticsearch-rest-high-level-client</artifactId> | |
<version>${elasticsearch.version}</version> | |
</dependency> |
- 因为 SpringBoot 默认的 ES 版本是 7.6.2,所以要换成之前安装的 ES 版本 7.16.3:
<properties> | |
<maven.compiler.source>8</maven.compiler.source> | |
<maven.compiler.target>8</maven.compiler.target> | |
<!-- 修改默认的 Spring Boot 中 elasticsearch 的版本,默认是 7.6.2 --> | |
<elasticsearch.version>7.16.3</elasticsearch.version> | |
</properties> |
- 初始化 RestHighLevelClient:
RestHighLevelClient client = new RestHighLevelClient(RestClient.builder( | |
HttpHost.create("http://你的虚拟机ip:9200") | |
)); |
# 4.3 JavaRestClient 操作索引库
对索引库操作代码模版示意:
代码如下:(包含创建、删除、判断是否存在)
import org.apache.http.HttpHost; | |
import org.elasticsearch.client.RequestOptions; | |
import org.elasticsearch.client.RestClient; | |
import org.elasticsearch.client.RestHighLevelClient; | |
import org.elasticsearch.client.indices.CreateIndexRequest; | |
import org.elasticsearch.xcontent.XContentType; | |
import org.junit.jupiter.api.AfterEach; | |
import org.junit.jupiter.api.BeforeEach; | |
import org.junit.jupiter.api.Test; | |
import java.io.IOException; | |
// 创建索引的 json 模版 | |
import static com.dublbo.hotel.constants.HotelConstant.MAPPING_TEMPLATE; | |
/** | |
* @author dubulingbo, 2023/3/29 22:09. | |
*/ | |
public class HotelIndexTest { | |
private RestHighLevelClient client; | |
@BeforeEach | |
void setUp() { | |
this.client = new RestHighLevelClient(RestClient.builder( | |
HttpHost.create("http://10.255.125.164:9200") | |
)); | |
} | |
@Test | |
void createHotelIndex() throws IOException { | |
// 1. 创建 Request 的对象 | |
CreateIndexRequest request = new CreateIndexRequest("hotel"); | |
// 2. 准备请求参数:DSL 语句 | |
request.source(MAPPING_TEMPLATE, XContentType.JSON); | |
// 3. 发送请求,默认就是 PUT 请求 | |
client.indices().create(request, RequestOptions.DEFAULT); | |
} | |
@Test | |
void deleteHotelIndex() throws IOException { | |
// 1. 创建 Request 的对象 | |
DeleteIndexRequest request = new DeleteIndexRequest("hotel"); | |
// 2. 发送删除请求 | |
client.indices().delete(request, RequestOptions.DEFAULT); | |
} | |
@Test | |
void existsHotelIndex() throws IOException { | |
// 1. 创建 Request 的对象 | |
GetIndexRequest request = new GetIndexRequest("hotel"); | |
// 2. 判断是否存在 | |
boolean exists = client.indices().exists(request, RequestOptions.DEFAULT); | |
System.out.println(exists ? "hotel索引库存在" : "hotel索引库不存在"); | |
} | |
@AfterEach | |
void tearDown() throws IOException { | |
this.client.close(); | |
} | |
} |
# 4.4 JavaRestClient 操作文档
案例:从数据库中导入 hotel 表到索引库,实现对 hotel 索引库的文档的 CRUD
- 初始化 JavaRestClient
- 利用 JavaRestClient 新增酒店数据
- 利用 JavaRestClient 根据 id 查询酒店数据
- 利用 JavaRestClient 删除酒店数据
- 修改 JavaRestClient 酒店数据
代码如下:
import com.alibaba.fastjson.JSON; | |
import com.dublbo.hotel.pojo.Hotel; | |
import com.dublbo.hotel.pojo.HotelDocument; | |
import com.dublbo.hotel.service.IHotelService; | |
import org.apache.http.HttpHost; | |
import org.elasticsearch.action.delete.DeleteRequest; | |
import org.elasticsearch.action.delete.DeleteResponse; | |
import org.elasticsearch.action.get.GetRequest; | |
import org.elasticsearch.action.get.GetResponse; | |
import org.elasticsearch.action.index.IndexRequest; | |
import org.elasticsearch.action.update.UpdateRequest; | |
import org.elasticsearch.action.update.UpdateResponse; | |
import org.elasticsearch.client.RequestOptions; | |
import org.elasticsearch.client.RestClient; | |
import org.elasticsearch.client.RestHighLevelClient; | |
import org.elasticsearch.xcontent.XContentType; | |
import org.junit.jupiter.api.AfterEach; | |
import org.junit.jupiter.api.BeforeEach; | |
import org.junit.jupiter.api.Test; | |
import org.springframework.boot.test.context.SpringBootTest; | |
import javax.annotation.Resource; | |
import java.io.IOException; | |
/** | |
* @author dubulingbo, 2023/3/30 11:08. | |
*/ | |
@SpringBootTest | |
public class HotelDocumentTest { | |
private RestHighLevelClient client; | |
@Resource | |
private IHotelService hotelService; | |
// 初始化 | |
@BeforeEach | |
void setUp() { | |
this.client = new RestHighLevelClient(RestClient.builder( | |
HttpHost.create("http://10.255.125.164:9200") | |
)); | |
} | |
// 新增文档 | |
@Test | |
void testAddDocument() throws IOException { | |
// 根据 id 查询酒店数据 | |
Hotel hotel = hotelService.getById(395702L); | |
// 转化为文档类型 | |
HotelDocument document = new HotelDocument(hotel); | |
// 1. 准备 Request 对象 | |
IndexRequest request = new IndexRequest("hotel").id(document.getId().toString()); | |
// 2. 准备 Json 文档 | |
request.source(JSON.toJSONString(document), XContentType.JSON); | |
// 3. 发送请求 | |
client.index(request, RequestOptions.DEFAULT); | |
} | |
// 查询文档,根据 id | |
@Test | |
void testGetDocumentById() throws IOException { | |
GetRequest request = new GetRequest("hotel", "395702"); | |
GetResponse response = client.get(request, RequestOptions.DEFAULT); | |
// 解析响应结果 | |
String json = response.getSourceAsString(); | |
HotelDocument doc = JSON.parseObject(json, HotelDocument.class); | |
System.out.println(doc); | |
} | |
// 局部更新,(全量更新和添加一样) | |
@Test | |
void testUpdateDocument() throws IOException { | |
UpdateRequest request = new UpdateRequest("hotel", "395702"); | |
request.doc( | |
"price", "-18", // 直接逗号隔开, key -> value | |
"starName", "啥也不是" | |
); | |
UpdateResponse response = client.update(request, RequestOptions.DEFAULT); | |
System.out.println(response); | |
} | |
// 删除文档 | |
@Test | |
void testDeleteDocumentById() throws IOException { | |
DeleteRequest request = new DeleteRequest("hotel", "395702"); | |
DeleteResponse r = client.delete(request, RequestOptions.DEFAULT); | |
System.out.println(r); | |
} | |
// 释放对象 | |
@AfterEach | |
void tearDown() throws IOException { | |
this.client.close(); | |
} | |
} |
- 批量导入文档
@Test | |
void testBatchAddDocument() throws IOException { | |
List<Hotel> list = hotelService.list(Wrappers.<Hotel>query().like("city", "上海")); | |
System.out.println("待新增文档数为:" + list.size()); | |
// 1. 创建 Request | |
BulkRequest request = new BulkRequest(); | |
// 2. 添加多个新增的 Request | |
for (Hotel hotel : list) { | |
HotelDocument doc = new HotelDocument(hotel); | |
request.add( | |
new IndexRequest("hotel") | |
.id(doc.getId().toString()) | |
.source(JSON.toJSONString(doc), XContentType.JSON) | |
); | |
} | |
// 3. 发送请求 | |
client.bulk(request, RequestOptions.DEFAULT); | |
} |
测试:
共插入 83 条文档,与 mysql 查询的数据量一致,导入成功!