在 Java 應用程式中使用 Elasticsearch

developerworks發表於2016-03-22

如果您使用過 Apache Lucene 或 Apache Solr,就會知道它們的使用體驗非常有趣。尤其在您需要擴充套件基於 Lucene 或 Solr 的解決方案時,您就會了解 Elasticsearch 專案背後的動機。Elasticsearch(構建於 Lucene 之上)在一個容易管理的包中提供了高效能的全文搜尋功能,支援開箱即用地叢集化擴充套件。您可以通過標準的 REST API 或從特定於程式語言的客戶端庫與 Elasticsearch 進行互動。

本教程將展示 Elasticsearch 的實際工作原理。首先從命令列訪問該 REST API 來了解它的基本資訊。然後設定一個本地 Elasticsearch 伺服器,並從一個簡單的 Java 應用程式與它互動。請參見 下載 部分,獲取有關的示例程式碼。

前提條件

要理解本教程的所有示例,需要在您的系統上安裝 Elasticsearch。下載針對您的平臺的 最新 Elastic Search 程式包。將該包解壓到一個方便的位置。在 UNIX 或 Linux 上,通過以下命令啟動該例項:

/elastic-search-dir/bin/elasticsearch

在 Windows 上,執行

/elastic-search-dir/bin/elasticsearch.bat

在看到日誌訊息 started 時,該節點已準備好接受請求。

對於 Java 示例,還需要安裝 Eclipse 和 Apache Maven。如果您的系統上還沒有它們,請下載和安裝它們。

您還需要 cURL。在 Microsoft Windows 上,我使用 Git Bash shell 來執行 cURL。

使用 cURL 執行 REST 命令

可以對 Elasticsearch 發出 cURL 請求,這樣很容易從命令列 shell 體驗該框架。

“Elasticsearch 是無模式的。它可以接受您提供的任何命令,並處理它以供以後查詢。”

Elasticsearch 是無模式的,這意味著它可以接受您提供的任何命令,並處理它以供以後查詢。Elasticsearch 中的所有內容都被儲存為文件,所以您的第一個練習是儲存一個包含歌詞的文件。首先建立一個索引,它是您的所有文件型別的容器 — 類似於 MySQL 等關聯式資料庫中的資料庫。然後,將一個文件插入該索引中,以便可以查詢該文件的資料。

建立一個索引

Elasticsearch 命令的一般格式是:REST VERBHOST:9200/index/doc-type— 其中 REST VERB 是 PUTGET 或 DELETE。(使用 cURL -X 動詞字首來明確指定 HTTP 方法。)

要建立一個索引,可在您的 shell 中執行以下命令:

curl -XPUT "http://localhost:9200/music/"

模式可選

儘管 Elasticsearch 是無模式的,但它在幕後使用了 Lucene,後者使用了模式。不過 Elasticsearch 為您隱藏了這種複雜性。實際上,您可以將 Elasticsearch 文件型別簡單地視為子索引或表名稱。但是,如果您願意,可以指定一個模式,所以您可以將它視為一種模式可選的資料儲存。

插入一個文件

要在 /music 索引下建立一個型別,可插入一個文件。在第一個示例中,您的文件包含資料(包含一行)“Deck the Halls” 的歌詞,這是一首最初由威爾士詩人 John Ceirog Hughes 於 1885 年編寫的傳統的聖誕歌曲。

要將包含 “Deck the Halls” 的文件插入索引中,可執行以下命令(將該命令和本教程的其他 cURL 命令都鍵入到一行中):

curl -XPUT "http://localhost:9200/music/songs/1" -d '
{ "name": "Deck the Halls", "year": 1885, "lyrics": "Fa la la la la" }'

前面的命令使用 PUT 動詞將一個文件新增到 /songs 文件型別,併為該文件分配 ID 1。URL 路徑顯示為 index/doctype/ID

檢視文件

要檢視該文件,可使用簡單的 GET 命令:

curl -XGET "http://localhost:9200/music/songs/1"

Elasticsearch 使用您之前 PUT 進索引中的 JSON 內容作為響應:

{"_index":"music","_type":"songs","_id":"1","_version":1,"found":true,"_source":
{ "name": "Deck the Halls", "year": 1885, "lyrics": "Fa la la la la" }}

更新文件

如果您認識到日期寫錯了,並想將它更改為 1886 怎麼辦?可執行以下命令來更新文件:

curl -XPUT "http://localhost:9200/music/lyrics/1" -d '{ "name": 
"Deck the Halls", "year": 1886, "lyrics": "Fa la la la la" }'

