Skip to content

Commit 1e7045d

Browse files
author
Tyler Rockwood
committed
Fix a bug with distinct and limit.
The Priority queue tried to keep a limit by assuming that each node would result in at least one result. This isn't true as a node's results could in theory *all* have distinct items that are already in the result set. This bug likely doesn't really happen in practice because maxWidth is larger than the limit. BENCHMARK RESULTS (effectively no change): Trie#populate x 8.32 ops/sec ±7.28% (25 runs sampled) Trie#lookupPrefix(t, 5) x 39,572,343 ops/sec ±0.91% (91 runs sampled) Trie#lookupPrefix(tee, 3) x 40,466,378 ops/sec ±0.86% (95 runs sampled) Trie#lookupPrefix(tee, 6) x 39,473,421 ops/sec ±0.80% (91 runs sampled)
1 parent 1399dde commit 1e7045d

File tree

4 files changed

+40
-35
lines changed

4 files changed

+40
-35
lines changed

src/index.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import Node from './node';
2-
import PQueue from './pqueue';
32
import { Item, SearchOptions } from './common';
43

54
interface TrieOptions {
@@ -78,7 +77,7 @@ class Trie<T> {
7877
limit: opts?.limit ?? Number.POSITIVE_INFINITY,
7978
};
8079

81-
node.getSortedResults(prefix, results, options, new PQueue(options.limit));
80+
node.getSortedResults(prefix, results, options);
8281

8382
return results;
8483
}

src/node.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,6 @@ class Node<T> {
196196
prefix: string,
197197
results: Array<T>,
198198
opts: SearchOptions,
199-
pqueue: PQueue<T>
200199
) {
201200
const seenKeys = new Set<string>();
202201

@@ -218,7 +217,8 @@ class Node<T> {
218217
}
219218
}
220219
} else {
221-
pqueue.addList([this]);
220+
const pqueue = new PQueue<T>();
221+
pqueue.addAll([this]);
222222

223223
let next: Node<T> | Item<T> | undefined;
224224
while ((next = pqueue.pop())) {
@@ -228,10 +228,10 @@ class Node<T> {
228228
}
229229

230230
if (next.leaf) {
231-
pqueue.addList(next.values as Array<Item<T>>);
231+
pqueue.addAll(next.values as Array<Item<T>>);
232232
} else {
233233
const children = next.children;
234-
pqueue.addList((next.values as Letter[]).map(v => children[v]));
234+
pqueue.addAll((next.values as Letter[]).map(v => children[v]));
235235
}
236236
} else {
237237
if (!opts.unique || !seenKeys.has(next.distinct || next.key)) {

src/pqueue.ts

Lines changed: 12 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,45 +1,28 @@
11
import Node from './node';
22
import { Item } from './common';
33

4-
/** A PQueue with a limited size. */
4+
/**
5+
* A standard priority queue.
6+
*
7+
* TODO(perf): We should probably have some sort of max heap solution and lazily merge the lists instead of eagerly
8+
* doing it. In the case of high maxWidth and low limits (likely) it's probably faster.
9+
*/
510
class PQueue<T> {
611
private readonly todo: Array<Node<T> | Item<T>> = [];
7-
constructor(private readonly limit: number) {}
812

9-
addList(list: Array<Node<T> | Item<T>>): void {
10-
let i = 0,
11-
j = 0;
13+
/** Add a sorted list of "score"-ables to the priority queue. */
14+
addAll(list: Array<Node<T> | Item<T>>): void {
15+
let j = 0;
1216

13-
// effectiveLength is the lower bound on the number of
14-
// item's we're guaranteed to be able to find in the trie.
15-
// In the case that unique is false this is the same as the length,
16-
// but in the case unique is true, it's the number of Nodes in the queue
17-
// (as items may be discarded).
18-
let effectiveLength = 0;
19-
20-
while (i < this.todo.length && effectiveLength < this.limit) {
21-
if (j < list.length && this.todo[i].score < list[j].score) {
17+
for (let i = 0; i < this.todo.length && j < list.length; ++i) {
18+
if (this.todo[i].score < list[j].score) {
2219
this.todo.splice(i, 0, list[j]);
2320
j += 1;
2421
}
25-
26-
if (this.todo[i] instanceof Node) {
27-
effectiveLength += 1;
28-
}
29-
30-
i += 1;
3122
}
3223

33-
while (this.todo.length > i) {
34-
this.todo.pop();
35-
}
36-
37-
while (effectiveLength < this.limit && j < list.length) {
24+
for (; j < list.length; ++j) {
3825
this.todo.push(list[j]);
39-
if (list[j] instanceof Node) {
40-
effectiveLength += 1;
41-
}
42-
j += 1;
4326
}
4427
}
4528

test/trie.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -293,4 +293,27 @@ describe('Trie', () => {
293293
expect(t.prefixSearch('')).toEqual(['a', 'b']);
294294
t.remove('a');
295295
});
296+
297+
it('distinct limit bug', () => {
298+
const t = new StrictTrie({maxWidth: 1});
299+
t.add({
300+
key: 'a',
301+
score: 1,
302+
value: 'a',
303+
distinct: '1',
304+
});
305+
t.add({
306+
key: 'b',
307+
score: 1,
308+
value: 'b',
309+
distinct: '1',
310+
});
311+
t.add({
312+
key: 'c',
313+
score: 1,
314+
value: 'c',
315+
distinct: '2',
316+
});
317+
expect(t.prefixSearch("", {limit: 2, unique: true})).toEqual(['a', 'c']);
318+
})
296319
});

0 commit comments

Comments
 (0)