3838 * Related fixes: commit 251e5b73 (fix(hnsw): six data-integrity fixes)
3939 */
4040import { suite , test , before , after } from 'node:test' ;
41- import { ok } from 'node:assert/strict' ;
41+ import { ok , strictEqual } from 'node:assert/strict' ;
4242import { setTimeout as sleep } from 'node:timers/promises' ;
4343import { startHarper , teardownHarper , type ContextWithHarper } from '@harperfast/integration-testing' ;
4444// @ts -expect-error no type declarations on .mjs utils
@@ -100,19 +100,6 @@ function seedVector(seed: number, dims: number = 8): number[] {
100100 return v . map ( ( x ) => x * inv ) ;
101101}
102102
103- /** Cosine distance — matches the HNSW default metric. */
104- function cosineDistance ( a : number [ ] , b : number [ ] ) : number {
105- let dot = 0 ;
106- let magA = 0 ;
107- let magB = 0 ;
108- for ( let i = 0 ; i < a . length ; i ++ ) {
109- dot += a [ i ] * b [ i ] ;
110- magA += a [ i ] * a [ i ] ;
111- magB += b [ i ] * b [ i ] ;
112- }
113- return 1 - dot / ( ( Math . sqrt ( magA ) || 1 ) * ( Math . sqrt ( magB ) || 1 ) ) ;
114- }
115-
116103// ---------------------------------------------------------------------------
117104// HTTP helpers
118105// ---------------------------------------------------------------------------
@@ -129,24 +116,37 @@ async function vectorSearch(
129116 headers : Record < string , string > ,
130117 resourcePath : string ,
131118 target : number [ ] ,
132- opts : { limit ?: number } = { }
119+ opts : { limit ?: number ; select ?: string [ ] } = { }
133120) : Promise < any [ ] > {
134121 const body : any = {
135122 sort : { attribute : 'embedding' , target, distance : 'cosine' } ,
136123 } ;
137124 if ( opts . limit !== undefined ) body . limit = opts . limit ;
125+ if ( opts . select !== undefined ) body . select = opts . select ;
126+ return queryResource ( httpURL , headers , resourcePath , body ) ;
127+ }
138128
139- const resp = await request ( httpURL )
140- . query ( resourcePath )
141- . set ( headers )
142- . set ( 'Content-Type' , 'application/json' )
143- . method ( 'QUERY' as any )
144- . send ( body ) ;
145-
129+ /**
130+ * Issue an HTTP QUERY request via fetch — supertest/superagent has no API for
131+ * non-standard verbs, while undici's fetch passes custom method tokens through.
132+ */
133+ async function queryResource (
134+ httpURL : string ,
135+ headers : Record < string , string > ,
136+ resourcePath : string ,
137+ body : any
138+ ) : Promise < any [ ] > {
139+ const resp = await fetch ( `${ httpURL } ${ resourcePath } ` , {
140+ method : 'QUERY' ,
141+ headers : { ...headers , 'Content-Type' : 'application/json' } ,
142+ body : JSON . stringify ( body ) ,
143+ } ) ;
144+ const text = await resp . text ( ) ;
146145 if ( resp . status !== 200 ) {
147- throw new Error ( `QUERY ${ resourcePath } returned ${ resp . status } : ${ JSON . stringify ( resp . body ) } ` ) ;
146+ throw new Error ( `QUERY ${ resourcePath } returned ${ resp . status } : ${ text } ` ) ;
148147 }
149- return Array . isArray ( resp . body ) ? resp . body : [ ] ;
148+ const data = JSON . parse ( text ) ;
149+ return Array . isArray ( data ) ? data : [ ] ;
150150}
151151
152152/**
@@ -167,18 +167,7 @@ async function vectorThresholdSearch(
167167 const body = {
168168 conditions : [ { attribute : 'embedding' , comparator, value, target } ] ,
169169 } ;
170-
171- const resp = await request ( httpURL )
172- . query ( resourcePath )
173- . set ( headers )
174- . set ( 'Content-Type' , 'application/json' )
175- . method ( 'QUERY' as any )
176- . send ( body ) ;
177-
178- if ( resp . status !== 200 ) {
179- throw new Error ( `QUERY threshold ${ resourcePath } returned ${ resp . status } : ${ JSON . stringify ( resp . body ) } ` ) ;
180- }
181- return Array . isArray ( resp . body ) ? resp . body : [ ] ;
170+ return queryResource ( httpURL , headers , resourcePath , body ) ;
182171}
183172
184173/** POST a record to the REST endpoint. */
@@ -416,47 +405,56 @@ suite('HNSW vector-index data-integrity (integration)', (ctx: ContextWithHarper)
416405 const headers = client . headers ;
417406 const path = `/${ TABLE } /` ;
418407
419- // 2-D vectors for exact, predictable cosine distances:
420- // [1,0] vs [1,0] → distance ≈ 0 (exact)
421- // [1,0] vs [1/√2, 1/√2] → distance ≈ 0.293 (near)
422- // [1,0] vs [0,1] → distance = 1 (far)
408+ // 2-D vectors at three well-separated cosine distances from [1,0]:
409+ // [1,0] → distance 0 (exact; representable in float32, so exactly 0)
410+ // [1/√2, 1/√2] → distance ≈0.293 (near)
411+ // [0,1] → distance 1 (far)
412+ // Vectors are STORED as float32, so a float64 prediction of the near distance
413+ // differs from the server's computed value at ~1e-8. Boundary assertions must
414+ // therefore use the server's own $distance values, not locally computed ones.
423415 const INV_SQRT2 = 1 / Math . sqrt ( 2 ) ;
424416 const target = [ 1 , 0 ] ;
425- const exactVec = [ 1 , 0 ] ;
426- const nearVec = [ INV_SQRT2 , INV_SQRT2 ] ;
427- const farVec = [ 0 , 1 ] ;
428-
429- const dExact = cosineDistance ( target , exactVec ) ;
430- const dNear = cosineDistance ( target , nearVec ) ;
431-
432- await insertRecord ( httpURL , headers , path , { id : 'exact' , embedding : exactVec } ) ;
433- await insertRecord ( httpURL , headers , path , { id : 'near' , embedding : nearVec } ) ;
434- await insertRecord ( httpURL , headers , path , { id : 'far' , embedding : farVec } ) ;
435417
436- // 3a. le(dNear) must include both "exact" (dist < boundary) and "near"
437- // (dist == boundary — this is the fix).
418+ await insertRecord ( httpURL , headers , path , { id : 'exact' , embedding : [ 1 , 0 ] } ) ;
419+ await insertRecord ( httpURL , headers , path , { id : 'near' , embedding : [ INV_SQRT2 , INV_SQRT2 ] } ) ;
420+ await insertRecord ( httpURL , headers , path , { id : 'far' , embedding : [ 0 , 1 ] } ) ;
421+
422+ // Fetch the actual stored distances; these are the exact values the threshold
423+ // filter compares against (same candidate-distance computation, no rerank on
424+ // a non-quantized index).
425+ const ranked = await vectorSearch ( httpURL , headers , path , target , { select : [ 'id' , '$distance' ] } ) ;
426+ const distanceOf = ( id : string ) => {
427+ const rec = ranked . find ( ( r : any ) => r . id === id ) ;
428+ ok ( rec && typeof rec . $distance === 'number' , `expected $distance for '${ id } ', got ${ JSON . stringify ( rec ) } ` ) ;
429+ return rec . $distance as number ;
430+ } ;
431+ const dExact = distanceOf ( 'exact' ) ;
432+ const dNear = distanceOf ( 'near' ) ;
433+ strictEqual ( dExact , 0 , `[1,0] is float32-representable; self-distance must be exactly 0, got ${ dExact } ` ) ;
434+
435+ // le(dNear) must include both "exact" (dist < boundary) and "near"
436+ // (dist == boundary — this is the <= fix).
438437 const leNear = await vectorThresholdSearch ( httpURL , headers , path , target , 'le' , dNear ) ;
439438 const leNearIds = new Set ( leNear . map ( ( r : any ) => r . id ) ) ;
440- ok ( leNearIds . has ( 'exact' ) , `le(dNear) must include 'exact' (${ dExact } ≤ ${ dNear } )` ) ;
439+ ok ( leNearIds . has ( 'exact' ) , `le(dNear) must include 'exact' (0 ≤ ${ dNear } )` ) ;
441440 ok ( leNearIds . has ( 'near' ) , `le(dNear) must include 'near' at exact boundary distance ${ dNear } ` ) ;
442441 ok ( ! leNearIds . has ( 'far' ) , `le(dNear) must not include 'far' (distance 1 > ${ dNear } )` ) ;
443442
444- // 3b. lt(dNear) must include "exact" but NOT "near" (strict).
443+ // lt(dNear) must include "exact" but NOT "near" (strict).
445444 const ltNear = await vectorThresholdSearch ( httpURL , headers , path , target , 'lt' , dNear ) ;
446445 const ltNearIds = new Set ( ltNear . map ( ( r : any ) => r . id ) ) ;
447- ok ( ltNearIds . has ( 'exact' ) , `lt(dNear) must include 'exact' (${ dExact } < ${ dNear } )` ) ;
446+ ok ( ltNearIds . has ( 'exact' ) , `lt(dNear) must include 'exact' (0 < ${ dNear } )` ) ;
448447 ok ( ! ltNearIds . has ( 'near' ) , `lt(dNear) must NOT include 'near' at boundary (strict less-than)` ) ;
449448
450- // 3c. le(~0) must return only the exact-match record.
451- // Pre-fix falsy-0 issue: if limit was checked with a truthy guard (if (limit))
452- // rather than (limit !== undefined), le(0) would skip the filter entirely.
453- const epsilon = 1e-10 ;
454- const leZero = await vectorThresholdSearch ( httpURL , headers , path , target , 'le' , dExact + epsilon ) ;
449+ // le(0) must return only the exact-match record. Pre-fix falsy-0 issue: a truthy
450+ // guard (if (limit)) skipped the filter entirely for a threshold of 0, returning
451+ // every record. dExact is exactly 0, so this exercises the real boundary.
452+ const leZero = await vectorThresholdSearch ( httpURL , headers , path , target , 'le' , dExact ) ;
455453 const leZeroIds = new Set ( leZero . map ( ( r : any ) => r . id ) ) ;
456- ok ( leZeroIds . has ( 'exact' ) , `le(~ 0) must include the exact-match record` ) ;
454+ ok ( leZeroIds . has ( 'exact' ) , `le(0) must include the exact-match record` ) ;
457455 ok (
458456 ! leZeroIds . has ( 'near' ) && ! leZeroIds . has ( 'far' ) ,
459- `le(~ 0) must return only the exact match; got ${ JSON . stringify ( [ ...leZeroIds ] ) } `
457+ `le(0) must return only the exact match; got ${ JSON . stringify ( [ ...leZeroIds ] ) } `
460458 ) ;
461459 } ) ;
462460
0 commit comments