墨西哥地震規模7.4釀5死 震央附近交通中斷傷亡恐增

摘錄自2020年6月24日中央社報導

墨西哥南部太平洋岸今(23日)發生規模7.4強震,造成至少5人喪生,偏遠村落交通中斷,地震威力之強,連遠在震央數百公里外的首都墨西哥市(Mexico City)都有建築物受損。

這起強震震央在瓦哈卡州(Oaxaca)附近,死傷也是在這裡傳出。由於州首府瓦哈卡市(Oaxaca)與海岸間的蜿蜒山路因落石中斷,搜救人員尚未抵達受災村落,傷亡可能更進一步攀升。

社群媒體影像顯示,震央附近山村的診所與老舊教堂也都嚴重受損。

美國地質調查所(USGS)表示,這起地震震源深度僅26公里,屬淺層地震。美國地質調查所初步測得規模達7.7,稍後下修為7.4。

土地水文
土地利用
國際新聞
墨西哥
地震
災害

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

委內瑞拉燃油短缺 單車通勤成生活新常態

摘錄自2020年6月19日公視報導

全球儲油量第一的委內瑞拉,卻因為美國加強禁運和經濟制裁,國內陷入石油危機,民眾排隊加油竟然要等上三天三夜,也促使許多人開始改以自行車代步。

在委內瑞拉,石油長年比水還便宜,民眾大多開車上班,現在因為沒油可買,許多民眾轉而騎單車通勤。自行車技師塞古拉表示:「修理自行車的量變多了,我計算至少多出20至30倍」

該地煉油設備老舊到不堪使用,過去以原油和俄羅斯交換燃油,如今交易也因為受到美國制裁而停擺,面臨歷年來最嚴重的汽油短缺,更有民眾說,沒油可用比新冠病毒更讓人苦惱。

現在委內瑞拉最低月薪3塊美金左右,台幣不到100塊,等於只夠買六公升左右的汽油,無疑是生活一大負擔,也難怪騎單車通勤,成了越來越多委內瑞拉民眾生活的新常態。

生活環境
能源議題
能源轉型
國際新聞
委內瑞拉
單車
自行車
石油危機
石油
交通運輸

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

為綠能發電犧牲演化寶庫 水壩工程威脅印尼古老波索湖

環境資訊中心綜合外電;黃鈺婷 翻譯;林大利 審校;稿源:Mongabay

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

Amazon設立20億美元氣候承諾基金

摘錄自2020年6月24日iThome報導

Amazon在23日宣布設立氣候承諾基金(The Climate Pledge Fund),初期將以20億美元的資金來推動各種能夠符合「氣候承諾」的技術與服務。

Amazon與Global Optimism在去年9月共同發起「氣候承諾」(Climate Pledge),計畫在2040年實現淨零碳(net zero carbon),比《巴黎協議》(Paris Agreement)訂定的2050年目標還早了10年。

Amazon創辦人暨執行長貝佐斯(Jeff Bezos)表示,氣候承諾基金將會投資那些有願景的企業家與創新者,讓他們建置能協助企業減少碳排放影響、而且能持久經營的產品及服務。該基金的投資對象涵蓋各個領域,從運輸、能源、儲存、製造與材料、循環經濟,到食物及農業。

此外,Amazon也揭露了目前該公司對於改善氣候變遷的成效,表示可能會提前5年,於2025年達到全數使用再生能源的目標。

能源議題
再生能源
能源轉型
國際新聞
亞馬遜
氣候基金

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價

香港環署縱容 回收亂龍

摘錄自2020年6月22日東方日報香港報導

歷時一年多的反送中條例加上疫情衝擊,雖然近日稍為平息,卻凸顯出香港政府環保政策漏洞。回收玻璃瓶行業開始恢復,但卻屢發越區搶收違規行為,環境保護署袖手旁觀。

政府按港九新界地區批出三份玻璃管理合約,由兩個承辦商承辦免市場壟斷,然而近期九龍區接連發生跨區「插旗」收瓶,更有承辦商司機越區偷瓶事件,負責監管的環保署卻沒介入糾正,使得分區收瓶制度名存實亡。

另外,廢棄塑膠回收業在沒有管理規劃下,近期大批回收店關門,令大批廢棄塑膠沒人要。有地方議員批評,署方監管不力,默許發生打擊對手的行為,長遠恐怕出現市場壟斷惡果,窒礙本地環保業發展。

公害污染
污染治理
國際新聞
香港
回收
玻璃回收
廢棄物

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

英醫學權威警告政界 小心出現第二波疫情

摘錄自2020年6月24日中央廣播電台報導

路透社今天(24日)報導,資深醫界人士警告英國各政黨,當地俗稱武漢肺炎的2019年冠狀病毒疾病(COVID-19)疫情可能升溫,而且第二波疫情有實質風險。

這些醫學界人士在一封寫給英國政界領袖的公開信中說:「雖然這場疫情未來在英國的情勢難以預估,現有證據顯示,當地爆發疫情的可能性愈來愈大,而第二波(疫情)有實質風險。」

在「英國醫學期刊」(British Medical Journal, BMJ)上簽署這封公開信的人士,包含英國皇家外科醫學院(Royal College of Surgeons)院長奧德森(Derek Alderson)、皇家內科醫學院(Royal College of Physicians)院長戈達(Andrew Goddard),以及皇家緊急醫藥大學院長韓德森(Katherine Henderson)。

他們表示:「遏止病毒必備的許多基礎要素已準備就緒,但重大的挑戰依然存在。」

生活環境
國際新聞
英國
武漢肺炎
疫情
新冠肺炎疫情
公共衛生

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價

封城後空氣污染大減 增印度德里太陽能產電量達 8%

摘錄自2020年6月24日立場新聞報導

武漢肺炎(COVID-19)下,幾乎全球國家都史無前例封城停擺,經濟發展停滯,但卻拯救了無數生命免受感染,也間接令空氣污染大幅減輕,造成意料不到的連鎖效應。最新刊於 Joule 的研究指,空氣污染減輕,令更多陽光直達太陽能電池板,增加了電力產量。

由德國基爾亥姆霍茲可再生能源研究所物理學家 Ian Marius Peters 領導的團隊,分析了疫情期間印度德里的太陽能發電量變化。團隊在疫情前已在多個城市研究霧霾和空氣污染如何阻擋陽光到達地面,以及其對太陽能電池板輸出的影響。

團隊以全天太陽輻射計(pyranometer)測量指定太陽光波長的輻射強度,並使用過去的研究數據,計算了德里到達地面的日照量變化。印度德里是全世界其中一個污染最嚴重的城市,團隊發現,與 2017 年至 2019 年同一段時間數據相比,德里 3 月 24 日封城後,空氣污染量下跌 45%–50% ,而3月下旬德里太陽能電池板接收到的陽光量增加了約 8% , 4 月則增加了 6% ,團隊推斷,污染減少是更多陽光照射電池板的主要原因。

公害污染
空氣污染
污染治理
國際新聞
印度德里
太陽能
COVID-19

本站聲明:網站內容來源環境資訊中心https://e-info.org.tw/,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※回頭車貨運收費標準

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

Python 3.9 beta2 版本發布了,看看這 7 個新的 PEP 都是什麼?

原作:Jake Edge

譯者:豌豆花下貓@Python貓

英文:https://lwn.net/Articles/819853/

隨着 Python 3.9.0b1 的發布,即開發周期中計劃的四個 beta 版本的首個,Python 3.9 的功能已經是完善了。在 10 月發布最終版本之前,還會有許多測試和穩定性方面的工作要做。

(譯註:beta1 版本發佈於 5 月 18 日,作者文章寫於 5 月 20,而到本篇譯文發布時,beta2 剛好在今天即 6 月 9 日發布,這是一個巧合!)

該發布說明中列出了被 3.9 接受的 7 個 Python 增強提案(PEP)。我們研究了其中的一些 PEP,看到有一些更新。現在似乎是一個介紹 Python 3.9 帶來的一些東西的好時機。

1、字符串操作

有時最簡單(表明上的)的事情最困難,或者至少會引起巨大的討論。其中大部分的爭議是關於命名(還能是什麼?),但是給標準字符串對象添加函數,來刪除前綴和後綴,這種想法是毫無爭議的。

是否可以將那些詞綴(前綴和後綴的統稱)指定為序列,以便在一次調用中處理多個詞綴,這一點尚不明確,最後它被從提案中刪除了,等待着其他人再次推動更改。

在 3 月底,Dennis Sweeney 在 python-dev 郵件列表上請求核心開發者支持 PEP 616(“字符串刪除前綴和後綴的方法”)。他指出了自 2019 年 3 月以來關於該話題的 python-ideas 討論。埃里克·史密斯(Eric V. Smith)同意支持該 PEP,這促使 Sweeney 發布並啟動了討論。

在最初版本中,他使用 cutprefix() 和 cutsuffix() 作為要添加給字符串對象的方法名。四種類型的 Python 對象將獲得新的方法:str(Unicode 字符串),byte(二進制序列),bytearray(可變的二進制序列)和 collections.UserString(字符串對象的一種封裝)。

它的寫法如下:

'abcdef'.cutprefix('abc') # 返回'def'
'abcdef'.cutsuffix('ef') # 返回'abcd'

針對命名部分,出現了一大堆的建議。基本上很少有人喜歡“cut”,因此“strip”、“strim”和“remove”被提出來了,並且都獲得了一些支持。

stripprefix() 以及 stripsuffix() 由於 PEP 中指出的一種理由,至少是被部分地反對了;現有的“strip”函數令人困惑,因此應避免重用該名稱。

str.lstrip() 和 str.rstrip() 方法也用於刪除前導字符和尾隨字符,但是它們對於真正在尋找 cutprefix() 功能的程序員來說是一個困惑的來源。

*strip() 在調用時接收一個字符串參數,但會將其視為一組字符,並從字符串開頭或結尾消除:

'abcdef'.lstrip('abc') # 返回“def”,符合預期
'abcbadefed'.lstrip('abc') # 返回'defed',完全不符合預期

最終,removeprefix() 和 removesuffix() 似乎佔據了上風,這正是 Sweeney 最終改成的版本。Guido van Rossum 也支持這些名字。

埃里克·法格倫(Eric Fahlgren)這樣搞笑地總結了命名的爭論:

我認為如果你先寫文檔,則名稱的選擇會更容易些:

cutprefix – 刪除指定的前綴。

trimprefix – 刪除指定的前綴。

stripprefix – 刪除指定的前綴。

removeprefix – 刪除指定的前綴。廢話

Sweeney 更新了 PEP,回應了許多評論,但還增加了提議將字符串元組作為詞綴的功能(可以在 PEP GitHub 倉庫中看到該版本)。

但是史蒂文·達普拉諾(Steven D’Aprano)不確定這樣做是否合理。他指出,唯一接受元組參數的字符串操作是 str.startswith() 和 str.endswith(),而它們不返回字符串(只是一個布爾值)。他懷疑添加這一種接收元組參數卻返回字符串的方法,因為無論選擇何種規則來處理元組,對於某些人來說都是“錯誤的”選擇。

例如:

這裏的困難在於,如果兩個或多個前綴都能匹配,則“剪切這些前綴中的一個”的概念是模稜兩可的。對 startwith 沒有區別:

 "extraordinary".startswith(('ex', 'extra'))

因為是從左到右,從最短到最大,甚至是隨機順序匹配都為True。但是對於 cutprefix,應該刪除哪個前綴?

如他所說,建議的規則是使用從左到右處理元組的第一個匹配字符串,但是有些人可能想要最長的匹配或最後一個匹配;這一切都取決於使用的上下文。他建議在提交添加此類行為之前,要給該功能更多的“浸泡時間”(譯註:即預備時間):“在添加多前綴/後綴的支持之前,我們首先應該對簡單的情況進行一些實際的體驗。”

伊桑·弗曼(Ethan Furman)同意達普拉諾(D’Aprano)的意見。但是維克托·斯汀納(Victor Stinner)強烈贊成元組參數的想法,只不過,他還想知道當傳入的元組有空字符串時,會怎麼處理。根據 PEP 提議,在處理元組時遇到空字符串(實際上可以匹配任何內容)只會返回原始字符串,這會導致令人驚訝的結果:

cutsuffix("Hello World", ("", " World"))    # 返回 "Hello World"
cutsuffix("Hello World", (" World", ""))    # 返回 "Hello"

這個例子不太明顯;詞綴不一定是硬編碼的,因此空字符串可能會溜進意想不到的位置。Stinner 建議如果遇到空字符串,則拋出 ValueError,類似於 str.split()。但是 Sweeney 決定完全刪除元組參數功能,以便“允許對此有更強見解的人在另外的 PEP 中提出並捍衛一系列的語義”。他在 3 月 28 日發布了該 PEP 的最新版本。

4 月 9 日,Sweeney 發起了一個指導委員會 issue,請求對其 PEP 進行評審。4 月 20 日,Stinner 代表委員會接受了該提案。

這是一個很小的更改,但值得花時間確保它具有長期適用的接口(和語義)。我們將在 Python 3.9 中看到 removeprefix() 和removesuffix()。

2、新解析器

並不令人感到驚訝的是,指導委員會已經接受了我們在 4 月中旬介紹過的 CPython 新解析器。PEP 617(“CPython 新的 PEG 解析器”)由 Python 創始人即前仁慈的獨裁者(BDFL) Guido van Rossum 以及 Pablo Galindo Salgado 和 Lysandros Nikolaou 共同提出。

它已經運行良好,並且在現有解析器的速度和內存使用方面提升了 10% 以內的性能。由於解析器是基於解析表達語法(PEG),因此也將簡化語言規範。CPython 現有的 LL(1) 解析器存在諸多缺點和一些 hack,新的解析器將會消除掉。

這一更改為 Python 超越 LL(1) 語法鋪平了道路,儘管現有語言並不完全是 LL(1)。這一更改不會太快,因為計劃是在 Python 3.9 的命令行中提供開關,保持現有解析器可用。

但是 Python 3.10 將刪除現有的解析器,這可能會導致語言變更。如果做了那些更改,那麼,其它的 Python 實現(例如 PyPy 和 MicroPython)就需要切換解析器的 LL(1) 實現,以便跟上語言規範的要求。這可能會使核心開發者暫停進行此類更改。

3、更多內容

我們在三月初查看了 PEP 615(“在標準庫中支持 IANA 時區數據庫”)。它將在標準庫中添加一個zoneinfo 模塊,該模塊將有助於從 IANA 時區數據庫中(也稱為“Olson數據庫”)獲取時區信息,以填充時區對象。在撰寫本文時,它看起來很順利。

在 3 月底,Paul Ganssle 請求就該 PEP 作出決議。他認為在一個有趣的時間範圍內接受它,可能會很有趣:

… 我希望(出於異想天開的原因)在 4 月 5 日(星期日)UTC 時間 02:00-04:00 或 13:00-17:30 之間接受它,因為這些時間代表着地球上某些地方的不明確時間(主要在澳大利亞)。還有另一個時機,那就是在 4 月 19 日星期日 UTC 01:00-03:00 之間,這段時間在西撒哈拉是不明確的。

他意識到這可能難以實現,它當然不是優先考慮的事。指導委員會沒有錯過第二個時間窗太多;Barry Warsaw 於 4 月 20 日宣布接受該 PEP。

Python 現在將具有一種機制來訪問系統的時區數據庫,以創建和處理時區。另外,Python 軟件包索引(PyPI)中有一個 tzdata 模塊,它為缺少 IANA 數據的系統提供這些數據;它將由 Python 核心開發者維護。

PEP 593(“靈活的函數和變量註釋”)添加了一種將上下文特定的(context-specific)元數據與函數和變量關聯的方法。實際上,type hint 註解已擠出了很多年前在 Python 3.0 中實現的 PEP 3107(“函數註釋”)中設想的其它用例。PEP 593 使用註解的(Annotated)類型提示為這些用例創建了一種新的機制。

PEP 585(“標準集合中的類型提示泛型”)提供了另一種清除方法。它將允許刪除在 typing 模塊中維護的一組并行的類型別名,以支持泛型。例如,type.List 類型將不再需要支持諸如“dict[str,list[int]]”之類的註解(例如,一個帶有字符串鍵和整數列表的值的字典)。

字典“加法”的聯合操作也會是 Python 3.9 的一部分。它曾不時引起爭議,但是 2 月中旬,PEP 584(“給字典添加聯合操作符”)被 Van Rossum 推薦採納。指導委員會迅速同意了,該特性於 2 月 24 日合入。

最後一個 PEP 是 PEP 602(“Python 的年度發布周期”)。如提案所書,它將發布節奏從每 18 個月更改為每年一次。但是,開發和發布周期是重疊的,因此整個功能開發需要 12 個月的時間。當第一個 Python 3.9 beta 版本發布時(即現在),Python 3.10 的功能開發就開始了。請繼續關注來年的下一輪 PEP。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※教你寫出一流的銷售文案?

※廣告預算用在刀口上,台北網頁設計公司幫您達到更多曝光效益

※回頭車貨運收費標準

※別再煩惱如何寫文案,掌握八大原則!

※超省錢租車方案

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※推薦台中搬家公司優質服務,可到府估價

Spring Boot 教程 – Elasticsearch

1. Elasticsearch簡介

Elasticsearch是一個基於Lucene的搜索服務器。它提供了一個分佈式多用戶能力的全文搜索引擎,基於RESTful web接口。Elasticsearch是用Java語言開發的,並作為Apache許可條款下的開放源碼發布,是一種流行的企業級搜索引擎。Elasticsearch用於雲計算中,能夠達到實時搜索,穩定,可靠,快速,安裝使用方便。官方客戶端在Java、.NET(C#)、PHP、Python、Apache Groovy、Ruby和許多其他語言中都是可用的。根據DB-Engines的排名显示,Elasticsearch是最受歡迎的企業搜索引擎,其次是Apache Solr,也是基於Lucene。以後再給大家詳細介紹solr。

它能很方便的使大量數據具有搜索、分析和探索的能力。充分利用Elasticsearch的水平伸縮性,能使數據在生產環境變得更有價值。Elasticsearch 的實現原理主要分為以下幾個步驟,首先用戶將數據提交到Elasticsearch 數據庫中,再通過分詞控制器去將對應的語句分詞,將其權重和分詞結果一併存入數據,當用戶搜索數據時候,再根據權重將結果排名,打分,再將返回結果呈現給用戶。

Elasticsearch可以用於搜索各種文檔。它提供可擴展的搜索,具有接近實時的搜索,並支持多租戶。”Elasticsearch是分佈式的,這意味着索引可以被分成分片,每個分片可以有0個或多個副本。每個節點託管一個或多個分片,並充當協調器將操作委託給正確的分片。再平衡和路由是自動完成的。“相關數據通常存儲在同一個索引中,該索引由一個或多個主分片和零個或多個複製分片組成。一旦創建了索引,就不能更改主分片的數量。

Elasticsearch使用Lucene,並試圖通過JSON和Java API提供其所有特性。它支持facetting和percolating,如果新文檔與註冊查詢匹配,這對於通知非常有用。另一個特性稱為“網關”,處理索引的長期持久性;例如,在服務器崩潰的情況下,可以從網關恢復索引。Elasticsearch支持實時GET請求,適合作為NoSQL數據存儲,但缺少分佈式事務。

2. Elasticsearch深入了解

2.1 Elasticsearch的底層實現

  • 2.1.1 lucene

    Es是一個比較複雜的搜索服務器,本身也是使用Java語言編寫的,在上面的簡介中,說明了ES是一個基於lucene的搜索服務器,lucene是什麼呢?Lucene是apache軟件基金會4 jakarta項目組的一個子項目,是一個開放源代碼的全文檢索引擎工具包,但它不是一個完整的全文檢索引擎,而是一個全文檢索引擎的架構,提供了完整的查詢引擎和索引引擎,部分文本分析引擎。lucene也是使用Java語言編寫的,Java天下第一!

    Lucene是一套用於全文檢索和搜尋的開源程式庫,由Apache軟件基金會支持和提供。Lucene提供了一個簡單卻強大的應用程式接口,能夠做全文索引和搜尋。在Java開發環境里Lucene是一個成熟的免費開源工具。就其本身而言,Lucene是當前以及最近幾年最受歡迎的免費Java信息檢索程序庫。至於lucene到底是怎麼實現的,牛牛們可能要自己去百度或者谷歌一下啦。

  • 2.1.2 Elasticsearch的基本概念

    1. 集群(Cluster):就是多台ES服務器在一起構成搜索服務器,現在很多應用基本上都有集群的概念,提高性能,讓應用具有高可用性,一台服務器掛掉,可以很快有另一台ES服務器補上。

    2. 節點(Node):節點就是集群中的某一台ES服務器就稱為一個節點。

    3. 索引庫(Index Indices):就是ES服務器上的某一個索引,相當於Mysql數據庫中的數據庫的概念,一個節點可以有很多個索引庫。

    4. 文檔類型(Type):這個概念就相當於Mysql數據庫中表的概念,一個索引庫可以有很多個文檔類型,但是這個概念現在慢慢淡化了,因為在ES中一個索引庫直接存數據文檔就挺好的,這個概念現在來說有點多餘了,所以ES官方也在淡化這個概念,在ES8中,這個概念將會徹底的消失。

    5. 文檔(Doc):文檔就相當於Mysql是數據庫中某個表的一條數據記錄,現在ES已經到7.7版本了,我們也就忽略type這個概念,直接在索引庫中存文檔即可。另外需要說一下,我們一般把數據文檔存到Es服務器的某個索引庫的這個動作稱之為索引

      最後還有兩個比較重要的概念,但是可能不是那麼直觀的可以感受得到:

      分片(Shards)和副本(Replicas)

      索引可能會存儲大量數據,這些數據可能超過單個節點的硬件限制。例如,十億個文檔的單個索引佔用了1TB的磁盤空間,可能不適合單個節點的磁盤,或者可能太慢而無法單獨滿足來自單個節點的搜索請求。

      為了解決此問題,Elasticsearch提供了將索引細分為多個碎片的功能。創建索引時,只需定義所需的分片數量即可。每個分片本身就是一個功能齊全且獨立的“索引”,可以託管在群集中的任何節點上。

      分片很重要,主要有兩個原因:

      • 它允許您水平分割/縮放內容量
      • 它允許您跨碎片(可能在多個節點上)分佈和并行化操作,從而提高性能/吞吐量

      分片如何分佈以及其文檔如何聚合回到搜索請求中的機制由Elasticsearch完全管理,並且對您作為用戶是透明的。

      在隨時可能發生故障的網絡/雲環境中,非常有用,強烈建議您使用故障轉移機制,以防碎片/節點因某種原因脫機或消失。為此,Elasticsearch允許您將索引分片的一個或多個副本製作為所謂的副本分片(簡稱副本)。

      複製很重要,主要有兩個原因:

      • 如果分片/節點發生故障,它可提供高可用性。因此,重要的是要注意,副本碎片永遠不會與從其複製原始/主要碎片的節點分配在同一節點上。
      • 由於可以在所有副本上并行執行搜索,因此它可以擴展搜索量/吞吐量。

      總而言之,每個索引可以分為多個碎片。索引也可以複製零(表示沒有副本)或多次。複製后,每個索引將具有主碎片(從中進行複製的原始碎片)和副本碎片(主碎片的副本)。可以在創建索引時為每個索引定義分片和副本的數量。創建索引后,您可以隨時動態更改副本數,但不能事後更改分片數。

      默認情況下,Elasticsearch中的每個索引分配有5個主碎片和1個副本,這意味着如果集群中至少有兩個節點,則索引將具有5個主碎片和另外5個副本碎片(1個完整副本),總共每個索引10個碎片。

  • 2.1.3 Elasticsearch的索引原理

    Es作為一個全文檢索服務器,那麼它在搜索方面肯定很在行啦!那它是怎麼做到的呢?

    Es官方有這麼一句話:一切設計都是為了提高搜索的性能!

    Es能夠快速的搜索出我們需要的內容,靠的就是倒排索引的思想,或者說是一種設計!

    在沒有使用倒排索引的情況下,正常思路是根據搜索關鍵字去查找相應的內容,但是使用了倒排索引之後,ES會先將文檔的所有內容拆分成多個詞條,創建一個包含所有不重複詞條的排序列表,然後列出每個詞條出現在哪個文檔。

    例如,假設我們有兩個文檔,每個文檔的 content 域包含如下內容:

    ​ Doc_1:The quick brown fox jumped over the lazy dog

    ​ Doc_2:Quick brown foxes leap over lazy dogs in summer

    ES首先會將這兩個文檔拆分成多個單獨的詞,或者叫做詞條,然後為所有的詞條創建一個排序列表,並記錄每個詞條出現的文檔的信息。就像下面這樣:

    Term      Doc_1  Doc_2
    -------------------------
    Quick   |       |  X                        /*
    The     |   X   |								Term就是詞條,比如第一個Term就是Quick關鍵字,在Doc_1中不存
    brown   |   X   |  X							在,在Doc_2中存在,其他的以此類推。
    dog     |   X   |							*/
    dogs    |       |  X
    fox     |   X   |
    foxes   |       |  X
    in      |       |  X
    jumped  |   X   |
    lazy    |   X   |  X
    leap    |       |  X
    over    |   X   |  X
    quick   |   X   |
    summer  |       |  X
    the     |   X   |
    ------------------------
    

    現在,如果我們想搜索 quickbrown這兩個關鍵字,我們只需要查找包含每個詞條的文檔,就相當於我們查詢的時候,是通過這個索引表找到文檔,在通過文檔去找文檔內容中的搜索關鍵字,與傳統的通過關鍵字去找內容是不同的。

    倒排索引到底是個怎麼實現的,怎麼個思想,我在這裏就不一一說明了,大家可以看下官方的詳細介紹:倒排索引的原理

    還有es官方的一系列的說明也都可以了解一下:什麼是Elasticsearch?

2.2 Elasticsearch的安裝

本演示項目ES版本為7.0.0版本,其他版本的ES的maven依賴與其他的jar包關係請自行查閱官方文檔,保證不衝突。

  • Windows

    Es服務器的安裝很簡單,Windows版本特別的簡單,直接去官網下載,運行 bin/elasticsearch 或者bin\elasticsearch.bat

  • Linux(CentOS7)

    首先我們去官網下載ES的tar.gz包,然後自建一個文件夾放好,然後解壓tar.zg壓縮包:

    tar -xvf elasticsearch-7.0.0.tar.gz
    

    然後進入到bin目錄下:

    cd elasticsearch-7.0.0/bin
    

    然後運行elasticsearch:

    ./elasticsearch
    

    這個時候肯定會報錯的,因為沒有進行配置,所以我們先對es進行一些簡單的配置,保證能單機運行,進入elasticsearch-7.7.0/config目錄,對es的核心配置文件進行編輯:

    vim elasticsearch.yml
    

    進入到了elasticsearch.yml文件的編輯頁面:

    首先我們配置集群名稱,集群名稱自己取一個喜歡的名字就好:

    接下來配置節點名稱,就是在這個集群中,這個es服務器的名稱:

    接下來配置一些必要的參數:

    bootstrap.memory_lock: 是否鎖住內存,避免交換(swapped)帶來的性能損失,默認值是: false。

    bootstrap.system_call_filter: 是否支持過濾掉系統調用。elasticsearch 5.2以後引入的功能,在bootstrap的時候check是否支持seccomp。

    配置network為所有人都可以訪問,因為我們一般是使用ssh連接工具在其他的電腦上操作Linux系統,所以我們需要配置一下:

    到這裏就配置完成了,但是當你重新去運行.elasticsearch的可執行文件的時候,依然會報錯。

    報錯信息中可能包含以下幾個錯誤:

    • max file descriptors [4096] for elasticsearch process is too low, increase to at least [65536]

      原因:無法創建本地文件問題,用戶最大可創建文件數太小。

      解決方法:切換到root賬戶下,進入Linux系統文件夾,編輯limits.conf文件:

      vim /etc/security/limits.conf
      

      在文件的末尾加上:

      *                soft    nofile          65536
      *                hard    nofile          65536
      *                soft    nproc           4096
      *                hard    nproc           4096
      
    • max virtual memory areas vm.max_map_count [65530] is too low, increase to at least [262144]

      原因:最大虛擬內存太小,需要修改系統變量的最大值。

      解決方法:切換到root賬戶下,進入Linux系統文件夾,編輯sysctl.conf文件:

      vim /etc/sysctl.conf
      

      在文件的末尾加上:

      vm.max_map_count=262144
      
    • max number of threads [1024] for user [es] likely too low, increase to at least [2048]

      原因:無法創建本地線程問題,用戶最大可創建線程數太小。

      解決方法:如果你是CentOS6及以下系統,編輯的文件是90-nproc.conf這個文件,如果你和我一樣使用的是CentOS7的話,編輯的文件是20-nproc.conf文件,其實這兩個文件是一樣的,只是在不同CentOS系統中名稱不一樣而已。

      CentOS7使用這個命令:

      vim /etc/security/limits.d/20-nproc.conf
      

      CentOS6使用這個命令:

      vim /etc/security/limits.d/90-nproc.conf
      

      只需要在文件中加上以下配置:

      *          soft    nproc     4096
      

      這個配置的意思是說賦予其他用戶的可創建本地線程數為4096。在這個文件中本來就有一個配置,意思是說賦予root賬戶創建線程數不受限制。我們就把上面的配置加在本來存在的配置的下面一行就可以了。

      如果是CentOS7的使用者,還需要配置另一個文件,否則這個最大線程數是不會生效的。CentOS 7 使用systemd替換了SysV,Systemd目的是要取代Unix時代以來一直在使用的init系統,兼容SysV和LSB的啟動腳本,而且夠在進程啟動過程中更有效地引導加載服務。在/etc/systemd目錄下有一個系統的默認管理配置,這裡有登陸、日誌、服務、系統等。所以CentOS7的使用者還需要配置下面這個文件:

      vim /etc/systemd/system.conf
      

      對其中的選項進行配置,在文件的末尾加上:

      DefaultLimitNOFILE=65536
      DefaultLimitNPROC=4096
      

    上面的所以錯誤解決完畢之後,我們再運行.elasticsearch可執行文件,es才可以啟動成功。

2.3 Elasticsearch的使用

首先給大家介紹一個谷歌瀏覽器插件,這個插件是用來可視化展示es的索引庫數據的,這個插件叫做ElasticVue,個人感覺挺好用的,展示也比較方便,給大家截個圖看看:

大家可以使用這個建立索引庫,然後調用es官方的es專用的語法操作es服務器進行CRUD操作,但是此處我只介紹Java語言如何調用es服務器API,廢話不多說,我們直接開始下一步。

  • 2.3.1 引入依賴

    搭建工程的過程我就不演示了,直接上pom.xml依賴文件。

    pom.xml

    <!--springboot父工程-->
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>2.2.2.RELEASE</version>
            <relativePath/> <!-- lookup parent from repository -->
        </parent>
    
        <dependencies>
            <!--springboot-web組件-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
                <version>2.2.2.RELEASE</version>
            </dependency>
            <!--elasticsearch-rest-client組件-->
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-client</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--elasticsearch-rest-high-level-client組件-->
            <dependency>
                <groupId>org.elasticsearch.client</groupId>
                <artifactId>elasticsearch-rest-high-level-client</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--elasticsearch組件-->
            <dependency>
                <groupId>org.elasticsearch</groupId>
                <artifactId>elasticsearch</artifactId>
                <version>7.7.0</version>
            </dependency>
            <!--mybatis整合springboot組件-->
            <dependency>
                <groupId>org.mybatis.spring.boot</groupId>
                <artifactId>mybatis-spring-boot-starter</artifactId>
                <version>2.1.0</version>
            </dependency>
            <!--mysql數據庫連接驅動-->
            <dependency>
                <groupId>mysql</groupId>
                <artifactId>mysql-connector-java</artifactId>
                <version>8.0.18</version>
            </dependency>
            <!--lombok組件-->
            <dependency>
                <groupId>org.projectlombok</groupId>
                <artifactId>lombok</artifactId>
                <version>1.18.10</version>
            </dependency>
            <!--json組件gson-->
            <dependency>
                <groupId>com.google.code.gson</groupId>
                <artifactId>gson</artifactId>
                <version>2.8.5</version>
            </dependency>
            <!--springboot-test組件-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-test</artifactId>
            </dependency>
            <!--單元測試junit組件-->
            <dependency>
                <groupId>junit</groupId>
                <artifactId>junit</artifactId>
                <version>4.12</version>
                <scope>test</scope>
            </dependency>
            <!--spring-test組件-->
            <dependency>
                <groupId>org.springframework</groupId>
                <artifactId>spring-test</artifactId>
                <version>5.2.2.RELEASE</version>
                <scope>test</scope>
            </dependency>
        </dependencies>
    
        <build>
            <!--springboot的maven插件-->
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                </plugin>
                <plugin>
                    <groupId>org.apache.maven.plugins</groupId>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <configuration>
                        <compilerArgs>
                            <arg>-parameters</arg>
                        </compilerArgs>
                    </configuration>
                </plugin>
            </plugins>
        </build>
    
  • 2.3.2 Elasticsearch的配置類和Gson配置類和應用配置文件

    application.yml

    butterflytri:
      databaseurl-port: 127.0.0.1:3306 # 數據庫端口
      database-name: student_db # 數據庫名
      host: 192.168.129.100:9200 # es服務端
    server:
      port: 8080 # 應用端口
      servlet:
        context-path: /butterflytri # 應用映射
    spring:
      application:
        name: mybatis # 應用名稱
      datasource:
        url: jdbc:mysql://${butterflytri.databaseurl-port}/${butterflytri.database-name}?useUnicode=true&characterEncoding=UTF-8&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
        driver-class-name: com.mysql.jdbc.Driver
        username: root
        password: root
    mybatis:
      type-aliases-package: com.butterflytri.entity # entity別名
      mapper-locations: classpath:com/butterflytri/mapper/*Mapper.xml # mapper映射包掃描
    

    注意:yml文件中的192.168.129.100:9200是es對外的端口,使用的http協議進行操作,es服務器還有個9300端口,這個端口是es集群中各個節點進行交流的端口,使用的是tcp協議。所以我們連接的時候,端口要使用9200端口。

    項目啟動類沒有什麼特別的東西,就不展示了。

    ElasticsearchConfig.java

    package com.butterflytri.config;
    
    import org.apache.http.HttpHost;
    import org.elasticsearch.client.RestClient;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.springframework.beans.factory.DisposableBean;
    import org.springframework.beans.factory.FactoryBean;
    import org.springframework.beans.factory.InitializingBean;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: ElasticSearchConfig
     */
    @Configuration
    public class ElasticSearchConfig implements FactoryBean<RestHighLevelClient>, InitializingBean, DisposableBean {
    
        /**
         * {@link FactoryBean<T>}:FactoryBean<T>是spring對外提供的對接接口,當向spring對象使用getBean("..")方法時,
         *                         spring會使用FactoryBean<T>的getObject 方法返回對象。所以當一個類實現的factoryBean<T>接口時,
         *                         那麼每次向spring要這個類時,spring就返回T對象。
         *
         * {@link InitializingBean}:InitializingBean接口為bean提供了初始化方法的方式,它只包括afterPropertiesSet方法,
         *                          凡是繼承該接口的類,在初始化bean的時候會執行該方法。在spring初始化bean的時候,如果該bean是
         *                          實現了InitializingBean接口,並且同時在配置文件中指定了init-method,系統則是
         *                          先調用afterPropertiesSet方法,然後在調用init-method中指定的方法。
         *
         * {@link DisposableBean}:DisposableBean接口為bean提供了銷毀方法destroy-method,會在程序關閉前銷毀對象。
         */
    
        @Value("#{'${butterflytri.host}'.split(':')}")
        private String[] host;
    
        private RestHighLevelClient restHighLevelClient;
    
        private RestHighLevelClient restHighLevelClient() {
            restHighLevelClient = new RestHighLevelClient(
    
                    RestClient.builder(new HttpHost(host[0],Integer.valueOf(host[1]),"http"))
    
            );
            return restHighLevelClient;
        }
    
        @Override
        public void destroy() throws Exception {
            restHighLevelClient.close();
        }
    
        @Override
        public RestHighLevelClient getObject() throws Exception {
            return restHighLevelClient;
        }
    
        @Override
        public Class<?> getObjectType() {
            return RestHighLevelClient.class;
        }
    
        @Override
        public void afterPropertiesSet() throws Exception {
            restHighLevelClient();
        }
    
    }
    

    ES的配置類,這個配置類實現了三個接口,三個接口的作用我也寫上了註釋,大家可以看下,需要注意的是FactoryBean這個接口,一但實現了這個接口,每當你需要使用泛型表示的對象T的時候,Spring不會從容器中去拿這個對象,而是會調用這個FactoryBean.getObject()方法去拿對象。其他的就沒有什麼了。

    Gson.java

    Gson是一個操作json數據的類,它的執行效率可能會慢一點,但是它在解析json數據的時候不會出Bug。

    package com.butterflytri.config;
    
    import com.google.gson.Gson;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: GsonConfig
     */
    @Configuration
    public class GsonConfig {
    
        /**
         * {@link Gson}:一個操作json的對象,有比較好的json操作體驗,相對於Alibaba的FastJson來說速度慢一些,但是FastJson在解析
         *              複雜的的json字符串時有可能會出現bug。
         * @return Gson
         */
    
        @Bean
        public Gson gson() {
            return new Gson();
        }
    
    }
    

    Constants.java

    這是我寫的常量類,放一些ES使用的常量,直接寫字符串也行,但是我建議這樣做。

    package com.butterflytri.constants;
    
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: Constants
     */
    public class Constants {
    
        /**
         * es搜索關鍵字
         */
        public static final String KEYWORD = ".keyword";
    
        /**
         * es的type類型:type字段將在 elasticsearch-version:8 中徹底刪除,本來就覺得沒得啥用。
         */
        public static final String DOC_TYPE = "_doc";
    
        /**
         * 學生信息索引類型
         */
        public static final String INDEX_STUDENT = "student_info";
    
    
        /**
         * 自定連接符
         */
        public static final String CONNECTOR = " --> ";
    
    }
    

    Student.java

    package com.butterflytri.entity;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.io.Serializable;
    
    /**
     * @author: WJF
     * @date: 2020/5/16
     * @description: Student
     */
    
    @ToString
    @Getter
    @Setter
    public class Student implements Serializable {
    
        private Long id;
    
        private String studentName;
    
        private String studentNo;
    
        private String sex;
    
        private Integer age;
    
        private String clazz;
    
    }
    

    StudentMapper.java

    package com.butterflytri.mapper;
    
    import com.butterflytri.entity.Student;
    import org.apache.ibatis.annotations.Mapper;
    
    import java.util.List;
    
    /**
     * @author: WJF
     * @date: 2020/5/16
     * @description: StudentMapper
     */
    @Mapper
    public interface StudentMapper {
    
        /**
         * 查詢所有學生信息
         * @return List<Student>
         */
        List<Student> findAll();
    
        /**
         * 通過id查詢學生信息
         * @param id:學生id
         * @return Student
         */
        Student findOne(Long id);
    
        /**
         * 通過學號查詢學生信息
         * @param studentNo:學生學號
         * @return Student
         */
        Student findByStudentNo(String studentNo);
    
    }
    

    mybatis的SQL映射文件我就不展示了,也很簡單,大家看接口方法名就應該可以想象得到SQL語句是怎樣的。

  • 2.3.3 索引數據到ES服務器

    IndexServiceImpl.java

    package com.butterflytri.service.impl;
    
    import com.butterflytri.constants.Constants;
    import com.butterflytri.entity.Student;
    import com.butterflytri.service.IndexService;
    import com.google.gson.Gson;
    import org.elasticsearch.action.ActionListener;
    import org.elasticsearch.action.index.IndexRequest;
    import org.elasticsearch.action.index.IndexResponse;
    import org.elasticsearch.client.RequestOptions;
    import org.elasticsearch.client.RestHighLevelClient;
    import org.elasticsearch.common.xcontent.XContentType;
    import org.springframework.stereotype.Service;
    
    import javax.annotation.Resource;
    import java.io.IOException;
    
    /**
     * @author: WJF
     * @date: 2020/5/22
     * @description: IndexServiceImpl
     */
    @Service
    public class IndexServiceImpl implements IndexService {
    
        @Resource
        private Gson gson;
    
        @Resource
        private RestHighLevelClient restHighLevelClient;
    
        @Override
        public String index(Student student) {
            StringBuilder builder = new StringBuilder();
            IndexRequest indexRequest = this.initIndexRequest(student);
            try {
                // 同步索引到elasticsearch服務器,獲取索引響應IndexResponse
                IndexResponse indexResponse = restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT);
                String statusName = indexResponse.status().name();
                int statusCode = indexResponse.status().getStatus();
                builder.append(statusName).append(Constants.CONNECTOR).append(statusCode);
            } catch (IOException e) {
                builder.append("Fail").append(Constants.CONNECTOR).append(e.getMessage());
            }
            return builder.toString();
        }
    
    
        @Override
        public String indexAsync(Student student) {
            StringBuilder builder = new StringBuilder();
            IndexRequest indexRequest = this.initIndexRequest(student);
            // 異步索引到elasticsearch服務器,獲取索引響應IndexResponse
            restHighLevelClient.indexAsync(indexRequest, RequestOptions.DEFAULT,actionListener(builder));
            return builder.toString();
        }
    
    
    
        /**
         * 初始化IndexRequest,並設置數據源。
         * @param student
         * @return IndexRequest
         */
        private IndexRequest initIndexRequest(Student student) {
            // 構建IndexRequest,設置索引名稱,索引類型,索引id
            IndexRequest indexRequest = new IndexRequest(Constants.INDEX_STUDENT);
            // 可以不設置,默認就是'_doc'
            indexRequest.type(Constants.DOC_TYPE);
            // 設置索引id為studentId
            indexRequest.id(String.valueOf(student.getId()));
            // 設置數據源
            String studentJson = gson.toJson(student);
            indexRequest.source(studentJson, XContentType.JSON);
            return indexRequest;
        }
    
        /**
         * 異步索引的回調監聽器,根據不同的結果做出不同的處理
         * @param builder
         * @return ActionListener<IndexResponse>
         */
        private ActionListener<IndexResponse> actionListener(StringBuilder builder) {
            return new ActionListener<IndexResponse>() {
                // 當索引數據到es服務器時,返回不同的狀態
                @Override
                public void onResponse(IndexResponse indexResponse) {
                    String statusName = indexResponse.status().name();
                    int statusCode = indexResponse.status().getStatus();
                    builder.append(statusName).append(Constants.CONNECTOR).append(statusCode);
                }
    
                // 當索引數據時出現異常
                @Override
                public void onFailure(Exception e) {
                    builder.append("Fail").append(Constants.CONNECTOR).append(e.getMessage());
                }
            };
        }
    }
    

    上面的內容很簡單,就是將Student對象格式化為Json字符串,然後存到es服務器中,大家只要遵守一個規則就好,就是操作es服務器,不管是什麼操作都是用RestHighLevelClient這個類去操作,上面的就是student對象索引的es服務器中,使用restHighLevelClient.index(indexRequest, RequestOptions.DEFAULT),首先就是構建indexRequest對象,這個對象就是索引請求對象,具體幹了什麼看代碼上的註釋。這裏還有個restHighLevelClient.indexAsync()這個方法,這個方法和上面的index方法一樣的效果,只不過是異步調用。

    接下來我們測試一下這個代碼,請看:

    @Test
        public void indexTest() {
            List<Student> list = studentMapper.findAll();
            for (Student student : list) {
                String message = indexService.index(student);
                System.out.println(message);
            }
        }
    

    我們使用ElasticVue插件連接es服務器即可看到有一個索引庫:

    當我們點擊到show按鈕的時候,可以看到student_info索引庫中有幾條記錄:

    索引數據到數據庫成功了。

  • 2.3.4 獲取Es服務器數據

    獲取數據,是es提供給我們的API,這個Api只能獲取某個索引的某一條文檔,示例如下:

    GetServiceImpl.java

    	@Override
        public Student get(String id) {
            Student student = new Student();
            GetRequest getRequest = new GetRequest(Constants.INDEX_STUDENT, id);
            try {
                GetResponse getResponse = restHighLevelClient.get(getRequest, RequestOptions.DEFAULT);
                String source = getResponse.getSourceAsString();
                student = gson.fromJson(source, Student.class);
            } catch (IOException e) {
                e.printStackTrace();
            }
            return student;
        }
    

    接着我們在測試類中,調用這個方法然後打印一下結果:

    GetServiceTest.java

    	@Test
        public void getTest() {
            Student student = getService.get("1");
            System.out.println(student);
        }
    

    結果如下:

    更新數據文檔和刪除數據文檔我就不演示了,都是大同小異,大家可以拉下我的代碼,好好研究一下,都有詳細的註釋,覺得可以的話,給我點下star也是極好的。下面演示一下searchApi,這個Api是我們經常需要使用的,特別重要。

  • 2.3.5 搜索Es服務器數據

    ES的搜索API包含很多,比如說組合搜索,區間搜索,高亮显示,分詞搜索等等。我先給大家演示一下組合搜索,區間搜索其實也是組合搜索的一個子條件,其他的搜索其實也都是,代碼如下:

    SearchServiceImpl.java

    	@Override
        public List<Student> searchRange(Object from, Object to, String field, String index) {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            // 需要搜索的區間字段field
            RangeQueryBuilder rangeQueryBuilder = QueryBuilders.rangeQuery(field);
            // 左區間
            if (from != null) {
                rangeQueryBuilder.from(from, true);
            }
            // 右區間
            if (to != null) {
                rangeQueryBuilder.to(to, true);
            }
            boolQueryBuilder.must();
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQueryBuilder);
            SearchRequest searchRequest = new SearchRequest(index);
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }
    

    上面的代碼其實很簡單,就是一個區間查詢構建器,查詢指定字段處於區間的所有數據,rangeQueryBuilder.from(from, true)的第一個參數就是字段的下邊界,第二個參數代表是否包含邊界。SearchResponse就是搜索的響應對象,所有的數據都在SearchHit對象中。

    接下來給大家演示一些組合查詢,這個方法搜索年齡在18到19歲並且班級為’G0305’的學生。記得ES默認是分頁的,如果想不分頁,一定要記得給搜索字段加上.keyword(字符串加,数字不支持)。

    SearchServiceImpl.java

    @Override
        public List<Student> searchBool() {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            boolQuery.must(QueryBuilders.rangeQuery("age").gte(18).lte(19));
            boolQuery.must(QueryBuilders.termQuery("clazz" + Constants.KEYWORD,"G0305"));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQuery);
            SearchRequest searchRequest = new SearchRequest(Constants.INDEX_STUDENT);
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }
    

    上面的代碼中的類BoolQueryBuilder就是組合查詢構建器,這個類可以用來構建組合的條件查詢。boolQuery.must()方法就是用來拼接條件的一種方式,使用這個方法代表必須滿足這個條件才會查詢出來,上面的代碼說明必須滿足年齡為18(包含18)到19(包含19)歲,並且班級為’G0305’的學生才會查詢出來。還有其他的一些常見的組合查詢方法,如下:

    • boolQuery.must():必須滿足此條件,相當於=或者&
    • boolQuery.mustNot():必須不滿足此條件,相當於!=
    • boolQuery.should():相當於||或者or
    • boolQuery.filter():過濾。

    然後是聚合查詢,很類似於MySQL中的聚合函數,這個示例我就不再解釋了,代碼註釋很清楚:

    @Override
        public void searchBoolAndAggregation() {
            BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
            boolQuery.must(QueryBuilders.rangeQuery("age").gte(18).lte(19));
            boolQuery.must(QueryBuilders.termQuery("clazz" + Constants.KEYWORD,"G0305"));
            // 聚合分組:按clazz字段分組,並將結果取名為clazz,es默認是分詞的,為了精確配置,需要加上‘.keyword’關鍵詞後綴。
            TermsAggregationBuilder aggregationBuilder = AggregationBuilders.terms("clazz").field("clazz" + Constants.KEYWORD);
            // 聚合求和:求符合查詢條件的學生的年齡的和,並將結果取名為ageSum,因為不是字符串,所以默認是精確匹配,不支持分詞。
            aggregationBuilder.subAggregation(AggregationBuilders.sum("ageSum").field("age"));
            // 聚合求平均:求符合查詢條件的學生的年齡的平均值,並將結果取名為ageAvg,因為不是字符串,所以默認是精確匹配,不支持分詞。
            aggregationBuilder.subAggregation(AggregationBuilders.avg("ageAvg").field("age"));
            // 聚合求數量:按學號查詢符合查詢條件的學生個數,並將結果取名為count,es默認是分詞的,為了精確配置,需要加上‘.keyword’關鍵詞後綴。
            aggregationBuilder.subAggregation(AggregationBuilders.count("count").field("studentNo" + Constants.KEYWORD));
            SearchSourceBuilder builder = new SearchSourceBuilder();
            builder.query(boolQuery);
            builder.aggregation(aggregationBuilder);
            // 按年齡降序排序。
            builder.sort("age", SortOrder.DESC);
            SearchRequest request = new SearchRequest("student_info");
            request.source(builder);
            try {
                SearchResponse search = restHighLevelClient.search(request, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    System.out.println(student);
                }
                // 使用Terms對象接收
                Terms clazz = search.getAggregations().get("clazz");
                for (Terms.Bucket bucket : clazz.getBuckets()) {
                    System.out.println(bucket.getDocCount());
    
                    System.out.println("=====================");
                    // 使用ParsedSum對象接收
                    ParsedSum ageCount = bucket.getAggregations().get("ageSum");
                    System.out.println(ageCount.getType());
                    System.out.println(ageCount.getValue());
                    System.out.println(ageCount.getValueAsString());
                    System.out.println(ageCount.getMetaData());
                    System.out.println(ageCount.getName());
    
                    System.out.println("=====================");
                    // 使用ParsedAvg對象接收
                    ParsedAvg ageAvg = bucket.getAggregations().get("ageAvg");
                    System.out.println(ageAvg.getType());
                    System.out.println(ageAvg.getValue());
                    System.out.println(ageAvg.getValueAsString());
                    System.out.println(ageAvg.getMetaData());
                    System.out.println(ageAvg.getName());
    
                    System.out.println("=====================");
                    // 使用ParsedValueCount對象接收
                    ParsedValueCount count = bucket.getAggregations().get("count");
                    System.out.println(count.getType());
                    System.out.println(count.getValue());
                    System.out.println(count.getValueAsString());
                    System.out.println(count.getMetaData());
                    System.out.println(count.getName());
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    

    最後還有分詞查詢,分詞查詢就不加.keyword關鍵字即可。

    @Override
        public List<Student> searchMatch(String matchStudentName) {
            List<Student> list = new ArrayList<>();
            BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();
            // 分詞查詢時不加'.keyword'關鍵字
            boolQueryBuilder.must(QueryBuilders.matchQuery("studentName",matchStudentName));
            SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();
            searchSourceBuilder.query(boolQueryBuilder);
            SearchRequest searchRequest = new SearchRequest("student_info");
            searchRequest.source(searchSourceBuilder);
            try {
                SearchResponse search = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
                for (SearchHit hit : search.getHits().getHits()) {
                    String source = hit.getSourceAsString();
                    Student student = gson.fromJson(source, Student.class);
                    list.add(student);
                }
    
            } catch (IOException e) {
                e.printStackTrace();
            }
            return list;
        }
    

    請記住,一般的進行分詞都是字符串才進行分詞搜索,数字等類型只能是精準匹配。

    最後,ES功能很強大,作為搜索界的扛把子,ES的功能遠遠不止這些,它還可以高亮搜索,數據分析等等。我在這裏演示的僅僅只是皮毛,甚至都不是皮毛,僅作為初學者的參考。如有大佬覺得我哪裡寫錯了,或者有不同見解,歡迎留言。

3. 項目地址

本項目傳送門:

  • GitHub —> spring-boot-elasticsearch
  • Gitee —> spring-boot-elasticsearch

此教程會一直更新下去,覺得博主寫的可以的話,關注一下,也可以更方便下次來學習。

  • 作者:Butterfly-Tri
  • 出處:Butterfly-Tri個人博客
  • 版權所有,歡迎保留原文鏈接進行轉載

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

網頁設計最專業,超強功能平台可客製化

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※教你寫出一流的銷售文案?

台中搬家公司教你幾個打包小技巧,輕鬆整理裝箱!

台中搬家公司費用怎麼算?

一行代碼引來的安全漏洞就讓我們丟失了整個服務器的控制權

之前在某廠的某次項目開發中,項目組同學設計和實現了一個“引以為傲”,額,有點擴張,不過自認為還說得過去的 feature,結果臨上線前被啪啪打臉,因為實現過程中因為一行代碼(沒有標題黨,真的是一行代碼)帶來的安全漏洞讓我們丟失了整個服務器控制權(測試環境)。多虧了上線之前有公司安全團隊的人會對代碼進行掃描,才讓這個漏洞被扼殺在搖籃里。

下面我們就一起來看看這個事故,啊,不對,是故事。

背景說明

我們的項目是一個面向全球用戶的 Web 項目,用 SpringBoot 開發。在項目開發過程中,離不開各種異常信息的處理,比如表單提交參數不符合預期,業務邏輯的處理時離不開各種異常信息(例如網絡抖動等)的處理。於是利用 SpringBoot 各種現成的組件支持,設計了一個統一的異常信息處理組件,統一管理各種業務流程中可能出現的錯誤碼和錯誤信息,通過國際化的資源配置文件進行統一輸出給用戶。

統一錯誤信息配置管理

我們的用戶遍布全球,為了給各個國家用戶比較好的體驗會進行不同的翻譯。具體而言,實現的效果如下,為了方便理解,以“找回登錄密碼”這樣一個業務場景來進行闡述說明。

假設找回密碼時,需要用戶輸入手機或者郵箱驗證碼,假設這個時候用戶輸入的驗證碼通過後台數據庫(可能是Redis)對比發現已經過期。在業務代碼中,只需要簡單的 throw new ErrorCodeException(ErrorCodes.AUTHCODE_EXPIRED) 即可。具體而言,針對不同國家地區不同的語言看到的效果不一樣:

  • 中文用戶看到的提示就是“您輸入的驗證碼已過期,請重新獲取”;
  • 歐美用戶看到的效果是“The verification code you input is expired, …”;
  • 德國用戶看到的是:“Der von Ihnen eingegebene Verifizierungscode ist abgelaufen, bitte wiederholen” 。(我瞎找的翻譯,不一定準)
  • ……

統一錯誤信息配置管理代碼實現

關鍵信息其實就在於一個 GlobalExceptionHandler,對所有Controller 入口進行 AOP 攔截,根據不同的錯誤信息,獲取相應資源文件配置的 key,並從語言資源文件中讀取不同國家的錯誤翻譯信息。

@ControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BadRequestException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, BadRequestException e){
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(Response.error(e.getCode(), i18message));
    }
    
    @ExceptionHandler(ErrorCodeException.class)
    @ResponseBody
    public ResponseEntity handle(HttpServletRequest request, ErrorCodeException e){
        String i18message = getI18nMessage(e.getKey(), request);
        return ResponseEntity.status(HttpStatus.OK).body(Response.error(e.getCode(), i18message));
    }
}

 

不同語言的資源文件示例

private String getI18nMessage(String key, HttpServletRequest request) {
   try {
       return messageSource.getMessage(key, null, LanguaggeUtils.currentLocale(request));
   } catch (Exception e) {
       // log
       return key;
   }
}

 

詳細代碼實現可以參考本人之前寫的這篇文章一文教你實現 SpringBoot 中的自定義 Validator 和錯誤信息國際化配置,上面有附完整的代碼實現。

基於註解的表單校驗(含自定義註解)

還有一種常見的業務場景就是後端接口需要對用戶提交的表單進行校驗。以“註冊用戶”這樣的場景舉例說明, 註冊用戶時,往往會提交昵稱,性別,郵箱等信息進行註冊,簡單起見,就以這 3 個屬性為例。

定義的表單如下:

public class UserRegForm {
 private String nickname;
 private String gender;
 private String email;
}

 

對於表單的約束,我們有:

  • 昵稱字段:“nickname” 必填,長度必須是 6 到 20 位;
  • 性別字段:“gender” 可選,如果填了,就必須是“Male/Female/Other/”中的一種。說啥,除了男女還有其他?對,是的。畢竟全球用戶嘛,你去看看非死不可,還有更多。
  • 郵箱: “email”,必填,必須滿足郵箱格式。

對於以上約束,我們只需要在對應的字段上添加如下註解即可。

public class UserRegForm {
 @Length(min = 6, max = 20, message = "validate.userRegForm.nickname")
 private String nickname;

 @Gender(message="validate.userRegForm.gender")
 private String gender;

 @NotNull
 @Email(message="validate.userRegForm.email")
 private String email;
}

 

然後在各個語言資源文件中配置好相應的錯誤信息提示即可。其中, @Gender 就是一個自定義的註解。

基於含自定義註解的表單校驗關鍵代碼

自定義註解的實現主要的其實就是一個自定義註解的定義以及一個校驗邏輯。 例如定義一個自定義註解 CustomParam

@Documented
@Constraint(validatedBy = CustomValidator.class)
@Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomParam {
    String message() default "name.tanglei.www.validator.CustomArray.defaultMessage";

    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default { };

    @Documented
    @Retention(RetentionPolicy.RUNTIME)
    @Target({FIELD, METHOD, PARAMETER, ANNOTATION_TYPE})
    @interface List {
        CustomParam[] value();
    }
}

 

校驗邏輯的實現 CustomValidator

public class CustomValidator implements ConstraintValidator<CustomParam, String> {
    @Override
    public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
        if (null == s || s.isEmpty()) {
            return true;
        }
        if (s.equals("tanglei")) {
            return true;
        } else {
            error(constraintValidatorContext, "Invalid params: " + s);
            return false;
        }
    }

    @Override
    public void initialize(CustomParam constraintAnnotation) {
    }

    private static void error(ConstraintValidatorContext context, String message) {
        context.disableDefaultConstraintViolation();
        context.buildConstraintViolationWithTemplate(message).addConstraintViolation();
    }
}

 

上面例子只為了闡述說明問題,其中校驗邏輯沒有實際意義,這樣,如果輸入參數不滿足條件,就會明確提示用戶輸入的哪個參數不滿足條件。例如輸入參數 xx,則會直接提示:Invalid params: xx

這個跟第一部分的處理方式類似,因為現有的 validator 組件實現中,如果違反相應的約束也是一種拋異常的方式實現的,因此只需要在上述的 GlobalExceptionHandler中添加相應的異常信息即可,這裏就不詳述了。 這不是本文的重點,這裏就不詳細闡述了。 詳細代碼實現可以參考本人之前寫的這篇文章一文教你實現 SpringBoot 中的自定義 Validator 和錯誤信息國際化配置,上面有附完整的代碼實現。

場景重現

一切都顯得很完美,直到上線前代碼提交至安全團隊掃描,就被“啪啪打臉”,掃描報告反饋了一個嚴重的安全漏洞。而這個安全漏洞,屬於很高危的遠程代碼執行漏洞。

用前文提到的自定義 Validator,輸入的參數用: “1+1=${1+1}”,看看效果:

太 TM 神奇了,居然幫我運算出來了,返回 "message": "Invalid params: 1+1=2"

問題就出現在實現自定義註解進行校驗的這行代碼(如下圖所示):

