Content Table

Elasticsearch 入门

ElasticSearch (下面简称 ES) 是一个基于 Lucene 的全文检索服务器,本文简单的介绍 ES 的安装、配置、启动、一些基本概念、中文分词以及使用 Java 编程访问 ES 等。

安装配置启动

  1. 安装: 目前 spring-data-elasticsearch 最高支持 elasticsearch-6.2.2 (可参考最后的版本对应进行选择),所以下载 https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.2.zip,解压即可

  2. 配置: 修改配置文件 config/elasticsearch.yml (只介绍单机环境的,中小型应用足够了):

    1
    2
    3
    4
    cluster.name: ebag      # 集群名称
    node.name: node-1 # 节点名称
    network.host: 0.0.0.0 # 访问地址, 局域网需要访问
    http.port: 9200 # 端口
  3. 启动: elasticsearch -d,注意: Linux 下不允许使用 root 用户启动,可以创建一个用户如 elasticsearch,然后使用此用户启动 ES:

    • useradd elasticsearch
    • passwd elasticsearch
    • su elasticsearch
    • elasticsearch -d
  4. 浏览器中访问 http://localhost:9200,输出如下则说明 ES 启动成功:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    {
    "name": "node-1",
    "cluster_name": "ebag",
    "cluster_uuid": "Ogsv5NneTHyHmWDWM5hH5A",
    "version": {
    "number": "6.2.2",
    "build_hash": "10b1edd",
    "build_date": "2018-02-16T19:01:30.685723Z",
    "build_snapshot": false,
    "lucene_version": "7.2.1",
    "minimum_wire_compatibility_version": "5.6.0",
    "minimum_index_compatibility_version": "5.0.0"
    },
    "tagline": "You Know, for Search"
    }

启动时如果发生错误,可参考 https://www.jianshu.com/p/312dfaa3a27b

端口

ES 中常用端口有:

  • 9200: 作为 Http 协议,主要用于外部通讯,例如使用 curl 和浏览器中访问 ES
  • 9300: 作为 Tcp 协议,ES 集群之间是通过 9300 进行通讯,Java 客户端访问 ES 也是使用端口 9300

中文分词器

ES 内置了很多分词器 (analyzer),例如 standard 分词器、simple 分词器、Whitespace 分词器、keyword 分词器等等,但是对中文支持都很不好,中文分词器推荐使用 IK,安装比较简单,在 Elasticsearch 目录下执行

1
./bin/elasticsearch-plugin install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v6.2.2/elasticsearch-analysis-ik-6.2.2.zip

如果下载失败,可以使用下载工具先下载得到 elasticsearch-analysis-ik-6.2.2.zip,解压得到目录 elasticsearch,把这个目录重命名为 ik,复制目录 ik 到目录 ${elasticsearch}/plugins,重启 Elasticsearch 即可生效。

IK 有 2 种类型: ik_smart 和 ik_max_word:

  • ik_smart: 会做最粗粒度的拆分,已被分出的词语将不会再次被其它词语占有,比如会将中华人民共和国国歌拆分为中华人民共和国国歌
  • ik_max_word: 会将文本做最细粒度的拆分,尽可能多的拆分出词语,比如会将中华人民共和国国歌拆分为中华人民共和国中华人民中华华人人民共和国人民共和国共和国歌等,会穷尽各种可能的组合

使用 curl 查看分词效果:

1
2
curl -XPOST http://localhost:9200/_analyze?pretty -H 'Content-Type:application/json' -d' { "analyzer": "ik_smart", "text": "中华人民共和国国歌" } '
curl -XPOST http://localhost:9200/_analyze?pretty -H 'Content-Type:application/json' -d' { "analyzer": "ik_max_word", "text": "中华人民共和国国歌" } '

如果使用 VS Code 的 Rest Client 的插件,还可以用下面的代码查看分词的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
### 分词 ik-smart
http://localhost:9200/_analyze?pretty
Content-Type: application/json

{
"analyzer": "simple",
"text": "中华人民共和国公民"
}

### 分词 ik_max_word
http://localhost:9200/_analyze?pretty
Content-Type: application/json

{
"analyzer": "ik_max_word",
"text": "中华人民共和国公民"
}

修改 analyzer 为不同的分词器,看看不同的分词效果。

可视化客户端

ES 可视化管理工具, 可使用 ElasticHD:

  1. 下载 (Mac 选择 elasticHD_darwin_i386.zip)
  2. 解压
  3. 启动: ./ElasticHD -p 127.0.0.1:9800
  4. 访问: http://localhost:9800

数据类型

ES 的数据类型有:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public enum FieldType {
Text,
Integer,
Long,
Date,
Float,
Double,
Boolean,
Object,
Auto,
Nested,
Ip,
Attachment,
Keyword
}

其中 Text 和 Keyword 都是存储字符串的,但是建立索引和搜索的时候是不太一样:

  • Text:

    • 支持分词、全文检索、支持模糊、精确查询,不支持聚合、排序操作
    • 最大支持的字符长度无限制,适合大字段存储
  • Keyword:

    • 不进行分词、直接索引、精确匹配、不支持模糊,支持聚合、排序操作
    • 最大支持的长度为 32766 个 UTF-8 类型的字符

Java 编程访问 ES

下面举例使用 spring-data-elasticsearch 的 ElasticsearchTemplate 访问 ES。

A. Gradle 依赖:

1
2
3
4
5
6
7
8
9
10
compile(
"org.springframework:spring-context-support:5.0.2.RELEASE",
"org.springframework.data:spring-data-elasticsearch:3.1.10.RELEASE",
)

// 注: 下面的依赖是为了开发方便而使用的,不是必须的依赖
testCompile "junit:junit:4.12"
testCompile "org.springframework:spring-test:5.0.2.RELEASE"
compileOnly "org.projectlombok:lombok:1.16.20"
annotationProcessor "org.projectlombok:lombok:1.16.20"

B. Spring 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch"
xsi:schemaLocation="http://www.springframework.org/schema/data/elasticsearch
http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<elasticsearch:transport-client id="client" cluster-nodes="127.0.0.1:9300" cluster-name="ebag"/>

<bean name="elasticsearchTemplate" class="org.springframework.data.elasticsearch.core.ElasticsearchTemplate">
<constructor-arg name="client" ref="client"/>
</bean>
</beans>

C. 定义类 User (对应 ES 的 Document):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import lombok.experimental.Accessors;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;

@Getter
@Setter
@ToString
@Accessors(chain = true)
@Document(indexName = "user")
public class User {
@Id
private long id;

private String username; // 英文
private String nickname; // 中文

public User() {}

public User(long id, String username, String nickname) {
this.id = id;
this.username = username;
this.nickname = nickname;
}
}

提示:

  • @Document(indexName = "user") 表示 User 的对象作为一个 Document 存储到 ES 的 Index user
  • @Id private long id 表示使用 User’s id 作为 ES Document 的 ID
  • User 的所有属性都会保存到 ES

D. 插入和查询:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import com.xtuer.bean.User;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchTemplate;
import org.springframework.data.elasticsearch.core.query.IndexQueryBuilder;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@ContextConfiguration({"classpath:elasticsearch.xml"})
public class XServiceTest {
@Autowired
private ElasticsearchTemplate elasticsearchTemplate;

// 初始化数据
@Test
public void initIndex() {
// 删除 index user
elasticsearchTemplate.deleteIndex(User.class);

// 插入 users 到 index user (index 不存在会自动创建)
elasticsearchTemplate.index(new IndexQueryBuilder().withObject(new User(1, "Marjory Joyce", "中华人民共和国公民")).build());
elasticsearchTemplate.index(new IndexQueryBuilder().withObject(new User(2, "Lamb Cook", "中华人民")).build());
elasticsearchTemplate.index(new IndexQueryBuilder().withObject(new User(3, "Katharine Warren", "和")).build());
elasticsearchTemplate.index(new IndexQueryBuilder().withObject(new User(4, "Dillon George", "共和国公民")).build());
elasticsearchTemplate.index(new IndexQueryBuilder().withObject(new User(5, "Dillon George", "你和我")).build());
}

// 使用 username 查询
@Test
public void findUsersByUsername() {
SearchQuery query = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchQuery("username", "Cook")).build();
List<User> users = elasticsearchTemplate.queryForList(query, User.class); // User.class 指定了 indexName, 返回类型
System.out.println(users);
}

