fix(8.0): column-store range queries honor exclusive bounds (lessThan/greaterThan)
getIdsForRange computed includeMin/includeMax correctly for every operator (gt→false/true, lt→true/false, between→true/true) but DROPPED them when the column store served the query — it called columnStore.rangeQuery(field,min,max) with no flags, and the column-store path was inclusive-only. So whenever a field lived in the column store, strict lessThan/greaterThan silently behaved as lte/gte. The sparse-fallback path already forwarded the flags, so this was column-store-only. Thread includeMin/includeMax end to end: - ColumnSegmentCursor: add textbook lowerBound/upperBound helpers and express binarySearchRange in terms of them. Inclusive/inclusive is byte-identical to the old impl (lowerBound(lo) == old binarySearchValue(lo).index; upperBound(hi) == old "first position after hi"). Exclusive lower advances past ALL duplicates of the boundary value; exclusive upper stops before them. - ColumnStore.rangeQuery: accept the flags, apply them in the segment cursor AND the tail-buffer linear scan (strict > / < when exclusive). A bound taken from a segment's own min/max stays inclusive — it is a real stored value. - VectorIndex/types interface + getIdsForRange call site updated. Surfaced by brainy-complete dual-bound test (year/popularity exclusive both ends → ['Express','Vue.js']; React@95 and Angular@75 correctly excluded). Added 5 column-store unit tests: exclusive lower, exclusive upper, both, duplicate boundary values, and the unflushed tail-buffer path. 1469 unit green.
This commit is contained in:
parent
d918f49287
commit
009e50681e
5 changed files with 185 additions and 37 deletions
|
|
@ -264,17 +264,42 @@ export class ColumnStore implements ColumnStoreProvider {
|
|||
}
|
||||
|
||||
/**
|
||||
* Range filter: find entities where field is within [min, max].
|
||||
* Range filter: find entities where field is within the bounds.
|
||||
*
|
||||
* @param field - Field name to filter on
|
||||
* @param min - Lower bound (undefined/null = unbounded below)
|
||||
* @param max - Upper bound (undefined/null = unbounded above)
|
||||
* @param includeMin - Include values equal to `min` (default: true). Lets
|
||||
* callers express strict `greaterThan` (exclusive lower) vs `gte`.
|
||||
* @param includeMax - Include values equal to `max` (default: true). Lets
|
||||
* callers express strict `lessThan` (exclusive upper) vs `lte`.
|
||||
*/
|
||||
async rangeQuery(field: string, min?: unknown, max?: unknown): Promise<RoaringBitmap32> {
|
||||
async rangeQuery(
|
||||
field: string,
|
||||
min?: unknown,
|
||||
max?: unknown,
|
||||
includeMin: boolean = true,
|
||||
includeMax: boolean = true
|
||||
): Promise<RoaringBitmap32> {
|
||||
const result = new RoaringBitmap32()
|
||||
const cursors = await this.getSegmentCursors(field)
|
||||
|
||||
const hasMin = min !== undefined && min !== null
|
||||
const hasMax = max !== undefined && max !== null
|
||||
|
||||
for (const cursor of cursors) {
|
||||
const lo = (min !== undefined && min !== null) ? min as number | string : cursor.minValue
|
||||
const hi = (max !== undefined && max !== null) ? max as number | string : cursor.maxValue
|
||||
const lo = hasMin ? min as number | string : cursor.minValue
|
||||
const hi = hasMax ? max as number | string : cursor.maxValue
|
||||
if (lo === undefined || hi === undefined) continue
|
||||
const ids = cursor.getEntityIdsInRange(lo, hi)
|
||||
// Exclusivity applies only to an explicitly provided bound. A bound taken
|
||||
// from the segment's own min/max is a real stored value and must stay
|
||||
// inclusive, or the segment's boundary entities would be wrongly dropped.
|
||||
const ids = cursor.getEntityIdsInRange(
|
||||
lo,
|
||||
hi,
|
||||
hasMin ? includeMin : true,
|
||||
hasMax ? includeMax : true
|
||||
)
|
||||
for (const id of ids) result.add(id)
|
||||
}
|
||||
|
||||
|
|
@ -282,9 +307,10 @@ export class ColumnStore implements ColumnStoreProvider {
|
|||
const tailCursor = this.getTailBufferCursor(field)
|
||||
if (tailCursor) {
|
||||
for (const entry of tailCursor.iterateForward()) {
|
||||
const v = entry.value
|
||||
const inRange = (min == null || v >= min) && (max == null || v <= max)
|
||||
if (inRange) result.add(entry.entityIntId)
|
||||
const v = entry.value as any
|
||||
const loOk = !hasMin || (includeMin ? v >= (min as any) : v > (min as any))
|
||||
const hiOk = !hasMax || (includeMax ? v <= (max as any) : v < (max as any))
|
||||
if (loOk && hiOk) result.add(entry.entityIntId)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue