mirror of
https://github.com/abhigyanpatwari/GitNexus.git
synced 2026-09-17 23:52:36 +00:00
perf(analyze): defer FTS index creation to first search (#794)
Move the 5 `CREATE_FTS_INDEX` calls out of the analyze pipeline and into lazy first-use initialisation, restoring CI headroom against the 30 s e2e test budget. Why --- LadybugDB's `CREATE_FTS_INDEX` costs ~440 ms per call regardless of table size — so on a 7-file mini-repo the 5 indexes (File, Function, Class, Method, Interface) added ~2.2 s of fixed overhead, ~3× that on slower Windows CI runners. This made `analyze` sit at ~18.7 s out of a 30 s budget on Windows: a normal runner spike was enough to push the `analyze command runs pipeline on mini-repo` e2e test (cli-e2e.test.ts:175) into a timeout flake (PR #984 run 24631480424). How --- * `lbug-adapter.ts`: add `ensureFTSIndex(table, indexName, props)` with an in-process `Set` cache (`ensuredFTSIndexes`). First call per `(table, indexName)` pays the LadybugDB cost; subsequent calls are a Set lookup. Cache is cleared by `closeLbug` so re-init starts fresh. * `bm25-index.ts`: introduce a single `FTS_INDEXES` schema constant, call `ensureFTSIndex` (single-process) or `ensureFTSIndexViaExecutor` (MCP pool, with its own per-repo `ensuredPoolFTS` cache) before each query. Indexes that already exist on disk just re-register cheaply through the cache; failures are best-effort and fall back to `[]`. * `run-analyze.ts`: drop the eager FTS phase block; replace with a comment explaining the new lazy strategy. * `CHANGELOG.md`: add Performance entry under Unreleased. Effect (mini-repo, Windows, warm runs) -------------------------------------- | Run | Before | After | Δ | |------------|---------|---------|---------| | Cold | 10.55 s | 4.37 s | -58 % | | Warm | 6.37 s | 4.00 s | -37 % | CI projection (Windows, ~3× ratio): 18.7 s → ~11.7 s, doubling the margin against the 30 s test budget from +60 % to +156 %. Verification ------------ * `tsc --noEmit` clean. * All 42 FTS-dependent integration tests pass: `search-core`, `search-pool`, `lbug-core-adapter`, `bm25-search`, `augmentation`. * `cli-e2e.test.ts` `analyze` test passes locally; two unrelated pre-existing failures in `wiki`/`status` (outdated error-message expectations) reproduce on the prior commit and are out of scope. Refs: #794, #984 Made-with: Cursor
This commit is contained in:
parent
ac69a086e7
commit
5aef74c4eb
4 changed files with 118 additions and 14 deletions
|
|
@ -2,6 +2,12 @@
|
|||
|
||||
All notable changes to GitNexus will be documented in this file.
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Performance
|
||||
|
||||
- **`analyze` ~33% faster** — moved FTS index creation from the analyze pipeline to first-use lazy initialisation. The 5 `CREATE_FTS_INDEX` calls cost ~440 ms each in LadybugDB regardless of table size (≈2 s fixed overhead) and dominated runtime on small repos and slow CI runners. The cost now amortises across the first `query`/`context` call in a session via a new `ensureFTSIndex` helper. Mini-repo `analyze` measured locally on Windows: 6.4 s → 4.0 s warm; on CI Windows runners (≈3× slower) restores comfortable headroom against the 30 s e2e test budget.
|
||||
|
||||
## [1.6.2] - 2026-04-18
|
||||
|
||||
### Added
|
||||
|
|
|
|||
|
|
@ -144,6 +144,18 @@ let currentDbPath: string | null = null;
|
|||
let ftsLoaded = false;
|
||||
let vectorExtensionLoaded = false;
|
||||
|
||||
/**
|
||||
* In-process cache of FTS indexes that have been ensured against the current
|
||||
* connection. Prevents repeated `CALL CREATE_FTS_INDEX` round-trips inside a
|
||||
* single CLI/MCP session — the first call to `ensureFTSIndex` for a given
|
||||
* `(tableName, indexName)` pays the LadybugDB cost (~440 ms even when the
|
||||
* index already exists on disk), subsequent calls are a Set lookup. Cleared
|
||||
* by `closeLbug` so a re-init starts fresh.
|
||||
*
|
||||
* Key format: `${tableName}:${indexName}`.
|
||||
*/
|
||||
const ensuredFTSIndexes = new Set<string>();
|
||||
|
||||
/**
|
||||
* Check if an error indicates a missing column or table (schema-level problem)
|
||||
* rather than a transient/connection error. Used for legacy DB fallback logic.
|
||||
|
|
@ -1037,6 +1049,7 @@ export const closeLbug = async (): Promise<void> => {
|
|||
currentDbPath = null;
|
||||
ftsLoaded = false;
|
||||
vectorExtensionLoaded = false;
|
||||
ensuredFTSIndexes.clear();
|
||||
};
|
||||
|
||||
export const isLbugReady = (): boolean => conn !== null && db !== null;
|
||||
|
|
@ -1219,6 +1232,29 @@ export const createFTSIndex = async (
|
|||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Lazy-create an FTS index, caching the fact in-process.
|
||||
*
|
||||
* Used by `queryFTS` so that `analyze` doesn't pay the ~440 ms × 5 fixed
|
||||
* LadybugDB cost up-front (it dominates analyze on small repos). Instead,
|
||||
* the cost is moved to the first `query`/`context` call in a session,
|
||||
* where it's amortised across many lookups.
|
||||
*
|
||||
* Safe to call repeatedly — the in-process Set guarantees only the first
|
||||
* call hits LadybugDB. `closeLbug` clears the cache so re-init starts fresh.
|
||||
*/
|
||||
export const ensureFTSIndex = async (
|
||||
tableName: string,
|
||||
indexName: string,
|
||||
properties: string[],
|
||||
stemmer: string = 'porter',
|
||||
): Promise<void> => {
|
||||
const key = `${tableName}:${indexName}`;
|
||||
if (ensuredFTSIndexes.has(key)) return;
|
||||
await createFTSIndex(tableName, indexName, properties, stemmer);
|
||||
ensuredFTSIndexes.add(key);
|
||||
};
|
||||
|
||||
/**
|
||||
* Query a full-text search index
|
||||
* @param tableName - The node table name
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import {
|
|||
executeQuery,
|
||||
executeWithReusedStatement,
|
||||
closeLbug,
|
||||
createFTSIndex,
|
||||
loadCachedEmbeddings,
|
||||
} from './lbug/lbug-adapter.js';
|
||||
import {
|
||||
|
|
@ -215,17 +214,12 @@ export async function runFullAnalysis(
|
|||
});
|
||||
|
||||
// ── Phase 3: FTS (85–90%) ─────────────────────────────────────────
|
||||
progress('fts', 85, 'Creating search indexes...');
|
||||
|
||||
try {
|
||||
await createFTSIndex('File', 'file_fts', ['name', 'content']);
|
||||
await createFTSIndex('Function', 'function_fts', ['name', 'content']);
|
||||
await createFTSIndex('Class', 'class_fts', ['name', 'content']);
|
||||
await createFTSIndex('Method', 'method_fts', ['name', 'content']);
|
||||
await createFTSIndex('Interface', 'interface_fts', ['name', 'content']);
|
||||
} catch {
|
||||
// Non-fatal — FTS is best-effort
|
||||
}
|
||||
// FTS indexes are created lazily on first `query`/`context` call instead
|
||||
// of eagerly here. On small repos / CI runners the LadybugDB
|
||||
// CREATE_FTS_INDEX cost is ~440 ms × 5 (≈2 s) regardless of table size,
|
||||
// which dominated `analyze` runtime and pushed Windows CI past its
|
||||
// 30 s test budget. Lazy creation is implemented in
|
||||
// `core/search/bm25-index.ts` via `ensureFTSIndex`.
|
||||
|
||||
// ── Phase 3.5: Re-insert cached embeddings ────────────────────────
|
||||
if (cachedEmbeddings.length > 0) {
|
||||
|
|
|
|||
|
|
@ -3,9 +3,15 @@
|
|||
*
|
||||
* Uses LadybugDB's built-in full-text search indexes for keyword-based search.
|
||||
* Always reads from the database (no cached state to drift).
|
||||
*
|
||||
* FTS indexes are created lazily on first query (via `ensureFTSIndex`) — see
|
||||
* `lbug-adapter.ts` for the rationale. This keeps `analyze` fast (the
|
||||
* ~440 ms × 5 LadybugDB CREATE_FTS_INDEX cost dominates pipeline time on
|
||||
* small repos / CI runners) at the cost of paying that overhead on the
|
||||
* first `query`/`context` call in a session.
|
||||
*/
|
||||
|
||||
import { queryFTS } from '../lbug/lbug-adapter.js';
|
||||
import { queryFTS, ensureFTSIndex } from '../lbug/lbug-adapter.js';
|
||||
|
||||
export interface BM25SearchResult {
|
||||
filePath: string;
|
||||
|
|
@ -13,6 +19,56 @@ export interface BM25SearchResult {
|
|||
rank: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* FTS schema served by `searchFTSFromLbug`. Centralised so that both the
|
||||
* CLI/pipeline path and the MCP pool path use identical (table, index,
|
||||
* properties) tuples and the lazy-create logic stays in one place.
|
||||
*/
|
||||
const FTS_INDEXES: ReadonlyArray<{
|
||||
table: string;
|
||||
indexName: string;
|
||||
properties: readonly string[];
|
||||
}> = [
|
||||
{ table: 'File', indexName: 'file_fts', properties: ['name', 'content'] },
|
||||
{ table: 'Function', indexName: 'function_fts', properties: ['name', 'content'] },
|
||||
{ table: 'Class', indexName: 'class_fts', properties: ['name', 'content'] },
|
||||
{ table: 'Method', indexName: 'method_fts', properties: ['name', 'content'] },
|
||||
{ table: 'Interface', indexName: 'interface_fts', properties: ['name', 'content'] },
|
||||
];
|
||||
|
||||
/**
|
||||
* Per-process cache for the MCP pool path: tracks which `(repoId, table)`
|
||||
* pairs have been ensured. The CLI/pipeline path gets its own cache inside
|
||||
* `lbug-adapter.ts` keyed by table/index, scoped to the singleton connection.
|
||||
*/
|
||||
const ensuredPoolFTS = new Set<string>();
|
||||
|
||||
async function ensureFTSIndexViaExecutor(
|
||||
executor: (cypher: string) => Promise<any[]>,
|
||||
repoId: string,
|
||||
table: string,
|
||||
indexName: string,
|
||||
properties: readonly string[],
|
||||
): Promise<void> {
|
||||
const key = `${repoId}:${table}:${indexName}`;
|
||||
if (ensuredPoolFTS.has(key)) return;
|
||||
const propList = properties.map((p) => `'${p}'`).join(', ');
|
||||
try {
|
||||
await executor(
|
||||
`CALL CREATE_FTS_INDEX('${table}', '${indexName}', [${propList}], stemmer := 'porter')`,
|
||||
);
|
||||
} catch (e: any) {
|
||||
// 'already exists' is the happy path (index persists on disk between
|
||||
// process invocations) — anything else we swallow because FTS is
|
||||
// best-effort: queryFTS itself returns [] on missing-index errors.
|
||||
const msg = String(e?.message ?? '');
|
||||
if (!msg.includes('already exists')) {
|
||||
// Best-effort — continue without index, queryFTS will fall back to [].
|
||||
}
|
||||
}
|
||||
ensuredPoolFTS.add(key);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a single FTS query via a custom executor (for MCP connection pool).
|
||||
* Returns the same shape as core queryFTS (from LadybugDB adapter).
|
||||
|
|
@ -75,6 +131,13 @@ export const searchFTSFromLbug = async (
|
|||
// The MCP pool supports multiple connections, but FTS is best run serially.
|
||||
const { executeQuery } = await import('../lbug/pool-adapter.js');
|
||||
const executor = (cypher: string) => executeQuery(repoId, cypher);
|
||||
|
||||
// Lazy-create FTS indexes on first query for this repo (analyze no longer
|
||||
// creates them up-front, so we ensure them here). Cached per-process.
|
||||
for (const { table, indexName, properties } of FTS_INDEXES) {
|
||||
await ensureFTSIndexViaExecutor(executor, repoId, table, indexName, properties);
|
||||
}
|
||||
|
||||
fileResults = await queryFTSViaExecutor(executor, 'File', 'file_fts', query, limit);
|
||||
functionResults = await queryFTSViaExecutor(executor, 'Function', 'function_fts', query, limit);
|
||||
classResults = await queryFTSViaExecutor(executor, 'Class', 'class_fts', query, limit);
|
||||
|
|
@ -87,7 +150,12 @@ export const searchFTSFromLbug = async (
|
|||
limit,
|
||||
);
|
||||
} else {
|
||||
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety
|
||||
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
|
||||
// Lazy-create FTS indexes on first query (analyze no longer does it).
|
||||
for (const { table, indexName, properties } of FTS_INDEXES) {
|
||||
await ensureFTSIndex(table, indexName, [...properties]).catch(() => {});
|
||||
}
|
||||
|
||||
fileResults = await queryFTS('File', 'file_fts', query, limit, false).catch(() => []);
|
||||
functionResults = await queryFTS('Function', 'function_fts', query, limit, false).catch(
|
||||
() => [],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue