diff --git a/src/main/java/com/vincent/es/util/SearchInfo.java b/src/main/java/com/vincent/es/util/SearchInfo.java index d155647..4724bb3 100644 --- a/src/main/java/com/vincent/es/util/SearchInfo.java +++ b/src/main/java/com/vincent/es/util/SearchInfo.java @@ -1,16 +1,22 @@ package com.vincent.es.util; import co.elastic.clients.elasticsearch._types.SortOptions; -import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; -import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import co.elastic.clients.elasticsearch._types.query_dsl.*; +import org.springframework.util.CollectionUtils; import java.util.List; public class SearchInfo { - private BoolQuery boolQuery; // 查詢條件 - private List sortOptions = List.of(); // 排序方式 - private Integer from; // 資料的跳過數量 - private Integer size; // 資料的擷取數量 + private BoolQuery boolQuery; // 查詢條件 + private List functionScores = List.of(); // 計分函數 + private List sortOptions = List.of(); // 排序方式 + private Integer from; // 資料的跳過數量 + private Integer size; // 資料的擷取數量 + + public SearchInfo() { + var matchAll = MatchAllQuery.of(b -> b)._toQuery(); + this.boolQuery = BoolQuery.of(b -> b.filter(matchAll)); + } public static SearchInfo of(BoolQuery bool) { var info = new SearchInfo(); @@ -32,6 +38,14 @@ public void setBoolQuery(BoolQuery boolQuery) { this.boolQuery = boolQuery; } + public List getFunctionScores() { + return functionScores; + } + + public void setFunctionScores(List functionScores) { + this.functionScores = functionScores; + } + public List getSortOptions() { return sortOptions; } @@ -58,6 +72,17 @@ public void setSize(Integer size) { // library 使用 Query 類別當作條件的傳遞介面 public Query toQuery() { - return boolQuery._toQuery(); + if (CollectionUtils.isEmpty(functionScores)) { + return boolQuery._toQuery(); + } + + return new FunctionScoreQuery.Builder() + .query(boolQuery._toQuery()) + .functions(functionScores) + .scoreMode(FunctionScoreMode.Sum) + .boostMode(FunctionBoostMode.Replace) + .maxBoost(30.0) + .build() + ._toQuery(); } } diff --git a/src/main/java/com/vincent/es/util/SearchUtils.java b/src/main/java/com/vincent/es/util/SearchUtils.java index 750a3bd..777e578 100644 --- a/src/main/java/com/vincent/es/util/SearchUtils.java +++ b/src/main/java/com/vincent/es/util/SearchUtils.java @@ -30,7 +30,7 @@ public static Query createTermQuery(String field, Object value) { .value((int) value); // 此方法接受 long 型態 } else if (value instanceof String){ builder - .field(field + ".keyword") + .field(field) .value((String) value); } else { throw new UnsupportedOperationException("Please implement for other type additionally."); @@ -188,4 +188,109 @@ public static SortOptions createSortOption(String field, SortOrder order, SortMo public static SortOptions createSortOption(String field, SortOrder order) { return createSortOption(field, order, null); } -} + + /** + *
+     *     {
+     *         "field_value_factor": {
+     *             "field": {@param field},
+     *             "factor": {@param factor},
+     *             "modifier": {@param modifier},
+     *             "missing": {@param missing}
+     *         }
+     *     }
+     * 
+ */ + public static FunctionScore createFieldValueFactor( + String field, Double factor, FieldValueFactorModifier modifier, Double missing) { + + return new FieldValueFactorScoreFunction.Builder() + .field(field) + .factor(factor) + .modifier(modifier) + .missing(missing) + .build() + ._toFunctionScore(); + } + + /** + *
+     *     {
+     *         "filter": {@param query},
+     *         "weight": {@param weight}
+     *     }
+     * 
+ */ + public static FunctionScore createConditionalWeightFunctionScore(Query query, Double weight) { + return new FunctionScore.Builder() + .filter(query) + .weight(weight) + .build(); + } + + /** + *
+     *     {
+     *         "field_value_factor": {@param function},
+     *         "weight": {@param weight}
+     *     }
+     * 
+ */ + public static FunctionScore createWeightedFieldValueFactor( + FieldValueFactorScoreFunction function, Double weight) { + + return new FunctionScore.Builder() + .fieldValueFactor(function) + .weight(weight) + .build(); + } + + /** + *
+     *     {
+     *         "gauss": {@param placement}
+     *     }
+     * 
+ */ + public static FunctionScore createGaussFunction(String field, DecayPlacement placement) { + var decayFunction = new DecayFunction.Builder() + .field(field) + .placement(placement) + .build(); + return new FunctionScore.Builder() + .gauss(decayFunction) + .build(); + } + + /** + *
+     *     {
+     *         "origin": {@param origin},
+     *         "offset": {@param offset},
+     *         "scale": {@param scale},
+     *         "decay": {@param decay}
+     *     }
+     * 
+ */ + public static DecayPlacement createDecayPlacement( + Number origin, Number offset, Number scale, Double decay) { + + return new DecayPlacement.Builder() + .origin(JsonData.of(origin)) + .offset(JsonData.of(offset)) + .scale(JsonData.of(scale)) + .decay(decay) + .build(); + } + + public static DecayPlacement createDecayPlacement( + String originExp, String offsetExp, String scaleExp, Double decay) { + + return new DecayPlacement.Builder() + .origin(JsonData.of(originExp)) + .offset(JsonData.of(offsetExp)) + .scale(JsonData.of(scaleExp)) + .decay(decay) + .build(); + } +} \ No newline at end of file diff --git a/src/test/java/com/vincent/es/SearchTests.java b/src/test/java/com/vincent/es/SearchTests.java index 289962b..6e34996 100644 --- a/src/test/java/com/vincent/es/SearchTests.java +++ b/src/test/java/com/vincent/es/SearchTests.java @@ -3,6 +3,7 @@ import co.elastic.clients.elasticsearch._types.SortMode; import co.elastic.clients.elasticsearch._types.SortOrder; import co.elastic.clients.elasticsearch._types.query_dsl.BoolQuery; +import co.elastic.clients.elasticsearch._types.query_dsl.FieldValueFactorModifier; import co.elastic.clients.elasticsearch._types.query_dsl.MatchAllQuery; import com.vincent.es.entity.Student; import com.vincent.es.repository.StudentEsRepository; @@ -162,17 +163,104 @@ public void testPaging() { assertDocumentIds(false, students, "101", "102"); } + @Test + public void testFunctionScore_FieldValueFactor() { + var fieldValueFactorScore = SearchUtils + .createFieldValueFactor("grade", 0.5, FieldValueFactorModifier.Square, 0.0); + + var searchInfo = new SearchInfo(); + searchInfo.setFunctionScores(List.of(fieldValueFactorScore)); + + var students = repository.find(searchInfo); + + // Dora (4.0) -> Mario (2.25) -> Vincent (1.0) -> Winnie (0.25) + assertDocumentIds(students, "101", "102", "103", "104"); + } + + @Test + public void testFunctionScore_ConditionalWeight() { + var departmentQuery = SearchUtils + .createTermQuery("departments.keyword", "財務金融"); + var departmentScore = SearchUtils + .createConditionalWeightFunctionScore(departmentQuery, 3.0); + + var courseQuery = SearchUtils + .createTermQuery("courses.name.keyword", "程式設計"); + var courseScore = SearchUtils + .createConditionalWeightFunctionScore(courseQuery, 1.5); + + var fieldValueFactorScore = SearchUtils + .createFieldValueFactor("grade", 1.0, FieldValueFactorModifier.None, 0.0); + var gradeScore = SearchUtils + .createWeightedFieldValueFactor(fieldValueFactorScore.fieldValueFactor(), 0.5); + + var searchInfo = new SearchInfo(); + searchInfo.setFunctionScores(List.of( + departmentScore, + courseScore, + gradeScore + )); + + var students = repository.find(searchInfo); + + // Vincent (5.5) -> Dora (5.0) -> Mario (1.5) -> Winnie (0.5) + assertDocumentIds(students, "103", "101", "102", "104"); + } + + @Test + public void testFunctionScore_DecayFunction_Number() { + var placement = SearchUtils + .createDecayPlacement(100, 15, 10, 0.5); + var decayFunctionScore = SearchUtils + .createGaussFunction("conductScore", placement); + + var searchInfo = new SearchInfo(); + searchInfo.setFunctionScores(List.of(decayFunctionScore)); + + var students = repository.find(searchInfo); + + // Vincent (1.0) -> Mario (0.9726) -> Dora (0.4322) -> Winnie (0.2570) + assertDocumentIds(students, "103", "102", "101", "104"); + } + + @Test + public void testFunctionScore_DecayFunction_Date() { + var placement = SearchUtils + .createDecayPlacement("now", "90d", "270d", 0.5); + var decayFunctionScore = SearchUtils + .createGaussFunction("englishIssuedDate", placement); + + var searchInfo = new SearchInfo(); + searchInfo.setFunctionScores(List.of(decayFunctionScore)); + + var students = repository.find(searchInfo); + + // Mario -> Winnie -> Dora -> Vincent + assertDocumentIds(students, "102", "104", "101", "103"); + } + private void assertDocumentIds(boolean ignoreOrder, List actualDocs, String... expectedIdArray) { + if (!ignoreOrder) { + assertDocumentIds(actualDocs, expectedIdArray); + return; + } + var expectedIds = List.of(expectedIdArray); var actualIds = actualDocs.stream() .map(Student::getId) .collect(Collectors.toList()); - if (ignoreOrder) { - assertTrue(expectedIds.containsAll(actualIds)); - assertTrue(actualIds.containsAll(expectedIds)); - } else { - assertEquals(expectedIds, actualIds); - } + assertTrue(expectedIds.containsAll(actualIds)); + assertTrue(actualIds.containsAll(expectedIds)); } + + private void assertDocumentIds(List actualDocs, String... expectedIdArray) { + var expectedIds = List.of(expectedIdArray); + var actualIds = actualDocs.stream() + .map(Student::getId) + .collect(Collectors.toList()); + + assertEquals(expectedIds, actualIds); + } + }