搜索關鍵字智能提示是一個搜索應用的標配。主要做用是避免用戶輸入錯誤的搜索詞,並將用戶引導到相應的關鍵詞上,以提高用戶搜索體驗。css
美團CRM系統中存在數以百萬計的商家,爲了讓用戶高速查找到目標商家,咱們基於solrcloud實現了商家搜索模塊。用戶在查找商家時主要輸入商戶名、商戶地址進行搜索,爲了提高用戶的搜索體驗和輸入效率。本文實現了一種基於solr前綴匹配查詢關鍵字智能提示(Suggestion)實現。html
public static List getPermutationSentence(List> termArrays,int start) {
if (CollectionUtils.isEmpty(termArrays))
return Collections.emptyList();
int size = termArrays.size();
if (start < 0 || start >= size) {
return Collections.emptyList();
}
if (start == size-1) {
return termArrays.get(start);
}
List<String> strings = termArrays.get(start);
List<String> permutationSentences = getPermutationSentence(termArrays, start + 1);
if (CollectionUtils.isEmpty(strings)) {
return permutationSentences;
}
if (CollectionUtils.isEmpty(permutationSentences)) {
return strings;
}
List<String> result = new ArrayList<String>();
for (String pre : strings) {
for (String suffix : permutationSentences) {
result.add(pre+suffix);
}
}
return result;
}
方案一 Trie樹 + TopK算法
Trie樹即字典樹,又稱單詞查找樹或鍵樹,是一種樹形結構。是一種哈希樹的變種。java
典型應用是用於統計和排序大量的字符串(但不只限於字符串)。因此經常被搜索引擎系統用於文本詞頻統計。它的長處是:最大限度地下降無謂的字符串比較,查詢效率比哈希表高。Trie是一顆存儲多個字符串的樹。算法
相鄰節點間的邊表明一個字符,這樣樹的每條分支表明一則子串,而樹的葉節點則表明完整的字符串。和普通樹不一樣的地方是。一樣的字符串前綴共享同一條分支。apache
好比。給出一組單詞inn, int, at, age, adv, ant, 咱們可以獲得如下的Trie:markdown
從上圖可知,當用戶輸入前綴i的時候,搜索框可能會展現以i爲前綴的「in」。「inn」。」int」等關鍵詞,再當用戶輸入前綴a的時候,搜索框裏面可能會提示以a爲前綴的「ate」等關鍵詞。如此。實現搜索引擎智能提示suggestion的第一個步驟便清晰了,即用trie樹存儲大量字符串,當前綴固定時。存儲相對來講比較熱的後綴。數據結構
TopK算法用於解決統計熱詞的問題。解決TopK問題主要有兩種策略:hashMap統計+排序、堆排序
hashmap統計: 先對這批海量數據預處理。詳細方法是:維護一個Key爲Query字串。Value爲該Query出現次數的HashTable,即hash_map(Query,Value),每次讀取一個Query。假設該字串不在Table中。那麼增長該字串,並且將Value值設爲1。假設該字串在Table中,那麼將該字串的計數加一就能夠,終於在O(N)的時間複雜度內用Hash表完畢了統計。
堆排序:藉助堆這個數據結構。找出Top K,時間複雜度爲N‘logK。app
即藉助堆結構,咱們可以在log量級的時間內查找和調整/移動。所以,維護一個K(該題目中是10)大小的小根堆,而後遍歷300萬的Query,分別和根元素進行對照。post
因此,咱們終於的時間複雜度是:O(N) + N’ * O(logK),(N爲1000萬,N’爲300萬)。ui
該方案存在的問題是:
方案二 Solr自帶Suggest智能提示
Solr做爲一個應用普遍的搜索引擎系統,它內置了智能提示功能。叫作Suggest模塊。
該模塊可選擇基於提示詞文本作智能提示。還支持經過針對索引的某個字段創建索引詞庫作智能提示。 (詳見solr的wiki頁面http://wiki.apache.org/solr/Suggester)
該方案存在的問題是:
返回的結果是基於索引中字段的詞頻進行排序,不是用戶搜索關鍵字的頻率,所以不能將一些熱門關鍵字排在前面。
拼音提示。多音字。縮寫仍是要另外加索引字段。
方案三 Solrcloud創建單獨的collection,利用solr前綴查詢實現
如前所述,以上兩個方案在實施起來都存在一些問題,Trie樹+TopK算法,在處理漢字suggest時不是很是優雅,且需要維護兩棵Trie樹,實施起來比較複雜。Solr自帶的suggest智能提示組件存在問題是使用freq排序算法。返回的結果全然基於索引中字符的出現次數。沒有兼顧用戶搜索詞語的頻率,所以沒法將一些熱門詞排在更靠前的位置。因而。咱們繼續尋找一種解決問題更加優雅的方案。
至此。咱們考慮專門爲關鍵字創建一個索引collection,利用solr前綴查詢實現。solr中的copyField能很是好解決咱們同一時候索引多個字段(漢字、pinyin, abbre)的需求,且field的multiValued屬性設置爲true時能解決同一個關鍵字的多音字組合問題。配置例如如下:
schema.xml:
<field name="kw" type="string" indexed="true" stored="true" />
<field name="pinyin" type="string" indexed="true" stored="false" multiValued="true"/>
<field name="abbre" type="string" indexed="true" stored="false" multiValued="true"/>
<field name="kwfreq" type="int" indexed="true" stored="true" />
<field name="_version_" type="long" indexed="true" stored="true"/>
<field name="suggest" type="suggest_text" indexed="true" stored="false" multiValued="true" />
------------------multiValued表示字段是多值的-------------------------------------
<uniqueKey>kw</uniqueKey>
<defaultSearchField>suggest</defaultSearchField>
說明:
kw爲原始關鍵字
pinyin和abbre的multiValued=true,在使用solrj建此索引時,定義成集合類型就能夠:如關鍵字「重慶」的pinyin字段爲{chongqing,zhongqing}, abbre字段爲{cq, zq}
kwfreq爲用戶搜索關鍵的頻率。用於查詢的時候排序
-------------------------------------------------------
<copyField source="kw" dest="suggest" />
<copyField source="pinyin" dest="suggest" />
<copyField source="abbre" dest="suggest" />
------------------suggest_text----------------------------------
<fieldType name="suggest_text" class="solr.TextField" positionIncrementGap="100" autoGeneratePhraseQueries="true">
<analyzer type="index">
<tokenizer class="solr.KeywordTokenizerFactory" />
<filter class="solr.SynonymFilterFactory"
synonyms="synonyms.txt"
ignoreCase="true"
expand="true" />
<filter class="solr.StopFilterFactory"
ignoreCase="true"
words="stopwords.txt"
enablePositionIncrements="true" />
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt" />
</analyzer>
<analyzer type="query">
<tokenizer class="solr.KeywordTokenizerFactory" />
<filter class="solr.StopFilterFactory"
ignoreCase="true"
words="stopwords.txt"
enablePositionIncrements="true" />
<filter class="solr.LowerCaseFilterFactory" />
<filter class="solr.KeywordMarkerFilterFactory" protected="protwords.txt" />
</analyzer>
</fieldType>
KeywordTokenizerFactory:這個分詞器不進行不論什麼分詞!整個字符流變爲單個詞元。String域類型也有相似的效果。但是它不能配置文本分析的其餘處理組件,比方大寫和小寫轉換。
不論什麼用於排序和大部分Faceting功能的索引域,這個索引域僅僅有能一個原始域值中的一個詞元。
前綴查詢構造:
private SolrQuery getSuggestQuery(String prefix, Integer limit) {
SolrQuery solrQuery = new SolrQuery();
StringBuilder sb = new StringBuilder();
sb.append(「suggest:").append(prefix).append("*"); solrQuery.setQuery(sb.toString()); solrQuery.addField("kw"); solrQuery.addField("kwfreq"); solrQuery.addSort("kwfreq", SolrQuery.ORDER.desc); solrQuery.setStart(0); solrQuery.setRows(limit); return solrQuery; }
效果例如如下圖所看到的: