使用Lucene的MoreLikeThisQuery实现相关内容推荐
在分析MoreLikeThisQuery之前,首先介绍一下MoreLikeThis。
在实现搜索应用的时候,时常会遇到"更多相似文章","更多相关问题"之类的需求,也即根据当前文档的文本内容,在索引库中查询相类似的文章。
我们可以使用MoreLikeThis实现此功能:
IndexReader reader = IndexReader.open(……); IndexSearcher searcher = new IndexSearcher(reader); MoreLikeThis mlt = new MoreLikeThis(reader); Reader target = ... //此是一个io reader,指向当前文档的文本内容。 Query query = mlt.like( target); //根据当前的文本内容,生成查询对象。 Hits hits = searcher.search(query); //查询得到相似文档的结果。 |
MoreLikeThis的Query like(Reader r)函数如下:
public Query like(Reader r) throws IOException { return createQuery(retrieveTerms(r)); //其首先从当前文档的文本内容中抽取term,然后利用这些term构建一个查询对象。 } |
public PriorityQueue <Object[]> retrieveTerms(Reader r) throws IOException { Map<String,Int> words = new HashMap<String,Int>(); //根据不同的域中抽取term,到底根据哪些域抽取,可用函数void setFieldNames(String[] fieldNames)设定。 for (int i = 0; i < fieldNames.length; i++) { String fieldName = fieldNames[i]; addTermFrequencies(r, words, fieldName); } //将抽取的term放入优先级队列中 return createQueue(words); } |
private void addTermFrequencies(Reader r, Map<String,Int> termFreqMap, String fieldName) throws IOException { //首先对当前的文本进行分词,分词器可以由void setAnalyzer(Analyzer analyzer)设定。 TokenStream ts = analyzer.tokenStream(fieldName, r); int tokenCount=0; TermAttribute termAtt = ts.addAttribute(TermAttribute.class); //遍历分好的每一个词 while (ts.incrementToken()) { String word = termAtt.term(); tokenCount++; //如果分词后的term的数量超过某个设定的值,则停止,可由void setMaxNumTokensParsed(int i)设定。 if(tokenCount>maxNumTokensParsed) { break; } //如果此词小于最小长度,或者大于最大长度,或者属于停词,则属于干扰词。 //最小长度由void setMinWordLen(int minWordLen)设定。 //最大长度由void setMaxWordLen(int maxWordLen)设定。 //停词表由void setStopWords(Set<?> stopWords)设定。 if(isNoiseWord(word)){ continue; } // 统计词频tf Int cnt = termFreqMap.get(word); if (cnt == null) { termFreqMap.put(word, new Int()); } else { cnt.x++; } } } |
private PriorityQueue createQueue(Map<String,Int> words) throws IOException { //根据统计的term及词频构造优先级队列。 int numDocs = ir.numDocs(); FreqQ res = new FreqQ(words.size()); // 优先级队列,将按tf*idf排序 Iterator<String> it = words.keySet().iterator(); //遍历每一个词 while (it.hasNext()) { String word = it.next(); int tf = words.get(word).x; //如果词频小于最小词频,则忽略此词,最小词频可由void setMinTermFreq(int minTermFreq)设定。 if (minTermFreq > 0 && tf < minTermFreq) { continue; } //遍历所有域,得到包含当前词,并且拥有最大的doc 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; } //如果文档频率小于最小文档频率,则忽略此词。最小文档频率可由void setMinDocFreq(int minDocFreq)设定。 if (minDocFreq > 0 && docFreq < minDocFreq) { continue; } //如果文档频率大于最大文档频率,则忽略此词。最大文档频率可由void setMaxDocFreq(int maxFreq)设定。 if (docFreq > maxDocFreq) { continue; } if (docFreq == 0) { continue; } //计算打分tf*idf float idf = similarity.idf(docFreq, numDocs); float score = tf * idf; //将object的数组放入优先级队列,只有前三项有用,按照第三项score排序。 res.insertWithOverflow(new Object[]{word, // 词 topField, // 域 Float.valueOf(score), // 打分 Float.valueOf(idf), // idf Integer.valueOf(docFreq), // 文档频率 Integer.valueOf(tf) //词频 }); } return res; } |
private Query createQuery(PriorityQueue 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 bestScore = ((Float) ar[2]).floatValue(); } float myScore = ((Float) ar[2]).floatValue(); //其他的词的打分除以最高打分,乘以boostFactor,得到相应的词所生成的查询的boost,从而在当前文本文档中打分越高的词在查询语句中也有更高的boost,起重要的作用。 tq.setBoost(boostFactor * myScore / bestScore); } try { query.add(tq, BooleanClause.Occur.SHOULD); } catch (BooleanQuery.TooManyClauses ignore) { break; } qterms++; //如果超过了设定的最大的查询词的数目,则停止,最大查询词的数目可由void setMaxQueryTerms(int maxQueryTerms)设定。 if (maxQueryTerms > 0 && qterms >= maxQueryTerms) { break; } } return query; } |
MoreLikeThisQuery只是MoreLikeThis的封装,其包含了MoreLikeThis所需要的参数,并在rewrite的时候,由MoreLikeThis.like生成查询对象。
- String likeText;当前文档的文本
- String[] moreLikeFields;根据哪个域来抽取查询词
- Analyzer analyzer;分词器
- float percentTermsToMatch=0.3f;最后生成的BooleanQuery之间都是SHOULD的关系,其中至少有多少比例必须得到满足
- int minTermFrequency=1;最少的词频
- int maxQueryTerms=5;最多的查询词数目
- Set<?> stopWords=null;停词表
- int minDocFreq=-1;最小的文档频率
public Query rewrite(IndexReader reader) throws IOException { MoreLikeThis mlt=new MoreLikeThis(reader); mlt.setFieldNames(moreLikeFields); mlt.setAnalyzer(analyzer); mlt.setMinTermFreq(minTermFrequency); if(minDocFreq>=0) { mlt.setMinDocFreq(minDocFreq); } mlt.setMaxQueryTerms(maxQueryTerms); mlt.setStopWords(stopWords); BooleanQuery bq= (BooleanQuery) mlt.like(new ByteArrayInputStream(likeText.getBytes())); BooleanClause[] clauses = bq.getClauses(); bq.setMinimumNumberShouldMatch((int)(clauses.length*percentTermsToMatch)); return bq; } |
举例,对于http://topic.csdn.net/u/20100501/09/64e41f24-e69a-40e3-9058-17487e4f311b.html?1469中的帖子
我们姑且将相关问题中的帖子以及其他共20篇文档索引。
File indexDir = new File("TestMoreLikeThisQuery/index"); IndexReader reader = IndexReader.open(indexDir); IndexSearcher searcher = new IndexSearcher(reader); //将《IT外企那点儿事》作为likeText,从文件读入。 StringBuffer contentBuffer = new StringBuffer(); BufferedReader input = new BufferedReader(new InputStreamReader(new FileInputStream("TestMoreLikeThisQuery/IT外企那点儿事.txt"), "utf-8")); String line = null; while((line = input.readLine()) != null){ contentBuffer.append(line); } String content = contentBuffer.toString(); //分词用中科院分词 MoreLikeThisQuery query = new MoreLikeThisQuery(content, new String[]{"contents"}, new MyAnalyzer(new ChineseAnalyzer())); //将80%都包括的词作为停词,在实际应用中,可以有其他的停词策略。 query.setStopWords(getStopWords(reader)); //至少包含5个的词才认为是重要的 query.setMinTermFrequency(5); //只取其中之一 query.setMaxQueryTerms(1); TopDocs docs = searcher.search(query, 50); for (ScoreDoc doc : docs.scoreDocs) { Document ldoc = reader.document(doc.doc); String title = ldoc.get("title"); System.out.println(title); } |
static Set<String> getStopWords(IndexReader reader) throws IOException{ HashSet<String> stop = new HashSet<String>(); int numOfDocs = reader.numDocs(); int stopThreshhold = (int) (numOfDocs*0.7f); TermEnum te = reader.terms(); while(te.next()){ String text = te.term().text(); if(te.docFreq() >= stopThreshhold){ stop.add(text); } } return stop; } |
结果为: 揭开外企的底儿(连载六)——外企招聘也有潜规则.txt 去央企还是外企,帮忙分析下.txt 哪种英语教材比较适合英语基础差的人.txt 有在达内外企软件工程师就业班培训过的吗.txt 两个月的“骑驴找马”,面试无数家公司的深圳体验.txt 一个看了可能改变你一生的小说《做单》,外企销售经理做单技巧大揭密.txt HR的至高机密:20个公司绝对不会告诉你的潜规则.txt |