Solr簡(jiǎn)介

1.Solr是什么?
Solr它是一種開(kāi)放源碼的、基于 Lucene Java 的搜索服務(wù)器,易于加入到 Web 應(yīng)用程序中。Solr 提供了層面搜索(就是統(tǒng)計(jì))、命中醒目顯示并且支持多種輸出格式(包括XML/XSLT 和JSON等格式)。Solr是一個(gè)高性能,采用Java開(kāi)發(fā),
基于Lucene的全文搜索服務(wù)器。同時(shí)對(duì)其進(jìn)行了擴(kuò)展,提供了比Lucene更為豐富的查詢語(yǔ)言,同時(shí)實(shí)現(xiàn)了可配置、可擴(kuò)展并對(duì)查詢性能進(jìn)行了優(yōu)化,并且提供了一個(gè)完善的功能管理界面,是一款非常優(yōu)秀的全文搜索引擎。
Solr是一個(gè)獨(dú)立的企業(yè)級(jí)搜索應(yīng)用服務(wù)器,它對(duì)外提供類似于Web-service的API接口。用戶可以通過(guò)http請(qǐng)求,向搜索引擎服務(wù)器提交一定格式的XML文件,生成索引;也可以通過(guò)Http Get操作提出查找請(qǐng)求,并得到XML格式的返回結(jié)果。
Solr易于安裝和配置,而且附帶了一個(gè)基于HTTP 的管理界面。可以使用 Solr 的表現(xiàn)優(yōu)異的基本搜索功能,也可以對(duì)它進(jìn)行擴(kuò)展從而滿足企業(yè)的需要。
Solr架構(gòu)圖

Solr的特性

高效、靈活的緩存功能,垂直搜索功能,高亮顯示搜索結(jié)果,通過(guò)索引復(fù)制來(lái)提高可用性,提供一套強(qiáng)大Data Schema來(lái)定義字段,類型和設(shè)置文本分析,提供基于Web的管理界面等.
· 高級(jí)的全文搜索功能
· 專為高通量的網(wǎng)絡(luò)流量進(jìn)行的優(yōu)化
· 基于開(kāi)放接口(XML和HTTP)的標(biāo)準(zhǔn)
· 綜合的HTML管理界面
· 可伸縮性-能夠有效地復(fù)制到另外一個(gè)Solr搜索服務(wù)器
· 使用XML配置達(dá)到靈活性和適配性
· 可擴(kuò)展的插件體系
2. Lucene 是什么?
Lucene是一個(gè)基于Java的全文信息檢索工具包,它不是一個(gè)完整的搜索應(yīng)用程序,而是為你的應(yīng)用程序提供索引和搜索功能。Lucene 目前是 Apache Jakarta(雅加達(dá)) 家族中的一個(gè)開(kāi)源項(xiàng)目。也是目前最為流行的基于Java開(kāi)源全文檢索工具包。目前已經(jīng)有很多應(yīng)用程序的搜索功能是基于 Lucene ,比如Eclipse 幫助系統(tǒng)的搜索功能。Lucene能夠?yàn)槲谋绢愋偷臄?shù)據(jù)建立索引,所以你只要把你要索引的數(shù)據(jù)格式轉(zhuǎn)化的文本格式,Lucene 就能對(duì)你的文檔進(jìn)行索引和搜索。
3. Solr vs Lucene
Solr與Lucene 并不是競(jìng)爭(zhēng)對(duì)立關(guān)系,恰恰相反Solr 依存于Lucene,因?yàn)镾olr底層的核心技術(shù)是使用Lucene 來(lái)實(shí)現(xiàn)的,Solr和Lucene的本質(zhì)區(qū)別有以下三點(diǎn):搜索服務(wù)器,企業(yè)級(jí)和管理。Lucene本質(zhì)上是搜索庫(kù),不是獨(dú)立的應(yīng)用程序,而Solr是。Lucene專注于搜索底層的建設(shè),而Solr專注于企業(yè)應(yīng)用。Lucene不負(fù)責(zé)支撐搜索服務(wù)所必須的管理,而Solr負(fù)責(zé)。所以說(shuō),一句話概括 Solr: Solr是Lucene面向企業(yè)搜索應(yīng)用的擴(kuò)展。
Solr與Lucene架構(gòu)圖:

