最后更新: 17-3-24
spring data是一个统一包括数据库系统和NoSQL数据存储在内不同持久化存储框架,旨在为jpa和nosql的存储框架提供统一接口,减轻开发难度。
Elasticsearch 是一个基于Lucene的搜索引擎框架,为啥是它而不是solr?虽然solr搜索性能但是在单节点读写并发的时候IO性能不如Elasticsearch,所以有整合Elasticsearch的需求。
剩余详细的介绍就不再赘述,自己百度,来点干货,节约篇幅,低碳环保。
首先再次吐个槽(之前写过一篇吐槽了),对于最新的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
要跑起来elasticsearch很简单,上面的下载的目录解压缩后,在解压目录bin找到elasticsearch.bat,头部加上一行你的JDK安装目录:
set JAVA_HOME=c:\jdk1.8
然后双击运行即可。如果你能看到
[INFO ][node ] [Scourge of the Underworld] started
的字样说明已经运行起来了,窗口不要关掉。如果还不行,先解决启动问题(无非就是端口占用或启动了多个之类)
elasticsearch的客户端配置比较简单。首先在头部申明命名空间
<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">
然后申明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来获得绝对路径,具体实现不在本文讨论。
编写你用于存储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;
//elasticsearch
@Document(indexName = "article_inf_index", type = "articleInf")
@Setting(settingPath = "elasticsearch-analyser.json")
pblic class ArticleInf implements Serializable{
//elasticsearch
@org.springframework.data.annotation.Id
private Integer articleInfId;
@Field(type = FieldType.String, analyzer="ngram_analyzer")//使用ngram进行单字分词
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;
}
}
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"
}
}
}
}
}
例如上面这个就是配置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;
}
先写一个接口,让它支持普通的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>>{ //spring 4.X 支持泛型注入
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 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;
}
}
}
然后你的业务service类:
public class ArticleInfSearchService extends BaseSearchService<ArticleInf, Integer, ArticleInfSearchRepository>{}
然后再啰嗦一下,如果你使用基于注解配置的话,注意配置package-scan,注意配置package-scan,注意配置package-scan,重要的话说3遍。
<elasticsearch:repositories base-package="org.**.search" />
spring-data默认是基于接口的方式,然后会自动帮你生成需要的类。那么elasticsearch的整合也不例外(体会一下统一接口编写方式的好处)。例如一个简单的搜索:
public interface ArticleInfSearchRepository extends BaseSearchRepository<ArticleInf, Integer>{
public List findByArticleTitle(String articleTitle);
}
还可以用@Query指定你的查询语句,query怎样写你得看elasticsearch的语法。
@Query("{"bool" : {"must" : {"field" : {"name" : "?0"}}}}")
Page findByName(String name,Pageable pageable);
除了按名称和按Query外,再复杂点就要用到QueryBuilder,接口基类有2个方法用到QueryBuilder来动态构建查询:
Iterable search(QueryBuilder query);
Page search(QueryBuilder query, Pageable pageable);
QueryBuilder看官方也没什么特别的,对照词霸就能搞定,这里列举几个从sql刚转过来的常见问题:
更复杂查询可以参考官方例子:
http://docs.spring.io/spring-data/elasticsearch/docs/2.0.6.RELEASE/reference/html/#repositories.query-methods
写到这里各位码哥码弟们是不觉得还少了点什么不完美?好吧也不卖关子了。上面标题已经写的很清楚。
当你保存数据库失败,或出现业务异常回滚时,你的搜索事务该咋办。这里处理不好,就会出现数据库保存进去,但是搜索却多了一条数据的情况,那就麻烦大了。
有经验的同学会想到事务,可惜:
Elasticsearch doesn’t support transactions!Elasticsearch doesn’t support transactions!Elasticsearch doesn’t support transactions!
重要的话说3遍
好吧,正规的方式实现不了,我们可以采用点变通的方法,将所有的操作搜集起来,然后在事务结束时统一提交,这样如果有异常回滚,所执行的操作也不会提交到数据库。
但这样做有个缺点,如果一个事务内后面的逻辑依赖前面的ES提交的结果,那么解决的方式和数据库操作一样——flush。将积累的操作立即写入到索引,然后等待个1S(ES通常1秒内完全能建立好)就能获取到索引后的数据了。
下面给个自己写的工具类,实现类似的操作,但仅限于以下场景:
抽离BaseSearchService来处理ES的基本操作和抽离BaseService来处理JPA基础操作
没提供更多的代码,你就当伪代码来读吧��
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> operationListLocal = new ThreadLocal>();
/**
* 启动事务
*/
public static void init(){
operationListLocal.set(new ArrayList());
}
/**
* 将对象操作保存到事务
*/
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)); //根据名称匹配找到xxxSearchService的springBean
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 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;
}
}
}
然后扩展下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();
}
}
}
到此我觉得就结束了,至于怎样注入到service去使用应该是spring架构的学习问题。在此不多扯,做一个纯粹的人,一个有益于码农的人(怎么感觉像哪篇小学课文?)
后面真没有了。