@jcode.labs/ragmir 2.17.1 → 2.18.0
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/README.md +53 -6
- package/dist/access-log.d.ts +10 -0
- package/dist/access-log.d.ts.map +1 -1
- package/dist/access-log.js +119 -49
- package/dist/access-log.js.map +1 -1
- package/dist/chunking.d.ts +5 -1
- package/dist/chunking.d.ts.map +1 -1
- package/dist/chunking.js +102 -16
- package/dist/chunking.js.map +1 -1
- package/dist/citation.d.ts +18 -0
- package/dist/citation.d.ts.map +1 -0
- package/dist/citation.js +54 -0
- package/dist/citation.js.map +1 -0
- package/dist/cli-entry.d.ts +3 -0
- package/dist/cli-entry.d.ts.map +1 -0
- package/dist/cli-entry.js +16 -0
- package/dist/cli-entry.js.map +1 -0
- package/dist/cli-fast.d.ts +13 -0
- package/dist/cli-fast.d.ts.map +1 -0
- package/dist/cli-fast.js +145 -0
- package/dist/cli-fast.js.map +1 -0
- package/dist/cli-options.d.ts +3 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +7 -0
- package/dist/cli-options.js.map +1 -1
- package/dist/cli-stdin.d.ts +3 -0
- package/dist/cli-stdin.d.ts.map +1 -0
- package/dist/cli-stdin.js +15 -0
- package/dist/cli-stdin.js.map +1 -0
- package/dist/cli.js +254 -47
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +13 -2
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +155 -10
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +6 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +227 -49
- package/dist/config.js.map +1 -1
- package/dist/context-resources.d.ts +4 -2
- package/dist/context-resources.d.ts.map +1 -1
- package/dist/context-resources.js +55 -24
- package/dist/context-resources.js.map +1 -1
- package/dist/defaults.d.ts +27 -0
- package/dist/defaults.d.ts.map +1 -1
- package/dist/defaults.js +42 -2
- package/dist/defaults.js.map +1 -1
- package/dist/destroy.d.ts +2 -2
- package/dist/destroy.d.ts.map +1 -1
- package/dist/destroy.js +34 -20
- package/dist/destroy.js.map +1 -1
- package/dist/doctor.d.ts +3 -2
- package/dist/doctor.d.ts.map +1 -1
- package/dist/doctor.js +86 -41
- package/dist/doctor.js.map +1 -1
- package/dist/durable-file.d.ts +13 -0
- package/dist/durable-file.d.ts.map +1 -0
- package/dist/durable-file.js +56 -0
- package/dist/durable-file.js.map +1 -0
- package/dist/embeddings.d.ts +14 -3
- package/dist/embeddings.d.ts.map +1 -1
- package/dist/embeddings.js +233 -95
- package/dist/embeddings.js.map +1 -1
- package/dist/errors.d.ts +1 -1
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js.map +1 -1
- package/dist/evaluate.d.ts +4 -3
- package/dist/evaluate.d.ts.map +1 -1
- package/dist/evaluate.js +611 -110
- package/dist/evaluate.js.map +1 -1
- package/dist/files.d.ts +4 -1
- package/dist/files.d.ts.map +1 -1
- package/dist/files.js +119 -33
- package/dist/files.js.map +1 -1
- package/dist/generation-retention.d.ts +52 -0
- package/dist/generation-retention.d.ts.map +1 -0
- package/dist/generation-retention.js +318 -0
- package/dist/generation-retention.js.map +1 -0
- package/dist/index-diagnostics.d.ts +3 -2
- package/dist/index-diagnostics.d.ts.map +1 -1
- package/dist/index-diagnostics.js +32 -3
- package/dist/index-diagnostics.js.map +1 -1
- package/dist/index-policy.d.ts.map +1 -1
- package/dist/index-policy.js +3 -0
- package/dist/index-policy.js.map +1 -1
- package/dist/index-write-lock.d.ts +16 -1
- package/dist/index-write-lock.d.ts.map +1 -1
- package/dist/index-write-lock.js +392 -13
- package/dist/index-write-lock.js.map +1 -1
- package/dist/index.d.ts +12 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -2
- package/dist/index.js.map +1 -1
- package/dist/ingest.d.ts +11 -2
- package/dist/ingest.d.ts.map +1 -1
- package/dist/ingest.js +680 -165
- package/dist/ingest.js.map +1 -1
- package/dist/ingestion-metrics.d.ts +58 -0
- package/dist/ingestion-metrics.d.ts.map +1 -0
- package/dist/ingestion-metrics.js +216 -0
- package/dist/ingestion-metrics.js.map +1 -0
- package/dist/ingestion-state.d.ts +11 -2
- package/dist/ingestion-state.d.ts.map +1 -1
- package/dist/ingestion-state.js +480 -49
- package/dist/ingestion-state.js.map +1 -1
- package/dist/limits.d.ts.map +1 -1
- package/dist/limits.js +11 -1
- package/dist/limits.js.map +1 -1
- package/dist/mcp-output.d.ts +44 -4
- package/dist/mcp-output.d.ts.map +1 -1
- package/dist/mcp-output.js +132 -159
- package/dist/mcp-output.js.map +1 -1
- package/dist/mcp-summaries.d.ts +32 -0
- package/dist/mcp-summaries.d.ts.map +1 -0
- package/dist/mcp-summaries.js +227 -0
- package/dist/mcp-summaries.js.map +1 -0
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/mcp.js +85 -46
- package/dist/mcp.js.map +1 -1
- package/dist/ocr-cache.d.ts +15 -0
- package/dist/ocr-cache.d.ts.map +1 -0
- package/dist/ocr-cache.js +90 -0
- package/dist/ocr-cache.js.map +1 -0
- package/dist/ocr.d.ts +31 -0
- package/dist/ocr.d.ts.map +1 -1
- package/dist/ocr.js +193 -17
- package/dist/ocr.js.map +1 -1
- package/dist/parsing.d.ts.map +1 -1
- package/dist/parsing.js +482 -85
- package/dist/parsing.js.map +1 -1
- package/dist/permissions.js +2 -2
- package/dist/permissions.js.map +1 -1
- package/dist/preview.d.ts.map +1 -1
- package/dist/preview.js +10 -11
- package/dist/preview.js.map +1 -1
- package/dist/quality-report.d.ts +12 -0
- package/dist/quality-report.d.ts.map +1 -0
- package/dist/quality-report.js +146 -0
- package/dist/quality-report.js.map +1 -0
- package/dist/query.d.ts +7 -4
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +371 -284
- package/dist/query.js.map +1 -1
- package/dist/ranking.d.ts +38 -0
- package/dist/ranking.d.ts.map +1 -0
- package/dist/ranking.js +278 -0
- package/dist/ranking.js.map +1 -0
- package/dist/redaction.d.ts +6 -1
- package/dist/redaction.d.ts.map +1 -1
- package/dist/redaction.js +118 -4
- package/dist/redaction.js.map +1 -1
- package/dist/research.d.ts +6 -1
- package/dist/research.d.ts.map +1 -1
- package/dist/research.js +330 -130
- package/dist/research.js.map +1 -1
- package/dist/runtime-info.d.ts +3 -0
- package/dist/runtime-info.d.ts.map +1 -0
- package/dist/runtime-info.js +81 -0
- package/dist/runtime-info.js.map +1 -0
- package/dist/security.d.ts +3 -2
- package/dist/security.d.ts.map +1 -1
- package/dist/security.js +242 -45
- package/dist/security.js.map +1 -1
- package/dist/semantic-config.d.ts +7 -1
- package/dist/semantic-config.d.ts.map +1 -1
- package/dist/semantic-config.js +30 -4
- package/dist/semantic-config.js.map +1 -1
- package/dist/setup.js +6 -3
- package/dist/setup.js.map +1 -1
- package/dist/skill.js +34 -12
- package/dist/skill.js.map +1 -1
- package/dist/source-fingerprint-cache.d.ts +20 -0
- package/dist/source-fingerprint-cache.d.ts.map +1 -0
- package/dist/source-fingerprint-cache.js +133 -0
- package/dist/source-fingerprint-cache.js.map +1 -0
- package/dist/storage-maintenance.d.ts +46 -0
- package/dist/storage-maintenance.d.ts.map +1 -0
- package/dist/storage-maintenance.js +329 -0
- package/dist/storage-maintenance.js.map +1 -0
- package/dist/store.d.ts +39 -2
- package/dist/store.d.ts.map +1 -1
- package/dist/store.js +559 -24
- package/dist/store.js.map +1 -1
- package/dist/text.d.ts +1 -1
- package/dist/text.d.ts.map +1 -1
- package/dist/text.js +12 -2
- package/dist/text.js.map +1 -1
- package/dist/types.d.ts +414 -5
- package/dist/types.d.ts.map +1 -1
- package/dist/vector-index.d.ts +42 -0
- package/dist/vector-index.d.ts.map +1 -0
- package/dist/vector-index.js +249 -0
- package/dist/vector-index.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/workload.d.ts +22 -0
- package/dist/workload.d.ts.map +1 -0
- package/dist/workload.js +181 -0
- package/dist/workload.js.map +1 -0
- package/examples/document-evidence-benchmark/README.md +3 -2
- package/examples/document-evidence-benchmark/golden-queries.json +1 -1
- package/package.json +36 -11
package/dist/query.js
CHANGED
|
@@ -1,37 +1,40 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { channel } from "node:diagnostics_channel";
|
|
2
|
+
import { MatchQuery, Operator, PhraseQuery, } from "@lancedb/lancedb";
|
|
3
|
+
import { flushAccessLog, recordAccess } from "./access-log.js";
|
|
4
|
+
import { citationForCoordinates, stripCitationCoordinates } from "./citation.js";
|
|
2
5
|
import { loadConfig } from "./config.js";
|
|
3
|
-
import { VECTOR_DISTANCE_METRIC } from "./defaults.js";
|
|
6
|
+
import { MAX_SEARCH_TOP_K, VECTOR_DISTANCE_METRIC } from "./defaults.js";
|
|
4
7
|
import { embedText } from "./embeddings.js";
|
|
5
8
|
import { RagmirError } from "./errors.js";
|
|
6
|
-
import {
|
|
9
|
+
import { acquireGenerationReadLease } from "./generation-retention.js";
|
|
10
|
+
import { indexFreshnessWarning } from "./index-diagnostics.js";
|
|
7
11
|
import { operationSignal, throwIfAborted } from "./operation.js";
|
|
8
12
|
import { sanitizeRetrievalQuery } from "./query-sanitizer.js";
|
|
9
|
-
import {
|
|
13
|
+
import { candidatePassesAbstention, queryEvidence, rankHybridRows, rankingPolicyFingerprint, rankingPolicyFor, tokensAreLexicallyRelated, } from "./ranking.js";
|
|
14
|
+
import { closeIndexReadSnapshot, loadIndexReadSnapshot } from "./store.js";
|
|
10
15
|
import { tokenize } from "./text.js";
|
|
16
|
+
import { configureAdaptiveVectorQuery, vectorIndexManifestCompatible } from "./vector-index.js";
|
|
17
|
+
import { runWorkload } from "./workload.js";
|
|
11
18
|
const VECTOR_CANDIDATE_POLICY = {
|
|
12
19
|
fast: { minimum: 40, multiplier: 3, maxChunksPerSource: 1 },
|
|
13
20
|
balanced: { minimum: 80, multiplier: 4, maxChunksPerSource: 2 },
|
|
14
21
|
quality: { minimum: 200, multiplier: 8, maxChunksPerSource: 4 },
|
|
15
22
|
custom: { minimum: 80, multiplier: 4, maxChunksPerSource: 2 },
|
|
16
23
|
};
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
* Equal weights let exact lexical evidence rescue a relevant document that is
|
|
25
|
-
* absent from the bounded vector candidate pool.
|
|
26
|
-
*/
|
|
27
|
-
const RRF_K = 60;
|
|
28
|
-
const RRF_VECTOR_WEIGHT = 1;
|
|
29
|
-
const RRF_LEXICAL_WEIGHT = 1;
|
|
30
|
-
const BM25_K1 = 1.2;
|
|
31
|
-
const BM25_B = 0.75;
|
|
24
|
+
const LEXICAL_CANDIDATE_POLICY = {
|
|
25
|
+
fast: { minimum: 100, multiplier: 10 },
|
|
26
|
+
balanced: { minimum: 250, multiplier: 20 },
|
|
27
|
+
quality: { minimum: 500, multiplier: 40 },
|
|
28
|
+
custom: { minimum: 250, multiplier: 20 },
|
|
29
|
+
};
|
|
32
30
|
const MAX_CONTEXT_RADIUS = 3;
|
|
33
|
-
const
|
|
34
|
-
const
|
|
31
|
+
const MAX_VECTOR_CANDIDATES = 1_000;
|
|
32
|
+
const MAX_LEXICAL_CANDIDATES = 4_000;
|
|
33
|
+
const FULL_TEXT_INDEX_NAME = "searchText_idx";
|
|
34
|
+
const LEXICAL_IDENTIFIER_PATTERN = /[\p{L}\p{N}]+(?:[-_.][\p{L}\p{N}]+)+/gu;
|
|
35
|
+
const SOURCE_PATH_QUERY_PATTERN = /^[\p{L}\p{N}_. /\\-]+\.[a-z0-9]{1,12}$/iu;
|
|
36
|
+
export const QUERY_EXPLANATION_DIAGNOSTICS_CHANNEL = "ragmir:query-explanation";
|
|
37
|
+
const queryExplanationDiagnostics = channel(QUERY_EXPLANATION_DIAGNOSTICS_CHANNEL);
|
|
35
38
|
const SEARCH_COLUMNS = [
|
|
36
39
|
"source",
|
|
37
40
|
"relativePath",
|
|
@@ -45,50 +48,88 @@ const SEARCH_COLUMNS = [
|
|
|
45
48
|
"lineEnd",
|
|
46
49
|
"pageStart",
|
|
47
50
|
"pageEnd",
|
|
51
|
+
"locationKind",
|
|
52
|
+
"locationStart",
|
|
53
|
+
"locationEnd",
|
|
54
|
+
"locationLabel",
|
|
55
|
+
"cellStart",
|
|
56
|
+
"cellEnd",
|
|
48
57
|
];
|
|
49
58
|
const VECTOR_SEARCH_COLUMNS = [...SEARCH_COLUMNS, "_distance"];
|
|
50
59
|
const FTS_SEARCH_COLUMNS = [...SEARCH_COLUMNS, "_score"];
|
|
51
60
|
export async function search(query, options = {}) {
|
|
52
61
|
const config = await loadConfig(String(options.cwd ?? process.cwd()));
|
|
53
|
-
|
|
62
|
+
const results = await searchWithConfig(query, options, config);
|
|
63
|
+
await flushAccessLog(config);
|
|
64
|
+
return results;
|
|
54
65
|
}
|
|
55
|
-
export async function searchWithConfig(query, options, config, connection, activeSignal) {
|
|
66
|
+
export async function searchWithConfig(query, options, config, connection, activeSignal, suppliedSnapshot, generationLeaseHeld = false) {
|
|
67
|
+
const signal = activeSignal ?? operationSignal(options);
|
|
68
|
+
return runWorkload(config, "search", signal, async ({ queueTimeMs }) => {
|
|
69
|
+
const ownsSnapshot = suppliedSnapshot === undefined;
|
|
70
|
+
const snapshot = suppliedSnapshot ?? (await loadIndexReadSnapshot(config, connection));
|
|
71
|
+
let lease;
|
|
72
|
+
try {
|
|
73
|
+
if (!generationLeaseHeld && snapshot.table) {
|
|
74
|
+
lease = await acquireGenerationReadLease(snapshot.tableName, config);
|
|
75
|
+
}
|
|
76
|
+
return await searchWithinGeneration(query, options, config, snapshot, signal, queueTimeMs);
|
|
77
|
+
}
|
|
78
|
+
finally {
|
|
79
|
+
await lease?.release().catch(() => undefined);
|
|
80
|
+
if (ownsSnapshot) {
|
|
81
|
+
closeIndexReadSnapshot(snapshot, config);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
async function searchWithinGeneration(query, options, config, snapshot, activeSignal, workloadQueueMs = 0) {
|
|
56
87
|
const signal = activeSignal ?? operationSignal(options);
|
|
57
88
|
const topK = normalizeTopK(options.topK ?? config.topK);
|
|
58
89
|
const defaultContextRadius = config.retrievalProfile === "quality" ? 1 : 0;
|
|
59
90
|
const contextRadius = normalizeContextRadius(options.contextRadius ?? defaultContextRadius);
|
|
60
91
|
throwIfAborted(signal);
|
|
61
|
-
const table =
|
|
92
|
+
const table = snapshot.table;
|
|
62
93
|
throwIfAborted(signal);
|
|
63
94
|
if (!table) {
|
|
64
95
|
return [];
|
|
65
96
|
}
|
|
66
|
-
|
|
97
|
+
assertIndexFreshness(config, snapshot.manifest);
|
|
67
98
|
const sanitized = sanitizeRetrievalQuery(query);
|
|
68
|
-
const
|
|
99
|
+
const evidence = queryEvidence(sanitized.query);
|
|
100
|
+
const queryTokens = evidence.tokens;
|
|
69
101
|
if (!sanitized.query || queryTokens.length === 0) {
|
|
70
102
|
return [];
|
|
71
103
|
}
|
|
72
104
|
const retrievalPredicate = searchPredicate(options.includePaths, options.excludePaths, options.contextPaths);
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
105
|
+
let embeddingQueueMs = 0;
|
|
106
|
+
const [vector, lexicalCandidates] = await Promise.all([
|
|
107
|
+
embedText(sanitized.query, config, signal, ({ queueTimeMs }) => {
|
|
108
|
+
embeddingQueueMs = queueTimeMs;
|
|
109
|
+
}),
|
|
110
|
+
lexicalCandidateRows(table, sanitized.query, {
|
|
111
|
+
ftsLimit: lexicalCandidateLimit(topK, config.retrievalProfile),
|
|
112
|
+
fallbackLimit: config.hybridTextScanLimit,
|
|
113
|
+
indexedChunkCount: snapshot.manifest?.chunkCount ?? 0,
|
|
114
|
+
minimumResults: topK,
|
|
115
|
+
pathPredicate: retrievalPredicate,
|
|
116
|
+
}),
|
|
76
117
|
]);
|
|
77
118
|
throwIfAborted(signal);
|
|
78
|
-
|
|
79
|
-
const vectorQuery = table.vectorSearch(vector).select(VECTOR_SEARCH_COLUMNS);
|
|
119
|
+
const manifest = assertVectorIndexCompatibility(config, snapshot.manifest, vector.length);
|
|
120
|
+
const vectorQuery = configureAdaptiveVectorQuery(table.vectorSearch(vector).select(VECTOR_SEARCH_COLUMNS), manifest.vectorIndex, options.vectorSearchMode === "exact");
|
|
80
121
|
const vectorRows = (await (retrievalPredicate
|
|
81
122
|
? vectorQuery.where(retrievalPredicate)
|
|
82
123
|
: vectorQuery)
|
|
83
124
|
.limit(vectorCandidateLimit(topK, config.retrievalProfile))
|
|
84
125
|
.toArray());
|
|
85
126
|
throwIfAborted(signal);
|
|
86
|
-
const
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
const rows = diversifyRows(relevantRows, topK, config.retrievalProfile);
|
|
91
|
-
const contextByRow = await contextChunksByRow(table, rows, contextRadius);
|
|
127
|
+
const rankingPolicy = rankingPolicyFor(config.embeddingProvider, config.retrievalProfile);
|
|
128
|
+
const rankedRows = rankHybridRows(sanitized.query, vectorRows, lexicalCandidates.rows, rankingPolicy);
|
|
129
|
+
const relevantRows = rankedRows.filter((ranked) => lexicalCandidates.exactPathMatches.has(rowKey(ranked.row)) ||
|
|
130
|
+
candidatePassesAbstention(evidence, ranked.row, rankingPolicy));
|
|
131
|
+
const rows = diversifyRows(relevantRows, topK, config.retrievalProfile, lexicalCandidates.exactPathMatches);
|
|
132
|
+
const contextByRow = await contextChunksByRow(table, rows, contextRadius, retrievalPredicate);
|
|
92
133
|
throwIfAborted(signal);
|
|
93
134
|
const results = rows.map((row) => {
|
|
94
135
|
const vectorDistance = typeof row.row._distance === "number" ? row.row._distance : null;
|
|
@@ -102,8 +143,8 @@ export async function searchWithConfig(query, options, config, connection, activ
|
|
|
102
143
|
distance: vectorDistance,
|
|
103
144
|
charStart: nullableNumber(row.row.charStart),
|
|
104
145
|
charEnd: nullableNumber(row.row.charEnd),
|
|
105
|
-
lineStart:
|
|
106
|
-
lineEnd:
|
|
146
|
+
lineStart: nullableLineNumber(row.row.lineStart),
|
|
147
|
+
lineEnd: nullableLineNumber(row.row.lineEnd),
|
|
107
148
|
pageStart: nullablePageNumber(row.row.pageStart),
|
|
108
149
|
pageEnd: nullablePageNumber(row.row.pageEnd),
|
|
109
150
|
context: contextByRow.get(rowKey(row.row)) ?? [],
|
|
@@ -118,13 +159,25 @@ export async function searchWithConfig(query, options, config, connection, activ
|
|
|
118
159
|
lexicalRank: row.lexicalRank,
|
|
119
160
|
vectorDistance,
|
|
120
161
|
lexicalBackendScore: row.lexicalBackendScore,
|
|
121
|
-
|
|
162
|
+
lexicalBackend: lexicalCandidates.backend,
|
|
163
|
+
lexicalFallbackActivated: lexicalCandidates.fallbackActivated,
|
|
164
|
+
lexicalFallbackReason: lexicalCandidates.fallbackReason,
|
|
165
|
+
lexicalExactPathMatch: lexicalCandidates.exactPathMatches.has(rowKey(row.row)),
|
|
166
|
+
lexicalCandidateLimit: lexicalCandidates.candidateLimit,
|
|
167
|
+
lexicalCandidatesMaterialized: lexicalCandidates.candidatesMaterialized,
|
|
168
|
+
lexicalQueryVariants: lexicalCandidates.queryVariants,
|
|
169
|
+
lexicalIndexedRows: lexicalCandidates.indexedRows,
|
|
170
|
+
lexicalUnindexedRows: lexicalCandidates.unindexedRows,
|
|
171
|
+
lexicalCoverage: lexicalCandidates.coverage,
|
|
172
|
+
workloadQueueMs: workloadQueueMs + embeddingQueueMs,
|
|
173
|
+
rankingPolicyFingerprint: rankingPolicyFingerprint(rankingPolicy),
|
|
174
|
+
matchedTerms: matchedQueryTerms(config.projectRoot, queryTokens, row.row.searchText),
|
|
122
175
|
},
|
|
123
176
|
}
|
|
124
177
|
: {}),
|
|
125
178
|
};
|
|
126
179
|
});
|
|
127
|
-
|
|
180
|
+
void recordAccess(config, {
|
|
128
181
|
action: "search",
|
|
129
182
|
query: sanitized.query,
|
|
130
183
|
topK,
|
|
@@ -132,11 +185,11 @@ export async function searchWithConfig(query, options, config, connection, activ
|
|
|
132
185
|
});
|
|
133
186
|
return results;
|
|
134
187
|
}
|
|
135
|
-
function diversifyRows(rows, topK, profile) {
|
|
188
|
+
function diversifyRows(rows, topK, profile, exactPathMatches) {
|
|
136
189
|
const uniqueRows = [];
|
|
137
190
|
const textIndexes = new Map();
|
|
138
191
|
for (const row of rows) {
|
|
139
|
-
const textKey =
|
|
192
|
+
const textKey = row.row.text.replace(/\s+/gu, " ").trim().toLowerCase();
|
|
140
193
|
const existingIndex = textIndexes.get(textKey);
|
|
141
194
|
if (existingIndex === undefined) {
|
|
142
195
|
textIndexes.set(textKey, uniqueRows.length);
|
|
@@ -144,23 +197,43 @@ function diversifyRows(rows, topK, profile) {
|
|
|
144
197
|
continue;
|
|
145
198
|
}
|
|
146
199
|
const existing = uniqueRows[existingIndex];
|
|
147
|
-
|
|
200
|
+
const existingIsExact = existing ? exactPathMatches.has(rowKey(existing.row)) : false;
|
|
201
|
+
const candidateIsExact = exactPathMatches.has(rowKey(row.row));
|
|
202
|
+
if (existing &&
|
|
203
|
+
(candidateIsExact !== existingIsExact
|
|
204
|
+
? candidateIsExact
|
|
205
|
+
: preferCanonicalPath(row.row.relativePath, existing.row.relativePath))) {
|
|
148
206
|
uniqueRows[existingIndex] = row;
|
|
149
207
|
}
|
|
150
208
|
}
|
|
151
209
|
const selected = [];
|
|
210
|
+
const selectedKeys = new Set();
|
|
152
211
|
const perSource = new Map();
|
|
153
212
|
const maxChunksPerSource = VECTOR_CANDIDATE_POLICY[profile].maxChunksPerSource;
|
|
154
|
-
|
|
213
|
+
const appendRow = (row, enforceSourceCap) => {
|
|
214
|
+
const key = rowKey(row.row);
|
|
215
|
+
if (selectedKeys.has(key)) {
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
155
218
|
if (selected.some((candidate) => overlapsSourceSpan(candidate.row, row.row))) {
|
|
156
|
-
|
|
219
|
+
return;
|
|
157
220
|
}
|
|
158
221
|
const sourceCount = perSource.get(row.row.relativePath) ?? 0;
|
|
159
|
-
if (sourceCount >= maxChunksPerSource) {
|
|
160
|
-
|
|
222
|
+
if (enforceSourceCap && sourceCount >= maxChunksPerSource) {
|
|
223
|
+
return;
|
|
161
224
|
}
|
|
162
225
|
selected.push(row);
|
|
226
|
+
selectedKeys.add(key);
|
|
163
227
|
perSource.set(row.row.relativePath, sourceCount + 1);
|
|
228
|
+
};
|
|
229
|
+
for (const row of uniqueRows) {
|
|
230
|
+
appendRow(row, true);
|
|
231
|
+
if (selected.length >= topK) {
|
|
232
|
+
return selected;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
for (const row of uniqueRows) {
|
|
236
|
+
appendRow(row, false);
|
|
164
237
|
if (selected.length >= topK) {
|
|
165
238
|
break;
|
|
166
239
|
}
|
|
@@ -185,63 +258,23 @@ function preferCanonicalPath(candidate, current) {
|
|
|
185
258
|
return (candidateDepth < currentDepth ||
|
|
186
259
|
(candidateDepth === currentDepth && candidate.length < current.length));
|
|
187
260
|
}
|
|
188
|
-
function hasLexicalOverlap(queryTokens, text) {
|
|
189
|
-
const textTokens = tokenize(text);
|
|
190
|
-
return queryTokens.some((queryToken) => textTokens.some((textToken) => tokensAreLexicallyRelated(queryToken, textToken)));
|
|
191
|
-
}
|
|
192
|
-
function tokensAreLexicallyRelated(queryToken, textToken) {
|
|
193
|
-
if (queryToken === textToken) {
|
|
194
|
-
return true;
|
|
195
|
-
}
|
|
196
|
-
if (queryToken.length >= 4 &&
|
|
197
|
-
textToken.length >= 4 &&
|
|
198
|
-
sharedPrefixLength(queryToken, textToken) >= 4) {
|
|
199
|
-
return true;
|
|
200
|
-
}
|
|
201
|
-
if (queryToken.length < MIN_FUZZY_TOKEN_LENGTH ||
|
|
202
|
-
textToken.length < MIN_FUZZY_TOKEN_LENGTH ||
|
|
203
|
-
Math.abs(queryToken.length - textToken.length) > 1 ||
|
|
204
|
-
!/^[a-z0-9_-]+$/u.test(queryToken) ||
|
|
205
|
-
!/^[a-z0-9_-]+$/u.test(textToken)) {
|
|
206
|
-
return false;
|
|
207
|
-
}
|
|
208
|
-
return trigramDiceSimilarity(queryToken, textToken) >= MIN_TRIGRAM_DICE_SIMILARITY;
|
|
209
|
-
}
|
|
210
|
-
function trigramDiceSimilarity(left, right) {
|
|
211
|
-
const leftTrigrams = tokenTrigrams(left);
|
|
212
|
-
const rightTrigrams = tokenTrigrams(right);
|
|
213
|
-
let shared = 0;
|
|
214
|
-
for (const trigram of leftTrigrams) {
|
|
215
|
-
if (rightTrigrams.has(trigram)) {
|
|
216
|
-
shared += 1;
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
return (2 * shared) / (leftTrigrams.size + rightTrigrams.size);
|
|
220
|
-
}
|
|
221
|
-
function tokenTrigrams(token) {
|
|
222
|
-
return new Set(Array.from({ length: token.length - 2 }, (_value, index) => token.slice(index, index + 3)));
|
|
223
|
-
}
|
|
224
|
-
function sharedPrefixLength(left, right) {
|
|
225
|
-
const limit = Math.min(left.length, right.length);
|
|
226
|
-
let index = 0;
|
|
227
|
-
while (index < limit && left[index] === right[index]) {
|
|
228
|
-
index += 1;
|
|
229
|
-
}
|
|
230
|
-
return index;
|
|
231
|
-
}
|
|
232
261
|
export function vectorCandidateLimit(topK, profile = "balanced") {
|
|
233
262
|
const policy = VECTOR_CANDIDATE_POLICY[profile];
|
|
234
|
-
return Math.max(policy.minimum, topK * policy.multiplier);
|
|
263
|
+
return Math.min(MAX_VECTOR_CANDIDATES, Math.max(policy.minimum, topK * policy.multiplier));
|
|
264
|
+
}
|
|
265
|
+
export function lexicalCandidateLimit(topK, profile = "balanced") {
|
|
266
|
+
const policy = LEXICAL_CANDIDATE_POLICY[profile];
|
|
267
|
+
return Math.min(MAX_LEXICAL_CANDIDATES, Math.max(policy.minimum, topK * policy.multiplier));
|
|
235
268
|
}
|
|
236
269
|
export async function ask(query, options = {}) {
|
|
237
270
|
const config = await loadConfig(String(options.cwd ?? process.cwd()));
|
|
238
271
|
return askWithConfig(query, options, config);
|
|
239
272
|
}
|
|
240
|
-
export async function askWithConfig(query, options, config, connection) {
|
|
273
|
+
export async function askWithConfig(query, options, config, connection, snapshot) {
|
|
241
274
|
const signal = operationSignal(options);
|
|
242
275
|
throwIfAborted(signal);
|
|
243
|
-
const sources = await searchWithConfig(query, options, config, connection, signal);
|
|
244
|
-
const staleWarning =
|
|
276
|
+
const sources = await searchWithConfig(query, options, config, connection, signal, snapshot);
|
|
277
|
+
const staleWarning = null;
|
|
245
278
|
throwIfAborted(signal);
|
|
246
279
|
if (sources.length === 0) {
|
|
247
280
|
return {
|
|
@@ -266,13 +299,30 @@ export async function expandCitation(citation, options = {}) {
|
|
|
266
299
|
const config = await loadConfig(String(options.cwd ?? process.cwd()));
|
|
267
300
|
return expandCitationWithConfig(citation, options, config);
|
|
268
301
|
}
|
|
269
|
-
export async function expandCitationWithConfig(citation, options, config, connection) {
|
|
302
|
+
export async function expandCitationWithConfig(citation, options, config, connection, suppliedSnapshot) {
|
|
303
|
+
const ownsSnapshot = suppliedSnapshot === undefined;
|
|
304
|
+
const snapshot = suppliedSnapshot ?? (await loadIndexReadSnapshot(config, connection));
|
|
305
|
+
let lease;
|
|
306
|
+
try {
|
|
307
|
+
if (snapshot.table) {
|
|
308
|
+
lease = await acquireGenerationReadLease(snapshot.tableName, config);
|
|
309
|
+
}
|
|
310
|
+
return await expandCitationWithinGeneration(citation, options, config, snapshot);
|
|
311
|
+
}
|
|
312
|
+
finally {
|
|
313
|
+
await lease?.release().catch(() => undefined);
|
|
314
|
+
if (ownsSnapshot) {
|
|
315
|
+
closeIndexReadSnapshot(snapshot, config);
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
async function expandCitationWithinGeneration(citation, options, config, snapshot) {
|
|
270
320
|
const signal = operationSignal(options);
|
|
271
321
|
throwIfAborted(signal);
|
|
272
322
|
const requestedCitation = citation.trim();
|
|
273
323
|
const target = parseCitationTarget(requestedCitation);
|
|
274
324
|
const contextRadius = normalizeContextRadius(options.contextRadius);
|
|
275
|
-
const table =
|
|
325
|
+
const table = snapshot.table;
|
|
276
326
|
throwIfAborted(signal);
|
|
277
327
|
if (!table) {
|
|
278
328
|
return {
|
|
@@ -284,11 +334,10 @@ export async function expandCitationWithConfig(citation, options, config, connec
|
|
|
284
334
|
passages: [],
|
|
285
335
|
};
|
|
286
336
|
}
|
|
287
|
-
|
|
288
|
-
if (!manifest) {
|
|
337
|
+
if (!snapshot.manifest) {
|
|
289
338
|
throw new Error("Index manifest is missing. Rebuild with `rgr ingest --rebuild` before expanding citations.");
|
|
290
339
|
}
|
|
291
|
-
const freshnessWarning =
|
|
340
|
+
const freshnessWarning = indexFreshnessWarning(config, snapshot.manifest);
|
|
292
341
|
if (freshnessWarning) {
|
|
293
342
|
throw new Error(freshnessWarning);
|
|
294
343
|
}
|
|
@@ -336,25 +385,115 @@ function retrievalOnlyAnswer(sources) {
|
|
|
336
385
|
snippets,
|
|
337
386
|
].join("\n");
|
|
338
387
|
}
|
|
339
|
-
async function lexicalCandidateRows(table, query,
|
|
340
|
-
const
|
|
341
|
-
|
|
388
|
+
async function lexicalCandidateRows(table, query, options) {
|
|
389
|
+
const ftsQueries = lexicalQuery(query);
|
|
390
|
+
const stats = await table.indexStats(FULL_TEXT_INDEX_NAME).catch(() => undefined);
|
|
391
|
+
let fallbackReason = "fts-index-unavailable";
|
|
392
|
+
if (ftsQueries && stats) {
|
|
342
393
|
try {
|
|
343
|
-
const
|
|
344
|
-
|
|
345
|
-
.
|
|
346
|
-
|
|
394
|
+
const sourcePathQuery = sourcePathPredicateForQuery(query);
|
|
395
|
+
const sourcePathRows = sourcePathQuery
|
|
396
|
+
? await executeSourcePathQuery(table, sourcePathQuery, options.ftsLimit, options.pathPredicate)
|
|
397
|
+
: [];
|
|
398
|
+
const primaryRows = await executeFullTextQuery(table, ftsQueries.primary, options.ftsLimit, options.pathPredicate);
|
|
399
|
+
const rows = [];
|
|
400
|
+
const initialRows = [...sourcePathRows, ...primaryRows];
|
|
401
|
+
const seenRows = new Set();
|
|
402
|
+
for (const row of initialRows) {
|
|
403
|
+
const key = rowKey(row);
|
|
404
|
+
if (!seenRows.has(key)) {
|
|
405
|
+
seenRows.add(key);
|
|
406
|
+
rows.push(row);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
let executedVariants = 1 + Number(sourcePathQuery !== null);
|
|
410
|
+
if (rows.length < options.minimumResults) {
|
|
411
|
+
for (const supplementalQuery of ftsQueries.supplemental) {
|
|
412
|
+
const supplementalRows = await executeFullTextQuery(table, supplementalQuery, options.ftsLimit, options.pathPredicate).catch(() => []);
|
|
413
|
+
executedVariants += 1;
|
|
414
|
+
for (const row of supplementalRows) {
|
|
415
|
+
const key = rowKey(row);
|
|
416
|
+
if (seenRows.has(key)) {
|
|
417
|
+
continue;
|
|
418
|
+
}
|
|
419
|
+
seenRows.add(key);
|
|
420
|
+
rows.push(row);
|
|
421
|
+
if (rows.length >= options.ftsLimit) {
|
|
422
|
+
break;
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
if (rows.length >= options.minimumResults || rows.length >= options.ftsLimit) {
|
|
426
|
+
break;
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
const indexedRows = stats.numIndexedRows;
|
|
431
|
+
const unindexedRows = stats.numUnindexedRows;
|
|
432
|
+
const coveredRows = indexedRows + unindexedRows;
|
|
433
|
+
const exactPathMatches = new Set(sourcePathQuery?.exactRelativePath
|
|
434
|
+
? sourcePathRows
|
|
435
|
+
.filter((row) => row.relativePath === sourcePathQuery.exactRelativePath)
|
|
436
|
+
.map(rowKey)
|
|
437
|
+
: []);
|
|
438
|
+
return {
|
|
439
|
+
rows,
|
|
440
|
+
exactPathMatches,
|
|
441
|
+
backend: "fts",
|
|
442
|
+
fallbackActivated: false,
|
|
443
|
+
fallbackReason: null,
|
|
444
|
+
candidateLimit: options.ftsLimit,
|
|
445
|
+
candidatesMaterialized: rows.length,
|
|
446
|
+
queryVariants: executedVariants,
|
|
447
|
+
indexedRows,
|
|
448
|
+
unindexedRows,
|
|
449
|
+
coverage: coveredRows === 0 ? 1 : indexedRows / coveredRows,
|
|
450
|
+
};
|
|
347
451
|
}
|
|
348
452
|
catch {
|
|
349
|
-
|
|
350
|
-
//
|
|
453
|
+
fallbackReason = "fts-query-failed";
|
|
454
|
+
// A complete bounded scan remains safe for small or explicitly bounded corpora.
|
|
351
455
|
}
|
|
352
456
|
}
|
|
457
|
+
if (options.fallbackLimit < options.indexedChunkCount) {
|
|
458
|
+
throw new RagmirError("INDEX_UNAVAILABLE", `Full-text search is unavailable and the fallback limit would scan only ${options.fallbackLimit} of ${options.indexedChunkCount} chunks. Run \`rgr storage optimize\` or rebuild the index before searching.`, { retryable: true });
|
|
459
|
+
}
|
|
353
460
|
const fallbackQuery = table.query().select(SEARCH_COLUMNS);
|
|
354
|
-
|
|
461
|
+
const rows = (await (options.pathPredicate
|
|
462
|
+
? fallbackQuery.where(options.pathPredicate)
|
|
463
|
+
: fallbackQuery)
|
|
464
|
+
.limit(options.fallbackLimit)
|
|
465
|
+
.toArray());
|
|
466
|
+
return {
|
|
467
|
+
rows,
|
|
468
|
+
exactPathMatches: new Set(),
|
|
469
|
+
backend: "fallback",
|
|
470
|
+
fallbackActivated: true,
|
|
471
|
+
fallbackReason,
|
|
472
|
+
candidateLimit: options.fallbackLimit,
|
|
473
|
+
candidatesMaterialized: rows.length,
|
|
474
|
+
queryVariants: 0,
|
|
475
|
+
indexedRows: 0,
|
|
476
|
+
unindexedRows: options.indexedChunkCount,
|
|
477
|
+
coverage: options.indexedChunkCount === 0 ? 1 : Math.min(1, rows.length / options.indexedChunkCount),
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
async function executeFullTextQuery(table, query, limit, pathPredicate) {
|
|
481
|
+
const searchQuery = table.search(query, "fts", "searchText").select(FTS_SEARCH_COLUMNS);
|
|
482
|
+
return (await (pathPredicate ? searchQuery.where(pathPredicate) : searchQuery)
|
|
355
483
|
.limit(limit)
|
|
356
484
|
.toArray());
|
|
357
485
|
}
|
|
486
|
+
async function executeSourcePathQuery(table, sourcePathQuery, limit, retrievalPredicate) {
|
|
487
|
+
const predicate = retrievalPredicate
|
|
488
|
+
? `(${sourcePathQuery.predicate}) AND (${retrievalPredicate})`
|
|
489
|
+
: sourcePathQuery.predicate;
|
|
490
|
+
return (await table.query().select(SEARCH_COLUMNS).where(predicate).limit(limit).toArray()).map((row) => ({
|
|
491
|
+
...row,
|
|
492
|
+
_score: sourcePathQuery.exactRelativePath === row.relativePath
|
|
493
|
+
? Number.MAX_SAFE_INTEGER
|
|
494
|
+
: Number.MAX_SAFE_INTEGER - 1,
|
|
495
|
+
}));
|
|
496
|
+
}
|
|
358
497
|
function searchPredicate(includePaths, excludePaths, contextPaths) {
|
|
359
498
|
const includes = normalizePathPrefixes(includePaths);
|
|
360
499
|
const excludes = normalizePathPrefixes(excludePaths);
|
|
@@ -393,41 +532,68 @@ function contextPathPredicate(prefix) {
|
|
|
393
532
|
`starts_with(contextPath, ${sqlString(`${prefix}[`)})`,
|
|
394
533
|
].join(" OR ");
|
|
395
534
|
}
|
|
396
|
-
async function contextChunksByRow(table, rows, requestedRadius) {
|
|
535
|
+
async function contextChunksByRow(table, rows, requestedRadius, retrievalPredicate) {
|
|
397
536
|
const radius = Math.min(MAX_CONTEXT_RADIUS, Math.max(0, requestedRadius));
|
|
398
537
|
if (radius === 0 || rows.length === 0) {
|
|
399
538
|
return new Map();
|
|
400
539
|
}
|
|
401
|
-
const
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
540
|
+
const ranges = contextRangesByPath(rows, radius);
|
|
541
|
+
const predicates = [...ranges.entries()].flatMap(([relativePath, pathRanges]) => pathRanges.map(({ minimum, maximum }) => `(relativePath = ${sqlString(relativePath)} AND chunkIndex >= ${minimum} AND chunkIndex <= ${maximum})`));
|
|
542
|
+
const rangePredicate = predicates.join(" OR ");
|
|
543
|
+
const hydrationPredicate = retrievalPredicate
|
|
544
|
+
? `(${rangePredicate}) AND (${retrievalPredicate})`
|
|
545
|
+
: rangePredicate;
|
|
406
546
|
const allContextRows = (await table
|
|
407
547
|
.query()
|
|
408
548
|
.select(SEARCH_COLUMNS)
|
|
409
|
-
.where(
|
|
549
|
+
.where(hydrationPredicate)
|
|
410
550
|
.toArray());
|
|
551
|
+
const contextRowsByPath = new Map();
|
|
552
|
+
for (const contextRow of allContextRows) {
|
|
553
|
+
const pathRows = contextRowsByPath.get(contextRow.relativePath) ?? [];
|
|
554
|
+
pathRows.push(contextRow);
|
|
555
|
+
contextRowsByPath.set(contextRow.relativePath, pathRows);
|
|
556
|
+
}
|
|
411
557
|
const contexts = new Map();
|
|
412
558
|
for (const { row } of rows) {
|
|
413
559
|
const minChunk = Math.max(0, row.chunkIndex - radius);
|
|
414
560
|
const maxChunk = row.chunkIndex + radius;
|
|
415
|
-
const contextRows =
|
|
416
|
-
candidate.chunkIndex >= minChunk &&
|
|
417
|
-
candidate.chunkIndex <= maxChunk);
|
|
561
|
+
const contextRows = (contextRowsByPath.get(row.relativePath) ?? []).filter((candidate) => candidate.chunkIndex >= minChunk && candidate.chunkIndex <= maxChunk);
|
|
418
562
|
contexts.set(rowKey(row), contextRows.sort(compareChunkRows).map(contextChunkForRow));
|
|
419
563
|
}
|
|
420
564
|
return contexts;
|
|
421
565
|
}
|
|
422
|
-
|
|
423
|
-
const
|
|
566
|
+
function contextRangesByPath(rows, radius) {
|
|
567
|
+
const rangesByPath = new Map();
|
|
568
|
+
for (const rankedRow of rows) {
|
|
569
|
+
const pathRows = rangesByPath.get(rankedRow.row.relativePath) ?? [];
|
|
570
|
+
pathRows.push(rankedRow);
|
|
571
|
+
rangesByPath.set(rankedRow.row.relativePath, pathRows);
|
|
572
|
+
}
|
|
573
|
+
return new Map([...rangesByPath.entries()].map(([relativePath, pathRows]) => {
|
|
574
|
+
const ranges = pathRows
|
|
575
|
+
.map(({ row }) => ({
|
|
576
|
+
minimum: Math.max(0, row.chunkIndex - radius),
|
|
577
|
+
maximum: row.chunkIndex + radius,
|
|
578
|
+
}))
|
|
579
|
+
.sort((left, right) => left.minimum - right.minimum);
|
|
580
|
+
const merged = [];
|
|
581
|
+
for (const range of ranges) {
|
|
582
|
+
const previous = merged.at(-1);
|
|
583
|
+
if (!previous || range.minimum > previous.maximum + 1) {
|
|
584
|
+
merged.push({ ...range });
|
|
585
|
+
}
|
|
586
|
+
else {
|
|
587
|
+
previous.maximum = Math.max(previous.maximum, range.maximum);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
return [relativePath, merged];
|
|
591
|
+
}));
|
|
592
|
+
}
|
|
593
|
+
function assertVectorIndexCompatibility(config, manifest, vectorDimension) {
|
|
424
594
|
if (!manifest) {
|
|
425
595
|
throw new Error("Index manifest is missing. Rebuild with `rgr ingest --rebuild` before searching.");
|
|
426
596
|
}
|
|
427
|
-
const freshnessWarning = await getIndexFreshnessWarning(config);
|
|
428
|
-
if (freshnessWarning) {
|
|
429
|
-
throw new Error(freshnessWarning);
|
|
430
|
-
}
|
|
431
597
|
if (manifest.vectorDimension !== undefined && manifest.vectorDimension !== vectorDimension) {
|
|
432
598
|
throw new Error(`Index vector dimension is ${manifest.vectorDimension} but the active embedding produced ${vectorDimension}. Rebuild with \`rgr ingest --rebuild\`.`);
|
|
433
599
|
}
|
|
@@ -435,6 +601,13 @@ async function assertVectorIndexCompatibility(config, vectorDimension) {
|
|
|
435
601
|
manifest.vectorDistanceMetric !== VECTOR_DISTANCE_METRIC) {
|
|
436
602
|
throw new Error(`Index vector distance metric is ${manifest.vectorDistanceMetric} but Ragmir expects ${VECTOR_DISTANCE_METRIC}. Rebuild with \`rgr ingest --rebuild\`.`);
|
|
437
603
|
}
|
|
604
|
+
if (!manifest.vectorIndex) {
|
|
605
|
+
throw new Error("Index vector strategy metadata is missing. Rebuild with `rgr ingest --rebuild`.");
|
|
606
|
+
}
|
|
607
|
+
if (!vectorIndexManifestCompatible(manifest.vectorIndex, config, vectorDimension)) {
|
|
608
|
+
throw new Error("Index vector strategy is incompatible or incomplete. Rebuild with `rgr ingest --rebuild` or repair coverage with `rgr storage optimize`.");
|
|
609
|
+
}
|
|
610
|
+
return { ...manifest, vectorIndex: manifest.vectorIndex };
|
|
438
611
|
}
|
|
439
612
|
function answerText(source) {
|
|
440
613
|
if (source.context.length === 0) {
|
|
@@ -449,160 +622,25 @@ function contextChunkForRow(row) {
|
|
|
449
622
|
text: row.text,
|
|
450
623
|
charStart: nullableNumber(row.charStart),
|
|
451
624
|
charEnd: nullableNumber(row.charEnd),
|
|
452
|
-
lineStart:
|
|
453
|
-
lineEnd:
|
|
625
|
+
lineStart: nullableLineNumber(row.lineStart),
|
|
626
|
+
lineEnd: nullableLineNumber(row.lineEnd),
|
|
454
627
|
pageStart: nullablePageNumber(row.pageStart),
|
|
455
628
|
pageEnd: nullablePageNumber(row.pageEnd),
|
|
456
629
|
citation: citationForRow(row),
|
|
457
630
|
};
|
|
458
631
|
}
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
* and vector score distributions have nothing in common) and is the standard
|
|
464
|
-
* hybrid retrieval approach.
|
|
465
|
-
*
|
|
466
|
-
* Ranks are 0-based internally; a candidate absent from a retriever contributes
|
|
467
|
-
* nothing from that retriever. Tie-breaks keep the result deterministic.
|
|
468
|
-
*/
|
|
469
|
-
function rankHybridRows(query, vectorRows, textRows) {
|
|
470
|
-
const queryTokens = tokenize(query);
|
|
471
|
-
const rows = mergeRows(vectorRows, textRows);
|
|
472
|
-
// Vector ranks: lower _distance is better, so sort ascending then assign rank 0..
|
|
473
|
-
const vectorRanked = [...vectorRows]
|
|
474
|
-
.filter((row) => Number.isFinite(rowDistance(row)))
|
|
475
|
-
.sort((a, b) => rowDistance(a) - rowDistance(b));
|
|
476
|
-
const vectorRanks = new Map();
|
|
477
|
-
vectorRanked.forEach((row, index) => {
|
|
478
|
-
vectorRanks.set(rowKey(row), index);
|
|
479
|
-
});
|
|
480
|
-
// LanceDB FTS already returns BM25-ranked rows. The fallback scan computes
|
|
481
|
-
// BM25 locally when an older index has no text index.
|
|
482
|
-
const ftsRows = textRows.filter((row) => typeof row._score === "number");
|
|
483
|
-
const lexicalRanked = ftsRows.length > 0
|
|
484
|
-
? ftsRows
|
|
485
|
-
.sort((a, b) => (b._score ?? 0) - (a._score ?? 0))
|
|
486
|
-
.map((row) => [rowKey(row), row._score ?? 0])
|
|
487
|
-
: [...bm25Scores(queryTokens, rows).entries()]
|
|
488
|
-
.filter(([, score]) => score > 0)
|
|
489
|
-
.sort((a, b) => b[1] - a[1]);
|
|
490
|
-
const lexicalRanks = new Map();
|
|
491
|
-
lexicalRanked.forEach(([key], index) => {
|
|
492
|
-
lexicalRanks.set(key, index);
|
|
493
|
-
});
|
|
494
|
-
const lexicalScores = new Map(lexicalRanked);
|
|
495
|
-
return rows
|
|
496
|
-
.map((row) => {
|
|
497
|
-
const key = rowKey(row);
|
|
498
|
-
const vectorRank = vectorRanks.get(key);
|
|
499
|
-
const lexicalRank = lexicalRanks.get(key);
|
|
500
|
-
let combinedScore = 0;
|
|
501
|
-
let vectorScore = 0;
|
|
502
|
-
let lexicalScore = 0;
|
|
503
|
-
if (vectorRank !== undefined) {
|
|
504
|
-
vectorScore = RRF_VECTOR_WEIGHT / (RRF_K + vectorRank);
|
|
505
|
-
combinedScore += vectorScore;
|
|
506
|
-
}
|
|
507
|
-
if (lexicalRank !== undefined) {
|
|
508
|
-
lexicalScore = RRF_LEXICAL_WEIGHT / (RRF_K + lexicalRank);
|
|
509
|
-
combinedScore += lexicalScore;
|
|
510
|
-
}
|
|
511
|
-
return {
|
|
512
|
-
row,
|
|
513
|
-
vectorScore,
|
|
514
|
-
lexicalScore,
|
|
515
|
-
combinedScore,
|
|
516
|
-
vectorRank: vectorRank === undefined ? null : vectorRank + 1,
|
|
517
|
-
lexicalRank: lexicalRank === undefined ? null : lexicalRank + 1,
|
|
518
|
-
lexicalBackendScore: lexicalScores.get(key) ?? null,
|
|
519
|
-
matchedTerms: matchedQueryTerms(queryTokens, row.searchText),
|
|
520
|
-
};
|
|
521
|
-
})
|
|
522
|
-
.filter((ranked) => ranked.combinedScore > 0)
|
|
523
|
-
.sort((a, b) => {
|
|
524
|
-
const scoreDelta = b.combinedScore - a.combinedScore;
|
|
525
|
-
if (scoreDelta !== 0) {
|
|
526
|
-
return scoreDelta;
|
|
527
|
-
}
|
|
528
|
-
const distanceDelta = rowDistance(a.row) - rowDistance(b.row);
|
|
529
|
-
if (Number.isFinite(distanceDelta) && distanceDelta !== 0) {
|
|
530
|
-
return distanceDelta;
|
|
531
|
-
}
|
|
532
|
-
return (a.row.relativePath.localeCompare(b.row.relativePath) || a.row.chunkIndex - b.row.chunkIndex);
|
|
533
|
-
});
|
|
534
|
-
}
|
|
535
|
-
function matchedQueryTerms(queryTokens, text) {
|
|
632
|
+
function matchedQueryTerms(projectRoot, queryTokens, text) {
|
|
633
|
+
if (queryExplanationDiagnostics.hasSubscribers) {
|
|
634
|
+
queryExplanationDiagnostics.publish({ projectRoot });
|
|
635
|
+
}
|
|
536
636
|
const textTokens = tokenize(text);
|
|
537
637
|
return [...new Set(queryTokens)].filter((queryToken) => textTokens.some((textToken) => tokensAreLexicallyRelated(queryToken, textToken)));
|
|
538
638
|
}
|
|
539
|
-
function
|
|
540
|
-
const rows = new Map();
|
|
541
|
-
for (const row of textRows) {
|
|
542
|
-
rows.set(rowKey(row), row);
|
|
543
|
-
}
|
|
544
|
-
for (const row of vectorRows) {
|
|
545
|
-
const existing = rows.get(rowKey(row));
|
|
546
|
-
if (!existing) {
|
|
547
|
-
rows.set(rowKey(row), row);
|
|
548
|
-
continue;
|
|
549
|
-
}
|
|
550
|
-
const merged = { ...existing, ...row };
|
|
551
|
-
if (existing._score !== undefined) {
|
|
552
|
-
merged._score = existing._score;
|
|
553
|
-
}
|
|
554
|
-
rows.set(rowKey(row), merged);
|
|
555
|
-
}
|
|
556
|
-
return [...rows.values()];
|
|
557
|
-
}
|
|
558
|
-
function bm25Scores(queryTokens, rows) {
|
|
559
|
-
const scores = new Map();
|
|
560
|
-
if (queryTokens.length === 0 || rows.length === 0) {
|
|
561
|
-
return scores;
|
|
562
|
-
}
|
|
563
|
-
const uniqueQueryTokens = [...new Set(queryTokens)];
|
|
564
|
-
const documents = rows.map((row) => {
|
|
565
|
-
const tokens = tokenize(row.searchText);
|
|
566
|
-
const frequencies = new Map();
|
|
567
|
-
for (const token of tokens) {
|
|
568
|
-
frequencies.set(token, (frequencies.get(token) ?? 0) + 1);
|
|
569
|
-
}
|
|
570
|
-
return { row, tokens, frequencies };
|
|
571
|
-
});
|
|
572
|
-
const averageLength = documents.reduce((sum, document) => sum + document.tokens.length, 0) / documents.length || 1;
|
|
573
|
-
const documentFrequencies = new Map();
|
|
574
|
-
for (const token of uniqueQueryTokens) {
|
|
575
|
-
documentFrequencies.set(token, documents.filter((document) => document.frequencies.has(token)).length);
|
|
576
|
-
}
|
|
577
|
-
for (const document of documents) {
|
|
578
|
-
let score = 0;
|
|
579
|
-
for (const token of uniqueQueryTokens) {
|
|
580
|
-
const frequency = document.frequencies.get(token) ?? 0;
|
|
581
|
-
if (frequency === 0) {
|
|
582
|
-
continue;
|
|
583
|
-
}
|
|
584
|
-
const documentFrequency = documentFrequencies.get(token) ?? 0;
|
|
585
|
-
const inverseDocumentFrequency = Math.log(1 + (documents.length - documentFrequency + 0.5) / (documentFrequency + 0.5));
|
|
586
|
-
const denominator = frequency + BM25_K1 * (1 - BM25_B + BM25_B * (document.tokens.length / averageLength));
|
|
587
|
-
score += inverseDocumentFrequency * ((frequency * (BM25_K1 + 1)) / denominator);
|
|
588
|
-
}
|
|
589
|
-
if (score > 0) {
|
|
590
|
-
scores.set(rowKey(document.row), score);
|
|
591
|
-
}
|
|
592
|
-
}
|
|
593
|
-
return scores;
|
|
594
|
-
}
|
|
595
|
-
function rowDistance(row) {
|
|
596
|
-
return typeof row._distance === "number" && row._distance >= 0
|
|
597
|
-
? row._distance
|
|
598
|
-
: Number.POSITIVE_INFINITY;
|
|
599
|
-
}
|
|
600
|
-
async function assertIndexFreshness(config) {
|
|
601
|
-
const manifest = await readIndexManifest(config);
|
|
639
|
+
function assertIndexFreshness(config, manifest) {
|
|
602
640
|
if (!manifest) {
|
|
603
641
|
throw new Error("Index manifest is missing. Rebuild with `rgr ingest --rebuild` before searching.");
|
|
604
642
|
}
|
|
605
|
-
const freshnessWarning =
|
|
643
|
+
const freshnessWarning = indexFreshnessWarning(config, manifest);
|
|
606
644
|
if (freshnessWarning) {
|
|
607
645
|
throw new Error(freshnessWarning);
|
|
608
646
|
}
|
|
@@ -618,8 +656,7 @@ function parseCitationTarget(citation) {
|
|
|
618
656
|
}
|
|
619
657
|
const chunkIndex = Number.parseInt(chunkIndexText, 10);
|
|
620
658
|
let relativePath = citation.slice(0, match.index);
|
|
621
|
-
relativePath = relativePath
|
|
622
|
-
relativePath = relativePath.replace(/:p\d+(?:-p\d+)?$/u, "");
|
|
659
|
+
relativePath = stripCitationCoordinates(relativePath);
|
|
623
660
|
if (!relativePath) {
|
|
624
661
|
throw new Error("Citation must include a source path before its chunk suffix.");
|
|
625
662
|
}
|
|
@@ -638,23 +675,70 @@ function normalizeTopK(topK) {
|
|
|
638
675
|
if (!Number.isSafeInteger(topK) || topK <= 0) {
|
|
639
676
|
throw new RagmirError("INVALID_ARGUMENT", "topK must be a positive integer.");
|
|
640
677
|
}
|
|
678
|
+
if (topK > MAX_SEARCH_TOP_K) {
|
|
679
|
+
throw new RagmirError("INVALID_ARGUMENT", `topK must be at most ${MAX_SEARCH_TOP_K}.`);
|
|
680
|
+
}
|
|
641
681
|
return topK;
|
|
642
682
|
}
|
|
643
683
|
function citationForRow(row) {
|
|
644
|
-
|
|
645
|
-
const lineEnd = nullableNumber(row.lineEnd);
|
|
646
|
-
const pageStart = nullablePageNumber(row.pageStart);
|
|
647
|
-
const pageEnd = nullablePageNumber(row.pageEnd);
|
|
648
|
-
const pageSegment = pageStart === null
|
|
649
|
-
? ""
|
|
650
|
-
: `:p${pageStart}${pageEnd !== null && pageEnd !== pageStart ? `-p${pageEnd}` : ""}`;
|
|
651
|
-
if (lineStart === null || lineEnd === null) {
|
|
652
|
-
return `${row.relativePath}${pageSegment}#${row.chunkIndex}`;
|
|
653
|
-
}
|
|
654
|
-
return `${row.relativePath}${pageSegment}:L${lineStart}-L${lineEnd}#${row.chunkIndex}`;
|
|
684
|
+
return citationForCoordinates(row);
|
|
655
685
|
}
|
|
656
686
|
function lexicalQuery(query) {
|
|
657
|
-
|
|
687
|
+
const tokens = [...new Set(tokenize(query))];
|
|
688
|
+
if (tokens.length === 0) {
|
|
689
|
+
return null;
|
|
690
|
+
}
|
|
691
|
+
const joined = tokens.join(" ");
|
|
692
|
+
const supplemental = [];
|
|
693
|
+
if (tokens.length > 1) {
|
|
694
|
+
supplemental.push(new PhraseQuery(joined, "searchText"));
|
|
695
|
+
}
|
|
696
|
+
const identifierTerms = [...query.matchAll(LEXICAL_IDENTIFIER_PATTERN)]
|
|
697
|
+
.map((match) => match[0])
|
|
698
|
+
.filter(Boolean);
|
|
699
|
+
for (const identifier of [...new Set(identifierTerms)]) {
|
|
700
|
+
supplemental.push(new MatchQuery(identifier, "searchText", {
|
|
701
|
+
boost: 2,
|
|
702
|
+
...(isFuzzyLexicalTerm(identifier) ? { fuzziness: 1, prefixLength: 3 } : {}),
|
|
703
|
+
}));
|
|
704
|
+
}
|
|
705
|
+
const rareTerms = tokens
|
|
706
|
+
.filter(isFuzzyLexicalTerm)
|
|
707
|
+
.sort((left, right) => right.length - left.length || left.localeCompare(right))
|
|
708
|
+
.slice(0, 3);
|
|
709
|
+
for (const term of rareTerms) {
|
|
710
|
+
supplemental.push(new MatchQuery(term, "searchText", {
|
|
711
|
+
boost: 1.25,
|
|
712
|
+
fuzziness: 1,
|
|
713
|
+
prefixLength: 3,
|
|
714
|
+
}));
|
|
715
|
+
}
|
|
716
|
+
return {
|
|
717
|
+
primary: new MatchQuery(joined, "searchText", { operator: Operator.Or }),
|
|
718
|
+
supplemental,
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
function isFuzzyLexicalTerm(term) {
|
|
722
|
+
return term.length >= 7 && /^[a-z0-9_.-]+$/u.test(term);
|
|
723
|
+
}
|
|
724
|
+
function sourcePathPredicateForQuery(query) {
|
|
725
|
+
const normalized = query.trim().replaceAll("\\", "/").replace(/^\.\//u, "");
|
|
726
|
+
if (!SOURCE_PATH_QUERY_PATTERN.test(normalized)) {
|
|
727
|
+
return null;
|
|
728
|
+
}
|
|
729
|
+
const fileName = normalized.split("/").at(-1);
|
|
730
|
+
if (!fileName) {
|
|
731
|
+
return null;
|
|
732
|
+
}
|
|
733
|
+
const clauses = new Set([
|
|
734
|
+
`relativePath = ${sqlString(normalized)}`,
|
|
735
|
+
`relativePath = ${sqlString(fileName)}`,
|
|
736
|
+
`ends_with(relativePath, ${sqlString(`/${fileName}`)})`,
|
|
737
|
+
]);
|
|
738
|
+
return {
|
|
739
|
+
predicate: `(${[...clauses].join(" OR ")})`,
|
|
740
|
+
exactRelativePath: normalized.includes("/") ? normalized : null,
|
|
741
|
+
};
|
|
658
742
|
}
|
|
659
743
|
function compareChunkRows(a, b) {
|
|
660
744
|
return a.chunkIndex - b.chunkIndex;
|
|
@@ -662,6 +746,9 @@ function compareChunkRows(a, b) {
|
|
|
662
746
|
function nullableNumber(value) {
|
|
663
747
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
664
748
|
}
|
|
749
|
+
function nullableLineNumber(value) {
|
|
750
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
751
|
+
}
|
|
665
752
|
function nullablePageNumber(value) {
|
|
666
753
|
return typeof value === "number" && Number.isInteger(value) && value > 0 ? value : null;
|
|
667
754
|
}
|