From 51eb6e8e3a81de0367a090a21850e9812499d605 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sun, 16 Oct 2022 16:02:47 +0800 Subject: [PATCH 1/3] Finish part 2. Provide util methods to provide query condition. Provide some test cases. --- .../es/repository/StudentEsRepository.java | 27 +++ .../java/com/vincent/es/util/SampleData.java | 16 ++ .../java/com/vincent/es/util/SearchInfo.java | 58 +++++++ .../java/com/vincent/es/util/SearchUtils.java | 159 ++++++++++++++++++ .../java/com/vincent/es/ApplicationTests.java | 13 -- src/test/java/com/vincent/es/SearchTests.java | 154 +++++++++++++++++ 6 files changed, 414 insertions(+), 13 deletions(-) create mode 100644 src/main/java/com/vincent/es/util/SampleData.java create mode 100644 src/main/java/com/vincent/es/util/SearchInfo.java create mode 100644 src/main/java/com/vincent/es/util/SearchUtils.java delete mode 100644 src/test/java/com/vincent/es/ApplicationTests.java create mode 100644 src/test/java/com/vincent/es/SearchTests.java diff --git a/src/main/java/com/vincent/es/repository/StudentEsRepository.java b/src/main/java/com/vincent/es/repository/StudentEsRepository.java index 40af134..3e8e2e5 100644 --- a/src/main/java/com/vincent/es/repository/StudentEsRepository.java +++ b/src/main/java/com/vincent/es/repository/StudentEsRepository.java @@ -8,15 +8,18 @@ import co.elastic.clients.elasticsearch.core.bulk.BulkOperation; import co.elastic.clients.elasticsearch.core.bulk.BulkResponseItem; import co.elastic.clients.elasticsearch.core.bulk.CreateOperation; +import co.elastic.clients.elasticsearch.core.search.Hit; import co.elastic.clients.elasticsearch.indices.CreateIndexRequest; import co.elastic.clients.elasticsearch.indices.DeleteIndexRequest; import com.vincent.es.entity.Student; import com.vincent.es.util.IOSupplier; +import com.vincent.es.util.SearchInfo; import java.io.IOException; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.stream.Collectors; public class StudentEsRepository { private final ElasticsearchClient client; @@ -120,6 +123,30 @@ public void deleteById(String id) { execute(() -> client.delete(request)); } + @SuppressWarnings({"squid:S112"}) + public List find(SearchInfo info) { + var request = new SearchRequest.Builder() + .index(indexName) + .query(info.getBoolQuery()._toQuery()) + .sort(info.getSortOptions()) + .from(info.getFrom()) + .size(info.getSize()) + .build(); + + try { + var searchResponse = client.search(request, Student.class); + return searchResponse + .hits() + .hits() + .stream() + .map(Hit::source) + .collect(Collectors.toList()); + } catch (IOException e) { + // 範例為求方便,只簡單做例外處理 + throw new RuntimeException(e); + } + } + private Map getPropertyMappings() { var englishIssuedDateProperty = DateProperty.of(b -> b)._toProperty(); return Map.of("englishIssuedDate", englishIssuedDateProperty); diff --git a/src/main/java/com/vincent/es/util/SampleData.java b/src/main/java/com/vincent/es/util/SampleData.java new file mode 100644 index 0000000..5f1622e --- /dev/null +++ b/src/main/java/com/vincent/es/util/SampleData.java @@ -0,0 +1,16 @@ +package com.vincent.es.util; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.vincent.es.entity.Student; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class SampleData { + public static List get() throws IOException { + var file = new File("students.json"); + return new ObjectMapper().readValue(file, new TypeReference<>() {}); + } +} diff --git a/src/main/java/com/vincent/es/util/SearchInfo.java b/src/main/java/com/vincent/es/util/SearchInfo.java new file mode 100644 index 0000000..acf5257 --- /dev/null +++ b/src/main/java/com/vincent/es/util/SearchInfo.java @@ -0,0 +1,58 @@ +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 java.util.List; + +public class SearchInfo { + private BoolQuery boolQuery; + private List sortOptions; + private Integer from; + private Integer size; + + public static SearchInfo of(BoolQuery bool) { + var info = new SearchInfo(); + info.boolQuery = bool; + + return info; + } + + public static SearchInfo of(Query query) { + var bool = BoolQuery.of(b -> b.filter(query)); + return of(bool); + } + + public BoolQuery getBoolQuery() { + return boolQuery; + } + + public void setBoolQuery(BoolQuery boolQuery) { + this.boolQuery = boolQuery; + } + + public List getSortOptions() { + return sortOptions; + } + + public void setSortOptions(List sortOptions) { + this.sortOptions = sortOptions; + } + + public Integer getFrom() { + return from; + } + + public void setFrom(Integer from) { + this.from = from; + } + + public Integer getSize() { + return size; + } + + public void setSize(Integer size) { + this.size = size; + } +} diff --git a/src/main/java/com/vincent/es/util/SearchUtils.java b/src/main/java/com/vincent/es/util/SearchUtils.java new file mode 100644 index 0000000..bdc7069 --- /dev/null +++ b/src/main/java/com/vincent/es/util/SearchUtils.java @@ -0,0 +1,159 @@ +package com.vincent.es.util; + +import co.elastic.clients.elasticsearch._types.*; +import co.elastic.clients.elasticsearch._types.query_dsl.*; +import co.elastic.clients.json.JsonData; + +import java.util.Collection; +import java.util.Date; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +public class SearchUtils { + private SearchUtils() {} + + /** + *
+     *     {
+     *         "term": {
+     *             "{field}": {value}
+     *         }
+     *     }
+     * 
+ */ + public static Query createTermQuery(String field, Object value) { + var builder = new TermQuery.Builder(); + if (value instanceof Integer) { + builder + .field(field) + .value((int) value); // 此方法接受 long 型態 + } else if (value instanceof String){ + builder + .field(field + ".keyword") + .value((String) value); + } else { + throw new UnsupportedOperationException("Please implement for other type additionally."); + } + + return builder.build()._toQuery(); + } + + /** + *
+     *     {
+     *         "terms": {
+     *             "{field}": [{values}[0], {values}[1], ...]
+     *         }
+     *     }
+     * 
+ */ + public static Query createTermsQuery(String field, Collection values) { + var elem = values.stream().findAny().orElseThrow(); + + Stream fieldValueStream; + if (elem instanceof Integer) { + fieldValueStream = values.stream() + .map(value -> FieldValue.of(b -> b.longValue((int) value))); + } else if (elem instanceof String) { + field = field + ".keyword"; + fieldValueStream = values.stream() + .map(value -> FieldValue.of(b -> b.stringValue((String) value))); + } else { + throw new UnsupportedOperationException("Please implement for other type additionally."); + } + + var fieldValues = fieldValueStream.collect(Collectors.toList()); + var termsQueryField = TermsQueryField.of(b -> b.value(fieldValues)); + + return new TermsQuery.Builder() + .field(field) + .terms(termsQueryField) + .build() + ._toQuery(); + } + + /** + *
+     *     {
+     *         "range": {
+     *             "{field}": {
+     *                 "gte": "{gte}",
+     *                 "lte": "{lte}"
+     *             }
+     *         }
+     *     }
+     * 
+ */ + public static Query createRangeQuery(String field, Number gte, Number lte) { + var builder = new RangeQuery.Builder().field(field); + + if (gte != null) { + builder.gte(JsonData.of(gte)); + } + + if (lte != null) { + builder.lte(JsonData.of(lte)); + } + + return builder.build()._toQuery(); + } + + public static Query createRangeQuery(String field, Date gte, Date lte) { + var builder = new RangeQuery.Builder().field(field); + + if (gte != null) { + builder.gte(JsonData.of(gte)); + } + + if (lte != null) { + builder.lte(JsonData.of(lte)); + } + + return builder.build()._toQuery(); + } + + /** + *
+     *     {
+     *         "bool": {
+     *             "should": [
+     *                 {
+     *                     "match": { "{fields}[0]": "{searchText}" }
+     *                 },
+     *                 {
+     *                     "match": { "{fields}[1]": "{searchText}" }
+     *                 }
+     *             ]
+     *         }
+     *     }
+     * 
+ */ + public static Query createMatchQuery(Set fields, String searchText) { + var bool = new BoolQuery.Builder(); + fields.stream() + .map(field -> { + var matchQuery = new MatchQuery.Builder() + .field(field) + .query(searchText) + .build(); + return matchQuery._toQuery(); + }) + .forEach(bool::should); + + return bool.build()._toQuery(); + } + + public static SortOptions createSortOption(String field, SortOrder order, SortMode mode) { + var fieldSort = new FieldSort.Builder() + .field(field) + .order(order) + .mode(mode) + .build(); + return SortOptions.of(b -> b.field(fieldSort)); + } + + public static SortOptions createSortOption(String field, SortOrder order) { + return createSortOption(field, order, null); + } +} diff --git a/src/test/java/com/vincent/es/ApplicationTests.java b/src/test/java/com/vincent/es/ApplicationTests.java deleted file mode 100644 index 31d8ca4..0000000 --- a/src/test/java/com/vincent/es/ApplicationTests.java +++ /dev/null @@ -1,13 +0,0 @@ -package com.vincent.es; - -import org.junit.jupiter.api.Test; -import org.springframework.boot.test.context.SpringBootTest; - -@SpringBootTest -class ApplicationTests { - - @Test - void contextLoads() { - } - -} diff --git a/src/test/java/com/vincent/es/SearchTests.java b/src/test/java/com/vincent/es/SearchTests.java new file mode 100644 index 0000000..e31304a --- /dev/null +++ b/src/test/java/com/vincent/es/SearchTests.java @@ -0,0 +1,154 @@ +package com.vincent.es; + +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.MatchAllQuery; +import com.vincent.es.entity.Student; +import com.vincent.es.repository.StudentEsRepository; +import com.vincent.es.util.SampleData; +import com.vincent.es.util.SearchInfo; +import com.vincent.es.util.SearchUtils; +import org.junit.Before; +import org.junit.jupiter.api.Test; +import org.junit.runner.RunWith; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.junit4.SpringRunner; + +import java.io.IOException; +import java.text.ParseException; +import java.text.SimpleDateFormat; +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(SpringRunner.class) +@SpringBootTest +public class SearchTests { + + @Autowired + private StudentEsRepository repository; + + @Before + public void setup() throws IOException { + repository.init(); + + var documents = SampleData.get(); + repository.insert(documents); + } + + @Test + public void testTerm_NumberField() { + var query = SearchUtils.createTermQuery("grade", 3); + var searchInfo = SearchInfo.of(query); + + var students = repository.find(searchInfo); + + // Mario + assertDocumentIds(true, students, "102"); + } + + @Test + public void testTerms_TextField() { + var values = List.of("資訊管理", "企業管理"); + var query = SearchUtils.createTermsQuery("departments.keyword", values); + var searchInfo = SearchInfo.of(query); + + var students = repository.find(searchInfo); + + // Vincent, Winnie + assertDocumentIds(true, students, "103", "104"); + } + + @Test + public void testNumberRange() { + var query = SearchUtils.createRangeQuery("grade", 2, 4); + var searchInfo = SearchInfo.of(query); + + var students = repository.find(searchInfo); + + // Dora, Mario, Vincent + assertDocumentIds(true, students, "101", "102", "103"); + } + + @Test + public void testDateRange() throws ParseException { + var sdf = new SimpleDateFormat("yyyy-MM-dd"); + var fromDate = sdf.parse("2021-07-01"); + var toDate = sdf.parse("2022-06-30"); + + var query = SearchUtils.createRangeQuery("englishIssuedDate", fromDate, toDate); + var searchInfo = SearchInfo.of(query); + + var students = repository.find(searchInfo); + + // Winnie, Mario + assertDocumentIds(true, students, "102", "104"); + } + + @Test + public void testFullTextSearch() { + var fields = Set.of("name", "introduction"); + var searchText = "vincent career"; + + var query = SearchUtils.createMatchQuery(fields, searchText); + var bool = BoolQuery.of(b -> b.must(query)); + var searchInfo = SearchInfo.of(bool); + + var students = repository.find(searchInfo); + + // Winnie, Vincent + assertDocumentIds(true, students, "103", "104"); + } + + @Test + public void testSortByMultipleFields() { + var coursePointSort = SearchUtils.createSortOption("courses.point", SortOrder.Desc, SortMode.Max); + var nameSort = SearchUtils.createSortOption("name.keyword", SortOrder.Asc); + + var query = MatchAllQuery.of(b -> b)._toQuery(); + + var searchInfo = SearchInfo.of(query); + searchInfo.setSortOptions(List.of(coursePointSort, nameSort)); + + var students = repository.find(searchInfo); + + // Mario -> Vincent -> Dora -> Winnie + assertDocumentIds(false, students, "102", "103", "101", "104"); + } + + @Test + public void testPaging() { + var gradeSort = SearchUtils.createSortOption("grade", SortOrder.Desc); + + var query = MatchAllQuery.of(b -> b)._toQuery(); + + var searchInfo = SearchInfo.of(query); + searchInfo.setSortOptions(List.of(gradeSort)); + searchInfo.setFrom(0); + searchInfo.setSize(2); + + var students = repository.find(searchInfo); + + // Dora -> Mario + assertDocumentIds(false, students, "101", "102"); + } + + private void assertDocumentIds(boolean ignoreOrder, List actualDocs, String... expectedIdArray) { + 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); + } + } +} From 3cb5d5062e8a7ca646b868c2aa23b0ede75011e4 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sat, 22 Oct 2022 16:28:31 +0800 Subject: [PATCH 2/3] Update comment. Refactor exception handle. Convert search info to query. --- .../es/repository/StudentEsRepository.java | 10 ++---- .../java/com/vincent/es/util/SearchInfo.java | 13 ++++--- .../java/com/vincent/es/util/SearchUtils.java | 34 ++++++++++++++----- src/test/java/com/vincent/es/SearchTests.java | 6 ++-- 4 files changed, 41 insertions(+), 22 deletions(-) diff --git a/src/main/java/com/vincent/es/repository/StudentEsRepository.java b/src/main/java/com/vincent/es/repository/StudentEsRepository.java index 0a9bb03..3c79b97 100644 --- a/src/main/java/com/vincent/es/repository/StudentEsRepository.java +++ b/src/main/java/com/vincent/es/repository/StudentEsRepository.java @@ -123,17 +123,16 @@ public void deleteById(String id) { execute(() -> client.delete(request)); } - @SuppressWarnings({"squid:S112"}) public List find(SearchInfo info) { var request = new SearchRequest.Builder() .index(indexName) - .query(info.getBoolQuery()._toQuery()) + .query(info.toQuery()) .sort(info.getSortOptions()) .from(info.getFrom()) .size(info.getSize()) .build(); - try { + return execute(() -> { var searchResponse = client.search(request, Student.class); return searchResponse .hits() @@ -141,10 +140,7 @@ public List find(SearchInfo info) { .stream() .map(Hit::source) .collect(Collectors.toList()); - } catch (IOException e) { - // 範例為求方便,只簡單做例外處理 - throw new RuntimeException(e); - } + }); } private Map getPropertyMappings() { diff --git a/src/main/java/com/vincent/es/util/SearchInfo.java b/src/main/java/com/vincent/es/util/SearchInfo.java index acf5257..7f751d3 100644 --- a/src/main/java/com/vincent/es/util/SearchInfo.java +++ b/src/main/java/com/vincent/es/util/SearchInfo.java @@ -7,10 +7,10 @@ import java.util.List; public class SearchInfo { - private BoolQuery boolQuery; - private List sortOptions; - private Integer from; - private Integer size; + private BoolQuery boolQuery; // 查詢條件 + private List sortOptions; // 排序方式 + private Integer from; // 資料的跳過數量 + private Integer size; // 資料的擷取數量 public static SearchInfo of(BoolQuery bool) { var info = new SearchInfo(); @@ -55,4 +55,9 @@ public Integer getSize() { public void setSize(Integer size) { this.size = size; } + + // library 使用 Query 類別當作條件的傳遞介面 + public Query toQuery() { + return boolQuery._toQuery(); + } } diff --git a/src/main/java/com/vincent/es/util/SearchUtils.java b/src/main/java/com/vincent/es/util/SearchUtils.java index bdc7069..12a2ff9 100644 --- a/src/main/java/com/vincent/es/util/SearchUtils.java +++ b/src/main/java/com/vincent/es/util/SearchUtils.java @@ -17,7 +17,7 @@ private SearchUtils() {} *
      *     {
      *         "term": {
-     *             "{field}": {value}
+     *             "{@param field}": {@param value}
      *         }
      *     }
      * 
@@ -43,7 +43,10 @@ public static Query createTermQuery(String field, Object value) { *
      *     {
      *         "terms": {
-     *             "{field}": [{values}[0], {values}[1], ...]
+     *             "{@param field}": [
+     *                 {@param values[0]},
+     *                 {@param values[1]}, ...
+     *             ]
      *         }
      *     }
      * 
@@ -56,7 +59,6 @@ public static Query createTermsQuery(String field, Collection values) { fieldValueStream = values.stream() .map(value -> FieldValue.of(b -> b.longValue((int) value))); } else if (elem instanceof String) { - field = field + ".keyword"; fieldValueStream = values.stream() .map(value -> FieldValue.of(b -> b.stringValue((String) value))); } else { @@ -77,9 +79,9 @@ public static Query createTermsQuery(String field, Collection values) { *
      *     {
      *         "range": {
-     *             "{field}": {
-     *                 "gte": "{gte}",
-     *                 "lte": "{lte}"
+     *             "{@param field}": {
+     *                 "gte": "{@param gte}",
+     *                 "lte": "{@param lte}"
      *             }
      *         }
      *     }
@@ -119,11 +121,15 @@ public static Query createRangeQuery(String field, Date gte, Date lte) {
      *         "bool": {
      *             "should": [
      *                 {
-     *                     "match": { "{fields}[0]": "{searchText}" }
+     *                     "match": {
+     *                         "{@param fields[0]}": "{@param searchText}"
+     *                     }
      *                 },
      *                 {
-     *                     "match": { "{fields}[1]": "{searchText}" }
-     *                 }
+     *                     "match": {
+     *                         "{@param fields[1]}": "{@param searchText}"
+     *                     }
+     *                 }, ...
      *             ]
      *         }
      *     }
@@ -144,6 +150,16 @@ public static Query createMatchQuery(Set fields, String searchText) {
         return bool.build()._toQuery();
     }
 
+    /**
+     * 
+     *     {
+     *         "{@param field}": {
+     *             "order": {@param order},
+     *             "mode": {@param mode}
+     *         }
+     *     }
+     * 
+ */ public static SortOptions createSortOption(String field, SortOrder order, SortMode mode) { var fieldSort = new FieldSort.Builder() .field(field) diff --git a/src/test/java/com/vincent/es/SearchTests.java b/src/test/java/com/vincent/es/SearchTests.java index e31304a..67b8e9f 100644 --- a/src/test/java/com/vincent/es/SearchTests.java +++ b/src/test/java/com/vincent/es/SearchTests.java @@ -10,7 +10,7 @@ import com.vincent.es.util.SearchInfo; import com.vincent.es.util.SearchUtils; import org.junit.Before; -import org.junit.jupiter.api.Test; +import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; @@ -33,12 +33,14 @@ public class SearchTests { @Autowired private StudentEsRepository repository; + @SuppressWarnings({"squid:S2925"}) @Before - public void setup() throws IOException { + public void setup() throws IOException, InterruptedException { repository.init(); var documents = SampleData.get(); repository.insert(documents); + Thread.sleep(2000); } @Test From 9a3003e514492f73383f7dc6d40d02e246f9d173 Mon Sep 17 00:00:00 2001 From: Vincent Date: Sun, 25 Dec 2022 16:25:22 +0800 Subject: [PATCH 3/3] Provide student test data. Implement field exist query and test. --- .../java/com/vincent/es/util/SearchInfo.java | 8 +- .../java/com/vincent/es/util/SearchUtils.java | 16 ++++ src/test/java/com/vincent/es/SearchTests.java | 22 +++++ students.json | 84 +++++++++++++++++++ 4 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 students.json diff --git a/src/main/java/com/vincent/es/util/SearchInfo.java b/src/main/java/com/vincent/es/util/SearchInfo.java index 7f751d3..d155647 100644 --- a/src/main/java/com/vincent/es/util/SearchInfo.java +++ b/src/main/java/com/vincent/es/util/SearchInfo.java @@ -7,10 +7,10 @@ import java.util.List; public class SearchInfo { - private BoolQuery boolQuery; // 查詢條件 - private List sortOptions; // 排序方式 - private Integer from; // 資料的跳過數量 - private Integer size; // 資料的擷取數量 + private BoolQuery boolQuery; // 查詢條件 + private List sortOptions = List.of(); // 排序方式 + private Integer from; // 資料的跳過數量 + private Integer size; // 資料的擷取數量 public static SearchInfo of(BoolQuery bool) { var info = new SearchInfo(); diff --git a/src/main/java/com/vincent/es/util/SearchUtils.java b/src/main/java/com/vincent/es/util/SearchUtils.java index 12a2ff9..750a3bd 100644 --- a/src/main/java/com/vincent/es/util/SearchUtils.java +++ b/src/main/java/com/vincent/es/util/SearchUtils.java @@ -150,6 +150,22 @@ public static Query createMatchQuery(Set fields, String searchText) { return bool.build()._toQuery(); } + /** + *
+     *     {
+     *         "exists": {
+     *             "field": {@param field}
+     *         }
+     *     }
+     * 
+ */ + public static Query createFieldExistsQuery(String field) { + return new ExistsQuery.Builder() + .field(field) + .build() + ._toQuery(); + } + /** *
      *     {
diff --git a/src/test/java/com/vincent/es/SearchTests.java b/src/test/java/com/vincent/es/SearchTests.java
index 67b8e9f..289962b 100644
--- a/src/test/java/com/vincent/es/SearchTests.java
+++ b/src/test/java/com/vincent/es/SearchTests.java
@@ -107,6 +107,28 @@ public void testFullTextSearch() {
         assertDocumentIds(true, students, "103", "104");
     }
 
+    @Test
+    public void testTextFieldExists() {
+        var query = SearchUtils.createFieldExistsQuery("bloodType");
+        var searchInfo = SearchInfo.of(query);
+
+        var students = repository.find(searchInfo);
+
+        // Dora, Mario, Vincent
+        assertDocumentIds(true, students, "101", "102", "103");
+    }
+
+    @Test
+    public void testTextArrayFieldExists() {
+        var query = SearchUtils.createFieldExistsQuery("phoneNumbers");
+        var searchInfo = SearchInfo.of(query);
+
+        var students = repository.find(searchInfo);
+
+        // Dora, Vincent
+        assertDocumentIds(true, students, "101", "103");
+    }
+
     @Test
     public void testSortByMultipleFields() {
         var coursePointSort = SearchUtils.createSortOption("courses.point", SortOrder.Desc, SortMode.Max);
diff --git a/students.json b/students.json
new file mode 100644
index 0000000..3e29421
--- /dev/null
+++ b/students.json
@@ -0,0 +1,84 @@
+[
+    {
+        "id": "103",
+        "name": "Vincent Zheng",
+        "departments": ["資訊管理", "財務金融"],
+        "courses": [
+            { "name": "計算機概論", "point": 3 },
+            { "name": "程式設計", "point": 4 },
+            { "name": "投資學", "point": 3 },
+            { "name": "會計學", "point": 0 }
+        ],
+        "grade": 2,
+        "conductScore": 86,
+        "job": {
+            "name": "班長",
+            "primary": true
+        },
+        "introduction": "I have a blog used to record what I learn in my career. All of them are about information technology and programming.",
+        "englishIssuedDate": "2021-01-01",
+        "bloodType": "A",
+        "phoneNumbers": [
+            "0988123456",
+            "0224123456"
+        ]
+    },
+    {
+        "id": "101",
+        "name": "Dora Pan",
+        "departments": ["財務金融"],
+        "courses": [
+            { "name": "財金概論", "point": 3 },
+            { "name": "保險學", "point": 3 },
+            { "name": "投資學", "point": 3 }
+        ],
+        "grade": 4,
+        "conductScore": 74,
+        "job": {
+            "name": "衛生股長",
+            "primary": null
+        },
+        "introduction": "Wealth ignores those who ignore it. So I apply knowledge about accounting in my life.",
+        "englishIssuedDate": "2021-04-01",
+        "bloodType": "B",
+        "phoneNumbers": [
+            "0912345678"
+        ]
+    },
+    {
+        "id": "104",
+        "name": "Winnie Kuo",
+        "departments": ["企業管理"],
+        "courses": [
+            { "name": "會計學", "point": 3 },
+            { "name": "商業概論", "point": 1 }
+        ],
+        "grade": 1,
+        "conductScore": 71,
+        "job": {
+            "name": "班長",
+            "primary": false
+        },
+        "introduction": "To lead a team in company in career, learn to lead students in university first.",
+        "englishIssuedDate": "2021-12-01",
+        "phoneNumbers": []
+    },
+    {
+        "id": "102",
+        "name": "Mario Lu",
+        "departments": ["會計"],
+        "courses": [
+            { "name": "會計學", "point": 5 },
+            { "name": "審計學", "point": 3 },
+            { "name": "企業資源規劃", "point": 3 }
+        ],
+        "grade": 3,
+        "conductScore": 83,
+        "job": {
+            "name": "康樂股長"
+        },
+        "introduction": "Accounting work can be done by technology. So I start to learn programming on internet.",
+        "englishIssuedDate": "2022-05-01",
+        "bloodType": "O"
+    }
+]
\ No newline at end of file