1. 程式人生 > 實用技巧 >哇,ElasticSearch多欄位權重排序居然可以這麼玩

哇,ElasticSearch多欄位權重排序居然可以這麼玩

背景

讀者提問:ES 的權重排序有沒有示列,參考參考?

剛好之前也稍微接觸過,於是寫了這篇文章,可以簡單參考下。

在很多複雜的業務場景下,排序的規則會比較複雜,單一的降序,升序無法滿足日常需求。不過 ES 中提供了給文件加權重的方式來排序,還是挺好用的。

首先初始化三條測試資料,方便檢視效果:

{
	id: 1,
	title: "Java怎麼學",
	type: 3,
	userId: 1,
	tags: [
		"java"
	],
	textContent: "我要學Java",
	status: 1,
	heat: 80
}
{
	id: 2,
	title: "Java怎麼學",
	type: 2,
	userId: 1,
	tags: [
		"java"
	],
	textContent: "我要學Java",
	status: 1,
	heat: 99
}
{
	id: 3,
	title: "Java怎麼學",
	type: 1,
	userId: 1,
	tags: [
		"java"
	],
	textContent: "我要學Java",
	status: 1,
	heat: 100
}

type:1 為翻譯,2 為轉載,3 為原創

需求是查詢 userId=1 的所有文章,按照熱度降序排序,但是原創型別的文章要顯示在前面,優先順序高於熱度。

如果我們簡單的按照熱度排序的話,那麼順序肯定是 id 為 3(熱度:100),2(熱度:99),1(熱度:80)這樣排列的。

但是原創型別的要在前面,那麼結果應該是 1(熱度:80,型別:原創),3(熱度:100,型別:翻譯),2(熱度:99,型別:轉載)。

排序條件肯定是以熱度來進行的,這個是肯定的。唯一需要處理的就是怎麼將原創型別的排在前面,如果只考慮實現,方式還是有很多種的。

比如:原創型別的熱度值可以調的比較高,但是呢,熱度值要重新弄一個欄位,只用於排序,給使用者展示的還是之前的熱度值,這樣排序就簡單了,還是根據熱度排就可以實現效果。

weightFactorFunction

在 ES 搜尋結果中_score 這個欄位相信大家並不陌生,這是 ES 給出的評分,我們可以根據評分來排序,然後將原創型別的評分提高就可以實現想要的效果。

直接看 Java 程式碼吧,通過 FunctionScoreQueryBuilder 來構建查詢。

@Test
public void testSort() {
    FunctionScoreQueryBuilder.FilterFunctionBuilder[] filterFunctionBuilders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("type", 3), ScoreFunctionBuilders.weightFactorFunction(100)),
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.termQuery("type", 2), ScoreFunctionBuilders.weightFactorFunction(1)),
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchQuery("type", 1), ScoreFunctionBuilders.weightFactorFunction(1))
    };
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    boolQuery.must(QueryBuilders.termQuery("userId", 1));
    FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(boolQuery, filterFunctionBuilders);
    searchSourceBuilder.query(functionScoreQueryBuilder)
            .sort("_score", SortOrder.DESC)
            .sort("heat", SortOrder.DESC);
    SearchRequest searchRequest = new SearchRequest(elasticSearchIndexConfig.getArticleSearchIndexName());
    searchRequest.types(EsConstant.DEFAULT_TYPE);
    searchRequest.source(searchSourceBuilder);

    List<ArticleDocument> searchResults = kittyRestHighLevelClient.search(searchRequest, ArticleDocument.class);
    searchResults.forEach(doc -> {
        System.out.println(doc.getId() + "\t" + doc.getType() + "\t" + doc.getHeat());
    });
}

通過 ScoreFunctionBuilders.weightFactorFunction 為文章型別設定對應的權重,原創文章權重為 100,其他的都為 1,這樣原創文章的得分就高於其他型別的文章。

在排序的時候優先得分排序,然後熱度排序。就可以得到我們想要的結果了。

scriptFunction

除了使用 weightFactorFunction 來設定權重,另外介紹一種靈活度更高,適用於更復雜的排序場景的方式 scriptFunction。

scriptFunction 允許我們通過指令碼的方式來實現權重,直接看程式碼:

@Test
public void testSort() {
    String scoreScript = "if (doc['type'].value == 3) {" +
            "   return 100;" +
            "} else {" +
            "   return 1;" +
            "}";
    FunctionScoreQueryBuilder.FilterFunctionBuilder[] filterFunctionBuilders = new FunctionScoreQueryBuilder.FilterFunctionBuilder[]{
            new FunctionScoreQueryBuilder.FilterFunctionBuilder(QueryBuilders.matchAllQuery(), ScoreFunctionBuilders.scriptFunction(new Script(scoreScript)))
    };
    SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
    boolQuery.must(QueryBuilders.termQuery("userId", 1));
    FunctionScoreQueryBuilder functionScoreQueryBuilder = QueryBuilders.functionScoreQuery(boolQuery, filterFunctionBuilders);
    searchSourceBuilder.query(functionScoreQueryBuilder)
            .sort("_score", SortOrder.DESC)
            .sort("heat", SortOrder.DESC);
    SearchRequest searchRequest = new SearchRequest(elasticSearchIndexConfig.getArticleSearchIndexName());
    searchRequest.types(EsConstant.DEFAULT_TYPE);
    searchRequest.source(searchSourceBuilder);

    List<ArticleDocument> searchResults = kittyRestHighLevelClient.search(searchRequest, ArticleDocument.class);
    searchResults.forEach(doc -> {
        System.out.println(doc.getId() + "\t" + doc.getType() + "\t" + doc.getHeat());
    });
}

scoreScript 就是控制權重的指令碼,也就是一段程式碼(指令碼預設是 groovy),是不是方便的多。

關於作者:尹吉歡,簡單的技術愛好者,《Spring Cloud 微服務-全棧技術與案例解析》, 《Spring Cloud 微服務 入門 實戰與進階》作者, 公眾號猿天地發起人。

我整理了一份很全的學習資料,感興趣的可以微信搜尋「猿天地」,回覆關鍵字 「學習資料」獲取我整理好了的 Spring Cloud,Spring Cloud Alibaba,Sharding-JDBC 分庫分表,任務排程框架 XXL-JOB,MongoDB,爬蟲等相關資料。