SpringBoot操作ES進行各種高級查詢

  • 2019 年 10 月 3 日
  • 筆記

SpringBoot整合ES

創建SpringBoot項目,導入 ES 6.2.1 的 RestClient 依賴和 ES 依賴。在項目中直接引用 es-starter 的話會報容器初始化異常錯誤,導致項目無法啟動。如果有讀者解決了這個問題,歡迎留言交流

<!-- ES 客戶端 -->  <dependency>      <groupId>org.elasticsearch.client</groupId>      <artifactId>elasticsearch-rest-high-level-client</artifactId>      <version>${elasticsearch.version}</version>  </dependency>  <!-- ES 版本 -->  <dependency>      <groupId>org.elasticsearch</groupId>      <artifactId>elasticsearch</artifactId>      <version>${elasticsearch.version}</version>  </dependency>

為容器定義 RestClient 對象

/**   * 在Spring容器中定義 RestClient 對象   * @Author: keats_coder   * @Date: 2019/8/9   * @Version 1.0   * */  @Configuration  public class ESConfig {      @Value("${yunshangxue.elasticsearch.hostlist}")      private String hostlist; // 127.0.0.1:9200        @Bean // 高版本客戶端      public RestHighLevelClient restHighLevelClient() {          // 解析 hostlist 配置資訊。假如以後有多個,則需要用 , 分開          String[] split = hostlist.split(",");          // 創建 HttpHost 數組,其中存放es主機和埠的配置資訊          HttpHost[] httpHostArray = new HttpHost[split.length];          for (int i = 0; i < split.length; i++) {              String item = split[i];              httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");          }          // 創建RestHighLevelClient客戶端          return new RestHighLevelClient(RestClient.builder(httpHostArray));      }        // 項目主要使用 RestHighLevelClient,對於低級的客戶端暫時不用      @Bean      public RestClient restClient() {          // 解析hostlist配置資訊          String[] split = hostlist.split(",");          // 創建HttpHost數組,其中存放es主機和埠的配置資訊          HttpHost[] httpHostArray = new HttpHost[split.length];          for (int i = 0; i < split.length; i++) {              String item = split[i];              httpHostArray[i] = new HttpHost(item.split(":")[0], Integer.parseInt(item.split(":")[1]), "http");          }          return RestClient.builder(httpHostArray).build();      }  }

在 yml 文件中配置 eshost

yunshangxue:    elasticsearch:      hostlist: ${eshostlist:127.0.0.1:9200}

調用相關 API 執行操作

  1. 創建操作索引的對象
  2. 構建操作索引的請求
  3. 調用對象的相關API發送請求
  4. 獲取響應消息
/**   * 刪除索引庫   */  @Test  public void testDelIndex() throws IOException {      // 操作索引的對象      IndicesClient indices = client.indices();      // 刪除索引的請求      DeleteIndexRequest deleteIndexRequest = new DeleteIndexRequest("ysx_course");      // 刪除索引      DeleteIndexResponse response = indices.delete(deleteIndexRequest);      // 得到響應      boolean b = response.isAcknowledged();      System.out.println(b);  }

創建索引, 步驟和刪除類似,需要注意的是刪除的時候需要指定 ES 庫分片的數量和副本的數量,並且在創建索引的時候可以將映射一起指定了。程式碼如下

    public void testAddIndex() throws IOException {          // 操作索引的對象          IndicesClient indices = client.indices();          // 創建索引的請求          CreateIndexRequest request = new CreateIndexRequest("ysx_course");          request.settings(Settings.builder().put("number_of_shards", "1").put("number_of_replicas", "0"));          // 創建映射          request.mapping("doc", "{n" +                  "                "properties": {n" +                  "                    "description": {n" +                  "                        "type": "text",n" +                  "                        "analyzer": "ik_max_word",n" +                  "                        "search_analyzer": "ik_smart"n" +                  "                    },n" +                  "                    "name": {n" +                  "                        "type": "text",n" +                  "                        "analyzer": "ik_max_word",n" +                  "                        "search_analyzer": "ik_smart"n" +                  "                    },n" +                  ""pic":{                    n" +                  ""type":"text",                        n" +                  ""index":false                        n" +                  "},                    n" +                  "                    "price": {n" +                  "                        "type": "float"n" +                  "                    },n" +                  "                    "studymodel": {n" +                  "                        "type": "keyword"n" +                  "                    },n" +                  "                    "timestamp": {n" +                  "                        "type": "date",n" +                  "                        "format": "yyyy-MM‐dd HH:mm:ss||yyyy‐MM‐dd||epoch_millis"n" +                  "                    }n" +                  "                }n" +                  "            }", XContentType.JSON);              // 執行創建操作          CreateIndexResponse response = indices.create(request);          // 得到響應          boolean b = response.isAcknowledged();          System.out.println(b);      }

Java API操作ES

準備數據環境

創建索引:ysx_course

創建映射:

PUT http://localhost:9200/ysx_course/doc/_mapping
{      "properties": {          "description": { // 課程描述              "type": "text", // String text 類型              "analyzer": "ik_max_word", // 存入的分詞模式:細粒度              "search_analyzer": "ik_smart" // 查詢的分詞模式:粗粒度          },          "name": { // 課程名稱              "type": "text",              "analyzer": "ik_max_word",              "search_analyzer": "ik_smart"          },          "pic":{ // 圖片地址              "type":"text",              "index":false // 地址不用來搜索,因此不為它構建索引          },          "price": { // 價格              "type": "scaled_float", // 有比例浮點              "scaling_factor": 100 // 比例因子 100          },          "studymodel": {              "type": "keyword" // 不分詞,全關鍵字匹配          },          "timestamp": {              "type": "date",              "format": "yyyy-MM-dd HH:mm:ss||yyyy-MM-dd||epoch_millis"          }      }  }

加入原始數據:

POST http://localhost:9200/ysx_course/doc/1
{      "name": "Bootstrap開發",      "description": "Bootstrap是由Twitter推出的一個前台頁面開發框架,是一個非常流行的開發框架,此框架集成了多種頁面效果。此開發框架包含了大量的CSS、JS程式程式碼,可以幫助開發者(尤其是不擅長頁面開發的程式人員)輕鬆的實現一個不受瀏覽器限制的精美介面效果。",      "studymodel": "201002",      "price":38.6,      "timestamp":"2018-04-25 19:11:35",      "pic":"group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg"  }

DSL搜索

DSL(Domain Specific Language)是ES提出的基於json的搜索方式,在搜索時傳入特定的json格式的數據來完成不
同的搜索需求。DSL比URI搜索方式功能強大,在項目中建議使用DSL方式來完成搜索。

查詢全部

原本我們想要查詢全部的話,需要使用 GET 請求發送 _search 命令,如今使用 DSL 方式搜索,可以使用 POST 請求,並在請求體中設置 JSON 字元串來構建查詢條件

POST http://localhost:9200/ysx_course/doc/_search

請求體 JSON

{      "query": {          "match_all": {} // 查詢全部      },      "_source" : ["name","studymodel"] // 查詢結果包括 課程名 + 學習模式兩個映射  }

具體的測試方法如下:過程比較繁瑣,好在條理還比較清晰

// 搜索全部記錄  @Test  public void testSearchAll() throws IOException, ParseException {      // 搜索請求對象      SearchRequest searchRequest = new SearchRequest("ysx_course");      // 指定類型      searchRequest.types("doc");      // 搜索源構建對象      SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();      // 搜索方式      // matchAllQuery搜索全部      searchSourceBuilder.query(QueryBuilders.matchAllQuery());      // 設置源欄位過慮,第一個參數結果集包括哪些欄位,第二個參數表示結果集不包括哪些欄位      searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{});      // 向搜索請求對象中設置搜索源      searchRequest.source(searchSourceBuilder);      // 執行搜索,向ES發起http請求      SearchResponse searchResponse = client.search(searchRequest);      // 搜索結果      SearchHits hits = searchResponse.getHits();      // 匹配到的總記錄數      long totalHits = hits.getTotalHits();      // 得到匹配度高的文檔      SearchHit[] searchHits = hits.getHits();      // 日期格式化對象      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");      for(SearchHit hit:searchHits){          // 文檔的主鍵          String id = hit.getId();          // 源文檔內容          Map<String, Object> sourceAsMap = hit.getSourceAsMap();          String name = (String) sourceAsMap.get("name");          // 由於前邊設置了源文檔欄位過慮,這時description是取不到的          String description = (String) sourceAsMap.get("description");          // 學習模式          String studymodel = (String) sourceAsMap.get("studymodel");          // 價格          Double price = (Double) sourceAsMap.get("price");          // 日期          Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp"));          System.out.println(name);          System.out.println(studymodel);          System.out.println("你看不見我,看不見我~" + description);          System.out.println(price);      }    }
坑:red>

執行過程中遇到的問題:不能對這個值進行初始化,導致 Spring 容器無法初始化

Caused by: java.lang.IllegalArgumentException: Could not resolve placeholder 'yunshangxue.elasticsearch.hostlist' in value "${yunshangxue.elasticsearch.hostlist}"

通過檢查 target 目錄發現,生成的 target 文件包中沒有將 yml 配置文件帶過來… 仔細對比發現,我的項目竟然變成了一個不是 Maven 的項目。重新使用 IDEA 導入 Mavaen 工程之後便能正常運行了

分頁查詢

我們來 look 一下 ES 的分頁查詢參數:

{      // from 起始索引      // size 每頁顯示的條數      "from" : 0, "size" : 1,      "query": {         "match_all": {}       },      "_source" : ["name","studymodel"]  }

1565524349684

通過查詢結果可以發現,我們設置了分頁參數之後, hits.total 仍然是 3,表示它找到了 3 條數據,而按照分頁規則,它只會返回一條數據,因此 hits.hits 裡面只有一條數據。這也符合我們的業務規則,在查詢前端頁面顯示總共的條數和當前的數據。

由此,我們就可以通過 Java API 來構建查詢條件了:對上面查詢全部的程式碼進行如下改造:

// 搜索源構建對象  SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();  int page = 2; // 頁碼  int size = 1; // 每頁顯示的條數  int index = (page - 1) * size;  searchSourceBuilder.from(index);  searchSourceBuilder.size(1);  // 搜索方式  // matchAllQuery搜索全部  searchSourceBuilder.query(QueryBuilders.matchAllQuery());
精確查詢 TermQuery

Term Query為精確查詢,在搜索時會整體匹配關鍵字,不再將關鍵字分詞

例如:

{      "query": {          "term": { // 查詢的方式為 term 精確查詢              "name": "spring" // 查詢的欄位為 name 關鍵字是 spring          }      },      "_source": [          "name",          "studymodel"      ]  }

此時查詢的結果是:

 "hits": [       {           "_index": "ysx_course",           "_type": "doc",           "_id": "3",           "_score": 0.9331132,           "_source": {               "studymodel": "201001",               "name": "spring開發基礎"           }       }   ]

查詢到了上面這條數據,因為 spring開發基礎 分完詞後是 spring 開發 基礎 ,而查詢關鍵字是 spring 不分詞,這樣當然可以匹配到這條記錄,但是當我們修改關鍵字為 spring開發,按照往常的查詢方法,也是可以查詢到的。但是 term 不一樣,它不會對關鍵字分詞。結果可想而知是查詢不到的

JavaAPI如下:

// 搜索方式  // termQuery 精確查詢  searchSourceBuilder.query(QueryBuilders.termQuery("studymodel", "201002"));
根據 ID 查詢:

根據 ID 精確查詢和根據其他條件精確查詢是一樣的,不同的是 id 欄位前面有一個下劃線注意寫上

searchSourceBuilder.query(QueryBuilders.termQuery("_id", "1"));

但是,當一次查詢多個 ID 時,相應的 API 也應該改變,使用 termsQuery 而不是 termQuery。多了一個 s

全文檢索 MatchQuery

MatchQuery 即全文檢索,會對關鍵字進行分詞後匹配詞條。

query:搜索的關鍵字,對於英文關鍵字如果有多個單詞則中間要用半形逗號分隔,而對於中文關鍵字中間可以用
逗號分隔也可以不用

operator:設置查詢的結果取交集還是並集,並集用 or, 交集用 and

{      "query": {          "match": {              "description": {                  "query": "spring開發",                  "operator": "or"              }          }      }  }

有時,我們需要設定一個量化的表達方式,例如查詢 spring開發基礎,這三個詞條。我們需求是至少匹配兩個詞條,這時 operator 屬性就不能滿足要求了,ES 還提供了另外一個屬性:minimum_should_match 用一個百分數來設定應該有多少個詞條滿足要求。例如查詢:

「spring開發框架」會被分為三個詞:spring、開發、框架
設置"minimum_should_match": "80%"表示,三個詞在文檔的匹配佔比為80%,即3*0.8=2.4,向下取整得2,表
示至少有兩個詞在文檔中要匹配成功。

JavaAPI

通過 matchQuery.minimumShouldMatch 的方式來設置條件

// matchQuery全文檢索          searchSourceBuilder.query(QueryBuilders.matchQuery("description", "Spring開發框架").minimumShouldMatch("70%"));
多欄位聯合搜索 MultiQuery

上面的 MatchQuery 有一個短板,假如用戶輸入了某關鍵字,我們在查找的時候並不知道他輸入的是 name 還是 description,這時我們用什麼都不合適,而 MultiQuery 的出現解決了這個問題,他可以通過 fields 屬性來設置多個域聯合查找:具體用法如下

{      "query": {          "multi_match": {              "query": "Spring開發",              "minimum_should_match": "70%",              "fields": ["name", "description"]          }      }  }

JavaAPI

searchSourceBuilder.query(QueryBuilders.multiMatchQuery("Spring開發框架", "name", "description").minimumShouldMatch("70%"));
提升 boost

在多域聯合查詢的時候,可以通過 boost 來設置某個域在計算得分時候的比重,比重越高的域當他符合條件時計算的得分越高,相應的該記錄也更靠前。通過在 fields 中給相應的欄位用 ^權重倍數來實現

"fields": ["name^10", "description"]

上面的程式碼表示給 name 欄位提升十倍權重,查詢到的結果:

{      "_index": "ysx_course",      "_type": "doc",      "_id": "3",      "_score": 13.802518, // 可以清楚的發現,得分竟然是 13 了      "_source": {          "name": "spring開發基礎",          "description": "spring 在java領域非常流行,java程式設計師都在用。",          "studymodel": "201001",          "price": 88.6,          "timestamp": "2018-02-24 19:11:35",          "pic": "group1/M00/00/00/wKhlQFs6RCeAY0pHAAJx5ZjNDEM428.jpg"      }  },

而在 Java 中,仍然可以通過鏈式編程來實現

searchSourceBuilder.query(QueryBuilders.multiMatchQuery("Spring開發框架", "name", "description").field("name", 10)); // 設置 name 10倍權重
布爾查詢 BoolQuery

如果我們既要對一些欄位進行分詞查詢,同時要對另一些欄位進行精確查詢,就需要使用布爾查詢來實現了。布爾查詢對應於Lucene的BooleanQuery查詢,實現將多個查詢組合起來,有三個可選的參數:

must:文檔必須匹配must所包括的查詢條件,相當於 「AND」

should:文檔應該匹配should所包括的查詢條件其中的一個或多個,相當於 "OR"

must_not:文檔不能匹配must_not所包括的該查詢條件,相當於「NOT」

{      "query": {          "bool": { // 布爾查詢              "must": [ // 查詢條件 must 表示數組中的查詢方式所規定的條件都必須滿足                  {                      "multi_match": {                          "query": "spring框架",                          "minimum_should_match": "50%",                          "fields": [                              "name^10",                              "description"                          ]                      }                  },                  {                      "term": {                          "studymodel": "201001"                      }                  }              ]          }      }  }

JavaAPI

// 搜索方式  // 首先構造多關鍵字查詢條件  MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("Spring開發框架", "name", "description").field("name", 10);  // 然後構造精確匹配查詢條件  TermQueryBuilder termQueryBuilder = QueryBuilders.termQuery("studymodel", "201002");  // 組合兩個條件,組合方式為 must 全滿足  BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();  boolQueryBuilder.must(matchQueryBuilder);  boolQueryBuilder.must(termQueryBuilder);  // 將查詢條件封裝給查詢對象  searchSourceBuilder.query(boolQueryBuilder);
過濾器

定義過濾器查詢,是在原本查詢結果的基礎上對數據進行篩選,因此省略了重新計算的分的步驟,效率更高。並且方便快取。推薦盡量使用過慮器去實現查詢或者過慮器和查詢共同使用,過濾器在布爾查詢中使用,下邊是在搜索結果的基礎上進行過濾:

{      "query": {          "bool": {              "must": [                  {                      "multi_match": {                          "query": "spring框架",                          "minimum_should_match": "50%",                          "fields": [                              "name^10",                              "description"                          ]                      }                  }              ],              "filter": [                  {                      // 過濾條件:studymodel 必須是 201001                      "term": {"studymodel": "201001"}                  },                  {                      // 過濾條件:價格 >=60 <=100                      "range": {"price": {"gte": 60,"lte": 100}}                  }              ]          }      }  }

注意:range和term一次只能對一個Field設置範圍過慮。

JavaAPI

// 首先構造多關鍵字查詢條件  MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("Spring框架", "name", "description").field("name", 10);  // 添加條件到布爾查詢  BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();  boolQueryBuilder.must(matchQueryBuilder);  // 通過布爾查詢來構造過濾查詢  boolQueryBuilder.filter(QueryBuilders.termQuery("studymodel", "201001"));  boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(60).lte(100));  // 將查詢條件封裝給查詢對象  searchSourceBuilder.query(boolQueryBuilder);
排序

我們可以在查詢的結果上進行二次排序,支援對 keyword、date、float 等類型添加排序,text類型的欄位不允許排序。排序使用的 JSON 格式如下:

{      "query": {          "bool": {              "filter": [                  {                      "range": {                          "price": {                              "gte": 0,                              "lte": 100                          }                      }                  }              ]          }      },      "sort": [ // 注意這裡排序是寫在 query key 的外面的。這就表示它的API也不是布爾查詢提供          {              "studymodel": "desc" // 對 studymodel(keyword)降序          },          {              "price": "asc" // 對 price(double)升序          }      ]  }

由上面的 JSON 數據可以發現,排序所屬的 API 是和 query 評級的,因此在調用 API 時也應該選擇對應的 SearchSourceBuilder 對象

// 排序查詢  @Test  public void testSort() throws IOException, ParseException {      // 搜索請求對象      SearchRequest searchRequest = new SearchRequest("ysx_course");      // 指定類型      searchRequest.types("doc");      // 搜索源構建對象      SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();      // 搜索方式      // 添加條件到布爾查詢      BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();      // 通過布爾查詢來構造過濾查詢      boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(0).lte(100));      // 將查詢條件封裝給查詢對象      searchSourceBuilder.query(boolQueryBuilder);      // 向搜索請求對象中設置搜索源      searchRequest.source(searchSourceBuilder);        // 設置排序規則      searchSourceBuilder.sort("studymodel", SortOrder.DESC); // 第一排序規則      searchSourceBuilder.sort("price", SortOrder.ASC); // 第二排序規則        // 執行搜索,向ES發起http請求      SearchResponse searchResponse = client.search(searchRequest);      // 搜索結果      SearchHits hits = searchResponse.getHits();      // 匹配到的總記錄數      long totalHits = hits.getTotalHits();      // 得到匹配度高的文檔      SearchHit[] searchHits = hits.getHits();      // 日期格式化對象      soutData(searchHits);  }
高亮顯示

高亮顯示可以將搜索結果一個或多個字突出顯示,以便向用戶展示匹配關鍵字的位置。

高亮三要素:高亮關鍵字、高亮前綴、高亮後綴

{      "query": {          "bool": {              "must": [                  {                      "multi_match": {                          "query": "開發框架",                          "minimum_should_match": "50%",                          "fields": [                              "name^10",                              "description"                          ],                          "type": "best_fields"                      }                  }              ]          }      },      "sort": [          {              "price": "asc"          }      ],      "highlight": {          "pre_tags": [              "<em>"          ],          "post_tags": [              "</em>"          ],          "fields": {              "name": {},              "description": {}          }      }  }

查詢結果的數據如下:

1565585272091

Java 程式碼如下,注意到上面的 JSON 數據, highlight 和 sort 和 query 依然是同級的,所以也需要用 SearchSourceBuilder 對象來設置到搜索條件中

// 高亮查詢  @Test  public void testHighLight() throws IOException, ParseException {      // 搜索請求對象      SearchRequest searchRequest = new SearchRequest("ysx_course");      // 指定類型      searchRequest.types("doc");      // 搜索源構建對象      SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();      // 搜索方式      // 首先構造多關鍵字查詢條件      MultiMatchQueryBuilder matchQueryBuilder = QueryBuilders.multiMatchQuery("Spring框架", "name", "description").field("name", 10);      // 添加條件到布爾查詢      BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();      boolQueryBuilder.must(matchQueryBuilder);      // 通過布爾查詢來構造過濾查詢      boolQueryBuilder.filter(QueryBuilders.rangeQuery("price").gte(60).lte(100));      // 將查詢條件封裝給查詢對象      searchSourceBuilder.query(boolQueryBuilder);      // ***********************        // 高亮查詢      HighlightBuilder highlightBuilder = new HighlightBuilder();      highlightBuilder.preTags("<em>"); // 高亮前綴      highlightBuilder.postTags("</em>"); // 高亮後綴      highlightBuilder.fields().add(new HighlightBuilder.Field("name")); // 高亮欄位      // 添加高亮查詢條件到搜索源      searchSourceBuilder.highlighter(highlightBuilder);        // ***********************        // 設置源欄位過慮,第一個參數結果集包括哪些欄位,第二個參數表示結果集不包括哪些欄位      searchSourceBuilder.fetchSource(new String[]{"name","studymodel","price","timestamp"},new String[]{});      // 向搜索請求對象中設置搜索源      searchRequest.source(searchSourceBuilder);      // 執行搜索,向ES發起http請求      SearchResponse searchResponse = client.search(searchRequest);      // 搜索結果      SearchHits hits = searchResponse.getHits();      // 匹配到的總記錄數      long totalHits = hits.getTotalHits();      // 得到匹配度高的文檔      SearchHit[] searchHits = hits.getHits();      // 日期格式化對象      soutData(searchHits);  }

根據查詢結果的數據結構來獲取高亮的數據,替換原有的數據:

private void soutData(SearchHit[] searchHits) throws ParseException {      SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");      for (SearchHit hit : searchHits) {          // 文檔的主鍵          String id = hit.getId();          // 源文檔內容          Map<String, Object> sourceAsMap = hit.getSourceAsMap();          String name = (String) sourceAsMap.get("name");            // 獲取高亮查詢的內容。如果存在,則替換原來的name          Map<String, HighlightField> highlightFields = hit.getHighlightFields();          if( highlightFields != null ){              HighlightField nameField = highlightFields.get("name");              if(nameField!=null){                  Text[] fragments = nameField.getFragments();                  StringBuffer stringBuffer = new StringBuffer();                  for (Text str : fragments) {                      stringBuffer.append(str.string());                  }                  name = stringBuffer.toString();              }          }            // 由於前邊設置了源文檔欄位過慮,這時description是取不到的          String description = (String) sourceAsMap.get("description");          // 學習模式          String studymodel = (String) sourceAsMap.get("studymodel");          // 價格          Double price = (Double) sourceAsMap.get("price");          // 日期          Date timestamp = dateFormat.parse((String) sourceAsMap.get("timestamp"));          System.out.println(name);          System.out.println(id);          System.out.println(studymodel);          System.out.println("你看不見我,看不見我~" + description);          System.out.println(price);      }  }