記一次線上的elasticsearch查詢(xún)采坑
第一次使用elasticsearch忍些,于是從網(wǎng)上找輪子復(fù)制粘貼。早好輪子測(cè)試完畢轰异,上線岖沛。可是幾天下來(lái)發(fā)現(xiàn)接口響應(yīng)時(shí)間一直都偏高(默認(rèn)的超時(shí)時(shí)間是500ms)搭独,所以就不停的對(duì)代碼優(yōu)化婴削,縮短時(shí)間。但是到最后代碼已經(jīng)不能再優(yōu)化了牙肝,響應(yīng)時(shí)間依然沒(méi)有明顯的下降趨勢(shì)唉俗,甚至在高峰期會(huì)嚴(yán)重超時(shí)。接下來(lái)會(huì)慢慢講解elasticsearch使用優(yōu)化配椭。
Spring Boot添加elasticsearch依賴(lài)
有很多種方案可以選擇虫溜,1)添加spring的data依賴(lài)。2)使用elasticsearch提供的client依賴(lài)股缸。3)使用jestClient依賴(lài)衡楞。前兩種并沒(méi)有什么區(qū)別,第三種是通過(guò)http請(qǐng)求訪問(wèn)elasticsearch的敦姻。
使用elasticsearch官方依賴(lài)
使用IDE初始化Springboot時(shí)勾選elasticsearch即可瘾境,或者你也可以直接添加如下依賴(lài):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
<version>{elasticserch.version}</version>
</dependency>
或者到maven網(wǎng)站查找對(duì)應(yīng)elasticsearch版本的依賴(lài):
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>{elasticserch.version}</version>
</dependency>
<dependency>
<groupId>org.elasticsearch.client</groupId>
<artifactId>transport</artifactId>
<version>{elasticserch.version}</version>
</dependency>
需要注意的是歧杏,一定要使用與你的elasticsearch版本一直的依賴(lài),否則可能會(huì)出錯(cuò)寄雀。
elasticsearch的配置
@Configuration
public class ElasticsearchConfig {
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchConfig.class);
@Value("${elasticsearch.port}")
private String port;
@Value("${elasticsearch.cluster.name}")
private String clusterName;
@Value("${elasticsearch.pool}")
private String poolSize;
@Value("${elasticsearch.ip}")
private String esHost;
@Bean(name = "transportClient")
public TransportClient transportClient() {
logger.info("Elasticsearch初始化開(kāi)始得滤。。盒犹。懂更。。");
TransportClient transportClient = null;
try {
// 配置信息
Settings esSetting = Settings.builder()
//集群名字
.put("cluster.name", clusterName)
//增加嗅探機(jī)制急膀,找到ES集群
.put("client.transport.sniff", true)
// .put("client.transport.ignore_cluster_name", true)
//增加線程池個(gè)數(shù)沮协,暫時(shí)設(shè)為5
.put("thread_pool.search.size", Integer.parseInt(poolSize))
.build();
//配置信息Settings自定義
transportClient = new PreBuiltTransportClient(esSetting);
TransportAddress transportAddress = new TransportAddress(InetAddress.getByName(esHost), Integer.valueOf(port));
transportClient.addTransportAddresses(transportAddress);
logger.info("連接elasticsearch");
} catch (Exception e) {
logger.error("elasticsearch TransportClient create error!!", e);
}
return transportClient;
}
}
低版本的elasticsearch在配置setting自定義內(nèi)容時(shí)會(huì)不一樣。使用elasticsearch節(jié)點(diǎn)連接的端口是9300卓嫂。
簡(jiǎn)單的使用:
@Component
public class ElasticsearchUtils {
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchUtils.class);
@Resource(name = "transportClient")
private TransportClient transportClient;
private static TransportClient client;
@PostConstruct
public void init() {
client = this.transportClient;
}
/**
* @author xiaosen
* @description 判斷索引是否存在
* @date 2019/1/23
* @param
* @return
*/
public static boolean isIndexExist(String index) {
IndicesExistsResponse inExistsResponse = client.admin().indices().exists(new IndicesExistsRequest(index)).actionGet();
if (inExistsResponse.isExists()) {
logger.info("索引:{}存在", index);
} else {
logger.info("索引:{}不存在", index);
}
return inExistsResponse.isExists();
}
public static List<Map<String, Object>> searchListData(String index, String type, long startTime, long endTime, Integer size, String fields, String sortField, boolean matchPhrase, String highlightField, String matchStr) {
SearchRequestBuilder searchRequestBuilder = client.prepareSearch(index);
if (StringUtils.isNotEmpty(type)) {
searchRequestBuilder.setTypes(type.split(","));
}
BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
if (startTime > 0 && endTime > 0) {
boolQuery.must(QueryBuilders.rangeQuery("processTime")
.format("epoch_millis")
.from(startTime)
.to(endTime)
.includeLower(true)
.includeUpper(true));
}
//搜索的的字段
if (StringUtils.isNotEmpty(matchStr)) {
for (String s : matchStr.split(",")) {
String[] ss = s.split("=");
if (ss.length > 1) {
if (matchPhrase == Boolean.TRUE) {
boolQuery.must(QueryBuilders.matchPhraseQuery(s.split("=")[0], s.split("=")[1]));
} else {
boolQuery.must(QueryBuilders.matchQuery(s.split("=")[0], s.split("=")[1]));
}
}
}
}
// 高亮(xxx=111,aaa=222)
if (StringUtils.isNotEmpty(highlightField)) {
HighlightBuilder highlightBuilder = new HighlightBuilder();
// 設(shè)置高亮字段
highlightBuilder.field(highlightField);
searchRequestBuilder.highlighter(highlightBuilder);
}
searchRequestBuilder.setQuery(boolQuery);
if (StringUtils.isNotEmpty(fields)) {
searchRequestBuilder.setFetchSource(fields.split(","), null);
}
searchRequestBuilder.setFetchSource(true);
if (StringUtils.isNotEmpty(sortField)) {
searchRequestBuilder.addSort(sortField, SortOrder.DESC);
}
if (size != null && size > 0) {
searchRequestBuilder.setSize(size);
}
SearchResponse searchResponse = searchRequestBuilder.execute().actionGet();
long totalHits = searchResponse.getHits().totalHits;
long length = searchResponse.getHits().getHits().length;
if (searchResponse.status().getStatus() == 200) {
// 解析對(duì)象
return setSearchResponse(searchResponse, highlightField);
}
return null;
}
使用JestClient
添加maven依賴(lài)(這里的elasticsearch版本比較低慷暂,而且還沒(méi)有開(kāi)放9300端口,只能使用http請(qǐng)求)
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch</artifactId>
<version>1.5.2</version>
</dependency>
<dependency>
<groupId>io.searchbox</groupId>
<artifactId>jest</artifactId>
<version>6.3.1</version>
</dependency>
io.searchbox是操作elasticsearch的依賴(lài)晨雳,使用其9200端口行瑞。
配置文件就比較簡(jiǎn)單了:
@Configuration
@RefreshScope
public class ElasticsearchConfigure {
private static final Logger logger = LoggerFactory.getLogger(ElasticsearchConfigure.class);
@Value("${elasticsearch.ip}")
private String hostAndPort;
@Bean(name = "elasticsearchClient")
public JestClient getJestClient() throws Exception{
JestClientFactory factory = new JestClientFactory();
SSLContext sslContext = new SSLContextBuilder().loadTrustMaterial(null, (X509Certificate[] arg0, String arg1) -> true).build();
// http配置
factory.setHttpClientConfig(new HttpClientConfig.Builder("http://"+hostAndPort).connTimeout(2000)
.readTimeout(2000).plainSocketFactory(PlainConnectionSocketFactory.getSocketFactory())
.sslSocketFactory(new SSLConnectionSocketFactory(sslContext, NoopHostnameVerifier.INSTANCE))
.multiThreaded(true).maxTotalConnection(100).defaultMaxTotalConnectionPerRoute(4).build());
return factory.getObject();
}
}
創(chuàng)建一個(gè)JestClientFactory并配置httpClient。
簡(jiǎn)單的一個(gè)例子:
@Resource(name = "elasticsearchClient")
private JestClient jestClient;
public static void main(String[] args){
FilterBuilder filterBuilder = FilterBuilders.boolFilter()
.must(FilterBuilders.geoDistanceRangeFilter("location")
.point(lat, lon).from(Constants.MIN_RADIUS).to(Constants.MAX_RADIUS))
.should(FilterBuilders.termFilter("status", 200), FilterBuilders.termFilter("status", 201));
FilteredQueryBuilder filteredQueryBuilder = new FilteredQueryBuilder(null, filterBuilder);
// 按在線時(shí)間排序餐禁,先按時(shí)間再按距離排序
FieldSortBuilder sortBuilderField = SortBuilders.fieldSort("time").order(SortOrder.DESC);
// 按距離排序血久,為返回客戶(hù)端距離,返回的單位:米
GeoDistanceSortBuilder sortBuilderDis = SortBuilders.geoDistanceSort("location").point(lat, lon).unit(DistanceUnit.KILOMETERS).order(SortOrder.ASC).geoDistance(GeoDistance.SLOPPY_ARC);
SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
searchSourceBuilder.query(filteredQueryBuilder).sort(sortBuilderField).sort(sortBuilderDis)
.from((queryNearbyDto.getCurrentPage()-1)*queryNearbyDto.getPageSize())
.size(queryNearbyDto.getPageSize()).fetchSource(Constants.QUERY_FIELD_TTID, null);
String query = searchSourceBuilder.toString();
result = search(jestClient, index, Constants.ES_NEARBY_TYPE, query);
}
private List<Map<String, Object>> search(JestClient jestClient, String indexName, String typeName, String query) throws Exception {
Search search = new Search.Builder(query).setSearchType(SearchType.QUERY_THEN_FETCH)
.addIndex(indexName)
.addType(typeName)
.build();
SearchResult jr = jestClient.execute(search);
if (!jr.isSucceeded()||jr.getResponseCode()!=200){
return null;
}
Long total = jr.getTotal();
List<SearchResult.Hit<Map, Void>> maps = jr.getHits(Map.class, false);
List<Map<String, Object>> sourceList = maps.stream().map(source -> {
source.source.put("sort", Double.valueOf(source.sort.get(1)));
return (Map<String, Object>)source.source;
}).collect(Collectors.toList());
return sourceList;
}
其中的變量query是查詢(xún)的elasticsearch的語(yǔ)句帮非,如果你知道elasticsearch的語(yǔ)法也可以直接寫(xiě)一個(gè)json代替氧吐。
距離排序
在jestClient中有一個(gè)按距離和時(shí)間排序的例子,是先按時(shí)間排序再按距離排序末盔,目的是返回距離筑舅。es是可以按多個(gè)字段排序的,靠前的為優(yōu)先匹配排序陨舱,最后的排序結(jié)果會(huì)在返回的sort數(shù)組中返回翠拣,數(shù)組中的位置即排序的匹配位置,我這里將返回的距離提取出來(lái)放到map中游盲。
5.2的elasticsearch的api的距離排序方法如下:
GeoDistanceSortBuilder sortBuilderDis = SortBuilders.geoDistanceSort("location", lat, lon).point(lat, lon).unit(DistanceUnit.METERS).order(SortOrder.ASC).geoDistance(GeoDistance.ARC);
這里如果不想讓elasticsearch計(jì)算距離也可以用他提供的方法自己計(jì)算心剥,前提知道二者的經(jīng)緯度,調(diào)用GeoDistance的calculate方法背桐,具體使用的精確度可以按照業(yè)務(wù)要求選擇优烧,不過(guò)我有做過(guò)測(cè)試,自己計(jì)算距離和elasticsearch計(jì)算的耗時(shí)幾乎相差不多链峭,如果是額外的計(jì)算距離可以不再查一遍elasticsearch減少io消耗畦娄。
分頁(yè)
對(duì)于elasticsearch不太熟悉的同學(xué),分頁(yè)也是一個(gè)坑。
淺分頁(yè)
elasticsearch的的淺分頁(yè)from&size熙卡,from是查詢(xún)的索引位置杖刷,size是每頁(yè)數(shù)量,優(yōu)點(diǎn)類(lèi)似于mysql的limit和start驳癌。
現(xiàn)在我們可以假設(shè)在一個(gè)有 5 個(gè)主分片的索引中搜索滑燃。 當(dāng)我們請(qǐng)求結(jié)果的第一頁(yè)(結(jié)果從 1 到 10 ),每一個(gè)分片產(chǎn)生前 10 的結(jié)果颓鲜,并且返回給 協(xié)調(diào)節(jié)點(diǎn) 表窘,協(xié)調(diào)節(jié)點(diǎn)對(duì) 50 個(gè)結(jié)果排序得到全部結(jié)果的前 10 個(gè)。現(xiàn)在假設(shè)我們請(qǐng)求第 1000 頁(yè)--結(jié)果從 10001 到 10010 甜滨。所有都以相同的方式工作除了每個(gè)分片不得不產(chǎn)生前10010個(gè)結(jié)果以外乐严。 然后協(xié)調(diào)節(jié)點(diǎn)對(duì)全部 50050 個(gè)結(jié)果排序最后丟棄掉這些結(jié)果中的 50040 個(gè)結(jié)果∫履Γ可以看到昂验,在分布式系統(tǒng)中,對(duì)結(jié)果排序的成本隨分頁(yè)的深度成指數(shù)上升艾扮。這就是 web 搜索引擎對(duì)任何查詢(xún)都不要返回超過(guò) 1000 個(gè)結(jié)果的原因既琴。你翻頁(yè)的時(shí)候,翻的越深泡嘴,每個(gè) Shard 返回的數(shù)據(jù)就越多呛梆,而且協(xié)調(diào)節(jié)點(diǎn)處理的時(shí)間越長(zhǎng),非晨恼铮坑爹。所以用 ES 做分頁(yè)的時(shí)候纹腌,你會(huì)發(fā)現(xiàn)越翻到后面霎终,就越是慢。我們之前也是遇到過(guò)這個(gè)問(wèn)題升薯,用 ES 作分頁(yè)莱褒,前幾頁(yè)就幾十毫秒,翻到 10 頁(yè)或者幾十頁(yè)的時(shí)候涎劈,基本上就要 5~10 秒才能查出來(lái)一頁(yè)數(shù)據(jù)了广凸。
使用from&size的最大查詢(xún)量是10000條數(shù)據(jù),這個(gè)值可以在elasticsearch中配置文件中設(shè)置蛛枚。
scroll 深分頁(yè)
為了解決上面的問(wèn)題谅海,elasticsearch提出了一個(gè)scroll滾動(dòng)的方式。scroll 類(lèi)似于sql中的cursor蹦浦,使用scroll扭吁,每次只能獲取一頁(yè)的內(nèi)容,然后會(huì)返回一個(gè)scroll_id。根據(jù)返回的這個(gè)scroll_id可以不斷地獲取下一頁(yè)的內(nèi)容侥袜,所以scroll并不適用于有跳頁(yè)的情景.
POST /twitter/_search?scroll=1m
{
"size": 100,
"query": {
"match" : {
"title" : "elasticsearch"
}
}
}
- scroll=1m表示設(shè)置scroll_id保留1分鐘可用蝌诡。
- 使用scroll必須要將from設(shè)置為0。
- size決定后面每次調(diào)用_search搜索返回的數(shù)量
POST /_search/scroll
{
"scroll" : "1m",
"scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ=="
}
然后我們可以通過(guò)數(shù)據(jù)返回的_scroll_id讀取下一頁(yè)內(nèi)容枫吧,每次請(qǐng)求將會(huì)讀取下10條數(shù)據(jù)浦旱,直到數(shù)據(jù)讀取完畢或者scroll_id保留時(shí)間截止。請(qǐng)求的接口不再使用索引名了九杂,而是 _search/scroll颁湖,其中GET和POST方法都可以使用。
search_after
Scroll 被推薦用于深度查詢(xún)尼酿,但是contexts的代價(jià)是昂貴的爷狈,不推薦用于實(shí)時(shí)用戶(hù)請(qǐng)求,而更適用于后臺(tái)批處理任務(wù)裳擎,比如群發(fā)涎永。search_after 提供了一個(gè)實(shí)時(shí)的光標(biāo)來(lái)避免深度分頁(yè)的問(wèn)題,其思想是使用前一頁(yè)的結(jié)果來(lái)幫助檢索下一頁(yè)鹿响。search_after不能自由跳到一個(gè)隨機(jī)頁(yè)面羡微,只能按照 sort values 跳轉(zhuǎn)到下一頁(yè)。使用 search_after 參數(shù)的時(shí)候惶我,from參數(shù)必須被設(shè)置成 0 或 -1 (當(dāng)然你也可以不設(shè)置這個(gè)from參數(shù))
search_after 需要使用一個(gè)唯一值的字段作為排序字段妈倔,否則不能使用search_after方法
推薦使用_uid 作為唯一值的排序字段。
GET twitter/_search
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
}
在下一次查詢(xún)的時(shí)候講返回的最后的一條數(shù)據(jù)的sort的數(shù)組放放到search_after中绸贡。
GET twitter/_search
{
"size": 10,
"query": {
"match" : {
"title" : "elasticsearch"
}
},
"search_after": [1463538857, "654323"],
"sort": [
{"date": "asc"},
{"tie_breaker_id": "asc"}
]
}
總結(jié)
- 深度分頁(yè)不管是關(guān)系型數(shù)據(jù)庫(kù)還是Elasticsearch還是其他搜索引擎盯蝴,都會(huì)帶來(lái)巨大性能開(kāi)銷(xiāo),特別是在分布式情況下听怕。
- 有些問(wèn)題可以考業(yè)務(wù)解決而不是靠技術(shù)解決捧挺,比如很多業(yè)務(wù)都對(duì)頁(yè)碼有限制,google 搜索尿瞭,往后翻到一定頁(yè)碼就不行了闽烙。
- scroll 并不適合用來(lái)做實(shí)時(shí)搜索,而更適用于后臺(tái)批處理任務(wù)声搁,比如群發(fā)黑竞。
- search_after不能自由跳到一個(gè)隨機(jī)頁(yè)面,只能按照 sort values 跳轉(zhuǎn)到下一頁(yè)疏旨。
排序與相關(guān)性
默認(rèn)情況下很魂,返回的結(jié)果是按照 相關(guān)性 進(jìn)行排序的——最相關(guān)的文檔排在最前。每個(gè)文檔都有相關(guān)性評(píng)分檐涝,用一個(gè)正浮點(diǎn)數(shù)字段 _score
來(lái)表示 莫换。 _score
的評(píng)分越高霞玄,相關(guān)性越高。
查詢(xún)語(yǔ)句會(huì)為每個(gè)文檔生成一個(gè) _score
字段拉岁。評(píng)分的計(jì)算方式取決于查詢(xún)類(lèi)型 不同的查詢(xún)語(yǔ)句用于不同的目的: fuzzy
查詢(xún)會(huì)計(jì)算與關(guān)鍵詞的拼寫(xiě)相似程度坷剧,terms
查詢(xún)會(huì)計(jì)算 找到的內(nèi)容與關(guān)鍵詞組成部分匹配的百分比,但是通常我們說(shuō)的 relevance 是我們用來(lái)計(jì)算全文本字段的值相對(duì)于全文本檢索詞相似程度的算法喊暖。
具體score算法可以到官網(wǎng)查詢(xún)惫企。
在代碼中設(shè)置:
// 設(shè)置是否按查詢(xún)匹配度排序
searchRequestBuilder.setExplain(true);
注意:
相關(guān)項(xiàng)排序消耗資源非常大,如果不是對(duì)文本精確度要求特別高的情況下陵叽,生產(chǎn)環(huán)境不建議按相關(guān)性排序狞尔。
參考:
https://www.elastic.co/guide/en/elasticsearch/reference/6.7/search-request-search-after.html
https://blog.csdn.net/yiyiholic/article/details/81661919
https://www.souyunku.com/2017/11/06/ElasticSearch-example/
https://www.cnblogs.com/yangzhenlong/p/9118089.html
https://www.elastic.co/guide/cn/elasticsearch/guide/current/relevance-intro.html
歡迎關(guān)注公眾號(hào):