其實,最開始的時候,這裏直接返回了“Invalid params”,當初為了更好的用戶體驗,要明確告訴用戶哪個參數沒有通過校驗,因此在輸出的提示上加上了用戶輸入的字段,也就是上面的"Invalid params: " + s,沒想到,這闖了大禍了(回過頭來想,感覺這裏沒必要這麼詳細啊,因為前端已經有相應的校驗了,正常情況下回攔住,針對不守規矩的用非常規手段來的接口請求,直接返回校驗不通過就行了,畢竟不是對外提供的 OpenAPI 服務)。

仔細看,這個方法實際上是 ConstraintValidatorContext這個接口中聲明的,看方法名字其實能知道輸入參數是一個字符串模板,內部會進行解析替換的(這其實也符合“見名知意”的良好編程習慣)。(教訓:大家應該把握好自己寫的每一行代碼背後實際在做什麼。)

/* ......
 * @param messageTemplate new un-interpolated constraint message
 * @return returns a constraint violation builder
 */
ConstraintViolationBuilder buildConstraintViolationWithTemplate(String messageTemplate);

 

這個 case,源碼調試進去之後,就能跟蹤到執行翻譯階段,在如下方法中: org.hibernate.validator.messageinterpolation.AbstractMessageInterpolator.interpolateMessage

