全球消息!【ES三周年】萬字長文帶你實戰(zhàn) Elasticsearch 搜索
2023-02-28 17:20:44 來源:騰訊云
ES 高級實戰(zhàn)
前言
上篇我們講到了 Elasticsearch 全文檢索的原理《別只會搜日志了,求你懂點原理吧》,通過在本地搭建一套 ES 服務,以多個案例來分析了 ES 的原理以及基礎使用。這次我們來講下 Spring Boot 中如何整合 ES,以及如何在 Spring Cloud 微服務項目中使用 ES 來實現(xiàn)全文檢索,來達到搜索題庫的功能。
(資料圖)
而且題庫的數(shù)據量是非常大的,題目的答案也是非常長的,通過 ES 正好可以解決 mysql 模糊搜索的低效性。
通過本實戰(zhàn)您可以學到如下知識點:
Spring Boot 如何整合 ES。微服務中 ES 的 API 使用。項目中如何使用 ES 來達到全文檢索。
本篇主要內容如下:
undefined主要內容
本文案例都是基于 PassJava 實戰(zhàn)項目來演示的。
:+1:Github 地址:https://github.com/Jackson0714/PassJava-Platform
一、Elasticsearch 組件庫介紹
在講解之前,我在這里再次提下全文檢索是什么:
全文檢索:指以全部文本信息作為檢索對象的一種信息檢索技術。而我們使用的數(shù)據庫,如 Mysql,MongoDB 對文本信息檢索能力特別是中文檢索并沒有 ES 強大。所以我們來看下 ES 在項目中是如何來代替 SQL 來工作的。
我使用的 Elasticsearch 服務是 7.4.2 的版本,然后采用官方提供的 Elastiscsearch-Rest-Client 庫來操作 ES,而且官方庫的 API 上手簡單。
另外這個組件庫是支持多種語言的:
undefined支持多語言
注意:Elasticsearch Clients就是指如何用 API 操作 ES 服務的組件庫。
可能有同學會提問,Elasticsearch 的組件庫中寫著 JavaScript API,是不是可以直接在前端訪問 ES 服務?可以是可以,但是會暴露 ES 服務的端口和 IP 地址,會非常不安全。所以我們還是用后端服務來訪問 ES 服務。
我們這個項目是 Java 項目,自然就是用上面的兩種:Java Rest Client或者 Java API。我們先看下 Java API,但是會發(fā)現(xiàn)已經廢棄了。如下圖所示:
undefinedJava API 已經廢棄了
所以我們只能用 Java REST Client 了。而它又分成兩種:高級和低級的。高級包含更多的功能,如果把高級比作MyBatis的話,那么低級就相當于JDBC。所以我們用高級的 Client。
undefined高級和低級 Client
二、整合檢索服務
我們把檢索服務單獨作為一個服務。就稱作 passjava-search 模塊吧。
1.1 添加搜索服務模塊
創(chuàng)建 passjava-search 模塊。
首先我們在 PassJava-Platform 模塊創(chuàng)建一個 搜索服務模塊 passjava-search。然后勾選 spring web 服務。如下圖所示。
第一步:選擇 Spring Initializr,然后點擊 Next。
undefined選擇 Spring Initializr
第二步:填寫模塊信息,然后點擊 Next。
undefinedpassjava-search 服務模塊
第三步:選擇 Web->Spring Web 依賴,然后點擊 Next。
undefinedmark
1.2 配置 Maven 依賴
參照 ES 官網配置。
進入到 ES 官方網站,可以看到有低級和高級的 Rest Client,我們選擇高階的(High Level Rest Client)。然后進入到高階 Rest Client 的 Maven 倉庫。官網地址如下所示:
https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/index.html
復制
undefinedRest Client 官方文檔
加上 Maven 依賴。
對應文件路徑:\passjava-search\pom.xml
org.elasticsearch.client elasticsearch-rest-high-level-client 7.4.2
復制配置 elasticsearch 的版本為7.4.2
因加上 Maven 依賴后,elasticsearch 版本為 7.6.2,所以遇到這種版本不一致的情況時,需要手動改掉。
對應文件路徑:\passjava-search\pom.xml
7.4.2
復制
刷新 Maven Project 后,可以看到引入的 elasticsearch 都是 7.4.2 版本了,如下圖所示:
undefined設置版本為 7.4.2
引入 PassJava 的 Common 模塊依賴。
Common 模塊是 PassJava 項目獨立的出來的公共模塊,引入了很多公共組件依賴,其他模塊引入 Common 模塊依賴后,就不需要單獨引入這些公共組件了,非常方便。
對應文件路徑:\passjava-search\pom.xml
com.jackson0714.passjava passjava-common 0.0.1-SNAPSHOT
復制
添加完依賴后,我們就可以將搜索服務注冊到 Nacos注冊中心了。 Nacos 注冊中心的用法在前面幾篇文章中也詳細講解過,這里需要注意的是要先啟動 Nacos 注冊中心,才能正常注冊 passjava-search 服務。
1.3 注冊搜索服務到注冊中心
修改配置文件:src/main/resources/application.properties。配置應用程序名、注冊中心地址、注冊中心的命名中間。
spring.application.name=passjava-searchspring.cloud.nacos.config.server-addr=127.0.0.1:8848spring.cloud.nacos.config.namespace=passjava-search
復制
給啟動類添加服務發(fā)現(xiàn)注解:@EnableDiscoveryClient。這樣 passjava-search 服務就可以被注冊中心發(fā)現(xiàn)了。
因 Common 模塊依賴數(shù)據源,但 search 模塊不依賴數(shù)據源,所以 search 模塊需要移除數(shù)據源依賴:
exclude = DataSourceAutoConfiguration.class
復制
以上的兩個注解如下所示:
@EnableDiscoveryClient@SpringBootApplication(exclude = DataSourceAutoConfiguration.class)public class PassjavaSearchApplication { public static void main(String[] args) { SpringApplication.run(PassjavaSearchApplication.class, args); }}
復制
接下來我們添加一個 ES 服務的專屬配置類,主要目的是自動加載一個 ES Client 來供后續(xù) ES API 使用,不用每次都 new 一個 ES Client。
1.4 添加 ES 配置類
配置類:PassJavaElasticsearchConfig.java
核心方法就是 RestClient.builder 方法,設置好 ES 服務的 IP 地址、端口號、傳輸協(xié)議就可以了。最后自動加載了 RestHighLevelClient。
package com.jackson0714.passjava.search.config;import org.apache.http.HttpHost;import org.elasticsearch.client.RestClient;import org.elasticsearch.client.RestHighLevelClient;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * @Author: 公眾號 | 悟空聊架構 * @Date: 2020/10/8 17:02 * @Site: www.passjava.cn * @Github: https://github.com/Jackson0714/PassJava-Platform */@Configurationpublic class PassJavaElasticsearchConfig { @Bean // 給容器注冊一個 RestHighLevelClient,用來操作 ES // 參考官方文檔:https://www.elastic.co/guide/en/elasticsearch/client/java-rest/7.9/java-rest-high-getting-started-initialization.html public RestHighLevelClient restHighLevelClient() { return new RestHighLevelClient( RestClient.builder( new HttpHost("192.168.56.10", 9200, "http"))); }}
復制
接下來我們測試下 ES Client 是否自動加載成功。
1.5 測試 ES Client 自動加載
在測試類 PassjavaSearchApplicationTests 中編寫測試方法,打印出自動加載的 ES Client。期望結果是一個 RestHighLevelClient 對象。
package com.jackson0714.passjava.search;import org.elasticsearch.client.RestHighLevelClient;import org.junit.jupiter.api.Test;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Qualifier;import org.springframework.boot.test.context.SpringBootTest;@SpringBootTestclass PassjavaSearchApplicationTests { @Qualifier("restHighLevelClient") @Autowired private RestHighLevelClient client; @Test public void contextLoads() { System.out.println(client); }}
復制
運行結果如下所示,打印出了 RestHighLevelClient。說明自定義的 ES Client 自動裝載成功。
undefinedES 測試結果
1.6 測試 ES 簡單插入數(shù)據
測試方法 testIndexData,省略 User 類。users 索引在我的 ES 中是沒有記錄的,所以期望結果是 ES 中新增了一條 users 數(shù)據。
/** * 測試存儲數(shù)據到 ES。 * */@Testpublic void testIndexData() throws IOException { IndexRequest request = new IndexRequest("users"); request.id("1"); // 文檔的 id //構造 User 對象 User user = new User(); user.setUserName("PassJava"); user.setAge("18"); user.setGender("Man"); //User 對象轉為 JSON 數(shù)據 String jsonString = JSON.toJSONString(user); // JSON 數(shù)據放入 request 中 request.source(jsonString, XContentType.JSON); // 執(zhí)行插入操作 IndexResponse response = client.index(request, RequestOptions.DEFAULT); System.out.println(response);}
復制
執(zhí)行 test 方法,我們可以看到控制臺輸出以下結果,說明數(shù)據插入到 ES 成功。另外需要注意的是結果中的 result 字段為 updated,是因為我本地為了截圖,多執(zhí)行了幾次插入操作,但因為 id = 1,所以做的都是 updated 操作,而不是 created 操作。
undefined控制臺輸出結果
我們再來到 ES 中看下 users 索引中數(shù)據。查詢 users 索引:
GET users/_search
復制
結果如下所示:
undefined查詢 users 索引結果
可以從圖中看到有一條記錄被查詢出來,查詢出來的數(shù)據的 _id = 1,和插入的文檔 id 一致。另外幾個字段的值也是一致的。說明插入的數(shù)據沒有問題。
"age" : "18","gender" : "Man","userName" : "PassJava"
復制
1.7 測試 ES 查詢復雜語句
示例:搜索 bank 索引,address 字段中包含 big 的所有人的年齡分布 ( 前 10 條 ) 以及平均年齡,以及平均薪資。
1.7.1 構造檢索條件
我們可以參照官方文檔給出的示例來創(chuàng)建一個 SearchRequest 對象,指定要查詢的索引為 bank,然后創(chuàng)建一個 SearchSourceBuilder 來組裝查詢條件??偣灿腥N條件需要組裝:
address 中包含 road 的所有人。按照年齡分布進行聚合。計算平均薪資。
代碼如下所示,需要源碼請到我的 Github/PassJava 上下載。
undefined查詢復雜語句示例
將打印出來的檢索參數(shù)復制出來,然后放到 JSON 格式化工具中格式化一下,再粘貼到 ES 控制臺執(zhí)行,發(fā)現(xiàn)執(zhí)行結果是正確的。
undefined打印出檢索參數(shù)
用在線工具格式化 JSON 字符串,結果如下所示:
然后我們去掉其中的一些默認參數(shù),最后簡化后的檢索參數(shù)放到 Kibana 中執(zhí)行。
Kibana Dev Tools 控制臺中執(zhí)行檢索語句如下圖所示,檢索結果如下圖所示:
undefined控制臺中執(zhí)行檢索語句
找到總記錄數(shù):29 條。
第一條命中記錄的詳情如下:
平均 balance:13136。
平均年齡:26。
地址中包含 Road 的:263 Aviation Road。
和 IDEA 中執(zhí)行的測試結果一致,說明復雜檢索的功能已經成功實現(xiàn)。
17.2 獲取命中記錄的詳情
而獲取命中記錄的詳情數(shù)據,則需要通過兩次 getHists() 方法拿到,如下所示:
// 3.1)獲取查到的數(shù)據。SearchHits hits = response.getHits();// 3.2)獲取真正命中的結果SearchHit[] searchHits = hits.getHits();
復制
我們可以通過遍歷 searchHits 的方式打印出所有命中結果的詳情。
// 3.3)、遍歷命中結果for (SearchHit hit: searchHits) { String hitStr = hit.getSourceAsString(); BankMember bankMember = JSON.parseObject(hitStr, BankMember.class);}
復制
拿到每條記錄的 hitStr 是個 JSON 數(shù)據,如下所示:
{"account_number": 431,"balance": 13136,"firstname": "Laurie","lastname": "Shaw","age": 26,"gender": "F","address": "263 Aviation Road","employer": "Zillanet","email": "laurieshaw@zillanet.com","city": "Harmon","state": "WV"}
復制
而 BankMember 是根據返回的結果詳情定義的的 JavaBean。可以通過工具自動生成。在線生成 JavaBean 的網站如下:
https://www.bejson.com/json2javapojo/new/
復制
把這個 JavaBean 加到 PassjavaSearchApplicationTests 類中:
@ToString@Datastatic class BankMember { private int account_number; private int balance; private String firstname; private String lastname; private int age; private String gender; private String address; private String employer; private String email; private String city; private String state;}
復制
然后將 bankMember 打印出來:
System.out.println(bankMember);
復制
undefinedbankMember
得到的結果確實是我們封裝的 BankMember 對象,而且里面的屬性值也都拿到了。
1.7.3 獲取年齡分布聚合信息
ES 返回的 response 中,年齡分布的數(shù)據是按照 ES 的格式返回的,如果想按照我們自己的格式來返回,就需要將 response 進行處理。
如下圖所示,這個是查詢到的年齡分布結果,我們需要將其中某些字段取出來,比如 buckets,它代表了分布在 21 歲的有 4 個。
undefinedES 返回的年齡分布信息
下面是代碼實現(xiàn):
Aggregations aggregations = response.getAggregations();Terms ageAgg1 = aggregations.get("ageAgg");for (Terms.Bucket bucket : ageAgg1.getBuckets()) { String keyAsString = bucket.getKeyAsString(); System.out.println("用戶年齡: " + keyAsString + " 人數(shù):" + bucket.getDocCount());}
復制
最后打印的結果如下,21 歲的有 4 人,26 歲的有 4 人,等等。
undefined打印結果:用戶年齡分布
1.7.4 獲取平均薪資聚合信息
現(xiàn)在來看看平均薪資如何按照所需的格式返回,ES 返回的結果如下圖所示,我們需要獲取 balanceAvg 字段的 value 值。
undefinedES 返回的平均薪資信息
代碼實現(xiàn):
Avg balanceAvg1 = aggregations.get("balanceAvg");System.out.println("平均薪資:" + balanceAvg1.getValue());
復制
打印結果如下,平均薪資 28578 元。
undefined打印結果:平均薪資
三、實戰(zhàn):同步 ES 數(shù)據
3.1 定義檢索模型
PassJava 這個項目可以用來配置題庫,如果我們想通過關鍵字來搜索題庫,該怎么做呢?
類似于百度搜索,輸入幾個關鍵字就可以搜到關聯(lián)的結果,我們這個功能也是類似,通過 Elasticsearch 做檢索引擎,后臺管理界面和小程序作為搜索入口,只需要在小程序上輸入關鍵字,就可以檢索相關的題目和答案。
首先我們需要把題目和答案保存到 ES 中,在存之前,第一步是定義索引的模型,如下所示,模型中有 title和 answer字段,表示題目和答案。
"id": { "type": "long"},"title": { "type": "text", "analyzer": "ik_smart"},"answer": { "type": "text", "analyzer": "ik_smart"},"typeName": { "type": "keyword"}
復制
3.2 在 ES 中創(chuàng)建索引
上面我們已經定義了索引結構,接著就是在 ES 中創(chuàng)建索引。
在 Kibana 控制臺中執(zhí)行以下語句:
PUT question{"mappings" : { "properties": { "id": { "type": "long" }, "title": { "type": "text", "analyzer": "ik_smart" }, "answer": { "type": "text", "analyzer": "ik_smart" }, "typeName": { "type": "keyword" }} }}
復制
執(zhí)行結果如下所示:
undefined創(chuàng)建 question 索引
我們可以通過以下命令來查看 question 索引是否在 ES 中:
GET _cat/indices
復制
執(zhí)行結果如下圖所示:
undefined查看 ES 中所有的索引
3.3 定義 ES model
上面我們定義 ES 的索引,接著就是定義索引對應的模型,將數(shù)據存到這個模型中,然后再存到 ES 中。
ES 模型如下,共四個字段:id、title、answer、typeName。和 ES 索引是相互對應的。
@Datapublic class QuestionEsModel { private Long id; private String title; private String answer; private String typeName;}
復制
3.4 觸發(fā)保存的時機
當我們在后臺創(chuàng)建題目或保存題目時,先將數(shù)據保存到 mysql 數(shù)據庫,然后再保存到 ES 中。
如下圖所示,在管理后臺創(chuàng)建題目時,觸發(fā)保存數(shù)據到 ES 。
undefinedmark
第一步,保存數(shù)據到 mysql 中,項目中已經包含此功能,就不再講解了,直接進入第二步:保存數(shù)據到 ES 中。
而保存數(shù)據到 ES 中,需要將數(shù)據組裝成 ES 索引對應的數(shù)據,所以我用了一個 ES model,先將數(shù)據保存到 ES model 中。
3.5 用 model 來組裝數(shù)據
這里的關鍵代碼時 copyProperties,可以將 question對象的數(shù)據取出,然后賦值到 ES model 中。不過 ES model 中還有些字段是 question 中沒有的,所以需要單獨拎出來賦值,比如 typeName 字段,question 對象中沒有這個字段,它對應的字段是 question.type,所以我們把 type 取出來賦值到 ES model 的 typeName 字段上。如下圖所示:
undefined用 model 來組裝數(shù)據
3.6 保存數(shù)據到 ES
我在 passjava-search 微服務中寫了一個保存題目的 api 用來保存數(shù)據到 ES 中。
undefined保存數(shù)據到 ES
然后在 passjava-question 微服務中調用 search 微服務的保存 ES 的方法就可以了。
// 調用 passjava-search 服務,將數(shù)據發(fā)送到 ES 中保存。searchFeignService.saveQuestion(esModel);
復制
3.7 檢驗 ES 中是否創(chuàng)建成功
我們可以通過 kibana 的控制臺來查看 question 索引中的文檔。通過以下命令來查看:
GET question/_search
復制
執(zhí)行結果如下圖所示,有一條記錄:
undefinedmark
另外大家有沒有疑問:可以重復更新題目嗎?
答案是可以的,保存到 ES 的數(shù)據是冪等的,因為保存的時候帶了一個類似數(shù)據庫主鍵的 id。
四、實戰(zhàn):查詢 ES 數(shù)據
我們已經將數(shù)據同步到了 ES 中,現(xiàn)在就是前端怎么去查詢 ES 數(shù)據中,這里我們還是使用 Postman 來模擬前端查詢請求。
4.1 定義請求參數(shù)
請求參數(shù)我定義了三個:
keyword:用來匹配問題或者答案。id:用來匹配題目 id。pageNum:用來分頁查詢數(shù)據。
這里我將這三個參數(shù)定義為一個類:
@Datapublic class SearchParam { private String keyword; // 全文匹配的關鍵字 private String id; // 題目 id private Integer pageNum; // 查詢第幾頁數(shù)據}
復制
4.2 定義返回參數(shù)
返回的 response 我也定義了四個字段:
questionList:查詢到的題目列表。pageNum:第幾頁數(shù)據。total:查詢到的總條數(shù)。totalPages:總頁數(shù)。
定義的類如下所示:
@Datapublic class SearchQuestionResponse { private List questionList; // 題目列表 private Integer pageNum; // 查詢第幾頁數(shù)據 private Long total; // 總條數(shù) private Integer totalPages; // 總頁數(shù)}
復制
4.3 組裝 ES 查詢參數(shù)
調用 ES 的查詢 API 時,需要構建查詢參數(shù)。
組裝查詢參數(shù)的核心代碼如下所示:
undefined組裝查詢參數(shù)
第一步:創(chuàng)建檢索請求。第二步:設置哪些字段需要模糊匹配。這里有三個字段:title,answer,typeName。第三步:設置如何分頁。這里分頁大小是 5 個。第四步:調用查詢 api。
4.4 格式化 ES 返回結果
ES 返回的數(shù)據是 ES 定義的格式,真正的數(shù)據被嵌套在 ES 的 response 中,所以需要格式化返回的數(shù)據。
核心代碼如下圖所示:
undefined格式化 ES 返回結果
第一步:獲取查到的數(shù)據。第二步:獲取真正命中的結果。第三步:格式化返回的數(shù)據。第四步:組裝分頁參數(shù)。
4.5 測試 ES 查詢
4.5.1 實驗一:測試 title 匹配
我們現(xiàn)在想要驗證 title 字段是否能匹配到,傳的請求參數(shù) keyword = 111,匹配到了 title = 111 的數(shù)據,且只有一條。頁碼 pageNum 我傳的 1,表示返回第一頁數(shù)據。如下圖所示:
undefined測試匹配 title
4.5.2 實驗二:測試 answer 匹配
我們現(xiàn)在想要驗證 answer 字段是否能匹配到,傳的請求參數(shù) keyword = 測試答案,匹配到了 title = 測試答案的數(shù)據,且只有一條,說明查詢成功。如下圖所示:
undefined測試匹配 answer
4.5.2 實驗三:測試 id 匹配
我們現(xiàn)在想要匹配題目 id 的話,需要傳請求參數(shù) id,而且 id 是精確匹配。另外 id 和 keyword 是取并集,所以不能傳 keyword 字段。
請求參數(shù) id = 5,返回結果也是 id =5 的數(shù)據,說明查詢成功。如下圖所示:
undefined測試 id 匹配
五、總結
本文通過我的開源項目 passjava 來講解 ES 的整合,ES 的 API 使用以及測試。非常詳細地講解了每一步該如何做,相信通過閱讀本篇后,再加上自己的實踐,一定能掌握前后端該如何使用 ES 來達到高效搜索的目的。
當然,ES API 還有很多功能未在本文實踐,有興趣的同學可以到 ES 官網進行查閱和學習。
關鍵詞:
推薦內容
- 當前頭條:極目快評|3胞胎姐妹同日出嫁,只需隨一
- 天天動態(tài):安徽亳州多地市民凌晨聽到巨響,官方回應
- 國網鄂州供電公司推進“鄂電紅馬甲”服務落地,保
- 環(huán)球觀焦點:走進湖北大學體育學院 感受沙湖畔
- 世界快看:一盤棋系統(tǒng)化 武漢市加速推進多城環(huán)保
- 環(huán)保先行,頗爾助力百威實現(xiàn)啤酒無硅藻土膜過濾項
- 請把手機橫過來! 這是一幅長長長長……長的各族
- 焦點訊息:ChatGPT爆火背后冷思考:人工智能落地
- 增額終身壽險持續(xù)受熱捧 “富德生命傳世金尊保障
- 怎樣選擇成長股?成長股的股票有哪些?
- 共享共創(chuàng)共贏:恩施神農架襄陽攜手簽約創(chuàng)客協(xié)議
- 大亞灣海事局聯(lián)合鹽田海事局開展學雷鋒志愿服務活
- 【全球時快訊】華中地區(qū)最大載重噸原油船“長祥洲
- 西安班列再出發(fā)!西部陸海新通道持續(xù)助力企業(yè)出口
- 瓊州海峽2人騎水上自行車遇險被安全救起
- 世界觀點:滬蘇VTS覆蓋水域船舶 “一次性船位報
- 元培智庫正式更名為元培工匠專家谷
- 燕園人合集團董事長應邀實地考察魯中職業(yè)學院
- 全球熱頭條丨桓臺縣召開大整治大提升行動暨全域公
- 當前快訊:臥龍區(qū)應急管理局:全面開展2023年備汛
- 世界快看點丨黎黃陂路歷史文化街區(qū)入選國家級旅游
- 開往春天的大巴——山東理工大學62名畢業(yè)生走進淄
- 【全球熱聞】武漢周末最高氣溫將超過20℃
- 焦點簡訊:對中國負責、對世界負責的現(xiàn)代化新路(
- 當前信息:開局之年“hui”藍圖丨未來農業(yè)什么樣
- 全球微頭條丨平凡英雄|我是人大代表
- 環(huán)球看熱訊:梁倩娟:加大農產品深加工 讓農產品
- 觀速訊丨年入賬100億、門店超8200家,瑞幸急赴下
- 焦點快播:雙休日天氣晴好,最高溫沖上20℃+
- 好戲連臺迎兩會!這里有個超10萬人看戲的“大劇場
- 天天快看:@太原人!婦女節(jié)即將來臨 我省景區(qū)專
- 天天熱文:宜賓南溪區(qū)發(fā)揮統(tǒng)戰(zhàn)資源優(yōu)勢助力鄉(xiāng)村教
- 環(huán)球熱點!武漢樓市回暖?2月新房成交創(chuàng)17年新高
- 指紋支付額度有限制嗎?指紋支付有什么要注意的?
- 如何設置指紋支付?指紋支付到底安全不安全?
- 法獅龍“頂墻一體·才高級”隱形變頻浴霸全國發(fā)布
- 全球觀熱點:中國博物館協(xié)會理事長劉曙光一行蒞臨
- 快資訊丨臨沂市體育局黨員志愿者到臨沭縣開展愛心
- 簡訊:先“成人”再“成才” 建立多元評價機制
- 天天視訊!“南粵21式”實在是高!哪招你最愛,快