// 使用 nickname 查询
@Test
public void findUsersByNickname() {
SearchQuery query = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchQuery("nickname", "中华人民共和国公民")).build();
List<User> users = elasticsearchTemplate.queryForList(query, User.class);
System.out.println(users);
}
}

执行 findUsersByNickname() 发现把 initIndex()中插入的所有数据都搜索出来了,上面安装的中文分词器 IK 并没有生效,这是因为 ES 中存储 User 的 Document 中的属性没有指定分词器,则自动使用默认的 standard 分词器 (每个中文字符作为一个 token),所以 nickname 只要包含中华人民共和国公民中的任何一个中文字符的 Document 都会被搜索出来。

为了使用 IK 分词器,需要修改 2 个地方:

  1. 使用注解 @Field 给属性 nickname 设定类型为 Text 和分词器 ik_smart (根据业务需求,也可以用 ik_max_word):

    1
    2
    @Field(type = FieldType.Text, searchAnalyzer = "ik_smart", analyzer = "ik_smart")
    private String nickname;
  2. 在插入数据前给 ElasticsearchTemplate 添加映射器 User.class,ElasticsearchTemplate 会根据 User.class 中的注解 @Field 创建 Document 的 schema,确定属性的类型和分词器,插入数据时就会使用正确的分词器建立索引了:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    public void initIndex() {
    // 删除 index user
    elasticsearchTemplate.deleteIndex(User.class);

    // 如果 Index 不存在则创建
    if (!elasticsearchTemplate.indexExists(User.class)) {
    elasticsearchTemplate.createIndex(User.class);
    }

    // 如果不设置,则会使用默认分词器
    elasticsearchTemplate.putMapping(User.class); // 需要索引已经存在, 会使用 User 中的 @Field 确定分词器

    // 插入 users 到 index user
    elasticsearchTemplate.index(new IndexQueryBuilder().withObject(new User(1, "Marjory Joyce", "中华人民共和国公民")).build());
    }
  3. 调用 findUsersByNickname() 进行查询,IK 分词器生效了 (搜索时不需要给 ElasticsearchTemplate 添加映射器 User.class)

推荐把 ES 的操作放到 Service 里,Service 实现 InitializingBean 接口,在 afterPropertiesSet() 中设置映射器:

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class ElasticSearchService implements InitializingBean {
... // 操作 ES

@Override
public void afterPropertiesSet() throws Exception {
if (!elasticsearchTemplate.indexExists(User.class)) {
elasticsearchTemplate.createIndex(User.class);
}

elasticsearchTemplate.putMapping(User.class);
}
}

E. 分页查询

使用 PageRequest 创建分页对象,页码从 0 开始:

1
2
3
4
5
6
7
8
public void findUsersByNickname() {
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("nickname", "中华人民共和国公民"))
.withPageable(PageRequest.of(0, 1))
.build();
List<User> users = elasticsearchTemplate.queryForList(query, User.class);
System.out.println(users);
}

F. 多条件查询

使用 BoolQueryBuilder 创建多条件 Query:

1
2
3
4
5
6
7
8
9
10
11
12
public void findUsers() {
BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
boolQueryBuilder.must(QueryBuilders.matchQuery("username", "Joyce"));
boolQueryBuilder.must(QueryBuilders.matchQuery("nickname", "中华人民共和国公民"));

SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(boolQueryBuilder)
.build();

List<User> users = elasticsearchTemplate.queryForList(query, User.class);
System.out.println(users);
}

使用 JPA 访问 ES

A. 创建 UserRepository 接口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.xtuer.repo;

