触类旁通Elasticsearch:关联
目錄
一、文檔間關(guān)系概覽
1. 對象類型
2. 嵌套類型
3. 父子關(guān)系
4. 反規(guī)范化
二、將對象最為字段值
1. 映射和索引對象
2. 搜索對象
三、嵌套類型
1. 映射并索引嵌套文檔
2. 搜索和聚合嵌套文檔
四、父子關(guān)系
1. 子文檔的索引、更新和刪除
2. 在父文檔和子文檔中搜索
五、反規(guī)范化
1. 反規(guī)范化使用案例
2. 索引、更新和刪除反規(guī)范化的數(shù)據(jù)
3. 查詢反規(guī)范化的數(shù)據(jù)
《Elasticsearch In Action》學(xué)習(xí)筆記。
? ? ? ? ES本身不支持SQL數(shù)據(jù)庫的join操作,在ES中定義關(guān)系的方法有對象類型、嵌套文檔、父子關(guān)系和反規(guī)范化。
一、文檔間關(guān)系概覽
1. 對象類型
? ? ? ? 允許將一個對象作為文檔字段的值,主要用于處理一對一關(guān)系。如果用對象類型表示一對多關(guān)系,可能出現(xiàn)邏輯上的錯誤。例如,使用對象類型(object type)表示一個小組多個活動的關(guān)系:
{"name": "Denver technology group""events": [{"date": "2014-12-22","title": "Introduction to Elasticsearch"},{"date": "2014-06-20","title": "Introduction to Hadoop"}] }? ? ? ? 如果希望搜索一個關(guān)于Elasticsearch的活動分組,可以在events.title字段里搜索。在系統(tǒng)內(nèi)部,文檔是像下面這樣進行索引的:
{"name": "Denver technology group","events.date": ["2014-12-22", "2014-06-20"],"events.title": ["Introduction to Elasticsearch", "Introduction to Hadoop"] }? ? ? ? 假設(shè)想過濾2014年12月主辦過Hadoop會議的分組,查詢可以是這樣的:
"bool": {"must": [{"term": {"events.title": "Hadoop"}},{"range": {"events.date": {"from": "2014-12-01","to": "2014-12-31"}}}] }? ? ? ? 這將匹配例中的那個文檔,但顯然錯誤的,Hadoop活動是在6月而不是12月。造成這種錯誤的原因是對象類型將所有數(shù)據(jù)都存儲在一篇文檔中,ES并不知道內(nèi)部文檔之間的邊界,如圖1所示。
圖1 在存儲的時候,內(nèi)部對象的邊界并未考慮在內(nèi),這導(dǎo)致了意外的搜索結(jié)果?
? ? ? ? 如果處理的是一對一關(guān)系,則不會出現(xiàn)這樣的邏輯錯誤,而且對象類型是最快、最便捷的關(guān)系處理方法。ES的關(guān)系類型類似Oracle中的嵌套表。
2. 嵌套類型
? ? ? ? 要避免跨對象匹配的發(fā)生,可以使用嵌套類型(nested type),它將活動索引到分隔的Lucene文檔。對象與嵌套的區(qū)別在于映射,這會促使ES將嵌套的內(nèi)部對象索引到鄰近的位置,但是保持獨立的Lucene文檔,如圖2所示。在搜索時,需要使用nested過濾器和查詢,這些會在Lucene文檔中搜索。
圖2 嵌套類型使得ES將多個對象索引到多個分隔的Lucene文檔?
? ? ? ? 在某些用例中,像對象和嵌套類型那樣,將所有數(shù)據(jù)存儲在同一個ES文檔中不見得是明智之舉。拿分組和活動的例子來說:如果一個分組所有數(shù)據(jù)都放在同一篇文檔中,那么在創(chuàng)建一項新的活動時,不得不為這個活動重新索引整篇文檔。這可能會降低性能和并發(fā)性,取決于文檔有多大,以及操作的頻繁程度。
3. 父子關(guān)系
? ? ? ? 通過父子關(guān)系,可以使用完全不同的ES文檔,并在映射中定義文檔間的關(guān)系。在索引一個子文檔時,可以將它指向其父文檔,如圖3所示。在搜索時,可以使用has_parent和has_child查詢和過濾器處理父子關(guān)系。
圖3 不同ES文檔可以有父子關(guān)系?
4. 反規(guī)范化
? ? ? ? 對象、嵌套和父子關(guān)系可以用于處理一對一或一對多關(guān)系,而反規(guī)范化用于處理多對多關(guān)系。反規(guī)范化(denormalizing)意味著一篇文檔將包含所有相關(guān)的數(shù)據(jù),即使是同樣的數(shù)據(jù)在其它文檔中有復(fù)本。
? ? ? ? 以分組和會員為例,一個分組可以擁有多個會員,一個用戶也可以成為多個分組的會員。分組和會員都有它們自己的一組屬性。為了表示這種關(guān)系,可以讓分組成為會員的父輩。對于身為多個分組會員的用戶而言,可以反規(guī)范化他們的數(shù)據(jù):每次表示一個其所屬的分組,如圖4所示。反規(guī)范化實際上是一種典型的以空間(數(shù)據(jù)冗余)換時間的處理方式。
圖4 反規(guī)范化技術(shù)將數(shù)據(jù)進行復(fù)制,避免了高成本的關(guān)系處理?
二、將對象最為字段值
? ? ? ? 通過對象,ES在內(nèi)部將層級結(jié)構(gòu)進行了扁平化,使用每個內(nèi)部字段的全路徑,將其放入Lucene內(nèi)的獨立字段。整個流程如圖5所示。
圖5 JSON層次結(jié)構(gòu),在Lucene中被存儲為扁平結(jié)構(gòu)?
1. 映射和索引對象
? ? ? ? 默認(rèn)情況下,內(nèi)部對象的映射是自動識別的。
# 自動創(chuàng)建索引 curl -XPOST '172.16.1.127:9200/event-object/_doc/1?pretty' -H 'Content-Type: application/json' -d' {"title": "Introduction to objects","location":?{"name": "Elasticsearch in Action book","address": "chapter 8"} }'# 查看索引映射 curl '172.16.1.127:9200/event-object/_mapping?pretty'? ? ? ? 結(jié)果返回:
{"event-object" : {"mappings" : {"_doc" : {"properties" : {"location" : {"properties" : { ? ? ? ? ? ? ? ? ? ?# 內(nèi)部對象及其屬性的映射是自動識別的"address" : {"type" : "text","fields" : {"keyword" : {"type" : "keyword","ignore_above" : 256}}},"name" : {"type" : "text","fields" : {"keyword" : {"type" : "keyword","ignore_above" : 256}}}}},"title" : {"type" : "text","fields" : {"keyword" : {"type" : "keyword","ignore_above" : 256}}}}}}} }? ? ? ? 如果有多個這樣的對象所構(gòu)成的數(shù)組,單個內(nèi)部對象的映射同樣奏效。例如,如果索引了下面的文檔,映射將會保持不變。
curl -XPOST '172.16.1.127:9200/event-object/_doc/2?pretty' -H 'Content-Type: application/json' -d' {"title": "Introduction to objects","location": [{"name": "Elasticsearch in Action book","address": "chapter 8"},{"name": "Elasticsearch Guide","address": "elasticsearch/reference/current/mapping-object-type.html"} ] }'2. 搜索對象
? ? ? ? 默認(rèn)情況下,需要設(shè)置所查找的字段路徑,來引用內(nèi)部對象。下面的代碼指定location_event.name的全路徑將其作為搜索的字段,從而搜索在辦公室舉辦的活動。
EVENT_PATH="172.16.1.127:9200/get-together/" curl "$EVENT_PATH/_search?q=location_event.name:office&pretty"? ? ? ? 下面的terms聚合返回了location.name字段中最為常用的單詞。
curl "172.16.1.127:9200/get-together/_search?pretty" -H 'Content-Type: application/json' -d' {"aggs": {"location_cloud": {"terms": {"field": "location.name"}}} }'? ? ? ? 再次強調(diào),對象擅于處理一對一關(guān)系,而對于一對多關(guān)系的查詢,可能出現(xiàn)邏輯錯誤。
三、嵌套類型
1. 映射并索引嵌套文檔
? ? ? ? 嵌套映射和對象映射看上去差不多,不過期type不是object,而必須是nested。
# 定義索引映射 curl -XPUT "172.16.1.127:9200/group-nested?pretty" -H 'Content-Type: application/json' -d' {"mappings": {"_doc": {"properties": {"name": {"type": "text"},"members": {"type": "nested", ? ? ? ? ? ? ? ? ? ? # 這里告訴ES將會員對象索引到同一個分塊中的不同文檔中"properties": {"first_name": {"type": "text"},"last_name": {"type": "text"}}}}}} }'# 增加一篇文檔 curl -XPUT "172.16.1.127:9200/group-nested/_doc/1?pretty" -H 'Content-Type: application/json' -d' {"name": "Elasticsearch News", ? ? ? ? ? ? ? ? # 這個屬性將存入主文檔"members": [{"first_name": "Lee", ? ? ? ? ? ? ? ? ? ? ?# 這些對象存入自己的文檔中,共同組成根文檔中的一個分塊"last_name": "Hinman"},{"first_name": "Radu","last_name": "Gheorghe"}] }'? ? ? ? 與對象不同,嵌套查詢和過濾器可以在文檔的邊界之內(nèi)搜索。例如,可以搜索名為“Lee”且姓為“Hinman”的分組會員。缺省時,嵌套的查詢不會進行跨多個對象的匹配,因此避免了名為“Lee”而姓為“Gheorghe”這樣的意外匹配。
2. 搜索和聚合嵌套文檔
? ? ? ? 使用nested在嵌套文檔上運行搜索和聚合,使ES連接在同一個分塊中的多個Lucene文檔,并將連接后的結(jié)果數(shù)據(jù)看作普通的ES文檔。
(1)Nested查詢和過濾器
? ? ? ? 運行nested查詢或過濾器時,需要指定path參數(shù),告訴ES這些嵌套對象位于哪里的Lucene分塊中。除夕之外,nested查詢或者過濾器將會分別封裝一個常規(guī)的查詢或過濾器。下面的代碼搜索名為“Lee”、姓為“Gheorghe”的會員。查詢不會返回匹配的文檔,因為沒有會員的名字是Lee Gheorghe。
(2)在多個嵌套層級上搜索
? ? ? ? ES支持多級嵌套。下面的代碼創(chuàng)建兩級嵌套的索引:會員(members)和他們的評論(comments)。
? ? ? ? 添加一個嵌套文檔:
curl -XPUT "172.16.1.127:9200/group-multinested/_doc/1?pretty" -H 'Content-Type: application/json' -d' {"name": "Elasticsearch News","members": {"first_name": "Radu","last_name": "Gheorghe","comments": { ? ? ? ? ? ? ? ? ? ? ? ? ? ? ? # 多個會員對象嵌套于分組中,而多個評論對象又嵌套在會員對象中"date": "2013-12-22","comment": "hello world"}} }'? ? ? ? 為了在內(nèi)嵌的評論文檔中搜索,需要指定members.comments的路徑:
curl '172.16.1.127:9200/group-multinested/_search?pretty' -H 'Content-Type: application/json' -d' {"query": {"nested": {"path": "members.comments", ? ? ? ? ? ? ? # 查找位于members之中的comments字段"query": {"term": {"members.comments.comment": "hello" ? # 查詢?nèi)匀惶峁┝俗侄蔚娜柯窂接糜诓檎襺}}} }'(3)整合嵌套對象的得分
? ? ? ? 一個nested查詢會計算得分。例如,根據(jù)查詢條件的匹配程度,每個內(nèi)部會員文檔會得到自己的得分。但是來自應(yīng)用的查詢是為了查找分組文檔,所以ES需要為整個分組文檔給出一個得分。在這點上一共有4中選項,通過score_mode設(shè)置。
- avg:這是默認(rèn)選項,系統(tǒng)獲取所有匹配的內(nèi)部文檔之分?jǐn)?shù),并返回其平均分。
- total:系統(tǒng)獲取所有匹配的內(nèi)部文檔之分?jǐn)?shù),將其求和并返回。
- max:返回匹配的內(nèi)部文檔之最大得分。
- none:考慮總文檔得分的計算時,不保留、不統(tǒng)計嵌套文檔的得分。
(4)獲知哪些內(nèi)部文檔匹配上了
? ? ? ? 可以在嵌套查詢或過濾器中添加一個inner_hits對象,來展示匹配上的嵌套文檔。
curl '172.16.1.127:9200/group-nested/_search?pretty' -H 'Content-Type: application/json' -d' {"query": {"nested": {"path": "members","query": {"term": {"members.first_name": "lee"}},"inner_hits": {"from": 0,"size": 1}}} }'? ? ? ? 結(jié)果返回:
..."inner_hits" : {"members" : {"hits" : {"total" : 1,"max_score" : 0.6931472,"hits" : [{"_index" : "group-nested","_type" : "_doc","_id" : "1","_nested" : {"field" : "members","offset" : 0},"_score" : 0.6931472,"_source" : {"first_name" : "Lee","last_name" : "Hinman"}}]}}}? ? ? ? 要識別子文檔,可以查看_nested對象。其中field字段是嵌套對象的路徑,而offset顯示了嵌套文檔在數(shù)組中的位置。上例中,Lee是查詢結(jié)果中的第一個member。
(5)嵌套和逆向嵌套聚合
? ? ? ? 為了在嵌套類型的對象上進行聚合,需要使用nested聚合。這是一個單桶聚合,在其中可以指定包含所需字段的嵌套對象之路徑。如圖6所示,nested聚合促使ES進行了必要的連接,以確保其它聚合在指定的路徑上能正常運行。
?
? ? ? ? 例如,為了獲得參與分組最多的活躍用戶,通常會在會員名字字段上運行一個terms聚合。如果這個name字段存儲在嵌套類型的members對象中,那么需要將terms聚合封裝在nested聚合中,并將聚合的路徑path設(shè)置為會員members:
curl '172.16.1.127:9200/get-together/_search?pretty' -H 'Content-Type: application/json' -d' {"aggs": {"members": {"nested": {"path": "members"},"aggs": {"frequent_members": {"terms": {"field": "members.name"}}}}} }'? ? ? ? 有些情況下,需要反向訪問父輩或者根文檔。例如,希望針對活躍會員,展示他們參加最多的分組之tags。為了實現(xiàn)這一點,使用reverse_nested聚合,它會告訴ES在嵌套層級中向上返回查找:
curl -X PUT "172.16.1.127:9200/get-together/_mapping/_doc?pretty" -H 'Content-Type: application/json' -d' {"properties": {"tags": {?"type": ? ? "text","fielddata": true}} }'curl '172.16.1.127:9200/get-together/_search?pretty' -H 'Content-Type: application/json' -d' {"aggs": {"members": {"nested": {"path": "members"},"aggs": {"frequent_members": {"terms": {"field": "member.name"},"aggs": {"back_to_group": {"reverse_nested": {},"aggs": {"tags_per_member": {"terms": {"field": "tags"}}}}}}}}} }'? ? ? ? Nested和reverse_nested聚合可以快速告訴ES,在哪些Lucene文檔中查找下一項聚合的字段。
四、父子關(guān)系
? ? ? ? 在嵌套的文檔中,實際情況是所有內(nèi)部的對象集中在同一個分塊中的Lucene文檔,這對于對象便捷地連接根文檔而言,是非常有好處的。父子文檔則是完全不同的ES文檔,所以只能分別搜索它們,效率更低。
? ? ? ? 對于文檔的索引、更新和刪除而言,父子的方式就顯得出類拔萃了。這是因為父輩和子輩文檔都是獨立的ES文檔,各自管理。舉例來說,如果一個分組有很多活動,要增加一個新活動,那么就是增加一篇新的活動文檔。如果使用嵌套類型的方式,ES不得不重新索引分組文檔,來囊括新的活動和全部已有活動,這個過程就會更慢。
1. 子文檔的索引、更新和刪除
(1)映射
? ? ? ? 在示例索引get-together的映射中定義了一對父子關(guān)系屬性如下;
(2)索引和檢索
? ? ? ? 索引子文檔時,需要在URI中放置routing值作為參數(shù)。routing字段向ES提供了散列的ID,即路由值,這使得ES將父子文檔路由到相同的分片,搜索的時候能從中獲益。ES會自動使用這個路由值來查詢父輩的分片并獲得其子輩,或者是查詢子輩的分片來獲得其父輩。
? ? ? ? routing參數(shù)是強制的,如果不加該參數(shù),報錯如下:
{"error" : {"root_cause" : [{"type" : "mapper_parsing_exception","reason" : "failed to parse"}],"type" : "mapper_parsing_exception","reason" : "failed to parse","caused_by" : {"type" : "illegal_argument_exception","reason" : "[routing] is missing for join field [relationship_type]"}},"status" : 400 }? ? ? ? 當(dāng)索引子文檔時,其父輩文檔可能已經(jīng)被索引,也可能尚未索引。這類似于關(guān)系數(shù)據(jù)庫中的主子表之間沒有強制的外鍵約束。在上例中,當(dāng)索引event子文檔1103時,其對應(yīng)的group父文檔2可以并不存在。
? ? ? ? _routing字段是被存儲的,因此可以檢索其內(nèi)容。同時,這個字段也是被索引的,這樣可以通過條件來搜索其值。為了檢索一篇活動文檔,這里運行了一個普通的索引請求:
curl '172.16.1.127:9200/get-together/_doc/1103?routing=2&pretty'? ? ? ? 結(jié)果返回:
{"_index" : "get-together","_type" : "_doc","_id" : "1103","_version" : 1,"_routing" : "2","found" : true,"_source" : {"host" : "Radu","title" : "Yet another Elasticsearch intro in Denver","relationship_type" : {"name" : "event","parent" : "2"}} }? ? ? ? 如果請求中不加routing=2,查詢會路由到1103的散列分片上去,而不是2的散列分片,最終導(dǎo)致查詢不到相應(yīng)的文檔。再者,子文檔ID,如1103在索引中并不唯一,只有parent ID和_id的組合才是唯一的。
(3)更新與刪除
? ? ? ? 類似地,更新與刪除子文檔同樣需要指定routing參數(shù)。
? ? ? ? 通過查詢來進行的刪除,不需要指定routing參數(shù):
curl -X POST "172.16.1.127:9200/get-together/_delete_by_query?pretty" -H 'Content-Type: application/json' -d' {"query": {"query_string": {"fields": ["host"],"query": "radu"}} }'2. 在父文檔和子文檔中搜索
(1)has_child查詢和過濾器
? ? ? ? 使用子輩的條件來搜索父輩的時候,如搜索Elasticsearch活動的分組,可以使用has_child查詢或過濾器。
? ? ? ? has_child查詢和這個過濾器的運行方式差不多,不過它可以通過聚合子文檔的得分,對每個父輩進行評分。可以將score_mode設(shè)置為max、sum、avg或none,和嵌套查詢是一樣的。例如,如下查詢在返回分組時,按照舉辦的Elasticsearch活動之最高相關(guān)性排序:
curl -X GET "172.16.1.127:9200/get-together/_doc/_search?pretty" -H 'Content-Type: application/json' -d' {"query": {"has_child": {"type": "event","score_mode": "max","query": {"term": {"title": "elasticsearch"}}}} }'(2)在結(jié)果中獲得子文檔
? ? ? ? 默認(rèn)情況下,has_child查詢只會返回父文檔,不會返回子文檔。通過添加inner_hits選項可以獲得子文檔:
(3)has_parent查詢和過濾器
? ? ? ? 使用父輩的條件來搜索子輩的時候使用has_parent查詢或過濾器。下面的代碼展示了如何搜索關(guān)于Elasticsearch的活動,而且它們只在Denver舉辦。
(4)子輩聚合
? ? ? ? ES允許在子文檔上嵌入聚合。假設(shè)已經(jīng)通過詞條聚合,獲得了get-together分組中最流行的標(biāo)簽。對于這些標(biāo)簽,需要知道每個標(biāo)簽的分組中,誰是最積極的活動參與者。下面代碼在標(biāo)簽的terms聚合下嵌套了children聚合,以此來發(fā)現(xiàn)這類會員。在children聚合中,又嵌套了另一個terms聚合來統(tǒng)計每個標(biāo)簽所對應(yīng)的活動參與者。
五、反規(guī)范化
1. 反規(guī)范化使用案例
? ? ? ? 反規(guī)范化利用數(shù)據(jù)冗余,以空間換時間,查詢時沒有必要連接不同的文檔。在分布式系統(tǒng)中這一點尤為重要,因為跨過網(wǎng)絡(luò)來連接多個文檔引入了很大的延時。ES中的反規(guī)范化主要用于處理多對多關(guān)系。與嵌套、父子的一對多實現(xiàn)不同,ES無法承諾讓多對多關(guān)系保持在一個節(jié)點內(nèi)。如圖7所示,一個單獨的關(guān)系可能會延伸到整個數(shù)據(jù)集。這種操作可能會非常昂貴,跨網(wǎng)絡(luò)的連接無法避免。
圖7 多對多關(guān)系會包含大量的數(shù)據(jù),使得本地連接成為不可能?
? ? ? ? 圖8展示了反規(guī)范化后,分組與會員之間的多對多關(guān)系。它將多對多關(guān)系的一端反規(guī)范化為許多一對多關(guān)系。
圖8 多對多關(guān)系反規(guī)范化為多個一對多關(guān)系,讓本地連接成為可能?
2. 索引、更新和刪除反規(guī)范化的數(shù)據(jù)
(1)反規(guī)范化哪個方向
? ? ? ? 是將會員復(fù)制為分組的子文檔呢。還是反過來將分組復(fù)制為會員的子文檔?必須要理解數(shù)據(jù)是如何索引、更新、刪除和查詢的,才能做出選擇。被反規(guī)范化的部分(也就是子文檔)從各方面看都是難以管理的。
- 會多次索引這些文檔,某文檔在父輩中每出現(xiàn)一次,就會被索引一次。
- 更新時,必須更新這篇文檔的所有實例。
- 刪除時,必須刪除所有實例。
- 當(dāng)單獨查詢這些子文檔時,將獲得多個同樣的內(nèi)容,所以需要在應(yīng)用端移除重復(fù)項。
? ? ? ? 基于這些假設(shè),看上去讓會員成為分組的子文檔更合理一些。會員文檔的規(guī)模更小,變動沒那么頻繁,查詢頻率也不像分組活動那么高。因此,管理復(fù)制后的會員文檔要容易一些。同理也可應(yīng)用于SQL數(shù)據(jù)庫的反規(guī)范化。
(2)如何表示一對多關(guān)系
? ? ? ? 是選擇父子關(guān)系還是嵌套文檔呢?這里,最好按照分組和會員一起搜索并獲取的頻率來選擇。嵌套查詢比has_parent或has_child查詢性能更佳。但如果會員更新頻繁,父子結(jié)構(gòu)性能更好,因為它們可以各自單獨更新。
? ? ? ? 對于本例,假設(shè)一并搜索并獲取分組和會員是很罕見的行為,而會員經(jīng)常會加入或者退出分組,因此選擇父子關(guān)系。
(3)索引
? ? ? ? 下面代碼首先定義了一個包含分組-會員父子關(guān)系的新索引,然后添加了兩個父文檔,并在兩個分組中分別添加了同一個子文檔。
(4)更新
? ? ? ? 下面代碼將搜索_id為3的全部文檔,并將其更名為Lee。為同一會員使用同樣的_id,對于會員所屬的分組每組使用一次。這樣通過會員的ID,快速并可靠地檢索某位會員的全部實例。
(5)刪除
curl -X DELETE '172.16.1.127:9200/my_index/_doc/3?routing=1&pretty' curl -X DELETE '172.16.1.127:9200/my_index/_doc/3?routing=2&pretty'3. 查詢反規(guī)范化的數(shù)據(jù)
? ? ? ? 下面的代碼首先索引兩個會員,然后在搜索的時候,將同時獲得兩者。
curl -X PUT "172.16.1.127:9200/my_index/_doc/4?routing=1&refresh&pretty" -H 'Content-Type: application/json' -d' {"first_name": "Radu","last_name": "Gheorghe","my_join_field": {"name": "member","parent": "1"} }'curl -X PUT "172.16.1.127:9200/my_index/_doc/4?routing=2&refresh&pretty" -H 'Content-Type: application/json' -d' {"first_name": "Radu","last_name": "Gheorghe","my_join_field": {"name": "member","parent": "2"} }'curl -X POST "172.16.1.127:9200/my_index/_refresh?pretty"curl '172.16.1.127:9200/my_index/_doc/_search?pretty' -H 'Content-Type: application/json' -d' {"query": {"term": {"first_name": "radu"}} }'? ? ? ? 對于多數(shù)索引和聚合,一種變通的方式是在獨立的索引中維護所有會員的副本。
總結(jié)
以上是生活随笔為你收集整理的触类旁通Elasticsearch:关联的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: layui 前端计算
- 下一篇: Exchange报错:452 4.3.1