1. 程式人生 > >某電商平臺專案開發記要——全文檢索

某電商平臺專案開發記要——全文檢索

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

Postgresql的中文分詞

在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);

第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)。過濾器很重要,因為它們執行速度非常快 —— 不會計算相關度(直接跳過了整個評分階段)而且很容易被快取。一般來說,在精確查詢時,相關度是可以忽略的,排序的話我們更多的是根據某個欄位自定義排序,所以為了效能考慮,我們應該儘可能地使用過濾器。

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

相關推薦

平臺專案開發記要——全文檢索

開發Web應用時,你經常要加上搜索功能。甚至還不知道要搜什麼,就在草圖上畫了一個放大鏡。 說到目前計算機的文字搜尋在應用上的實現,象形文字天生就比拼音字母劣勢的多,分詞、詞性判斷、拼音文字轉換啥的,容易讓人香菇。 首先我們來了解下什麼是Inverted index,翻譯過來

真實資料對接 從0開發前後端分離的企業級上線專案(再談前後端分離式 手把手從0打造平臺-前端開發

第1章 課程介紹(2018配套教程:電商前端+電商後端+電商許可權管理系統課程) 本章中會先讓大家瞭解課程整體情況,然後手把手帶大家做一些開發前的準備工作。 1-1 課程導學 1-2 電商平臺需求分析 1-3 架構設計及技術選型 1-4 前後端配合方式及資料介面

平臺專案之——Ajax請求,服務端處理完不跳到success

1、問題描述:       最近在修改電商平臺的釋出商品頁面,釋出商品時,前端與後臺互動採用Ajax  Post請求,就這麼一個簡單的畫面,我遇到一個非常奇怪且困擾我很久的問題:       (1)商品釋出失敗(有時候能釋

【SSM專案平臺專案第1天

課程目標 目標1:瞭解電商行業特點以及理解電商的模式 目標2:瞭解整體品優購的架構特點 目標3:能夠運用Dubbox+SSM搭建分散式應用 目標4:搭建工程框架,完成品牌列表後端程式碼 1.走進電商 1.1電商行業分析 近年來,中國的電子商務快速發展,交易額連創

關於Vue平臺專案的總結

該專案主要是仿照電商平臺,使用vue-router處理路由,主要實現資料的展示,其中在專案中,自己封裝瞭如下拉框,單選框等可複用元件。 第一: App.vue: 實現首頁的header和footer,content部分採用vue-router方式進行切換

CK2040-Spring高效開發帶前後端開發完整平臺

detail 整合 gmv 定時任務 每次 ack 基類 全部 交流 CK2040-Spring高效開發帶前後端開發完整電商平臺 隨筆背景:在很多時候,很多入門不久的朋友都會問我:我是從其他語言轉到程序開發的,有沒有一些基礎性的資料給我們學習學習呢,你的框架感覺一下太大了,

跨境平臺開發

安全 在線 會員 國際 客戶 bubuko 版本 領域 inf 跨境電商全稱跨境電子商務,是指分屬不同關境的交易主體,通過電子商務平臺達成交易、進行支付結算,並通過跨境物流配送商品、完成交易過程的一種國際商業活動。跨境電商平臺結合跨境物流,國際速遞,進口物流,金融鏈條,跨境

以太坊開發DApp實戰教程——用區塊鏈、星際文件系統(IPFS)、Node.js和MongoDB來構建平臺

IPFS 區塊鏈電商 區塊鏈開發 以太坊開發 以太坊dapp 以太坊教程 智能合約 以太坊 星際文件系統 區塊鏈 第一節 簡介 歡迎和我們一起來用以太坊開發構建一個去中心化電商DApp!我們將用區塊鏈、星際文件系統(IPFS)、Node.js和Mong

前端開發視訊 再談前後端分離式 手把手從0打造平臺

「 女程式媛崛起 」 今天這篇文,意義特殊,是我的一個迷妹程式媛-祈澈姑娘寫的,她發給我後,我看了通篇,感覺寫的很真實,而且又是記錄女程式媛的日常,比較少見,所以我很有興趣,相信大家也很有興趣。 熟悉我的人都知道,我很少轉別人文字,基本都是堅持原創,我更喜歡真實的人和事,

以太坊開發DApp實戰教程——用區塊鏈、星際檔案系統(IPFS)、Node.js和MongoDB來構建平臺

第一節 簡介 歡迎和我們一起來用以太坊實戰開發構建一個去中心化電商DApp!我們將會構建一個類似淘寶的線上電子商務應用,我將使用區塊鏈、星際檔案系統(IPFS)、Node.js和MongoDB來構建電商平臺,賣家可以自由地出售商品,買家可以自由地購物: 去中心化:

平臺搭建--商品管理功能模組開發(一)

Hi,大家好,我們又見面了。相信通過前面幾篇博文的學習,大家已經對如何搭建一款屬於自己的電商平臺有了初步的瞭解,也大致懂了SSM框架的主要開發流程,那麼在接下來的幾篇博文中,我將帶領大家完成商品管理功能模組的開發,還在等什麼,直接進入正題吧!一、商品管理功能模組-概要   

教育平臺專案開發之--使用SSM框架開發過程遇到的問題總結

本次開發一個系統,前端是Android端,互動方式用json。 一、關於@RequestBody和@JsonIgnoreProperties(ignoreUnknown=true)的問題 前端用json資料傳輸。json資料格式如下: { "mobil

Thinkphp 5.0 仿百度糯米開發多商家平臺

百度地圖 表設計 完美 技術點 下載 直接 商品詳情 ont 前臺 第1章 課程簡介 本章內容會給大家通覽本門課程的所有知識點第2章 需求分析本章會先帶領大家預覽下整個系統包括商家、主平臺、前臺等,對數據表結構、數據表結構的對應關系進行講解 最後會講解每個模塊的功能分

平臺搭建--購物車功能模組開發(二)

Hi,大家好。在上一篇博文中,我們完成了搭建一個高複用的購物車時需要準備的搭建環境,封裝了一個高複用的購物車計算方法,定義了兩個與購物車商品有關的Value-Object值物件,那麼接下來就進入核心功能的開發。一、購物車模組-獲取購物車商品列表功能的實現      獲取購車商

指尖上的---(4).net開發solr

1.4 port pro endpoint lean abi char contract java 這一節我們看下如何把查詢數據放到server端存儲,這裏我們須要使用client工具來操作與服務端數據打交道,網上有好多基於.NET開發的SOLRc

網紅直播平臺有什麽特點

系統開發 網紅直播,大家並不陌生。網紅們在各個直播平臺上利用自己的優勢吸引著粉絲,實現視頻流量變現。如今的電商平臺的商品展現形式單一,僅僅只是圖片和文字的信息已經不能完全滿足消費者的需求,而網絡直播平臺的出現,拯救了電商平臺,解決了電商平臺的痛點。 小編來介紹一下網紅直播電

分布式架構設計之平臺

用戶服 base 介紹 val 重要 本地 交互 pac 一定的 分布式架構設計之電商平臺 何為軟件架構?不同人的答案會有所不同,而我認為一個好的軟件架構除了要具備業務功能外,還應該具備一定的高性能、高可用、高伸縮性及可拓展等非功能需求。而軟件架構是由業務架構和技術架構

劉德:小米已投89家生態鏈企業 有品要做百億平臺(本質上是是利用了小米的大火爐的余熱,但也有反向的正面作用)

效率 個人 最好 深入 網上 聯合 初創公司 方法 也有 小米科技聯合創始人、副總裁,小米生態鏈負責人劉德(微博)文/騰訊科技 王潘小米對生態鏈企業的投資正在接近雷軍(微博)當初預期的100家目標,截至6月30日,小米已經投資了89家。通過三年左右的布局,小米投資的多家生態

暴改無人機,探秘活躍在平臺中的地下黑工坊

無人機人類一直癡迷於速度。不論是汽車、火車還是飛機都在提速,仿佛速度才能顯示技術的強大。美國空軍計劃開發超音速噴氣式飛機,每小時超過6100公裏,但不載人。原因是人類無法承受這樣的高速運動,其實在1969年,阿波羅10號的三名宇航員乘坐的飛船從月球後方繞過時,他們相對地球的運動速度高達每小時39897公裏。假

項目PostgreSQL數據庫備份恢復方案

postgresql、備份恢復某電商項目PostgreSQL數據庫備份恢復方案:下載地址:某電商項目PostgreSQL數據庫備份恢復方案本文出自 “雲計算與大數據” 博客,請務必保留此出處http://linuxzkq.blog.51cto.com/9379412/1967693某電商項目PostgreSQ