因為此命令使用了相同的唯一 ID 1,所以該文件會被更新。

刪除文件(但暫時不要刪除)

暫時不要刪除該文件,知道如何刪除它就行了:

curl -XDELETE "http://localhost:9200/music/lyrics/1"

從檔案插入文件

這是另一個技巧。您可以使用一個檔案的內容來從命令列插入文件。嘗試此方法,新增另一首針對傳統歌曲 “Ballad of Casey Jones” 的文件。將清單 1 複製到一個名為 caseyjones.json 的檔案中;也可以使用示例程式碼包中的 caseyjones.json 檔案(參見 下載)。將該檔案放在任何方便對它執行 cURL 命令的地方。(在下載的程式碼中,該檔案位於根目錄中。)

清單 1. “Ballad of Casey Jones” 的 JSON 文件
{
  "artist": "Wallace Saunders",
  "year": 1909,
  "styles": ["traditional"],
  "album": "Unknown",
  "name": "Ballad of Casey Jones",
  "lyrics": "Come all you rounders if you want to hear
The story of a brave engineer
Casey Jones was the rounder's name....
Come all you rounders if you want to hear
The story of a brave engineer
Casey Jones was the rounder's name
On the six-eight wheeler, boys, he won his fame
The caller called Casey at half past four
He kissed his wife at the station door
He mounted to the cabin with the orders in his hand
And he took his farewell trip to that promis'd land

Chorus:
Casey Jones--mounted to his cabin
Casey Jones--with his orders in his hand
Casey Jones--mounted to his cabin
And he took his... land"
}

執行以下命令,將此文件 PUT 到您的 music 索引中:

$ curl -XPUT "http://localhost:9200/music/lyrics/2" -d @caseyjones.json

在該索引中時,將清單 2 的內容(包含另一手民歌 “Walking Boss”)儲存到 walking.json 檔案中。

清單 2. “Walking Boss” JSON
{
  "artist": "Clarence Ashley",
  "year": 1920
  "name": "Walking Boss",
  "styles": ["folk","protest"],
  "album": "Traditional",
  "lyrics": "Walkin' boss
Walkin' boss
Walkin' boss
I don't belong to you

I belong
I belong
I belong
To that steel driving crew

Well you work one day
Work one day
Work one day
Then go lay around the shanty two"
}

將此文件推送到索引中:

$ curl -XPUT "http://localhost:9200/music/lyrics/3" -d @walking.json

搜尋 REST API

是時候執行一次基本查詢了,此查詢比您執行來查詢 “Get the Halls” 文件的簡單 GET 要複雜一些。文件 URL 有一個內建的 _search 端點用於此用途。在歌詞中找到所有包含單詞 you 的歌曲:

curl -XGET "http://localhost:9200/music/lyrics/_search?q=lyrics:'you'"

q 參數列示一個查詢。

響應是:

{"took":107,"timed_out":false,"_shards":{"total":5,"successful":5,"failed":0},"hits":{"total":2,"max
_score":0.15625,"hits":[{"_index":"music","_type":"songs","_id":"2","_
score":0.15625,"_source":{"artist": "Wallace Saunders","year": 1909,"styles":
["traditional"],"album": "Unknown","name": "Ballad of Casey Jones","lyrics": "Come all you rounders
if you want to hear The story of a brave engineer Casey Jones was the rounder's name.... Come all
you rounders if you want to hear The story of a brave engineer Casey Jones was the rounder's name
On the six-eight wheeler, boys, he won his fame The caller called Casey at half past four He kissed
his wife at the station door He mounted to the cabin with the orders in his hand And he took his
farewell trip to that promis'd land Chorus: Casey Jones--mounted to his cabin Casey Jones--with his
orders in his hand Casey Jones--mounted to his cabin And he took his... land"
}},{"_index":"music","_type":"songs","_id":"3","_score":0.06780553,"_source":{"artist": "Clarence
Ashley","year": 1920,"name": "Walking Boss","styles": ["folk","protest"],"album":
"Traditional","lyrics": "Walkin' boss Walkin' boss Walkin' boss I don't belong to you I belong I
belong I belong To that steel driving crew Well you work one day Work one day Work one day Then go
lay around the shanty two"}}]}}

使用其他比較符

還有其他各種比較符可供使用。例如,找到所有 1900 年以前編寫的歌曲:

curl -XGET "http://localhost:9200/music/lyrics/_search?q=year:<1900

此查詢將返回完整的 “Casey Jones” 和 “Walking Boss” 文件。

限制欄位

要限制您在結果中看到的欄位,可將 fields 引數新增到您的查詢中:

curl -XGET "http://localhost:9200/music/lyrics/_search?q=year:>1900&fields=year"

檢查搜尋返回物件

清單 3 給出了 Elasticsearch 從前面的查詢返回的資料。

清單 3. 查詢結果
{
    "took": 6,
    "timed_out": false,
    "_shards": {
        "total": 5,
        "successful": 5,
        "failed": 0
    },
    "hits": {
        "total": 2,
        "max_score": 1.0,
        "hits": [{
            "_index": "music",
            "_type": "lyrics",
            "_id": "1",
            "_score": 1.0,
            "fields": {
                "year": [1920]
            }
        }, {
            "_index": "music",
            "_type": "lyrics",
            "_id": "3",
            "_score": 1.0,
            "fields": {
                "year": [1909]
            }
        }]
    }
}

在結果中,Elasticsearch 提供了多個 JSON 物件。第一個物件包含請求的後設資料:看看該請求花了多少毫秒 (took) 和它是否超時 (timed_out)。_shards 欄位需要考慮 Elasticsearch 是一個叢集化服務的事實。甚至在這個單節點本地部署中,Elasticsearch 也在邏輯上被叢集化為分片。

繼續檢視清單 3 中的搜尋結果,可以觀察到 hits 物件包含:

  • total 欄位,它會告訴您獲得了多少個結果
  • max_score,用於全文搜尋
  • 實際結果

實際結果包含 fields 屬性,因為您將 fields 引數新增到了查詢中。否則,結果中會包含 source,而且包含完整的匹配文件。_index_type 和 _id 的用途不言自明;_score 指的是全文搜尋命中長度。這 4 個欄位始終會在結果中返回。

使用 JSON 查詢 DSL

基於查詢字串的搜尋很快會變得很複雜。對於更高階的查詢,Elasticsearch 提供了一種完全基於 JSON 的特定於領域的語言 (DSL)。例如,要搜尋 album 值為 traditional 的每首歌曲,可建立一個包含以下內容的 query.json 檔案:

{
    "query" : {
        "match" : {
            "album" : "Traditional"
        }
    }
}

然後執行:

curl -XGET "http://localhost:9200/music/lyrics/_search" -d @query.json

從 Java 程式碼使用 Elasticsearch

“Elasticsearch 強大功能會在通過語言 API 使用它時體現出來。”

Elasticsearch 強大功能會在通過語言 API 使用它時體現出來。現在我將介紹 Java API,您將從一個應用程式執行搜尋。請參見 下載 部分,獲取相關的示例程式碼。該應用程式使用了 Spark 微型框架,所以可以很快設定它。

示例應用程式

為一個新專案建立一個目錄,然後執行(將該命令鍵入到一行上):

mvn archetype:generate -DgroupId=com.dw -DartifactId=es-demo 
-DarchetypeArtifactId=maven-archetype-quickstart -DinteractiveMode=false

要生成一個專案來在 Eclipse 中使用,可通過 cd 進入 Maven 建立的專案目錄,並執行 mvn eclipse:eclipse

在 Eclipse 中,選擇 File > Import > Existing Project into Workspace。導航到您使用 Maven 的資料夾,選擇該專案,單擊 Finish

在 Eclipse 中,您可以看到一個基本的 Java 專案佈局,包括根目錄中的 pom.xml 檔案和一個 com.dw.App.java 主要類檔案。將您所需的依賴項新增到 pom.xml 檔案中。清單 4 給出了完整的 pom.xml 檔案。

清單 4. 完整的 pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.dw</groupId>
  <artifactId>es-demo</artifactId>
  <packaging>jar</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>es-demo</name>
  <url>http://maven.apache.org</url>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <compilerVersion>1.8</compilerVersion>
          <source>1.8</source>
          <target>1.8</target>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <dependencies>
    <dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-core</artifactId>
    <version>2.3</version>
</dependency>
<dependency>
    <groupId>com.sparkjava</groupId>
    <artifactId>spark-template-freemarker</artifactId>
    <version>2.3</version>
</dependency>
<dependency>
    <groupId>org.elasticsearch</groupId>
    <artifactId>elasticsearch</artifactId>
    <version>2.1.1</version>
</dependency>
  </dependencies>
</project>

清單 4 中的依賴項獲取 Spark 框架核心、Spark Freemarker 模板支援和 Elasticsearch。另請注意,我將 <source> 版本設定為 Java 8,Spark 需要該版本(因為它大量使用了 lambda)。

我不知道您的情況,但我不久前構建了許多 RESTful 應用程式,所以為了改變以下步調,您將為應用程式提供一個更加傳統的 “提交和載入 (submit-and-load)” UI。

在 Eclipse 中,在導航器中右鍵單擊專案,選擇 Configure > Convert to Maven Project,以便 Eclipse 可以解析 Maven 依賴項。轉到專案,右鍵單擊該專案,然後選擇 Maven > Update Project

Java 客戶端配置

Elasticsearch 的 Java 客戶端非常強大;它可以建立一個嵌入式例項並在必要時執行管理任務。但我在這裡將重點介紹如何執行鍼對您已執行的節點的應用程式任務。

執行一個 Java 應用程式和 Elasticsearch 時,有兩種操作模式可供使用。該應用程式可在 Elasticsearch 叢集中扮演更加主動或更加被動的角色。在更加主動的情況下(稱為 Node Client),應用程式例項將從叢集接收請求,確定哪個節點應處理該請求,就像正常節點所做的一樣。(應用程式甚至可以託管索引和處理請求。)另一種模式稱為 Transport Client,它將所有請求都轉發到另一個 Elasticsearch 節點,由後者來確定最終目標。

獲取 Transport Client

對於演示應用程式,(通過 App.java 中執行的初始化)選擇 Transport Client,並保持 Elasticsearch 執行最低階別的處理:

Client client = TransportClient.builder().build()
   .addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("localhost"), 9300));

如果連線到一個 Elasticsearch 叢集,構建器可以接受多個地址。(在本例中,您只有一個 localhost 節點。)連線到埠 9300,而不是像之前在 REST API 的 cURL 中一樣連線到 9200。Java 客戶端將會使用這個特殊埠,使用埠 9200 不起作用。(其他 Elasticsearch 客戶端,Python 客戶端就是其中之一,將會 使用 9200 來訪問 REST API。)

在伺服器啟動時建立該客戶端,並在整個請求處理過程中使用它。Spark 通過 Mustache 模板引擎的 Java 實現來呈現該頁面,而且 Spark 定義了請求端點 — 但我不會太多地解釋這些簡單的用例。(請參見 參考資料,獲取 Spark 的詳細資訊的連結。)

該應用程式的索引頁面顯示了 Java 客戶端的功能:

搜尋結果的螢幕截圖

UI:

  • 呈現現有歌曲的列表
  • 提供一個新增歌曲的按鈕
  • 實現按藝術家和歌詞進行搜尋
  • 返回突出顯示了匹配內容的結果

搜尋和處理結果

在清單 5 中,根 URL / 被對映到 index.mustache 頁面。

清單 5. 基本搜尋
Spark.get("/", (request, response) -> {
        SearchResponse searchResponse = 
            client.prepareSearch("music").setTypes("lyrics").execute().actionGet();
        SearchHit[] hits = searchResponse.getHits().getHits();

            Map<String, Object> attributes = new HashMap<>();
            attributes.put("songs", hits);

            return new ModelAndView(attributes, "index.mustache");
        }, new MustacheTemplateEngine());

清單 5 中的有趣部分始於:

SearchResponse searchResponse = client.prepareSearch("music").setTypes("lyrics").execute().actionGet();

這一行顯示了搜尋 API 的簡單用法。使用 prepareSearch 方法指定一個索引(在本例中為 music),然後執行查詢。查詢基本上顯示為 “Give me all of the records in the music index.”。另外,將文件型別設定為 lyrics,但在這個簡單用例中沒有必要這麼做,因為索引僅包含一種文件型別。在更大的應用程式,需要執行這種設定。這個 API 呼叫類似於您之前看到的 curl -XGET "http://localhost:9200/music/lyrics/_search" 呼叫。

SearchResponse 物件包含有趣的功能(例如命中數量和評分),但就目前而言,您只想要一個結果陣列,可使用searchResponse.getHits().getHits(); 獲得它。

最後,將結果陣列新增到檢視上下文中,並讓 Mustache 呈現它。Mustache 模板如下所示:

清單 6. index.mustache
<html>
<body>
<form name="" action="/search">
  <input type="text" name="artist" placeholder="Artist"></input>
  <input type="text" name="query" placeholder="lyric"></input>
  <button type="submit">Search</button>
</form>
<button onclick="window.location='/add'">Add</button>
<ul>
{{#songs}}
  <li>{{id}} - {{getSource.name}} - {{getSource.year}}
    {{#getHighlightFields}} -
      {{#lyrics.getFragments}}
        {{#.}}{{{.}}}{{/.}}
      {{/lyrics.getFragments}}
    {{/getHighlightFields}}
  </li>
{{/songs}}
</ul>

</body>
</html>

突出顯示高階查詢和匹配內容

要支援突出顯示更高階的查詢和匹配內容,可以使用 /search,如下所示:

清單 7. 搜尋和突出顯示
Spark.get("/search", (request, response) -> {
        SearchRequestBuilder srb = client.prepareSearch("music").setTypes("lyrics");

        String lyricParam = request.queryParams("query");
        QueryBuilder lyricQuery = null;
        if (lyricParam != null && lyricParam.trim().length() > 0){
            lyricQuery = QueryBuilders.matchQuery("lyrics", lyricParam);
        }
        String artistParam = request.queryParams("artist");
        QueryBuilder artistQuery = null;
        if (artistParam != null && artistParam.trim().length() > 0){
          artistQuery = QueryBuilders.matchQuery("artist", artistParam);
        }

        if (lyricQuery != null && artistQuery == null){
          srb.setQuery(lyricQuery).addHighlightedField("lyrics", 0, 0);
        } else if (lyricQuery == null && artistQuery != null){
          srb.setQuery(artistQuery);
        } else if (lyricQuery != null && artistQuery != null){
          srb.setQuery(QueryBuilders.andQuery(artistQuery, 
              lyricQuery)).addHighlightedField("lyrics", 0, 0);
        }

        SearchResponse searchResponse = srb.execute().actionGet();

SearchHit[] hits = searchResponse.getHits().getHits();

    Map<String, Object> attributes = new HashMap<>();
    attributes.put("songs", hits);

    return new ModelAndView(attributes, "index.mustache");
}, new MustacheTemplateEngine());

在清單 7 中,要注意的第一個有趣的 API 用法是 QueryBuilders.matchQuery("lyrics", lyricParam);。這是您設定對 lyrics 欄位的查詢的地方。另外要注意的是 QueryBuilders.andQuery(artistQuery, lyricQuery),它是將查詢的 artist 和 lyrics 部分合併到 AND 查詢中的一種方法。

.addHighlightedField("lyrics", 0, 0); 呼叫告訴 Elasticsearch 生成 lyrics 欄位上的搜尋命中突出顯示結果。第二和第三個引數分別指定無線大小的分段和無限數量的分段。

在呈現搜尋結果時,將突出顯示結果放入 HTML 中。使用 Elasticsearch 就能生成有效的 HTML,使用 <em> 標記來突出顯示匹配字串所在的位置。

插入文件

讓我們來看看如何以程式設計方式將文件插入索引中。清單 8 給出了新增過程。

清單 8. 插入索引中
Spark.post("/save", (request, response) -> {
      StringBuilder json = new StringBuilder("{");
      json.append("\"name\":\""+request.raw().getParameter("name")+"\",");
      json.append("\"artist\":\""+request.raw().getParameter("artist")+"\",");
      json.append("\"year\":"+request.raw().getParameter("year")+",");
      json.append("\"album\":\""+request.raw().getParameter("album")+"\",");
      json.append("\"lyrics\":\""+request.raw().getParameter("lyrics")+"\"}");

      IndexRequest indexRequest = new IndexRequest("music", "lyrics",
          UUID.randomUUID().toString());
      indexRequest.source(json.toString());
      IndexResponse esResponse = client.index(indexRequest).actionGet();

      Map<String, Object> attributes = new HashMap<>();
      return new ModelAndView(attributes, "index.mustache");
    }, new MustacheTemplateEngine());

使用 StringBuilder 直接生成一個 JSON 字串來建立它。在生產應用程式中,可使用 Boon 或 Jackson 等庫。

執行 Elasticsearch 工作的部分是:

IndexRequest indexRequest = new IndexRequest("music", "lyrics", UUID.randomUUID().toString());

在本例中,使用了 UUID 來生成 ID。

結束語

您已快速掌握瞭如何從命令列和在 Java 應用程式中使用 Elasticsearch。您現在已經熟悉了索引、查詢、突出顯示和多欄位搜尋。Elasticsearch 在一個相對容易使用的包中提供了大量的功能。作為一個專案,Elasticsearch 帶來了一些您可能也會感興趣的結果。具體地講,所謂的 ELK 堆疊,即 Elasticsearch、Logstash(用於日誌管理)和 Kibana(用於報告/視覺化),正在迅速發展。

示例程式碼下載:es-demo.zip

相關文章