自己动手写分布式搜索引擎
上QQ阅读APP看书,第一时间看更新

3.3.9 排序

为了实现情景搜索,根据用户IP判断用户所在地区,然后把与用户所在区域相同的文档排在前面,即通过排序把地名相同的搜索结果放在前面。

若采用自定义排序对象的方法实现把和用户所在地域相同的结果放在前面,需要实现FieldComparatorSource和FieldComparator接口。FieldComparatorSource功能是返回一个用来排序的比较器。这个接口只定义一个方法newComparator(),用来生成FieldComparator对象。生成的FieldComparator对象可作为指定文档列的排序比较器。

当使用TopFieldCollector收集最前面的结果时,要用FieldComparator比较命中的文档,从而确定它们的排列顺序。

具体的FieldComparator类对应不同的SortField类型。

FieldComparator对象需要实现compare()、compareBottom()、copy()、setBottom()、setNextReader()、value()等方法,如果需要用到文档的评分值,还需要实现setScorer()方法。

● compare(int, int):比较槽a和槽b的命中文档。

● setBottom(int):通过FieldValueHitQueue调用此方法,以通知FieldComparator当前最底部的槽。

● compareBottom(int):新的命中文档(docID)和队列中的最弱的底部进行比较。

● copy(int, int) 安排一个新的命中文档到优先队列中去。当一个新的命中文档具有竞争力时,FieldValueHitQueue调用这个方法。

● setNextReader(org.apache.lucene.index.IndexReader, int):当搜索切换到下一个段时调用这个方法。可能需要更新比较器的内部状态,例如从FieldCache检索新值。

● value(int):返回存储在指定位置的值。当返回顶部结果时,为了填充FieldDoc.fields,才在搜索结束时调用它。

首先要定义一个继承抽象类FieldComparatorSource的实现类CityFieldComparator,重写其中的抽象方法:

        public FieldComparator newComparator(String field, int numHits,
                    int sortPos, boolean reversed) throws IOException;

该方法返回一个FieldComparator实例,实现自定义排序。

然后定义一个类CitySortComparator继承自FieldComparator抽象类。重写其中的抽象方法,在public int compare(int slot1, int slot2)方法中实现排序业务逻辑。

TopFieldCollector是一个用SortField排序的Collector。SortField使用FieldComparator构造。

最后再调用该自定义排序类。

        //这里设置为true是正序,false为倒序。调用CityFieldComparator的构造方法初始化
        //城市名,城市名称可以根据用户IP获得。
        SortField citySort = new SortField("city", new CityFieldComparator(city),
        true);
        //按时间排序,true为时间减序,false为时间增序。
        SortField dateSort = new SortField("date", SortField.INT, true);
        //首先按城市排序,然后按日期排序
        Sort sort = new Sort(new SortField[] {citySort, dateSort});


        TopFieldCollector collector = TopFieldCollector.create(sort, offset+rows,
                        false, true, false, false);
        //isearcher是IndexSearcher的实例
        isearcher.search(bq, collector);
        ScoreDoc[] hits = collector.topDocs().scoreDocs;

排序速度很慢,因为排序列的值都缓存在FieldCache中。对于要用来排序的字段,先从索引中将每篇文档中该字段的值都读出来,放到一个大小为maxDoc的数组中。maxDoc是文档编号的最大值。倒排索引的词列是字符串或者字节数组类型,从倒排索引的词列解析出值的过程叫作反倒排。

有两点需要注意:

(1) FieldCache中的字段值是从倒排表中读出来的,它不是索引文件中存储的字段值,所以排序的字段必须是索引字段。

(2) 用来排序的字段在索引的时候不能拆分,因为FieldCache数组中,每个文档只对应一个字段值,拆分的话,缓存中只会保存词典中靠后的值。

FieldCache是Lucene最占用内存的部分,大部分内存溢出的错误都是由它引起的,需要特别注意。

每个商品只有一个价格,也就是说,商品的价格是唯一的。用按列存储值的方法依价格排序。

每个文档有几个数值类型的字段。可根据这些列的加权和来排序。例如:

        field1=100
        field2=002
        field3=014

加权函数类似:

        f(d) = field1 * 0.5 + field2 * 1.4 + field3 * 1.8

结果通过f(d)的值排序,这里的变量d表示文档。排序方法应该是非静态的,并且在不同的搜索之间是不同的,因为对执行搜索的不同用户来说,常量是不一样的。也就是说,排序方法是个性化的。

        public class ScaledComparator extends FieldComparator {
            private String[] fields;
    private float[] scalars;
    private int[][] slotValues;
    private int[][] currentReaderValues;
    private int bottomSlot;


    public ScaledComparator(int numHits, String[] fields, float[] scalars)
{
        this.fields = fields;
        this.scalars = scalars;


        this.slotValues = new int[this.fields.length][];
        for (int fieldIndex = 0; fieldIndex < this.fields.length; fieldIndex++)
{
          this.slotValues[fieldIndex] = new int[numHits];
        }


        this.currentReaderValues = new int[this.fields.length][];
    }


    protected float score(int[][] values, int secondaryIndex) {
        float score = 0;


        for (int fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
          int value = values[fieldIndex][secondaryIndex];
          float scalar = scalars[fieldIndex];
          score += (value * scalar);
        }


        return score;
    }


    protected float scoreSlot(int slot) {
        return score(slotValues, slot);
    }


    protected float scoreDoc(int doc) {
        return score(currentReaderValues, doc);
    }


    @Override
    public int compare(int slot1, int slot2) {
        float score1 = scoreSlot(slot1);
        float score2 = scoreSlot(slot2);
        return Float.compare(score1, score2);
    }


    @Override
          public int compareBottom(int doc) throws IOException {
              float bottomScore = scoreSlot(bottomSlot);
              float docScore = scoreDoc(doc);
              return Float.compare(bottomScore, docScore);
          }


          @Override
          public void copy(int slot, int doc) throws IOException {
              for (int fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
                slotValues[fieldIndex][slot] =
      currentReaderValues[fieldIndex][doc];
              }
          }


          @Override
          public void setBottom(int slot) {
              bottomSlot = slot;
          }


          @Override
          public void setNextReader(IndexReader reader, int docBase, int
      numSlotsFull) throws IOException {
              for (int fieldIndex = 0; fieldIndex < fields.length; fieldIndex++) {
                String field = fields[fieldIndex];
                currentReaderValues[fieldIndex] =
                          FieldCache.DEFAULT.getInts(reader, field);
              }
          }


          @Override
          public int sortType() {
              return SortField.CUSTOM;
          }


          @Override
          public Comparable<? > value(int slot) {
              float score = scoreSlot(slot);
              return Float.valueOf(score);
          }
      }

使用ScaledComparator实现排序的功能。FieldComparatorSource的匿名类重写newComparator()方法。newComparator()方法返回ScaledComparator类的一个实例。

        //列名数组
        final String[] fields = new String[]{ "field1", "field2", "field3" };
        final float[] scalars = new float[]{ 0.5f, 1.4f, 1.8f }; //列的权重
        Sort sort = new Sort(
            new SortField(
              "",
              new FieldComparatorSource() {
                  public FieldComparator newComparator(String fieldname, int
        numHits, int sortPos, boolean reversed) throws IOException {
                      return new ScaledComparator(numHits, fields, scalars);
                  }
              }
            )
        );