SpringBoot鏈接Elasticsearch實戰總結

記一次線上的elasticsearch查詢採坑html

第一次使用elasticsearch,因而從網上找輪子複製粘貼。早好輪子測試完畢,上線。但是幾天下來發現接口響應時間一直都偏高(默認的超時時間是500ms),因此就不停的對代碼優化,縮短期。可是到最後代碼已經不能再優化了,響應時間依然沒有明顯的降低趨勢,甚至在高峯期會嚴重超時。接下來會慢慢講解elasticsearch使用優化。java

Spring Boot添加elasticsearch依賴

有不少種方案能夠選擇,1)添加spring的data依賴。2)使用elasticsearch提供的client依賴。3)使用jestClient依賴。前兩種並無什麼區別,第三種是經過http請求訪問elasticsearch的。mysql

使用elasticsearch官方依賴

使用IDE初始化Springboot時勾選elasticsearch便可,或者你也能夠直接添加以下依賴:web

<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-elasticsearch</artifactId>
  <version>{elasticserch.version}</version>
		</dependency>
複製代碼

或者到maven網站查找對應elasticsearch版本的依賴:算法

<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版本一直的依賴,不然可能會出錯spring

elasticsearch的配置sql

@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初始化開始。。。。。");
        TransportClient transportClient = null;
        try {
            // 配置信息
            Settings esSetting = Settings.builder()
                    //集羣名字
                    .put("cluster.name", clusterName)
                    //增長嗅探機制,找到ES集羣
                    .put("client.transport.sniff", true)
// .put("client.transport.ignore_cluster_name", true)
                    //增長線程池個數,暫時設爲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自定義內容時會不同。使用elasticsearch節點鏈接的端口是9300。數據庫

簡單的使用:json

@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();
            // 設置高亮字段
            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) {
            // 解析對象
            return setSearchResponse(searchResponse, highlightField);
        }

        return null;

    }
複製代碼

使用JestClient

添加maven依賴(這裏的elasticsearch版本比較低,並且尚未開放9300端口,只能使用http請求)api

<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的依賴,使用其9200端口。

配置文件就比較簡單了:

@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();
    }
}
複製代碼

建立一個JestClientFactory並配置httpClient。

簡單的一個例子:

@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);
            // 按在線時間排序,先按時間再按距離排序
            FieldSortBuilder sortBuilderField = SortBuilders.fieldSort("time").order(SortOrder.DESC);
            // 按距離排序,爲返回客戶端距離,返回的單位:米
            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是查詢的elasticsearch的語句,若是你知道elasticsearch的語法也能夠直接寫一個json代替。

距離排序

在jestClient中有一個按距離和時間排序的例子,是先按時間排序再按距離排序,目的是返回距離。es是能夠按多個字段排序的,靠前的爲優先匹配排序,最後的排序結果會在返回的sort數組中返回,數組中的位置即排序的匹配位置,我這裏將返回的距離提取出來放到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計算距離也能夠用他提供的方法本身計算,前提知道兩者的經緯度,調用GeoDistance的calculate方法,具體使用的精確度能夠按照業務要求選擇,不過我有作過測試,本身計算距離和elasticsearch計算的耗時幾乎相差很少,若是是額外的計算距離能夠再也不查一遍elasticsearch減小io消耗。

分頁

對於elasticsearch不太熟悉的同窗,分頁也是一個坑。

淺分頁

elasticsearch的的淺分頁from&size,from是查詢的索引位置,size是每頁數量,優勢相似於mysql的limit和start。

如今咱們能夠假設在一個有 5 個主分片的索引中搜索。 當咱們請求結果的第一頁(結果從 1 到 10 ),每個分片產生前 10 的結果,而且返回給 協調節點 ,協調節點對 50 個結果排序獲得所有結果的前 10 個。如今假設咱們請求第 1000 頁--結果從 10001 到 10010 。全部都以相同的方式工做除了每一個分片不得不產生前10010個結果之外。 而後協調節點對所有 50050 個結果排序最後丟棄掉這些結果中的 50040 個結果。能夠看到,在分佈式系統中,對結果排序的成本隨分頁的深度成指數上升。這就是 web 搜索引擎對任何查詢都不要返回超過 1000 個結果的緣由。你翻頁的時候,翻的越深,每一個 Shard 返回的數據就越多,並且協調節點處理的時間越長,很是坑爹。因此用 ES 作分頁的時候,你會發現越翻到後面,就越是慢。咱們以前也是遇到過這個問題,用 ES 做分頁,前幾頁就幾十毫秒,翻到 10 頁或者幾十頁的時候,基本上就要 5~10 秒才能查出來一頁數據了。

