SpringData3.x以及SpringBoot2整合Elasticsearch5.x
說明
關於如何在SpringBoot 1.x 的版本中整合Elasticsearch 2.x可以參考前文Elasticsearch實踐(二)在Springboot微服務中整合搜尋服務。2017年底,SpringData專案終於更新了Elasticsearch5.x版本的對應release版本:3.0.2.RELEASE。本文結合一個本地的示例,對於ES版本升級進行簡單介紹。目前ES已經出到了6.x版本。但是SpringData專案的更新速度一直比較慢。目前比較適合整合的版本就是3.0.2.RELEASE。同時,對應的Springboot也需要升級到2.x。
因為Elasticsearch,以及其周邊的相關平臺都是強版本依賴的,所以升級的過程也會需要升級其他相關元件。本文主要介紹使用Docker容器來部署Elasticsearch5.x叢集。文中原始碼地址:
Elasticsearch5.x以及2.x版本對比
5.x版本和2.x版本的對比
Elasticsearch的5.x相當於3.x。之所以從2一躍跳到5,Elastic體系內還有logstash、Kibana,beats等產品。為了統一各產品版本,所以直接將Elasticsearch的版本從2提升到5。
5.x版本提供了許多新的特性,並且基於Lucene6.x。下面簡單列舉一些升級的特性:
效能方面
- 磁碟空間可以節省近一半
- 索引時間減少近50%
- 查詢效能提升近30%
支援IPV6
效能的具體資料可以檢視Elasticsearch效能監控。elasricsearch效能的提升,主要是Lucene6版本之後的很多底層結構的優化。Lucene6使用Block K-D trees資料結構來構建索引。BKD Trees是一種可以動態擴充套件的KD-tree結構。詳細的解釋可以參考這篇論文Bkd-tree: A Dynamic Scalable kd-tree。
功能新增
Elasticsearch2.x的版本,在建立索引時指定了shard數,並且不支援修改。如果要改變shard數,只能重建索引。5.x新增的Shrink介面,可將分片數進行收縮成它的因數,如果原有的shard數=15,可以收縮成5個或者3個又或者1個。
Rollover API對於日誌型別的索引提供了友好的建立和管理。比如通過
POST /my_alias/_rollover/my_new_index_name
{
"conditions": {
"max_age": "7d",
"max_docs": 1000,
"max_size": "5gb"
}
}
可以給索引設定rollover規則:索引文件不超過1000個、最多儲存7天的資料、每個索引檔案不超過5G,超過限制會自動建立新的索引檔案別名,如logs-2018.01.25-000002。
2.x版本的ES的索引重建一直是很麻煩的事情。5.x提供的Reindex可以直接在搜尋叢集中對資料進行重建。如下可以直接修改mapping。
curl -XPOST 'localhost:9200/_reindex?pretty' -H 'Content-Type: application/json' -d'
{
"source": {
"index": "twitter"
},
"dest": {
"index": "new_twitter"
}
}
'
其他特性
- Mapping變更中,String的型別的對映,被替換為兩種: text/keyword。
text:類似於全文形式,包括被分析。
keyword:屬於字串型別的精確搜尋。 - 叢集節點的配置方面,在節點啟動時就會校驗配置,如Max File Descriptors,Memory Lock, Virtual Memory等的驗證,會啟動丟擲異常,降低後期穩定性的風險。同時更新配置時更加嚴格和保證原子性,如果其中一項失敗,那個整個都會更新請求都會失敗。
- 外掛方面,Delete-by-query和Update-by-query重新增加回core。2.x時被移除,以至於需要手動安裝外掛,5.x的外掛構建在Reindex機制之上,已經可以直接使用了。
使用Docker部署Elasticsearch5.x
使用Elastic官方映象
Elasticsearch官方的映象基於Centos,並且內建了X-Pack。安裝過程可以參考官方教程-5.6. 官方5.x版本的Docker映象內建了X-pack。選擇正確的版本即可。
自定義Dockerfile安裝ik分詞器外掛
Docker官方的ES映象Dockerhub-ES也是基於Elastic官方的基礎映象。對於IK 外掛版本需要嚴格對應Elasticsearch的版本。
分詞器等外掛的安裝,可以直接基於官方的映象,在Dockerfile中重新build自己的映象。如下示例5.5.0版本Elasticsearch的Dockerfile:
FROM elasticsearch:5.5.0
RUN sed -i 's/deb.debian.org/mirrors.ustc.edu.cn/g' /etc/apt/sources.list
RUN apt-get update && apt-get install zip
RUN mkdir -p /usr/share/elasticsearch/plugins/ik
RUN cd /usr/share/elasticsearch/plugins/ik && wget https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v5.5.0/elasticse arch-analysis-ik-5.5.0.zip && unzip elasticsearch-analysis-ik-5.5.0.zip
編寫好Dockerfile之後,再執行docker build即可。
SpringDataElasticsearch以及SpringBoot整合
SpringDataElasticsearch整合
SpringDataElasticsearch版本選擇
Elasticsearch是強版本依賴的。相信所有在折騰過Elasticsearch的DevOps都被各種外掛版本、關聯Logstash,Kibana等的版本、整合SpringData相關版本弄得暈頭轉向。目前SpringDataElasticsearch更新了新的支援ES5.x的版本。本文示例Elasticsearch升級到版本5.5對應SpringDataElasticsearch版本3.0.2.RELEASE。使用Gradle構建的專案新增:
compile 'org.springframework.data:spring-data-elasticsearch:3.0.2.RELEASE'
升級注意
因為ES 5.x中的Mapping改變,所以原有的@Field(type = FieldType.string)型別的索引對映需要替換成@Field(type = FieldType.keyword)或者@Field(type = FieldType.text)。
對於index原來的analyzed/not_analyzed/no也有相應的改變。keyword,text對於index只接受true/false值。分別代替not_analyzed/no。SpringDataEs中,index預設=true。
所以原有的索引欄位,需要根據索引特徵進行修改,否則會編譯錯誤。如果要精確搜尋就用keyword,否則用text。如果不需要通過該欄位進行查詢,則index設定false即可。
引入log4j三方包
服務中需要顯示log4j-core, log4j-api包。否則會啟動異常。
ext.log4jCore = "org.apache.logging.log4j:log4j-core:2.10.0"
ext.log4jApi = "org.apache.logging.log4j:log4j-api:2.10.0"
ClassNotFound-SimpleElasticsearchMappingContext
需要顯示引入spring-data-commons。SimpleElasticsearchMappingContext 依賴的org.springframework.data.mapping.model.Property需要spring-data-commons的2.x版本
ext.springDataCommon = "org.springframework.data:spring-data-commons:2.0.2.RELEASE"
分詞器相關報錯
如果在2.x升級到5.x的過程。使用分詞器的document的索引會引起異常:
failed to load elasticsearch nodes : org.elasticsearch.index.mapper.MapperParsingException: analyzer [ik] not found for field
解決方式:先關掉相關的索引,然後修改對應的settings的analyzer,最後再開啟索引。示例程式碼如下:
curl -XPOST '127.0.0.1:9200/items/_close?pretty'
curl -XPUT '127.0.0.1:9200/items/_settings' -d '{
"analysis": {
"analyzer": {
"ik": {
"type": "custom",
"tokenizer": "ik_smart"
}
}
}
}'
curl -XPOST '127.0.0.1:9200/items/_open?pretty'
Springboot2及相關外掛升級
Gradle升級到4.4.1
使用Gradle構建工程的專案,如果是4.2以下版本的也需要升級。因為Springboot2.x的plugin需要gradle 4.2以上的版本。否則啟動時會報錯。
如果idea在build工程時還是報錯”Could not get unknown property ‘projectConfiguration’ for DefaultProjectDependency”,可以更新最新版的idea。支援更高級別的gradle。
目前的idea版本資訊:
IntelliJ IDEA 2017.3.3 (Community Edition)
Build #IC-173.4301.25, built on January 16, 2018
JRE: 1.8.0_152-release-1024-b11 x86_64
SpringBoot2.x
SpringBoot1.5.x的版本不支援ElasticSearch 5.x。所以需要升級專案到Spring Boot 2。目前Spring Boot 2只有milestone版本。本文示例選擇了2.0.0.M2。
相應的 SpringBootGradle 外掛也需升級到2.0.0.M2,版本示例見:spring-boot-s-new-gradle-plugin
SpringCore升級到5.0.x
相應的,原有工程的Spring版本也需要升級到5.x版本。本示例升級到了5.0.2.RELEASE。
SpringCloud升級
如果專案中使用了Spring cloud。也需要隨著Springboot升級到符合的版本如Eureka,Feign,Ribbon 可以對應到: 2.0.0.M2。
示例程式碼
示例中以一個簡單的商品的資訊的搜尋為例。使用Springboot2,Spring5,Gradle4.4.1。 Elasticsearhc使用5.5.0的映象部署。
先看下gradle的build檔案:
apply plugin: 'java'
apply plugin: 'idea'
apply plugin: 'org.springframework.boot'
apply plugin: 'io.spring.dependency-management'
buildscript {
ext {
springBootVersion = '2.0.0.M2'
}
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
dependencies {
classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
}
}
sourceCompatibility = 1.8
targetCompatibility = 1.8
repositories {
mavenCentral()
maven { url "https://repo.spring.io/snapshot" }
maven { url "https://repo.spring.io/milestone" }
}
idea {
project {
jdkName = '1.8'
languageLevel = '1.8'
vcs = 'Git'
}
module {
downloadJavadoc = false
downloadSources = true
}
}
dependencies {
ext.springVersion = "5.0.2.RELEASE"
ext.slf4jVersion = "1.7.21"
compile "org.springframework:spring-beans:${springVersion}"
compile "org.springframework:spring-core:${springVersion}"
compile "org.springframework:spring-context:${springVersion}"
compile "org.springframework:spring-expression:${springVersion}"
compile "org.springframework:spring-web:${springVersion}"
compile "org.springframework:spring-webmvc:${springVersion}"
compile "org.springframework:spring-test:${springVersion}"
compile "org.springframework:spring-orm:${springVersion}"
compile "org.springframework.boot:spring-boot-autoconfigure:${springBootVersion}"
compile "org.springframework.boot:spring-boot:${springBootVersion}"
compile "org.springframework.boot:spring-boot-starter:${springBootVersion}"
compile "org.springframework.boot:spring-boot-starter-aop:${springBootVersion}"
compile "org.springframework.boot:spring-boot-starter-tomcat:${springBootVersion}"
compile "org.apache.commons:commons-lang3:3.4"
compile "commons-io:commons-io:2.5"
compile "commons-logging:commons-logging:1.2"
compile "commons-codec:commons-codec:1.10"
compile "javax.servlet:javax.servlet-api:3.1.0"
compile "org.slf4j:slf4j-api:${slf4jVersion}"
compile "org.apache.logging.log4j:log4j-core:2.10.0"
compile "org.apache.logging.log4j:log4j-api:2.10.0"
compile 'org.springframework.data:spring-data-commons:2.0.2.RELEASE'
compile 'org.springframework.data:spring-data-elasticsearch:3.0.2.RELEASE'
compile 'org.springframework.boot:spring-boot-starter-web'
compile 'org.springframework.boot:spring-boot-starter-logging'
compile 'org.springframework.boot:spring-boot-starter-data-elasticsearch'
compile 'org.elasticsearch:elasticsearch:5.4.1'
compile 'org.elasticsearch.client:transport:5.4.1'
}
示例Document、Controller
Document entity如下 :
/**
* 商品Document -example
* <p>
* Created by lijingyao on 2018/1/18 18:03.
*/
@Document(indexName = ItemDocument.INDEX, type = ItemDocument.TYPE)
public class ItemDocument {
public static final String INDEX = "items";
public static final String TYPE = "item";
public ItemDocument() {
}
public ItemDocument(String id, Integer catId, String name, Long price, String description) {
this.id = id;
this.catId = catId;
this.name = name;
this.price = price;
this.description = description;
}
/**
* 商品唯一標識
*/
@Id
@Field(type = FieldType.keyword)
private String id;
/**
* 類目id
*/
@Field(type = FieldType.Integer)
private Integer catId;
/**
* 商品名稱
*/
@Field(type = FieldType.text,index = false)
private String name;
/**
* 商品價格
*/
@Field(type = FieldType.Long)
private Long price;
/**
* 商品的描述
*/
@Field(type = FieldType.text, searchAnalyzer = "ik", analyzer = "ik")
private String description;
... getset ...
@Override
public String toString() {
return "ItemDocument{" +
"id='" + id + '\'' +
", catId=" + catId +
", name='" + name + '\'' +
", description='" + description + '\'' +
", price=" + price +
'}';
}
}
Repository介面程式碼:
public interface ItemDocumentRepository extends ElasticsearchRepository<ItemDocument, String> {
}
Controller程式碼:
@RestController
@RequestMapping("/items")
public class SearchController {
@Autowired
private ItemDocumentRepository repository;
@RequestMapping(value = "/{id}",method = {RequestMethod.GET})
public ResponseEntity getItem(@PathVariable("id") String id) {
ItemDocument com = repository.findById(id).get();
return new ResponseEntity(com.toString(), HttpStatus.OK);
}
@RequestMapping(method = {RequestMethod.POST})
public ResponseEntity createItem(@RequestBody ItemDocument document) {
repository.save(document);
return new ResponseEntity(document.toString(), HttpStatus.OK);
}
}
Elasticsearch配置
@Configuration
@EnableElasticsearchRepositories(basePackages = "com.lijingyao.es")
public class SearchConfig {
private static final Logger logger = LoggerFactory.getLogger(SearchConfig.class);
@Value("${elasticsearch.port}")
private int esPort;
@Value("${elasticsearch.clustername}")
private String esClusterName;
@Value("#{'${elasticsearch.hosts:localhost}'.split(',')}")
private List<String> hosts = new ArrayList<>();
private Settings settings() {
Settings settings = Settings.builder()
.put("cluster.name", esClusterName)
.put("client.transport.sniff", true).build();
return settings;
}
@Bean
protected Client buildClient() {
TransportClient preBuiltTransportClient = new PreBuiltTransportClient(settings());
if (!CollectionUtils.isEmpty(hosts)) {
hosts.stream().forEach(h -> {
try {
preBuiltTransportClient.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName(h), esPort));
} catch (UnknownHostException e) {
logger.error("Error addTransportAddress,with host:{}.", h);
}
});
}
return preBuiltTransportClient;
}
@Bean
public ElasticsearchTemplate elasticsearchTemplate() {
Client client = buildClient();
return new ElasticsearchTemplate(client);
}
}
測試請求
- 新增商品資訊:
POST http://localhost:8088/items
body:
{
"id":"123",
"catId":1,
"description":"商品,質量好,包郵,售後服務保障",
"name":"商品123",
"price":1000
}
- 獲取商品資訊:
GET http://localhost:8088/items/123
- 測試delete by query
使用search API 通過id 刪除。
curl -XPOST 'http://127.0.0.1:9200/items/item/_delete_by_query?q=id:123&pretty'
示例程式碼Git
如果有其他問題歡迎隨時交流。