Elasticsearch-1

3713 字
19 分钟
Elasticsearch-1

引例#

现在假设我们有如下的一张数据库表

现在假设我们有一个需求是从这张表中搜索“手机”或者“华为手机”的相关信息,我们会怎么做?

select * from product where name like "%手机%";
select * from product where name like "%华为手机%";

但是针对这张表,我们如果直接通过以上SQL语句来查询,会存在两个问题:

  • 针对单表的全表扫描效率低
  • 关系数据库中提供的查询功能弱

思考一下,我们的一个电商网站中,有上百万商品数据,如果我们采用like的方式,去实现商品搜索的功能,其效率是非常低下的,同时由于用户输入的商品关键字是比较随意的,使用like的方式往往也很难真正查询到用户想要的商品。

ES介绍及基本概念#

在很多场景下,比如电商网站的商品搜索,我们都需要使用全文检索的功能查询所需的内容,那么如何高效的实现全文检索的功能呢?解决之道就在于Elastic Search:

ElasticSearch是一个基于Lucene的分布式、高扩展、高实时的基于RESTful 风格API的搜索与数据分析引擎

RestFUL风格的API:
1. 发送JSON数据,返回JSON响应
2. 请求的类型就代表了对资源的操作类型
GET: 查询
POST: 修改
DELETE:删除
PUT:新增

为什么Elastic Search能够实现大规模数据场景下的高效全文检索呢?主要是因为在ES中,数据的存储和组织方式与关系数据库不同。

我们首先需要了解ES中的基本概念:

  • 字段(Field): 一个字段表示一个属性,类比于数据库表中的属性,数据库中的一行数据通常是多个属性值组成

  • 文档(document):在ES中数据的存储和关系数据库不同,所有的数据都是以文档的JSON document的形式存在,document是ES中索引搜索的最小的数据单位,类比于数据库中的一行数据,通常有多个字段值组成

  • 映射(mapping): mapping定义了document中每个字段的类型、字段所使用的分词器等。相当于关系型数据库中的表结构。

  • 索引(index): ElasticSearch存储数据的地方,可以理解成关系型数据库中的数据库概念,存放一类相同或者类似的document,比如一个员工索引,商品索引。

  • 类型(Type):逻辑上的数据分类,一种type就像一张表。如用户表、角色表等。在Elasticsearch6.X默认type为_doc,es 7.x中删除了type的概念

在了解了ES的基本概念之后,接下来,我们可以大致解释下,为何ES可以实现高效的全文检索功能,其中一个很重要的原因是ES使用了倒排索引(这里要注意的一点是,倒排索引和文档的存储本身没有关系,只是为了快速的全文检索,针对document的一个或者多个字段,所创建的索引)

那么什么是倒排索引呢?为了理解什么是倒排索引,我们先来理解正排索引。针对以下3条document文档数据所创建的正排索引如下:

  • 凡是索引其本质都是在建立一种映射关系
  • 无论是正排索引还是倒排索引,在创建索引时都是要对目标字段的值进行分词的。
  • 正排索引,建立的是文档的唯一标识Id(内容所在位置) ——> 文档目标字段内容的映射

如果我们是基于正向索引来查找和“华为手机”相关的商品信息(根据商品的title字段的值来匹配),首先对搜索的关键字也会做分词处理,比如分解为”华为”和”手机”两个关键词,然后遍历,每一个文档,和将关键词和文档中的分词内容进行匹配,此时,我们可以查找到我们所需要的华为手机的相关商品信息。

但是,使用正向索引我们仍然无法避免,遍历每一个商品对应的文档(“类似全表扫描”),这种匹配方式,效率比较低下。接下来,我们换种方式,基于倒排索引,来实现搜索。

  • 反向索引是以文段目标字段所有可能的分词结果为key,其value表达的是,包含改词的文档Id(即文档位置)
  • 通过对比可知,反向索引表达的是文档字段内容 ——> 包含该内容的文档位置(文档Id)的映射

如果我们,基于倒排索引,来查找和关键字“华为手机”相关的商品,那么很显然,我们很容易就可以查询出想要的结果,并且还不需要,遍历每一个商品信息。同理,ES在搜索时就是基于倒排索引,所以它的搜索性能很好。

ES及可视化客户端安装#

ElasticSearch安装#

参考环境搭建文档

kibana安装#

参考环境搭建文档

Restful API操作ES#

ES虽然是基于Java语言开发的,但它的使用却不仅仅局限于Java语言,因为ES对外提供了Restful风格的API,我们可以通过这些Restful风格的API向ES发送请求,从而操作ES。

