在solr中有兩種方式實現MoreLikeThis:MoreLikeThisHandler和在SearchHandler中的MoreLikeThisComponent。
兩種方式大同小異:
一是:將MoreLikeThis作為一個單獨的Handler來處理,體現主體地位。
二是:將MoreLikeThis作為一個組件放到SearchHandler中,為Search加入了MLT的功能,是一種輔助功能。
這里我們借助方法一,來簡單闡述MLT的實現步驟。
步驟1:
MLT是根據一篇文檔(document)的相關字段進行“相似匹配”,例如:
這里我們提供的檢索式為:q=id:82790,因此其只有唯一一個檢索結果。
MLT第一步工作就是根據我們提供的檢索式獲取文檔(document)。
步驟2:
MLT可以看成是一種特殊的檢索,只是他的檢索式是根據我們提供的一篇文檔(document)生成的。
因此關鍵是怎么生成這個檢索式!!!
MoreLikeThis.java
public Query like(int docNum) throws IOException {
if (fieldNames == null) {
// gather list of valid fields from lucene
Collection<String> fields = ir
.getFieldNames(IndexReader.FieldOption.INDEXED);
fieldNames = fields.toArray(new String[fields.size()]);
}
return createQuery(retrieveTerms(docNum));
}
在創建這個“神奇”的query之前,我們先要獲得相關的原始term(retrieveTerms)。
public PriorityQueue<Object[]> retrieveTerms(int docNum) throws IOException {
Map<String,Int> termFreqMap = new HashMap<String,Int>();
for (int i = 0; i < fieldNames.length; i++) {
String fieldName = fieldNames[i];
TermFreqVector vector = ir.getTermFreqVector(docNum, fieldName);
// field does not store term vector info
if (vector == null) {
Document d = ir.document(docNum);
String text[] = d.getValues(fieldName);
if (text != null) {
for (int j = 0; j < text.length; j++) {
addTermFrequencies(new StringReader(text[j]), termFreqMap,
fieldName);
}
}
} else {
addTermFrequencies(termFreqMap, vector);
}
}
return createQueue(termFreqMap);
}
首先獲取每一個字段的TermFreqVector,然后將其添加到TermFrequencies中,該過程是計算TF的過程,結果存放在map<String,Int>中,key為term,value為該term出現的次數(termFrequencies)。
在該過程中需要降噪,及去掉一些無關緊要的term,其判斷方式如下:
private boolean isNoiseWord(String term) {
int len = term.length();
if (minWordLen > 0 && len < minWordLen) {
return true;
}
if (maxWordLen > 0 && len > maxWordLen) {
return true;
}
if (stopWords != null && stopWords.contains(term)) {
return true;
}
return false;
}
主要兩個依據:
1.term長度必須在minWordLen和maxWordLen范圍內;
2.term不應出現在stopWords內。
我們再回到retrieveTerms方法中,他返回的是一個PriorityQueue<Object[]>,因此我們還要將之前創建的map<String,Int>(tf)進行一定的處理(重要)。
“Find words for a more-like-this query former.”
“Create a PriorityQueue from a word->tf map.”
private PriorityQueue<Object[]> createQueue(Map<String,Int> words)
throws IOException {
// have collected all words in doc and their freqs
int numDocs = ir.numDocs();
FreqQ res = new FreqQ(words.size()); // will order words by score
Iterator<String> it = words.keySet().iterator();
while (it.hasNext()) { // for every word
String word = it.next();
int tf = words.get(word).x; // term freq in the source doc
if (minTermFreq > 0 && tf < minTermFreq) {
continue; // filter out words that don't occur enough times in the
// source
}
// go through all the fields and find the largest document frequency
String topField = fieldNames[0];
int docFreq = 0;
for (int i = 0; i < fieldNames.length; i++) {
int freq = ir.docFreq(new Term(fieldNames[i], word));
topField = (freq > docFreq) ? fieldNames[i] : topField;
docFreq = (freq > docFreq) ? freq : docFreq;
}
if (minDocFreq > 0 && docFreq < minDocFreq) {
continue; // filter out words that don't occur in enough docs
}
if (docFreq > maxDocFreq) {
continue; // filter out words that occur in too many docs
}
if (docFreq == 0) {
continue; // index update problem?
}
float idf = similarity.idf(docFreq, numDocs);
float score = tf * idf;
// only really need 1st 3 entries, other ones are for troubleshooting
res.insertWithOverflow(new Object[] {word, // the word
topField, // the top field
Float.valueOf(score), // overall score
Float.valueOf(idf), // idf
Integer.valueOf(docFreq), // freq in all docs
Integer.valueOf(tf)});
}
return res;
}
該方法我們遍歷所有的term,並取出其tf以及在所有指定字段(例如:mlt.fl=ti,ab,mcn)中最大的df。根據df和當前索引文檔數計算idf,然后計算該term的score=tf*idf。
創建好PriorityQueue后,我們就可以將他轉變成之前提到的那個“神奇”的query了。
“Create the More like query from a PriorityQueue”
private Query createQuery(PriorityQueue<Object[]> q) {
BooleanQuery query = new BooleanQuery();
Object cur;
int qterms = 0;
float bestScore = 0;
while (((cur = q.pop()) != null)) {
Object[] ar = (Object[]) cur;
TermQuery tq = new TermQuery(new Term((String) ar[1], (String) ar[0]));
if (boost) {
if (qterms == 0) {
bestScore = ((Float) ar[2]).floatValue();
}
float myScore = ((Float) ar[2]).floatValue();
tq.setBoost(boostFactor * myScore / bestScore);
}
try {
query.add(tq, BooleanClause.Occur.SHOULD);
} catch (BooleanQuery.TooManyClauses ignore) {
break;
}
qterms++;
if (maxQueryTerms > 0 && qterms >= maxQueryTerms) {
break;
}
}
return query;
}
構建一個BooleanQuery,按照score從大到小取出一定數量的term(maxQueryTerm)進行組建:
query.add(tq, BooleanClause.Occur.SHOULD);
這里簡單理解就是——取出文檔中(相關字段)最重要(tf*idf)的前N個term,組建一個BooleanQuery(Should關聯)。
步驟3:
用第二步創建的query進行一次檢索,取出得分最高的N篇文檔即可。
原理分析:
(1)在MLT中主要是tf、idf,根據score(tf*idf)獲取對分類最重要的term,並構建目標Query。
MLT可以理解為:找出給定文檔同一類的其他文檔。
在一份給定的文件里,詞頻(term frequency,TF)指的是某一個給定的詞語在該文件中出現的頻率。這個數字是對詞數(term count)的歸一化,以防止它偏向長的文件。(同一個詞語在長文件里可能會比短文件有更高的詞數,而不管該詞語重要與否。)對於在某一特定文件里的詞語 ti 來說,它的重要性可表示為:
以上式子中 ni,j 是該詞在文件dj中的出現次數,而分母則是在文件dj中所有字詞的出現次數之和。
逆向文件頻率(inverse document frequency,IDF)是一個詞語普遍重要性的度量。某一特定詞語的IDF,可以由總文件數目除以包含該詞語之文件的數目,再將得到的商取對數得到:
其中
- |D|:語料庫中的文件總數
:包含詞語ti的文件數目(即
的文件數目)如果該詞語不在語料庫中,就會導致被除數為零,因此一般情況下使用
然后
某一特定文件內的高詞語頻率,以及該詞語在整個文件集合中的低文件頻率,可以產生出高權重的TF-IDF。因此,TF-IDF傾向於過濾掉常見的詞語,保留重要的詞語。
(2)根據提供的Query,利用lucene的打分算法,找到相似文檔。
Lucene 將信息檢索中的Boolean model (BM)和Vector Space Model (VSM)聯合起來,實現了自己的評分機制。
具體內容參見:
那么有哪些環節可以提高相似檢索精度呢?
1.降噪環節需要強化,目前solr中是基於term長度和停用此表聯合過濾。
例如將term的最小長度限定成2,即單個字不能作為計算的term,例如:
ab:擴印 ab:膠卷 ab:印機 ab:彩色 ab:傳動軸 ab:兩根 ab:墊板 ab:手輪 ab:齒輪 ab:從動 ab:傳動 ab:設置 ab:自動 ab:電動機 mcn:g03b27/46 ab:電動 ab:上片 ab:上手 ab:支撐 ab:精確度 ab:動機 ab:壓片 ab:以及 ab:機構 ab:下壓
2.提高分詞器的精度,並且對於行業性的業務最好提供行業性的詞庫,並且進行人工維護。
3.調整、改進相似度算法。
簡單的我們試試將term的數量(構建目標query的term數量)進行控制,設置成10。例如:
ab:擴印 ab:膠卷 ab:印機 ab:彩色 ab:傳動軸 ab:兩根 ab:墊板 ab:手輪 ab:齒輪 ab:從動
以上實例只是一個簡單說明,更多調整(挑戰)還需要在實踐中具體分析。
