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。

把每个会员的相关信息都存到一个优先队列。所有的搜索结果就是每个会员组成的优先队列。也就是说,优先队列有很多。