2016-11-22 24 views
1

我正在寻找一种方法来自定义排序我的Lucene.Net结果,我放置null -values(字段不存在于文档)在底部无论方向(上升或下降)。Lucene.Net 3.0.3中的空值自定义排序

以下数据总结了情况和想要的结果:

data in index wanted sort result 
data    desc asc 
----    ---- ---- 
100    400  100 
400    300  300 
null    100  400 
300    null null 

我的情况是,我有一些产品不是所有产品价格。按升序排序时,我首先需要最便宜的产品,而不是没有价格的产品(因为是预期的默认行为)。没有价格的产品仍然应该在结果中,但最终,因为这些在按价格排序时最不相关。

我已经看了不少有关谷歌,我还没有真正找到任何答案如何实现自定义排序Lucene.Net 3.0.3

我发现的最好的例子是this answer,这似乎指向我正在寻找的方向。但答案是旧的,它指的是ScoreDocComparator,原始来源似乎是deprecated,因此也是在Lucene.Net的当前版本3.0.3中。 原始项目是指FieldComparator作为替代,但这似乎比ScoreDocComparator(需要实施/重写的许多方法以及许多可以受益于继承而不是重复实现的方法)实现起来更加复杂,并且I不要怀疑这是一条正确的道路吗?

理想情况下,我想实现一些通用的int/long字段,它可以像SortField对象那样考虑字段名,因为我期望在将来有更多的字段可以受益于此自定义排序行为。

我认为,执行是围绕Sort/SortField类的用法地方完成的,所以我结束使用代码可能是这样的:

var sort = new Sort(new MyNullLastSortField("Price", SortField.INT, reverse));

但也许,这也是错误的方式? SortField有一个构造函数,它将FieldComparator作为参数,但我似乎无法围绕如何构建和实现以及索引的实际数据值在哪里流入和流出。

任何帮助指向我在正确的方向(最好与示例代码)非常感谢。

我的故障转移解决方案(不是首选的)是将两个字段添加到仅用于执行排序的索引,手动处理插入时的空值并在降序情况下将它们设置为-1, 9999999在递增的情况下。然后按照价格和方向的特定字段名字段进行正常排序。

+0

您是否找到了解决方案?我可能有时间进入它... OOTB NumericFields不可为空。你有一些哨兵价值或自定义字段类型。 ValueComparitor(和一个关联的FieldComparatorSource)将需要一个版本。 – AndyPook

回答

1

好奇心得到了我最好的。这里有一个解决方案(含警告)

完整的源代码是在https://github.com/AndyPook/SO_CustomSort-40744865

扩展方法来添加可空整数。 NumericField使用编码来存储值,我不想进入,所以我只使用了一个标记值。

public static class NumericFieldExtensions 
{ 
    public static NumericField SetIntValue(this NumericField f, int? value) 
    { 
     if (value.HasValue) 
      f.SetIntValue(value.Value); 
     else 
      f.SetIntValue(int.MinValue); 

     return f; 
    } 
} 

“理解”标记的自定义compatitor。它只是lucene的IntComparator的一个副本,即sealed,因此可以进行复制。查看int.MinValue以查看差异。

public class NullableIntComparator : FieldComparator 
{ 
    private int[] values; 
    private int[] currentReaderValues; 
    private string field; 
    private IntParser parser; 
    private int bottom; // Value of bottom of queue 
    private bool reversed; 

    public NullableIntComparator(int numHits, string field, Parser parser, bool reversed) 
    { 
     values = new int[numHits]; 
     this.field = field; 
     this.parser = (IntParser)parser; 
     this.reversed = reversed; 
    } 

    public override int Compare(int slot1, int slot2) 
    { 
     // TODO: there are sneaky non-branch ways to compute 
     // -1/+1/0 sign 
     // Cannot return values[slot1] - values[slot2] because that 
     // may overflow 
     int v1 = values[slot1]; 
     int v2 = values[slot2]; 

     if (v1 == int.MinValue) 
      return reversed ? -1 : 1; 
     if (v2 == int.MinValue) 
      return reversed ? 1 : -1; 

     if (v1 > v2) 
     { 
      return 1; 
     } 
     else if (v1 < v2) 
     { 
      return -1; 
     } 
     else 
     { 
      return 0; 
     } 
    } 

