Elasticsearch 的搜索功能
建議閱讀順序:
- Elasticsearch 入門
- Elasticsearch 搜索(本文)
- Elasticsearch 搜索高級
- Elasticsearch 高級
1. 介紹
使用 Elasticsearch 最終目的是為了實現搜索功能,現在先將文檔添加到索引中,接下來完成搜索的方法。
查詢的分類:
- 葉子查詢:葉查詢子句在特定字段中查找特定值,例如
match
、term
或range
查詢。- 精確查詢:根據精確詞條值查找數據,一般是查找 keyword、數值、日期、boolean 等類型字段。例如:
- ids:根據文檔 ID 查找文檔
- range:返回包含指定范圍內的文檔,比如:查詢年齡在 10 到 20 歲的學生信息。
- term:根據精確值(例如價格、產品 ID 或用戶名)查找文檔。
- 全文檢索查詢:利用分詞器對用戶輸入內容分詞,然后去倒排索引庫中匹配。例如:
- match_query:對一個字段進行全文檢索
- multi_match_query:對多個字段進行全文檢索
- 精確查詢:根據精確詞條值查找數據,一般是查找 keyword、數值、日期、boolean 等類型字段。例如:
- 復合查詢:以邏輯方式組合多個葉子查詢或者更改葉子查詢的行為方式。
1.1 精確查詢
1.1.1 term
語法:
GET /{索引庫名}/_search
{"query": {"term": {"字段名": {"value": "搜索條件"}}}
}

當輸入的搜索條件不是詞條,而是短語時,由于不做分詞,反而搜索不到:

1.1.2 range
語法:
GET /{索引庫名}/_search
{"query": {"range": {"字段名": {"gte": {最小值},"lte": {最大值}}}}
}
range
是范圍查詢,對于范圍篩選的關鍵字有:
gte
:大于等于gt
:大于lte
:小于等于lt
:小于

1.2 全文檢索
會對搜索條件進行拆分。
1.2.1 match
語法:
GET /{索引庫名}/_search
{"query": {"match": {"字段名": "搜索條件"}}
}

1.2.2 multi_match
同時對多個字段搜索,而且多個字段都要滿足,語法:
GET /{索引庫名}/_search
{"query": {"multi_match": {"query": "搜索條件","fields": ["字段1", "字段2"]}}
}

