升级Lucene.Net至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对比但性能问题依然存在,以下是代码层面的优化建议:
优化建议
减少
BytesRef对象的重复创建
在Copy方法中,每次新建BytesRef实例会导致频繁内存分配和GC压力。可复用已有的termCopy对象,避免重复创建:public override void Copy(int slot, int doc) { sortedResults.Get(doc, termCopy); // 复制BytesRef内容而非引用,避免后续被覆盖 bvalues[slot] = BytesRef.DeepCopyOf(termCopy); }同时在构造函数中初始化
termCopy,无需每次新建。优化
CompareBottom方法的调用逻辑
当前CompareBottom中先通过GetOrd获取序号再LookupOrd获取BytesRef,两步操作比直接读取字段值更耗时。若字段是存储的,可改为直接读取存储值;若必须使用SortedDocValues,可缓存常用BytesRef对象,减少重复查找。简化
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分支,提升执行效率。检查
FieldCache的使用方式
4.8版本中FieldCache.DEFAULT.GetTermsIndex的性能可能不如直接读取字段存储值。若Tag字段是Stored类型,可在SetNextReader中获取StoredFieldsReader,直接读取字符串再转为BytesRef,减少缓存转换开销。复用
IndexSearcher和DirectoryReader
当前代码每次搜索都新建DirectoryReader和IndexSearcher,初始化开销极大。可将二者作为单例复用,仅在索引更新时重新打开,大幅减少初始化耗时。
内容的提问来源于stack exchange,提问作者user1019042

