@tanstack/db-ivm 0.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -0
- package/package.json +63 -0
- package/src/d2.ts +150 -0
- package/src/graph.ts +131 -0
- package/src/index.ts +4 -0
- package/src/indexes.ts +117 -0
- package/src/multiset.ts +215 -0
- package/src/operators/concat.ts +45 -0
- package/src/operators/consolidate.ts +50 -0
- package/src/operators/count.ts +52 -0
- package/src/operators/debug.ts +58 -0
- package/src/operators/distinct.ts +98 -0
- package/src/operators/filter.ts +48 -0
- package/src/operators/filterBy.ts +31 -0
- package/src/operators/groupBy.ts +354 -0
- package/src/operators/index.ts +18 -0
- package/src/operators/join.ts +265 -0
- package/src/operators/keying.ts +34 -0
- package/src/operators/map.ts +48 -0
- package/src/operators/negate.ts +33 -0
- package/src/operators/orderBy.ts +213 -0
- package/src/operators/orderByBTree.ts +20 -0
- package/src/operators/output.ts +53 -0
- package/src/operators/pipe.ts +55 -0
- package/src/operators/reduce.ts +124 -0
- package/src/operators/topK.ts +92 -0
- package/src/operators/topKWithFractionalIndex.ts +347 -0
- package/src/operators/topKWithFractionalIndexBTree.ts +306 -0
- package/src/types.ts +376 -0
- package/src/utils.ts +163 -0
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
import { generateKeyBetween } from "fractional-indexing"
|
|
2
|
+
import { DifferenceStreamWriter, UnaryOperator } from "../graph.js"
|
|
3
|
+
import { StreamBuilder } from "../d2.js"
|
|
4
|
+
import { MultiSet } from "../multiset.js"
|
|
5
|
+
import { Index } from "../indexes.js"
|
|
6
|
+
import { binarySearch, globalObjectIdGenerator } from "../utils.js"
|
|
7
|
+
import type { DifferenceStreamReader } from "../graph.js"
|
|
8
|
+
import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
|
|
9
|
+
|
|
10
|
+
export interface TopKWithFractionalIndexOptions {
|
|
11
|
+
limit?: number
|
|
12
|
+
offset?: number
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export type TopKChanges<V> = {
|
|
16
|
+
/** Indicates which element moves into the topK (if any) */
|
|
17
|
+
moveIn: IndexedValue<V> | null
|
|
18
|
+
/** Indicates which element moves out of the topK (if any) */
|
|
19
|
+
moveOut: IndexedValue<V> | null
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* A topK data structure that supports insertions and deletions
|
|
24
|
+
* and returns changes to the topK.
|
|
25
|
+
*/
|
|
26
|
+
export interface TopK<V> {
|
|
27
|
+
insert: (value: V) => TopKChanges<V>
|
|
28
|
+
delete: (value: V) => TopKChanges<V>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Implementation of a topK data structure.
|
|
33
|
+
* Uses a sorted array internally to store the values and keeps a topK window over that array.
|
|
34
|
+
* Inserts and deletes are O(n) operations because worst case an element is inserted/deleted
|
|
35
|
+
* at the start of the array which causes all the elements to shift to the right/left.
|
|
36
|
+
*/
|
|
37
|
+
class TopKArray<V> implements TopK<V> {
|
|
38
|
+
#sortedValues: Array<IndexedValue<V>> = []
|
|
39
|
+
#comparator: (a: V, b: V) => number
|
|
40
|
+
#topKStart: number
|
|
41
|
+
#topKEnd: number
|
|
42
|
+
|
|
43
|
+
constructor(
|
|
44
|
+
offset: number,
|
|
45
|
+
limit: number,
|
|
46
|
+
comparator: (a: V, b: V) => number
|
|
47
|
+
) {
|
|
48
|
+
this.#topKStart = offset
|
|
49
|
+
this.#topKEnd = offset + limit
|
|
50
|
+
this.#comparator = comparator
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
insert(value: V): TopKChanges<V> {
|
|
54
|
+
const result: TopKChanges<V> = { moveIn: null, moveOut: null }
|
|
55
|
+
|
|
56
|
+
// Lookup insert position
|
|
57
|
+
const index = this.#findIndex(value)
|
|
58
|
+
// Generate fractional index based on the fractional indices of the elements before and after it
|
|
59
|
+
const indexBefore =
|
|
60
|
+
index === 0 ? null : getIndex(this.#sortedValues[index - 1]!)
|
|
61
|
+
const indexAfter =
|
|
62
|
+
index === this.#sortedValues.length
|
|
63
|
+
? null
|
|
64
|
+
: getIndex(this.#sortedValues[index]!)
|
|
65
|
+
const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)
|
|
66
|
+
|
|
67
|
+
// Insert the value at the correct position
|
|
68
|
+
const val = indexedValue(value, fractionalIndex)
|
|
69
|
+
// Splice is O(n) where n = all elements in the collection (i.e. n >= k) !
|
|
70
|
+
this.#sortedValues.splice(index, 0, val)
|
|
71
|
+
|
|
72
|
+
// Check if the topK changed
|
|
73
|
+
if (index < this.#topKEnd) {
|
|
74
|
+
// The inserted element is either before the top K or within the top K
|
|
75
|
+
// If it is before the top K then it moves the element that was right before the topK into the topK
|
|
76
|
+
// If it is within the top K then the inserted element moves into the top K
|
|
77
|
+
// In both cases the last element of the old top K now moves out of the top K
|
|
78
|
+
const moveInIndex = Math.max(index, this.#topKStart)
|
|
79
|
+
if (moveInIndex < this.#sortedValues.length) {
|
|
80
|
+
// We actually have a topK
|
|
81
|
+
// because in some cases there may not be enough elements in the array to reach the start of the topK
|
|
82
|
+
// e.g. [1, 2, 3] with K = 2 and offset = 3 does not have a topK
|
|
83
|
+
result.moveIn = this.#sortedValues[moveInIndex]!
|
|
84
|
+
|
|
85
|
+
// We need to remove the element that falls out of the top K
|
|
86
|
+
// The element that falls out of the top K has shifted one to the right
|
|
87
|
+
// because of the element we inserted, so we find it at index topKEnd
|
|
88
|
+
if (this.#topKEnd < this.#sortedValues.length) {
|
|
89
|
+
result.moveOut = this.#sortedValues[this.#topKEnd]!
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return result
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Deletes a value that may or may not be in the topK.
|
|
99
|
+
* IMPORTANT: this assumes that the value is present in the collection
|
|
100
|
+
* if it's not the case it will remove the element
|
|
101
|
+
* that is on the position where the provided `value` would be.
|
|
102
|
+
*/
|
|
103
|
+
delete(value: V): TopKChanges<V> {
|
|
104
|
+
const result: TopKChanges<V> = { moveIn: null, moveOut: null }
|
|
105
|
+
|
|
106
|
+
// Lookup delete position
|
|
107
|
+
const index = this.#findIndex(value)
|
|
108
|
+
// Remove the value at that position
|
|
109
|
+
const [removedElem] = this.#sortedValues.splice(index, 1)
|
|
110
|
+
|
|
111
|
+
// Check if the topK changed
|
|
112
|
+
if (index < this.#topKEnd) {
|
|
113
|
+
// The removed element is either before the top K or within the top K
|
|
114
|
+
// If it is before the top K then the first element of the topK moves out of the topK
|
|
115
|
+
// If it is within the top K then the removed element moves out of the topK
|
|
116
|
+
result.moveOut = removedElem!
|
|
117
|
+
if (index < this.#topKStart) {
|
|
118
|
+
// The removed element is before the topK
|
|
119
|
+
// so actually, the first element of the topK moves out of the topK
|
|
120
|
+
// and not the element that we removed
|
|
121
|
+
// The first element of the topK is now at index topKStart - 1
|
|
122
|
+
// since we removed an element before the topK
|
|
123
|
+
const moveOutIndex = this.#topKStart - 1
|
|
124
|
+
if (moveOutIndex < this.#sortedValues.length) {
|
|
125
|
+
result.moveOut = this.#sortedValues[moveOutIndex]!
|
|
126
|
+
} else {
|
|
127
|
+
// No value is moving out of the topK
|
|
128
|
+
// because there are no elements in the topK
|
|
129
|
+
result.moveOut = null
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Since we removed an element that was before or in the topK
|
|
134
|
+
// the first element after the topK moved one position to the left
|
|
135
|
+
// and thus falls into the topK now
|
|
136
|
+
const moveInIndex = this.#topKEnd - 1
|
|
137
|
+
if (moveInIndex < this.#sortedValues.length) {
|
|
138
|
+
result.moveIn = this.#sortedValues[moveInIndex]!
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return result
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// TODO: see if there is a way to refactor the code for insert and delete in the topK above
|
|
146
|
+
// because they are very similar, one is shifting the topK window to the left and the other is shifting it to the right
|
|
147
|
+
// so i have the feeling there is a common pattern here and we can implement both cases using that pattern
|
|
148
|
+
|
|
149
|
+
#findIndex(value: V): number {
|
|
150
|
+
return binarySearch(this.#sortedValues, indexedValue(value, ``), (a, b) =>
|
|
151
|
+
this.#comparator(getValue(a), getValue(b))
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Operator for fractional indexed topK operations
|
|
158
|
+
* This operator maintains fractional indices for sorted elements
|
|
159
|
+
* and only updates indices when elements move position
|
|
160
|
+
*/
|
|
161
|
+
export class TopKWithFractionalIndexOperator<K, V1> extends UnaryOperator<
|
|
162
|
+
[K, V1],
|
|
163
|
+
[K, IndexedValue<V1>]
|
|
164
|
+
> {
|
|
165
|
+
#index = new Index<K, V1>()
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* topK data structure that supports insertions and deletions
|
|
169
|
+
* and returns changes to the topK.
|
|
170
|
+
*/
|
|
171
|
+
#topK: TopK<TaggedValue<V1>>
|
|
172
|
+
|
|
173
|
+
constructor(
|
|
174
|
+
id: number,
|
|
175
|
+
inputA: DifferenceStreamReader<[K, V1]>,
|
|
176
|
+
output: DifferenceStreamWriter<[K, [V1, string]]>,
|
|
177
|
+
comparator: (a: V1, b: V1) => number,
|
|
178
|
+
options: TopKWithFractionalIndexOptions
|
|
179
|
+
) {
|
|
180
|
+
super(id, inputA, output)
|
|
181
|
+
const limit = options.limit ?? Infinity
|
|
182
|
+
const offset = options.offset ?? 0
|
|
183
|
+
const compareTaggedValues = (a: TaggedValue<V1>, b: TaggedValue<V1>) => {
|
|
184
|
+
// First compare on the value
|
|
185
|
+
const valueComparison = comparator(untagValue(a), untagValue(b))
|
|
186
|
+
if (valueComparison !== 0) {
|
|
187
|
+
return valueComparison
|
|
188
|
+
}
|
|
189
|
+
// If the values are equal, compare on the tag (object identity)
|
|
190
|
+
const tieBreakerA = getTag(a)
|
|
191
|
+
const tieBreakerB = getTag(b)
|
|
192
|
+
return tieBreakerA - tieBreakerB
|
|
193
|
+
}
|
|
194
|
+
this.#topK = this.createTopK(offset, limit, compareTaggedValues)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
protected createTopK(
|
|
198
|
+
offset: number,
|
|
199
|
+
limit: number,
|
|
200
|
+
comparator: (a: TaggedValue<V1>, b: TaggedValue<V1>) => number
|
|
201
|
+
): TopK<TaggedValue<V1>> {
|
|
202
|
+
return new TopKArray(offset, limit, comparator)
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
run(): void {
|
|
206
|
+
const result: Array<[[K, [V1, string]], number]> = []
|
|
207
|
+
for (const message of this.inputMessages()) {
|
|
208
|
+
for (const [item, multiplicity] of message.getInner()) {
|
|
209
|
+
const [key, value] = item
|
|
210
|
+
this.processElement(key, value, multiplicity, result)
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (result.length > 0) {
|
|
215
|
+
this.output.sendData(new MultiSet(result))
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
processElement(
|
|
220
|
+
key: K,
|
|
221
|
+
value: V1,
|
|
222
|
+
multiplicity: number,
|
|
223
|
+
result: Array<[[K, [V1, string]], number]>
|
|
224
|
+
): void {
|
|
225
|
+
const oldMultiplicity = this.#index.getMultiplicity(key, value)
|
|
226
|
+
this.#index.addValue(key, [value, multiplicity])
|
|
227
|
+
const newMultiplicity = this.#index.getMultiplicity(key, value)
|
|
228
|
+
|
|
229
|
+
let res: TopKChanges<TaggedValue<V1>> = {
|
|
230
|
+
moveIn: null,
|
|
231
|
+
moveOut: null,
|
|
232
|
+
}
|
|
233
|
+
if (oldMultiplicity <= 0 && newMultiplicity > 0) {
|
|
234
|
+
// The value was invisible but should now be visible
|
|
235
|
+
// Need to insert it into the array of sorted values
|
|
236
|
+
const taggedValue = tagValue(value)
|
|
237
|
+
res = this.#topK.insert(taggedValue)
|
|
238
|
+
} else if (oldMultiplicity > 0 && newMultiplicity <= 0) {
|
|
239
|
+
// The value was visible but should now be invisible
|
|
240
|
+
// Need to remove it from the array of sorted values
|
|
241
|
+
const taggedValue = tagValue(value)
|
|
242
|
+
res = this.#topK.delete(taggedValue)
|
|
243
|
+
} else {
|
|
244
|
+
// The value was invisible and it remains invisible
|
|
245
|
+
// or it was visible and remains visible
|
|
246
|
+
// so it doesn't affect the topK
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (res.moveIn) {
|
|
250
|
+
const valueWithoutTieBreaker = mapValue(res.moveIn, untagValue)
|
|
251
|
+
result.push([[key, valueWithoutTieBreaker], 1])
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (res.moveOut) {
|
|
255
|
+
const valueWithoutTieBreaker = mapValue(res.moveOut, untagValue)
|
|
256
|
+
result.push([[key, valueWithoutTieBreaker], -1])
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Limits the number of results based on a comparator, with optional offset.
|
|
265
|
+
* This works on a keyed stream, where the key is the first element of the tuple.
|
|
266
|
+
* The ordering is within a key group, i.e. elements are sorted within a key group
|
|
267
|
+
* and the limit + offset is applied to that sorted group.
|
|
268
|
+
* To order the entire stream, key by the same value for all elements such as null.
|
|
269
|
+
*
|
|
270
|
+
* Uses fractional indexing to minimize the number of changes when elements move positions.
|
|
271
|
+
* Each element is assigned a fractional index that is lexicographically sortable.
|
|
272
|
+
* When elements move, only the indices of the moved elements are updated, not all elements.
|
|
273
|
+
*
|
|
274
|
+
* @param comparator - A function that compares two elements
|
|
275
|
+
* @param options - An optional object containing limit and offset properties
|
|
276
|
+
* @returns A piped operator that orders the elements and limits the number of results
|
|
277
|
+
*/
|
|
278
|
+
export function topKWithFractionalIndex<
|
|
279
|
+
KType extends T extends KeyValue<infer K, infer _V> ? K : never,
|
|
280
|
+
V1Type extends T extends KeyValue<KType, infer V> ? V : never,
|
|
281
|
+
T,
|
|
282
|
+
>(
|
|
283
|
+
comparator: (a: V1Type, b: V1Type) => number,
|
|
284
|
+
options?: TopKWithFractionalIndexOptions
|
|
285
|
+
): PipedOperator<T, KeyValue<KType, [V1Type, string]>> {
|
|
286
|
+
const opts = options || {}
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
stream: IStreamBuilder<T>
|
|
290
|
+
): IStreamBuilder<KeyValue<KType, [V1Type, string]>> => {
|
|
291
|
+
const output = new StreamBuilder<KeyValue<KType, [V1Type, string]>>(
|
|
292
|
+
stream.graph,
|
|
293
|
+
new DifferenceStreamWriter<KeyValue<KType, [V1Type, string]>>()
|
|
294
|
+
)
|
|
295
|
+
const operator = new TopKWithFractionalIndexOperator<KType, V1Type>(
|
|
296
|
+
stream.graph.getNextOperatorId(),
|
|
297
|
+
stream.connectReader() as DifferenceStreamReader<KeyValue<KType, V1Type>>,
|
|
298
|
+
output.writer,
|
|
299
|
+
comparator,
|
|
300
|
+
opts
|
|
301
|
+
)
|
|
302
|
+
stream.graph.addOperator(operator)
|
|
303
|
+
stream.graph.addStream(output.connectReader())
|
|
304
|
+
return output
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Abstraction for fractionally indexed values
|
|
309
|
+
export type FractionalIndex = string
|
|
310
|
+
export type IndexedValue<V> = [V, FractionalIndex]
|
|
311
|
+
|
|
312
|
+
export function indexedValue<V>(
|
|
313
|
+
value: V,
|
|
314
|
+
index: FractionalIndex
|
|
315
|
+
): IndexedValue<V> {
|
|
316
|
+
return [value, index]
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
export function getValue<V>(indexedVal: IndexedValue<V>): V {
|
|
320
|
+
return indexedVal[0]
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
export function getIndex<V>(indexedVal: IndexedValue<V>): FractionalIndex {
|
|
324
|
+
return indexedVal[1]
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function mapValue<V, W>(
|
|
328
|
+
indexedVal: IndexedValue<V>,
|
|
329
|
+
f: (value: V) => W
|
|
330
|
+
): IndexedValue<W> {
|
|
331
|
+
return [f(getValue(indexedVal)), getIndex(indexedVal)]
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export type Tag = number
|
|
335
|
+
export type TaggedValue<V> = [V, Tag]
|
|
336
|
+
|
|
337
|
+
function tagValue<V>(value: V): TaggedValue<V> {
|
|
338
|
+
return [value, globalObjectIdGenerator.getId(value)]
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function untagValue<V>(tieBreakerTaggedValue: TaggedValue<V>): V {
|
|
342
|
+
return tieBreakerTaggedValue[0]
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function getTag<V>(tieBreakerTaggedValue: TaggedValue<V>): Tag {
|
|
346
|
+
return tieBreakerTaggedValue[1]
|
|
347
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { generateKeyBetween } from "fractional-indexing"
|
|
2
|
+
import { DifferenceStreamWriter } from "../graph.js"
|
|
3
|
+
import { StreamBuilder } from "../d2.js"
|
|
4
|
+
import {
|
|
5
|
+
TopKWithFractionalIndexOperator,
|
|
6
|
+
getIndex,
|
|
7
|
+
getValue,
|
|
8
|
+
indexedValue,
|
|
9
|
+
} from "./topKWithFractionalIndex.js"
|
|
10
|
+
import type { IStreamBuilder, KeyValue, PipedOperator } from "../types.js"
|
|
11
|
+
import type { DifferenceStreamReader } from "../graph.js"
|
|
12
|
+
import type {
|
|
13
|
+
IndexedValue,
|
|
14
|
+
TaggedValue,
|
|
15
|
+
TopK,
|
|
16
|
+
TopKChanges,
|
|
17
|
+
TopKWithFractionalIndexOptions,
|
|
18
|
+
} from "./topKWithFractionalIndex.js"
|
|
19
|
+
|
|
20
|
+
interface BTree<Key, Value> {
|
|
21
|
+
nextLowerPair: (key: Key) => [Key, Value] | undefined
|
|
22
|
+
nextHigherPair: (key: Key) => [Key, Value] | undefined
|
|
23
|
+
set: (key: Key, value: Value, overwrite?: boolean) => boolean
|
|
24
|
+
maxKey: () => Key | undefined
|
|
25
|
+
get: (key: Key, defaultValue?: Value) => Value | undefined
|
|
26
|
+
delete: (key: Key) => boolean
|
|
27
|
+
size: number
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
interface BTreeClass {
|
|
31
|
+
new <Key, Value>(
|
|
32
|
+
entries?: Array<[Key, Value]>,
|
|
33
|
+
compare?: (a: Key, b: Key) => number,
|
|
34
|
+
maxNodeSize?: number
|
|
35
|
+
): BTree<Key, Value>
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let BTree: BTreeClass | undefined
|
|
39
|
+
|
|
40
|
+
export async function loadBTree() {
|
|
41
|
+
if (BTree === undefined) {
|
|
42
|
+
const { default: _BTreeClass } = await import(`sorted-btree`)
|
|
43
|
+
BTree = _BTreeClass
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Implementation of a topK data structure that uses a B+ tree.
|
|
49
|
+
* The tree allows for logarithmic time insertions and deletions.
|
|
50
|
+
*/
|
|
51
|
+
class TopKTree<V> implements TopK<V> {
|
|
52
|
+
#comparator: (a: V, b: V) => number
|
|
53
|
+
// topK is a window at position [topKStart, topKEnd[
|
|
54
|
+
// i.e. `topKStart` is inclusive and `topKEnd` is exclusive
|
|
55
|
+
#topKStart: number
|
|
56
|
+
#topKEnd: number
|
|
57
|
+
|
|
58
|
+
#tree: BTree<V, IndexedValue<V>>
|
|
59
|
+
#topKFirstElem: IndexedValue<V> | null = null // inclusive
|
|
60
|
+
#topKLastElem: IndexedValue<V> | null = null // inclusive
|
|
61
|
+
|
|
62
|
+
constructor(
|
|
63
|
+
offset: number,
|
|
64
|
+
limit: number,
|
|
65
|
+
comparator: (a: V, b: V) => number
|
|
66
|
+
) {
|
|
67
|
+
if (BTree === undefined) {
|
|
68
|
+
throw new Error(
|
|
69
|
+
`B+ tree not loaded. You need to call loadBTree() before using TopKTree.`
|
|
70
|
+
)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.#topKStart = offset
|
|
74
|
+
this.#topKEnd = offset + limit
|
|
75
|
+
this.#comparator = comparator
|
|
76
|
+
this.#tree = new BTree(undefined, comparator)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Insert a *new* value.
|
|
81
|
+
* Ignores the value if it is already present.
|
|
82
|
+
*/
|
|
83
|
+
insert(value: V): TopKChanges<V> {
|
|
84
|
+
const result: TopKChanges<V> = { moveIn: null, moveOut: null }
|
|
85
|
+
|
|
86
|
+
// Get the elements before and after the value
|
|
87
|
+
const [, indexedValueBefore] = this.#tree.nextLowerPair(value) ?? [
|
|
88
|
+
null,
|
|
89
|
+
null,
|
|
90
|
+
]
|
|
91
|
+
const [, indexedValueAfter] = this.#tree.nextHigherPair(value) ?? [
|
|
92
|
+
null,
|
|
93
|
+
null,
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
const indexBefore = indexedValueBefore ? getIndex(indexedValueBefore) : null
|
|
97
|
+
const indexAfter = indexedValueAfter ? getIndex(indexedValueAfter) : null
|
|
98
|
+
|
|
99
|
+
// Generate a fractional index for the value
|
|
100
|
+
// based on the fractional indices of the elements before and after it
|
|
101
|
+
const fractionalIndex = generateKeyBetween(indexBefore, indexAfter)
|
|
102
|
+
const insertedElem = indexedValue(value, fractionalIndex)
|
|
103
|
+
|
|
104
|
+
// Insert the value into the tree
|
|
105
|
+
const inserted = this.#tree.set(value, insertedElem, false)
|
|
106
|
+
if (!inserted) {
|
|
107
|
+
// The value was already present in the tree
|
|
108
|
+
// ignore this insertions since we don't support overwrites!
|
|
109
|
+
return result
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (this.#tree.size - 1 < this.#topKStart) {
|
|
113
|
+
// We don't have a topK yet
|
|
114
|
+
// so we don't need to do anything
|
|
115
|
+
return result
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (this.#topKFirstElem) {
|
|
119
|
+
// We have a topK containing at least 1 element
|
|
120
|
+
if (this.#comparator(value, getValue(this.#topKFirstElem)) < 0) {
|
|
121
|
+
// The element was inserted before the topK
|
|
122
|
+
// so it moves the element that is right before the topK into the topK
|
|
123
|
+
const firstElem = getValue(this.#topKFirstElem)
|
|
124
|
+
const [, newFirstElem] = this.#tree.nextLowerPair(firstElem)!
|
|
125
|
+
this.#topKFirstElem = newFirstElem
|
|
126
|
+
result.moveIn = this.#topKFirstElem
|
|
127
|
+
} else if (
|
|
128
|
+
!this.#topKLastElem ||
|
|
129
|
+
this.#comparator(value, getValue(this.#topKLastElem)) < 0
|
|
130
|
+
) {
|
|
131
|
+
// The element was inserted within the topK
|
|
132
|
+
result.moveIn = insertedElem
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
this.#topKLastElem &&
|
|
137
|
+
this.#comparator(value, getValue(this.#topKLastElem)) < 0
|
|
138
|
+
) {
|
|
139
|
+
// The element was inserted before or within the topK
|
|
140
|
+
// the newly inserted element pushes the last element of the topK out of the topK
|
|
141
|
+
// so the one before that becomes the new last element of the topK
|
|
142
|
+
const lastElem = this.#topKLastElem
|
|
143
|
+
const lastValue = getValue(lastElem)
|
|
144
|
+
const [, newLastElem] = this.#tree.nextLowerPair(lastValue)!
|
|
145
|
+
this.#topKLastElem = newLastElem
|
|
146
|
+
result.moveOut = lastElem
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// If the tree has as many elements as the offset (i.e. #topKStart)
|
|
151
|
+
// then the insertion shifted the elements 1 position to the right
|
|
152
|
+
// and the last element in the tree is now the first element of the topK
|
|
153
|
+
if (this.#tree.size - 1 === this.#topKStart) {
|
|
154
|
+
const topKFirstKey = this.#tree.maxKey()!
|
|
155
|
+
this.#topKFirstElem = this.#tree.get(topKFirstKey)!
|
|
156
|
+
result.moveIn = this.#topKFirstElem
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// By inserting this new element we now have a complete topK
|
|
160
|
+
// store the last element of the topK
|
|
161
|
+
if (this.#tree.size === this.#topKEnd) {
|
|
162
|
+
const topKLastKey = this.#tree.maxKey()!
|
|
163
|
+
this.#topKLastElem = this.#tree.get(topKLastKey)!
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return result
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
delete(value: V): TopKChanges<V> {
|
|
170
|
+
const result: TopKChanges<V> = { moveIn: null, moveOut: null }
|
|
171
|
+
|
|
172
|
+
const deletedElem = this.#tree.get(value)
|
|
173
|
+
const deleted = this.#tree.delete(value)
|
|
174
|
+
if (!deleted) {
|
|
175
|
+
return result
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (!this.#topKFirstElem) {
|
|
179
|
+
// We didn't have a topK before the delete
|
|
180
|
+
// so we still can't have a topK after the delete
|
|
181
|
+
return result
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (this.#comparator(value, getValue(this.#topKFirstElem)) < 0) {
|
|
185
|
+
// We deleted an element that was before the topK
|
|
186
|
+
// so the topK has shifted one position to the left
|
|
187
|
+
|
|
188
|
+
// the old first element moves out of the topK
|
|
189
|
+
result.moveOut = this.#topKFirstElem
|
|
190
|
+
// the element that was right after the first element of the topK
|
|
191
|
+
// is now the new first element of the topK
|
|
192
|
+
const firstElem = getValue(this.#topKFirstElem)
|
|
193
|
+
const [, newFirstElem] = this.#tree.nextHigherPair(firstElem) ?? [
|
|
194
|
+
null,
|
|
195
|
+
null,
|
|
196
|
+
]
|
|
197
|
+
this.#topKFirstElem = newFirstElem
|
|
198
|
+
} else if (
|
|
199
|
+
!this.#topKLastElem ||
|
|
200
|
+
// TODO: if on equal order the element is inserted *after* the already existing one
|
|
201
|
+
// then this check should become < 0
|
|
202
|
+
this.#comparator(value, getValue(this.#topKLastElem)) <= 0
|
|
203
|
+
) {
|
|
204
|
+
// The element we deleted was within the topK
|
|
205
|
+
// so we need to signal that that element is no longer in the topK
|
|
206
|
+
result.moveOut = deletedElem!
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
if (
|
|
210
|
+
this.#topKLastElem &&
|
|
211
|
+
// TODO: if on equal order the element is inserted *after* the already existing one
|
|
212
|
+
// then this check should become < 0
|
|
213
|
+
this.#comparator(value, getValue(this.#topKLastElem)) <= 0
|
|
214
|
+
) {
|
|
215
|
+
// The element we deleted was before or within the topK
|
|
216
|
+
// So the first element after the topK moved one position to the left
|
|
217
|
+
// and thus falls into the topK now
|
|
218
|
+
const lastElem = this.#topKLastElem
|
|
219
|
+
const lastValue = getValue(lastElem)
|
|
220
|
+
const [, newLastElem] = this.#tree.nextHigherPair(lastValue) ?? [
|
|
221
|
+
null,
|
|
222
|
+
null,
|
|
223
|
+
]
|
|
224
|
+
this.#topKLastElem = newLastElem
|
|
225
|
+
if (newLastElem) {
|
|
226
|
+
result.moveIn = newLastElem
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return result
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Operator for fractional indexed topK operations
|
|
236
|
+
* This operator maintains fractional indices for sorted elements
|
|
237
|
+
* and only updates indices when elements move position
|
|
238
|
+
*/
|
|
239
|
+
export class TopKWithFractionalIndexBTreeOperator<
|
|
240
|
+
K,
|
|
241
|
+
V1,
|
|
242
|
+
> extends TopKWithFractionalIndexOperator<K, V1> {
|
|
243
|
+
protected override createTopK(
|
|
244
|
+
offset: number,
|
|
245
|
+
limit: number,
|
|
246
|
+
comparator: (a: TaggedValue<V1>, b: TaggedValue<V1>) => number
|
|
247
|
+
): TopK<TaggedValue<V1>> {
|
|
248
|
+
if (BTree === undefined) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`B+ tree not loaded. You need to call loadBTree() before using TopKWithFractionalIndexBTreeOperator.`
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
return new TopKTree(offset, limit, comparator)
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Limits the number of results based on a comparator, with optional offset.
|
|
259
|
+
* This works on a keyed stream, where the key is the first element of the tuple.
|
|
260
|
+
* The ordering is within a key group, i.e. elements are sorted within a key group
|
|
261
|
+
* and the limit + offset is applied to that sorted group.
|
|
262
|
+
* To order the entire stream, key by the same value for all elements such as null.
|
|
263
|
+
*
|
|
264
|
+
* Uses fractional indexing to minimize the number of changes when elements move positions.
|
|
265
|
+
* Each element is assigned a fractional index that is lexicographically sortable.
|
|
266
|
+
* When elements move, only the indices of the moved elements are updated, not all elements.
|
|
267
|
+
*
|
|
268
|
+
* @param comparator - A function that compares two elements
|
|
269
|
+
* @param options - An optional object containing limit and offset properties
|
|
270
|
+
* @returns A piped operator that orders the elements and limits the number of results
|
|
271
|
+
*/
|
|
272
|
+
export function topKWithFractionalIndexBTree<
|
|
273
|
+
KType extends T extends KeyValue<infer K, infer _V> ? K : never,
|
|
274
|
+
V1Type extends T extends KeyValue<KType, infer V> ? V : never,
|
|
275
|
+
T,
|
|
276
|
+
>(
|
|
277
|
+
comparator: (a: V1Type, b: V1Type) => number,
|
|
278
|
+
options?: TopKWithFractionalIndexOptions
|
|
279
|
+
): PipedOperator<T, KeyValue<KType, [V1Type, string]>> {
|
|
280
|
+
const opts = options || {}
|
|
281
|
+
|
|
282
|
+
if (BTree === undefined) {
|
|
283
|
+
throw new Error(
|
|
284
|
+
`B+ tree not loaded. You need to call loadBTree() before using topKWithFractionalIndexBTree.`
|
|
285
|
+
)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return (
|
|
289
|
+
stream: IStreamBuilder<T>
|
|
290
|
+
): IStreamBuilder<KeyValue<KType, [V1Type, string]>> => {
|
|
291
|
+
const output = new StreamBuilder<KeyValue<KType, [V1Type, string]>>(
|
|
292
|
+
stream.graph,
|
|
293
|
+
new DifferenceStreamWriter<KeyValue<KType, [V1Type, string]>>()
|
|
294
|
+
)
|
|
295
|
+
const operator = new TopKWithFractionalIndexOperator<KType, V1Type>(
|
|
296
|
+
stream.graph.getNextOperatorId(),
|
|
297
|
+
stream.connectReader() as DifferenceStreamReader<KeyValue<KType, V1Type>>,
|
|
298
|
+
output.writer,
|
|
299
|
+
comparator,
|
|
300
|
+
opts
|
|
301
|
+
)
|
|
302
|
+
stream.graph.addOperator(operator)
|
|
303
|
+
stream.graph.addStream(output.connectReader())
|
|
304
|
+
return output
|
|
305
|
+
}
|
|
306
|
+
}
|