gitnexus 1.6.4-rc.93 → 1.6.4-rc.94
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/augmentation/engine.js +1 -1
- package/dist/core/search/bm25-index.d.ts +6 -1
- package/dist/core/search/bm25-index.js +27 -10
- package/dist/core/search/hybrid-search.d.ts +4 -3
- package/dist/core/search/hybrid-search.js +6 -5
- package/dist/mcp/local/local-backend.js +5 -4
- package/dist/server/api.js +16 -7
- package/package.json +1 -1
|
@@ -90,7 +90,7 @@ export async function augment(pattern, cwd) {
|
|
|
90
90
|
await initLbug(repoId, repo.lbugPath);
|
|
91
91
|
}
|
|
92
92
|
// Step 1: BM25 search (fast, no embeddings)
|
|
93
|
-
const bm25Results = await searchFTSFromLbug(pattern, 10, repoId);
|
|
93
|
+
const { results: bm25Results } = await searchFTSFromLbug(pattern, 10, repoId);
|
|
94
94
|
if (bm25Results.length === 0)
|
|
95
95
|
return '';
|
|
96
96
|
// Step 2: Map BM25 file results to symbols
|
|
@@ -10,6 +10,11 @@ export interface BM25SearchResult {
|
|
|
10
10
|
rank: number;
|
|
11
11
|
nodeIds?: string[];
|
|
12
12
|
}
|
|
13
|
+
export interface FTSSearchResponse {
|
|
14
|
+
results: BM25SearchResult[];
|
|
15
|
+
/** True when at least one FTS index query succeeded (index exists). */
|
|
16
|
+
ftsAvailable: boolean;
|
|
17
|
+
}
|
|
13
18
|
/**
|
|
14
19
|
* Search using LadybugDB's built-in FTS (always fresh, reads from disk)
|
|
15
20
|
*
|
|
@@ -21,4 +26,4 @@ export interface BM25SearchResult {
|
|
|
21
26
|
* @param repoId - If provided, queries will be routed via the MCP connection pool
|
|
22
27
|
* @returns Ranked search results from FTS indexes
|
|
23
28
|
*/
|
|
24
|
-
export declare const searchFTSFromLbug: (query: string, limit?: number, repoId?: string) => Promise<
|
|
29
|
+
export declare const searchFTSFromLbug: (query: string, limit?: number, repoId?: string) => Promise<FTSSearchResponse>;
|
|
@@ -8,7 +8,8 @@ import { queryFTS } from '../lbug/lbug-adapter.js';
|
|
|
8
8
|
import { FTS_INDEXES } from './fts-schema.js';
|
|
9
9
|
/**
|
|
10
10
|
* Execute a single FTS query via a custom executor (for MCP connection pool).
|
|
11
|
-
* Returns the
|
|
11
|
+
* Returns `null` when the query fails (e.g. FTS index does not exist) so the
|
|
12
|
+
* caller can distinguish "zero matches" from "index missing".
|
|
12
13
|
*/
|
|
13
14
|
async function queryFTSViaExecutor(executor, tableName, indexName, query, limit) {
|
|
14
15
|
// Escape single quotes and backslashes to prevent Cypher injection
|
|
@@ -32,7 +33,7 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
|
|
|
32
33
|
});
|
|
33
34
|
}
|
|
34
35
|
catch {
|
|
35
|
-
return
|
|
36
|
+
return null;
|
|
36
37
|
}
|
|
37
38
|
}
|
|
38
39
|
/**
|
|
@@ -48,6 +49,7 @@ async function queryFTSViaExecutor(executor, tableName, indexName, query, limit)
|
|
|
48
49
|
*/
|
|
49
50
|
export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
50
51
|
const resultsByIndex = [];
|
|
52
|
+
let queriesSucceeded = 0;
|
|
51
53
|
if (repoId) {
|
|
52
54
|
// Use MCP connection pool via dynamic import
|
|
53
55
|
// IMPORTANT: FTS queries run sequentially to avoid connection contention.
|
|
@@ -56,15 +58,27 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
56
58
|
const { executeQuery } = poolMod;
|
|
57
59
|
const executor = (cypher) => executeQuery(repoId, cypher);
|
|
58
60
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
59
|
-
|
|
61
|
+
const result = await queryFTSViaExecutor(executor, table, indexName, query, limit);
|
|
62
|
+
if (result !== null) {
|
|
63
|
+
queriesSucceeded++;
|
|
64
|
+
resultsByIndex.push(result);
|
|
65
|
+
}
|
|
60
66
|
}
|
|
61
67
|
}
|
|
62
68
|
else {
|
|
63
69
|
// Use core lbug adapter (CLI / pipeline context) — also sequential for safety.
|
|
64
70
|
for (const { table, indexName } of FTS_INDEXES) {
|
|
65
|
-
|
|
71
|
+
try {
|
|
72
|
+
const result = await queryFTS(table, indexName, query, limit, false);
|
|
73
|
+
queriesSucceeded++;
|
|
74
|
+
resultsByIndex.push(result);
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
// FTS index may not exist — count as failed
|
|
78
|
+
}
|
|
66
79
|
}
|
|
67
80
|
}
|
|
81
|
+
const ftsAvailable = queriesSucceeded > 0;
|
|
68
82
|
// Collect all node scores per filePath to track which nodes actually matched
|
|
69
83
|
const fileNodeScores = new Map();
|
|
70
84
|
const addResults = (results) => {
|
|
@@ -92,10 +106,13 @@ export const searchFTSFromLbug = async (query, limit = 20, repoId) => {
|
|
|
92
106
|
const sorted = Array.from(merged.values())
|
|
93
107
|
.sort((a, b) => b.score - a.score)
|
|
94
108
|
.slice(0, limit);
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
109
|
+
return {
|
|
110
|
+
results: sorted.map((r, index) => ({
|
|
111
|
+
filePath: r.filePath,
|
|
112
|
+
score: r.score,
|
|
113
|
+
rank: index + 1,
|
|
114
|
+
nodeIds: r.nodeIds,
|
|
115
|
+
})),
|
|
116
|
+
ftsAvailable,
|
|
117
|
+
};
|
|
101
118
|
};
|
|
@@ -32,9 +32,10 @@ export interface HybridSearchResult {
|
|
|
32
32
|
*/
|
|
33
33
|
export declare const mergeWithRRF: (bm25Results: BM25SearchResult[], semanticResults: SemanticSearchResult[], limit?: number) => HybridSearchResult[];
|
|
34
34
|
/**
|
|
35
|
-
* Check if hybrid search is available
|
|
36
|
-
*
|
|
37
|
-
*
|
|
35
|
+
* Check if hybrid search is available.
|
|
36
|
+
* FTS indexes may be missing on read-only MCP connections (see #1403);
|
|
37
|
+
* callers should inspect `ftsAvailable` from searchFTSFromLbug for
|
|
38
|
+
* per-query availability. This helper is a coarse gate only.
|
|
38
39
|
*/
|
|
39
40
|
export declare const isHybridSearchReady: () => boolean;
|
|
40
41
|
/**
|
|
@@ -79,12 +79,13 @@ export const mergeWithRRF = (bm25Results, semanticResults, limit = 10) => {
|
|
|
79
79
|
return sorted;
|
|
80
80
|
};
|
|
81
81
|
/**
|
|
82
|
-
* Check if hybrid search is available
|
|
83
|
-
*
|
|
84
|
-
*
|
|
82
|
+
* Check if hybrid search is available.
|
|
83
|
+
* FTS indexes may be missing on read-only MCP connections (see #1403);
|
|
84
|
+
* callers should inspect `ftsAvailable` from searchFTSFromLbug for
|
|
85
|
+
* per-query availability. This helper is a coarse gate only.
|
|
85
86
|
*/
|
|
86
87
|
export const isHybridSearchReady = () => {
|
|
87
|
-
return true; // FTS is
|
|
88
|
+
return true; // FTS is attempted on every query; ftsAvailable signals actual availability
|
|
88
89
|
};
|
|
89
90
|
/**
|
|
90
91
|
* Format hybrid results for LLM consumption
|
|
@@ -112,7 +113,7 @@ export const formatHybridResults = (results) => {
|
|
|
112
113
|
*/
|
|
113
114
|
export const hybridSearch = async (query, limit, executeQuery, semanticSearch) => {
|
|
114
115
|
// Use LadybugDB FTS for always-fresh BM25 results
|
|
115
|
-
const bm25Results = await searchFTSFromLbug(query, limit);
|
|
116
|
+
const { results: bm25Results } = await searchFTSFromLbug(query, limit);
|
|
116
117
|
const semanticResults = await semanticSearch(executeQuery, query, limit);
|
|
117
118
|
return mergeWithRRF(bm25Results, semanticResults, limit);
|
|
118
119
|
};
|
|
@@ -818,7 +818,7 @@ export class LocalBackend {
|
|
|
818
818
|
definitions: definitions.slice(0, 20), // cap standalone definitions
|
|
819
819
|
timing,
|
|
820
820
|
...(!ftsUsed && {
|
|
821
|
-
warning: 'FTS
|
|
821
|
+
warning: 'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.',
|
|
822
822
|
}),
|
|
823
823
|
};
|
|
824
824
|
}
|
|
@@ -827,15 +827,16 @@ export class LocalBackend {
|
|
|
827
827
|
*/
|
|
828
828
|
async bm25Search(repo, query, limit) {
|
|
829
829
|
const { searchFTSFromLbug } = await import('../../core/search/bm25-index.js');
|
|
830
|
-
let
|
|
830
|
+
let ftsResponse;
|
|
831
831
|
try {
|
|
832
|
-
|
|
832
|
+
ftsResponse = await searchFTSFromLbug(query, limit, repo.id);
|
|
833
833
|
}
|
|
834
834
|
catch (err) {
|
|
835
835
|
logger.error({ err: err.message }, 'GitNexus: BM25/FTS search failed (FTS indexes may not exist) -');
|
|
836
836
|
return { results: [], ftsUsed: false };
|
|
837
837
|
}
|
|
838
|
-
const
|
|
838
|
+
const bm25Results = ftsResponse.results;
|
|
839
|
+
const ftsUsed = ftsResponse.ftsAvailable;
|
|
839
840
|
const results = [];
|
|
840
841
|
for (const bm25Result of bm25Results) {
|
|
841
842
|
const fullPath = bm25Result.filePath;
|
package/dist/server/api.js
CHANGED
|
@@ -932,10 +932,11 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
932
932
|
const enrich = req.body.enrich !== false; // default true
|
|
933
933
|
const results = await withLbugDb(lbugPath, async () => {
|
|
934
934
|
let searchResults;
|
|
935
|
+
let ftsAvailable;
|
|
935
936
|
if (mode === 'semantic') {
|
|
936
937
|
const { isEmbedderReady } = await import('../core/embeddings/embedder.js');
|
|
937
938
|
if (!isEmbedderReady()) {
|
|
938
|
-
return [];
|
|
939
|
+
return { searchResults: [], ftsAvailable: undefined };
|
|
939
940
|
}
|
|
940
941
|
const { semanticSearch: semSearch } = await import('../core/embeddings/embedding-pipeline.js');
|
|
941
942
|
searchResults = await semSearch(executeQuery, query, limit);
|
|
@@ -948,8 +949,9 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
948
949
|
}));
|
|
949
950
|
}
|
|
950
951
|
else if (mode === 'bm25') {
|
|
951
|
-
|
|
952
|
-
|
|
952
|
+
const ftsResponse = await searchFTSFromLbug(query, limit);
|
|
953
|
+
ftsAvailable = ftsResponse.ftsAvailable;
|
|
954
|
+
searchResults = ftsResponse.results.map((r, i) => ({
|
|
953
955
|
...r,
|
|
954
956
|
rank: i + 1,
|
|
955
957
|
sources: ['bm25'],
|
|
@@ -963,11 +965,13 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
963
965
|
searchResults = await hybridSearch(query, limit, executeQuery, semSearch);
|
|
964
966
|
}
|
|
965
967
|
else {
|
|
966
|
-
|
|
968
|
+
const ftsResponse = await searchFTSFromLbug(query, limit);
|
|
969
|
+
ftsAvailable = ftsResponse.ftsAvailable;
|
|
970
|
+
searchResults = ftsResponse.results;
|
|
967
971
|
}
|
|
968
972
|
}
|
|
969
973
|
if (!enrich)
|
|
970
|
-
return searchResults;
|
|
974
|
+
return { searchResults, ftsAvailable };
|
|
971
975
|
// Server-side enrichment: add connections, cluster, processes per result
|
|
972
976
|
// Uses parameterized queries to prevent Cypher injection via nodeId
|
|
973
977
|
const validLabel = (label) => NODE_TABLES.includes(label);
|
|
@@ -1029,9 +1033,14 @@ export const createServer = async (port, host = '127.0.0.1') => {
|
|
|
1029
1033
|
}
|
|
1030
1034
|
return { ...r, ...enrichment };
|
|
1031
1035
|
}));
|
|
1032
|
-
return enriched;
|
|
1036
|
+
return { searchResults: enriched, ftsAvailable };
|
|
1033
1037
|
});
|
|
1034
|
-
|
|
1038
|
+
const response = { results: results.searchResults ?? results };
|
|
1039
|
+
if (results.ftsAvailable === false) {
|
|
1040
|
+
response.warning =
|
|
1041
|
+
'FTS indexes missing — keyword search degraded. Run: gitnexus analyze --force to rebuild indexes.';
|
|
1042
|
+
}
|
|
1043
|
+
res.json(response);
|
|
1035
1044
|
}
|
|
1036
1045
|
catch (err) {
|
|
1037
1046
|
res.status(500).json({ error: err.message || 'Search failed' });
|
package/package.json
CHANGED