You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

升级Lucene.Net至4.8后搜索性能下降问题排查

Lucene.Net 3.0→4.8升级后搜索性能下降的代码优化建议

将Lucene.Net从3.0版本升级至4.8版本后,搜索乘客姓氏的功能可正常返回正确结果,但搜索性能明显下降。本次测试涉及47K条记录,匹配结果共185条,分页返回每组25条数据的耗时对比:

  • 第1组:从140ms增至396ms
  • 第2组:从216ms增至380ms

搜索器调用代码改动极小,仅适配了4.8版本的API,代码如下:

var searchFields = new string[4];
searchFields[0] = "Id";
searchFields[1] = "FirstName";
searchFields[2] = "LastName";
searchFields[3] = "Tag";

var analyzer = new StandardAnalyzer(LuceneVersion.LUCENE_48);
var parser = new MultiFieldQueryParser(LuceneVersion.LUCENE_48, searchFields, analyzer)
{
    AllowLeadingWildcard = true,
    DefaultOperator = QueryParserBase.AND_OPERATOR
};

var searchCriteria = "Jones";
var query = parser.Parse(searchCriteria);

var directory = FSDirectory.Open(new DirectoryInfo(indexDirectory));
DirectoryReader di = DirectoryReader.Open(directory);
var searcher = new IndexSearcher(di);   

var start = DateTime.Now; // --> Start Timer
var filter = new QueryWrapperFilter(query);
var topDocs = searcher.Search(query, filter, (1 * 25), sort);
var stop = DateTime.Now;  // --> End Timer

性能问题主要集中在自定义的TagComparator类的实现差异上,新旧代码对比:

3.0版本旧代码

internal class TagComparator : FieldComparator
{
    private string[] values;
    private string[] currentReaderValues;
    private string field;
    private string bottom; 
    private bool reversed;

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

    public override int Compare(int slot1, int slot2)
    {
        string v1 = values[slot1];
        string v2 = values[slot2];

        return DoCompare(v1, v2);
    }

    public override int CompareBottom(int doc)
    {
        string v2 = currentReaderValues[doc];

        return DoCompare(bottom, v2);
    }

    private int DoCompare(string v1, string v2)
    {
        if (string.IsNullOrEmpty(v1))
        {
            if (string.IsNullOrEmpty(v2))
            {
                return 0;
            }

            return reversed ? -1 : 1;
        }

        if (string.IsNullOrEmpty(v2))
        {
            return reversed ? 1 : -1;
        }

        return v1.CompareTo(v2);
    }

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

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

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

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

4.8版本新代码

internal class TagComparator : FieldComparer<BytesRef>
{
    protected readonly ILog Log;
    private readonly BytesRef[] bvalues;
    private readonly string field;
    private readonly bool reversed;
    private BytesRef termCopy = new BytesRef();
    private SortedDocValues sortedResults;
    private int bottomSlot;

    public override BytesRef this[int slot] => bvalues[slot];

    public TagComparator(int numHits, string field, bool reversed)
    {
        bvalues = new BytesRef[numHits];
        this.field = field;
        this.reversed = reversed;
    }

    public override int Compare(int slot1, int slot2)
    {
        var result1 = DoCompare(bvalues[slot1], bvalues[slot2]);
        return result1;
    }

    private int DoCompare(BytesRef v1, BytesRef v2)
    {
        if (v1.Length == 0)
        {
            if (v2.Length == 0)
            {
                return 0;
            }

            return reversed ? -1 : 1;
        }

        if (v2.Length == 0)
        {
            return reversed ? 1 : -1;
        }

        if (v1.CompareTo(v2) > 0)
            return 1;
        else
            return -1;
    }

    public override void Copy(int slot, int doc)
    {
        termCopy = new BytesRef();
        sortedResults.Get(doc, termCopy);
        bvalues[slot] = termCopy;
    }

    public override int CompareBottom(int doc)
    {
        BytesRef termOrd = new BytesRef();
        int ord = sortedResults.GetOrd(doc);
        sortedResults.LookupOrd(ord, termOrd);
        var result = DoCompare(bvalues[bottomSlot], termOrd);

        return result;
    }

    public override void SetBottom(int bottom)
    {
        bottomSlot = bottom;
    }

    public override FieldComparer SetNextReader(AtomicReaderContext context)
    {
        sortedResults = FieldCache.DEFAULT.GetTermsIndex(context.AtomicReader, field);
        return this;
    }

    public override int CompareTop(int doc)
    {
        throw new NotImplementedException();
    }

    public override void SetTopValue(BytesRef value)
    {
        throw new NotImplementedException();
    }
}

已知未实现的CompareTop和SetTopValue方法并未执行,现有方法执行顺序为SetNextReader→Copy→Compare,已将字符串对比改为BytesRef对比但性能问题依然存在,以下是代码层面的优化建议:


优化建议

  1. 减少BytesRef对象的重复创建
    在Copy方法中,每次新建BytesRef实例会导致频繁内存分配和GC压力。可复用已有的termCopy对象,避免重复创建:

    public override void Copy(int slot, int doc)
    {
        sortedResults.Get(doc, termCopy);
        // 复制BytesRef内容而非引用,避免后续被覆盖
        bvalues[slot] = BytesRef.DeepCopyOf(termCopy);
    }
    

    同时在构造函数中初始化termCopy,无需每次新建。

  2. 优化CompareBottom方法的调用逻辑
    当前CompareBottom中先通过GetOrd获取序号再LookupOrd获取BytesRef,两步操作比直接读取字段值更耗时。若字段是存储的,可改为直接读取存储值;若必须使用SortedDocValues,可缓存常用BytesRef对象,减少重复查找。

  3. 简化DoCompare方法的逻辑
    直接复用BytesRef.CompareTo的结果,结合反转逻辑,减少分支判断:

    private int DoCompare(BytesRef v1, BytesRef v2)
    {
        if (v1.Length == 0 && v2.Length == 0) return 0;
        if (v1.Length == 0) return reversed ? -1 : 1;
        if (v2.Length == 0) return reversed ? 1 : -1;
        int cmp = v1.CompareTo(v2);
        return reversed ? -cmp : cmp;
    }
    

    避免多余的if-else分支,提升执行效率。

  4. 检查FieldCache的使用方式
    4.8版本中FieldCache.DEFAULT.GetTermsIndex的性能可能不如直接读取字段存储值。若Tag字段是Stored类型,可在SetNextReader中获取StoredFieldsReader,直接读取字符串再转为BytesRef,减少缓存转换开销。

  5. 复用IndexSearcher和DirectoryReader
    当前代码每次搜索都新建DirectoryReader和IndexSearcher,初始化开销极大。可将二者作为单例复用,仅在索引更新时重新打开,大幅减少初始化耗时。


内容的提问来源于stack exchange,提问作者user1019042

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.30 09:48:08