使用from&size的最大查詢量是10000條數據,這個值能夠在elasticsearch中配置文件中設置。

scroll 深分頁

爲了解決上面的問題,elasticsearch提出了一個scroll滾動的方式。scroll 相似於sql中的cursor,使用scroll,每次只能獲取一頁的內容,而後會返回一個scroll_id。根據返回的這個scroll_id能夠不斷地獲取下一頁的內容,因此scroll並不適用於有跳頁的情景.

POST /twitter/_search?scroll=1m
{
    "size": 100,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    }
}
複製代碼
  1. scroll=1m表示設置scroll_id保留1分鐘可用。
  2. 使用scroll必需要將from設置爲0。
  3. size決定後面每次調用_search搜索返回的數量
POST /_search/scroll 
{
    "scroll" : "1m", 
    "scroll_id" : "DXF1ZXJ5QW5kRmV0Y2gBAAAAAAAAAD4WYm9laVYtZndUQlNsdDcwakFMNjU1QQ==" 
}
複製代碼

而後咱們能夠經過數據返回的_scroll_id讀取下一頁內容,每次請求將會讀取下10條數據,直到數據讀取完畢或者scroll_id保留時間截止。請求的接口再也不使用索引名了,而是 _search/scroll,其中GET和POST方法均可以使用。

search_after

Scroll 被推薦用於深度查詢,可是contexts的代價是昂貴的,不推薦用於實時用戶請求,而更適用於後臺批處理任務,好比羣發。search_after 提供了一個實時的光標來避免深度分頁的問題,其思想是使用前一頁的結果來幫助檢索下一頁。search_after不能自由跳到一個隨機頁面,只能按照 sort values 跳轉到下一頁。使用 search_after 參數的時候,from參數必須被設置成 0 或 -1 (固然你也能夠不設置這個from參數)

search_after 須要使用一個惟一值的字段做爲排序字段,不然不能使用search_after方法 推薦使用_uid 做爲惟一值的排序字段。

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    },
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}      
    ]
}
複製代碼

在下一次查詢的時候講返回的最後的一條數據的sort的數組放放到search_after中。

GET twitter/_search
{
    "size": 10,
    "query": {
        "match" : {
            "title" : "elasticsearch"
        }
    },
    "search_after": [1463538857, "654323"],
    "sort": [
        {"date": "asc"},
        {"tie_breaker_id": "asc"}
    ]
}
複製代碼

總結

  • 深度分頁不論是關係型數據庫仍是Elasticsearch仍是其餘搜索引擎,都會帶來巨大性能開銷,特別是在分佈式狀況下。
  • 有些問題能夠考業務解決而不是靠技術解決,好比不少業務都對頁碼有限制,google 搜索,日後翻到必定頁碼就不行了。
  • scroll 並不適合用來作實時搜索,而更適用於後臺批處理任務,好比羣發。
  • search_after不能自由跳到一個隨機頁面,只能按照 sort values 跳轉到下一頁。

排序與相關性

默認狀況下,返回的結果是按照 相關性 進行排序的——最相關的文檔排在最前。每一個文檔都有相關性評分,用一個正浮點數字段 _score 來表示 。 _score 的評分越高,相關性越高。

查詢語句會爲每一個文檔生成一個 _score 字段。評分的計算方式取決於查詢類型 不一樣的查詢語句用於不一樣的目的: fuzzy 查詢會計算與關鍵詞的拼寫類似程度,terms 查詢會計算 找到的內容與關鍵詞組成部分匹配的百分比,可是一般咱們說的 relevance 是咱們用來計算全文本字段的值相對於全文本檢索詞類似程度的算法。

具體score算法能夠到官網查詢。

在代碼中設置:

// 設置是否按查詢匹配度排序
searchRequestBuilder.setExplain(true);
複製代碼

注意:

相關項排序消耗資源很是大,若是不是對文本精確度要求特別高的狀況下,生產環境不建議按相關性排序。

參考:

www.elastic.co/guide/en/el…

blog.csdn.net/yiyiholic/a…

www.souyunku.com/2017/11/06/…

www.cnblogs.com/yangzhenlon…

www.elastic.co/guide/cn/el…


歡迎關注公衆號:

公衆號微信
相關文章
相關標籤/搜索