貸前系統ElasticSearch實踐總結
貸前系統負責從進件到放款前所有業務流程的實現,其中涉及一些資料量較大、條件多樣且複雜的綜合查詢,引入ElasticSearch主要是為了提高查詢效率,並希望基於ElasticSearch快速實現一個簡易的資料倉儲,提供一些OLAP相關功能。本文將介紹貸前系統ElasticSearch的實踐經驗。
一、索引
描述:為快速定位資料而設計的某種資料結構。
索引好比是一本書前面的目錄,能加快資料庫的查詢速度。瞭解索引的構造及使用,對理解ES的工作模式有非常大的幫助。
常用索引:
-
點陣圖索引
-
雜湊索引
-
BTREE索引
-
倒排索引
1.1 點陣圖索引(BitMap)
點陣圖索引適用於欄位值為可列舉的有限個數值的情況。
點陣圖索引使用二進位制的數字串(bitMap)標識資料是否存在,1標識當前位置(序號)存在資料,0則表示當前位置沒有資料。
下圖1 為使用者表,儲存了性別和婚姻狀況兩個欄位;
圖2中分別為性別和婚姻狀態建立了兩個點陣圖索引。
例如:性別->男 對應索引為:101110011,表示第1、3、4、5、8、9個使用者為男性。其他屬性以此類推。
使用點陣圖索引查詢:
-
男性 並且已婚 的記錄 = 101110011 & 11010010 = 100100010,即第1、4、8個使用者為已婚男性。
-
女性 或者未婚的記錄 = 010001100 | 001010100 = 011011100, 即第2、3、5、6、7個使用者為女性或者未婚。
1.2 雜湊索引
顧名思義,是指使用某種雜湊函式實現key->value 對映的索引結構。
雜湊索引適用於等值檢索,透過一次雜湊計算即可定位資料的位置。
下圖3 展示了雜湊索引的結構,與JAVA中HashMap的實現類似,是用衝突表的方式解決雜湊衝突的。
1.3 BTREE索引
BTREE索引是關係型資料庫最常用的索引結構,方便了資料的查詢操作。
BTREE: 有序平衡N階樹, 每個節點有N個鍵值和N+1個指標, 指向N+1個子節點。
一棵BTREE的簡單結構如下圖4所示,為一棵2層的3叉樹,有7條資料:
以Mysql最常用的InnoDB引擎為例,描述下BTREE索引的應用。
Innodb下的表都是以索引組織表形式儲存的,也就是整個資料表的儲存都是B+tree結構的,如圖5所示。
主鍵索引為圖5的左半部分(如果沒有顯式定義自主主鍵,就用不為空的唯一索引來做聚簇索引,如果也沒有唯一索引,則innodb內部會自動生成6位元組的隱藏主鍵來做聚簇索引),葉子節點儲存了完整的資料行資訊(以主鍵 + row_data形式儲存)。
二級索引也是以B+tree的形式進行儲存,圖5右半部分,與主鍵不同的是二級索引的葉子節點儲存的不是行資料,而是索引鍵值和對應的主鍵值,由此可以推斷出,二級索引查詢多了一步查詢資料主鍵的過程。
維護一顆有序平衡N叉樹,比較複雜的就是當插入節點時節點位置的調整,尤其是插入的節點是隨機無序的情況;而插入有序的節點,節點的調整隻發生了整個樹的區域性,影響範圍較小,效率較高。
可以參考紅黑樹的節點的插入演算法:
%E2%80%93black_tree
因此如果innodb表有自增主鍵,則資料寫入是有序寫入的,效率會很高;如果innodb表沒有自增的主鍵,插入隨機的主鍵值,將導致B+tree的大量的變動操作,效率較低。這也是為什麼會建議innodb表要有無業務意義的自增主鍵,可以大大提高資料插入效率。
注:
-
Mysql Innodb使用自增主鍵的插入效率高。
-
使用類似Snowflake的ID生成演算法,生成的ID是趨勢遞增的,插入效率也比較高。
1.4 倒排索引(反向索引)
倒排索引也叫反向索引,可以相對於正向索引進行比較理解。
正向索引反映了一篇文件與文件中關鍵詞之間的對應關係;給定文件標識,可以獲取當前文件的關鍵詞、詞頻以及該詞在文件中出現的位置資訊,如圖6 所示,左側是文件,右側是索引。
反向索引則是指某關鍵詞和該詞所在的文件之間的對應關係;給定了關鍵詞標識,可以獲取關鍵詞所在的所有文件列表,同時包含詞頻、位置等資訊,如圖7所示。
反向索引(倒排索引)的單詞的集合和文件的集合就組成了如圖8所示的”單詞-文件矩陣“,打鉤的單元格表示存在該單詞和文件的對映關係。
倒排索引的儲存結構可以參考圖9。其中詞典是存放的記憶體裡的,詞典就是整個文件集合中解析出的所有單詞的列表集合;每個單詞又指向了其對應的倒排列表,倒排列表的集合組成了倒排檔案,倒排檔案存放在磁碟上,其中的倒排列表內記錄了對應單詞在文件中資訊,即前面提到的詞頻、位置等資訊。
下面以一個具體的例子來描述下,如何從一個文件集合中生成倒排索引。
如圖10,共存在5個文件,第一列為文件編號,第二列為文件的文字內容。
將上述文件集合進行分詞解析,其中發現的10個單詞為:[谷歌,地圖,之父,跳槽,Facebook,加盟,創始人,拉斯,離開,與],以第一個單詞”谷歌“為例:首先為其賦予一個唯一標識 ”單詞ID“, 值為1,統計出文件頻率為5,即5個文件都有出現,除了在第3個文件中出現2次外,其餘文件都出現一次,於是就有了圖11所示的倒排索引。
1.4.1 單詞詞典查詢最佳化
對於一個規模很大的文件集合來說,可能包含幾十萬甚至上百萬的不同單詞,能否快速定位某個單詞,這直接影響搜尋時的響應速度,其中的最佳化方案就是為單詞詞典建立索引,有以下幾種方案可供參考:
- 詞典Hash索引
Hash索引簡單直接,查詢某個單詞,透過計算雜湊函式,如果雜湊表命中則表示存在該資料,否則直接返回空就可以;適合於完全匹配,等值查詢。如圖12,相同hash值的單詞會放在一個衝突表中。
- 詞典BTREE索引
類似於Innodb的二級索引,將單詞按照一定的規則排序,生成一個BTree索引,資料節點為指向倒排索引的指標。
- 二分查詢
同樣將單詞按照一定的規則排序,建立一個有序單詞陣列,在查詢時使用二分查詢法;二分查詢法可以對映為一個有序平衡二叉樹,如圖14這樣的結構。
- FST(Finite State Transducers )實現
FST為一種有限狀態轉移機,FST有兩個優點:1)空間佔用小。透過對詞典中單詞字首和字尾的重複利用,壓縮了儲存空間;2)查詢速度快。O(len(str))的查詢時間複雜度。
以插入“cat”、 “deep”、 “do”、 “dog” 、“dogs”這5個單詞為例構建FST(注:必須已排序)。
如圖15 最終我們得到了如上一個有向無環圖。利用該結構可以很方便的進行查詢,如給定一個詞 “dog”,我們可以透過上述結構很方便的查詢存不存在,甚至我們在構建過程中可以將單詞與某一數字、單詞進行關聯,從而實現key-value的對映。
當然還有其他的最佳化方式,如使用Skip List、Trie、Double Array Trie等結構進行最佳化,不再一一贅述。
二、ElasticSearch使用心得
下面結合貸前系統具體的使用案例,介紹ES的一些心得總結。
2.1 概況
目前使用的ES版本:5.6
官網地址:
ES一句話介紹:The Heart of the Elastic Stack(摘自官網)
ES的一些關鍵資訊:
-
2010年2月首次釋出
-
Elasticsearch Store, Search, and Analyze
-
豐富的Restful介面
2.2 基本概念
- 索引(index)
ES的索引,也就是Index,和前面提到的索引並不是一個概念,這裡是指所有文件的集合,可以類比為RDB中的一個資料庫。
- 文件(document)
即寫入ES的一條記錄,一般是JSON形式的。
- 對映(Mapping)
文件資料結構的後設資料描述,一般是JSON schema形式,可動態生成或提前預定義。
- 型別(type)
由於理解和使用上的錯誤,type已不推薦使用,目前我們使用的ES中一個索引只建立了一個預設type。
- 節點
一個ES的服務例項,稱為一個服務節點。為了實現資料的安全可靠,並且提高資料的查詢效能,ES一般採用叢集模式進行部署。
- 叢集
多個ES節點相互通訊,共同分擔資料的儲存及查詢,這樣就構成了一個叢集。
- 分片
分片主要是為解決大量資料的儲存,將資料分割為若干部分,分片一般是均勻分佈在各ES節點上的。需要注意:分片數量無法修改。
- 副本
分片資料的一份完全的複製,一般一個分片會有一個副本,副本可以提供資料查詢,叢集環境下可以提高查詢效能。
2.3 安裝部署
-
JDK版本: JDK1.8
-
安裝過程比較簡單,可參考官網:下載安裝包 -> 解壓 -> 執行
-
安裝過程遇到的坑:
ES啟動佔用的系統資源比較多,需要調整諸如檔案控制程式碼數、執行緒數、記憶體等系統引數,可參考下面的文件。
http://www.cnblogs.com/sloveling/p/elasticsearch.html
2.4 例項講解
下面以一些具體的操作介紹ES的使用:
2.4.1 初始化索引
初始化索引,主要是在ES中新建一個索引並初始化一些引數,包括索引名、文件對映(Mapping)、索引別名、分片數(預設:5)、副本數(預設:1)等,其中分片數和副本數在資料量不大的情況下直接使用預設值即可,無需配置。
下面舉兩個初始化索引的方式,一個使用基於Dynamic Template(動態模板) 的Dynamic Mapping(動態對映),一個使用顯式預定義對映。
1) 動態模板 (Dynamic Template)
<p style="line-height: 2em;"><span style="font-size: 14px;">curl -X PUT {"mappings":{ "order_info":{ "dynamic_date_formats":["yyyy-MM-dd HH:mm:ss||yyyy-MM-dd],<br> "dynamic_templates":[<br> {"orderId2":{<br> "match_mapping_type":"string",<br> "match_pattern":"regex",<br> "match":"^orderId$",<br> "mapping":{<br> "type":"long"<br> }<br> }<br> },<br> {"strings_as_keywords":{<br> "match_mapping_type":"string",<br> "mapping":{<br> "type":"keyword",<br> "norms":false<br> }<br> }<br> }<br> ]<br> }<br>},<br>"aliases":{<br> "loan_alias":{}<br>}}'<br></span></p>
上面的JSON串就是我們用到的動態模板,其中定義了日期格式:dynamic_date_formats 欄位;定義了規則orderId2:凡是遇到orderId這個欄位,則將其轉換為long型;定義了規則strings_as_keywords:凡是遇到string型別的欄位都對映為keyword型別,norms屬性為false;關於keyword型別和norms關鍵字,將在下面的資料型別小節介紹。
2)預定義對映
預定義對映和上面的區別就是預先把所有已知的欄位型別描述寫到mapping裡,下圖擷取了一部分作為示例:
圖16中JSON結構的上半部分與動態模板相同,紅框中內容內容為預先定義的屬性:apply.applyInfo.appSubmissionTime, apply.applyInfo.applyId, apply.applyInfo.applyInputSource等欄位,type表明了該欄位的型別,對映定義完成後,再插入的資料必須符合欄位定義,否則ES將返回異常。
2.4.2 常用資料型別
常用的資料型別有text, keyword, date, long, double, boolean, ip
實際使用中,將字串型別定義為keyword而不是text,主要原因是text型別的資料會被當做文字進行語法分析,做一些分詞、過濾等操作,而keyword型別則是當做一個完整資料儲存起來,省去了多餘的操作,提高索引效能。
配合keyword使用的還有一個關鍵詞norm,置為false表示當前欄位不參與評分;所謂評分是指根據單詞的TF/IDF或其他一些規則,對查詢出的結果賦予一個分值,供展示搜尋結果時進行排序, 而一般的業務場景並不需要這樣的排序操作(都有明確的排序欄位),從而進一步最佳化查詢效率。
2.4.3 索引名無法修改
初始化一個索引,都要在URL中明確指定一個索引名,一旦指定則無法修改,所以一般建立索引都要指定一個預設的別名(alias):
<p style="line-height: 2em;"><span style="font-size: 14px;">"aliases":{ "loan_alias":{ }<br> }<br></span></p>
別名和索引名是多對多的關係,也就是一個索引可以有多個別名,一個別名也可以對映多個索引;在一對一這種模式下,所有用到索引名的地方都可以用別名進行替換;別名的好處就是可以隨時的變動,非常靈活。
2.4.4 Mapping中已存在的欄位無法更新
如果一個欄位已經初始化完畢(動態對映透過插入資料,預定義透過設定欄位型別),那就確定了該欄位的型別,插入不相容的資料則會報錯,比如定義了一個long型別欄位,如果寫入一個非數字型別的資料,ES則會返回資料型別錯誤的提示。
這種情況下可能就需要重建索引,上面講到的別名就派上了用場;一般分3步完成:
- 新建一個索引將格式錯誤的欄位指定為正確格式;
- 2)使用ES的Reindex API將資料從舊索引遷移到新索引;
- 3)使用Aliases API將舊索引的別名新增到新索引上,刪除舊索引和別名的關聯。
上述步驟適合於離線遷移,如果要實現不停機實時遷移步驟會稍微複雜些。
2.4.5 API
基本的操作就是增刪改查,可以參考ES的官方文件:
https://www.elastic.co/guide/en/elasticsearch/reference/current/docs.html
一些比較複雜的操作需要用到ES Script,一般使用類Groovy的painless script,這種指令碼支援一些常用的JAVA API(ES安裝使用的是JDK8,所以支援一些JDK8的API),還支援Joda time等。
舉個比較複雜的更新的例子,說明painless script如何使用:
需求描述
appSubmissionTime表示進件時間,lenssonStartDate表示開課時間,expectLoanDate表示放款時間。要求2018年9月10日的進件,如果進件時間 與 開課時間的日期差小於2天,則將放款時間設定為進件時間。
Painless Script如下:
<p style="line-height: 2em;"><span style="font-size: 14px;">POST loan_idx/_update_by_query<br> { "script":{ "source":"long getDayDiff(def dateStr1, def dateStr2){ <br> LocalDateTime date1= toLocalDate(dateStr1); LocalDateTime date2= toLocalDate(dateStr2); ChronoUnit.DAYS.between(date1, date2);<br> }<br> LocalDateTime toLocalDate(def dateStr)<br> { <br> DateTimeFormatter formatter = DateTimeFormatter.ofPattern(\"yyyy-MM-dd HH:mm:ss\"); LocalDateTime.parse(dateStr, formatter);<br> }<br> if(getDayDiff(ctx._source.appSubmissionTime, ctx._source.lenssonStartDate) < 2)<br> { <br> ctx._source.expectLoanDate=ctx._source.appSubmissionTime<br> }", "lang":"painless"<br> }<br> , "query":<br> { "bool":{ "filter":[<br> { "bool":{ "must":[<br> { "range":{ <br> "appSubmissionTime":<br> {<br> "from":"2018-09-10 00:00:00", "to":"2018-09-10 23:59:59", "include_lower":true, "include_upper":true<br> }<br> }<br> }<br> ]<br> }<br> }<br> ]<br> }<br> }<br>}<br></span></p>
解釋:整個文字分兩部分,下半部分query關鍵字表示一個按範圍時間查詢(2018年9月10號),上半部分script表示對匹配到的記錄進行的操作,是一段類Groovy程式碼(有Java基礎很容易讀懂),格式化後如下, 其中定義了兩個方法getDayDiff()和toLocalDate(),if語句裡包含了具體的操作:
<p style="line-height: 2em;"><span style="font-size: 14px;">long getDayDiff(def dateStr1, def dateStr2){<br> LocalDateTime date1= toLocalDate(dateStr1);<br> LocalDateTime date2= toLocalDate(dateStr2);<br> ChronoUnit.DAYS.between(date1, date2);<br>}<br>LocalDateTime toLocalDate(def dateStr){<br> DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");<br> LocalDateTime.parse(dateStr, formatter);<br>}if(getDayDiff(ctx._source.appSubmissionTime, ctx._source.lenssonStartDate) < 2){<br> ctx._source.expectLoanDate=ctx._source.appSubmissionTime<br>}<br></span></p>
然後提交該POST請求,完成資料修改。
2.4.6 查詢資料
這裡重點推薦一個ES的外掛ES-SQL:
這個外掛提供了比較豐富的SQL查詢語法,讓我們可以使用熟悉的SQL語句進行資料查詢。其中,有幾個需要注意的點:
-
ES-SQL使用Http GET方式傳送情況,所以SQL的長度是受限制的(4kb),可以透過以下引數進行修改:http.max_initial_line_length: "8k"
-
計算總和、平均值這些數字操作,如果欄位被設定為非數值型別,直接使用ESQL會報錯,可改用painless指令碼。
-
使用Select as語法查詢出的結果和一般的查詢結果,資料的位置結構是不同的,需要單獨處理。
-
NRT(Near Real Time):準實時
向ES中插入一條記錄,然後再查詢出來,一般都能查出最新的記錄,ES給人的感覺就是一個實時的搜尋引擎,這也是我們所期望的,然而實際情況卻並非總是如此,這跟ES的寫入機制有關,做個簡單介紹:
- Lucene 索引段 -> ES 索引
寫入ES的資料,首先是寫入到Lucene索引段中的,然後才寫入ES的索引中,在寫入ES索引前查到的都是舊資料。
- commit:原子寫操作
索引段中的資料會以原子寫的方式寫入到ES索引中,所以提交到ES的一條記錄,能夠保證完全寫入成功,而不用擔心只寫入了一部分,而另一部分寫入失敗。
- refresh:重新整理操作,可以保證最新的提交被搜尋到
索引段提交後還有最後一個步驟:refresh,這步完成後才能保證新索引的資料能被搜尋到。
出於效能考慮,Lucene推遲了耗時的重新整理,因此它不會在每次新增一個文件的時候重新整理,預設每秒重新整理一次。這種重新整理已經非常頻繁了,然而有很多應用卻需要更快的重新整理頻率。如果碰到這種狀況,要麼使用其他技術,要麼審視需求是否合理。
不過,ES給我們提供了方便的實時查詢介面,使用該介面查詢出的資料總是最新的,呼叫方式描述如下:
GET
上述介面使用了HTTP GET方法,基於資料主鍵(id)進行查詢,這種查詢方式會同時查詢ES索引和Lucene索引段中的資料,並進行合併,所以最終結果總是最新的。但有個副作用:每次執行完這個操作,ES就會強制執行refresh操作,導致一次IO,如果使用頻繁,對ES效能也會有影響。
2.4.7 陣列處理
陣列的處理比較特殊,拿出來單獨講一下。
1)表示方式就是普通的JSON陣列格式,如:
[1, 2, 3]、 [“a”, “b”]、 [ { "first" : "John", "last" : "Smith" },{"first" : "Alice", "last" : "White"} ]
2)需要注意ES中並不存在陣列型別,最終會被轉換為object,keyword等型別。
3)普通陣列物件查詢的問題。
普通陣列物件的儲存,會把資料打平後將欄位單獨儲存,如:
<p style="line-height: 2em;"><span style="font-size: 14px;">{ "user":[<br> { "first":"John", "last":"Smith"<br> },<br> { "first":"Alice", "last":"White"<br> }<br> ]<br>}<br></span></p>
會轉化為下面的文字
<p style="line-height: 2em;"><span style="font-size: 14px;">{ "user.first":[ "John", "Alice"<br> ], "user.last":[ "Smith", "White"<br> ]<br>}<br></span></p>
將原來文字之間的關聯打破了,圖17展示了這條資料從進入索引到查詢出來的簡略過程:
-
組裝資料,一個JSONArray結構的文字。
-
寫入ES後,預設型別置為object。
-
查詢user.first為Alice並且user.last為Smith的文件(實際並不存在同時滿足這兩個條件的)。
-
返回了和預期不符的結果。
4)巢狀(Nested)陣列物件查詢
巢狀陣列物件可以解決上面查詢不符的問題,ES的解決方案就是為陣列中的每個物件單獨建立一個文件,獨立於原始文件。如圖18所示,將資料宣告為nested後,再進行相同的查詢,返回的是空,因為確實不存在user.first為Alice並且user.last為Smith的文件。
5)一般對陣列的修改是全量的,如果需要單獨修改某個欄位,需要藉助painless script,參考:
2.5 安全
資料安全是至關重要的環節,主要透過以下三點提供資料的訪問安全控制:
- XPACK
XPACK提供了Security外掛,可以提供基於使用者名稱密碼的訪問控制,可以提供一個月的免費試用期,過後收取一定的費用換取一個license。
- IP白名單
是指在ES伺服器開啟防火牆,配置只有內網中若干伺服器可以直接連線本服務。
- 代理
一般不允許業務系統直連ES服務進行查詢,需要對ES介面做一層包裝,這個工作就需要代理去完成;並且代理伺服器可以做一些安全認證工作,即使不適用XPACK也可以實現安全控制。
2.6 網路
ElasticSearch伺服器預設需要開通9200、9300 這兩個埠。
下面主要介紹一個和網路相關的錯誤,如果大家遇到類似的錯誤,可以做個借鑑。
-
引出異常前,先介紹一個網路相關的關鍵詞,keepalive :
-
Http keep-alive和Tcp keepalive。
HTTP1.1中預設啟用"Connection: Keep-Alive",表示這個HTTP連線可以複用,下次的HTTP請求就可以直接使用當前連線,從而提高效能,一般HTTP連線池實現都用到keep-alive;
TCP的keepalive的作用和HTTP中的不同,TPC中主要用來實現連線保活,相關配置主要是net.ipv4.tcp keepalivetime這個引數,表示如果經過多長時間(預設2小時)一個TCP連線沒有交換資料,就傳送一個心跳包,探測下當前連結是否有效,正常情況下會收到對方的ack包,表示這個連線可用。
下面介紹具體異常資訊,描述如下:
兩臺業務伺服器,用restClient(基於HTTPClient,實現了長連線)連線的ES叢集(叢集有三臺機器),與ES伺服器分別部署在不同的網段,有個異常會有規律的出現:
每天9點左右會發生異常Connection reset by peer. 而且是連續有三個Connection reset by peer
<p style="line-height: 2em;"><span style="font-size: 14px;">Caused by: java.io.IOException: Connection reset by peer <br> at sun.nio.ch.FileDispatcherImpl.read0(Native Method) <br> at sun.nio.ch.SocketDispatcher.read(SocketDispatcher.java:39) <br> at sun.nio.ch.IOUtil.readIntoNativeBuffer(IOUtil.java:223) <br> at sun.nio.ch.IOUtil.read(IOUtil.java:197)<br></span></p>
為了解決這個問題,我們嘗試了多種方案,查官方文件、比對程式碼、抓包。。。經過若干天的努力,最終發現這個異常是和上面提到keepalive關鍵詞相關(多虧運維組的同事幫忙)。
實際線上環境,業務伺服器和ES叢集之間有一道防火牆,而防火牆策略定義空閒連線超時時間為例如為1小時,與上面提到的linux伺服器預設的例如為2小時不一致。由於我們當前系統晚上訪問量較少,導致某些連線超過2小時沒有使用,在其中1小時後防火牆自動就終止了當前連線,到了2小時後伺服器嘗試傳送心跳保活連線,直接被防火牆攔截,若干次嘗試後服務端傳送RST中斷了連結,而此時的客戶端並不知情;當第二天早上使用這個失效的連結請求時,服務端直接返回RST,客戶端報錯Connection reset by peer,嘗試了叢集中的三臺伺服器都返回同樣錯誤,所以連續報了3個相同的異常。解決方案也比較簡單,修改服務端keepalive超時配置,小於防火牆的1小時即可。
參考
《深入理解ElasticSearch》
http://www.cnblogs.com/Creator/p/3722408.html
http://www.cnblogs.com/LBSer/p/4119841.html
http://www.cnblogs.com/yjf512/p/5354055.html
作者:雷鵬
來源:宜信技術學院
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/69918724/viewspace-2654184/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Rust 日誌系統實踐總結Rust
- 前後端聯調實踐總結後端
- 推薦系統 embedding 技術實踐總結
- 某資產管理公司部署貸前徵信調查報表系統實錄
- Elasticsearch 實戰經驗總結Elasticsearch
- RESTful API實踐總結RESTAPI
- hadoop實踐總結Hadoop
- Linux實踐總結Linux
- Elasticsearch 問題總結Elasticsearch
- ElasticSearch 學習總結Elasticsearch
- Elasticsearch調優實踐Elasticsearch
- 最新的網貸系統
- 小程式初實踐總結
- React Hooks工程實踐總結ReactHook
- iOS App 瘦身實踐總結iOSAPP
- react-redux實踐總結ReactRedux
- 軟體工程實踐總結軟體工程
- 前端優化實踐總結前端優化
- iOS程式碼實踐總結iOS
- 螞蟻微貸互動營銷技術體系實踐
- ElasticSearch實戰-編碼實踐Elasticsearch
- Elasticsearch 索引容量管理實踐Elasticsearch索引
- Taro實踐 - 深度開發實踐體驗及總結
- ⚠️Flutter 效能優化實踐 總結⚠️Flutter優化
- 小程式開發實踐總結
- 《美團機器學習實踐》—— 讀後總結機器學習
- 機器學習總結 (機器學習實踐筆記)機器學習筆記
- Oracle高可用最佳實踐總結Oracle
- 實施PLM系統的總結及建議
- 作業系統總結作業系統
- 騰訊健康碼 16 億亮碼背後的 Elasticsearch 系統調優實踐Elasticsearch
- Elasticsearch 5.6 原理和使用總結Elasticsearch
- ElasticSearch極簡入門總結Elasticsearch
- 系統優化總結——系統層面優化
- Elasticsearch 7.2 在 Laravel 中實踐ElasticsearchLaravel
- 實踐003-elasticsearch之analyzerElasticsearch
- Elasticsearch實踐(三):MappingElasticsearchAPP
- Elasticsearch在Laravel中的實踐ElasticsearchLaravel