操作索引#

  • 创建索引
PUT http://ip:端口/索引名称
  • 查询索引
GET http://ip:端口/索引名称 # 查询单个索引信息
GET http://ip:端口/索引名称1,索引名称2... # 查询多个索引信息
GET http://ip:端口/_all # 查询所有索引信息
  • 删除索引
DELETE http://ip:端口/索引名称

•关闭、打开索引

POST http://ip:端口/索引名称/_close
POST http://ip:端口/索引名称/_open
# 如果一个索引是关闭的状态,那么这个索引中的文档是不能被搜索的;但是如果一个索引是关闭的状态,索引本身的信息还是可以查询的

数据类型#

简单数据类型#

我们先来看一个简单的映射定义:

PUT teacher/_mapping
{
"properties": {
"id": {
"type": "integer"
},
"name": {
"type": "text"
},
"isMale": {
"type": "boolean"
}
}
}

在定义映射的时候,类比于定义数据库中的表结构,我们需要指明每一个字段的名称,数据类型等等信息,所以我们先得了解映射中包含的数据类型。

  • 字符串
text:会分词,不支持聚合
keyword:不会分词,将全部内容作为一个词条,支持聚合
  • 数值:long, integer, short, byte, double, float, half_float, scaled_float

  • 布尔:boolean

  • 二进制:binary

  • 范围类型

integer_range, float_range, long_range, double_range, date_range
  • 日期

复杂数据类型#

•数组:[ ] 没有专门的数组类型,ES会自动处理数组类型数据

•对象:{ } Object: object(for single JSON objects 单个JSON对象)

操作映射#

  • 添加映射
#添加映射
PUT student/_mapping
{
"properties":{
"name":{
"type":"text"
},
"age":{
"type":"integer"
}
}
}
#查询映射
GET studnt/_mapping
  • 创建索引并添加映射
#创建索引并添加映射
PUT teacher
{
"mappings": {
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "integer"
}
}
}
}
# 查询映射
GET teacher/_mapping
  • 添加字段
#添加字段
PUT teacher/_mapping
{
"properties": {
"name": {
"type": "text"
},
"age": {
"type": "integer"
}
}
}
# 查询映射
GET teacher/_mapping

操作文档#

•添加文档,指定id

POST teacher/_doc/2
{
"name":"张三",
"age":18,
"address":"北京"
}
GET /teacher/_doc/2

•添加文档,不指定id

#添加文档,不指定id,自动生成
POST teacher/_doc/
{
"name":"张三",
"age":18,
"address":"北京"
}
#查询所有文档
GET /teacher/_search
  • 修改文档(全量覆盖)

    POST teacher/_doc/2
    {
    "name":"张三",
    "age":18,
    "address":"北京"
    }
  • 修改文档(只修改部分)

    POST teacher/_update/2
    {
    "doc": {
    "name": "李四"
    }
    }
  • 删除文档

#删除指定id文档
DELETE teacher/_doc/1

分词器#

对于Elastic Search而言,在生成倒排索引时,需要对文档字段分词,在搜索时,还需要对搜索关键字进行分词,分词的工作是由分词器来完成的,但是很遗憾,ES中默认使用的分词器,对中文支持的并不好,会对中文逐字拆分。所以对于中文内容的分词,我们通常会采用,对中文支持比较好的IK分词器。

IKAnalyzer是一个开源的,基于java语言开发的轻量级的中文分词工具包,基于Maven构建,具有60万字/秒的高速处理能力,并且支持用户词典扩展定义。

IK分词器的安装#

参考环境搭建文档

IK分词器的使用#

IK分词器有两种分词模式:ik_max_word和ik_smart模式。

1、ik_max_word

会将文本做最细粒度的拆分,比如会将“好好学习, 天天向上”拆分为“好好学习,好好学、好好、好学、学习、天天向上、天天,向上。

#方式一ik_max_word
GET _analyze
{
"analyzer": "ik_max_word",
"text": "好好学习, 天天向上"
}

ik_max_word分词器执行如下:

{
"tokens" : [
{
"token" : "好好学习",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "好好学",
"start_offset" : 0,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 1
},
{
"token" : "好好",
"start_offset" : 0,
"end_offset" : 2,
"type" : "CN_WORD",
"position" : 2
},
{
"token" : "好学",
"start_offset" : 1,
"end_offset" : 3,
"type" : "CN_WORD",
"position" : 3
},
{
"token" : "学习",
"start_offset" : 2,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 4
},
{
"token" : "天天向上",
"start_offset" : 6,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 5
},
{
"token" : "天天",
"start_offset" : 6,
"end_offset" : 8,
"type" : "CN_WORD",
"position" : 6
},
{
"token" : "向上",
"start_offset" : 8,
"end_offset" : 10,
"type" : "CN_WORD",
"position" : 7
}
]
}

