Elastic Search Java Api

楊高超發表於2018-01-16

前言

前文我們提到過Elastic Search 操作索引的 Rest Api。實際上 Elastic Search 的 Rest Api 提供了所有的操作介面。在程式語言中可以直接這麼使用 Rest Api 可以呼叫 Elastic Search 的所有功能,但是非常的不方便和直觀,所以Elastic Search 官方也為很多語言提供了訪問的 Api 介面。官方提供的程式語言介面包括:

  • Java
  • JavaScript
  • Groovy
  • PHP
  • .NET
  • Perl
  • Python
  • Ruby

同時程式設計社群也提供了大量的程式語言的 Api。目前主要有

  • B4J
  • Clojure
  • ColdFusion (CFML)
  • Erlang
  • Go
  • Groovy
  • Haskell
  • Java
  • JavaScript
  • kotlin
  • Lua
  • .NET
  • OCaml
  • Perl
  • PHP
  • Python
  • R
  • Ruby
  • Rust
  • Scala
  • Smalltalk
  • Vert.x

平時我們都是用 Java 進行開發。所以這裡我會談談 Elastic Search 的 Java Api 的使用方式

準備工作

為了說明 Java Api 的功能,我們準備了一個場景。在這裡我們假定有一批作者,每個作者都有標識、姓名、性別、年齡,描述著幾個欄位。我們需要通過姓名、年齡、描述中的關鍵詞來查詢作者,

在這裡,程式主要通過 JUnit 測試用例的方式來執行,所以首先引入了 JUnit 的依賴

<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
複製程式碼

Java Api 概述

Elastic Search 提供了官方的 Java Api。這裡包括兩類,一類是 Low Level Rest Api(低階 Rest Api)和 High Leve Rest Api(高階 Rest Api)。

所謂低階 Api 並不是功能比較弱,而是指 Api 離底層實現比較近。官方提供的低階 Api 是對原始的 Rest Api 的第一層封裝。只是把 Http 呼叫的細節封裝起來。程式還是要自己組裝查詢的條件字串、解析返回的結果 json 字串等。同時也要處理 http 協議的 各種方法、協議頭等內容。

高階 api 是在低階 api 上的進一步封裝,不用在在意介面的方法,協議頭,也不用人工組合呼叫的引數字串,同時對返回的 json 字串有一定的解析。使用上更方便一些。但是高階 api 並沒有實現所有低階 api 實現的功能。所以如果遇到這種情況,還需要利用低階 api 來實現自己功能。

第三方 Java 客戶端是有社群自己開發的 Elastic Search 客戶端。官方提到了兩個開源在 GitHub 上的專案 FlummiJest

Java Low Level Rest Api 使用說明

低階 Api 的優勢在於依賴的其他庫非常少,而且功能完備。缺點在於封裝不夠高階,所以使用起來還是非常的繁瑣。我們這裡先來看看低階的 api 是怎麼使用的。

引入依賴

在前面建立的 Maven Java 工程中,要使用 Elastic Search 的低階 Api,首先要引入 低階 Api 的依賴。如下所示

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-client</artifactId>
    <version>6.1.1</version>
</dependency>
複製程式碼

建立客戶端

RestClient restClient = RestClient.builder(
        new HttpHost("localhost", 9200, "http"),
        new HttpHost("localhost", 9201, "http")).build();
複製程式碼

我們通過 RestClient 物件的靜態方法 builder(HttpHost... hosts) 和 builder()建立一個 Elastic Search 的 Rest 客戶端。其中 hosts 是一個可變引數,用來指定 Elastic Cluster 叢集的節點的 ip、埠、協議。

方法呼叫

建立了客戶端以後,通過兩類方法來呼叫 Rest Api。一類是同步呼叫,一類是非同步呼叫。

同步呼叫

同步呼叫主要的方法宣告如下所示:

public Response performRequest(String method, String endpoint, Header... headers) throws IOException

public Response performRequest(String method, String endpoint, Map<String, String> params, Header... headers) throws IOException

public Response performRequest(String method, String endpoint, Map<String, String> params,
                                   HttpEntity entity, Header... headers) throws IOException

複製程式碼

這是三個過載的方法,引數 method 代表的是 Rest Api 的方法,例如 PUT、GET、POST、DELETE等;引數 endpoint 代表的是 Rest Api 引數的地址,從 Rest Api 的 URL 的 ip:port 欄位之後開始;params 是通過 url 引數形式傳遞的引數;entity 是通過 http body 傳遞的引數;headers 是一個可變引數,可以傳入對應的 http 頭資訊。

