Skip to content

Commit e2b9e16

Browse files
authored
feat(core): diversify retrieval results by document (#161)
Release highlights: - Broaden top-k document coverage while preserving the requested result count. - Keep lexical fallback available for corpora larger than one scan batch. Release details: - Add a configurable document cap across CLI, MCP, ask, evaluation, and portable status. - Over-retrieve before ranking, then diversify with ranked backfill. - Scan lexical fallback batches across the full eligible corpus. - Refresh compatible dependencies and pin patched transitive packages. Verification: - Pass pnpm validate on the release branch with 609 tests. - Pass audit, CI, Linux, macOS, Commitlint, and CodeQL checks. - Pass 100/100 reference quality cases with recall@10 1.000.
2 parents 1eda346 + 880174f commit e2b9e16

42 files changed

Lines changed: 2518 additions & 1963 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -224,7 +224,10 @@ pnpm exec rgr research "release obligations" --compact --timeout-ms 10000
224224
```
225225

226226
`preview` inspects redacted chunks without writing an index. `audit` compares sources with indexed
227-
state. `research` combines bounded query variants with deterministic cross-query ranking. Use
227+
state. Search keeps one primary passage per document by default, over-retrieves before applying the
228+
cap, and preserves ranked backfill when fewer distinct documents are available. Override the cap
229+
with `--max-chunks-per-document`; context-radius neighbors remain attached to their primary result.
230+
`research` combines bounded query variants with deterministic cross-query ranking. Use
228231
`rgr doctor --deep` only when you need a live O(corpus) inventory; normal status and doctor checks
229232
read the compact activation manifest.
230233

biome.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
{
2-
"$schema": "https://biomejs.dev/schemas/2.5.1/schema.json",
2+
"$schema": "https://biomejs.dev/schemas/2.5.9/schema.json",
33
"vcs": {
44
"enabled": true,
55
"clientKind": "git",

docs/api-reference.md

Lines changed: 16 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,13 @@ instead of presenting an unverifiable line claim.
5858

5959
With `explain: true`, `score` includes the vector and lexical ranks, their reciprocal-rank-fusion
6060
contributions, matched terms, backend scores, FTS or complete-fallback activation and reason,
61-
candidate materialization, query-variant count, indexed/unindexed rows, coverage, queue wait as
62-
`workloadQueueMs`, and
63-
`rankingPolicyFingerprint`. The fingerprint
64-
identifies the provider, retrieval profile, fusion parameters, and abstention threshold used by
65-
the result. Equal scores are ordered by stable source and chunk keys, so identical indexes return
66-
the same order regardless of backend row order. Search returns an empty array when every candidate
67-
fails the active provider's evidence threshold.
61+
fallback scan batches, candidate materialization, query-variant count, indexed/unindexed rows,
62+
coverage, document-cap and ranked-backfill state, queue wait as `workloadQueueMs`, and
63+
`rankingPolicyFingerprint`. The fingerprint identifies the provider, retrieval profile, document
64+
cap, fusion parameters, and abstention threshold used by the result. Equal scores are ordered by
65+
stable source and chunk keys, so identical indexes return the same order regardless of backend row
66+
order. Search returns an empty array when every candidate fails the active provider's evidence
67+
threshold.
6868

6969
### Persistent client for Node.js workers
7070

@@ -236,13 +236,17 @@ to perform an external action.
236236

237237
One evaluation pins a single configuration, connection, manifest generation, table handle, and
238238
embedding model. Cases run with bounded concurrency, preserve file order in the report, and release
239-
all scoped resources when evaluation finishes.
239+
all scoped resources when evaluation finishes. The report records the configured
240+
`maxChunksPerDocument` beside the ranking-policy fingerprint so reference results are reproducible.
240241

241-
`SearchOptions` accepts `cwd`, `topK`, `contextRadius`, `includePaths`, `excludePaths`,
242-
`contextPaths`, `explain`, `vectorSearchMode`, `signal`, and `timeoutMs`. Set
242+
`SearchOptions` accepts `cwd`, `topK`, `maxChunksPerDocument`, `contextRadius`, `includePaths`,
243+
`excludePaths`, `contextPaths`, `explain`, `vectorSearchMode`, `signal`, and `timeoutMs`. Set
243244
`vectorSearchMode: "exact"` to bypass ANN for diagnostic comparison; the default `"adaptive"`
244-
uses the compatible strategy recorded in the manifest. `topK` is limited to 100 and
245-
`contextRadius` is clamped to three chunks. `IngestOptions` also accepts `rebuild`, a
245+
uses the compatible strategy recorded in the manifest. `topK` and `maxChunksPerDocument` are
246+
limited to 100. The document cap defaults to one, applies after scoring, and is preceded by internal
247+
over-retrieval. Ranked backfill preserves the requested result count when too few distinct
248+
documents are available. `contextRadius` is clamped to three chunks and attaches neighbors after
249+
primary-result diversification. `IngestOptions` also accepts `rebuild`, a
246250
positive `batchSize` that defaults to 25 files and is capped at 128, `incrementalFailurePolicy`, and
247251
an optional `onProgress` callback. Set `collectMetrics: true` to include privacy-safe phase,
248252
throughput, cache-state, RSS, OCR subprocess, fallback, error, timeout, and bound-activation metrics

docs/cli-reference.md

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -40,25 +40,29 @@ rgr sources add "docs/**/*.md" "!docs/archive/**"
4040
rgr sources list
4141
rgr preview --path docs --max-files 5 --max-chunks 3
4242
rgr search "migration" --top-k 5 --context-radius 1
43+
rgr search "migration" --top-k 5 --max-chunks-per-document 2
4344
rgr search "migration" --include-path docs --exclude-path docs/archive
4445
rgr search "migration" --context-path "Guide > Migration" --explain
4546
rgr search "migration" --exact-vector-search
4647
```
4748

4849
`sources add` accepts paths, globs, and `!` exclusions. Search, ask, and research accept `--top-k`,
4950
`--include-path`, `--exclude-path`, and repeatable `--context-path`. Search and ask accept
50-
`--explain`; the optional score object reports RRF contributions, retriever ranks, raw backend
51-
scores, FTS or complete-fallback activation and reason, candidate and index coverage, queue wait,
52-
and matched query terms without changing ranking. Use `--compact` on search or research when
51+
`--max-chunks-per-document` and `--explain`. The document cap defaults to one, applies after scoring,
52+
and over-retrieves internally before final truncation. Ranked backfill keeps the requested result
53+
count when the corpus has too few distinct documents. The optional score object reports RRF
54+
contributions, retriever ranks, raw backend scores, document-cap and backfill state, FTS or
55+
complete-fallback activation and reason, fallback scan batches, candidate and index coverage, queue
56+
wait, and matched query terms without changing ranking. Use `--compact` on search or research when
5357
agent context is limited. This remains explicit for CLI automation; MCP search, ask, and research
54-
are compact by default. Search and ask accept `--exact-vector-search` to bypass an active ANN
55-
index for diagnostics against exhaustive vector search. `--top-k` is limited to 100 and
56-
`--context-radius` is clamped to three chunks.
58+
are compact by default. Search and ask accept `--exact-vector-search` to bypass an active ANN index
59+
for diagnostics against exhaustive vector search. `--top-k` and `--max-chunks-per-document` are
60+
limited to 100, and `--context-radius` is clamped to three chunks.
5761

5862
The explanation also contains a ranking-policy fingerprint so a stored quality report can be tied
59-
to the exact provider, profile, fusion, and abstention settings. Equal backend scores have a stable
60-
source-and-chunk tie-break. Search returns no result when all candidates fail the provider-aware
61-
evidence threshold; it does not force a low-confidence passage into the response.
63+
to the exact provider, profile, document cap, fusion, and abstention settings. Equal backend scores
64+
have a stable source-and-chunk tie-break. Search returns no result when all candidates fail the
65+
provider-aware evidence threshold; it does not force a low-confidence passage into the response.
6266

6367
`preview` uses the active redaction and chunking configuration but never writes storage. `audit`
6468
reports min, mean, p50, p95, and max chunk sizes plus structural-context coverage.

docs/configuration.md

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ edit JSON only for a real need.
2424
| `embeddingModelRevision` | Pinned commit for bundled profiles | Use an immutable 40-character commit for reproducible model artifacts. Unknown custom models default to the mutable `main` revision until explicitly pinned. |
2525
| `embeddingModelDigest` | `null` | `rgr models pull --enable` records a SHA-256 identity for the resolved local artifact tree. Do not set it by hand unless the local files were verified independently. |
2626
| `topK` | `8` | Change the CLI and TypeScript default, up to the hard limit of 100. MCP requests without `topK` start at the lower of this value and 3. |
27+
| `maxChunksPerDocument` | `1` | Limit primary ranked passages from one document before final `topK` truncation, up to 100. Ranked backfill preserves the requested result count when the candidate set has too few documents. |
2728
| `mcpMaxTopK` | `10` | Bound explicit MCP passage requests; values above 100 are rejected. |
2829
| `mcpMaxOutputBytes` | `32768` | Cap variable-size MCP tool and resource JSON; the server also enforces an absolute 1 MiB ceiling. |
2930
| `chunkSize` / `chunkOverlap` | `1200` / `200` | Tune chunking, then rebuild the index. Chunk size is capped at 1,000,000 characters. |
@@ -32,7 +33,7 @@ edit JSON only for a real need.
3233
| `embeddingBatchSize` | `32` | Bound one model call; values above `128` are rejected. |
3334
| `sourceFingerprintMode` | `fast` | Use `strict` to hash every source on every inventory instead of reusing unchanged private fingerprints. |
3435
| `incrementalFailurePolicy` | `preserve-last-good` | Use `remove-stale` only when failed changed files must disappear immediately. |
35-
| `hybridTextScanLimit` | `5000` | Bound only the complete-scan fallback used when FTS is unavailable; values above 10,000 are rejected. A fallback smaller than the active corpus is rejected instead of returning silently truncated lexical evidence. |
36+
| `hybridTextScanLimit` | `5000` | Set the batch size for the complete lexical scan used when FTS is unavailable; values above 10,000 are rejected. The fallback scans every matching chunk across as many batches as the corpus requires. |
3637
| `workloadLimits` | See below | Bound active search, embedding, and ingestion work plus their queues and queue deadlines. |
3738
| `includeExtensions` | `[]` | Add safe custom text extensions. |
3839

@@ -81,21 +82,33 @@ Profiles bound retrieval work. They are candidate and diversification budgets, n
8182
a larger budget improves every corpus. Evaluate the profile against a representative golden set
8283
before changing production configuration.
8384

84-
| Profile | Quality intent | Latency intent | Default `topK` | Fallback scan cap | Vector candidates | FTS candidates | First-pass chunks per source | Context radius |
85+
| Profile | Quality intent | Latency intent | Default `topK` | Fallback batch size | Vector candidates | FTS candidates | Document cap | Context radius |
8586
| --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |
86-
| `fast` | Narrow, diverse evidence | Lowest work budget | 5 | 2,000 | max(40, 3 x `topK`) | max(100, 10 x `topK`) | 1, then backfill | 0 |
87-
| `balanced` | General-purpose evidence | Default work budget | 8 | 5,000 | max(80, 4 x `topK`) | max(250, 20 x `topK`) | 2, then backfill | 0 |
88-
| `quality` | Broader multi-section evidence | Highest work budget | 12 | 10,000 | max(200, 8 x `topK`) | min(4,000, max(500, 40 x `topK`)) | 4, then backfill | 1 |
89-
| `custom` | Golden-set validated | Operator-defined | configured | configured | max(80, 4 x `topK`) | max(250, 20 x `topK`) | 2, then backfill | 0 |
87+
| `fast` | Narrow, diverse evidence | Lowest work budget | 5 | 2,000 | max(40, 3 x `demand`) | max(100, 10 x `demand`) | configured, default 1 | 0 |
88+
| `balanced` | General-purpose evidence | Default work budget | 8 | 5,000 | max(80, 4 x `demand`) | max(250, 20 x `demand`) | configured, default 1 | 0 |
89+
| `quality` | Broader multi-section evidence | Highest work budget | 12 | 10,000 | max(200, 8 x `demand`) | min(4,000, max(500, 40 x `demand`)) | configured, default 1 | 1 |
90+
| `custom` | Golden-set validated | Operator-defined | configured | configured | max(80, 4 x `demand`) | max(250, 20 x `demand`) | configured, default 1 | 0 |
9091

92+
`demand` is `topK * ceil(4 / maxChunksPerDocument)`, with a minimum multiplier of one. This internal
93+
over-retrieval gives the diversity pass enough lower-ranked documents before final truncation.
9194
Vector candidates are capped at 1,000. The FTS pool is profile-aware and capped at 4,000,
92-
independently from `hybridTextScanLimit`. Structural context and body text feed the primary local
93-
index. Exact file paths use a bounded scalar variant. Controlled exact-phrase, identifier, and fuzzy
94-
rare-term queries expand only a primary pool that cannot fill `topK`, preserving established ranks.
95-
The diversity pass prefers distinct sources first, then backfills ranked non-duplicate, non-overlapping
96-
chunks to `topK`. Hybrid ranking uses deterministic reciprocal-rank fusion with `k = 60` and equal
97-
vector and lexical weights. Stable source and chunk keys break score ties before ranks are assigned.
98-
The active provider, profile, and ranking parameters form a policy fingerprint stored in quality
95+
independently from the complete-scan batch size. Structural context and body text feed the primary
96+
local index. Exact file paths use a bounded scalar variant. Controlled exact-phrase, identifier,
97+
and fuzzy rare-term queries expand only a primary pool that cannot fill the demand, preserving
98+
established ranks.
99+
100+
After scoring and abstention, the deterministic diversity pass keeps at most
101+
`maxChunksPerDocument` primary passages per relative path while preserving rank order. It then
102+
backfills ranked, non-duplicate, non-overlapping chunks only when too few documents or candidates
103+
remain to fill `topK`. Neighbor chunks requested through `contextRadius` are attached afterward and
104+
do not consume the primary document cap. MMR is not part of the default policy: pairwise similarity
105+
adds more work and corpus-dependent ordering, and the current golden benchmark does not show an
106+
advantage over the simple cap default. It can be evaluated later as an explicit opt-in strategy
107+
without changing this predictable default.
108+
109+
Hybrid ranking uses deterministic reciprocal-rank fusion with `k = 60` and equal vector and lexical
110+
weights. Stable source and chunk keys break score ties before ranks are assigned. The active
111+
provider, profile, document cap, and ranking parameters form a policy fingerprint stored in quality
99112
reports and exposed by score explanations.
100113

101114
Abstention is provider-aware. `local-hash` requires lexical evidence and gives query identifiers

package.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,12 +74,12 @@
7474
"validate": "pnpm lint && pnpm audit:security && pnpm check && pnpm test:coverage && pnpm build && pnpm api:check && pnpm smoke && pnpm package:check && pnpm release:semantic:smoke && pnpm release:artifacts"
7575
},
7676
"devDependencies": {
77-
"@biomejs/biome": "^2.5.1",
78-
"@commitlint/cli": "^21.1.0",
79-
"@commitlint/config-conventional": "^21.1.0",
77+
"@biomejs/biome": "^2.5.9",
78+
"@commitlint/cli": "^21.2.2",
79+
"@commitlint/config-conventional": "^21.2.2",
8080
"@semantic-release/exec": "^7.1.0",
81-
"@vitest/coverage-v8": "4.1.9",
82-
"semantic-release": "^25.0.5",
81+
"@vitest/coverage-v8": "4.1.10",
82+
"semantic-release": "^25.0.9",
8383
"yaml": "^2.9.0"
8484
},
8585
"main": "index.js",

