博客被黑,天杀的把我置顶的精华文章给干掉了。幸好有人复制了此文,以得部分恢复。现在Spring-data 2.0.1已经release了,等有空升级至spring-data 2.0版本。
Spring-data +elasticsearch 2.4.4 整合搭建指南
最后更新: 17-3-24
1. 简介
spring data是一个统一包括数据库系统和NoSQL数据存储在内不同持久化存储框架,旨在为jpa和nosql的存储框架提供统一接口,减轻开发难度。
Elasticsearch 是一个基于Lucene的搜索引擎框架,为啥是它而不是solr?虽然solr搜索性能但是在单节点读写并发的时候IO性能不如Elasticsearch,所以有整合Elasticsearch的需求。
剩余详细的介绍就不再赘述,自己百度,来点干货,节约篇幅,低碳环保。
2. 准备
2.1 版本条件
首先再次吐个槽(之前写过一篇吐槽了),对于最新的elasticsearch 5.X的版本目前spring-data尚未支持,无法使用最新特性,因此最高只能使用elasticsearch 2.x的版本2.4.4。笔者使用的各版本参考如下:
组件 | 版本 |
---|
spring framework | 4.3.2 |
spring-data-commons | 1.12.2 |
spring-data-elasticsearch | 2.0.6 |
elasticsearch | 2.4.4 |
其它附加的包可以从elasticsearch 官方网站和spring-data网站下载zip获得
https://download.elastic.co/elasticsearch/release/org/elasticsearch/distribution/zip/elasticsearch/2.4.4/elasticsearch-2.4.4.zip
https://github.com/spring-projects/spring-data-elasticsearch/archive/2.0.6.RELEASE.zip
2.2 配置好elasticsearch独立节点服务
要跑起来elasticsearch很简单,上面的下载的目录解压缩后,在解压目录bin找到elasticsearch.bat,头部加上一行你的JDK安装目录:
set JAVA_HOME=c:\jdk1.8
然后双击运行即可。如果你能看到
[INFO ][node ] [Scourge of the Underworld] started
的字样说明已经运行起来了,窗口不要关掉。如果还不行,先解决启动问题(无非就是端口占用或启动了多个之类)
3. 开始整合
3.1. step1 配置客户端
elasticsearch的客户端配置比较简单。首先在头部申明命名空间
<?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:mvc="http://www.springframework.org/schema/mvc"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
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-4.3.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.3.xsd
http://www.springframework.org/schema/mvc
http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd
http://www.springframework.org/schema/data/jpa
http://www.springframework.org/schema/data/jpa/spring-jpa-1.8.xsd
http://www.springframework.org/schema/data/elasticsearch
http://www.springframework.org/schema/data/elasticsearch/spring-elasticsearch-1.0.xsd
"
default-autowire="byName" default-lazy-init="false">
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
然后申明elasticsearch的client接入,elasticsearch的client接入有两种模式。一种是embedded,一种是独立服务的方式也是elasticsearch官方建议的方式:
<elasticsearch:transport-client id="client" cluster-nodes="${app.elasticsearch.address:localhost:9300,localhost:9300}" cluster-name="elasticsearch" />
cluster-nodes数据为你远程的elastic服务的端口,注意不是elasticsearch管理的端口。elasticsearch节点管理端口默认用9200,这里要写9300。可以配置一个或多个节点,用逗号分隔。
cluster-name是你端口运行节点的集群名称。注意要和你节点的elasticsearch.yml里配置的cluster-name一致,默认是elasticsearch。
embedded模式可以用于本地开发和测试。与独立节点的区别就是local为true,不用配置cluster-nodes而改为配置path-home和path-data。path-home需要指向你本地的路径,你可以自己写个Filter抓取ServletContext里的ContextPath来获得绝对路径,具体实现不在本文讨论。
3.2. step2 编写你的实体类
编写你用于存储elasticsearch数据的实体类,先看代码片段:
import org.springframework.data.elasticsearch.annotations.DateFormat;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
import org.springframework.data.elasticsearch.annotations.Setting;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnore;
@Document(indexName = "article_inf_index", type = "articleInf")
@Setting(settingPath = "elasticsearch-analyser.json")
pblic class ArticleInf implements Serializable{
@org.springframework.data.annotation.Id
private Integer articleInfId;
@Field(type = FieldType.String, analyzer="ngram_analyzer")
private String articleTitle;
@Field(type = FieldType.Date, store = true, format = DateFormat.custom, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
@JsonFormat (shape = JsonFormat.Shape.STRING, pattern ="yyyy-MM-dd'T'HH:mm:ss.SSSZZ")
private Date releaseTime;
public Date getReleaseTime() {
return releaseTime;
}
public void setReleaseTime(Date releaseTime) {
this.releaseTime = releaseTime;
}
public String getArticleTitle() {
return articleTitle;
}
public void setArticleTitle(String articleTitle) {
this.articleTitle = articleTitle;
}
}
- 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
- 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
elasticsearch实体的申明主要靠那几个注解。我逐一做个简单介绍:
@Document 标识了该实体用于elasticsearch,indexName和type必须指定。indexName为save时保存到节点的索引名称,type为你的保存的类型(废话,百度翻译的吧-_-b,要真全面理解index和type还真不是
一两句话能解释的,这是elasticsearch的知识,现在为了整合,先来点不求甚解)
@Setting 可以省略,但是如果你要进行单字搜索等处理,需要配置索引的setting,这就需要指定配置的位置。settingPath指定的路径可以用fullPath,如果用相对路径则是你的classpath?(不知道配置在哪,简单说一般是你的log4j.properties相同的目录,没有log4j?我…)
elasticsearch-analyser.json的格式请注意,是settings子对象的结构,下面是一个例子:
{
"index":{
"number_of_shards":1,
"number_of_replicas":0,
"analysis":{
"tokenizer":{
"ngram_tokenizer":{
"type":"nGram",
"min_gram":1,
"max_gram":20
}
},
"analyzer":{
"ngram_analyzer":{
"type":"custom",
"tokenizer":"ngram_tokenizer"
}
}
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
例如上面这个就是配置ngram分词,让你建立数据”CHINA2017”时能通过”NA20”搜索到内容,和数据库like模式完全一致。
@org.springframework.data.annotation.Id 用于标记elasticsearch mapping里的_id。没有ID是保存不了的。
@Field 可以对mappings做一些额外的配置,例如指定数据的分词器设置ngram或ik、paoding等,还可以设置index的store等属性,具体你可以搜索一下elasticsearch的mappings配置文章,在此不再赘述(没办法,知识联系大,百度一下很容易的,限于篇幅,这里就埋个引子)。注意一下如果要实现时间的排序和范围搜索,还要额外指定下@JsonFormat,可以参考例子的代码。
其它类型如果不用额外指定mapping的,就是annotation Field的默认属性:
public @interface Field {
FieldType type() default FieldType.Auto;
FieldIndex index() default FieldIndex.analyzed;
DateFormat format() default DateFormat.none;
String pattern() default "";
boolean store() default false;
String searchAnalyzer() default "";
String analyzer() default "";
String[] ignoreFields() default {};
boolean includeInParent() default false;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
3.3. step3 编写CRUD支持
先写一个接口,让它支持普通的CRUD和分页。
public interface BaseSearchRepository<E, ID extends Serializable> extends ElasticsearchRepository<E, ID>, PagingAndSortingRepository<E, ID>{
}
然后写你自己的repository类:
public interface ArticleInfSearchRepository extends BaseSearchRepository<ArticleInf, Integer>{}
然后还可以写个service注入你要的接口,还有很多方法可以扩展,限于篇幅,你可以看下ElasticsearchRepository和PagingAndSortingRepository接口
public abstract class BaseSearchService <E,ID extends Serializable,R extends BaseSearchRepository<E,ID>>{
private Logger log = Logger.getLogger(this.getClass());
private R repository;
@Autowired
public void setRepository(R repository) {
this.repository = repository;
}
protected R getRepository(){
return repository;
}
public E getById(ID id) {
return getRepository().findOne(id);
}
public Iterable<E> listAll() {
return getRepository().findAll();
}
public void save(E data){
getRepository().save(data);
}
public void delete(E data){
getRepository().delete(data);
}
public void deleteById(ID id){
getRepository().delete(id);
}
public E getByKey(String fieldName, Object value){
try{
return getRepository().search(QueryBuilders.matchQuery(fieldName, value)).iterator().next();
}catch(NoSuchElementException e){
return null;
}
}
}
- 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
- 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
然后你的业务service类:
public class ArticleInfSearchService extends BaseSearchService<ArticleInf, Integer, ArticleInfSearchRepository>{}
然后再啰嗦一下,如果你使用基于注解配置的话,注意配置package-scan,注意配置package-scan,注意配置package-scan,重要的话说3遍。
<elasticsearch:repositories base-package="org.**.search" />
3.4. step4 编写搜索
spring-data默认是基于接口的方式,然后会自动帮你生成需要的类。那么elasticsearch的整合也不例外(体会一下统一接口编写方式的好处)。例如一个简单的搜索:
public interface ArticleInfSearchRepository extends BaseSearchRepository<ArticleInf, Integer>{
public List<ArticleInf> findByArticleTitle(String articleTitle);
}
还可以用@Query指定你的查询语句,query怎样写你得看elasticsearch的语法。
@Query("{"bool" : {"must" : {"field" : {"name" : "?0"}}}}")
Page<Book> findByName(String name,Pageable pageable);
除了按名称和按Query外,再复杂点就要用到QueryBuilder,接口基类有2个方法用到QueryBuilder来动态构建查询:
Iterable<T> search(QueryBuilder query);
Page<T> search(QueryBuilder query, Pageable pageable);
QueryBuilder看官方也没什么特别的,对照词霸就能搞定,这里列举几个从sql刚转过来的常见问题:
- 用match默认无法对英文做部分模糊匹配,要配置ngram,前面提过一次;
- 进行多条件查询先建立个BoolQueryBuilder,然后再boolQueryBuilder.must来添加and条件;
- boolQueryBuilder的or条件叫should,boolQueryBuilder.should(….);
更复杂查询可以参考官方例子:
http://docs.spring.io/spring-data/elasticsearch/docs/2.0.6.RELEASE/reference/html/#repositories.query-methods
3.5. step5 spring事务的整合
写到这里各位码哥码弟们是不觉得还少了点什么不完美?好吧也不卖关子了。上面标题已经写的很清楚。
当你保存数据库失败,或出现业务异常回滚时,你的搜索事务该咋办。这里处理不好,就会出现数据库保存进去,但是搜索却多了一条数据的情况,那就麻烦大了。
有经验的同学会想到事务,可惜:
Elasticsearch doesn’t support transactions!Elasticsearch doesn’t support transactions!Elasticsearch doesn’t support transactions!
重要的话说3遍
好吧,正规的方式实现不了,我们可以采用点变通的方法,将所有的操作搜集起来,然后在事务结束时统一提交,这样如果有异常回滚,所执行的操作也不会提交到数据库。
但这样做有个缺点,如果一个事务内后面的逻辑依赖前面的ES提交的结果,那么解决的方式和数据库操作一样——flush。将积累的操作立即写入到索引,然后等待个1S(ES通常1秒内完全能建立好)就能获取到索引后的数据了。
下面给个自己写的工具类,实现类似的操作,但仅限于以下场景:
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import org.apache.commons.lang.SerializationUtils;
import org.apache.commons.lang3.StringUtils;
import org.xxxxx.client.Global;
import org.xxxxx.commons.base.BaseSearchService;
import org.xxxxx.commons.helper.SpringContextHelper;
@SuppressWarnings("rawtypes")
public class ElasticsearchTransactionUtil {
/**
* 线程操作队列
*/
private static ThreadLocal<List<Operation>> operationListLocal = new ThreadLocal<List<Operation>>();
/**
* 启动事务
*/
public static void init(){
operationListLocal.set(new ArrayList<Operation>());
}
/**
* 将对象操作保存到事务
*/
private static void innerPushOperation(Class jpaServiceClass, Action action, Serializable data, boolean needClone){
String beanName = StringUtils.uncapitalize(jpaServiceClass.getSimpleName().replace(Global.SERVICE_CLASS_SUFFIX, Global.SEARCH_SERVICE_CLASS_SUFFIX));
if(!SpringContextHelper.containsBean(beanName)){
return;
}
Object bean = SpringContextHelper.getBean(beanName);
if(bean instanceof BaseSearchService){
operationListLocal.get().add(new Operation((BaseSearchService)bean, action, needClone? (Serializable)SerializationUtils.clone(data) : data));
}
}
public static void pushSave(Class jpaServiceClass, Serializable data){
innerPushOperation(jpaServiceClass, Action.SAVE, data, true);
}
public static void pushDelete(Class jpaServiceClass, Serializable data){
innerPushOperation(jpaServiceClass, Action.DELETE, data, true);
}
public static void pushDeleteById(Class jpaServiceClass, Serializable id){
innerPushOperation(jpaServiceClass, Action.DELETE_BY_ID, id, false);
}
public static enum Action{
SAVE, DELETE, DELETE_BY_ID
}
/**
* 需要flush时 也调用此方法
*/
public static void commit(){
List<Operation> operationList = operationListLocal.get();
for(Operation operation: operationList){
switch(operation.getAction()){
case SAVE:
operation.getSearchService().save(operation.getData());
break;
case DELETE:
operation.getSearchService().delete(operation.getData());
break;
case DELETE_BY_ID:
operation.getSearchService().deleteById(operation.getData());
break;
default:
break;
}
}
operationList.clear();
}
public static void rollback(){
operationListLocal.get().clear();
}
/**
* 索引操作对象.
* @author JIM
*/
private static class Operation{
public Operation(BaseSearchService searchService, Action action, Serializable data){
this.searchService = searchService;
this.action = action;
this.data = data;
}
private Action action;
public Action getAction() {
return action;
}
public void setAction(Action action) {
this.action = action;
}
private Serializable data;
public Serializable getData() {
return data;
}
public void setData(Serializable data) {
this.data = data;
}
private BaseSearchService searchService;
public BaseSearchService getSearchService() {
return searchService;
}
public void setSearchService(BaseSearchService searchService) {
this.searchService = searchService;
}
}
}
- 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
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 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
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
然后扩展下JpaTransactionManager,在原来的事务动作上添加你自己的动作
public class MyTransactionManager extends JpaTransactionManager {
/**
*
*/
private static final long serialVersionUID = -3878501009638970644L;
@Override
protected void doBegin(Object transaction, TransactionDefinition definition) {
super.doBegin(transaction, definition);
if(!definition.isReadOnly()){
ElasticsearchTransactionUtil.init();
}
}
@Override
protected void doCommit(DefaultTransactionStatus status) {
super.doCommit(status);
if(!status.isReadOnly()){
ElasticsearchTransactionUtil.commit();
}
}
@Override
protected void doRollback(DefaultTransactionStatus status) {
if(!status.isReadOnly()){
ElasticsearchTransactionUtil.rollback();
}
}
}
- 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
- 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
3.6. step6 结束
到此我觉得就结束了,至于怎样注入到service去使用应该是spring架构的学习问题。在此不多扯,做一个纯粹的人,一个有益于码农的人(怎么感觉像哪篇小学课文?)
后面真没有了。
【完】