120 lines
4.8 KiB
TypeScript
120 lines
4.8 KiB
TypeScript
|
|
/**
|
|||
|
|
* @module tests/unit/utils/metadataIndex-sort-callshape
|
|||
|
|
* @description THE ASYMPTOTIC CALL-SHAPE PIN for ordered reads
|
|||
|
|
* (BRAINY-PROD-LATENCY-TRIAD, David-approved plan Track A1). The defect it
|
|||
|
|
* keeps dead: `getSortedIdsForFilter`'s value resolution did a SERIAL
|
|||
|
|
* `storage.getNoun()` (the heavyweight VECTOR record) per filtered row —
|
|||
|
|
* 62–98ms × 3,224 rows = the measured 199–317 SECOND production sort, with
|
|||
|
|
* `topK` applied only after the full scan. These pins assert the SHAPE of
|
|||
|
|
* the storage traffic, not wall-clock (latency-blind, so they hold on any
|
|||
|
|
* machine): an ordered read performs ZERO per-row vector-record reads and
|
|||
|
|
* resolves sort values through BATCHED metadata-record calls only.
|
|||
|
|
*/
|
|||
|
|
import { describe, it, expect, beforeAll, afterAll, vi } from 'vitest'
|
|||
|
|
import { Brainy } from '../../../src/index.js'
|
|||
|
|
import { NounType } from '../../../src/types/graphTypes.js'
|
|||
|
|
|
|||
|
|
const ROWS = 60
|
|||
|
|
|
|||
|
|
describe('ordered reads — the batched call-shape law (no per-row storage loops)', () => {
|
|||
|
|
let brain: Brainy
|
|||
|
|
let storage: {
|
|||
|
|
getNoun: (id: string) => Promise<unknown>
|
|||
|
|
getNounMetadata: (id: string) => Promise<unknown>
|
|||
|
|
getNounMetadataBatch: (ids: string[]) => Promise<Map<string, unknown>>
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
beforeAll(async () => {
|
|||
|
|
brain = new Brainy({ storage: { type: 'memory' }, requireSubtype: false })
|
|||
|
|
await brain.init()
|
|||
|
|
for (let i = 0; i < ROWS; i++) {
|
|||
|
|
await brain.add({
|
|||
|
|
data: `row ${i}`,
|
|||
|
|
type: NounType.Document,
|
|||
|
|
metadata: { rank: (i * 7) % ROWS, plain: `p${i}` }
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
storage = (brain as unknown as { storage: typeof storage }).storage
|
|||
|
|
}, 120000)
|
|||
|
|
|
|||
|
|
afterAll(async () => {
|
|||
|
|
await brain.close().catch(() => {})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('user-field orderBy: zero vector-record reads, zero serial metadata reads — batch calls only', async () => {
|
|||
|
|
const getNounSpy = vi.spyOn(storage, 'getNoun')
|
|||
|
|
const singleReadSpy = vi.spyOn(storage, 'getNounMetadata')
|
|||
|
|
const batchSpy = vi.spyOn(storage, 'getNounMetadataBatch')
|
|||
|
|
|
|||
|
|
const rows = await brain.find({
|
|||
|
|
type: NounType.Document,
|
|||
|
|
orderBy: 'rank',
|
|||
|
|
order: 'desc',
|
|||
|
|
limit: 10
|
|||
|
|
})
|
|||
|
|
expect(rows.length).toBe(10)
|
|||
|
|
expect((rows[0].metadata as Record<string, unknown>).rank).toBe(ROWS - 1)
|
|||
|
|
|
|||
|
|
// THE PIN: the sort's value resolution never opens a vector record and
|
|||
|
|
// never falls into a per-row metadata loop. (Result hydration after
|
|||
|
|
// pagination is allowed to read; the SORT itself must be batch-only —
|
|||
|
|
// hence the ceiling: strictly fewer single reads than sorted rows.)
|
|||
|
|
expect(getNounSpy.mock.calls.length, 'per-row vector-record reads in an ordered read').toBe(0)
|
|||
|
|
expect(batchSpy.mock.calls.length, 'the batch door was used').toBeGreaterThanOrEqual(1)
|
|||
|
|
expect(
|
|||
|
|
singleReadSpy.mock.calls.length,
|
|||
|
|
'serial per-row metadata reads (the 199s shape)'
|
|||
|
|
).toBeLessThan(ROWS / 2)
|
|||
|
|
|
|||
|
|
vi.restoreAllMocks()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('system.createdAt orderBy: exact values from batched records — the bucketed index is never a per-row disk excuse', async () => {
|
|||
|
|
const getNounSpy = vi.spyOn(storage, 'getNoun')
|
|||
|
|
const batchSpy = vi.spyOn(storage, 'getNounMetadataBatch')
|
|||
|
|
|
|||
|
|
const rows = await brain.find({
|
|||
|
|
type: NounType.Document,
|
|||
|
|
orderBy: 'system.createdAt',
|
|||
|
|
order: 'asc',
|
|||
|
|
limit: 15
|
|||
|
|
})
|
|||
|
|
expect(rows.length).toBe(15)
|
|||
|
|
|
|||
|
|
expect(getNounSpy.mock.calls.length, 'per-row vector-record reads').toBe(0)
|
|||
|
|
expect(batchSpy.mock.calls.length).toBeGreaterThanOrEqual(1)
|
|||
|
|
|
|||
|
|
// Exactness: ascending createdAt must be non-decreasing with full
|
|||
|
|
// millisecond precision (the old path sorted minute-BUCKETED values or
|
|||
|
|
// paid a per-row disk read for exact ones — both are dead). Find results
|
|||
|
|
// carry the timestamps on the nested full entity.
|
|||
|
|
const stamps = rows.map(
|
|||
|
|
(r) => ((r as unknown as { entity?: { createdAt?: number } }).entity?.createdAt ??
|
|||
|
|
(r as unknown as { createdAt?: number }).createdAt) as number
|
|||
|
|
)
|
|||
|
|
for (let i = 1; i < stamps.length; i++) {
|
|||
|
|
expect(stamps[i]).toBeGreaterThanOrEqual(stamps[i - 1])
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
vi.restoreAllMocks()
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
it('the ordering contract survives the batch path: missing values LAST both directions, ties by id asc, rows never dropped', async () => {
|
|||
|
|
// Three rows lack `rank`? No — all carry it; add two rows WITHOUT it.
|
|||
|
|
const a = await brain.add({ data: 'no-rank a', type: NounType.Document, metadata: { plain: 'x' } })
|
|||
|
|
const b = await brain.add({ data: 'no-rank b', type: NounType.Document, metadata: { plain: 'y' } })
|
|||
|
|
|
|||
|
|
for (const order of ['asc', 'desc'] as const) {
|
|||
|
|
const rows = await brain.find({
|
|||
|
|
type: NounType.Document,
|
|||
|
|
orderBy: 'rank',
|
|||
|
|
order,
|
|||
|
|
limit: ROWS + 10
|
|||
|
|
})
|
|||
|
|
expect(rows.length, `complete result (${order})`).toBe(ROWS + 2)
|
|||
|
|
const lastTwo = rows.slice(-2).map((r) => r.id).sort()
|
|||
|
|
expect(lastTwo, `missing-value rows sort LAST (${order})`).toEqual([a, b].sort())
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
})
|