import com.xtuer.bean.User;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.repository.ElasticsearchRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface UserRepository extends ElasticsearchRepository<User, Long> {
List<User> findByNickname(String nickname);
List<User> findByNickname(String nickname, PageRequest pageRequest);
}

方法名 findByNickname 和 findUsersByNickname 都是对的。
更多方法命名请参考 JPA 规范。

B. Spring 配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:elasticsearch="http://www.springframework.org/schema/data/elasticsearch"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/data/elasticsearch
http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch.xsd">
<import resource="classpath:elasticsearch.xml"/>

<!-- 配置 Repository 的包路径,自动生成 Repository 对象 -->
<elasticsearch:repositories base-package="com.xtuer.repo"/>
</beans>

如果不喜欢上面的 XML 配置 Elasticsearch Repository,也可以使用下面的 Java 代码来创建:

1
2
3
4
5
6
7
8
9
package com.xtuer.config;

import org.springframework.context.annotation.Configuration;
import org.springframework.data.elasticsearch.repository.config.EnableElasticsearchRepositories;

@Configuration
@EnableElasticsearchRepositories(basePackages = "com.xtuer.repo")
public class ESConfig {
}

记得配置 <context:component-scan base-package="com.xtuer.config"/>

C. 测试代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
import com.xtuer.bean.User;
import com.xtuer.repo.UserRepository;
import org.elasticsearch.index.query.QueryBuilders;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.elasticsearch.core.query.NativeSearchQueryBuilder;
import org.springframework.data.elasticsearch.core.query.SearchQuery;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@ContextConfiguration({"classpath:bean.xml"})
public class TestWithRepository {
@Autowired
private UserRepository userRepository;

@Test
public void initIndex() {
userRepository.deleteAll();

userRepository.save(new User(1, "Marjory Joyce", "中华人民共和国公民"));
userRepository.save(new User(2, "Lamb Cook", "中华人民"));
userRepository.save(new User(3, "Katharine Warren", "和"));
userRepository.save(new User(4, "Dillon George", "共和国公民"));
userRepository.save(new User(5, "Dillon George", "你和我"));
}

// 注意: 使用 JPA 规范命名的接口查询得到的数据不全 (不知道为啥)
@Test
public void findByNickname() {
// 不使用分页
System.out.println(userRepository.findByNickname("共和国公民"));

// 使用分页
List<User> users = userRepository.findByNickname("共和国公民", PageRequest.of(0, 11));
System.out.println(users);
}

// 注意: 自定义 Query 的方式查询得到的数据是全的,和使用 ElasticsearchTemplate 查询得到的结果一样
@Test
public void searchByNickname() {
SearchQuery query = new NativeSearchQueryBuilder()
.withQuery(QueryBuilders.matchQuery("nickname", "共和国公民"))
.withPageable(PageRequest.of(0, 11))
.build();
Page<User> users = userRepository.search(query);
users.forEach(System.out::println);
}
}

说明:

  • 创建数据时不需要 ElasticsearchTemplate 先调用 putMapping(User.class),Repository 会自动调用
  • 使用 JPA 规范命名的接口 UserRepository.findByNickname() 查询得到的数据不全,但是使用自定义 Query 的方式查询到的数据是全的 (userRepository.search(query))
  • 既能按照 JPA 的命名规范进行简单操作,还能使用自定义 Query 实现复杂查询

版本问题

Spring Data Elasticsearch 和 Elasticsearch 的版本如果选择不正确,运行时就会报错,可以参考下面版本之间的关系,开发时选择对应的版本:

Spring Data Elasticsearch Elasticsearch
3.1.x 6.2.2
3.0.x 5.5.0
2.1.x 2.4.0
2.0.x 2.2.0
1.3.x 1.5.2

Spring Boot and Elasticsearch 之间的版本关系:

Spring Boot Spring Boot Elasticsearch Starter Spring Data Elasticsearch Elasticsearch
2.1.6.RELEASE 6.4.3 3.1.9.RELEASE 6.2.2
2.1.5.RELEASE 6.4.3 3.1.8.RELEASE 6.2.2
2.1.4.RELEASE 6.4.3 3.1.6.RELEASE 6.2.2