例如,我要檢視一個索引 author_test 的資訊,我們可以用如下的程式碼來獲取

Response response = restClient.performRequest("GET", "/author_test");
複製程式碼

再比如,我們要檢視一個索引 author_test 中 des 欄位中包含軟體的文件資訊,我們可以用如下程式碼來獲取:

String queryJson = "{\n" +
        "    \"query\": {\n" +
        "        \"match\": {\n" +
        "            \"des\": \"軟體\"\n" +
        "        }\n" +
        "    }\n" +
        "}";
Response response = restClient.performRequest("POST",
        "/author_test/_search",
        new HashMap<String, String>(),
        new NStringEntity(queryJson,ContentType.APPLICATION_JSON));
複製程式碼

非同步呼叫

非同步呼叫和同步呼叫的引數是一樣的,但是非同步呼叫沒有返回值,而是在引數中有一個 ResponseListener 回撥物件,在呼叫完成後自動呼叫。這個回撥物件是一個介面,需要程式設計師自己來實現。

非同步呼叫的方法宣告如下所示:

public void performRequestAsync(String method, String endpoint, ResponseListener responseListener, Header... headers)

public void performRequestAsync(String method, String endpoint, Map<String, String> params,
                                    ResponseListener responseListener, Header... headers)

public void performRequestAsync(String method, String endpoint, Map<String, String> params,
                                    HttpEntity entity, ResponseListener responseListener, Header... headers) 
複製程式碼

例如,我要用非同步呼叫的方式查詢 author_test 索引中 des 中包含 “軟體” 的所有文件,則程式碼實現如下

String queryJson = "{\n" +
        "    \"query\": {\n" +
        "        \"match\": {\n" +
        "            \"des\": \"軟體\"\n" +
        "        }\n" +
        "    }\n" +
        "}";