再往後,就是表達式求值了。

以為就這樣就完了嗎?

剛開始感覺,能幫忙算簡單的運算規則也就完了吧,你還能把我怎麼樣?其實這個相當於暴露了一個入口,支持用戶輸入任意 EL 表達式進行執行。網上通過關鍵字 “SpEL表達式注入漏洞” 找找,就能發現事情並沒有想象中那麼簡單。

我們構造恰當的 EL 表達式(注意各種轉義,下文的輸入參數相對比較明顯在做什麼了,實際上還有更多黑科技,比如各種二進制轉義編碼啊等等),就能直接執行輸入代碼,例如:可以直接執行命令,“ls -al”, 返回了一個 UNIXProcess 實例,命令已經被執行過了。

比如,我們執行個打開計算器的命令,搞個計算器玩玩~

我錄製了一個動圖,來個演示可能更生動一些。

這還得了嗎?這相當於提供了一個 webshell 的功能呀,你看想運行啥命令就能運行啥命令,例如 ping 本人博客地址(ping www.tanglei.name),下面動圖演示一下整個過程(從運行 ping 到 kill ping)。

我錄製了一個視頻,點擊這裏可以訪問。

豈不是直接創建一個用戶,然後遠程登錄就可以了。後果很嚴重啊,別人想幹嘛就幹嘛了。