2、ik_smart 会做最粗粒度的拆分,比如会将“好好学习, 天天向上”拆分为好好学习、天天向上。

#方式二ik_smart
GET _analyze
{
"analyzer": "ik_smart",
"text": "好好学习, 天天向上"
}

ik_smart分词器执行如下:

{
"tokens" : [
{
"token" : "好好学习",
"start_offset" : 0,
"end_offset" : 4,
"type" : "CN_WORD",
"position" : 0
},
{
"token" : "天天向上",
"start_offset" : 5,
"end_offset" : 9,
"type" : "CN_WORD",
"position" : 1
}
]
}

指定分词器查询文档#

文档的查询可以分为两种查询方式:

•词条查询(term):词条查询不会分析查询条件,只有当词条和查询字符串完全匹配时才匹配搜索

•全文查询(match):全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集

准备工作如下:

  • 创建索引,添加映射,并指定分词器为ik分词器
PUT member
{
"mappings": {
"properties": {
"name": {
"type": "keyword"
},
"address": {
"type": "text",
"analyzer": "ik_max_word"
}
}
}
}
  • 添加文档
POST member/_doc/1
{
"name":"zs",
"age":18,
"address":"武汉市洪山区"
}
POST member/_doc/2
{
"name":"lisi",
"age":18,
"address":"武汉市"
}
POST /member/_doc/3
{
"name":"ww",
"age":18,
"address":"武汉黄陂"
}
  • 查询映射
GET member/_search

4.查看分词效果

GET _analyze
{
"analyzer": "ik_max_word",
"text": "武汉市洪山区"
}

下面分别采用两种方式查询,分词都是用IK分词器:

  • 词条查询:term

查询member中匹配到”武汉”两字的词条

GET member/_search
{
"query": {
"term": {
"address": {
"value": "武汉"
}
}
}
}
  • 全文查询:match

    全文查询会分析查询条件,先将查询条件进行分词,然后查询,求并集

GET member/_search
{
"query": {
"match": {
"address":"武汉黄陂"
}
}
}

Java 操作ES#

我们仍然基于SpringBoot工程,首先引入依赖

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.17</version>
</parent>
<!-- springboot-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
</dependency>
<!-- springboot test-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
<!--引入es的坐标-->
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-high-level-client</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>elasticsearch-rest-client</artifactId>
<version>7.8.0</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>7.8.0</version>
</dependency>

定义ES配置类

@Configuration
@ConfigurationProperties(prefix="elasticsearch")
public class ElasticSearchConfig {
private String host;
private int port;
public String getHost() {
return host;
}
public void setHost(String host) {
this.host = host;
}
public int getPort() {
return port;
}
public void setPort(int port) {
this.port = port;
}
@Bean
public RestHighLevelClient client(){
return new RestHighLevelClient(RestClient.builder(
new HttpHost(host,port,"http")
));
}
}

准备好测试类

@SpringBootTest(classes = ESApplication.class)
class ElasticsearchDay01ApplicationTests {
@Autowired
RestHighLevelClient client;
@Test
public void test() {
System.out.println(client);
}
}

创建索引#

1.添加索引

/**
* 添加索引
* @throws IOException
*/
@Test
public void addIndex() throws IOException {
//1.使用client获取操作索引对象
IndicesClient indices = client.indices();
//2.具体操作获取返回值
//2.1 设置索引名称
CreateIndexRequest createIndexRequest=new CreateIndexRequest("member");
CreateIndexResponse createIndexResponse = indices.create(createIndexRequest, RequestOptions.DEFAULT);
//3.根据返回值判断结果
System.out.println(createIndexResponse.isAcknowledged());
}

2.添加索引,并添加映射

/**
* 添加索引,并添加映射
*/
@Test
public void addIndexAndMapping() throws IOException {
//1.使用client获取操作索引对象
IndicesClient indices = client.indices();
//2.具体操作获取返回值
//2.具体操作,获取返回值
CreateIndexRequest createIndexRequest = new CreateIndexRequest("member");
//2.1 设置mappings
String mapping = "{\n" +
" \"properties\" : {\n" +
" \"address\" : {\n" +
" \"type\" : \"text\",\n" +
" \"analyzer\" : \"ik_max_word\"\n" +
" },\n" +
" \"age\" : {\n" +
" \"type\" : \"long\"\n" +
" },\n" +
" \"name\" : {\n" +
" \"type\" : \"keyword\"\n" +
" }\n" +
" }\n" +
" }";
createIndexRequest.mapping(mapping,XContentType.JSON);
CreateIndexResponse createIndexResponse = indices.create(createIndexRequest, RequestOptions.DEFAULT);
//3.根据返回值判断结果
System.out.println(createIndexResponse.isAcknowledged());
}

