mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-10-25 05:53:44 +00:00
fix(local-backend): (#1178)skip vector index query on unsupported platforms
Co-authored-by: Copilot <copilot@github.com>
This commit is contained in:
parent
dafda284bc
commit
27feb40e98
2 changed files with 83 additions and 16 deletions
|
|
@ -35,7 +35,10 @@ import {
|
|||
type ExactEmbeddingRow,
|
||||
} from '../../core/embeddings/exact-search.js';
|
||||
import { EMBEDDING_TABLE_NAME, EMBEDDING_INDEX_NAME } from '../../core/lbug/schema.js';
|
||||
import { getExactScanLimit } from '../../core/platform/capabilities.js';
|
||||
import {
|
||||
getExactScanLimit,
|
||||
isVectorExtensionSupportedByPlatform,
|
||||
} from '../../core/platform/capabilities.js';
|
||||
import { PhaseTimer } from '../../core/search/phase-timer.js';
|
||||
import { checkStaleness, checkCwdMatch } from '../../core/git-staleness.js';
|
||||
// AI context generation is CLI-only (gitnexus analyze)
|
||||
|
|
@ -223,6 +226,14 @@ export class LocalBackend {
|
|||
*/
|
||||
private warnedSiblingDrift: Set<string> = new Set();
|
||||
|
||||
/**
|
||||
* One-shot stderr warning for the VECTOR-extension fallback. Without this
|
||||
* guard the diagnostic would fire on every `semanticSearch()` call on
|
||||
* platforms where the extension is unsupported (e.g. Windows), making MCP
|
||||
* stderr noisy per DoD §2.8.
|
||||
*/
|
||||
private warnedVectorUnsupported = false;
|
||||
|
||||
/**
|
||||
* Cross-repo group tools (CLI). Shares logic with MCP `group_*` handlers.
|
||||
*/
|
||||
|
|
@ -1071,9 +1082,10 @@ export class LocalBackend {
|
|||
string,
|
||||
{ distance: number; chunkIndex: number; startLine: number; endLine: number }
|
||||
>();
|
||||
try {
|
||||
bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
|
||||
const vectorQuery = `
|
||||
if (isVectorExtensionSupportedByPlatform()) {
|
||||
try {
|
||||
bestChunks = await collectBestChunks(limit, async (fetchLimit) => {
|
||||
const vectorQuery = `
|
||||
CALL QUERY_VECTOR_INDEX('${EMBEDDING_TABLE_NAME}', '${EMBEDDING_INDEX_NAME}',
|
||||
CAST(${queryVecStr} AS FLOAT[${dims}]), ${fetchLimit})
|
||||
YIELD node AS emb, distance
|
||||
|
|
@ -1084,17 +1096,28 @@ export class LocalBackend {
|
|||
ORDER BY distance
|
||||
`;
|
||||
|
||||
const embResults = await executeQuery(repo.id, vectorQuery);
|
||||
return embResults.map((row) => ({
|
||||
nodeId: row.nodeId ?? row[0],
|
||||
chunkIndex: row.chunkIndex ?? row[1] ?? 0,
|
||||
startLine: row.startLine ?? row[2] ?? 0,
|
||||
endLine: row.endLine ?? row[3] ?? 0,
|
||||
distance: row.distance ?? row[4],
|
||||
}));
|
||||
});
|
||||
} catch {
|
||||
bestChunks = new Map();
|
||||
const embResults = await executeQuery(repo.id, vectorQuery);
|
||||
return embResults.map((row) => ({
|
||||
nodeId: row.nodeId ?? row[0],
|
||||
chunkIndex: row.chunkIndex ?? row[1] ?? 0,
|
||||
startLine: row.startLine ?? row[2] ?? 0,
|
||||
endLine: row.endLine ?? row[3] ?? 0,
|
||||
distance: row.distance ?? row[4],
|
||||
}));
|
||||
});
|
||||
} catch {
|
||||
bestChunks = new Map();
|
||||
}
|
||||
} else if (!this.warnedVectorUnsupported) {
|
||||
// Rare diagnostic: surface why we fell back to the exact scan path so
|
||||
// operators can see at a glance that the VECTOR extension is missing on
|
||||
// this runtime (e.g. Windows builds without the optional native
|
||||
// dependency). Emitted once per `LocalBackend` instance lifetime to
|
||||
// avoid noisy stderr on hot semantic-search paths (DoD §2.8).
|
||||
this.warnedVectorUnsupported = true;
|
||||
console.error(
|
||||
'GitNexus [query:vector]: VECTOR index unavailable for this runtime; using exact scan fallback',
|
||||
);
|
||||
}
|
||||
|
||||
if (bestChunks.size === 0) {
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|||
// local-backend.ts imports from core/lbug/pool-adapter.js; the mcp/core/lbug-adapter.js
|
||||
// re-exports from the same module, so we mock the canonical source.
|
||||
// vi.hoisted runs before vi.mock hoisting, making the fns available to both factories.
|
||||
const { lbugMocks } = vi.hoisted(() => ({
|
||||
const { lbugMocks, platformMocks } = vi.hoisted(() => ({
|
||||
lbugMocks: {
|
||||
initLbug: vi.fn().mockResolvedValue(undefined),
|
||||
executeQuery: vi.fn().mockResolvedValue([]),
|
||||
|
|
@ -21,6 +21,9 @@ const { lbugMocks } = vi.hoisted(() => ({
|
|||
closeLbug: vi.fn().mockResolvedValue(undefined),
|
||||
isLbugReady: vi.fn().mockReturnValue(true),
|
||||
},
|
||||
platformMocks: {
|
||||
isVectorExtensionSupportedByPlatform: vi.fn().mockReturnValue(true),
|
||||
},
|
||||
}));
|
||||
|
||||
vi.mock('../../src/core/lbug/pool-adapter.js', async (importOriginal) => {
|
||||
|
|
@ -48,6 +51,14 @@ vi.mock('../../src/core/git-staleness.js', () => ({
|
|||
checkCwdMatch: vi.fn().mockResolvedValue({ match: 'none' }),
|
||||
}));
|
||||
|
||||
vi.mock('../../src/core/platform/capabilities.js', async (importOriginal) => {
|
||||
const actual = await importOriginal<typeof import('../../src/core/platform/capabilities.js')>();
|
||||
return {
|
||||
...actual,
|
||||
isVectorExtensionSupportedByPlatform: platformMocks.isVectorExtensionSupportedByPlatform,
|
||||
};
|
||||
});
|
||||
|
||||
// Also mock the search modules to avoid loading onnxruntime
|
||||
vi.mock('../../src/core/search/bm25-index.js', () => ({
|
||||
searchFTSFromLbug: vi.fn().mockResolvedValue([]),
|
||||
|
|
@ -157,6 +168,7 @@ describe('LocalBackend.callTool', () => {
|
|||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
platformMocks.isVectorExtensionSupportedByPlatform.mockReturnValue(true);
|
||||
backend = new LocalBackend();
|
||||
setupSingleRepo();
|
||||
await backend.init();
|
||||
|
|
@ -181,6 +193,38 @@ describe('LocalBackend.callTool', () => {
|
|||
expect(result).toHaveProperty('definitions');
|
||||
});
|
||||
|
||||
it('skips vector index query when VECTOR is unsupported by the platform', async () => {
|
||||
const consoleError = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
||||
platformMocks.isVectorExtensionSupportedByPlatform.mockReturnValue(false);
|
||||
(executeQuery as any).mockImplementation(async (_repoId: string, cypher: string) => {
|
||||
if (cypher.includes('COUNT(*) AS cnt')) return [{ cnt: 1 }];
|
||||
if (cypher.includes('MATCH (e:CodeEmbedding)')) return [];
|
||||
return [];
|
||||
});
|
||||
(executeParameterized as any).mockResolvedValue([]);
|
||||
|
||||
try {
|
||||
await backend.callTool('query', { query: 'auth' });
|
||||
|
||||
const queries = (executeQuery as any).mock.calls.map(
|
||||
([, cypher]: [string, string]) => cypher,
|
||||
);
|
||||
expect(queries.some((cypher: string) => cypher.includes('QUERY_VECTOR_INDEX'))).toBe(false);
|
||||
expect(
|
||||
queries.some(
|
||||
(cypher: string) =>
|
||||
cypher.includes('RETURN e.nodeId AS nodeId') &&
|
||||
cypher.includes('e.embedding AS embedding'),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(consoleError).toHaveBeenCalledWith(
|
||||
expect.stringContaining('GitNexus [query:vector]: VECTOR index unavailable'),
|
||||
);
|
||||
} finally {
|
||||
consoleError.mockRestore();
|
||||
}
|
||||
});
|
||||
|
||||
it('query tool returns error for empty query', async () => {
|
||||
const result = await backend.callTool('query', { query: '' });
|
||||
expect(result.error).toContain('query parameter is required');
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue