翻譯自官方文檔英文版,有刪減。
這里使用的Java客戶端版本是5.1.2,Elasticsearch的版本號也要是5.1.2,否則一些功能可能不支持。
之前介紹過Spring Data Elasticsearch,那里也是使用了本文介紹的官方客戶端,只不過Spring Data Elasticsearch是一個社區項目,更新較慢,目前支持到Elasticsearch 2.4。
一、客戶端簡介
你可以使用Java client來執行多種操作:
- 在一個已經存在的集群中執行標準的index, get, delete 和 search操作。
- 在一個正在運行的集群中執行管理員任務
獲得一個Client
是簡單的。最通用的方式是創建一個TransportClient
連接到集群。
maven依賴:
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>5.1.2</version>
</dependency>
二、Transport Client
TransportClient
遠程連接到一個Elasticsearch集群。它并不加入集群,只是獲得一個或多個初始化transport地址,并且對于每個行為以循環方式與它們通訊(盡管大多數行為將會分成兩段式操作)。
// 啟動時
TransportClient client = new PreBuiltTransportClient(Settings.EMPTY)
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("host1"), 9300))
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("host2"), 9300));
// 關閉時
client.close();
注意,如果你的集群名稱不叫"elasticsearch",那么你必須指定它的名字:
Settings settings = Settings.builder()
.put("cluster.name", "myClusterName").build();
TransportClient client = new PreBuiltTransportClient(settings);
//Add transport addresses and do something with the client...
Transport client具有一個集群嗅探特性,允許你動態的增加新主機或者移除老主機。當嗅探被激活時,transport client將會連接到內部的節點列表,就是通過addTransportAddress
方法構建的節點。然后client將會在這些節點上調用內部的集群狀態API來發現可用的數據節點。內部的節點列表將會被這些數據節點替換。這個列表默認每5秒刷新一次。注意嗅探連接的IP地址是那些在節點的elasticsearch配置中被聲明為發布的地址。
記住,上面的節點列表可能不包活原始的節點,如果這個原始節點不是一個數據節點的話。舉個例子,你初始化時連接到一個主節點,當嗅探后,不會有任何請求再會進入那個主節點,而是其他任意一個數據節點。這樣做的原因是避免搜索流量發送給主節點。
為了啟用嗅探,設置client.transport.sniff
為true
:
Settings settings = Settings.builder()
.put("client.transport.sniff", true).build();
TransportClient client = new PreBuiltTransportClient(settings);
其他transport client設置如下:
| 參數 | 描述 |
| ------- | ----- | ---- |
| client.transport.ignore_cluster_name | 當設置為true
時忽略對節點集群名稱的驗證(0.19.4及以后支持) |
|client.transport.ping_timeout | 等待從一個節點返回ping響應的時間,默認是5秒 |
|client.transport.nodes_sampler_interval | 采樣節點列表并連接的間隔,默認是5秒 |
三、文檔APIs
3.1 索引API
索引API允許你將一個JSON格式的文檔添加到特定的索引中,并使它可以被搜索到。
生成JSON文檔
這里有幾個不同的方式來生產JSON文檔:
- 人工的拼接成
String
或者使用byte[]
- 使用一個
Map
,它將會自動轉換成相等的JSON - 使用第三方的類庫來序列化你的對象,例如Jackson
- 使用內置的輔助工具
XContentFactory.jsonBuilder()
在內部,沒種類型的結果都會轉換成byte[]
。如果結果已經是byte[]
形式的話,那么會直接使用它。jsonBuilder
是高度優化的JSON生成器,會直接構造一個byte[]
。
1)自己拼接
沒什么說的,根據各API的格式自己寫,注意日期格式問題。
String json = "{" +
"\"user\":\"kimchy\"," +
"\"postDate\":\"2013-01-30\"," +
"\"message\":\"trying out Elasticsearch\"" +
"}";
2)使用Map
Map<String, Object> json = new HashMap<String, Object>();
json.put("user","kimchy");
json.put("postDate",new Date());
json.put("message","trying out Elasticsearch");
3)使用第三方類庫
以jackson為例。
import com.fasterxml.jackson.databind.*;
// instance a json mapper
ObjectMapper mapper = new ObjectMapper(); // create once, reuse
// generate json
byte[] json = mapper.writeValueAsBytes(yourbeaninstance);
4)使用Elasticsearch輔助工具
import static org.elasticsearch.common.xcontent.XContentFactory.*;
XContentBuilder builder = jsonBuilder()
.startObject()
.field("user", "kimchy")
.field("postDate", new Date())
.field("message", "trying out Elasticsearch")
.endObject()
添加文檔到索引
下面的例子將一個JSON文檔添加到名為twitter,類型為tweet的索引中,其id為1。
import static org.elasticsearch.common.xcontent.XContentFactory.*;
IndexResponse response = client.prepareIndex("twitter", "tweet", "1")
.setSource(jsonBuilder()
.startObject()
.field("user", "kimchy")
.field("postDate", new Date())
.field("message", "trying out Elasticsearch")
.endObject()
)
.get();
另外一種方式,注意沒有指定id。
String json = "{" +
"\"user\":\"kimchy\"," +
"\"postDate\":\"2013-01-30\"," +
"\"message\":\"trying out Elasticsearch\"" +
"}";
IndexResponse response = client.prepareIndex("twitter", "tweet")
.setSource(json)
.get();
IndexResponse
對象將會給你一個報告。
// 索引名稱
String _index = response.getIndex();
// 類型名稱
String _type = response.getType();
// 文檔ID
String _id = response.getId();
// 版本 (如果你是第一次添加這個文檔你將會得到:1)
long _version = response.getVersion();
// 當前實例的狀態
RestStatus status = response.status();
線程化操作
這個將文檔添加到索引的API允許你將操作放在另一個線程中執行(默認的),你可以通過修改operationThreaded
的設置為false
來關閉它。
3.2 獲取文檔API
這個API允許你根據文檔的ID獲取一個JSON類型的文檔。下面的例子展示的是從twitter索引的tweet類型下獲得ID為1的文檔。
GetResponse response = client.prepareGet("twitter", "tweet", "1").get();
String json = response.getSourceAsString();
與添加文檔到索引的API類似,它默認是在另一個線程中執行獲取文檔操作的,下面的例子可以關閉它。
GetResponse response = client.prepareGet("twitter", "tweet", "1")
.setOperationThreaded(false)
.get();
3.3 刪除文檔API
與獲取API很類似,這個API允許你根據文檔的ID刪除一個JSON類型的文檔。下面的例子展示的是從twitter索引的tweet類型下刪除ID為1的文檔。
DeleteResponse response = client.prepareDelete("twitter", "tweet", "1").get();
它默認也是在另一個線程中執行刪除文檔操作的,下面的例子可以關閉它。
DeleteResponse response = client.prepareDelete("twitter", "tweet", "1")
.setOperationThreaded(false)
.get();
3.4 根據查詢條件刪除文檔API
這個API可以根據查詢的結果集來批量刪除文檔。
BulkIndexByScrollResponse response =
DeleteByQueryAction.INSTANCE.newRequestBuilder(client)
.filter(QueryBuilders.matchQuery("gender", "male")) // 查詢
.source("persons") // 索引
.get(); // 執行操作
long deleted = response.getDeleted(); // 被刪除的文檔數量
考慮到它可能是一個耗時很長的操作,如果你想異步的進行此操作參看下面的例子:
DeleteByQueryAction.INSTANCE.newRequestBuilder(client)
.filter(QueryBuilders.matchQuery("gender", "male")) // 查詢
.source("persons") // 索引
.execute(new ActionListener<BulkIndexByScrollResponse>() { // 監聽器
@Override
public void onResponse(BulkIndexByScrollResponse response) {
long deleted = response.getDeleted(); // 被刪除的文檔數量
}
@Override
public void onFailure(Exception e) {
// Handle the exception
}
});
3.5 更新文檔API
1)使用UpdateRequest
UpdateRequest updateRequest = new UpdateRequest();
updateRequest.index("index");
updateRequest.type("type");
updateRequest.id("1");
updateRequest.doc(jsonBuilder()
.startObject()
.field("gender", "male")
.endObject());
client.update(updateRequest).get();
2)使用prepareUpdate()
這種方式又有兩個不同的用法。
client.prepareUpdate("ttl", "doc", "1")
.setScript(new Script("ctx._source.gender = \"male\"" , ScriptService.ScriptType.INLINE, null, null))
.get();
client.prepareUpdate("ttl", "doc", "1")
.setDoc(jsonBuilder()
.startObject()
.field("gender", "male")
.endObject())
.get();
注意,你不能同時提供script 和 doc
3)upsert
如果待更新文檔還不存在,那么會使用upsert元素來創建一個新文檔。
IndexRequest indexRequest = new IndexRequest("index", "type", "1")
.source(jsonBuilder()
.startObject()
.field("name", "Joe Smith")
.field("gender", "male")
.endObject());
UpdateRequest updateRequest = new UpdateRequest("index", "type", "1")
.doc(jsonBuilder()
.startObject()
.field("gender", "male")
.endObject())
.upsert(indexRequest);
client.update(updateRequest).get();
3.6 批量獲得文檔API
你可以根據index, type 和 id來獲得多個文檔。
MultiGetResponse multiGetItemResponses = client.prepareMultiGet()
.add("twitter", "tweet", "1")
.add("twitter", "tweet", "2", "3", "4")
.add("another", "type", "foo")
.get();
for (MultiGetItemResponse itemResponse : multiGetItemResponses) {
GetResponse response = itemResponse.getResponse();
if (response.isExists()) {
String json = response.getSourceAsString();
}
}
3.7 bulk API
bulk API允許你在單個請求里添加或者刪除多個文檔。下面是一個示例用法:
import static org.elasticsearch.common.xcontent.XContentFactory.*;
BulkRequestBuilder bulkRequest = client.prepareBulk();
// either use client#prepare, or use Requests# to directly build index/delete requests
bulkRequest.add(client.prepareIndex("twitter", "tweet", "1")
.setSource(jsonBuilder()
.startObject()
.field("user", "kimchy")
.field("postDate", new Date())
.field("message", "trying out Elasticsearch")
.endObject()
)
);
bulkRequest.add(client.prepareIndex("twitter", "tweet", "2")
.setSource(jsonBuilder()
.startObject()
.field("user", "kimchy")
.field("postDate", new Date())
.field("message", "another post")
.endObject()
)
);
BulkResponse bulkResponse = bulkRequest.get();
if (bulkResponse.hasFailures()) {
// process failures by iterating through each bulk response item
}
3.8 使用Bulk處理器
BulkProcessor
類提供了一個簡單的接口來自動的刷新批量操作,它基于請求的數量或者請求的大小或者手動指定一個范圍。
為了使用它,首先需要創建一個BulkProcessor
實例。
import org.elasticsearch.action.bulk.BackoffPolicy;
import org.elasticsearch.action.bulk.BulkProcessor;
import org.elasticsearch.common.unit.ByteSizeUnit;
import org.elasticsearch.common.unit.ByteSizeValue;
import org.elasticsearch.common.unit.TimeValue;
BulkProcessor bulkProcessor = BulkProcessor.builder(
client, // 添加elasticsearch client
new BulkProcessor.Listener() {
@Override
public void beforeBulk(long executionId,
BulkRequest request) { ... }
@Override
public void afterBulk(long executionId,
BulkRequest request,
BulkResponse response) { ... }
@Override
public void afterBulk(long executionId,
BulkRequest request,
Throwable failure) { ... } // 當批處理失敗并且拋出一個異常時
})
.setBulkActions(10000) // 每10000個請求作為一批處理
.setBulkSize(new ByteSizeValue(5, ByteSizeUnit.MB)) // 每5mb寫入一批數據
.setFlushInterval(TimeValue.timeValueSeconds(5)) // 每5秒寫入一批,不管請求的數量有多少
.setConcurrentRequests(1) // 請求并發的數量 0表示同時只允許1個請求執行
.setBackoffPolicy(
BackoffPolicy.exponentialBackoff(TimeValue.timeValueMillis(100), 3)) // 回退策略
.build();
其中回退策略初始時會等待100ms,并且指數級增長,重試3次。要想關閉回退需要設置BackoffPolicy.noBackoff()
。
一些BulkProcessor
默認參數:
- bulkActions 1000
- bulkSize 5mb
- 沒有flushInterval
- concurrentRequests 1
- backoffPolicy 等待50ms,重試8次,大致最多等待5.1秒
實例完BulkProcessor
就可以添加請求:
bulkProcessor.add(new IndexRequest("twitter", "tweet", "1").source(/* your doc here */));
bulkProcessor.add(new DeleteRequest("twitter", "tweet", "2"));
使用完了后要關閉BulkProcessor
:
// 10分鐘后關閉
bulkProcessor.awaitClose(10, TimeUnit.MINUTES);
// 立即關閉
bulkProcessor.close();
如果在10分鐘內所有的請求執行完畢,awaitClose
方法返回true
,否則返回false
。這兩個方法都會將剩下的文檔寫入,如果設置了flushInterval
則會禁用其他的計劃寫入。
測試時使用Bulk Processor
如果在測試時你想使用BulkProcessor
填充你的數據集你最好將concurrentRequests
設置為0:
BulkProcessor bulkProcessor = BulkProcessor.builder(client, new BulkProcessor.Listener() { /* Listener methods */ })
.setBulkActions(10000)
.setConcurrentRequests(0)
.build();
// 添加你的請求
bulkProcessor.add(/* Your requests */);
// 寫入剩余的請求
bulkProcessor.flush();
// 關閉
bulkProcessor.close();
// 刷新你的索引
client.admin().indices().prepareRefresh().get();
// 現在你可以開始搜索
client.prepareSearch().get();
四、Query DSL
Elasticsearch提供了一個基于JSON的Query DSL(domain specific languages)來定義查詢。它由兩種類型的從句組成:
1)葉子查詢從句
葉子查詢從句在一個指定的域里尋找指定的值。例如match, term 或者 range查詢。這些查詢可以單獨使用。
2)復合查詢從句
復合查詢從句包裝了其他葉子查詢子句或者復合查詢從句,被用在一個邏輯范式里聯合多條件查詢(例如:bool
或者 dis_max
查詢),或者改變它們的行為(例如:constant_score
查詢)。
查詢構建器的工廠類是QueryBuilders
,一旦你的查詢準備好后,你就可以使用搜索API了。
要想使用QueryBuilders
,你只需要將它們導入進你的類即可:
import static org.elasticsearch.index.query.QueryBuilders.*;
你可以使用QueryBuilder
對象的toString()
方法輕松的將生成的JSON查詢條件打印出來。
QueryBuilder
可以用在任何接受查詢條件的API里,例如count
和search
。
4.1 Match All Query
最簡單的查詢,匹配所有的文檔,并將_score
的值設置為1.0。
QueryBuilder qb = matchAllQuery();
4.2 全文查詢
高層次的全文查詢通常被用來在文本域里執行全文查詢并返回相關性最強的結果,比如一封電子郵件的正文里。Elasticsearch中的相關性概念非常重要,也是完全區別于傳統關系型數據庫的一個概念,數據庫中的一條記錄要么匹配要么不匹配。
這個類別下的查詢有:
match query
這是執行全文查詢的標準方式, 包括fuzzy matching(模糊匹配),短語或者近似查詢。
multi_match query
多字段版本匹配查詢
common_terms query
一個更專業化的查詢,更適合一些不尋常的詞語。它是stopwords的一個現代化替代。
query_string query
支持Lucene查詢字符串語法協議,允許你指定AND|OR|NOT
條件和在一個單獨的查詢字符串里進行多字段搜索。只建議高級用戶使用。
simple_query_string
一個更簡單的,更穩定的query_string版本
Match Query
QueryBuilder qb = matchQuery(
"name", // 文檔域的名稱
"kimchy elasticsearch" // 要搜索的文本
);
Multi Match Query
QueryBuilder qb = multiMatchQuery(
"kimchy elasticsearch", // 要搜索的文本
"user", "message" // 文檔域的名稱
);
Common Terms Query
QueryBuilder qb = commonTermsQuery("name", // 文檔域的名稱
"kimchy"); // 值
Query String Query
QueryBuilder qb = queryStringQuery("+kimchy -elasticsearch"); // 要搜索的文本
Simple Query String Query
QueryBuilder qb = simpleQueryStringQuery("+kimchy -elasticsearch"); // 要搜索的文本
4.3 術語級別查詢(Term level queries)
全文查詢會在執行前分析查詢字符串,術語級別的查詢會在索引中精確匹配要查詢的詞語。
這些查詢通常被用在結構化的數據上,比如數字、日期和一些字典表類的數據,而不是一堆很長的文本。另外,它們還允許你手工處理低等級查詢。
這個組里有如下查詢:
term query
在指定域里精確的查詢包含指定詞語的文檔。
QueryBuilder qb = termQuery(
"name", // 文檔域的名稱
"kimchy" // 要搜索的詞
);
terms query
在指定域里精確的查詢包含任一指定詞語的文檔。
QueryBuilder qb = termsQuery("tags", // 文檔域的名稱
"blue", "pill"); // 要搜索的詞
range query
查詢指定域的值(日期、數字或者字符串)在指定范圍內的文檔。
QueryBuilder qb = rangeQuery("price") // 文檔域的名稱
.from(5) // 范圍的開始
.to(10) // 范圍的結束
.includeLower(true) // 包括范圍的開始
.includeUpper(false); // 不包括范圍的結束
exists query
查詢指定域里有不是null
值的所有文檔。
QueryBuilder qb = existsQuery("name"); // 文檔域的名稱
prefix query
查詢所有指定域的值包含特定前綴的文檔
QueryBuilder qb = prefixQuery(
"brand", // 文檔域的名稱
"heine" // 前綴
);
wildcard query
查詢所有指定域的值與通配符表達式匹配的文檔。支持的通配符有單字符通配符?
和多字符通配符*
QueryBuilder qb = wildcardQuery("user", "k?mc*");
regexp query
查詢所有指定域的值與正則表達式匹配的文檔。
QueryBuilder qb = regexpQuery(
"name.first", // 文檔域的名稱
"s.*y"); // 正則表達式
fuzzy query
過時的,針對字符串域,它將會被移除而且沒有替代功能
查詢所有指定域的值與指定的術語相近的文檔。模糊性是由Levenshtein(編輯距離算法)編輯距離1或者2來衡量。
QueryBuilder qb = fuzzyQuery(
"name", // 文檔域的名稱
"kimzhy" // 搜索文本
);
type query
查詢制定類型的所有文檔
QueryBuilder qb = typeQuery("my_type");
ids query
查詢指定類型與ID的文檔
QueryBuilder qb = idsQuery("my_type", "type2")
.addIds("1", "4", "100");
QueryBuilder qb = idsQuery() // 類型是可選的
.addIds("1", "4", "100");
4.4 復合查詢(Compound queries)
復合查詢包裝了其他復合或者葉子查詢,用來合并它們的結果和分數,改變它們的行為,或者從查詢切換到過濾器上下文。
本組有如下查詢:
constant_score query
這個查詢包裹了另一個查詢,但是在過濾器上下文執行它。所有匹配的文檔都會被賦予一個相同的_score
。
QueryBuilder qb = constantScoreQuery(
termQuery("name","kimchy") // 查詢語句
)
.boost(2.0f); // 分數
bool query
默認的復合查詢,具體有must
, should
, must_not
, 或者filter
從句。must
和should
從句會將它們的分數相加,越多的匹配條件分數越高。
QueryBuilder qb = boolQuery()
.must(termQuery("content", "test1")) // must query
.must(termQuery("content", "test4"))
.mustNot(termQuery("content", "test2")) // must not query
.should(termQuery("content", "test3")) // should query
.filter(termQuery("content", "test5")); // 與一般查詢作用一樣,只不過不參與評分
dis_max query
這種查詢接受多個子查詢,并且返回所有子查詢的結果。與bool query不同的是,它會使用最匹配子查詢的分數。
QueryBuilder qb = disMaxQuery()
.add(termQuery("name", "kimchy"))
.add(termQuery("name", "elasticsearch"))
.boost(1.2f)
.tieBreaker(0.7f);
五、搜索APIs
搜索API允許你執行一個搜索查詢,并且取回查詢匹配的數據,查詢條件在后面的章節介紹。它可以被執行在1個或多個索引和類型上。這里有一個例子:
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchType;
import org.elasticsearch.index.query.QueryBuilders.*;
SearchResponse response = client.prepareSearch("index1", "index2")
.setTypes("type1", "type2")
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(QueryBuilders.termQuery("multi", "test")) // Query
.setPostFilter(QueryBuilders.rangeQuery("age").from(12).to(18)) // Filter
.setFrom(0).setSize(60).setExplain(true) // 分頁參數
.get();
注意,所有的參數都是可選的。下面是個條件最少的搜索:
// 使用默認參數匹配整個集群所有的文檔
SearchResponse response = client.prepareSearch().get();
盡管Java API定義了附加的searchType:QUERY_AND_FETCH
和 DFS_QUERY_AND_FETCH
,這些模式是內部優化用的,用戶不應該在API里使用它們。
實際使用中的常見問題
我是將客戶端與Spring集成后使用的。期間遇到了一些問題特此記錄下。
1)找不到Log4j 2的相關方法
因為我項目本身就是Log4j 2所以不用做什么配置。用其他日志框架的可以參考這里
但是我第一次啟動時提示NoSuchMethodException,后來嘗試把Log4j 2的版本升高一些解決這個問題了。原來使用的是2.0.2升級到2.7。
2)failed to get node info for [#transport#-1]
Elasticsearch服務器安裝好后運行起來,通過瀏覽器可以訪問,通過HTTP的接口也正常。上網搜索后發現HTTP接口的默認端口號是9200,但是TransportClient
默認的端口號是9300。
未完,待續...