1.3 排序
語法:
GET /indexName/_search
{"query": {"match_all": {}},"sort": [{"排序字段": {"order": "排序方式asc和desc"}}]
}
如果按照商品價格排序:
GET /items/_search
{"query": {"match_all": {}},"sort": [ { "price": { "order": "desc" } } ]
}
1.4 分頁查詢
elasticsearch 默認情況下只返回 top10 的數據。而如果要查詢更多數據就需要修改分頁參數了。
elasticsearch 中通過修改 from
、size
參數來控制要返回的分頁結果:
from
:從第幾個文檔開始size
:總共查詢幾個文檔
語法:
GET /items/_search
{"query": {"match_all": {}},"from": 0, // 分頁開始的位置,默認為0"size": 10, // 每頁文檔數量,默認10"sort": [ { "price": { "order": "desc" } } ]
}
2. Java Client 實現搜索
2.1 準備
代碼:
@SpringBootTest
public class SearchTest {@Autowiredprivate IItemService itemService;private RestClient restClient = null;private ElasticsearchTransport transport = null;private ElasticsearchClient esClient = null;{// 使用 RestClient 作為底層傳輸對象restClient = RestClient.builder(new HttpHost("192.168.101.68", 9200)).build();ObjectMapper objectMapper = new ObjectMapper();objectMapper.registerModule(new JavaTimeModule());// 使用 Jackson 作為 JSON 解析器transport = new RestClientTransport(restClient, new JacksonJsonpMapper(objectMapper));}// 實現后續操作// TODO@BeforeEachpublic void searchTest() {// 創建客戶端esClient = new ElasticsearchClient(transport);System.out.println(esClient);}@AfterEachpublic void close() throws IOException {transport.close();}}
后續代碼放在代碼的 TODO 處運行即可!!!
2.2 精準查詢
2.2.1 Term 查詢
根據 DSL 語句編寫 java 代碼:
GET /items/_search
{"query": {"term": {"category": { "value": "拉桿箱" }}}
}
代碼:
@Test
public void testTermSearch() throws IOException {SearchResponse<ItemDoc> search = esClient.search(// 搜索索引s -> s.index("items").query(// 精準匹配q -> q.term(t -> t.field("category").value("牛奶"))),// 指定返回類型ItemDoc.class);handleResponse(search);
}
2.2.2 range 查詢
GET /items/_search
{"query": {"range": {"price": { "gte": 100000, "lte": 20 }}}
}
代碼:
@Test
public void testRangeSearch() throws IOException {SearchResponse<ItemDoc> search = esClient.search(// 搜索索引s -> s.index("items").query(// 范圍匹配,price >= 100000 && price < 200000q -> q.range(t -> t.field("price").gte(JsonData.of(100000)).lt(JsonData.of(200000)))),// 指定返回類型ItemDoc.class);
2.3 全文檢索
2.3.1 match 查詢
GET /items/_search
{"query": {"match": {"name": "德國進口純奶"}}
}
代碼:
@Test
public void testMatchSearch() throws IOException {SearchResponse<ItemDoc> search = esClient.search(// 搜索索引s -> s.index("items").query(// 模糊匹配q -> q.match(// 在 name 字段中模糊匹配 "德國進口純奶"t -> t.field("name").query("德國進口純奶"))),// 返回值類型ItemDoc.class);handleResponse(search);
}
2.3.2 multi_match 查詢
GET /items/_search
{"query": {"multi_match": {"query": "筆記本","fields": ["name", "category"]}}
}
代碼:
@Test
public void testMultiMatchSearch() throws IOException {SearchResponse<ItemDoc> search = esClient.search(// 搜索索引s -> s.index("items").query(// 多字段模糊匹配q -> q.multiMatch(// 匹配字關鍵字t -> t.query("筆記本")// 匹配字段.fields("name", "category"))),// 指定返回類型ItemDoc.class);handleResponse(search);
}
2.4 排序和分頁
GET /items/_search
{"query": {"multi_match": {"query": "綠色拉桿箱","fields": ["name","category"]}},"sort": [{ "price": { "order": "asc" } }],"size": 20,"from": 0
}
代碼:
@Test
public void testSortSearch() throws IOException {SearchResponse<ItemDoc> search = esClient.search(// 搜索索引s -> s.index("items")// 查詢條件.query(q -> q.multiMatch(// 匹配字段m -> m.query("綠色拉桿箱").fields("name", "category")))// 排序規則.sort(s1 -> s1.field(// 排序字段f -> f.field("price")// 排序規則.order(SortOrder.Desc)))// 分頁.from(0).size(10),// 指定返回類型ItemDoc.class);handleResponse(search);
}
3. 復合查詢
3.1 布爾查詢
bool 查詢,即布爾查詢。就是利用邏輯運算來組合一個或多個查詢子句的組合。bool 查詢支持的邏輯運算有:
- must:必須匹配每個子查詢,類似 “與”;
- should:選擇性匹配子查詢,類似 “或”;
- must_not:必須不匹配,不參與算分,類似 “非”;
- filter:必須匹配,不參與算分。
舉例:
GET /items/_search
{"query": {"bool": {"must": [ {"match": {"name": "手機"}} ],"should": [{"term": {"brand": { "value": "vivo" }}},{"term": {"brand": { "value": "小米" }}}],"must_not": [{"range": {"price": {"gte": 2500}}}]}},"sort": [ { "brand": { "order": "desc" } } ]
}
說明:
- 必須條件(
must
):- 文檔的
name
字段必須包含“手機”。
- 文檔的
- 可選條件(
should
):- 文檔的
brand
字段應該是 “vivo” 或者 “小米”。只要滿足其中一個條件即可。
- 文檔的
- 排除條件(
must_not
):- 文檔的
price
字段不能大于等于 2500 元。
- 文檔的
- 過濾條件(
filter
):- 文檔的
price
字段必須小于等于 1000 元。
- 文檔的
當 should 與 must、must_not 同時使用時 should 會失效,需要指定 minimum_should_match。
3.2 盡量使用 filter
出于性能考慮,與搜索關鍵字無關的查詢盡量采用 must_not 或 filter 邏輯運算,避免參與相關性算分(如:下拉菜單、多級菜單等)。
比如,要搜索 手機
,但品牌必須是 華為
,價格必須是 900~1599
,那么可以這樣寫:
GET /items/_search
{"query": {"bool": {"must": [{"match": {"name": "手機"}}],"filter": [{"term": {"brand": { "value": "華為" }}},{"range": {"price": {"gte": 90000, "lte": 159900}}}]}}
}
3.3 Java Client
@Test
void testBoolQuery() throws Exception {//構建請求SearchRequest.Builder builder = new SearchRequest.Builder();//設置索引builder.index("items");//設置查詢條件SearchRequest.Builder searchRequestBuilder = builder.query(// bool 查詢,多條件匹配q -> q.bool(// must 連接b -> b.must(m -> m.match(// name 檢索mm -> mm.field("name").query("手機"))).should(s1 -> s1.term( t -> t.field("brand").value("小米"))).should(s1 -> s1.term(t -> t.field("brand").value("vivo"))).minimumShouldMatch("1")))// 排序·.sort(sort -> sort.field(f -> f.field("brand").order(SortOrder.Asc)));SearchRequest build = searchRequestBuilder.build();//執行請求SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);//解析結果handleResponse(searchResponse);
}
4. 高亮顯示
4.1 高亮顯示原理
什么是高亮顯示呢?
我們在百度,京東搜索時,關鍵字會變成紅色,比較醒目,這叫高亮顯示。
觀察頁面源碼,你會發現兩件事情:
- 高亮詞條都被加了
<em>
標簽 <em>
標簽都添加了紅色樣式
因此實現高亮的思路就是:
- 用戶輸入搜索關鍵字搜索數據
- 服務端根據搜索關鍵字到 elasticsearch 搜索,并給搜索結果中的關鍵字詞條添加
html
標簽 - 前端提前給約定好的
html
標簽添加CSS
樣式
4.2 實現高亮
語法:
GET /{索引庫名}/_search
{"query": {"match": {"搜索字段": "搜索關鍵字"}},"highlight": {"fields": {"高亮字段名稱": {"pre_tags": "<em>","post_tags": "</em>"}},"require_field_match": "true"}
}
注意:
- 搜索必須有查詢條件,而且是全文檢索類型的查詢條件,例如
match
; - 參與高亮的字段必須是
text
類型的字段; - 默認情況下參與高亮的字段要與搜索字段一致,除非添加:
required_field_match = false
。

代碼:
@Test
public void testHighLightSearch() throws Exception {SearchResponse<ItemDoc> search = esClient.search(// 搜索索引s -> s.index("items").query(// 匹配字段q -> q.match(// 匹配字段m -> m.field("name").query("筆記本")))// 高亮.highlight(h -> h.fields("name", f -> f)// 高亮標簽,前后綴.preTags("<b style='color:red'>").postTags("</b>")//.requireFieldMatch(false)),ItemDoc.class);long total = search.hits().total().value();System.out.println("total = " + total);List<Hit<ItemDoc>> hits = search.hits().hits();hits.forEach(hit -> {ItemDoc source = hit.source();// 高亮數據Map<String, List<String>> highlight = hit.highlight();List<String> highlightName = highlight.get("name");if(highlightName != null && !highlightName.isEmpty()){String s = highlightName.get(0);source.setName(s);System.out.println("s = " + s);}});
}
5. 數據聚合
5.1 介紹
聚合(aggregations
)可以讓我們極其方便的實現對數據的統計、分析、運算。
應用場景:
- 對數據進行統計
- 在搜索界面顯示符合條件的品牌、分類、規格等信息
聚合常見的有三類:
- 桶(
Bucket
)聚合:用來對文檔做分組
TermAggregation
:按照文檔字段值分組,例如按照品牌值分組、按照國家分組Date Histogram
:按照日期階梯分組,例如一周為一組,或者一月為一組
- 度量(
Metric
)聚合:用以計算一些值,比如:最大值、最小值、平均值等
Avg
:求平均值Max
:求最大值Min
:求最小值Stats
:同時求max
、min
、avg
、sum
等
- 管道(
pipeline
)聚合:其它聚合的結果為基礎做進一步運算
5.2 Bucket 聚合
5.2.1 語法
例如我們要統計所有商品中共有哪些商品分類,其實就是以分類(category)字段對數據分組。category 值一樣的放在同一組,屬于 Bucket
聚合中的 Term
聚合。
基本語法如下:
GET /items/_search
{"size": 0, "aggs": {"category_agg": {"terms": {"field": "category","size": 20,"order": { "_count": "desc" }}}}
}
屬性說明:
aggregations
:定義聚合
-
category_agg
:聚合名稱,自定義,但不能重復-
terms
:聚合的類型,按分類聚合,所以用term
-
field
:參與聚合的字段名稱 -
size
:希望返回的聚合結果的最大數量設置
size
為 0,查詢 0 條數據即結果中不包含文檔,只包含聚合 -
order
:對聚合結果排序
-
-
5.2.2 多級聚合
同時對品牌分組統計,此時需要按分類統計,按品牌統計,這時需要定義多個桶,如下:
GET /items/_search
{"size": 0, "aggs": {"category_agg": {"terms": { "field": "category", "size": 20 }},"brand_agg":{"terms": { "field": "brand", "size": 20 }}}
}
現在需要統計同一分類下的不同品牌的商品數量,這時就需要對桶內的商品二次聚合,如下:
GET /items/_search
{"aggs" : {"category_agg" : {"aggs" : {"brand_agg" : {"terms" : { "field" : "brand", "size" : 20 }}},"terms" : { "field" : "category", "size" : 20 }}},"size" : 0
}
5.3 帶條件聚合
默認情況下,Bucket 聚合是對索引庫的所有文檔做聚合,例如我們統計商品中所有的品牌,結果如下:

可以看到統計出的品牌非常多。
但真實場景下,用戶會輸入搜索條件,因此聚合必須是對搜索結果聚合。那么聚合必須添加限定條件。
例如,我想知道價格高于 3000 元的手機品牌有哪些,該怎么統計呢?
語法如下:
增加 "query"
標簽。
GET /items/_search
{"query": {"bool": {"filter": [{ "term": { "category": "手機" } },{ "range": { "price": { "gte": 300000 } } }]}}, "size": 0, "aggs": { "brand_agg": { "terms": { "field": "brand", "size": 20 } } }
}
5.4 Metric 聚合
統計了價格高于 3000 的手機品牌,形成了一個個桶。現在我們需要對桶內的商品做運算,獲取每個品牌價格的最小值、最大值、平均值。
語法:
GET /items/_search
{"query": {"bool": {"filter": [{ "term": { "category": "手機" } },{ "range": { "price": { "gte": 300000 } } }]}}, "size": 0, "aggs": {"brand_agg": {"terms": {"field": "brand","size": 20,"order": { "stats_metric.avg": "desc" }},"aggs": { "stats_metric": { "stats": { "field": "price" } } }}}
}
屬性說明:
stats_meric
:聚合名稱,自定義名稱
stats
:聚合類型,stats 是metric
聚合的一種field
:聚合字段,這里選擇price
,統計價格
另外,我們還可以讓聚合按照每個品牌的價格平均值排序:
5.5 Java Client
參考 DSL 語句編寫 Java Client 代碼
@Test
void testAggs() throws Exception {//構建請求SearchRequest.Builder builder = new SearchRequest.Builder();//設置索引名builder.index("items");//設置查詢條件builder.query(q -> q.bool(b -> b.filter(f -> f.term(t -> t.field("category").value("手機"))).filter(f -> f.range(r -> r.field("price").gte(JsonData.of(3000))))));//設置返回數量builder.size(0);//設置聚合builder.aggregations("brand_agg", a -> a.terms(t -> t.field("brand").size(10).order(NamedValue.of("stats_metric.avg", SortOrder.Desc))).aggregations("stats_metric", a1 -> a1.stats(s -> s.field("price"))));SearchRequest build = builder.build();//執行請求SearchResponse<ItemDoc> searchResponse = esClient.search(build, ItemDoc.class);//解析出聚合結果Aggregate brandAgg = searchResponse.aggregations().get("brand_agg");brandAgg.sterms().buckets().array().forEach(bucket -> {String key = bucket.key().stringValue();Long docCount = bucket.docCount();StatsAggregate statsMetric = bucket.aggregations().get("stats_metric").stats();//平均價格Double avg = statsMetric.avg();//最大價格Double max = statsMetric.max();//最小價格Double min = statsMetric.min();log.info("品牌:{},商品數量:{},平均價格:{},最大價格:{},最小價格:{}", key, docCount, avg, max, min);});
}