restClient.performRequestAsync("POST",
        "/author_test/_search",
        new HashMap<String, String>(),
        new NStringEntity(queryJson, ContentType.APPLICATION_JSON), new ResponseListener() {
            public void onSuccess(Response response) {
                try {
                    String responseData = readResposne(response);
                    System.out.println("******* search success ******");
                    System.out.println(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            public void onFailure(Exception exception) {
                exception.printStackTrace();
            }
        });
複製程式碼

Java High Level Rest Api 使用說明

Elastic Search 的 Java 高階 Api 相對低階 Api 來說,抽象程度更高一些。不過我個人覺得還是挺難用的。而且高階 Api 並不支援所有的 Rest Api 的功能。官方有高階 Api 支援的功能列表。從這裡看,如果你只是做查詢,用高階 Api 介面還是夠用的。

引入依賴

在前面建立的 Maven Java 工程中,要使用 Elastic Search 的低階 Api,首先要引入 低階 Api 的依賴。如下所示

<dependency>
    <groupId>org.elasticsearch.client</groupId>
    <artifactId>elasticsearch-rest-high-level-client</artifactId>
    <version>6.1.1</version>
</dependency>
複製程式碼

建立客戶端

RestHighLevelClient client = new RestHighLevelClient(
        RestClient.builder(
                new HttpHost("localhost", 9200, "http"),
                new HttpHost("localhost", 9201, "http")));
複製程式碼

和低階介面類似,先通過 RestClient 物件的靜態方法 builder(HttpHost... hosts)方法建立一個 RestClientBuilder 物件,然後作為 RestHighLevelClient 物件建構函式的引數,來建立一個新的高階客戶端物件。其中 hosts 是一個可變引數,用來指定 Elastic Cluster 叢集的節點的 ip、埠、協議。

方法呼叫

這裡用高階介面來實現低階介面中第一個查詢的功能。程式碼如下

SearchRequest searchRequest = new SearchRequest("author_test");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("des", "軟體"));
sourceBuilder.from(0);
sourceBuilder.size(5);
searchRequest.source(sourceBuilder);
SearchResponse response = restClient.search(searchRequest);
複製程式碼

其他的介面的呼叫都可以查詢對應的 api 文件說明來完成

完整程式碼

最後一個章節將完整的程式碼貼出來。

初始化程式碼

這部分程式碼負責初始化測試的索引和索引文件。需要注意一下,前面我們說過 Elastic Search 是一個準實時的系統,所以索引完文件後,如果馬上查詢,可能查詢不到資料,需要有一個小的延遲。

package com.x9710.es.test;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHost;
import org.apache.http.entity.ContentType;
import org.apache.http.nio.entity.NStringEntity;
import org.elasticsearch.client.RestClient;
import org.elasticsearch.client.RestHighLevelClient;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;

public class IndexInitUtil {
    public RestClient initLowLevelClient() {
        // 通過 ip 、port 和協議建立 Elastic Search 客戶端
        RestClient restClient = RestClient.builder(
                new HttpHost("10.110.2.53", 9200, "http")).build();


        try {
            initIndex(restClient);

        } catch (Exception e) {
            e.printStackTrace();
        }
        return restClient;
    }

    public RestHighLevelClient initHighLevelClient() {
        // 通過 ip 、port 和協議建立 Elastic Search 客戶端
        RestHighLevelClient highLevelClient = new RestHighLevelClient(
                RestClient.builder(
                        new HttpHost("10.110.2.53", 9200, "http"))
        );

        RestClient restClient = RestClient.builder(
                new HttpHost("10.110.2.53", 9200, "http")).build();

        try {
            initIndex(restClient);
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                restClient.close();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }


        return highLevelClient;
    }

    private void initIndex(RestClient restClient) {

        String authIndexDefine = "{\n" +
                "\t\"settings\" : {\n" +
                "        \"index\" : {\n" +
                "            \"number_of_shards\" : 6,\n" +
                "            \"number_of_replicas\" : 0\n" +
                "        }\n" +
                "    },\n" +
                "    \"mappings\": {\n" +
                "        \"doc\": {\n" +
                "            \"properties\": {\n" +
                "            \t\"id\": {\"type\": \"text\"},\n" +
                "                \"name\": {\"type\": \"text\"},\n" +
                "                \"sex\": {\"type\": \"text\"},\n" +
                "                \"age\": {\"type\": \"integer\"},\n" +
                "                \"des\":{\n" +
                "                \t\"type\":\"text\",\n" +
                "                \t\"analyzer\": \"ik_max_word\",\n" +
                "\t\t\t\t\t\"search_analyzer\": \"ik_max_word\"\n" +
                "                }\n" +
                "            }\n" +
                "        }\n" +
                "    }\n" +
                "}";

        HttpEntity authorIndexEntity = new NStringEntity(authIndexDefine, ContentType.APPLICATION_JSON);
        //初始化要索引的 author 文件列表
        List<HttpEntity> authorDocs = new ArrayList<HttpEntity>();
        authorDocs.add(new NStringEntity(" {\n" +
                "\t\"id\":\"A1001\",\n" +
                "\t\"name\":\"任盈盈\",\n" +
                "\t\"age\":24,\n" +
                "\t\"sex\":\"女\",\n" +
                "\t\"des\":\"IT軟體工程師,擅長Java和軟體架構\"\n" +
                " }", ContentType.APPLICATION_JSON));
        authorDocs.add(new NStringEntity(" {\n" +
                "\t\"id\":\"A1002\",\n" +
                "\t\"name\":\"風清揚\",\n" +
                "\t\"age\":47,\n" +
                "\t\"sex\":\"男\",\n" +
                "\t\"des\":\"IT軟體技術經理,擅長技術管理過程控制\"\n" +
                " }", ContentType.APPLICATION_JSON));


        try {
            //建立 author_test 索引
            restClient.performRequest("PUT", "/author_test", new HashMap<String, String>(), authorIndexEntity);

            //索引 author_index 文件
            for (int i = 0; i < authorDocs.size(); i++) {
                restClient.performRequest("POST", "/author_test/doc", new HashMap<String, String>(), authorDocs.get(i));
            }
            //注意索引文件完成後,做一個小的延遲,保證後續查詢能查到資料
            Thread.currentThread().sleep(1000);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

複製程式碼

低階 Api 測試樣例

package com.x9710.es.test;

import org.apache.http.entity.ContentType;
import org.apache.http.nio.entity.NStringEntity;
import org.codehaus.jettison.json.JSONObject;
import org.elasticsearch.client.Response;
import org.elasticsearch.client.ResponseListener;
import org.elasticsearch.client.RestClient;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.util.HashMap;

/**
 * Elastic Search 低階 Api 測試類
 *
 * @author 楊高超
 * @since 2018-01-11
 */
public class LowLeveApiTest {
    RestClient restClient = null;

    @Before
    public void before() {
        restClient = new IndexInitUtil().initLowLevelClient();
    }

    @Test
    public void testLocateAuthorIndex() {
        try {
            Response response = restClient.performRequest("GET", "/author_test");
            String responseData = readResposne(response);
            Assert.assertTrue(new JSONObject(responseData).has("author_test"));
            System.out.println(responseData);
        } catch (Exception e) {
            e.printStackTrace();
            Assert.assertTrue(false);
        }
    }


    @Test
    public void testQueryAuthDoc() {
        try {
            String queryJson = "{\n" +
                    "    \"query\": {\n" +
                    "        \"match\": {\n" +
                    "            \"des\": \"Java\"\n" +
                    "        }\n" +
                    "    }\n" +
                    "}";
            Response response = restClient.performRequest("POST",
                    "/author_test/_search",
                    new HashMap<String, String>(),
                    new NStringEntity(queryJson, ContentType.APPLICATION_JSON));

            String responseData = readResposne(response);
            JSONObject responseJson = new JSONObject(responseData);
            Assert.assertTrue(responseJson.has("hits")
                    && responseJson.getJSONObject("hits").getInt("total") == 1);
            System.out.println(responseData);
        } catch (Exception e) {
            e.printStackTrace();
            Assert.assertTrue(false);
        }
    }

    @Test
    public void testQueryAuthDocAsy() {
        try {
String queryJson = "{\n" +
        "    \"query\": {\n" +
        "        \"match\": {\n" +
        "            \"des\": \"軟體\"\n" +
        "        }\n" +
        "    }\n" +
        "}";


restClient.performRequestAsync("POST",
        "/author_test/_search",
        new HashMap<String, String>(),
        new NStringEntity(queryJson, ContentType.APPLICATION_JSON), new ResponseListener() {
            public void onSuccess(Response response) {
                try {
                    String responseData = readResposne(response);
                    System.out.println("******* search success ******");
                    System.out.println(responseData);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }

            public void onFailure(Exception exception) {
                exception.printStackTrace();
            }
        });
        } catch (Exception e) {
            e.printStackTrace();
            Assert.assertTrue(false);
        }
    }


    @After
    public void after() {
        try {
            if (restClient != null) {
                restClient.performRequest("DELETE", "/author_test");
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (restClient != null) {
                try {
                    restClient.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }

    private String readResposne(Response response) throws Exception {
        BufferedReader brd = new BufferedReader(new BufferedReader(new InputStreamReader(response.getEntity().getContent())));
        String line;
        StringBuilder respongseContext = new StringBuilder();

        while ((line = brd.readLine()) != null) {
            respongseContext.append(line).append("\n");
        }
        //rd.close();
        if (respongseContext.length() > 0) {
            respongseContext.deleteCharAt(respongseContext.length() - 1);
        }
        return respongseContext.toString();
    }
}

複製程式碼

高階 Api 測試樣例

package com.x9710.es.test;

import org.elasticsearch.action.admin.indices.delete.DeleteIndexRequest;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.QueryBuilders;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.junit.After;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;

/**
 * Elastic Search 高階 Api 測試類
 *
 * @author 楊高超
 * @since 2018-01-11
 */
public class HighLevelApiTest {
    RestHighLevelClient restClient = null;

    @Before
    public void before() {
        restClient = new IndexInitUtil().initHighLevelClient();
    }


    @Test
    public void testQueryAuthDoc() {
        try {
SearchRequest searchRequest = new SearchRequest("author_test");
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(QueryBuilders.matchQuery("des", "軟體"));
sourceBuilder.from(0);
sourceBuilder.size(5);
searchRequest.source(sourceBuilder);
SearchResponse response = restClient.search(searchRequest);
            Assert.assertTrue(response.getHits().getTotalHits() == 2);
            System.out.println(response.toString());
        } catch (Exception e) {
            e.printStackTrace();
            Assert.assertTrue(false);
        }
    }


    @After
    public void after() {
        try {
            if (restClient != null) {
                restClient.indices().deleteIndex(new DeleteIndexRequest("author_test"));
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if (restClient != null) {
                try {
                    restClient.close();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
複製程式碼

後記

前面也提到了,社群也貢獻了很多 Elastic Search 的客戶端庫,但是沒有時間去研究。如果有人用過覺得好用,希望推薦。

原文發表在簡書上。

相關文章