開發Web應用時,你經常要加上搜尋功能。甚至還不知道要搜什麼,就在草圖上畫了一個放大鏡。
說到目前計算機的文字搜尋在應用上的實作,象形文字天生就比拼音字母劣勢的多,分詞、詞性判斷、拼音文字轉換啥的,容易讓人香菇。
首先我們來了解下什麼是Inverted index,翻譯過來的名字有很多,比如反轉索引、反向索引什麼的,讓人不明是以,可以了解為:一個未經處理的資料庫中,一般是以文檔ID作為索引,以文檔内容作為記錄。而Inverted index 指的是将單詞或記錄作為索引,将文檔ID作為記錄,這樣便可以友善地通過單詞或記錄查找到其所在的文檔。并不是什麼高深概念。
oracle裡常用的位圖索引(Bitmap index)也可認為是Inverted index。位圖索引對于相異基數低的資料最為合适,即記錄多,但取值較少。比如一個100W行的表有一個字段會頻繁地被當做查詢條件,我們會想到在這一列上面建立一個索引,但是這一列隻可能取3個值。那麼如果建立一個B*樹索引(普通索引)是不合适的,因為無論查找哪一個值,都可能會查出很多資料,這時就可以考慮使用位圖索引。位圖索引相對于傳統的B*樹索引,在葉子節點上采用了完全不同的結構組織方式。傳統B*樹索引将每一行記錄儲存為一個葉子節點,上面記錄對應的索引列取值和行rowid資訊。而位圖索引将每個可能的索引取值組織為一個葉子節點。每個位圖索引的葉子節點上,記錄着該索引鍵值的起始截止rowid和一個位圖向量串。如果不考慮起止rowid,那麼就是取值有幾個,就有幾個索引,比如上例,雖說有100W條記錄,但是針對隻有3個可取值的字段來說,索引節點隻有3個,類似于下圖:

需要注意的是,由于所有索引字段同值行共享一個索引節點,位圖索引不适用于頻繁增删改的字段,否則可能會導緻針對該字段(其它行)的增删改阻塞(對其它非索引字段的操作無影響),是一種索引段級鎖。具體請參看 深入解析B-Tree索引與Bitmap位圖索引的鎖代價。
下面說說筆者知道的一些全文搜尋的工具。
文中綠色文字表示筆者并不确定描述是否正确,紅色表示筆者疑問,若有知道的同學請不吝賜教,多謝!
- ICTCLAS分詞系統
- Postgresql的中文分詞
- Elasticsearch
- Quartz.net:用于定時任務,和全文檢索無關,我們可以用它來進行定時索引管理,比如說過期店鋪的産品索引删除
本來想借着ICTCLAS簡單介紹下中文分詞的一些原理和算法,不過網上已有比較好的文章了,可參看 ICTCLAS分詞系統研究。中文分詞基本上是基于詞典,[可能]涉及到的知識 —— HMM(隐馬爾科夫鍊)、動态規劃、TF-IDF、凸優化,更基礎的就是資訊論、機率論、矩陣等等,我們在讀書的時候可能并不知道所學何用,想較快重溫的同學可閱讀吳軍博士的《數學之美》。這些概念我會擇要在後續博文中介紹。下面我們就來看看分詞系統在資料庫中的具體應用。
在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查詢場景,而且圍繞分詞系統的查詢方案和邏輯大部分需要自己處理,涉及到稍複雜的應用就力不從心,或者效率低下了(比如上述的自動補完功能),另外分布部署的時候也要在上層另做叢集架構。
基于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);
第6、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