Skip to content

Commit ed1469a

Browse files
committed
enhancement(16076): Implement 3-ary priority queue
1 parent dbc915f commit ed1469a

3 files changed

Lines changed: 109 additions & 31 deletions

File tree

lucene/core/src/java/org/apache/lucene/search/FieldValueHitQueue.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@
2929
*/
3030
public class FieldValueHitQueue<T extends FieldValueHitQueue.Entry> extends PriorityQueue<T> {
3131

32+
private static final int HEAP_ARITY = 3;
33+
3234
/** Extension of ScoreDoc to also store the {@link FieldComparator} slot. */
3335
public static class Entry extends ScoreDoc {
3436
public int slot;
@@ -112,7 +114,7 @@ public boolean lessThan(Entry hitA, Entry hitB) {
112114

113115
// prevent instantiation and extension.
114116
private FieldValueHitQueue(SortField[] fields, int size, EntryLessThan lessThan) {
115-
super(size, lessThan);
117+
super(size, HEAP_ARITY, lessThan);
116118
// When we get here, fields.length is guaranteed to be > 0, therefore no
117119
// need to check it again.
118120
this.fields = fields;

lucene/core/src/java/org/apache/lucene/util/PriorityQueue.java

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,20 @@ public static <T> PriorityQueue<T> usingComparator(
7373
protected int size = 0;
7474
private final int maxSize;
7575
private final T[] heap;
76+
private final int arity;
7677
private final LessThan<? super T> lessThan;
7778

7879
/** Create an empty priority queue of the configured size using the specified {@link LessThan}. */
7980
public PriorityQueue(int maxSize, LessThan<? super T> lessThan) {
80-
this(maxSize, lessThan, () -> null);
81+
this(maxSize, arity, lessThan, () -> null);
82+
}
83+
84+
/**
85+
* Create an empty priority queue of the configured size and heap arity using the specified {@link
86+
* LessThan}.
87+
*/
88+
protected PriorityQueue(int maxSize, int arity, LessThan<? super T> lessThan) {
89+
this(maxSize, arity, lessThan, () -> null);
8190
}
8291

8392
/**
@@ -112,8 +121,24 @@ public PriorityQueue(int maxSize, LessThan<? super T> lessThan) {
112121
*/
113122
public PriorityQueue(
114123
int maxSize, LessThan<? super T> lessThan, Supplier<T> sentinelObjectSupplier) {
124+
this(maxSize, 2, lessThan, sentinelObjectSupplier);
125+
}
126+
127+
/**
128+
* Create a priority queue with a configurable heap arity that is pre-filled with
129+
* sentinel objects
130+
*/
131+
protected PriorityQueue(
132+
int maxSize,
133+
int arity,
134+
LessThan<? super T> lessThan,
135+
Supplier<T> sentinelObjectSupplier) {
115136
final int heapSize;
116137

138+
if (arity < 2) {
139+
throw new IllegalArgumentException("arity mist be >= 2; got: " + arity);
140+
}
141+
117142
if (0 == maxSize) {
118143
// We allocate 1 extra to avoid if statement in top()
119144
heapSize = 2;
@@ -134,6 +159,7 @@ public PriorityQueue(
134159
final T[] h = (T[]) new Object[heapSize];
135160
this.heap = h;
136161
this.maxSize = maxSize;
162+
this.arity = arity;
137163
this.lessThan = lessThan;
138164

139165
// If sentinel objects are supported, populate the queue with them
@@ -168,7 +194,7 @@ public void addAll(Collection<T> elements) {
168194
}
169195
} finally {
170196
// The loop goes down to 1 as heap is 1-based not 0-based.
171-
for (int i = (size >>> 1); i >= 1; i--) {
197+
for (int i = (size <=1 ? 0 : parent(size); i >= 1; i--) {
172198
downHeap(i);
173199
}
174200
}
@@ -200,7 +226,7 @@ public void addAll(Stream<T> elements) {
200226
});
201227
} finally {
202228
// The loop goes down to 1 as heap is 1-based not 0-based.
203-
for (int i = (size >>> 1); i >= 1; i--) {
229+
for (int i = (size <=1 ? 0 : parent(size); i >= 1; i--) {
204230
downHeap(i);
205231
}
206232
}
@@ -332,35 +358,46 @@ public T[] drainToArrayHighestFirst(IntFunction<T[]> newArray) {
332358
protected boolean upHeap(int origPos) {
333359
int i = origPos;
334360
T node = heap[i]; // save bottom node
335-
int j = i >>> 1;
361+
int j = i > 1 ? parent(i) : 0;
336362
while (j > 0 && lessThan.lessThan(node, heap[j])) {
337363
heap[i] = heap[j]; // shift parents down
338364
i = j;
339-
j = j >>> 1;
365+
j = i > 1 ? parent(i) : 0;
340366
}
341367
heap[i] = node; // install saved node
342368
return i != origPos;
343369
}
344370

345371
protected void downHeap(int i) {
346372
T node = heap[i]; // save top node
347-
int j = i << 1; // find smaller child
348-
int k = j + 1;
349-
if (k <= size && lessThan.lessThan(heap[k], heap[j])) {
350-
j = k;
351-
}
352-
while (j <= size && lessThan.lessThan(heap[j], node)) {
353-
heap[i] = heap[j]; // shift up child
354-
i = j;
355-
j = i << 1;
356-
k = j + 1;
357-
if (k <= size && lessThan.lessThan(heap[k], heap[j])) {
358-
j = k;
373+
int j = firstChild(i);
374+
while (j <= size) {
375+
final int lastChild = Math.min(j + arity - 1, size);
376+
int bestChild = j;
377+
for (int c = j + 1; c <= lastChild; ++c) {
378+
if (lessThan.lessThan(heap[c], heap[bestChild])) {
379+
bestChild = c;
380+
}
381+
}
382+
if (lessThan.lessThan(heap[bestChild], node)) {
383+
heap[i] = heap[bestChild]; // shift up child
384+
i = bestChild;
385+
j = firstChild(i);
386+
} else {
387+
break;
359388
}
360389
}
361390
heap[i] = node; // install saved node
362391
}
363392

393+
private int parent(int i) {
394+
return ((i - 2) / arity) + 1;
395+
}
396+
397+
private int firstChild(int i) {
398+
return arity * (i - 1) + 2;
399+
}
400+
364401
/**
365402
* This method returns the internal heap array as Object[].
366403
*

lucene/core/src/test/org/apache/lucene/util/TestPriorityQueue.java

Lines changed: 52 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import java.util.HashMap;
2727
import java.util.Iterator;
2828
import java.util.List;
29+
import java.util.Map;
2930
import java.util.NoSuchElementException;
3031
import java.util.Random;
3132
import org.apache.lucene.tests.util.LuceneTestCase;
@@ -50,6 +51,12 @@ protected final void checkValidity() {
5051
}
5152
}
5253

54+
private static class TernaryIntegerQueue extends PriorityQueue<Integer> {
55+
public TernaryIntegerQueue(int count) {
56+
super(count, 3, (a, b) -> a < b);
57+
}
58+
}
59+
5360
public void testZeroSizedQueue() {
5461
PriorityQueue<Integer> pq = new IntegerQueue(0);
5562
assertEquals((Object) 1, pq.insertWithOverflow(1));
@@ -98,6 +105,11 @@ public void testComparatorPQ() throws Exception {
98105
testPQ(PriorityQueue.usingComparator(size, Integer::compareTo), size, random());
99106
}
100107

108+
public void testTernaryPriorityQueue() {
109+
int size = atLeast(10000);
110+
testPQ(new TernaryIntegerQueue(size), size, random());
111+
}
112+
101113
public static void testPQ(PriorityQueue<Integer> pq, int count, Random gen) {
102114
int sum = 0, sum2 = 0;
103115

@@ -196,20 +208,16 @@ public void testAddAllWithStreamNotFitIntoQueue() {
196208
}
197209

198210
private boolean assertHeap(PriorityQueue<Integer> pq) {
211+
return assertHeap(pq, 2);
212+
}
213+
214+
private boolean assertHeap(PriorityQueue<Integer> pq, int arity) {
199215
Object[] heapArray = pq.getHeapArray();
200-
// The loop goes down to 1 as heap is 1-based not 0-based.
201-
for (int i = (heapArray.length >>> 1); i >= 1; i--) {
202-
int left = i << 1;
203-
int right = left + 1;
204-
if (right < heapArray.length) {
205-
if ((Integer) heapArray[i] > (Integer) heapArray[right]) {
206-
return false;
207-
}
208-
if ((Integer) heapArray[i] > (Integer) heapArray[left]) {
209-
return false;
210-
}
211-
} else if (left < heapArray.length) {
212-
if ((Integer) heapArray[i] > (Integer) heapArray[left]) {
216+
for (int i = 1; i <= pq.size(); i++) {
217+
int firstChild = arity * (i - 1) +2;
218+
int lastChild = Math.min(firstChild + arity - 1, pq.size());
219+
for (int child = firstChild; child <= lastChild; child++) {
220+
if ((Integer) heapArray[i] > (Integer) heapArray[child]) {
213221
return false;
214222
}
215223
}
@@ -307,6 +315,37 @@ public void testRandomAdditionsAgainstJavaPq() {
307315
pq.checkValidity();
308316
}
309317

318+
public void testRandomAdditionsAgainstJavaPriorityTernaryHeap() {
319+
int maxElement = RandomNumbers.randomIntBetween(random(), 1, 500);
320+
int size = maxElement / 2 + 1;
321+
322+
var reference = new java.util.PriorityQueue<Integer>();
323+
var pq = new TernaryIntegerQueue(size);
324+
325+
Random localRandom = nonAssertingRandom(random());
326+
327+
Map<Integer, Integer> ints = new HashMap<>();
328+
for (int i = 0, iters = size * 2; i < iters; i++) {
329+
Integer element = ints.computeIfAbsent(localRandom.nextInt(maxElement), k -> k);
330+
331+
var dropped = pq.insertWithOverflow(element);
332+
333+
reference.add(element);
334+
Integer droppedReference;
335+
if (reference.size() > size) {
336+
droppedReference = reference.remove();
337+
} else {
338+
droppedReference = null;
339+
}
340+
341+
assertEquals("insertWithOverflow() difference.", dropped, droppedReference);
342+
assertEquals("insertWithOverflow() size difference?", reference.size(), pq.size());
343+
assertEquals("top() difference?", reference.peek(), pq.top());
344+
}
345+
346+
assertTrue(assertHeap(pq, 3));
347+
}
348+
310349
public void testIteratorEmpty() {
311350
IntegerQueue queue = new IntegerQueue(3);
312351

0 commit comments

Comments
 (0)