ElasticSearch (下面简称 ES) 是一个基于 Lucene 的全文检索服务器,本文简单的介绍 ES 的安装、配置、启动、一些基本概念、中文分词以及使用 Java 编程访问 ES 等。
安装配置启动
安装: 目前 spring-data-elasticsearch 最高支持 elasticsearch-6.2.2 (可参考最后的版本对应进行选择),所以下载 https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.2.2.zip ,解压即可
配置: 修改配置文件 config/elasticsearch.yml
(只介绍单机环境的,中小型应用足够了):
1 2 3 4 cluster.name: ebag node.name: node-1 network.host: 0.0 .0 .0 http.port: 9200
启动: elasticsearch -d
,注意: Linux 下不允许使用 root 用户启动,可以创建一个用户如 elasticsearch,然后使用此用户启动 ES:
useradd elasticsearch
passwd elasticsearch
su elasticsearch
elasticsearch -d
浏览器中访问 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 :
下载 (Mac 选择 elasticHD_darwin_i386.zip)
解压
启动: ./ElasticHD -p 127.0.0.1:9800
访问: 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 () { elasticsearchTemplate.deleteIndex(User.class); 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()); } @Test public void findUsersByUsername () { SearchQuery query = new NativeSearchQueryBuilder().withQuery(QueryBuilders.matchQuery("username" , "Cook" )).build(); List<User> users = elasticsearchTemplate.queryForList(query, User.class); System.out.println(users); } @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 个地方:
使用注解 @Field
给属性 nickname 设定类型为 Text 和分词器 ik_smart (根据业务需求,也可以用 ik_max_word ):
1 2 @Field(type = FieldType.Text, searchAnalyzer = "ik_smart", analyzer = "ik_smart") private String nickname;
在插入数据前给 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 () { elasticsearchTemplate.deleteIndex(User.class); if (!elasticsearchTemplate.indexExists(User.class)) { elasticsearchTemplate.createIndex(User.class); } elasticsearchTemplate.putMapping(User.class); elasticsearchTemplate.index(new IndexQueryBuilder().withObject(new User(1 , "Marjory Joyce" , "中华人民共和国公民" )).build()); }
调用 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 { ... @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" /> <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" , "你和我" )); } @Test public void findByNickname () { System.out.println(userRepository.findByNickname("共和国公民" )); List<User> users = userRepository.findByNickname("共和国公民" , PageRequest.of(0 , 11 )); System.out.println(users); } @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