一、關於搜索引擎
各位知道,搜索程序一般由索引鏈及搜索組件組成。
索引鏈功能的實現需要按照幾個獨立的步驟依次完成:檢索原始內容、根據原始內容來創建對應的文檔、對創建的文檔進行索引。
搜索組件用於接收用戶的查詢請求並返回相應結果,一般由用戶接口、構建可編程查詢語句的方法、查詢語句執行引擎及結果展示組件組成。

著名的開源程序Lucene是為索引組件,它提供了搜索程序的核心索引和搜索模塊,例如圖中的“Index”及下面的部分;而ElasticSearch則更像一款搜索組件,它利用Lucene進行文檔索引,並向用戶提供搜索組件,例如“Index”上面的部分。二者結合起來組成了一個完整的搜索引擎。
我們先說索引組件。
索引是一種數據結構,它允許對存儲在其中的單詞進行快速隨機訪問。
當需要從大量文本中快速檢索文本目標時,必須首先將文本內容轉換成能夠進行快速搜索的格式,以建立針對文本的索引數據結構,此即為索引過程。
它通常由邏輯上互不相關的幾個步驟組成
第一步:獲取內容。
過網絡爬蟲或蜘蛛程序等來搜集及界定需要索引的內容。
Lucene並不提供任何獲取內容的組件,因此,需要由其它應用程序負責完成這一功能,例如著名的開源爬蟲程序Solr、Nutch、Grub及Aperture等。
必要時,還可以自行開發相關程序以高效獲取自有的特定環境中的數據。獲取到的內容需要建立為小數據塊,即文檔(Document)。
第二步:建立文檔。
獲取的原始內容需要轉換成專用部件(文檔)才能供搜索引擎使用。
一般來說,一個網頁、一個PDF文檔、一封郵件或一條日志信息可以作為一個文檔。文檔由帶“值(Value)”的“域(Field)”組成,例如標題(Title)、正文(body)、摘要(abstract)、作者(Author)和鏈接(url)等。
不過,二進制格式的文檔處理起來要麻煩一些,例如PDF文件。
對於建立文檔的過程來說有一個常見操作:向單個的文檔和域中插入加權值,以便在搜索結果中對其進行排序。權值可在索引操作前靜態生成,也可在搜索期間才動態生成。權值決定了其搜索相關度。
第三步:文檔分析。
搜索引擎不能直接對文本進行索引,確切地說,必須首先將文本分割成一系列被稱為語匯單元(token)的獨立原子元素,此過程即為文檔分析。
每個token大致能與自然語言中的“單詞”對應起來,文檔分析就是用於確定文檔中的文本域如何分割成token序列。
文檔分析中要解決的問題包括如何處理連接一體的各個單詞、是否需要語法修正(例如原始內容存在錯別字)、是否需要向原始token中插入同義詞(例如laptop和notebook)、是否需要將大寫字符統一轉換為小寫字符,以及是否將單數和復數格式的單詞合並成同一個token等。
這通常需要詞干分析器等來完成此類工作,Lucene提供了大量內嵌的分析器,也支持用戶自定義分析器,甚至聯合Lucene的token工具和過濾器創建自定義的分析鏈。
第四步、文檔索引
在索引步驟中,文檔將被加入到索引列表。事實上,Lucene為此僅提供了一個非常簡單的API,而后自行內生地完成了此步驟的所有功能。
接下來,我們說搜索組件。
索引處理就是從索引中查找單詞,從而找到包含該單詞的文檔的過程。搜索質量主要由查准率(Precision)和查全率(Recall)兩個指標進行衡量。
查准率用來衡量搜索系列過濾非相關文檔的能力,而查全率用來衡量搜索系統查找相關文檔的能力。
另外,除了快速搜索大量文本和搜索速度之后,搜索過程還涉及到了許多其它問題,例如單項查詢、多項查詢、短語查詢、通配符查詢、結果ranking和排序,以及友好的查詢輸入方式等。這些問題的解決,通常需要多個組件協作完成。
搜索組件主要由以下幾個部分組成:
1、用戶搜索界面
UI(User Interface)是搜索引擎的重要組成部分,用戶通過搜索引擎界面進行搜索交互時,他們會提交一個搜索請求,該請求需要先轉換成合適的查詢對象格式,以便搜索引擎能執行查詢。
2、建立查詢
用戶提交的搜索請求通常以HTML表單或Ajax請求的形式由瀏覽器提交到搜索引擎服務器,因此,需要事先由查詢解析器一類的組件將這個請求轉換成搜索引擎使用的查詢對象格式。
3、搜索查詢
當查詢請求建立完成后,就需要查詢檢索索引並返回與查詢語句匹配的並根據請求排好序的文檔。
搜索查詢組件有着復雜的工作機制,它們通常根據搜索理論模型執行查詢操作。
常見的搜索理論模型有純布爾模型、向量空間模型及概率模型三種。Lucene采用了向量空間模型和純布爾模型。
4、展現結果
查詢獲得匹配查詢語句並排好序的文檔結果集后,需要用直觀、經濟的方式為用戶展現結果。
UI也需要為后續的搜索或操作提供清晰的向導,如完善搜索結果、尋找與匹配結果相似的文檔、進入下一頁面等。
本節的最后簡單說一下Lucene。
Lucene是一款高性能的、可擴展的信息檢索(IR)工具庫,是由Java語言開發的成熟、自由開源的搜索類庫,基於Apache協議授權。
Lucene只是一個軟件類庫,如果要發揮Lucene的功能,還需要開發一個調用Lucene類庫的應用程序。
文檔是Lucene索引和搜索的原子單位,它是包含了一個或多個域的容器,而域的值則是真正被搜索的內容。每個域都有其標識名稱,通常為一個文本值或二進制值。
將文檔加入索引中時,需要首先將數據轉換成Lucene能識別的文檔和域,域值是被搜索的對象。
例如,用戶輸入搜索內容“title:elasticsearch”時,則表示搜索“標題”域值中包含單詞“elasticsearch”的所有文檔。
默認情況下,所有文檔都沒有加權值,或者說其加權因子都為1.0。
通過改變文檔的加權因子,可以指示Lucene在計算相關性時或多或少地考慮到該文檔針對索引中其它文檔的重要程度,從而能夠將有着較大加權因子的文檔排列在較前的位置。
除了對文檔進行加權,Lucene還支持對域進行加權操作。
二、ElasticSearch工作原理、查詢及常用插件
ElasticSearch(簡稱ES)是一個基於Lucene構建的開源、分布式、RESTful的全文本搜索引擎。
不過,ElasticSearch卻也不僅只是一個全文本搜索引擎,它還是一個分布式實時文檔存儲,其中每個field均是被索引的數據且可被搜索;也是一個帶實時分析功能的分布式搜索引擎,並且能夠擴展至數以百計的服務器存儲及處理PB級的數據。
如前所述,ElasticSearch在底層利用Lucene完成其索引功能,因此其許多基本概念源於Lucene。
我們先說說ES的基本概念。
索引(Index)
ES將數據存儲於一個或多個索引中,索引是具有類似特性的文檔的集合。類比傳統的關系型數據庫領域來說,索引相當於SQL中的一個數據庫,或者一個數據存儲方案(schema)。
索引由其名稱(必須為全小寫字符)進行標識,並通過引用此名稱完成文檔的創建、搜索、更新及刪除操作。一個ES集群中可以按需創建任意數目的索引。
類型(Type)
類型是索引內部的邏輯分區(category/partition),然而其意義完全取決於用戶需求。因此,一個索引內部可定義一個或多個類型(type)。一般來說,類型就是為那些擁有相同的域的文檔做的預定義。
例如,在索引中,可以定義一個用於存儲用戶數據的類型,一個存儲日志數據的類型,以及一個存儲評論數據的類型。類比傳統的關系型數據庫領域來說,類型相當於“表”。
文檔(Document)
文檔是Lucene索引和搜索的原子單位,它是包含了一個或多個域的容器,基於JSON格式進行表示。
文檔由一個或多個域組成,每個域擁有一個名字及一個或多個值,有多個值的域通常稱為“多值域”。每個文檔可以存儲不同的域集,但同一類型下的文檔至應該有某種程度上的相似之處。
映射(Mapping)
ES中,所有的文檔在存儲之前都要首先進行分析。用戶可根據需要定義如何將文本分割成token、哪些token應該被過濾掉,以及哪些文本需要進行額外處理等等。
另外,ES還提供了額外功能,例如將域中的內容按需排序。事實上,ES也能自動根據其值確定域的類型。
接下去再說說ES Cluster相關的一些概念。
集群(Cluster)
ES集群是一個或多個節點的集合,它們共同存儲了整個數據集,並提供了聯合索引以及可跨所有節點的搜索能力。
多節點組成的集群擁有冗余能力,它可以在一個或幾個節點出現故障時保證服務的整體可用性。
集群靠其獨有的名稱進行標識,默認名稱為“elasticsearch”。節點靠其集群名稱來決定加入哪個ES集群,一個節點只能屬一個集群。
如果不考慮冗余能力等特性,僅有一個節點的ES集群一樣可以實現所有的存儲及搜索功能。
節點(Node)
運行了單個實例的ES主機稱為節點,它是集群的一個成員,可以存儲數據、參與集群索引及搜索操作。
類似於集群,節點靠其名稱進行標識,默認為啟動時自動生成的隨機Marvel字符名稱。
用戶可以按需要自定義任何希望使用的名稱,但出於管理的目的,此名稱應該盡可能有較好的識別性。
節點通過為其配置的ES集群名稱確定其所要加入的集群。
分片(Shard)和副本(Replica)
ES的“分片(shard)”機制可將一個索引內部的數據分布地存儲於多個節點,它通過將一個索引切分為多個底層物理的Lucene索引完成索引數據的分割存儲功能,這每一個物理的Lucene索引稱為一個分片(shard)。
每個分片其內部都是一個全功能且獨立的索引,因此可由集群中的任何主機存儲。創建索引時,用戶可指定其分片的數量,默認數量為5個。
Shard有兩種類型:primary和replica,即主shard及副本shard。
Primary shard用於文檔存儲,每個新的索引會自動創建5個Primary shard,當然此數量可在索引創建之前通過配置自行定義,不過,一旦創建完成,其Primary shard的數量將不可更改。
Replica shard是Primary Shard的副本,用於冗余數據及提高搜索性能。
每個Primary shard默認配置了一個Replica shard,但也可以配置多個,且其數量可動態更改。ES會根據需要自動增加或減少這些Replica shard的數量。
ES集群可由多個節點組成,各Shard分布式地存儲於這些節點上。
ES可自動在節點間按需要移動shard,例如增加節點或節點故障時。簡而言之,分片實現了集群的分布式存儲,而副本實現了其分布式處理及冗余功能。
下面說說ES系統及插件。
ES依賴於JDK,使用Oracke JDK或OpenJDK均可。
JDK在不同平台的安裝方式各異,具體方法這里不再介紹。ES的安裝也非常容易,通常只需要簡單修改其配置文件中的集群名稱,並啟動服務即可,這里不再贅述。
ElasticSearch在設計上支持插件式體系結構,用戶可根據需要通過插件來增強ElasticSearch的功能。
目前,常用的通過插件擴展的功能包括添加自定義映射類型、自定義分析器、本地腳本、自定義發現方式等等。
安裝及移除插件
插件的安裝有兩種方式:直接將插件放置於plugins目錄中,或通過plugin腳本進行安裝。
Marvel、BigDesk及Head這三個是較為常用的插件。
ElasticSearch提供了易用但功能強大的RESTful API以用於與集群進行交互,這些API大體可分為如下四類:
(1) 檢查集群、節點、索引等健康與否,以及獲取其相關狀態與統計信息;
(2) 管理集群、節點、索引數據及元數據;
(3) 執行CRUD操作及搜索操作;
(4) 執行高級搜索操作,例如paging、filtering、scripting、faceting、aggregations及其它操作;
下面簡單說一說ES中的數據查詢。
Query API是ElasticSearch的API中較大的一部分,基於Query DSL(JSON based language for building complex queries),可完成諸多類型查詢操作,例如simple term query, phrase, range, boolean, fuzzy, span, wildcard, spatial等簡單類型查詢、組合簡單查詢類型為復雜類型查詢,以及文檔過濾等。
另外,查詢執行過程通常要分成兩個階段,分散階段及合並階段。
分散階段是向所查詢的索引中的所有shard發起執行查詢的過程,合並階段是將各shard返回的結果合並、排序並響應給客戶端的過程。
向ElasticSearch發起查詢操作有兩種方式:一是通過RESTful request API傳遞查詢參數,也稱“query-string”;另一個是通過發送REST request body,也稱作JSON格式。
通過發送request body的方式進行查詢,可以通過JSON定義查詢體編寫更具表現形式的查詢請求。訪問ElasticSearch的search API需要通過_search端點進行。例如,向students索引發起一個空查詢。
~]$ curl -XGET 'localhost:9200/students/_search?pretty'
上面的查詢命令也可改寫為帶request body的格式,其等同效果的命令如下。
~]$ curl -XGET 'localhost:9200/students/_search?pretty' -d '
{
"query": { "match_all": { } }
}'
此命令所示的查詢語句是ElasticSearch提供的JSON風格的域類型查詢語言,也即所謂的Query DSL。
上面的命令中,“query”參數給出了查詢定義,match_all給出了查詢類型,它表示返回給定索引的所有文檔。
除了query參數之外,還可以額外指定其它參數來控制搜索結果,例如“size”參數可定義返回的文檔數量(默認為10),而“from”參數可指定結果集中要顯示出的文檔的起始偏移量(默認為0),“sort”參數可指明排序規則等。
ElasticSearch的大多數search API(除了Explain API)都支持多索引(mutli-index)和多類型(multi-type)。如果不限制查詢時使用的索引和類型,查詢請求將發給集群中的所有文檔。
ElasticSearch會把查詢請求並行發給所有shard的主shard或某一副本shard,將返回的結果集中的前10返回給用戶。
不過,如果是想向某一或某些個索引的某一或某些類型發起查詢請求,可通過指定查詢的URL進行。
/_search:搜索所有索引的所有類型; /students/_search:搜索students索引的所有類型; /students,tutors/_search:搜索students和tutors索引的所有類型; /s*,t*/_search:搜索名稱以s和t開頭的所有索引的所有類型; /students/class1/_search:搜索students索引的class1類型; /_all/class1,class2/_search:搜索所有索引的class1和class2類型;
索引一個文檔時,Elasticsearch會取得其所有域的所有值,並將其連接起來合並為一個大字符串,其被索引為一個特殊域_all。
在某次查詢中,如果在query-string中未指定查詢的域,則使用_all域進行查詢。
下面四個查詢的功用會有所不同。前兩個在_all域中搜索,而后兩個將會在class域上做精確搜索。
GET /_search?q="Huashan" GET /_search?q="Huashan Pai" GET /_search?q=class:"Huashan Pai" GET /_search?q=class:"Huahan"
需要注意的是,文檔中每個域的值可能會存儲為特定類型,而非字符串類型,因此,_all域的索引方式與特域的索引方式未必完全相同。
文檔中,域的數據存儲時支持“string”、“numbers”、“Booleans”和“dates”幾種類型,不同類型的數據在索引時是略有區別的。
在創建文檔時,Elasticsearch會通過檢查域的值來動態為其創建mapping,可通過Mapping API來查看type的mapping,其訪問端點是_mapping。
下面,我們聊一個麻煩一點的問題,ES的精確值、full-text及倒排索引。
ES的數據可被廣義的分為兩種類型:“types:exect”和“full-text”。
精確值(Exact values)就是指數據未曾加工過的原始值,而Full-text則用於引用文本中的數據。
在查詢中,精確值是很容易進行搜索的,但full-text則需要判斷文檔在“多大程度上”匹配查詢請求,換句話講,即需要評估文檔與給定查詢的相關度(relevant)。
因此,所謂的full-text查詢通常是指在給定的文本域內部搜索指定的關鍵字,但搜索操作該需要真正理解查詢者的目的,例如:
(1) 搜索“UK”應該返回包含“United Kingdom”的相關文檔;
(2) 搜索“jump”應該返回包含“JUMP”、“jumped”、“jumps”、“jumping”甚至是“leap”的文檔;
(3) 搜索“johnny walker”應該匹配包含“Johnnie Walker”的文檔;
為了完成此類full-text域的搜索,ES必須首先分析文本並將其構建成為倒排索引(inverted index),倒排索引由各文檔中出現的單詞列表組成,列表中的各單詞不能重復且需要指向其所在的各文檔。
因此,為了創建倒排索引,需要先將各文檔中域的值切分為獨立的單詞(也稱為term或token),而后將之創建為一個無重復的有序單詞列表。這個過程稱之為“分詞(tokenization)”。