我們跟蹤下對應的代碼,看看內部實現,就會“恍然大悟”了。

經驗教訓

幸虧這個漏洞被扼殺在搖籃里,否則後果還真的挺嚴重的。通過這個案例,我們有啥經驗和教訓呢?那就是作為程序員,我們要對每一行代碼都保持“敬畏”之心。也許就是因為你的不經意的一行代碼就帶來了嚴重的安全漏洞,要是不小心被壞人利用,輕則……重則……(自己想象吧)

此外,我們也應該看到,程序員需要對常見的安全漏洞(例如XSS/CSRF/SQL注入等等)有所了解,並且要有足夠的安全意識(其實有時候研究一些安全問題還挺好玩的,比如這篇《RSA算法及一種”旁門左道”的攻擊方式》就比較有趣)。例如:

  • 用戶權限分離:運行程序的用戶不應該用 root,例如新建一個“web”或者“www”之類的用戶,並設置該用戶的權限,比如不能有可執行 xx 的權限之類的。本文 case,如果權限進行了分離(遵循最小權限原則),應該也不會這麼嚴重。(本文就剛好是因為是測試環境,所以沒有強制實施)
  • 任何時候都不要相信用戶的輸入,必須對用戶輸入的進行校驗和過濾,又特別是針對公網上的應用。
  • 敏感信息加密保存。退一萬步講,假設攻擊者攻入了你的服務器,如果這個時候,你的數據庫賬戶信息等配置都直接明文保存在服務器中。那數據庫也被脫走了。

