1. 程式人生 > >SpringData3.x以及SpringBoot2整合Elasticsearch5.x

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叢集。文中原始碼地址:

https://github.com/lijingyao/elasticsearch5-example

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

如果有其他問題歡迎隨時交流。

相關資料