其次,為了完成此類full-text域的搜索,倒排索引中的數據還需進行“正規化(normalization)”為標准格式,才能評估其與用戶搜索請求字符串的相似度。
例如,將所有大寫字符轉換為小寫,將復數統一單數,將同義詞統一進行索引等。
另外,執行查詢之前,還需要將查詢字符串按照同與索引過程的同種格式進行“正規化(normalization)”。
這里的“分詞”及“正規化”操作也稱為“分析(analysis)”。
Analysis過程由兩個步驟的操作組成:首先將文本切分為terms(詞項)以適合構建倒排索引,其次將各terms正規化為標准形式以提升其“可搜索度”。這兩個步驟由分析器(analyzers)完成。
一個分析器通常需要由三個組件構成:字符過濾器(Character filters)、分詞器(Tokenizer)和分詞過濾器(Token filters)組成。
-
字符過濾器:在文本被切割之前進行清理操作,例如移除HTML標簽,將&替換為字符等;
-
分詞器:將文本切分為獨立的詞項;簡單的分詞器通常是根據空白及標點符號進行切分;
-
分詞過濾器:轉換字符(如將大寫轉為小寫)、移除詞項(如移除a、an、of及the等)或者添加詞項(例如,添加同義詞);
Elasticsearch內置了許多字符過濾器、分詞器和分詞過濾器,用戶可按需將它們組合成“自定義”的分析器。
固然,創建倒排索引時需要用到分析器,但傳遞搜索字符串時也可能需要分析器,甚至還要用到與索引創建時相同的分析器才能保證單詞匹配的精確度。
執行full-text域搜索時,需要用到分析器,但執行精確值搜索時,查詢過程不會分析查詢字符串而是直接進行精確值匹配。
關於ES,我們最后說一說Queries and Filters。
盡管統一稱之為query DSL,事實上Elasticsearch中存在兩種DSL:查詢DSL(query DSL)和過濾DSL(filter DSL)。
查詢子句和過濾子句的自然屬性非常相近,但在使用目的上略有區別。
簡單來講,當執行full-text查詢或查詢結果依賴於相關度分值時應該使用查詢DSL,當執行精確值(extac-value)查詢或查詢結果僅有“yes”或“no”兩種結果時應該使用過濾DSL。
Filter DSL計算及過濾速度較快,且適於緩存,因此可有效提升后續查詢請求的執行速度。
而query DSL不僅要查找匹配的文檔,還需要計算每個文件的相關度分值,因此為更重量級的查詢,其查詢結果不會被緩存。
不過,得益於倒排索引,一個僅返回少量文檔的簡單query或許比一個跨數百萬文檔的filter執行起來並得顯得更慢。
Elasticsearch支持許多的query和filter,但最常用的也不過幾種。
Filter DSL中常見的有term Filter、terms Filter、range Filter、exists and missing Filters和bool Filter。
而Query DSL中常見的有match_all、match 、multi_match及bool Query。鑒於時間關系,這里不再細述,朋友們可參考官方文檔學習。
Queries用於查詢上下文,而filters用於過濾上下文,不過,Elasticsearch的API也支持此二者合並運行。
組合查詢可用於合並查詢子句,組合過濾用於合並過濾子句,然而,Elasticsearch的使用習慣中,也常會把filter用於query上進行過濾。不過,很少有機會需要把query用於filter上的。
