做者:robben,騰訊高級工程師
商業轉載請聯繫騰訊WeTest得到受權,非商業轉載請註明出處。 java
導語:互聯網產品中的檢索功能隨處可見。當你的項目規模是百度大搜|商搜或者微信公衆號搜索這種體量的時候,本身開發一個搜索引擎,加入各類定製的需求和優化,是很是天然的事情。但若是隻是普通的中小型項目甚至創業團隊|創業項目,直接拿輪子則是更合理的選擇。
ElasticSearch就是這樣一個搜索引擎的輪子。更重要的是,除去常規的全文檢索功能以外,它還具備基礎的統計分析功能(最多見的就是聚合),這也讓他變得更增強大和實用。
還在用數據庫的like來實現產品的全文檢索嗎?拋棄她,用ElasticSearch吧~node
ElasticSearch(下簡稱ES)是基於Lucene的一個開源搜索引擎產品。Lucene是java編寫的一套開源文檔檢索的基礎庫,包括詞、文檔、域、倒排索引、段、相關性得分等基本功能,而ES則是使用了這些庫,搭建的一個能夠直接拿來使用的搜索引擎產品。直觀地理解,Lucene提供汽車零部件,而ES直接賣車。程序員
提及ES的誕生,也是個頗有意思的故事。ES的做者Shay Banon——「幾年前他仍是一個待業工程師,跟隨本身的新婚妻子來到倫敦。妻子想在倫敦學習作一名廚師,而本身則想爲妻子開發一個方便搜索菜譜的應用,因此才接觸到Lucene。直接使用Lucene構建搜索有不少問題,包含大量重複性的工做,因此Shay便在Lucene的基礎上不斷地進行抽象,讓Java程序嵌入搜索變得更容易,通過一段時間的打磨便誕生了他的第一個開源做品Compass,中文即'指南針'的意思。以後,Shay找到了一份面對高性能分佈式開發環境的新工做,在工做中他漸漸發現愈來愈須要一個易用的、高性能、實時、分佈式搜索服務,因而他決定重寫Compass,將它從一個庫打形成了一個獨立的server,並將其更名爲Elasticsearch。「算法
引自(http://www.infoq.com/cn/news/2014/12/elasticsearch-birth-development)。數據庫
可見鼓搗起來的程序員是多麼有愛,雖然聽說Shay Banon承諾給妻子的菜譜搜索還沒問世......json
本文大概地介紹了ES的原理,以及Wetest在使用ES中的一些經驗總結。由於ES自己涉及的功能和知識點很是普遍,因此這裏重點挑出了實際項目中可能會用到,也可能會踩坑的一些關鍵點進行了闡述。微信
集羣(Cluster): ES是一個分佈式的搜索引擎,通常由多臺物理機組成。這些物理機,經過配置一個相同的cluster name,互相發現,把本身組織成一個集羣。app
節點(Node):同一個集羣中的一個 Elasticearch主機。jvm
主分片(Primary shard):索引(下文介紹)的一個物理子集。同一個索引在物理上能夠切多個分片,分佈到不一樣的節點上。分片的實現是Lucene 中的索引。elasticsearch
注意:ES中一個索引的分片個數是創建索引時就要指定的,創建後不可再改變。因此開始建一個索引時,就要預計數據規模,將分片的個數分配在一個合理的範圍。
副本分片(Replica shard):每一個主分片能夠有一個或者多個副本,個數是用戶本身配置的。ES會盡可能將同一索引的不一樣分片分佈到不一樣的節點上,提升容錯性。對一個索引,只要不是全部shards所在的機器都掛了,就還能用。主、副本、節點的概念以下圖:
索引(Index):邏輯概念,一個可檢索的文檔對象的集合。相似與DB中的database概念。同一個集羣中可創建多個索引。好比,生產環境常見的一種方法,對每月產生的數據建索引,以保證單個索引的量級可控。索引->類型->文檔,ES中的文檔以這樣的邏輯關係組織了起來。
類型(Type):索引的下一級概念,大概至關於數據庫中的table。同一個索引裏能夠包含多個 Type。 我的感受在實際使用中type這一級經常用的很少,直接就在一個索引中建一個type,在這個type下去創建文檔集合和進行搜索了。
文檔(Document):即搜索引擎中的文檔概念,也是ES中一個能夠被檢索的基本單位,至關於數據庫中的row,一條記錄。
字段(Field):至關於數據庫中的column。ES中,每一個文檔,實際上是以json形式存儲的。而一個文檔能夠被視爲多個字段的集合。好比一篇文章,可能包括了主題、摘要、正文、做者、時間等信息,每一個信息都是一個字段,最後被整合成一個json串,落地到磁盤。
映射(Mapping):至關於數據庫中的schema,用來約束字段的類型,不過 Elasticsearch 的 mapping 能夠不顯示地指定、自動根據文檔數據建立。
Elasticsearch很友好地提供了RestFul的API,能夠經過HTTP請求直接完成全部操做。好比下面官方的一個例子,往索引twitter添加文檔,type是tweet,文檔的id是1:
相應地,根據user字段檢索文檔:
一、索引的shards個數:
shards的個數,最好是和節點數相關的。理論上對同一個索引,單機上的shards個數最好不要超過兩個,這樣每一個查詢儘量並行。但由於ES中shards的個數是肯定了就沒辦法再調整的,因此若是考慮到數據會高速增加,一開始分配多些也能夠。另外一個常見思路是按時間緯度(如月)去定義ES索引——由於能夠動態調整新加的索引的shards個數。其餘的一些狀況,好比下面舉到的Wetest聚合的例子,由於須要數據儘可能地按照渠道切分開,因此定義了不少個shards(200個),但太多的shards一般是不推薦的,ES管理起來也有開銷。
二、heap內存:官方建議是可用內存的一半,是經過啓動ES的環境中,定義環境變量的方式完成的。如export ES_HEAP_SIZE=10g
三、cluster.name:集羣的邏輯名稱。只有cluster name相同的機器,纔會在邏輯上組成一個集羣。好比,內網中有5臺ES機器的實例,是能夠構成幾個互不干擾的ES集羣的。
四、discovery.zen.minimum_master_nodes:
這個是用於集羣的分佈式決策的最少master機器個數。和常見的分佈式協調算法同樣,爲了不腦裂現象,建議超過一半的機器,n/2+1
五、discovery.zen.ping.unicast.hosts:
ES集羣的機器列表。注意ES單點不用配置集羣中的全部機器列表,像一個連通圖同樣,只要每臺機器配置了其餘機器,而這些配置又是互相能夠鏈接的,那ES最終就會發現全部機器,構成集羣。如['111.111.111.0','111.111.111.1','111.111.111.2']
mapping相似於數據庫裏的表結構,定義個mapping就意味着建立了一個索引。與數據庫不一樣的是,一個索引並不須要顯示地創建mapping,好比,上面那個在twitter索引插入文檔數據的例子,若是執行的時候尚未定義索引,ES便會根據文檔的字段和內容,自動建立索引和mapping。然而,這樣建立的索引字段,每每可能不是咱們所須要的。因此,仍是本身預先經過手動定義mapping來建立索引比較好。下面是建立mapping的例子,這個例子在my_index這個目錄下,爲user、blogpost這些type建立了mapping。其中properties下面是各類字段的定義,包括了string、數值、日期等類型的定義。
如圖中的紅框部分,這個例子中有兩個須要注意的地方:
一、user_id是string類型的,但它的index被定義爲了「not_analzyed",這個須要搞清其中的意義:一般,搜索引擎中全文檢索的功能簡單說是這樣實現的:對原始文檔進行分詞後用這些詞去創建倒排索引,在線上檢索時,再將用戶的查詢詞進行分詞,用分詞結果去拉取多個倒排索引的拉鍊結果、歸併、相關性排序等,獲得最終結果。可是,對於有些string類型的字段,其實並不想建倒排,就只想精確匹配,好比用戶的名字,只想查到name字段精確爲「張三」的人,而不是分詞後獲得的「張四」和「李三」兩我的,這個時候,就須要定義index類型字段。這個字段有no、analyzed、not_analyzed三種類型,no是壓根兒不給這字段建索引,analyzed是分析和按全文檢索的方式建,not_analyzed是徹底匹配的關鍵詞查詢方式。
二、date類型,建立mapping時須要經過「format」指定錄入的多種可能時間格式。這樣建立文檔的時候,ES會根據輸入文檔的字段自動去肯定是哪種。不過直觀地想象下,在建立文檔時,指定明確的時間格式,省去ES動態判斷的開銷,應該會提高些微小的性能。此外,要注意,epoch_second(秒單位時間戳)和epoch_millis(毫秒單位)儘可能不要混用,若是非要混用也要在插入的時候明確指明是哪一個。曾經踩過坑,插入epoch_second的是秒級時間戳,但ES優先認爲是毫秒,致使時間被縮小1000倍,最近的時間變成了1970年當年的某個時間。
下圖列出了ES當前版本中能夠進行mapping的數據類型、內置的字段、mapping操做能夠攜帶的參數。由於篇幅緣由這裏就不詳細解釋了:
這裏要詳細介紹的,是上圖中紅框標出的,咱們建立mapping時實際用到的比較關鍵的兩個內置類型,和兩個mapping參數。這幾個都會直接影響最後索引訪問的性能:
1)_source: es會把全部字段拼成一個原始的json落入磁盤,因此這個能夠理解爲全量原始數據,他不能用來索引,卻能夠在須要的時候返回。注意儘可能不要禁用,好比禁用後,用script去update就不支持了。
2)_all:一個「僞」字段,用來實現模糊的全文索引。能夠這樣理解:在建索引的時候,把全部字段拼成一個字符串,而後對這個「大」字段進行切詞,建倒排,而後這個字段就被丟棄了,沒有真正落入磁盤。當全文檢索時,若是沒有指明查詢的域,好比標題、正文(這種是很常見的),就從這個大的倒排中拉取文檔拉鍊。能夠想象,一些標記或值類型的字段,如日期、得分,這種在全文檢索時是沒意義的,就能夠不包含在_all內,而文本域,如title、doc,就包含在_all之中。這些都是在建mapping時能夠、並且最好指定的。
3)doc_values: doc_values和下面的field_data都是在聚合(後面會介紹)、排序這些統計時用的參數,默認都是開啓的。排序、聚合,這種在文檔全局進行的工做,用倒排索引確定不合適。因此,對not_analyzed(即不建倒排)的字段,doc_values用一種列模式的方式(能夠參考hbase)來存儲文檔的正排,方便在文檔全局作統計。doc_values是存儲在磁盤的,若是你明確有些字段只是展現,不用於統計的話,能夠把這個禁用掉。Doc_values必定不會對analyzed域建索引(都切詞了,想一想也不合適,怎麼建列索引嘛),而是用下面的field data。
4)field_data:對analyzed的文本域,好比正文,其實也會有統計的需求(好比ES也支持按一些關鍵詞對文檔進行聚合統計,但這種任務經常使用的方法是經過離線工具,如hadoop或者單機的分析,作好了後推送到在線索引,直接在ES去算其實感受有些奇怪)。雖然並不適合在搜索引擎中作,但你真的作了,es也會把這個數據動態地load內存的一個field data中進行運算。因此,想一想就知道,這是個很是耗內存的操做,極可能把jvm heap吃完了!!es默認是隻打開,但不load,只是在你須要進行analyzed域的排序和聚合的時候,纔去動態load這個內存(lazy的方式)。因此,儘可能不要在查詢的時候去打開這個潘多拉魔盒,或者乾脆就把這個選項關掉吧。
誰說搜索引擎只能用來搜索?ES不只能搜索,還能在搜索的結果集合上直接進行統計,很強大吧。ES目前穩定的非實驗階段聚合主要分兩種:Metrics Aggregation(指標聚合)和Bucket Aggregation(桶聚合)。
指標聚合主要指常規的集合數學統計類運算,如官方guide的這個例子:找到交易的全部紅色的車,而後求它們的平均價格:
結果大概是這樣的:
神奇吧~指標運算還包括其餘,如最大、最小、求和、個數、地理座標運算等。然而咱們今天要進行實例講解的則主要是Bucket Aggregation,桶聚合。桶聚合是指把文檔,按照某個給定字段分紅不一樣的組,而後在組內進行進一步聚合運算,並返回桶級的結果。比較直觀的理解,如:直方圖、分時間段統計等等。以下面這個例子,是桶聚合中的term聚合,即按照color這個字段,精確匹配後進行分桶,而後桶內還進一步嵌套了平均價格聚合、和按製造商進一步的分桶聚合。
統計的結果相似下面這樣,紅色的車共有4輛,平均價格是32500,而且又包含了3輛本田和1輛寶馬:
上面是簡單的例子。在咱們的WeTest輿情中,有論壇熱帖這樣一個功能,即,實時統計某個數據源中(如百度貼吧),某個論壇裏(如王者榮耀吧),一段時間內(如3個月),回覆數最多的TopN個帖子。
這個功能如今在線上的實現方法就不詳細介紹了,大體是從數據庫和Hbase中掃描對應的數據,維持一個堆,獲取出TOP N的思路。一方面是稍微有些耗時,另外一方面是請求量很大時可能對DB和Hbase的訪問帶來壓力,因此也想找一種備選的方案,咱們想到了用ES。
爲了用ES的桶聚合,咱們首先設計如何存儲文檔(即全部用戶評論)的方案。因爲數據量很是大(十億級),因此咱們首先想到了把文檔按時間分紅不一樣的索引(如按月),而後在指定月份(如3個月)的索引上,聚合出評論最多的Top帖子。然而這樣是有問題的:當在多個ES索引上聚合時,ES不會把全部索引的結果放在一塊兒聚合TopN,而是單獨在每一個索引求得TopN後,再放在一塊兒聚合。這是個使用時要注意的小坑。這樣致使的結果是,直接在多個索引上聚合出的TopN,並非真正的TopN(好比3個月中,每月都是否是Top 1,但三個月加起來就是Top了 1。局部最優不等於全局最優)。
因此,從時間上切分,這條路基本被堵死了。那隻能從空間上切分了(您問能不能不切分?十億級的數據量,上百個GB,不切分的話,乖乖,每次都要從這幾百GB的文件裏找東西,想一想也知道有多慢了...)。從空間切分,一樣須要考慮兩個問題:1)如何將數據hash到shards。2)切分多少個shards。對於第一個問題,由於咱們的聚合統計是在每一個渠道(能夠理解爲論壇)下的,不會跨渠道,因此,按照渠道ID進行shards分配,把相同論壇的數據hash到一個shard便可。這樣,每次請求某個渠道的聚合結果,把請求按渠道ID routing到對應的shard去運算。對於第二個問題,要看具體的規模了。咱們的數據量有上百G,數據源上千個,因此咱們但願每一個shard上的內容儘可能少,保證在單個shard上聚合的時候會更快,固然shards個數又不能太多,不然會給ES引入很是大的管理開銷。綜合下來,咱們選擇的shards個數是200個。
遺憾的是,ES只能根據你指定的key(論壇ID)去作hash後進行路由,這就致使了不一樣的shards上數據不是徹底平均的,最多的能超過10GB,最少的只有幾十MB。若是哪一天,ES若是開放自定義routing規則或者對shards數據進行均衡的方法,那就行了。
ES常常爲人詬病的一個地方是建索引比較慢,10億數據的索引構建時間要花幾天。這也容易理解,天下沒有免費的午飯,讀寫的性能每每是互斥的,快速讀取和檢索意味着大量索引和輔助數據的預先創建,那寫入時勢必會慢。如何取捨,須要看實際的業務場景而定了。下面就是建好索引後,去聚合某論壇內指定時間段內Top帖子的接口調用方式。
而後,咱們按連續統計最熱的TopN(N爲不一樣的個數)個渠道內的Top30熱帖結果的方式分別對ES和線上已有的服務進行了測試:
上面的五個結果圖直觀地反應了用如今Wetest輿情線上的常規統計方式和ES聚合統計的方式獲取結果的耗時。
從結果中,咱們大概推斷出了ES統計聚合運算的作法:先把全部符合過濾條件的數據所有檢索出來,而後在內存中進行排序和聚合運算。也就是說,符合條件的數據量級越大,聚合運算越慢。本着這個原則,結果圖也就比較好理解了:
1)在連續對最熱的Top1000個渠道去進行熱帖聚合時,ES的表現大部分都優於現有實現。這是由於Top1000的渠道中,大部分渠道被分在了很是小的shards上,有的只有幾MB,數據量很小,在這樣的shards中聚合,是很快的。
2)時間緯度上,統計3個月的數據,ES大部分狀況下都比現有方法慢,而1個月或1天的狀況下,ES都要快。這是由於3個月的條件下,符合條件的數據量級增大(最大的一個話題下有3萬跟帖),ES的運算效率降低比較厲害。
3)從Top1000到Top10,ES的總時間逐漸變差於現有方法。這是由於,空間緯度上,Top10渠道符合條件的數據量級很是大,因此ES的運算效率降低比較厲害。
作了這個實驗後,ES在WeTest頭部數據源上的聚合速度並不比如今快,但在中部和長尾上的效果更優,這說明ES的聚合受候選集數據量的影響很是大,因此是否切換這種方式也還沒最終決定。不過,這個實驗證實了ES聚合的強大能力,至少,不用本身寫什麼代碼,只經過接口調用就能把這樣海量數據的統計運算完成了,仍是很方便的一件事情,同時性能也不錯。若是自行實現的統計運算中會增大DB的壓力,那麼經過ES聚合分離這部分請求,也是一個很是好的選擇。
WeTest產品輿情,一站式瞭解你的產品口碑和用戶喜愛。