    public override int CompareBottom(int doc) 
    { 
     if (bottom == int.MinValue) 
      return reversed ? -1 : 1; 

     // TODO: there are sneaky non-branch ways to compute 
     // -1/+1/0 sign 
     // Cannot return bottom - values[slot2] because that 
     // may overflow 
     int v2 = currentReaderValues[doc]; 

     if (v2 == int.MinValue) 
      return reversed ? 1 : -1; 

     if (bottom > v2) 
     { 
      return 1; 
     } 
     else if (bottom < v2) 
     { 
      return -1; 
     } 
     else 
     { 
      return 0; 
     } 
    } 

    public override void Copy(int slot, int doc) 
    { 
     values[slot] = currentReaderValues[doc]; 
    } 

    public override void SetNextReader(IndexReader reader, int docBase) 
    { 
     currentReaderValues = FieldCache_Fields.DEFAULT.GetInts(reader, field, parser); 
    } 

    public override void SetBottom(int bottom) 
    { 
     this.bottom = values[bottom]; 
    } 

    public override IComparable this[int slot] => values[slot]; 
} 

最后一个FieldComparatorSource定义自定义排序

public class NullableIntFieldCompatitorSource : FieldComparatorSource 
{ 
    public override FieldComparator NewComparator(string fieldname, int numHits, int sortPos, bool reversed) 
    { 
     return new NullableIntComparator(numHits, fieldname, FieldCache_Fields.NUMERIC_UTILS_INT_PARSER, reversed); 
    } 
} 

一些测试。了解Sort是如何为这种插件组合而创建的。

private class DataDoc 
    { 
     public int ID { get; set; } 
     public int? Data { get; set; } 
    } 

    private IEnumerable<DataDoc> Search(Sort sort) 
    { 
     var result = searcher.Search(new MatchAllDocsQuery(), null, 99, sort); 

     foreach (var topdoc in result.ScoreDocs) 
     { 
      var doc = searcher.Doc(topdoc.Doc); 
      int id = int.Parse(doc.GetFieldable("id").StringValue); 
      int data = int.Parse(doc.GetFieldable("data").StringValue); 

      yield return new DataDoc 
      { 
       ID = id, 
       Data = data == int.MinValue ? (int?)null : data 
      }; 
     } 
    } 

    [Fact] 
    public void SortAscending() 
    { 
     var sort = new Sort(new SortField("data", new NullableIntFieldCompatitorSource())); 

     var result = Search(sort).ToList(); 

     Assert.Equal(4, result.Count); 
     Assert.Equal(new int?[] { 100, 300, 400, null }, result.Select(x => x.Data)); 
    } 


    [Fact] 
    public void SortDecending() 
    { 
     var sort = new Sort(new SortField("data", new NullableIntFieldCompatitorSource(),true)); 

     var result = Search(sort).ToList(); 

     Assert.Equal(4, result.Count); 
     Assert.Equal(new int?[] { 400, 300, 100, null }, result.Select(x => x.Data)); 
    } 

  • 每个文档MUST包含一个有效的int “数据” 字段。您不能只是省略该字段
  • 您需要使NullableIntFieldCompatitorSource更复杂,以便它为您的字段名称返回正确的比较值。
  • 您需要为其他数字类型创建比较器。请参阅https://github.com/apache/lucenenet/blob/3.0.3/src/core/Search/FieldComparator.cs
  • 如果您不想使用定位值,则需要进入NumericField并了解如何编码null。但这意味着要进入其他几个类别
+0

看起来很有趣......但我只是在阅读代码......并想知道......如果你有多个阅读器呢? –

+0

处理内置比较器的方式相同。 如果您有多个阅读器,他们可能都是在'MultiReader'下收集的。记住'I​​ndexSearcher'只需要一个阅读器。如果这种情况发生在多个子阅读器上,它并不关心 – AndyPook

+0

非常感谢你的回答 - 它确实让我走上了正确的道路,现在我有了一些像我想要的那样工作的东西。我已经稍微修改了它,以满足我的需要,因为我不喜欢在索引中仅用于排序的特殊字段和值的想法,我想使用搜索时也使用的现有字段。因为我从来没有把索引中的字段如果值为0(或低于),我可以将零值视为空情况,唯一不同于原始IntFieldComparator的是Copy方法,其中将值更改为MaxValue/MinValue if值为0。 – jan