Solr使用Lucene并且擴(kuò)展了它!
· 一個(gè)真正的擁有動(dòng)態(tài)字段(Dynamic Field)和唯一鍵(Unique Key)的數(shù)據(jù)模式(Data Schema)
· 對(duì)Lucene查詢語(yǔ)言的強(qiáng)大擴(kuò)展!
· 支持對(duì)結(jié)果進(jìn)行動(dòng)態(tài)的分組和過(guò)濾
· 高級(jí)的,可配置的文本分析
· 高度可配置和可擴(kuò)展的緩存機(jī)制
· 性能優(yōu)化
· 支持通過(guò)XML進(jìn)行外部配置
· 擁有一個(gè)管理界面
· 可監(jiān)控的日志
· 支持高速增量式更新(Fast incremental Updates)和快照發(fā)布(Snapshot Distribution)
下載安裝
下載
https://mirrors.tuna.tsinghua.edu.cn/apache/lucene/solr/8.5.1/solr-8.5.1.tgz
解壓即可.目錄結(jié)構(gòu):

啟動(dòng)命令
-
cd ~/solr/bin回車 -
solr start -p 8983回車,等待啟動(dòng)成功 -
solr stop -p 8983這個(gè)是停止solr命令
solr-8.5.1/bin$ ./solr start -p 8983
*** [WARN] *** Your open file limit is currently 256.
It should be set to 65000 to avoid operational disruption.
If you no longer wish to see this warning, set SOLR_ULIMIT_CHECKS to false in your profile or solr.in.sh
*** [WARN] *** Your Max Processes Limit is currently 1418.
It should be set to 65000 to avoid operational disruption.
If you no longer wish to see this warning, set SOLR_ULIMIT_CHECKS to false in your profile or solr.in.sh
Waiting up to 180 seconds to see Solr running on port 8983 [-]
Started Solr server on port 8983 (pid=44785). Happy searching!
啟動(dòng)成功后,訪問(wèn) http://127.0.0.1:8983/solr/#/
可以看到Solr的管理界面:

