與傳統數據庫的對比
結構
Relational DB -> Databases -> Tables -> Rows -> Columns
Elasticsearch -> Indices -> Types -> Documents -> Fields
sql查詢
es也可以使用sql進行查詢,將es結構對應關係數據庫進行sql的查詢即可如下:
http://127.0.0.1:9200/_sql?sql=select count(*) from abc_index*
僅用於驗證es查詢結果,因爲sql的執行在內存中進行,非常耗時。
JAVA API
Elasticsearch的交互,可以使用JAVA API,也可以直接使用es提供的REST API 。
REST API
功能
- 檢查集羣、節點和索引的健康信息、狀態以及各種統計信息
- 管理集羣、節點、索引數據以及元數據
- 對索引進行 CRUD(創建、讀取、更新和刪除)和搜索操作
- 執行高級的搜索操作, 例如分頁、排序、過濾、腳本編寫(scripting)、聚合(aggregations)以及其它操作
格式
curl -X <HTTP Verb> /<Index>/<Type>/<ID>
Elasticsearch原理
配置
es的安裝和可視化
運行elasticsearch-5.1.1、kibana-5.1.1。
java yml配置
首先添加依賴;
yml文件的配置如下:
#es的默認名稱,如果安裝es時沒有做特殊的操作名字都是此名稱
spring.data.elasticsearch.cluster-name=elasticsearch
# Elasticsearch 集羣節點服務地址,用逗號分隔,如果沒有指定其他就啓動一個客戶端節點,默認java訪問端口9300
spring.data.elasticsearch.cluster-nodes=localhost:9300
# 設置連接超時時間
spring.data.elasticsearch.properties.transport.tcp.connect_timeout=120s
查看集羣健康信息
REST API
curl -X GET "localhost:9200/_cat/health?v"
get爲訪問方式,localhost:9200/_cat/health?v
爲訪問連接。
從這個響應中,我們可以看到集羣的名稱,狀態,節點數,分片數等等,其中:
- status 狀態有green、yellow和red三種:
- green:集羣運行正常;
- yellow:集羣所有數據都是可用的,集羣功能也齊全,但存在某些複製沒有被分配;
- red:集羣的部分數據不可用,集羣的功能也是不全的,但是集羣還是可以運行的,它可以繼續處理搜索請求,不過開發者要儘快修復它。一般這種情況下es已經基本掛掉了。
- 當前集羣:一共有3個節點,5724個分片等信息。
如果已經安裝了Kibana,也可以通過Kibana查看這些信息。
JAVA API
ClusterHealthResponse response = client.admin().cluster()
.prepareHealth("library")
.execute().actionGet();
查看節點的詳細信息
curl -X GET "localhost:9200/_cat/nodes?v"
TransportClient
import org.elasticsearch.client.transport.TransportClient;
import org.elasticsearch.common.settings.Settings;
Settings settings = Settings.builder()
.put("cluster.name", "elasticsearch").build();
TransportClient client= = new PreBuiltTransportClient(settings). addTransportAddress(new TransportAddress(InetAddress.getByName("XXX.XXX.XX.XX"), 9300));
索引操作
創建index
curl -X PUT "localhost:9200/customer?pretty"
通過一個PUT請求,添加了一個名爲customer的索引;
pretty參數:表示請求響應的JSON格式化之後打印出來,方便開發者閱讀。
刪除index
curl -X DELETE http://192.168.168.101:9200/index02
判斷index是否存在
在es查詢之前,判斷index是否存在,以免拋出異常。
private Client client;
private void init() throws Exception{
Settings settings = Settings.settingsBuilder().put("cluster.name", "log-test")
.build();
client = TransportClient.builder().settings(settings)
.addPlugin(DeleteByQueryPlugin.class)
.build()
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9300))
.addTransportAddress(new InetSocketTransportAddress(InetAddress.getByName("127.0.0.1"), 9300));
}
public boolean indexExists(String index){
IndicesExistsRequest request = new IndicesExistsRequest(index);
IndicesExistsResponse response = client.admin().indices().exists(request).actionGet();
if (response.isExists()) {
return true;
}
return false;
}
查詢所有index
http://127.0.0.1:9200/_cat/indices?v
查詢具體索引內容
查詢bank,bank2:
curl -X GET "localhost:9200/bank,bank2/_search?q=*&sort=account_number:asc&pretty&ignore_unavailable=true"
多索引查詢:
- 支持使用簡單表示法,如
test1,test2,test3
表示法 - 使用
_all
表示所有索引 - 使用通配符,如
test*或 *test或 te*t或 *test*
等 - 也支持排除能力,例如:
test*,-test3
常用其它參數:
參數 | 說明 | 默認值 | 舉例 |
---|---|---|---|
ignore_unavailable | 當指定多個索引時,如果有索引不可用(不存在或者已經關閉)那麼是否忽略該索引 | false | |
allow_no_indices | 允許通配符匹配索引 | true | curl -X GET "localhost:9200/bank3*/_search?q=*&pretty&allow_no_indices=false" ,若bank3不存在,則報錯。 |
expand_wildcards | 查詢索引的範圍 | open表示查詢所有匹配並open的索引,closed則表示查詢所有匹配的索引 | |
pretty | 響應的JSON將被格式化 | true | |
format=yaml | 請求以更可讀的yaml格式響應 | ||
human=true | 以人類可讀的格式來返回數據 | true則返回{"exists_time":"1h"} ,否則,返回:{"exists_time_in_millis":3600000} |
打開/關閉index
curl -XPOST http://192.168.168.101:9200/index01/_close
curl -XPOST http://192.168.168.101:9200/index01/_open
文檔操作
創建/全量更新Document
不存在則創建,存在則覆蓋。
curl -X PUT "localhost:9200/customer/_doc/1?pretty" -H 'Content-Type: application/json' -d'
{
"name": "John Doe"
}'
- document是不可變的,如果要修改document的內容,可以通過全量替換,直接對document重新建立索引,替換裏面所有的內容。
- es會將老的document標記爲deleted(邏輯刪除),然後新增我們給定的一個document,當我們創建越來越多的document的時候,es會在適當的時機在後臺自動刪除(物理刪除)標記爲deleted的document。
- 替換必須帶上所有的field,否則其他數據會丟失。
- Elasticsearch中,並不強制要求顯式的創建索引,即前面案例中,如果開發者在添加文檔之前,還沒有創建customer索引,那麼該文檔一樣也會創建成功的(此時索引會被自動創建)。
未指定id時,系統會自動創建id。
POST /_bulk
{ "index": { "_index": "ecommerce", "_type":"product"}}
{ "name": "test yagao", "desc": "youxiao fangzhu"}
強制創建文檔create
POST /_bulk
{ "create": { "_index": "ecommerce", "_type": "product", "_id": "4" }}
{ "test_field": "test12" }
查詢Document
Rest api:curl -X GET "localhost:9200/customer/_doc/1?pretty"
java api:queryBuilder = QueryBuilders.matchAllQuery().boost(11f).normsField("title");
boost參數被用來增加一個子句的相對權重(當boost大於1時),或者減小相對權重(當boost介於0到1時),但是增加或者減小不是線性的。換言之,boost設爲2並不會讓最終的_score加倍。
相反,新的_score會在適用了boost後被歸一化(Normalized)。每種查詢都有自己的歸一化算法(Normalization Algorithm)。但是能夠說一個高的boost值會產生一個高的_score。
如果你在實現你自己的不基於TF/IDF的相關度分值模型並且你需要對提升過程擁有更多的控制,你可以使用function_score查詢,它不通過歸一化步驟對文檔的boost進行操作。
更新Document
POST /ecommerce/product/1/_update
{
"doc": {
"name": "jiaqiangban gaolujie yagao"
}
}
POST /_bulk
{ "update": { "_index": "ecommerce", "_type": "product", "_id": "4","retry_on_conflict" : 3 }}
{ "doc" : {"test_field" : "test update"} }
刪除Document
curl -X DELETE "localhost:9200/customer?pretty"
在刪除一個document之後,我們可以從側面證明,它不是立即物理刪除的,因爲它的一些版本號等信息還是保留的。
POST /_bulk
{ "delete": { "_index": "ecommerce", "_type": "product", "_id": "4"}}
由於刪除只需要被刪除文檔的ID,所以並沒有對應的源文檔。
bulk API按順序執行這些操作。如果其中一個操作因爲某些原因失敗了,它將會繼續處理後面的操作。當bulk API返回時,它將提供每個操作的狀態(按照同樣的順序),所以開發者能夠看到每個操作成功與否。
查詢操作
term query
場景:用於精確查詢一個字段。
curl -XGET http://192.168.168.101:9200/index01/_search -d {'query':{'term':{'title':'你好'}}}
term查找時直接對關鍵詞進行查找。
java api:
SearchResponse response = client.prepareSearch("index1", "index2")
.setTypes("type1", "type2")
.setSearchType(SearchType.DFS_QUERY_THEN_FETCH)
.setQuery(QueryBuilders.termQuery("multi", "test")) //term Query
.setPostFilter(QueryBuilders.rangeQuery("age").from(12).to(18)) // range query、Filter query
.setFrom(0).setSize(60).setExplain(true)
.get();
terms query
場景:用於精確查詢多個字段。
{
'query':{
'terms':{
'tag':["search",'nosql','hello'] //json中必須包含數組。
}
}
}
i> Preparing a query 準備查詢請求
SearchResponse response = client.prepareSearch("library")
.addFields("title", "_source")
.execute().actionGet();
for(SearchHit hit: response.getHits().getHits()) {
System.out.println(hit.getId());
if (hit.getFields().containsKey("title")) {
System.out.println("field.title: "+ hit.getFields().get("title").getValue());
}
System.out.println("source.title: " + hit.getSource().get("title"));
}
ii> Building queries 構造查詢
import org.elasticsearch.index.query.QueryBuilder;
import org.elasticsearch.index.query.QueryBuilders;
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(QueryBuilders.termQuery("sid_s", text));
Search search = new Search.Builder(searchSourceBuilder.toString()).addIndex("webpage").build();
SearchResult result = client.execute(search);
match query
場景:用於當前文檔的全文模糊查找。
match在匹配時會對所查找的關鍵詞進行分詞,然後按分詞匹配查找。
GET /ecommerce/product/_search
{'query':{'match':{'title':'你好'}}}
{
"query": {
"match": {
"__type": "info"
}
},
"sort": [
{
"campaign_end_time": {
"order": "desc"
}
}
]
}
匹配查詢:
queryBuilder = QueryBuilders
.matchQuery("message", "a quick brown fox")
.operator(Operator.AND)
.zeroTermsQuery(ZeroTermsQuery.ALL);
match_all
場景:查詢指定索引下的所有文檔。
{'query':{'match_all':{'title':'標題一樣'}}}
multi match
場景:多值匹配查詢。
{
"query": {
"multi_match": {
"query": "運動 上衣",
"fields": [
"brandName^100",
"brandName.brandName_pinyin^100",
"brandName.brandName_keyword^100",
"sortName^80",
"sortName.sortName_pinyin^80",
"productName^60",
"productKeyword^20"
],
"type": <multi-match-type>,
"operator": "AND"
}
}
}
match_phrase(短語查詢)
場景:精確查找所有字段。
match(全文檢索)會將輸入的搜索串拆解開來,去倒排索引裏面去一一匹配,只要能匹配上任意一個拆解後的單詞,就可以作爲結果返回。
match_phrase要求輸入的搜索串,必須在指定的字段文本中,完全包含一模一樣的,纔可以算匹配,才能作爲結果返回。
GET /ecommerce/product/_search
{
"query" : {
"match_phrase" : {
"producer" : "yagao producer"
}
}
}
Bool query
場景:多條件查詢。
bool查詢包含四個子句,must,filter,should,must_not。
- must:表示一定要滿足,相當於and;
- should:表示可以滿足也可以不滿足,相當於or;
- must_not:表示不能滿足該條件,相當於not。
- filter: 過濾查詢。Elasticsearch在2.x版本將filter query去掉,併入bool query。
{
'query':{
'bool':{
'must':[{
'term':{
'_type':{
'value':'age'
}
}
},{
'term':{
'account_grade':{
'value':'23'
}
}
}
]
}
}
}
{
"bool":{
"must":{
"term":{"user":"lucy"}
},
"filter":{
"term":{"tag":"teach"}
},
"should":[
{"term":{"tag":"wow"}},
{"term":{"tag":"elasticsearch"}}
],
"mininum_should_match":1,
"boost":1.0
}
}
“minimum_should_match”: 1,表示最小匹配度,可以設置爲百分百,詳情看源文檔Elasticsearch Reference [6.4] » Query DSL » Minimum Should Match,設置了這個值的時候就必須滿足should裏面的設置了,另外注意這邊should裏面同一字段設置的多個值(意思是當這個值等於X或者等於Y的時候都成立,務必注意格式)。
match來指定查詢條件;
filter執行速度高於查詢,原因如下:
- 過濾器不會計算相關度的得分(即結果中的_score字段,如果不關注這個字段,使用filter更好)
- 過濾器可以被緩存到內存中,在重複搜索時,速度會比較快。
過濾查詢java api:
QueryBuilder filterBuilder = QueryBuilders
.filteredQuery(
QueryBuilders.existsQuery("title").queryName("exist"),
QueryBuilders.termQuery("title", "elastic")
);
SearchResponse response = client.prepareSearch("library")
.setPostFilter(filterBuilder)
.execute().actionGet();
range query
{
'query':{
'range':{
'age':{
'gte':'30',
'lte':'20'
}
}
}
}
java api 見 term query例子。
java api-精確匹配:termQuery,相當於=
java api-範圍匹配:rangeQuery,相當於SQL between and
分頁查詢
GET /ecommerce/product/_search
{
"query": { "match_all": {} },
"_source": ["name", "price"]
"size": 1,
"from": 10
}
size表示返回的文檔個數爲1,默認爲10。
from表示從第10個開始查詢,主要用於分頁查詢。
_source表示自定義返回字段。默認會返回文檔的所有字段。
分頁查詢:
SearchResponse response = client.prepareSearch("library")
.setQuery(QueryBuilders.matchAllQuery())
.setFrom(10) //跳過前10個文檔
.setSize(20) //獲取20個文檔
.execute().actionGet();
response.getHits().totalHits()//可以統計當前匹配到的結果數
批量查詢
優點:能夠大大減少網絡的請求次數,縮減網絡開銷。
自定義設置index、type以及document id:(id爲1的沒有查到(found爲false))
GET /_mget
{
"docs" : [
{
"_index" : "ecommerce",
"_type" : "product",
"_id" : 1
},
{
"_index" : "ecommerce",
"_type" : "product",
"_id" : 2
}
]
}
在對應的index、type下進行批量查詢:(注意:在ElasticSearch6.0以後一個index下只能有一個type,否則會報錯)
GET /ecommerce/product/_mget
{
"ids": [2, 3]
}
或者:
GET /ecommerce/product/_mget
{
"docs" : [
{
"_id" : 2
},
{
"_id" : 3
}
]
}
通配符查詢
{
'query':{
'wildcard':{
'title':'cr?me'
}
}
}
正則表達式查詢
{
'query':{
'regex':{
'title':{
'value':'cr.m[ae]',
'boost':10.0
}
}
}
}
match_phrase_prefix query
場景:前綴查詢。
{
'query':{
'match_phrase_prefix':{
'title':{
'query':'crime punish',
'slop':1
}
}
}
}
query_string
{
'query':{
'query_string':{
'query':'title:crime^10 +title:punishment -otitle:cat +author:(+Fyodor +dostoevsky)'
}
}
}
sort 排序
降序排序desc,升序asc。
GET /ecommerce/product/_search
{
"query" : {
"match" : {
"name" : "yagao"
}
},
"sort": [
{ "price": "desc" }
]
}
java api:
searchSourceBuilder.fetchSource(null, "content").sort("_score");
searchSourceBuilder.sort("date", SortOrder.DESC);
SortBuilders.scriptSort(script, type) //使用腳本來實現排序
SortBuilders.geoDistanceSort(fieldName) //根據空間距離來進行排序
searchRequestBuilder.addSort("publish_time", SortOrder.DESC);
按照某個字段排序的話,hit.getScore()將會失效。
聚合查詢
bucket和metric:
- bucket(桶):group by 分組之後,相同的數據放進一個bucket。
- metric(度量/指標):對一個數據分組執行的統計。如:avg\max\min
group by缺點:
- 涉及group by的查詢會降低查詢速率
- group by之後無法拿到其它信息(通過後文講解的tophits可以拿到)
- group by之後無法排序
terms聚合
terms根據字段值項分組聚合。field按什麼字段分組,size指定返回多少個分組,shard_size指定每個分片上返回多少個分組,order排序方式。可以指定include和exclude正則篩選表達式的值,指定missing設置缺省值。
【terms】 java api見【max/min/avg/sum/stats】中的例子。
計算每個tag下的商品數量:
GET /ecommerce/product/_search
{
"size": 0, //size=0,表示只獲取聚合結果,而不要執行聚合的原始數據。
"aggs": { //aggs:固定語法,要對一份數據執行分組聚合操作
"all_tags": { //all_tags:自定義對每個aggs取名。
"terms": { "field": "tags" } //terms根據字段的值進行分組;field:根據指定的字段的值進行分組將文本
}
}
}
返回結果:
{
"took": 53,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 2,
"max_score": 0,
"hits": []
},
"aggregations": {
"all_tags": {
"doc_count_error_upper_bound": 0,
"sum_other_doc_count": 0,
"buckets": [
{
"key": "fangzhu",
"doc_count": 2
},
{
"key": "meibai",
"doc_count": 2
}
]
}
}
}
hits.hits:我們指定了size是0,所以hits.hits就是空的,否則會把執行聚合的那些原始數據給你返回回來
aggregations:聚合結果
all_tags:我們指定的某個聚合的名稱
buckets:根據我們指定的field劃分出的buckets
key:每個bucket對應的分組字段的值
doc_count:這個bucket分組內,有多少個數據
默認的排序規則:按照doc_count降序排序
max/min/avg/sum/stats
stats:bucket,terms,自動就會有一個doc_count,就相當於是數量。
avg:avg aggs,求平均值
max:求一個bucket內,指定field值最大的那個數據
min:求一個bucket內,指定field值最小的那個數據
sum:求一個bucket內,指定field值的總和先分組,再算每組的平均值
GET /ecommerce/product/_search
{
"size": 0,
"aggs" : {
"group_by_tags" : {
"terms" : { "field" : "tags" },
"aggs" : {
"avg_price": { "avg": { "field": "price" } },
"min_price" : { "min": { "field": "price"} },
"max_price" : { "max": { "field": "price"} },
"sum_price" : { "sum": { "field": "price" } }
}
}
}
{
"aggs":{
"avg_fees":{
"avg":{
"field":"fees"
}
}
}
}
聚合操作主要是調用了SearchRequestBuilder的addAggregation方法,通常是傳入一個TermsBuilder。
多字段上的聚合操作需要用到子聚合(subAggregation),子聚合調用TermsBuilder的subAggregation方法,可以添加的子聚合有TermsBuilder、SumBuilder、AvgBuilder、MaxBuilder、MinBuilder等常見的聚合操作。
從實現上來講,SearchRequestBuilder在內部保持了一個私有的 SearchSourceBuilder實例, SearchSourceBuilder內部包含一個List,每次調用addAggregation時會調用 SearchSourceBuilder實例,添加一個AggregationBuilder。
同樣的,TermsBuilder也在內部保持了一個List,調用addAggregation方法(來自父類addAggregation)時會添加一個AggregationBuilder。
聚合操作
例如要計算每個球隊年齡最大/最小/總/平均的球員年齡,如果使用SQL語句,應表達如下:
select team, max(age) as max_age from player group by team;
ES的java api:
TermsBuilder teamAgg= AggregationBuilders.terms("player_count ").field("team");
MaxBuilder ageAgg= AggregationBuilders.max("max_age").field("age");
sbuilder.addAggregation(teamAgg.subAggregation(ageAgg));
SearchResponse response = sbuilder.execute().actionGet();
子聚合
例如要計算每個球隊球員的平均年齡,同時又要計算總年薪,如果使用SQL語句,應表達如下:
select team, avg(age)as avg_age, sum(salary) as total_salary from player group by team;
ES的java api:
TermsBuilder teamAgg= AggregationBuilders.terms("team");
AvgBuilder ageAgg= AggregationBuilders.avg("avg_age").field("age");
SumBuilder salaryAgg= AggregationBuilders.avg("total_salary ").field("salary");
sbuilder.addAggregation(teamAgg.subAggregation(ageAgg).subAggregation(salaryAgg));
SearchResponse response = sbuilder.execute().actionGet();
一次計算出count max min avg sum
public void stats(){
SearchResponse response = client.prepareSearch(indexName).setTypes(typeName)
.addAggregation(AggregationBuilders.stats("ageAgg").field("age"))
.get();
Stats ageAgg = response.getAggregations().get("ageAgg");
System.out.println("總數:"+ageAgg.getCount());
System.out.println("最小值:"+ageAgg.getMin());
System.out.println("最大值:"+ageAgg.getMax());
System.out.println("平均值:"+ageAgg.getAvg());
System.out.println("和:"+ageAgg.getSum());
}
group by多個field
例如要計算每個球隊每個位置的球員數,如果使用SQL語句,應表達如下:
select team, position, count(*) as pos_count from player group by team, position;
ES的java api:
TermsBuilder teamAgg= AggregationBuilders.terms("player_count ").field("team");
TermsBuilder posAgg= AggregationBuilders.terms("pos_count").field("position");
sbuilder.addAggregation(teamAgg.subAggregation(posAgg));
SearchResponse response = sbuilder.execute().actionGet();
group by/count
例如要計算每個球隊的球員數,如果使用SQL語句,應表達如下:
select team, count(*) as player_count from player group by team;
TermsBuilder teamAgg= AggregationBuilders.terms("player_count ").field("team");
sbuilder.addAggregation(teamAgg);
SearchResponse response = sbuilder.execute().actionGet();
CountResponse response = client.prepareCount("library")
.setQuery(QueryBuilders.termQuery("title", "elastic"))
.execute().actionGet();
聚合後對Aggregation結果排序
例如要計算每個球隊總年薪,並按照總年薪倒序排列,如果使用SQL語句,應表達如下:
select team, sum(salary) as total_salary from player group by team order by total_salary desc;
ES的java api:
TermsBuilder teamAgg= AggregationBuilders.terms("team").order(Order.aggregation("total_salary ", false);
SumBuilder salaryAgg= AggregationBuilders.avg("total_salary ").field("salary");
sbuilder.addAggregation(teamAgg.subAggregation(salaryAgg));
SearchResponse response = sbuilder.execute().actionGet();
需要特別注意的是,排序是在TermAggregation處執行的,Order.aggregation函數的第一個參數是aggregation的名字,第二個參數是boolean型,true表示正序,false表示倒序。
Aggregation結果條數的問題
默認情況下,search執行後,僅返回10條聚合結果,如果想反悔更多的結果,需要在構建TermsBuilder 時指定size:
TermsBuilder teamAgg= AggregationBuilders.terms("team").size(15);
Aggregation結果的解析/輸出
得到response後:
Map<String, Aggregation> aggMap = response.getAggregations().asMap();
StringTerms teamAgg= (StringTerms) aggMap.get("keywordAgg");
Iterator<Bucket> teamBucketIt = teamAgg.getBuckets().iterator();
while (teamBucketIt .hasNext()) {
Bucket buck = teamBucketIt .next(); //分桶
//球隊名
String team = buck.getKey();
//記錄數
long count = buck.getDocCount();
//得到所有子聚合
Map subaggmap = buck.getAggregations().asMap();
//avg值獲取方法
double avg_age= ((InternalAvg) subaggmap.get("avg_age")).getValue();
//sum值獲取方法
double total_salary = ((InternalSum) subaggmap.get("total_salary")).getValue();
//...
//max/min以此類推
}
Top Hits Aggregation
i> 作用
Top Hits聚合主要用於桶聚合後查詢分組後的其它數據。
比如對於下表,通過max(time)group by ip
進行分組後,我們還想知道每一組數據hostname等其它字段內容,則需要使用Top Hits,再每個bucket中查詢對應的數據,具體代碼如下:
score | ip | hostname | time |
---|
TermsAggregationBuilder depIpGroup = AggregationBuilders.terms("group_by_ip").field("dip").size(10000);//一次最多拿到10000條數據,要拿到更多的數據參考後文scroll的相關講解
TopHitsAggregationBuilder detail = AggregationBuilders.topHits("detail").size(1);//用於拿到分組以外的其它詳情數據。size來確定數量,默認返回3條數據。sort用於組內排序。
MaxAggregationBuilder maxTime = AggregationBuilders.max("max_time").field("time");
SearchRequestBuilder searchRequestBuilder = client
.prepareSearch(indexExistsList.toArray(new String[indexNameList.size()]))//通過變長數組查詢多個index
.setTypes(indexType).addAggregation(depIpGroup.subAggregation(maxTime).subAggregation(detail));
SearchResponse searchResponse = searchRequestBuilder
.execute()
.actionGet();
Terms ipTerms = searchResponse.getAggregations().get("group_by_ip");
for (Terms.Bucket bucket : ipTerms.getBuckets()) {//分桶
ListEntity listEntity = new ListEntity();
listEntity.setIpAddress(bucket.getKey().toString());
TopHits topHits = bucket.getAggregations().get("detail");
SearchHit hit = topHits.getHits().getHits()[0];//????沒看懂原理,先這樣用吧。返回的是一個id不同、其它數據相同的數組,由size決定長度。
listEntity.setHostname(hit.getSource().get("hostname").toString());
listEntity.setScore(Integer.parseInt(hit.getSource().get("score").toString()));
dataList.add(listEntity);
}
cardinality去重
{
"size": 0,
"aggs": {
"count_type": {
"cardinality": {
"field": "__type"
}
}
}
}
cardinality
percentiles百分比
percentiles對指定字段(腳本)的值按從小到大累計每個值對應的文檔數的佔比(佔所有命中文檔數的百分比),返回指定佔比比例對應的值。默認返回[ 1, 5, 25, 50, 75, 95, 99 ]分位上的值。
{
"size": 0,
"aggs": {
"age_percents":{
"percentiles": {
"field": "age",
"percents": [
1,
5,
25,
50,
75,
95,
99
]
}
}
}
}
{
"size": 0,
"aggs": {
"states": {
"terms": {
"field": "gender"
},
"aggs": {
"banlances": {
"percentile_ranks": {
"field": "balance",
"values": [
20000,
40000
]
}
}
}
}
}
percentiles rank
統計小於等於指定值的文檔比。
{
"size": 0,
"aggs": {
"tests": {
"percentile_ranks": {
"field": "age",
"values": [
10,
15
]
}
}
}
}
filter聚合
場景:對不同的bucket下的aggs,進行filter。
filter對滿足過濾查詢的文檔進行聚合計算,在查詢命中的文檔中選取過濾條件的文檔進行聚合,先過濾在聚合。
如果放query裏面的filter,是全局的,會對所有的數據都有影響。
但是,如果,比如說,你要統計,長虹電視,最近1個月的平均值; 最近3個月的平均值; 最近6個月的平均值,用bucket filter。
{
"size": 0,
"aggs": {
"agg_filter":{
"filter": {
"match":{"gender":"F"}
},
"aggs": {
"avgs": {
"avg": {
"field": "age"
}
}
}
}
}
}
filtters聚合
多個過濾組聚合計算。
{
"size": 0,
"aggs": {
"message": {
"filters": {
"filters": {
"errors": {
"exists": {
"field": "__type"
}
},
"warring":{
"term": {
"__type": "info"
}
}
}
}
}
}
}
range聚合
{
"aggs": {
"agg_range": {
"range": {
"field": "cost",
"ranges": [
{
"from": 50,
"to": 70
},
{
"from": 100
}
]
},
"aggs": {
"bmax": {
"max": {
"field": "cost"
}
}
}
}
}
}
date_range聚合
{
"aggs": {
"date_aggrs": {
"date_range": {
"field": "accepted_time",
"format": "MM-yyy",
"ranges": [
{
"from": "now-10d/d",
"to": "now"
}
]
}
}
}
}
date_histogram聚合(時間直方圖聚合)
按天、月、年等進行聚合統計。可按 year (1y), quarter (1q), month (1M), week (1w), day (1d), hour (1h), minute (1m), second (1s) 間隔聚合或指定的時間間隔聚合。
{
"aggs": {
"sales_over_time": {
"date_histogram": {
"field": "accepted_time",
"interval": "quarter",
"min_doc_count" : 0, //可以返回沒有數據的月份
"extended_bounds" : { //強制返回數據的範圍
"min" : "2014-01-01",
"max" : "2014-12-31"
}
}
}
}
}
missing聚合
{
"aggs": {
"account_missing": {
"missing": {
"field": "__type"
}
}
}
}
global bucket
將所有數據納入聚合的scope,而不管之前的query。
aggregation,scope,一個聚合操作,必須在query的搜索結果範圍內執行
出來兩個結果,一個結果,是基於query搜索結果來聚合的; 一個結果,是對所有數據執行聚合的。
GET /tvs/sales/_search
{
"size": 0,
"query": {
"term": {
"brand": {
"value": "長虹"
}
}
},
"aggs": {
"single_brand_avg_price": {
"avg": {
"field": "price"
}
},
"all": {
"global": {},
"aggs": {
"all_brand_avg_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
返回結果:
{
"took": 4,
"timed_out": false,
"_shards": {
"total": 5,
"successful": 5,
"failed": 0
},
"hits": {
"total": 3,
"max_score": 0,
"hits": []
},
"aggregations": {
"all": {
"doc_count": 8,
"all_brand_avg_price": {
"value": 2650
}
},
"single_brand_avg_price": {
"value": 1666.6666666666667
}
}
}
single_brand_avg_price:就是針對query搜索結果,執行的,拿到的,就是長虹品牌的平均價格
all.all_brand_avg_price:拿到所有品牌的平均價格
top_hits 按搜索結果聚合
top_hits 獲取前幾個doc_(即分組內前幾個doc_,由size指定,默認爲3個)
source 返回指定field(主要用於group by之後不能查看其它字段詳情)。
GET /ecommerce/product/_search
{
"size": 0,
"aggs" : {
"group_by_tags" : {
"terms" : { "field" : "tags" },
"aggs" : {
"top_tags": {
"top_hits": {
"_source": {
"include": "name"
},
"size": 1
}
}
}
}
}
}
collect_mode 子聚合計算
depth_first
直接進行子聚合的計算
計算每個tag下的商品的平均價格,並且按照平均價格降序排序:
"order": { "avg_price": "desc" }
GET /ecommerce/product/_search
{
"size": 0,
"aggs" : {
"all_tags" : {
"terms" : { "field" : "tags", "collect_mode" : "breadth_first", "order": { "avg_price": "desc" } },
"aggs" : {
"avg_price" : {
"avg" : { "field" : "price" }
}
}
}
}
}
vii> breadth_first
先計算出當前聚合的結果,針對這個結果在對子聚合進行計算。
"ranges": [{},{}]
按照指定的價格範圍區間進行分組,然後在每組內再按照tag進行分組,最後再計算每組的平均價格:
GET /ecommerce/product/_search
{
"size": 0,
"aggs": {
"group_by_price": {
"range": {
"field": "price",
"ranges": [
{
"from": 0,
"to": 20
},
{
"from": 20,
"to": 40
},
{
"from": 40,
"to": 50
}
]
},
"aggs": {
"group_by_tags": {
"terms": {
"field": "tags"
},
"aggs": {
"average_price": {
"avg": {
"field": "price"
}
}
}
}
}
}
}
}
histogram
類似於terms,也是進行bucket分組操作,接收一個field,按照這個field的值的各個範圍區間,進行bucket分組操作
interval:10,劃分範圍,010,1020,20~30
GET /ecommerce/product/_search
{
"size" : 0,
"aggs":{
"price":{
"histogram":{
"field": "price",
"interval": 10
},
"aggs":{
"revenue": {
"sum": {
"field" : "price"
}
}
}
}
}
}
date histogram
按照我們指定的某個date類型的日期field,以及日期interval,按照一定的日期間隔,去劃分bucket
date interval = 1m,
2017-01-01~2017-01-31,就是一個bucket
2017-02-01~2017-02-28,就是一個bucket
然後會去掃描每個數據的date field,判斷date落在哪個bucket中,就將其放入那個bucket
min_doc_count:即使某個日期interval,2017-01-01~2017-01-31中,一條數據都沒有,那麼這個區間也是要返回的,不然默認是會過濾掉這個區間的
extended_bounds,min,max:劃分bucket的時候,會限定在這個起始日期,和截止日期內
GET /tvs/sales/_search
{
"size" : 0,
"aggs": {
"sales": {
"date_histogram": {
"field": "sold_date",
"interval": "month",
"format": "yyyy-MM-dd",
"min_doc_count" : 0,
"extended_bounds" : {
"min" : "2016-01-01",
"max" : "2017-12-31"
}
}
}
}
}
滾動(翻頁)查詢
java api
滾動搜索(Scroll API)
String scrollId = "";
QueryBuilder queryBuilder = QueryBuilders.matchAllQuery();
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
sourceBuilder.query(queryBuilder).size(10000);
SearchRequest request = Requests.searchRequest(indexName);
request.scroll("1s");
request.source(sourceBuilder);
SearchResponse response = client.search(request).actionGet();
severityCount = deelHits(severityCount, response.getHits());
scrollId = response.getScrollId();
while (true) {
SearchScrollRequestBuilder searchScrollRequestBuilder = client.prepareSearchScroll(scrollId);
searchScrollRequestBuilder.setScroll("1s");
// 請求
SearchResponse response1 = searchScrollRequestBuilder.get();
SearchHits hits = response1.getHits();
if (hits.getHits().length == 0) {
break;
}else {
severityCount = deelHits(severityCount, hits); //hit.getSource().get("detail").toString()讀取數據
//下一批處理
scrollId = response1.getScrollId();
}
}
public class Scroll {
public static void main(String[] args) {
try{
long startTime = System.currentTimeMillis();
/*創建客戶端*/
//client startup
//設置集羣名稱
Settings settings = Settings.builder()
.put("cluster.name", "elsearch")
.put("client.transport.sniff", true)
.build();
//創建client
TransportClient client = new PreBuiltTransportClient(settings)
.addTransportAddress(new InetSocketTransportAddress(
InetAddress.getByName("54.223.232.95"),9300));
List<String> result = new ArrayList<>();
String scrollId = "";
//第一次請求
SearchSourceBuilder sourceBuilder = new SearchSourceBuilder();
//TODO: 設置查詢條件
RangeQueryBuilder rangequerybuilder = QueryBuilders
.rangeQuery("inputtime")
.from("2016-12-14 02:00:00").to("2016-12-14 07:59:59");
sourceBuilder.query(QueryBuilders.boolQuery()
.must(QueryBuilders
.matchPhraseQuery("pointid","W3.UNIT1.10HFC01CT013"))
.must(rangequerybuilder))
.size(100)//如果開啓遊標,則滾動獲取
.sort("inputtime", SortOrder.ASC);
//查詢
SearchRequest request = Requests.searchRequest("pointdata");
request.scroll("2m");
request.source(sourceBuilder);
SearchResponse response = client.search(request).actionGet();
//TODO:處理數據
SearchHits hits = response.getHits();
for(int i = 0; i < hits.getHits().length; i++) {
//System.out.println(hits.getHits()[i].getSourceAsString());
result.add(hits.getHits()[i].getSourceAsString());
}
//記錄滾動ID
scrollId = response.getScrollId();
while(true){
//後續的請求
//scrollId = query.getScollId();
SearchScrollRequestBuilder searchScrollRequestBuilder = client
.prepareSearchScroll(scrollId);
// 重新設定滾動時間
//TimeValue timeValue = new TimeValue(30000);
searchScrollRequestBuilder.setScroll("2m");
// 請求
SearchResponse response1 = searchScrollRequestBuilder.get();
//TODO:處理數據
SearchHits hits2 = response1.getHits();
if(hits2.getHits().length == 0){
break;
}
for(int i = 0; i < hits2.getHits().length; i++) {
result.add(hits2.getHits()[i].getSourceAsString());
}
//下一批處理
scrollId = response1.getScrollId();
}
System.out.println(result.size());
long endTime = System.currentTimeMillis();
System.out.println("Java程序運行時間:" + (endTime - startTime) + "ms");
}catch(Exception e){
e.printStackTrace();
}
}
REST API
scroll原理
scroll搜索會在第一次搜索的時候,保存一個當時的視圖快照,之後只會基於該舊的視圖快照提供數據搜索,如果這個期間數據變更,是不會讓用戶看到的;
採用基於_doc(不使用_score)進行排序的方式,性能較高
每次發送scroll請求,我們還需要指定一個scroll參數,指定一個時間窗口,每次搜索請求只要在這個事件窗口內能完成就可以了
# sort默認是相關度排序("sort":[{"FIELD":{"order":"desc"}}]),不按_score排序,按_doc排序
# size設置的是這批查三條
# 第一次查詢會生成快照
GET /lib3/user/_search?scroll=1m #這一批查詢在一分鐘內完成
{
"query":{
"match":{}
},
"sort":[
"_doc"
],
"size":3
}
# 第二次查詢通過第一次的快照ID來查詢,後面以此類推
GET /_search/scroll
{
"scroll":"1m",
"scroll_id":"DnF1ZXJ5VGhIbkXIdGNoAwAAAAAAAAAdFkEwRENOVTdnUUJPWVZUd1p2WE5hV2cAAAAAAAAAHhZBMERDTIU3Z1FCT1|WVHdadIhOYVdnAAAAAAAAAB8WQTBEQ05VN2dRQk9ZVIR3WnZYTmFXZw=="
}
基於 scroll 解決深度分頁問題
原理上是對某次查詢生成一個遊標 scroll_id , 後續的查詢只需要根據這個遊標去取數據,直到結果集中返回的 hits 字段爲空,就表示遍歷結束。
注意:scroll_id 的生成可以理解爲建立了一個臨時的歷史快照,在此之後的增刪改查等操作不會影響到這個快照的結果。
使用 curl 進行分頁讀取過程如下:
- 先獲取第一個 scroll_id,url 參數包括 /index/_type/ 和 scroll,scroll 字段指定了scroll_id 的有效生存期,以分鐘爲單位,過期之後會被es 自動清理。如果文檔不需要特定排序,可以指定按照文檔創建的時間返回會使迭代更高效。
GET /product/info/_search?scroll=2m
{
"query":{
"match_all":{
}
},
"sort":["_doc"]
}
# 返回結果
{
"_scroll_id": "DnF1ZXJ5VGhIbkXIdGNoAwAAAAAAAAAdFkEwRENOVTdnUUJPWVZUd1p2WE5hV2cAAAAAAAAAHhZBMERDTIU3Z1FCT1|WVHdadIhOYVdnAAAAAAAAAB8WQTBEQ05VN2dRQk9ZVIR3WnZYTmFXZw==",
"took": 1,
"timed_out": false,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits":{...}
}
- 後續的文檔讀取上一次查詢返回的scroll_id 來不斷的取下一頁,如果srcoll_id 的生存期很長,那麼每次返回的 scroll_id 都是一樣的,直到該 scroll_id 過期,纔會返回一個新的 scroll_id。請求指定的 scroll_id 時就不需要 /index/_type 等信息了。每讀取一頁都會重新設置 scroll_id 的生存時間,所以這個時間只需要滿足讀取當前頁就可以,不需要滿足讀取所有的數據的時間,1 分鐘足以。
GET /product/info/_search?scroll=DnF1ZXJ5VGhIbkXIdGNoAwAAAAAAAAAdFkEwRENOVTdnUUJPWVZUd1p2WE5hV2cAAAAAAAAAHhZBMERDTIU3Z1FCT1|WVHdadIhOYVdnAAAAAAAAAB8WQTBEQ05VN2dRQk9ZVIR3WnZYTmFXZw==
{
"query":{
"match_all":{
}
},
"sort":["_doc"]
}
# 返回結果
{
"_scroll_id": "DnF1ZXJ5VGhIbkXIdGNoAwAAAAAAAAAdFkEwRENOVTdnUUJPWVZUd1p2WE5hV2cAAAAAAAAAHhZBMERDTIU3Z1FCT1|WVHdadIhOYVdnAAAAAAAAAB8WQTBEQ05VN2dRQk9ZVIR3WnZYTmFXZw==",
"took": 106,
"_shards": {
"total": 1,
"successful": 1,
"failed": 0
},
"hits": {
"total": 22424,
"max_score": 1.0,
"hits": [{
"_index": "product",
"_type": "info",
"_id": "did-519392_pdid-2010",
"_score": 1.0,
"_routing": "519392",
"_source": {
....
}
}
]
}
}
- 所有文檔獲取完畢之後,需要手動清理掉 scroll_id 。雖然es 會有自動清理機制,但是 srcoll_id 的存在會耗費大量的資源來保存一份當前查詢結果集映像,並且會佔用文件描述符。所以用完之後要及時清理。使用 es 提供的 CLEAR_API 來刪除指定的 scroll_id。
# 刪掉指定的多個 srcoll_id
DELETE /_search/scroll -d
{
"scroll_id":[
"cXVlcnlBbmRGZXRjaDsxOzg3OTA4NDpTQzRmWWkwQ1Q1bUlwMjc0WmdIX2ZnOzA7"
]
}
# 刪除掉所有索引上的 scroll_id
DELETE /_search/scroll/_all
# 查詢當前所有的scroll 狀態
GET /_nodes/stats/indices/_search?pretty
# 返回結果
{
"cluster_name" : "200.200.107.232",
"nodes" : {
"SC4fYi0CT5mIp274ZgH_fg" : {
"timestamp" : 1514346295736,
"name" : "200.200.107.232",
"transport_address" : "200.200.107.232:9300",
"host" : "200.200.107.232",
"ip" : [ "200.200.107.232:9300", "NONE" ],
"indices" : {
"search" : {
"open_contexts" : 0,
"query_total" : 975758,
"query_time_in_millis" : 329850,
"query_current" : 0,
"fetch_total" : 217069,
"fetch_time_in_millis" : 84699,
"fetch_current" : 0,
"scroll_total" : 5348,
"scroll_time_in_millis" : 92712468,
"scroll_current" : 0
}
}
}
}
}
基於 search_after 實現深度分頁
search_after 是 ES5.0 及之後版本提供的新特性,search_after 有點類似 scroll,但是和 scroll 又不一樣,它提供一個活動的遊標,通過上一次查詢最後一條數據來進行下一次查詢。
search_after 分頁的方式和 scroll 有一些顯著的區別,首先它是根據上一頁的最後一條數據來確定下一頁的位置,同時在分頁請求的過程中,如果有索引數據的增刪改查,這些變更也會實時的反映到遊標上。
- 第一頁的請求和正常的請求一樣。
GET /order/info/_search
{
"size": 10,
"query": {
"match_all" : {
}
},
"sort": [
{"date": "asc"}
]
}
# 返回結果
{
"_index": "zmrecall",
"_type": "recall",
"_id": "60310505115909",
"_score": null,
"_source": {
...
"date": 1545037514
},
"sort": [
1545037514
]
}
- 第二頁的請求,使用第一頁返回結果的最後一個數據的值,加上 search_after 字段來取下一頁。注意:使用 search_after 的時候要將 from 置爲 0 或 -1。
curl -XGET 127.0.0.1:9200/order/info/_search
{
"size": 10,
"query": {
"match_all" : {
}
},
"search_after": [1463538857], # 這個值與上次查詢最後一條數據的sort值一致,支持多個
"sort": [
{"date": "asc"}
]
}
注意:
如果 search_after 中的關鍵字爲654,那麼654323的文檔也會被搜索到,所以在選擇 search_after 的排序字段時需要謹慎,可以使用比如文檔的id或者時間戳等。
search_after 適用於深度分頁+ 排序,因爲每一頁的數據依賴於上一頁最後一條數據,所以無法跳頁請求。
返回的始終是最新的數據,在分頁過程中數據的位置可能會有變更。這種分頁方式更加符合 moa 的業務場景。
es返回結果
獲取所有數據:
GET /_search
接口訪問鏈接:127.0.0.1:9200/_search
返回數據含義:
返回參數 | 說明 | 備註 |
---|---|---|
found | 表示查詢的數據是否存在 | |
took | 耗費時間(毫秒)。 | |
timed_out | 是否超時 | 默認無timeout |
_source | 表示查詢到的數據 | |
_shards | shards fail的條件(primary和replica全部掛掉),不影響其他shard | 默認情況下來說,一個搜索請求,會打到一個index的所有primary shard上去,當然了,每個primary shard都可能會有一個或多個replic shard,所以請求也可以到primary shard的其中一個replica shard上去 |
_shards.total | 表示應執行索引操作的分片(主分片和副本分片)的數量 | |
_shards.successful | 表示索引操作成功的分片數 | |
_shards.failed | 返回一個數組,這個數組是在副本分片上索引操作失敗的情況下相關錯誤的數組 | 如果沒有失敗的分片,failed將會爲0。 |
hits.total | 本次搜索返回了幾條結果 | |
hits.max_score | score的含義,就是document對於一個search的相關度的匹配分數,越相關,就越匹配,分數也高 | |
hits.hits | 包含了匹配搜索的document的詳細數據,默認查詢前10條數據,按_score降序排序 | 在java api中,可以通過client.setSize設置返回數量。 |
hits.hits._index | 索引名,對應sql的庫 | |
hits.hits._type | 類型,對應sql的表 | |
hits.hits._id | 搜索的id | |
hits.hits._score | 描述搜索結果的匹配度,得分越高,文檔匹配度越高,得分越低,文檔的匹配度越低。 | |
hits.hits._source | 搜索到的具體數據 | |
hits.hits._source.fields | 搜索到的具體字段 | 在java api中通過hit.getSource().get(“field_name”)獲取 |
可以通過設置timeout這個值,來定時返回已經搜索到的數據。timeout機制,指定每個shard,就只能在timeout時間範圍內,將搜索到的部分數據(也可能是搜索到的全部數據),直接返回給client,而不是等到所有數據全部搜索出來後再返回。可以通過如下方式進行設置:
timeout=10ms,timeout=1s,timeout=1m
GET /_search?timeout=10m
基於bulk的增刪改
Elasticsearch也提供了相關操作的批處理功能,這些批處理功能通過使用_bulk API實現。通過批處理可以非常高效的完成多個文檔的操作,同時可以減少不必要的網絡請求。
bulk語法:
- delete:刪除一個文檔,只要1個json串就可以了
- create:PUT /index/type/id/_create,強制創建
- index:普通的put操作,可以是創建文檔,也可以是全量替換文檔
- update:執行的partial update操作
注意點:
- bulk api對json的語法有嚴格的要求,除了delete外,每一個操作都要兩個json串,且每個json串內不能換行,非同一個json串必須換行,否則會報錯;
- bulk操作中,任意一個操作失敗,是不會影響其他的操作的,但是在返回結果裏,會告訴你異常日誌;
bulk api奇特的json格式
目前處理流程:
- 直接按照換行符切割json,不用將其轉換爲json對象,不會出現內存中的相同數據的拷貝;
- 對每兩個一組的json,讀取meta,進行document路由;
- 直接將對應的json發送到node上去;
換成良好json格式的處理流程:
- 將json數組解析爲JSONArray對象,這個時候,整個數據,就會在內存中出現一份一模一樣的拷貝,一份數據是json文本,一份數據是JSONArray對象;
- 解析json數組裏的每個json,對每個請求中的document進行路由;
- 爲路由到同一個shard上的多個請求,創建一個請求數組;
- 將這個請求數組序列化;
- 將序列化後的請求數組發送到對應的節點上去;
數據錄入
可以將json數據(可以在http://www.json-generator.com/網站上自動生成)放到當前用戶目錄下,然後執行如下命令,將數據導入到Elasticsearch中,如下:
curl -H "Content-Type: application/json" -XPOST "localhost:9200/bank/_doc/_bulk?pretty&refresh" --data-binary "@accounts.json"
參考文獻
ElasticSearch教程——彙總篇
elasticsearch文檔Delete By Query API(二)
Elasticsearch-基礎介紹及索引原理分析
ElasticSearch教程——Java常用操作
ElasticSearch AggregationBuilders java api常用聚合查詢