查询、删除、判断索引#

查询索引

/**
* 查询索引
*/
@Test
public void queryIndex() throws IOException {
IndicesClient indices = client.indices();
GetIndexRequest getRequest=new GetIndexRequest("member");
GetIndexResponse response = indices.get(getRequest, RequestOptions.DEFAULT);
Map<String, MappingMetaData> mappings = response.getMappings();
for (String key : mappings.keySet()) {
System.out.println(key+":"+mappings.get(key).getSourceAsMap());
}
}

删除索引

/**
* 删除索引
*/
@Test
public void deleteIndex() throws IOException {
IndicesClient indices = client.indices();
DeleteIndexRequest deleteRequest=new DeleteIndexRequest("member");
AcknowledgedResponse delete = indices.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(delete.isAcknowledged());
}

索引是否存在

/**
* 索引是否存在
*/
@Test
public void existIndex() throws IOException {
IndicesClient indices = client.indices();
GetIndexRequest getIndexRequest=new GetIndexRequest("member");
boolean exists = indices.exists(getIndexRequest, RequestOptions.DEFAULT);
System.out.println(exists);
}

添加文档#

1.添加文档,使用map作为数据

@Test
public void addDoc1() throws IOException {
Map<String, Object> map=new HashMap<>();
map.put("name","zs");
map.put("age","18");
map.put("address","武汉市洪山区");
IndexRequest request=new IndexRequest("member").id("1").source(map);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}

2.添加文档,使用对象作为数据

@Test
public void addDoc2() throws IOException {
Person person=new Person();
person.setId("2");
person.setName("lisi");
person.setAge(18);
person.setAddress("武汉");
String data = JSON.toJSONString(person);
IndexRequest request=new IndexRequest("member").id(person.getId()).source(data,XContentType.JSON);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}

修改、查询、删除文档#

1.修改文档:添加文档时,如果id存在则修改(全量修改),或者做增量修改

/**
* 修改文档:添加文档时,如果id存在则修改,id不存在则添加
*/
@Test
public void UpdateDoc() throws IOException {
Person person=new Person();
person.setId("2");
person.setName("ww");
person.setAge(20);
person.setAddress("长城");
String data = JSON.toJSONString(person);
IndexRequest request=new IndexRequest("member").id(person.getId()).source(data,XContentType.JSON);
IndexResponse response = client.index(request, RequestOptions.DEFAULT);
System.out.println(response.getId());
}
/*
增量修改,只修改文档的一部分
*/
@Test
public void update() throws IOException {
// 构造修改请求,修改member索引中,_id值为1的文档
UpdateRequest updateRequest = new UpdateRequest("member", "100023501");
// 准备修改的字段以及字段值
HashMap<String, Object> fieldMap = new HashMap<>();
// 假设只修改age字段的值
fieldMap.put("age", 100);
updateRequest.doc(fieldMap);
UpdateResponse update = client.update(updateRequest, RequestOptions.DEFAULT);
System.out.println(update.status());
}

3.根据id查询文档

/**
* 根据id查询文档
*/
@Test
public void getDoc() throws IOException {
//设置查询的索引、文档
GetRequest indexRequest=new GetRequest("member","1");
GetResponse response = client.get(indexRequest, RequestOptions.DEFAULT);
System.out.println(response.getSourceAsString());
}

4.根据id删除文档

/**
* 根据id删除文档
*/
@Test
public void delDoc() throws IOException {
//设置要删除的索引、文档
DeleteRequest deleteRequest=new DeleteRequest("member","1");
DeleteResponse response = client.delete(deleteRequest, RequestOptions.DEFAULT);
System.out.println(response.getId());
}

文章分享

如果这篇文章对你有帮助,欢迎分享给更多人!

Elasticsearch-1
https://firefly-mu-weld.vercel.app/posts/microservice-17-elasticsearch-1/
作者
Daisy
发布于
2026-06-14
许可协议
CC BY-NC-SA 4.0
Profile Image of the Author
Daisy
Hello, I'm Daisy.
公告
欢迎来到我的博客!这是一则示例公告。
分类
标签

文章目录