3.3.11 使用Collector筛选搜索结果
Collector主要用来从一次查询中收集原始结果。可以通过它遍历查询结果中的每个文档。执行搜索的Searcher.search()方法可以接收Collector对象作为参数。查询类每次匹配到一个文档都会调用Collector对象上的collect(int doc)方法。下面是一个收集文档,但不对文档评分的例子:
private static final String FIELD = "contents"; public static void main(String[] args) throws Exception { // 建立使用内存索引的Lucene Directory directory = new RAMDirectory(); Analyzer analyzer = new StandardAnalyzer(); IndexWriterConfig iwc = new IndexWriterConfig(analyzer); IndexWriter writer = new IndexWriter(directory, iwc); // 索引一些文档 writer.addDocument(createDocument("1", "foo bar baz")); writer.addDocument(createDocument("2", "red green blue")); writer.addDocument(createDocument("3", "The Lucene was made by Doug Cutting")); writer.close(); IndexReader reader = DirectoryReader.open(directory); IndexSearcher searcher = new IndexSearcher(reader); Term t1 = new Term(FIELD, "lucene"); TermQuery query = new TermQuery(t1); final BitSet bits = new BitSet(reader.maxDoc()); searcher.search(query, new Collector() { private int docBase; //忽略评分器 @Override public void setScorer(Scorer scorer) { } //允许文档乱序 public boolean acceptsDocsOutOfOrder() { return true; } public void collect(int doc) { bits.set(doc + docBase); } public void setNextReader(AtomicReaderContext context) { this.docBase = context.docBase; } }); System.out.println("结果数:" + bits.cardinality()); for (int i = bits.nextSetBit(0); i >= 0; i = bits.nextSetBit(i + 1)) { System.out.println("文档编号:" + i); } } private static Document createDocument(String id, String content) { Document doc = new Document(); doc.add(new Field("id", id, StringField.TYPE_STORED)); doc.add(new Field(FIELD, content, TextField.TYPE_STORED)); return doc; }
例如,有个Collector的子类TopDocsCollector处理原始的查询结果,并且返回最相关的N个文档。
它是一个基类,用于做所有返回TopDocs输出的类的父类。TopDocsCollector.topDocs()方法返回最相关的N个文档。
例如:
Query q = ...; IndexSearcher searcher = ...; TopDocsCollector<ScoreDoc> tdc = new MyTopsDocCollector(numResults); searcher.search(q, tdc); //传递一个查询对象和一个Collector对象 //给Searcher.search方法
TopDocsCollector接收一个优先队列和一个返回结果的总数作为参数。TopScoreDocCollector是TopDocsCollector的子类,也是可以直接使用的类。使用TopScoreDocCollector返回最相关的10个文档的例子如下:
int hitsPerPage = 10; IndexSearcher searcher = new IndexSearcher(index, true); TopScoreDocCollector collector = TopScoreDocCollector.create(hitsPerPage, true); searcher.search(q, collector); ScoreDoc[] hits = collector.topDocs().scoreDocs;
一个会员可以发布多条信息,搜索的时候如何将会员发布的信息按每个会员一条的方式显示。也就是说每个会员编号最多只显示一条。每个会员最多只显示和查询词最相关的一条信息。这个功能是在结果集而不是在索引库上遍历有效文档,所以应该采用Collector而不是Filter。
可以按指定列排序,然后过滤信息。但是Searcher.search()方法不能同时接收Collector对象和Sort对象。只存在:
searcher.search(query, collector);
对搜索结果首先按会员列排序,然后按相关度排序。找出每个会员最多只显示和查询词最相关的一条信息。让Collector把这样的结果放入一个TopDocs对象。
可以扩展TopDocsCollector类。在collect()方法中构造一个新的ScoreDoc对象。
private static final class MyTopsDocCollector extends TopDocsCollector<ScoreDoc> { private int idx = 0; private int base = 0; public MyTopsDocCollector(int size) { super(new HitQueue(size, false)); } @Override protected TopDocs newTopDocs(ScoreDoc[] results, int start) { if (results == null) { return EMPTY_TOPDOCS; } float maxScore = Float.NaN; if (start == 0) { maxScore = results[0].score; } else { for (int i = pq.size(); i > 1; i--) { pq.pop(); } maxScore = pq.pop().score; } return new TopDocs(totalHits, results, maxScore); } @Override public void collect(int doc) throws IOException { ++totalHits; pq.insertWithOverflow(new ScoreDoc(doc + base, scores[idx++])); } @Override public void setNextReader(IndexReader reader, int docBase) throws IOException { base = docBase; } @Override public void setScorer(Scorer scorer) throws IOException { //不做任何事。随机分配评分值 } @Override public boolean acceptsDocsOutOfOrder() { return true; } }
Collector类的setScorer()方法是一个方法,当IndexSearcher实际执行搜索时,通过IndexSearcher传入score。
对每个匹配的文档,调用collect()方法,传递一个索引段内部的文档编号给collect()方法。
会员发布的信息要一轮一轮地显示,不是说折叠起来就显示一条。比如会员1有两条数据:信息1,信息2;会员2也有两条数据:信息1,信息2,要的结果是会员1的信息1,会员2的信息1,会员1的信息2,会员2的信息2。
把每个会员的相关信息都存到一个优先队列。所有的搜索结果就是每个会员组成的优先队列。也就是说,优先队列有很多。