如果可能的話,需要對開發者的代碼進行漏洞掃描。一些常見的安全漏洞現在應該是有現成的工具支持的。另外,讓專業的人做專業的事情,例如要有安全團隊,可能你會說你們公司沒有不也活的好好的,哈哈,只不過可能還沒有被壞人盯上而已,壞人也會考慮到他們的成本和預期收益的,當然這就更加對我們開發者提高了要求。一些敏感權限盡量控制在少部分人手中,配合相應的流程來支撐(不得不說,大公司繁瑣的流程還是有一定道理的)。

畢竟我不是專業研究Web安全的,以上說得可能也不一定對,如果你有不同意見或者更好的建議歡迎留言參与討論。

這篇文章從寫代碼做實驗,到錄屏做視頻動圖等等耗時還蠻久的(好幾個周末的時間呢),原創真心不易,希望你能幫我個小忙唄,如果本文內容你覺得有所啟發,有所收穫,請幫忙點個“在看”唄,或者轉發分享讓更多的小夥伴看到。

精彩推薦
  • 一個由跨平台產生的浮點數bug | 有你意想不到的結果。
  • RSA算法及一種”旁門左道”的攻擊方式。
  • 震驚! 阿里的程序員也不過如此,竟被一個簡單的 SQL 查詢難住。
  • 面了7輪 Google,最終還是逃不脫被掛的命運。

文章首發於本人微信公眾號(ID:tangleithu),請感興趣的同學關注我的微信公眾號,及時獲取技術乾貨。

本站聲明:網站內容來源於博客園,如有侵權,請聯繫我們,我們將及時處理

【其他文章推薦】

※產品缺大量曝光嗎?你需要的是一流包裝設計!

※自行創業缺乏曝光? 網頁設計幫您第一時間規劃公司的形象門面

※回頭車貨運收費標準

※推薦評價好的iphone維修中心

※超省錢租車方案

台中搬家遵守搬運三大原則,讓您的家具不再被破壞!

※推薦台中搬家公司優質服務,可到府估價