某電商平臺開發記要——全文檢索 Lucene 3.0 原理與代碼分析

開發Web應用時,你常常要加上搜索功能。甚至還不知道要搜什麼,就在草圖上畫了一個放大鏡。php

說到目前計算機的文字搜索在應用上的實現,象形文字天生就比拼音字母劣勢的多,分詞、詞性判斷、拼音文字轉換啥的,容易讓人香菇。html

首先咱們來了解下什麼是Inverted index,翻譯過來的名字有不少,好比反轉索引、倒排索引什麼的,讓人不明因此,能夠理解爲:一個未經處理的數據庫中,通常是以文檔ID做爲索引,以文檔內容做爲記錄。而Inverted index 指的是將單詞或記錄做爲索引,將文檔ID做爲記錄,這樣即可以方便地經過單詞或記錄查找到其所在的文檔。並非什麼高深概念。前端

oracle裏經常使用的位圖索引(Bitmap index)也可認爲是Inverted index。位圖索引對於相異基數低的數據最爲合適,即記錄多,但取值較少。好比一個100W行的表有一個字段會頻繁地被當作查詢條件,咱們會想到在這一列上面創建一個索引,可是這一列只可能取3個值。那麼若是創建一個B*樹索引(普通索引)是不合適的,由於不管查找哪個值,均可能會查出不少數據,這時就能夠考慮使用位圖索引。位圖索引相對於傳統的B*樹索引,在葉子節點上採用了徹底不一樣的結構組織方式。傳統B*樹索引將每一行記錄保存爲一個葉子節點,上面記錄對應的索引列取值和行rowid信息。而位圖索引將每一個可能的索引取值組織爲一個葉子節點。每一個位圖索引的葉子節點上,記錄着該索引鍵值的起始截止rowid和一個位圖向量串。若是不考慮起止rowid,那麼就是取值有幾個,就有幾個索引,好比上例,雖然說有100W條記錄,可是針對只有3個可取值的字段來講,索引節點只有3個,相似於下圖:git

須要注意的是,因爲全部索引字段同值行共享一個索引節點,位圖索引不適用於頻繁增刪改的字段,不然可能會致使針對該字段(其它行)的增刪改阻塞(對其它非索引字段的操做無影響),是一種索引段級鎖。具體請參看 深刻解析B-Tree索引與Bitmap位圖索引的鎖代價github

下面說說筆者知道的一些全文搜索的工具。redis

文中綠色文字表示筆者並不肯定描述是否正確,紅色表示筆者疑問,如有知道的同窗請不吝賜教,多謝!算法


ICTCLAS分詞系統sql

原本想借着ICTCLAS簡單介紹下中文分詞的一些原理和算法,不過網上已有比較好的文章了,可參看 ICTCLAS分詞系統研究。中文分詞基本上是基於詞典,[可能]涉及到的知識 —— HMM(隱馬爾科夫鏈)、動態規劃、TF-IDF、凸優化,更基礎的就是信息論、機率論、矩陣等等,咱們在讀書的時候可能並不知道所學何用,想較快重溫的同窗可閱讀吳軍博士的《數學之美》。這些概念我會擇要在後續博文中介紹。下面咱們就來看看分詞系統在數據庫中的具體應用。數據庫


Postgresql的中文分詞windows

在PostgreSQL中,GIN索引就是Inverted index,GIN索引存儲一系列(key, posting list)對, 這裏的posting list是一組出現鍵的行ID。 每個被索引的項目均可能包含多個鍵,所以同一個行ID可能會出如今多個posting list中。 每一個鍵值只被存儲一次,所以在相同的鍵出如今不少項目的狀況下,GIN索引是很是緊湊的(來自PostgreSQL 9.4.4 中文手冊)。顯然,將之應用到數組類型的字段上是很是合適的。全文檢索類型(tsvector)一樣支持GIN索引,能夠加速查詢。據說9.6版本出了一個什麼RUM索引,對比GIN,檢索效率獲得了很大的提高,可參看 PostgreSQL 全文檢索加速 快到沒有朋友 - RUM索引接口(潘多拉魔盒)