packages/ragmir-chat/package.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,12 +64,12 @@
6464
"test:coverage": "vitest run --coverage"
6565
},
6666
"dependencies": {
67-
"node-llama-cpp": "3.19.0"
67+
"node-llama-cpp": "3.20.0"
6868
},
6969
"devDependencies": {
7070
"@types/node": "^24.10.1",
71-
"publint": "^0.3.21",
71+
"publint": "^0.3.23",
7272
"typescript": "^5.9.3",
73-
"vitest": "^4.0.15"
73+
"vitest": "^4.1.10"
7474
}
7575
}

packages/ragmir-chat/src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
DEFAULT_CHAT_MODEL_PATH,
1414
DEFAULT_CHAT_PROFILE,
1515
inspectChatModel,
16+
NODE_LLAMA_RUNTIME_VERSION,
1617
resolveChatModelPaths,
1718
setupChatModelFiles,
1819
} from "./profiles.js"
@@ -52,7 +53,7 @@ export async function doctor(options: DoctorOptions = {}): Promise<DoctorReport>
5253
return {
5354
node: process.versions.node,
5455
provider: "node-llama-cpp",
55-
runtimeVersion: "3.19.0",
56+
runtimeVersion: NODE_LLAMA_RUNTIME_VERSION,
5657
profile,
5758
defaultProfile: DEFAULT_CHAT_PROFILE,
5859
defaultModel: DEFAULT_CHAT_MODEL,

packages/ragmir-chat/src/profiles.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import type {
1111
SetupChatModelResult,
1212
} from "./types.js"
1313

14-
export const NODE_LLAMA_RUNTIME_VERSION = "3.19.0" as const
14+
export const NODE_LLAMA_RUNTIME_VERSION = "3.20.0" as const
1515
export const DEFAULT_CHAT_PROFILE: ChatModelProfile = "fast"
1616
export const DEFAULT_CHAT_MODEL_PATH = ".ragmir/models/chat"
1717
export const CHAT_MODEL_MANIFEST_FILE = "manifest.json"

packages/ragmir-chat/src/types.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ export interface ChatModelProfileDefinition {
5959
export interface ChatModelManifest {
6060
schemaVersion: 1
6161
provider: "node-llama-cpp"
62-
runtimeVersion: "3.19.0"
62+
runtimeVersion: "3.20.0"
6363
profile: ChatModelProfile
6464
modelId: string
6565
revision: string
@@ -196,7 +196,7 @@ export interface DoctorOptions {
196196
export interface DoctorReport {
197197
node: string
198198
provider: "node-llama-cpp"
199-
runtimeVersion: "3.19.0"
199+
runtimeVersion: "3.20.0"
200200
profile: ChatModelProfile
201201
defaultProfile: ChatModelProfile
202202
defaultModel: string

0 commit comments

Comments
 (0)