fix(find): near() searches around the anchor's own vector, and refuses by name without one
Some checks failed
CI / Node 22 (push) Successful in 12m30s
CI / Node 24 (push) Successful in 12m29s
CI / Integration + conformance (Node 22) (push) Failing after 17m24s
CI / Bun (latest) (push) Successful in 12m23s

The proximity search fetched its anchor through get(), which omits vectors
by default, then handed a zero-length vector to the index — every
find({ near }) refused with a dimension mismatch, for every caller. Found
by the Rust planner's first-contact pins comparing outcomes with and
without the planner on a refused shape. The anchor is now fetched with its
vector, and an anchor that has none refuses by name — a proximity search
around an unvectored row has no meaning and must not fail inside the index.
Pinned in tests/integration/find-near.test.ts.
This commit is contained in:
David Snelling 2026-09-02 08:43:39 -07:00
parent 34f1886f7c
commit a8c5fbf9dc
2 changed files with 59 additions and 1 deletions

View file

@ -15892,8 +15892,18 @@ export class Brainy<T = any> implements BrainyInterface<T> {
)
}
const nearEntity = await this.get(params.near.id)
// The anchor's VECTOR is the query; get() omits vectors by default, which
// fed a zero-length vector to the index and refused every near() with a
// dimension mismatch. Ask for it, and refuse by name when the anchor has
// none — a proximity search around an unvectored row has no meaning.
const nearEntity = await this.get(params.near.id, { includeVectors: true })
if (!nearEntity) return []
if (!nearEntity.vector || nearEntity.vector.length === 0) {
throw new Error(
`find({ near }): entity '${params.near.id}' has no vector to search around — ` +
`it was never embedded (or was unvectored). Embed it, or search with a query instead.`
)
}
const nearResults: [string, number][] = await this.index.search(nearEntity.vector, params.limit || 10)

View file

@ -0,0 +1,48 @@
/**
* @module tests/integration/find-near
* @description find({ near }) searches around the anchor's OWN vector (10.4.10).
*
* The proximity search fetched its anchor without vectors and fed a
* zero-length vector to the index every near() refused with a dimension
* mismatch, for every caller. Found by the Rust planner's first-contact pins
* (the planner declines `near`; the pin compared outcomes with and without
* it). Now the anchor is fetched with its vector, and an anchor without one
* refuses by name instead of failing inside the index.
*/
import { describe, it, expect, beforeAll } from 'vitest'
import { Brainy } from '../../src/brainy'
import { NounType } from '../../src/types/graphTypes'
import { v5 } from '../../src/universal/uuid'
import { generateTestVector } from '../helpers/test-factory'
describe('find({ near }) uses the anchor vector', () => {
let brain: Brainy<any>
const anchorVector = generateTestVector()
beforeAll(async () => {
brain = new Brainy({ requireSubtype: false, storage: { type: 'memory' } })
await brain.init()
await brain.add({ id: 'anchor', data: 'anchor row', type: NounType.Thing, vector: anchorVector })
// A twin with the identical vector and a far row.
await brain.add({ id: 'twin', data: 'twin row', type: NounType.Thing, vector: [...anchorVector] })
await brain.add({ id: 'far', data: 'far row', type: NounType.Thing, vector: generateTestVector() })
})
it('returns the anchor\'s neighbours by its own vector', async () => {
const results = await brain.find({ near: { id: 'anchor' }, limit: 3 })
expect(results.length).toBeGreaterThan(0)
const ids = results.map((r) => r.entity.id)
expect(ids).toContain(v5('twin'))
})
it('refuses by name when the anchor has no vector', async () => {
await brain.add({
id: 'unvectored',
data: 'no vector here',
type: NounType.Thing,
deferEmbedding: true
})
;(brain as any).kickEmbedWorker = () => {}
await expect(brain.find({ near: { id: 'unvectored' }, limit: 3 })).rejects.toThrow(/has no vector to search around/)
})
})