幸運的是,阿里雲RDS PgSQL已支持zhparser(基於SCWS)中文分詞插件。

鏈接要分詞的數據庫,執行如下語句:

-- 安裝擴展 create extension zhparser; -- 查看該數據庫的全部擴展 select * from pg_ts_parser; -- 支持的token類型,即詞性,好比形容詞名詞啥的 select ts_token_type('zhparser'); -- 建立使用zhparser做爲解析器的全文搜索的配置 CREATE TEXT SEARCH CONFIGURATION testzhcfg (PARSER = zhparser); -- 往全文搜索配置中增長token映射,上面的token映射只映射了名詞(n),動詞(v),形容詞(a),成語(i),嘆詞(e)和習慣用語(l)6種,這6種之外的token所有被屏蔽。 -- 詞典使用的是內置的simple詞典,即僅作小寫轉換。 ALTER TEXT SEARCH CONFIGURATION testzhcfg ADD MAPPING FOR n,v,a,i,e,l WITH simple; 
set zhparser.punctuation_ignore = t; --
忽略標點符號

如今咱們就能夠方便的進行中文分詞了,好比「select to_tsvector('testzhcfg','南京市長江大橋');」,會拆分爲「'南京市':1 '長江大橋':2」。若是要分的更細粒度,那麼能夠設置複合分詞,複合分詞的級別:1~15,按位異或的 1|2|4|8 依次表示 短詞|二元|主要字|所有字,缺省不復合分詞,這是SCWS的配置選項,對應的zhparser選項爲zhparser.multi_short、zhparser.multi_duality、zhparser.multi_zmain、zhparser.multi_zall。好比咱們要設置短詞複合分詞,那麼就set zhparser.multi_short=on;那麼「select to_tsvector('testzhcfg','南京市長江大橋');」獲得的分詞結果將是「'南京':2 '南京市':1 '大橋':5 '長江':4 '長江大橋':3」,這樣就能夠匹配到更多的關鍵詞,固然檢索效率會變慢。

短詞複合分詞是根據詞典來的,好比詞典中有'一次性'、'一次性使用'、’'一次性使用吸痰管'、'使用'、'吸痰管'5個詞語,當multi_short=off時,select to_tsvector('testzhcfg','"一次性使用吸痰管"');返回最大匹配的"一次性使用吸痰管",而爲on時,返回的是"'一次性':2 '一次性使用吸痰管':1 '使用':3 '吸痰管':4",讓人困惑的是,結果裏沒有提取出'一次性使用'這個詞,不知怎麼回事。

在產品表上建一列tsv存儲產品名稱的tsvector值,並對該列建GIN索引。

CREATE OR REPLACE FUNCTION func_get_relatedkeywords(keyword text) RETURNS SETOF text[] AS $BODY$ begin if (char_length(keyword)>0) then RETURN QUERY select string_to_array(tsv::text,' ') from "Merchandises" where tsv @@ plainto_tsquery('testzhcfg',keyword); end if; end $BODY$ LANGUAGE plpgsql VOLATILE

注意plainto_tsquery和to_tsquery稍微有點區別,好比前者不認識':*',然後者遇到空格會報錯。

這會返回全部包含傳入關鍵詞的tsvector格式的字符串,因此咱們要在業務層分解去重再傳遞給前端。

 1 public async Task<ActionResult> GetRelatedKeywords(string keyword)  2 {  3 var keywords = await MerchandiseContext.GetRelatedKeywords(keyword);  4 if(keywords != null && keywords.Count>0)  5  {  6 //將全部產品的關鍵詞彙總去重  7 var relatedKeywords = new List<string>();  8 foreach(var k in keywords)  9  { 10 for(int i=0;i<k.Count();i++) //pg返回的是帶冒號的tsvector格式 11  { 12 k[i] = k[i].Split(':')[0].Trim('\''); 13  } 14  relatedKeywords.AddRange(k);//k能夠做爲總體,好比多個詞語做爲一個組合加入返回結果,更科學(這裏是拆分後獨立加入返回結果) 15  } 16 //根據出現重複次數排序(基於重複次數多,說明關聯性高的預設) 17 relatedKeywords = relatedKeywords.GroupBy(rk => rk).OrderByDescending(g => g.Count()).Select(g => g.Key).Distinct().ToList(); 18 relatedKeywords.RemoveAll(rk=>keyword.Contains(rk)); 19 return this.Json(new OPResult<IEnumerable<string>> { IsSucceed = true, Data = relatedKeywords.Take(10) }, JsonRequestBehavior.AllowGet); 20  } 21 return this.Json(new OPResult { IsSucceed = true }, JsonRequestBehavior.AllowGet); 22 }

now,咱們就初步實現了相似各大電商的搜索欄關鍵詞聯想功能:

然而,尚有一些值得考慮的細節。當數據庫中產品表愈來愈大,毫無疑問查詢時間會變長,雖然咱們只須要前面10個關聯詞,但可能有重複詞,因此並不能簡單的在sql語句後面加limit 10。暫時縮小不了查詢範圍,能夠減小相同關鍵詞的數據庫查詢頻率,即在上層加入緩存。key是關鍵詞或關鍵詞組合,value是關聯關鍵詞,關鍵詞多的話,加上各類組合那麼數據量確定很大,因此咱們緩存時間要根據數據量和用戶搜索量定個合適時間。以redis爲例:

 1 public static async Task SetRelatedKeywords(string keyword, IEnumerable<string> relatedKeywords)  2 {  3 var key = string.Format(RedisKeyTemplates.MERCHANDISERELATEDKEYWORDS, keyword);  4 IDatabase db = RedisGlobal.MANAGER.GetDatabase();  5 var count = await db.SetAddAsync(key, relatedKeywords.Select<string, RedisValue>(kw => kw).ToArray());  6 if (count > 0)  7 db.KeyExpire(key, TimeSpan.FromHours(14), CommandFlags.FireAndForget); //緩存  8 }  9 10 public static async Task<List<string>> GetRelatedKeywords(string keyword) 11 { 12 IDatabase db = RedisGlobal.MANAGER.GetDatabase(); 13 var keywords = await db.SetMembersAsync(string.Format(RedisKeyTemplates.MERCHANDISERELATEDKEYWORDS, keyword)); 14 return keywords.Select(kw => kw.ToString()).ToList(); 15 }

當用戶在搜索欄裏輸入的並不是完整的關鍵詞——輸入的文字並未精確匹配到數據庫裏的任一tsvector——好比就輸入一個「交」或者「鎖型」之類,並無提供用戶預期的自動補完功能(雖然自動補完和關鍵詞聯想本質上是兩個不一樣的功能,不過用戶可能並不這麼想)。咱們知道,在關鍵詞後加':*',好比「交:*」,那麼是能夠匹配到的,如:select '交鎖型:2 交鎖型股骨重建釘主釘:1 股骨:3 重建:4'::tsvector @@ to_tsquery('交:*'),返回的就是true。然而咱們總不能讓用戶輸入的時候帶上:*,在代碼裏給自動附加:*是一種解決方法(select to_tsquery('testzhcfg','股骨重建:*'),結果是"'股骨':* & '重建':*"),然而會帶來可能的效率問題,好比select to_tsquery('testzhcfg','一次性使用吸痰管:*'),它會拆分爲"'一次性使用吸痰管':* & '一次性':* & '使用':* & '吸痰管':*",而且出於空格的考慮,咱們用的是plainto_tsquery,而它是不認識:*的。

當用戶輸入一些字符的時候,如何判斷是已完成的關鍵詞(進行關鍵詞聯想)仍是未輸完的關鍵詞(自動補完),這是個問題。咱們能夠將用戶常搜的一些關鍵詞緩存起來(或者按期從tsv字段獲取),當用戶輸入匹配到多個(>1)緩存關鍵詞時,說明關鍵詞還未輸完整,返回關鍵詞列表供用戶選擇,不然(匹配數量<=1)時,則去查詢關聯關鍵詞。一樣用redis(很幸運,redis2.8版本後支持set集合的值正則匹配):

/// <summary> /// 獲取關鍵詞(模糊匹配) /// </summary> public static List<string> GetKeywords(string keyword, int takeSize = 10) { IDatabase db = RedisGlobal.MANAGER.GetDatabase(); //這裏的pageSize表示單次遍歷數量,而不是說最終返回數量 var result = db.SetScan(RedisKeyTemplates.SearchKeyword, keyword + "*", pageSize: Int32.MaxValue); return result.Take(takeSize).Select<RedisValue, string>(r => r).ToList(); }

固然,也有可能用戶輸入已經匹配到一個完整關鍵詞,但同時該關鍵詞是另一些關鍵詞的一部分。咱們能夠先去緩存裏面取關鍵詞,若數量少於10個(頁面上提示至多10個),那麼就再去看是否有關聯關鍵詞補充。

大部分網站搜索還支持拼音搜索,即按全拼或拼音首字母搜索。

對關鍵詞[組合]賦予權重,權重計算能夠依據搜索量、搜索結果等,每次返回給用戶最有效的前幾條。這之後再說吧。

總的來講,數據庫自帶的全文檢索仍是創建在字段檢索的基礎上,適合傳統SQL查詢場景,並且圍繞分詞系統的查詢方案和邏輯大部分須要本身處理,涉及到稍複雜的應用就力不從心,或者效率低下了(好比上述的自動補完功能),另外分佈部署的時候也要在上層另作集羣架構。


Elasticsearch

基於5.4版本

節點:一個運行中的 Elasticsearch 實例稱爲一個 節點。

集羣是由一個或者多個擁有相同 cluster.name 配置的節點組成, 它們共同承擔數據和負載的壓力。當有節點加入集羣中或者從集羣中移除節點時,集羣將會從新平均分佈全部的數據。一個集羣只能有一個主節點。

索引:做爲名詞時,相似於傳統關係型數據庫中的一個數據庫。索引其實是指向一個或者多個物理 分片邏輯命名空間 。一個索引應該是(非強制)因共同的特性被分組到一塊兒的文檔集合, 例如,你可能存儲全部的產品在索引 products 中,而存儲全部銷售的交易到索引 sales 中。

分片:一個分片是一個 Lucene 的實例(亦即一個 Lucene 索引 ),它僅保存了所有數據中的一部分。索引內任意一個文檔都歸屬於一個主分片,因此主分片的數目決定着索引可以保存的最大數據量;副本分片做爲硬件故障時保護數據不丟失的冗餘備份,併爲搜索和返回文檔等讀操做提供服務。

類型:由類型名和mapping組成,mapping相似於數據表的schema,或者說類[以及字段的具體]定義。

技術上講,多個類型能夠在相同的索引中存在,只要它們的字段不衝突,即同名字段類型必須相同。可是,若是兩個類型的字段集是互不相同的,這就意味着索引中將有一半的數據是空的(字段將是 稀疏的 ),最終將致使性能問題。——致使這一限制的根本緣由,是Lucene沒有文檔類型的概念,一個Lucene索引(ES裏的分片)以扁平的模式定義其中全部字段,即假如該分片裏有兩個類型A\B,A中定義了a\c兩個字符串類型的字段,B定義了b\c兩個字符串類型的字段,那麼Lucene建立的映射包括的是a\b\c三個字符串類型的字段,若是A\B中c字段類型不同,那麼配置這個映射時,將會出現異常。由此亦知,一個分片可包含不一樣類型的文檔。

文檔:一個對象被序列化成爲 JSON,它被稱爲一個 JSON 文檔,指定了惟一 ID 。

假如文檔中新增了一個未事先定義的字段,或者給字段傳遞了非定義類型的值,那麼就涉及到動態映射的概念了。另外,儘管能夠增長新的類型到索引中,或者增長新的字段到類型中,可是不能添加新的分析器或者對現有的字段作改動,遇到這種狀況,咱們可能須要針對此類文檔重建索引。

在 Elasticsearch 中, 每一個字段的全部數據 都是 默認被索引的 。 即每一個字段都有爲了快速檢索設置的專用倒排索引。

樂觀併發控制,Elasticsearch 使用 version 版本號控制、處理衝突。

Lucene中的[倒排]索引(在Lucene索引中表現爲 段 的概念,Lucene索引除表示全部 的集合外,還有一個 提交點 的概念 ),[一旦建立]是不可變的,這有諸多好處:

  • 不須要鎖;
  • 重用索引緩存[,而非每次去磁盤獲取索引](即緩存不會失效,由於索引不變),進一步能夠重用相同查詢[構建過程和返回的數據],而不須要每次都從新查詢;
  • 容許[索引被]壓縮;

可是 數據/文檔 變化後,畢竟仍是得更新 索引/段 的,那麼怎麼更新呢?—— 新的文檔和段會被建立,而舊的文檔和段被標記爲刪除狀態,查詢時,後者會被拋棄。

安裝Elasticsearch前須要安裝JRE(Java運行時,注意和JDK的區別),而後去到https://www.elastic.co/start裏,根據提示步驟安裝運行便可。(筆者爲windows環境)

安裝完以後咱們就能夠在經過http://localhost:5601打開kibana的工做臺。爲了讓遠程機子能夠訪問,在啓動kibana以前要先設置kibana.yml中的server.host,改成安裝了kibana的機器的IP地址,即server.host: "192.168.0.119",注意中間冒號和引號之間要有空格,不然無效,筆者被此處坑成狗,也是醉了。同理,要elasticsearch遠程可訪問,須要設置elasticsearch.yml中的network.host。

單機上啓動多個節點,文檔中說 「你能夠在同一個目錄內,徹底依照啓動第一個節點的方式來啓動一個新節點。多個節點能夠共享同一個目錄。」 沒搞懂什麼意思,試了下再開個控制檯進入es目錄執行命令行,會拋異常。因此仍是老老實實按照網上其它資料提到的,拷貝一份es目錄先,要幾個節點就拷貝幾份。。

ES官方給.Net平臺提供了兩個工具—— Elasticsearch.Net 和 NEST,前者較底層,後者基於前者基礎上進行了更高級的封裝以方便開發調用。

NEST有個Connection pools,這跟咱們日常認爲的鏈接池不是同一個概念,而是一種策略——以什麼方式鏈接到ES——有四種策略:

  • SingleNodeConnectionPool:每次鏈接指向到同一個節點(通常設置爲主節點,專門負責路由)
  • StaticConnectionPool:若是知道一些節點Uri的話,那麼每次就[隨機]鏈接到這些節點[中的一個]
  • SniffingConnectionPool:derived from StaticConnectionPool,a sniffing connection pool allows itself to be reseeded at run time。然而暫時並不知道具體用處。。。
  • StickyConnectionPool:選擇第一個節點做爲請求主節點。一樣不知用這個有什麼好處。。。

下面咱們使用ES實現自動補完的功能,順帶介紹涉及到的知識點。

服務器根據用戶當前輸入返回可能的[用戶真正想輸的]字符串——"Suggest As You Type"。ES提供了四個Suggester API(可參看 Elasticsearch Suggester詳解,這篇文章沒有介紹第四個Context Suggester,我會在本節後面稍做描述),本文舉例的自動補完,適合使用Completion Suggester(後面會說到使用上存在問題)。

咱們先來看類型定義:

 1 public class ProductIndexES
 2 {
 3     public long Id { get; set; }
 4     public string ProductName { get; set; }
 5     /// <summary>
 6     /// 品牌標識
 7     /// </summary>
 8     public long BrandId { get; set; }
 9     public string BrandName { get; set; }
10     /// <summary>
11     /// 店鋪標識
12     /// </summary>
13     public long ShopId { get; set; }
14     public string ShopName { get; set; }
15     /// <summary>
16     /// 價格
17     /// </summary>
18     public decimal Price { get; set; }
19     /// <summary>
20     /// 上架時間
21     /// </summary>
22     public DateTime AddDate { get; set; }
23     /// <summary>
24     /// 售出數量
25     /// </summary>
26     public long SaleCount { get; set; }
27     //產品自定義屬性
28     public object AttrValues { get; set; }
29     public Nest.CompletionField Suggestions { get; set; }
30 }

若要使用Completion Suggester,類型中須要有一個CompletionField的字段,能夠將原有字段改爲CompletionField類型,好比ProductName,咱們一樣能夠針對CompletionField設置Analyzer,因此不影響該字段原有的索引功能CompletionField接受的是字符串數組Input字段,經測試也看不出Analyzer對它的做用(自動補完返回的字符串是Input數組中與用戶輸入起始匹配的字符串,對分詞後的字符串沒有體現),因此Analyzer配置項的做用是什麼使人費解);或者另外加字段,用於專門存放Input數組,這就更加靈活了,本例採用的是後者。

建立索引:

 1 var descriptor = new CreateIndexDescriptor("products")
 2     .Mappings(ms => ms.Map<ProductIndexES>("product", m => m.AutoMap()
 3         .Properties(ps => ps
 4         //string域index屬性默認是 analyzed 。若是咱們想映射這個字段爲一個精確值,咱們須要設置它爲 not_analyzed或no或使用keyword
 5         .Text(p => p
 6         .Name(e => e.ProductName).Analyzer("ik_max_word").SearchAnalyzer("ik_max_word")
 7         .Fields(f => f.Keyword(k => k.Name("keyword"))))//此處做爲演示
 8         .Keyword(p => p.Name(e => e.BrandName))
 9         .Keyword(p => p.Name(e => e.ShopName))
10         .Completion(p => p.Name(e => e.Suggestions)))));//此處能夠設置Analyzer,可是看不出做用
11 
12 Client.CreateIndex(descriptor);

第六、7行表示ProductName有多重配置,做爲Text,它能夠用做全文檢索,固然咱們但願用戶在輸入產品全名時也能精確匹配到,因此又設置其爲keyword表示是個關鍵詞,這種狀況就是Multi fields。不過因爲咱們設置了SearchAnalyzer,和Analyzer同樣,用戶輸入會按一樣方式分詞後再去匹配,因此無論是全名輸入或者部分輸入,均可以經過全文檢索到。

接着把對象寫入索引,方法以下:

 1 public void IndexProduct(ProductIndexES pi)
 2 {
 3     var suggestions = new List<string>() { pi.BrandName, pi.ShopName, pi.ProductName };
 4     var ar = this.Analyze(pi.ProductName);//分詞
 5     suggestions.AddRange(ar.Tokens.Select(t => t.Token));
 6     suggestions.RemoveAll(s => s.Length == 1);//移除單個字符(由於對自動補完來講沒有意義)
 7     pi.Suggestions = new CompletionField { Input = suggestions.Distinct() };
 8 
 9     //products是索引,product是類型
10     Client.Index(pi, o => o.Index("products").Id(pi.Id).Type("product"));
11 }

假設我新插入了三個文檔,三個suggestions裏的input分別是["產品"],["產家合格"],["產品測試","產品","測試"],顯然,根據上述方法的邏輯,最後那個數組中的後兩項是第一項分詞出來的結果。

接下來就是最後一步,經過用戶輸入返回匹配的記錄:

1 public void SuggestCompletion(string text)
2 {
3     var result = Client.Search<ProductIndexES>(d => d.Index("products").Type("product")
4     .Suggest(s => s.Completion("prd-comp-suggest", cs => cs.Field(p => p.Suggestions).Prefix(text).Size(8))));
5     Console.WriteLine(result.Suggest);
6 }

好,一切看似很完美,這時候用戶輸入「產」這個字,咱們指望的是返回["產品","產家合格","產品測試"],次一點的話就再多一個"產品"(由於全部input中有兩個"產品")。然而結果卻出我意料,我在kibana控制檯裏截圖:

返回的是["產品","產品","產家合格"]。查找資料發現這彷佛是ES團隊故意爲之——若是結果指向同一個文檔(或者說_source的值相同),那麼結果合併(保留其中一個)——因此Completion Suggester並非爲了自動補完的場景設計的,它的做用主要仍是查找文檔,文檔找到就好,無論你的suggestions裏是否還有其它與輸入匹配的input。這時聰明的同窗可能會說要不不返回_source試試看,很遺憾,官方說_source meta-field must be enabled,並且並無給你設置的地方。以前有版本mapping時有個配置項是payloads,設置成false貌似能夠返回全部匹配的input,還有output什麼的,總之仍是有辦法改變默認行爲的,然而筆者試的這個版本把這些都去掉了,不知之後是否會有改變。。。

Completion only retrieves one result when multiple documents share same output

這麼看來,Suggester更像自定義標籤(依據標籤搜索文檔,Completion Suggester只是可讓咱們只輸入標籤的一部分而已)。因此說自動補全的功能仍是得另外實現咯?要麼之後有精力看下ES的源碼看怎麼修改吧。。

在Completion Suggester基礎上,ES另外提供了Context Suggester,有兩種context:category 和 geo,在查詢時帶上context便可取得與之相關的結果。意即在標籤基礎上再加一層過濾。

相關性:與之對應的重要概念就是評分,主要用在全文檢索時。Elasticsearch 的類似度算法 被定義爲檢索詞頻率/反向文檔頻率, TF/IDF。默認狀況下,返回結果是按相關性倒序排列的。

緩存:當進行精確值查找時, 咱們會使用過濾器(filters)。過濾器很重要,由於它們執行速度很是快 —— 不會計算相關度(直接跳過了整個評分階段)並且很容易被緩存。通常來講,在精確查找時,相關度是能夠忽略的,排序的話咱們更多的是根據某個字段自定義排序,因此爲了性能考慮,咱們應該儘量地使用過濾器。

數組:ES並無顯式定義數組的概念,你能夠在一個string類型的字段賦值爲"abc",也能夠賦值爲["abc","ddd"],ES會自動處理好。這在一些場景下頗有用,好比產品屬於某個葉子類目,它的類目Id設爲該葉子類目的Id,這樣用戶能搜索到該類目下的全部產品,但這樣會有問題:當用戶搜索父類目時將得不到任何產品。顯然這是不合理的,因此咱們能夠將產品的類目Id賦值包含從根類目到葉子類目的類目Id數組,用戶搜索其中任何類目都能獲得該產品。 官方文檔


Quartz.Net

在給內容建索引時能夠實時創建,也能夠異步[批量]建立,後者的話咱們經常使用計劃任務的方式,涉及到的工具比較常見的是Quartz.Net。

如下對Quartz.Net的描述基於2.5版本。

Quartz.Net支持多個trigger觸發同一個job,但不支持一個trigger觸發多個job,不明其意。

Quartz.Net的job和trigger聲明方式有多種,能夠經過代碼

IJobDetail job = JobBuilder.Create<IndexCreationJob>().Build();
ITrigger trigger = TriggerBuilder.Create().StartNow().WithSimpleSchedule(x => x.WithIntervalInSeconds(600).RepeatForever()).Build(); _scheduler.ScheduleJob(job, trigger);

或者經過xml文件。如果經過xml文件,則要指定是哪一個xml文件,也能夠設置xml文件的watch interval,還能夠設置線程數量等等(大部分都有默認值,可選擇設置),一樣能夠經過代碼

XMLSchedulingDataProcessor processor = new XMLSchedulingDataProcessor(new SimpleTypeLoadHelper()); ISchedulerFactory factory = new StdSchedulerFactory(); IScheduler sched = factory.GetScheduler(); processor.ProcessFileAndScheduleJobs(IOHelper.GetMapPath("/quartz_jobs.xml"), sched);

以上代碼即表示讀取根目錄下的quartz.jobs.xml獲取job和trigger的聲明。還有另外一種代碼方式:

var properties = new NameValueCollection(); properties["quartz.plugin.jobInitializer.type"] = "Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin"; properties["quartz.plugin.jobInitializer.fileNames"] = "~/quartz_jobs.xml"; properties["quartz.plugin.jobInitializer.failOnFileNotFound"] = "true"; properties["quartz.plugin.jobInitializer.scanInterval"] = "600"; ISchedulerFactory sf = new StdSchedulerFactory(properties); _scheduler = sf.GetScheduler();

以上600表示makes it watch for changes every ten minutes (600 seconds)

固然咱們能夠經過配置文件(同聲明job和trigger的xml文件,二者目的不一樣),如:

  <configSections> <section name="quartz" type="System.Configuration.NameValueSectionHandler"/> </configSections> <quartz> <add key="quartz.scheduler.instanceName" value="ExampleDefaultQuartzScheduler"/> <add key="quartz.threadPool.type" value="Quartz.Simpl.SimpleThreadPool, Quartz"/> <add key="quartz.threadPool.threadCount" value="10"/> <add key="quartz.threadPool.threadPriority" value="2"/> <add key="quartz.jobStore.misfireThreshold" value="60000"/> <add key="quartz.jobStore.type" value="Quartz.Simpl.RAMJobStore, Quartz"/> <!--*********************Plugin配置**********************--> <add key="quartz.plugin.xml.type" value="Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz" /> <add key="quartz.plugin.xml.fileNames" value="~/quartz_jobs.xml"/> </quartz>

或者單獨一個文件quartz.config:

# You can configure your scheduler in either <quartz> configuration section
# or in quartz properties file
# Configuration section has precedence

quartz.scheduler.instanceName = QuartzTest

# configure thread pool info
quartz.threadPool.type = Quartz.Simpl.SimpleThreadPool, Quartz
quartz.threadPool.threadCount = 10
quartz.threadPool.threadPriority = Normal

# job initialization plugin handles our xml reading, without it defaults are used
quartz.plugin.xml.type = Quartz.Plugin.Xml.XMLSchedulingDataProcessorPlugin, Quartz
quartz.plugin.xml.fileNames = ~/quartz_jobs.xml

# export this server to remoting context
#quartz.scheduler.exporter.type = Quartz.Simpl.RemotingSchedulerExporter, Quartz
#quartz.scheduler.exporter.port = 555
#quartz.scheduler.exporter.bindName = QuartzScheduler
#quartz.scheduler.exporter.channelType = tcp
#quartz.scheduler.exporter.channelName = httpQuartz

不須要特地指定是放在配置節中,仍是quartz.config中,或者二者皆有,Quartz.Net會自動加載配置項。代碼和配置方式也能夠混着使用,總之給人的選擇多而雜,加之官方文檔並不完善,初次接觸容易讓人困惑。

 

參考資料:

Elasticsearch: 權威指南

HBuilder處理git衝突,同 10_Eclipse中演示Git衝突的解決

PostgreSQL的全文檢索插件zhparser的中文分詞效果

SCWS 中文分詞

聊一聊雙十一背後的技術 - 分詞和搜索

詳細講解PostgreSQL中的全文搜索的用法

Lucene 3.0 原理與代碼分析

 

 

轉載請註明出處:http://www.cnblogs.com/newton/p/6873508.html

相關文章
相關標籤/搜索