| using Lucene.Net.Diagnostics; |
| using Lucene.Net.Support; |
| using Lucene.Net.Util; |
| using System; |
| using System.Buffers; |
| using System.Collections.Generic; |
| using System.Diagnostics.CodeAnalysis; |
| using System.Runtime.CompilerServices; |
| using System.Runtime.InteropServices; |
| |
| namespace Lucene.Net.Search |
| { |
| /* |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| |
| /// <summary> |
| /// Represents hits returned by |
| /// <see cref="IndexSearcher.Search(Query,Filter,int)"/> and |
| /// <see cref="IndexSearcher.Search(Query,int)"/>. |
| /// </summary> |
| public class TopDocs |
| { |
| /// <summary> |
| /// The total number of hits for the query. </summary> |
| public int TotalHits { get; set; } |
| |
| /// <summary> |
| /// The top hits for the query. </summary> |
| [WritableArray] |
| [SuppressMessage("Microsoft.Performance", "CA1819", Justification = "Lucene's design requires some writable array properties")] |
| public ScoreDoc[] ScoreDocs { get; set; } |
| |
| /// <summary> |
| /// Stores the maximum score value encountered, needed for normalizing. </summary> |
| private float maxScore; |
| |
| /// <summary> |
| /// Returns the maximum score value encountered. Note that in case |
| /// scores are not tracked, this returns <see cref="float.NaN"/>. |
| /// </summary> |
| public virtual float MaxScore |
| { |
| get => maxScore; |
| set => this.maxScore = value; |
| } |
| |
| /// <summary> |
| /// Constructs a <see cref="TopDocs"/> with a default <c>maxScore=System.Single.NaN</c>. </summary> |
| internal TopDocs(int totalHits, ScoreDoc[] scoreDocs) |
| : this(totalHits, scoreDocs, float.NaN) |
| { |
| } |
| |
| public TopDocs(int totalHits, ScoreDoc[] scoreDocs, float maxScore) |
| { |
| this.TotalHits = totalHits; |
| this.ScoreDocs = scoreDocs; |
| this.maxScore = maxScore; |
| } |
| |
| #nullable enable |
| |
| private static readonly int ShardByteSize = Marshal.SizeOf(typeof(Shard)); // LUCENENET specific so we can calculate stack size |
| |
| // LUCENENET specific - Renamed ShardRef to Shard and made it into a struct |
| // so we can allocate arrays of them on the stack. |
| // Refers to one hit: |
| [StructLayout(LayoutKind.Sequential)] |
| private struct Shard : IEquatable<Shard> |
| { |
| [SuppressMessage("CodeQuality", "IDE0079:Remove unnecessary suppression", Justification = "This is a SonarCloud issue")] |
| [SuppressMessage("Major Code Smell", "S2933:Fields that are only assigned in the constructor should be \"readonly\"", Justification = "Structs are known to have performance issues with readonly fields")] |
| [SuppressMessage("Style", "IDE0044:Add readonly modifier", Justification = "Structs are known to have performance issues with readonly fields")] |
| private int shardIndex; |
| private int hitIndex; |
| |
| // Which shard (index into shardHits[]): |
| internal int ShardIndex => shardIndex; |
| |
| // Which hit within the shard: |
| internal int HitIndex |
| { |
| get => hitIndex; |
| set => hitIndex = value; |
| } |
| |
| public Shard(int shardIndex) |
| { |
| this.shardIndex = shardIndex; |
| this.hitIndex = 0; |
| } |
| |
| public bool Equals(Shard other) |
| { |
| return shardIndex == other.shardIndex && hitIndex == other.hitIndex; |
| } |
| |
| public override bool Equals(object? obj) |
| { |
| if (obj is Shard shard) |
| return Equals(shard); |
| return false; |
| } |
| |
| public override int GetHashCode() |
| => shardIndex.GetHashCode() ^ hitIndex.GetHashCode(); |
| |
| public override string ToString() |
| { |
| return $"{nameof(Shard)}({nameof(shardIndex)}={shardIndex} {nameof(hitIndex)}={hitIndex})"; |
| } |
| |
| public static bool operator ==(Shard shard1, Shard shard2) |
| { |
| return shard1.Equals(shard2); |
| } |
| |
| public static bool operator !=(Shard shard1, Shard shard2) |
| => !(shard1 == shard2); |
| } |
| |
| // LUCENENET specific - refactored ScoreMergeSortQueue into ScoreMergeSortComparer so it can be passed into a ValuePriorityQueue |
| |
| // Specialized MergeSortComparer that just merges by |
| // relevance score, descending: |
| private sealed class ScoreMergeSortComparer : PriorityComparer<Shard> // LUCENENET specific - marked sealed |
| { |
| internal readonly ScoreDoc[][] shardHits; |
| |
| public ScoreMergeSortComparer(TopDocs[] shardHits) |
| { |
| if (shardHits is null) |
| throw new ArgumentNullException(nameof(shardHits)); |
| |
| this.shardHits = new ScoreDoc[shardHits.Length][]; |
| for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++) |
| { |
| this.shardHits[shardIDX] = shardHits[shardIDX].ScoreDocs; |
| } |
| } |
| |
| // Returns true if first is < second |
| protected internal override bool LessThan(Shard first, Shard second) |
| { |
| if (Debugging.AssertsEnabled) Debugging.Assert(first != second); |
| float firstScore = shardHits[first.ShardIndex][first.HitIndex].Score; |
| float secondScore = shardHits[second.ShardIndex][second.HitIndex].Score; |
| |
| // LUCENENET specific - compare bits rather than using equality operators to prevent these comparisons from failing in x86 in .NET Framework with optimizations enabled |
| if (NumericUtils.SingleToSortableInt32(firstScore) < NumericUtils.SingleToSortableInt32(secondScore)) |
| { |
| return false; |
| } |
| else if (NumericUtils.SingleToSortableInt32(firstScore) > NumericUtils.SingleToSortableInt32(secondScore)) |
| { |
| return true; |
| } |
| else |
| { |
| // Tie break: earlier shard wins |
| if (first.ShardIndex < second.ShardIndex) |
| { |
| return true; |
| } |
| else if (first.ShardIndex > second.ShardIndex) |
| { |
| return false; |
| } |
| else |
| { |
| // Tie break in same shard: resolve however the |
| // shard had resolved it: |
| if (Debugging.AssertsEnabled) Debugging.Assert(first.HitIndex != second.HitIndex); |
| return first.HitIndex < second.HitIndex; |
| } |
| } |
| } |
| } |
| |
| // LUCENENET specific - refactored MergeSortQueue into MergeSortComparer so it can be passed into a ValuePriorityQueue |
| private sealed class MergeSortComparer : PriorityComparer<Shard> // LUCENENET specific - marked sealed |
| { |
| // These are really FieldDoc instances: |
| internal readonly ScoreDoc[][] shardHits; |
| |
| internal readonly FieldComparer[] comparers; |
| internal readonly int[] reverseMul; |
| |
| public MergeSortComparer(Sort sort, TopDocs[] shardHits) |
| { |
| if (shardHits is null) |
| throw new ArgumentNullException(nameof(shardHits)); |
| |
| this.shardHits = new ScoreDoc[shardHits.Length][]; |
| for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++) |
| { |
| ScoreDoc[] shard = shardHits[shardIDX].ScoreDocs; |
| //System.out.println(" init shardIdx=" + shardIDX + " hits=" + shard); |
| if (shard != null) |
| { |
| this.shardHits[shardIDX] = shard; |
| // Fail gracefully if API is misused: |
| for (int hitIDX = 0; hitIDX < shard.Length; hitIDX++) |
| { |
| ScoreDoc sd = shard[hitIDX]; |
| if (!(sd is FieldDoc)) |
| { |
| throw new ArgumentException("shard " + shardIDX + " was not sorted by the provided Sort (expected FieldDoc but got ScoreDoc)"); |
| } |
| FieldDoc fd = (FieldDoc)sd; |
| if (fd.Fields is null) |
| { |
| throw new ArgumentException("shard " + shardIDX + " did not set sort field values (FieldDoc.fields is null); you must pass fillFields=true to IndexSearcher.search on each shard"); |
| } |
| } |
| } |
| } |
| |
| SortField[] sortFields = sort.GetSort(); |
| comparers = new FieldComparer[sortFields.Length]; |
| reverseMul = new int[sortFields.Length]; |
| for (int compIDX = 0; compIDX < sortFields.Length; compIDX++) |
| { |
| SortField sortField = sortFields[compIDX]; |
| comparers[compIDX] = sortField.GetComparer(1, compIDX); |
| reverseMul[compIDX] = sortField.IsReverse ? -1 : 1; |
| } |
| } |
| |
| // Returns true if first is < second |
| protected internal override bool LessThan(Shard first, Shard second) |
| { |
| if (Debugging.AssertsEnabled) Debugging.Assert(first != second); |
| FieldDoc firstFD = (FieldDoc)shardHits[first.ShardIndex][first.HitIndex]; |
| FieldDoc secondFD = (FieldDoc)shardHits[second.ShardIndex][second.HitIndex]; |
| //System.out.println(" lessThan:\n first=" + first + " doc=" + firstFD.doc + " score=" + firstFD.score + "\n second=" + second + " doc=" + secondFD.doc + " score=" + secondFD.score); |
| |
| for (int compIDX = 0; compIDX < comparers.Length; compIDX++) |
| { |
| FieldComparer comp = comparers[compIDX]; |
| //System.out.println(" cmp idx=" + compIDX + " cmp1=" + firstFD.fields[compIDX] + " cmp2=" + secondFD.fields[compIDX] + " reverse=" + reverseMul[compIDX]); |
| |
| int cmp = reverseMul[compIDX] * comp.CompareValues(firstFD.Fields[compIDX], secondFD.Fields[compIDX]); |
| |
| if (cmp != 0) |
| { |
| //System.out.println(" return " + (cmp < 0)); |
| return cmp < 0; |
| } |
| } |
| |
| // Tie break: earlier shard wins |
| if (first.ShardIndex < second.ShardIndex) |
| { |
| //System.out.println(" return tb true"); |
| return true; |
| } |
| else if (first.ShardIndex > second.ShardIndex) |
| { |
| //System.out.println(" return tb false"); |
| return false; |
| } |
| else |
| { |
| // Tie break in same shard: resolve however the |
| // shard had resolved it: |
| //System.out.println(" return tb " + (first.hitIndex < second.hitIndex)); |
| if (Debugging.AssertsEnabled) Debugging.Assert(first.HitIndex != second.HitIndex); |
| return first.HitIndex < second.HitIndex; |
| } |
| } |
| } |
| |
| /// <summary> |
| /// Returns a new <see cref="TopDocs"/>, containing <paramref name="topN"/> results across |
| /// the provided <see cref="TopDocs"/>, sorting by the specified |
| /// <see cref="Sort"/>. Each of the <see cref="TopDocs"/> must have been sorted by |
| /// the same <see cref="Sort"/>, and sort field values must have been |
| /// filled (ie, <c>fillFields=true</c> must be |
| /// passed to |
| /// <see cref="TopFieldCollector.Create(Sort, int, bool, bool, bool, bool)"/>. |
| /// |
| /// <para/>Pass <paramref name="sort"/>=null to merge sort by score descending. |
| /// <para/> |
| /// @lucene.experimental |
| /// </summary> |
| /// <exception cref="ArgumentNullException"><paramref name="shardHits"/> is <c>null</c>.</exception> |
| [MethodImpl(MethodImplOptions.NoInlining)] |
| public static TopDocs Merge(Sort? sort, int topN, TopDocs[] shardHits) |
| { |
| return Merge(sort, 0, topN, shardHits); |
| } |
| |
| /// <summary> |
| /// Same as <see cref="Merge(Sort, int, TopDocs[])"/> but also slices the result at the same time based |
| /// on the provided start and size. The return <c>TopDocs</c> will always have a scoreDocs with length of |
| /// at most <see cref="Util.PriorityQueue{T}.Count"/>. |
| /// </summary> |
| /// <exception cref="ArgumentNullException"><paramref name="shardHits"/> is <c>null</c>.</exception> |
| [MethodImpl(MethodImplOptions.NoInlining)] |
| public static TopDocs Merge(Sort? sort, int start, int size, TopDocs[] shardHits) |
| { |
| // LUCENENET specific - added guard clause. |
| if (shardHits is null) |
| throw new ArgumentNullException(nameof(shardHits)); |
| |
| // LUCENENET: Refactored PriorityQueue<T> subclasses into PriorityComparer<T> |
| // implementations, which can be passed into ValuePriorityQueue. ValuePriorityQueue |
| // lives on the stack, and if the array size is small enough, we also allocate the |
| // array on the stack. Fallback to the array pool if it is beyond MaxStackByteLimit. |
| IComparer<Shard> comparer; |
| if (sort is null) |
| { |
| comparer = new ScoreMergeSortComparer(shardHits); |
| } |
| else |
| { |
| comparer = new MergeSortComparer(sort, shardHits); |
| } |
| int bufferSize = PriorityQueue.GetArrayHeapSize(shardHits.Length); |
| bool usePool = ShardByteSize * bufferSize > Constants.MaxStackByteLimit; |
| Shard[]? arrayToReturnToPool = usePool ? ArrayPool<Shard>.Shared.Rent(bufferSize) : null; |
| try |
| { |
| Span<Shard> buffer = usePool ? arrayToReturnToPool : stackalloc Shard[bufferSize]; |
| var queue = new ValuePriorityQueue<Shard>(buffer, comparer); |
| |
| int totalHitCount = 0; |
| int availHitCount = 0; |
| float maxScore = float.Epsilon; // LUCENENET: Epsilon in .NET is the same as MIN_VALUE in Java |
| for (int shardIDX = 0; shardIDX < shardHits.Length; shardIDX++) |
| { |
| TopDocs shard = shardHits[shardIDX]; |
| // totalHits can be non-zero even if no hits were |
| // collected, when searchAfter was used: |
| totalHitCount += shard.TotalHits; |
| if (shard.ScoreDocs != null && shard.ScoreDocs.Length > 0) |
| { |
| availHitCount += shard.ScoreDocs.Length; |
| queue.Add(new Shard(shardIDX)); |
| maxScore = Math.Max(maxScore, shard.MaxScore); |
| //System.out.println(" maxScore now " + maxScore + " vs " + shard.getMaxScore()); |
| } |
| } |
| |
| if (availHitCount == 0) |
| { |
| maxScore = float.NaN; |
| } |
| |
| ScoreDoc[] hits; |
| if (availHitCount <= start) |
| { |
| hits = Array.Empty<ScoreDoc>(); |
| } |
| else |
| { |
| hits = new ScoreDoc[Math.Min(size, availHitCount - start)]; |
| int requestedResultWindow = start + size; |
| int numIterOnHits = Math.Min(availHitCount, requestedResultWindow); |
| int hitUpto = 0; |
| while (hitUpto < numIterOnHits) |
| { |
| if (Debugging.AssertsEnabled) Debugging.Assert(queue.Count > 0); |
| // LUCENENET NOTE: Since we are popping this from the queue and then |
| // adding it back, we properly get our updated HitIndex into the queue. |
| Shard @ref = queue.Pop(); |
| ScoreDoc hit = shardHits[@ref.ShardIndex].ScoreDocs[@ref.HitIndex++]; |
| hit.ShardIndex = @ref.ShardIndex; |
| if (hitUpto >= start) |
| { |
| hits[hitUpto - start] = hit; |
| } |
| |
| //System.out.println(" hitUpto=" + hitUpto); |
| //System.out.println(" doc=" + hits[hitUpto].doc + " score=" + hits[hitUpto].score); |
| |
| hitUpto++; |
| |
| if (@ref.HitIndex < shardHits[@ref.ShardIndex].ScoreDocs.Length) |
| { |
| // Not done with this these TopDocs yet: |
| queue.Add(@ref); |
| } |
| } |
| } |
| |
| if (sort is null) |
| { |
| return new TopDocs(totalHitCount, hits, maxScore); |
| } |
| else |
| { |
| return new TopFieldDocs(totalHitCount, hits, sort.GetSort(), maxScore); |
| } |
| } |
| finally |
| { |
| if (arrayToReturnToPool is not null) |
| ArrayPool<Shard>.Shared.Return(arrayToReturnToPool); |
| } |
| |
| } |
| } |
| } |