JVM
Runtime
Oracle Corporation Java HotSpot(TM) 64-Bit Server VM 1.8.0_40 25.40-b25
Processors
12
Args
-DSTOP.KEY=solrrocks-DSTOP.PORT=7983-Djetty.home=/Users/jack/soft/solr-8.5.1/server-Djetty.port=8983-Dsolr.data.home=-Dsolr.default.confdir=/Users/jack/soft/solr-8.5.1/server/solr/configsets/_default/conf-Dsolr.install.dir=/Users/jack/soft/solr-8.5.1-Dsolr.jetty.https.port=8983-Dsolr.jetty.inetaccess.excludes=-Dsolr.jetty.inetaccess.includes=-Dsolr.log.dir=/Users/jack/soft/solr-8.5.1/server/logs-Dsolr.log.muteconsole-Dsolr.solr.home=/Users/jack/soft/solr-8.5.1/server/solr-Duser.timezone=UTC-XX:+AlwaysPreTouch-XX:+ParallelRefProcEnabled-XX:+PerfDisableSharedMem-XX:+PrintGCApplicationStoppedTime-XX:+PrintGCDateStamps-XX:+PrintGCDetails-XX:+PrintGCTimeStamps-XX:+PrintHeapAtGC-XX:+PrintTenuringDistribution-XX:+UseG1GC-XX:+UseGCLogFileRotation-XX:+UseLargePages-XX:GCLogFileSize=20M-XX:MaxGCPauseMillis=250-XX:NumberOfGCLogFiles=9-XX:OnOutOfMemoryError=/Users/jack/soft/solr-8.5.1/bin/oom_solr.sh 8983 /Users/jack/soft/solr-8.5.1/server/logs-Xloggc:/Users/jack/soft/solr-8.5.1/server/logs/solr_gc.log-Xms512m-Xmx512m-Xss256k-verbose:gc
啟動(dòng)并重新啟動(dòng) Solr
您可以使用 start 命令來(lái)啟動(dòng) Solr,使用 restart 命令允許您在 Solr 已經(jīng)運(yùn)行或者已經(jīng)停止的情況下重新啟動(dòng) Solr。
該 start 和 restart 命令有多種選擇,讓您在 SolrCloud 模式下運(yùn)行,使用一個(gè)示例配置集,從一個(gè)不是默認(rèn)的主機(jī)名或端口開(kāi)始并指向本地的 ZooKeeper 集合。
bin/solr start [options]
bin/solr start -help
bin/solr restart [options]
bin/solr restart -help
使用 restart 命令時(shí),必須傳遞您在啟動(dòng) Solr 時(shí)最初傳遞的所有參數(shù)。在幕后,啟動(dòng)了一個(gè)停止請(qǐng)求,所以 Solr 將在被再次啟動(dòng)之前停止。如果沒(méi)有節(jié)點(diǎn)已經(jīng)運(yùn)行,則重新啟動(dòng)將跳過(guò)此步驟停止并繼續(xù)啟動(dòng) Solr。
啟動(dòng)參數(shù)
bin/solr 腳本提供了許多選項(xiàng),允許您以常見(jiàn)的方式自定義服務(wù)器,例如更改偵聽(tīng)端口。但是,大多數(shù)默認(rèn)設(shè)置對(duì)于大多數(shù) Solr 安裝都是足夠的,特別是剛開(kāi)始時(shí)。
-a "<string>"
使用額外的 JVM 參數(shù)(例如以 -X 開(kāi)頭的參數(shù))啟動(dòng) Solr。如果您正在傳遞以 “-D” 開(kāi)頭的 JVM 參數(shù),則可以省略 -a 選項(xiàng)。例如:
bin/solr start -a "-Xdebug -Xrunjdwp:transport=dt_socket, server=y,suspend=n,address=1044"
-cloud
以 SolrCloud 模式啟動(dòng) Solr,該模式也將啟動(dòng) Solr 附帶的嵌入式 ZooKeeper 實(shí)例。
這個(gè)選項(xiàng)可以簡(jiǎn)單地縮短為-c。
如果您已經(jīng)在運(yùn)行您想要使用的 ZooKeeper 集合,而不是嵌入式(單節(jié)點(diǎn))ZooKeeper,則還應(yīng)該傳遞 -z 參數(shù)。
有關(guān)更多詳細(xì)信息,請(qǐng)參閱下面的 SolrCloud 模式部分。例如:
bin/solr start -c
-d <dir>
定義一個(gè)服務(wù)器目錄,默認(rèn)為server(如,$SOLR_HOME/server)。重寫此選項(xiàng)的情況并不常見(jiàn)。在同一臺(tái)主機(jī)上運(yùn)行多個(gè) Solr 實(shí)例時(shí),更常見(jiàn)的是為每個(gè)實(shí)例使用相同的服務(wù)器目錄,并使用 -s 選項(xiàng)使用唯一的Solr主目錄更為常見(jiàn)。例如:
bin/solr start -d newServerDir
應(yīng)用示例
Building and Running SolrJ Applications: put the following dependency in the project’s pom.xml:
<dependency>
<groupId>org.apache.solr</groupId>
<artifactId>solr-solrj</artifactId>
<version>x.y.z</version>
</dependency>
Single node Solr client
String urlString = "http://localhost:8983/solr/techproducts";
SolrClient solr = new HttpSolrClient.Builder(urlString).build();
SolrCloud client
// Using a ZK Host String
String zkHostString = "zkServerA:2181,zkServerB:2181,zkServerC:2181/solr";
SolrClient solr = new CloudSolrClient.Builder().withZkHost(zkHostString).build();
// Using already running Solr nodes
SolrClient solr = new CloudSolrClient.Builder().withSolrUrl("http://localhost:8983/solr").build();
Once you have a SolrClient, you can use it by calling methods like query(), add(), and commit().
客戶端API簡(jiǎn)介
Solr的核心是一個(gè)Web應(yīng)用程序,但是由于它是建立在開(kāi)放的協(xié)議之上的,任何類型的客戶端應(yīng)用程序都可以使用Solr。
HTTP是客戶端應(yīng)用程序和Solr之間使用的基本協(xié)議??蛻舳颂岢稣?qǐng)求,Solr做一些工作并提供響應(yīng)。客戶使用請(qǐng)求來(lái)請(qǐng)求Solr執(zhí)行查詢或索引文件等操作。
客戶端應(yīng)用程序可以通過(guò)創(chuàng)建HTTP請(qǐng)求和解析HTTP響應(yīng)到達(dá)Solr。客戶端API封裝了發(fā)送請(qǐng)求和解析響應(yīng)的大部分工作,這使得編寫客戶端應(yīng)用程序變得更加容易。
客戶使用Solr的五個(gè)基本操作來(lái)與Solr一起工作。這五個(gè)操作分別是:查詢、索引、刪除、提交和優(yōu)化。
查詢通過(guò)創(chuàng)建一個(gè)包含所有查詢參數(shù)的URL來(lái)執(zhí)行。Solr檢查請(qǐng)求URL,執(zhí)行查詢并返回結(jié)果。其他操作是相似的,雖然在某些情況下,HTTP請(qǐng)求是一個(gè)POST操作,并包含除請(qǐng)求URL中包含的任何信息之外的信息。例如,索引操作可能包含請(qǐng)求正文中的文檔。
Solr 還具有一個(gè) EmbeddedSolrServer,它提供了一個(gè) Java API 而不需要 HTTP 連接。
Setting XMLResponseParser
SolrJ uses a binary format, rather than XML, as its default response format. If you are trying to mix Solr and SolrJ versions where one is version 1.x and the other is 3.x or later, then you MUST use the XML response parser. The binary format changed in 3.x, and the two javabin versions are entirely incompatible. The following code will make this change:
solr.setParser(new XMLResponseParser());
Performing Queries
Use query() to have Solr search for results. You have to pass a SolrQuery object that describes the query, and you will get back a QueryResponse (from the org.apache.solr.client.solrj.response package).
SolrQuery has methods that make it easy to add parameters to choose a request handler and send parameters to it. Here is a very simple example that uses the default request handler and sets the query string:
SolrQuery query = new SolrQuery();
query.setQuery(mQueryString);
To choose a different request handler, there is a specific method available in SolrJ version 4.0 and later:
query.setRequestHandler("/spellCheckCompRH");
You can also set arbitrary parameters on the query object. The first two code lines below are equivalent to each other, and the third shows how to use an arbitrary parameter q to set the query string:
query.set("fl", "category,title,price");
query.setFields("category", "title", "price");
query.set("q", "category:books");
Once you have your SolrQuery set up, submit it with query():
QueryResponse response = solr.query(query);
The client makes a network connection and sends the query. Solr processes the query, and the response is sent and parsed into a QueryResponse.
The QueryResponse is a collection of documents that satisfy the query parameters. You can retrieve the documents directly with getResults() and you can call other methods to find out information about highlighting or facets.
SolrDocumentList list = response.getResults();
Indexing Documents
Other operations are just as simple. To index (add) a document, all you need to do is create a SolrInputDocument and pass it along to the SolrClient’s `add() method. This example assumes that the SolrClient object called 'solr' is already created based on the examples shown earlier.
SolrInputDocument document = new SolrInputDocument();
document.addField("id", "552199");
document.addField("name", "Gouda cheese wheel");
document.addField("price", "49.99");
UpdateResponse response = solr.add(document);
// Remember to commit your changes!
solr.commit();
Uploading Content in XML or Binary Formats
SolrJ lets you upload content in binary format instead of the default XML format. Use the following code to upload using binary format, which is the same format SolrJ uses to fetch results. If you are trying to mix Solr and SolrJ versions where one is version 1.x and the other is 3.x or later, then you MUST stick with the XML request writer. The binary format changed in 3.x, and the two javabin versions are entirely incompatible.
solr.setRequestWriter(new BinaryRequestWriter());
Lucence工作原理
lucence 是一個(gè)高性能的java全文檢索工具包,他使用倒排序文件索引結(jié)構(gòu),改結(jié)構(gòu)和相應(yīng)的生成算法如下:
一、設(shè)有兩篇文章1和2
文章1的內(nèi)容為:Tom lives in guangzhou,i live in guangzhou too
文章2的內(nèi)容為:He once lived in shanghai
由于lucence是基于關(guān)鍵詞索引和查詢的,因此我們首先要取得這兩篇文章的關(guān)鍵詞。通常我們要做一下處理:
a.我們現(xiàn)在有的是文章內(nèi)容,即一個(gè)字符串,我們先要找出字符串中的所有單詞,即分詞。英文單詞由于用空格分隔,比較好處理。中文單詞間是連在一起的需要特殊的分詞處理。
b.文章中的”in”, “once” “too”等詞沒(méi)有什么實(shí)際意義,中文中的“的”“是”等字通常也無(wú)具體含義,這些不代表概念的詞可以過(guò)濾掉
c.用戶通常希望查“He”時(shí)能把含“he”,“HE”的文章也找出來(lái),所以所有單詞需要統(tǒng)一大小寫。
d.用戶通常希望查“l(fā)ive”時(shí)能把含“l(fā)ives”,“l(fā)ived”的文章也找出來(lái),所以需要把“l(fā)ives”,“l(fā)ived”還原成“l(fā)ive”
e.文章中的標(biāo)點(diǎn)符號(hào)通常不表示某種概念,也可以過(guò)濾掉
在lucene中以上措施由Analyzer類完成
經(jīng)過(guò)上面處理后,
文章1的所有關(guān)鍵詞為:[tom] [live] [guangzhou] [live] [guangzhou]
文章2的所有關(guān)鍵詞為:[he] [live] [shanghai]**
- 有了關(guān)鍵詞后,我們就可以建立倒排索引了。上面的對(duì)應(yīng)關(guān)系是:“文章號(hào)”對(duì)“文章中所有關(guān)鍵詞”。倒排索引把這個(gè)關(guān)系倒過(guò)來(lái),變成:“關(guān)鍵詞”對(duì)“擁有該關(guān)鍵詞的所有文章號(hào)”。文章1,2經(jīng)過(guò)倒排后變成
| 關(guān)鍵詞 | 文章號(hào) |
|---|---|
| guangzhou | 1 |
| he | 2 |
| i | 1 |
| live | 1,2 |
| shanghai | 2 |
| tom | 1 |
通常僅知道關(guān)鍵詞在哪些文章中出現(xiàn)還不夠,我們還需要知道關(guān)鍵詞在文章中出現(xiàn)次數(shù)和出現(xiàn)的位置,通常有兩種位置:a)字符位置,即記錄該詞是文章中第幾個(gè)字符(優(yōu)點(diǎn)是關(guān)鍵詞亮顯時(shí)定位快);b)關(guān)鍵詞位置,即記錄該詞是文章中第幾個(gè)關(guān)鍵詞(優(yōu)點(diǎn)是節(jié)約索引空間、詞組(phase)查詢快),lucene 中記錄的就是這種位置。
加上“出現(xiàn)頻率”和“出現(xiàn)位置”信息后,我們的索引結(jié)構(gòu)變?yōu)椋?/p>
| 關(guān)鍵詞 | 文章號(hào)[出現(xiàn)頻率] | 出現(xiàn)位置 |
|---|---|---|
| guangzhou | 1[2] | 3,6 |
| he | 2[1] | 1 |
| i | 1[1] | 4 |
| live | 1[2],2[1] | 2,5,2 |
| shanghai | 2[1] | 3 |
| tom | 1[1] | 1 |
以live 這行為例我們說(shuō)明一下該結(jié)構(gòu):live在文章1中出現(xiàn)了2次,文章2中出現(xiàn)了一次,它的出現(xiàn)位置為“2,5,2”這表示什么呢?我們需要結(jié)合文章號(hào)和出現(xiàn)頻率來(lái)分析,文章1中出現(xiàn)了2次,那么“2,5”就表示live在文章1中出現(xiàn)的兩個(gè)位置,文章2中出現(xiàn)了一次,剩下的“2”就表示live是文章2中第 2個(gè)關(guān)鍵字。
以上就是lucene索引結(jié)構(gòu)中最核心的部分。我們注意到關(guān)鍵字是按字符順序排列的(lucene沒(méi)有使用B樹(shù)結(jié)構(gòu)),因此lucene可以用二元搜索算法快速定位關(guān)鍵詞。
實(shí)現(xiàn)時(shí) lucene將上面三列分別作為詞典文件(Term Dictionary)、頻率文件(frequencies)、位置文件 (positions)保存。其中詞典文件不僅保存有每個(gè)關(guān)鍵詞,還保留了指向頻率文件和位置文件的指針,通過(guò)指針可以找到該關(guān)鍵字的頻率信息和位置信息。
Lucene中使用了field的概念,用于表達(dá)信息所在位置(如標(biāo)題中,文章中,url中),在建索引中,該field信息也記錄在詞典文件中,每個(gè)關(guān)鍵詞都有一個(gè)field信息(因?yàn)槊總€(gè)關(guān)鍵字一定屬于一個(gè)或多個(gè)field)。
為了減小索引文件的大小,Lucene對(duì)索引還使用了壓縮技術(shù)。首先,對(duì)詞典文件中的關(guān)鍵詞進(jìn)行了壓縮,關(guān)鍵詞壓縮為<堉?綴長(zhǎng)度,后綴>,例如:當(dāng)前詞為“阿拉伯語(yǔ)”,上一個(gè)詞為“阿拉伯”,那么“阿拉伯語(yǔ)”壓縮為<3,語(yǔ)>。其次大量用到的是對(duì)數(shù)字的壓縮,數(shù)字只保存與上一個(gè)值的差值(這樣可以減小數(shù)字的長(zhǎng)度,進(jìn)而減少保存該數(shù)字需要的字節(jié)數(shù))。例如當(dāng)前文章號(hào)是16389(不壓縮要用3個(gè)字節(jié)保存),上一文章號(hào)是16382,壓縮后保存7(只用一個(gè)字節(jié))。
下面我們可以通過(guò)對(duì)該索引的查詢來(lái)解釋一下為什么要建立索引。
假設(shè)要查詢單詞 “l(fā)ive”,lucene先對(duì)詞典二元查找、找到該詞,通過(guò)指向頻率文件的指針讀出所有文章號(hào),然后返回結(jié)果。詞典通常非常小,因而,整個(gè)過(guò)程的時(shí)間是毫秒級(jí)的。
而用普通的順序匹配算法,不建索引,而是對(duì)所有文章的內(nèi)容進(jìn)行字符串匹配,這個(gè)過(guò)程將會(huì)相當(dāng)緩慢,當(dāng)文章數(shù)目很大時(shí),時(shí)間往往是無(wú)法忍受的。
參考資料
https://lucene.apache.org/solr/guide/8_5/solr-tutorial.html
https://lucene.apache.org/solr/
http://www.itdecent.cn/p/be9e4accb486
https://www.w3cschool.cn/solr_doc/solr_doc-emn62fs5.html
https://lucene.apache.org/solr/guide/6_6/using-solrj.html#UsingSolrJ-SettingXMLResponseParser
https://www.cnblogs.com/senlinyang/p/8064202.html
Kotlin開(kāi)發(fā)者社區(qū)

專注分享 Java、 Kotlin、Spring/Spring Boot、MySQL、redis、neo4j、NoSQL、Android、JavaScript、React、Node、函數(shù)式編程、編程思想、"高可用,高性能,高實(shí)時(shí)"大型分布式系統(tǒng)架構(gòu)設(shè)計(jì)主題。
High availability, high performance, high real-time large-scale distributed system architecture design。
分布式框架:Zookeeper、分布式中間件框架等
分布式存儲(chǔ):GridFS、FastDFS、TFS、MemCache、redis等
分布式數(shù)據(jù)庫(kù):Cobar、tddl、Amoeba、Mycat
云計(jì)算、大數(shù)據(jù)、AI算法
虛擬化、云原生技術(shù)
分布式計(jì)算框架:MapReduce、Hadoop、Storm、Flink等
分布式通信機(jī)制:Dubbo、RPC調(diào)用、共享遠(yuǎn)程數(shù)據(jù)、消息隊(duì)列等
消息隊(duì)列MQ:Kafka、MetaQ,RocketMQ
怎樣打造高可用系統(tǒng):基于硬件、軟件中間件、系統(tǒng)架構(gòu)等一些典型方案的實(shí)現(xiàn):HAProxy、基于Corosync+Pacemaker的高可用集群套件中間件系統(tǒng)
Mycat架構(gòu)分布式演進(jìn)
大數(shù)據(jù)Join背后的難題:數(shù)據(jù)、網(wǎng)絡(luò)、內(nèi)存和計(jì)算能力的矛盾和調(diào)和
Java分布式系統(tǒng)中的高性能難題:AIO,NIO,Netty還是自己開(kāi)發(fā)框架?
高性能事件派發(fā)機(jī)制:線程池模型、Disruptor模型等等。。。
合抱之木,生于毫末;九層之臺(tái),起于壘土;千里之行,始于足下。不積跬步,無(wú)以至千里;不積小流,無(wú)以成江河。