← Back to PRs

#16968: fix(qmd): per-collection search to prevent large collections drowning out smaller ones

by ProgramCaiCai open 2026-02-15 08:52 View on GitHub →
stale size: L
## Summary - Problem: When using `search`/`vsearch` mode with multiple QMD collections, all collections are queried in a single `qmd search` call. Large collections (e.g. `custom-1` with 432 docs) dominate the top-N results, causing smaller collections like `memory-dir` (38 docs) to return **zero results** — even though the index has matching data. - Why it matters: Users with both custom report collections and memory directories lose all memory-dir recall. The agent cannot retrieve any personal memory/knowledge-base content. - What changed: - Generalized `runQueryAcrossCollections` → `runSearchAcrossCollections` supporting `search`/`vsearch`/`query` commands - All multi-collection searches now query each collection independently and merge results by best score per docid - Added automatic fallback: if per-collection `search` reports unsupported flags, falls back to per-collection `query` - Self-healing retry for missing collections and `--no-expand` fallback (from earlier commits on this branch) - `resolveDocLocation` scoped to managed collections only - What did NOT change (scope boundary): Single-collection search paths remain unchanged. No config schema changes. No changes to qmd CLI invocation format. ## Change Type (select all) - [x] Bug fix ## Scope (select all touched areas) - [x] Memory / storage ## Linked Issue/PR - Related #11727 ## User-visible / Behavior Changes Memory search now returns results from all configured collections proportionally, instead of being dominated by the largest collection. No config changes needed. ## Security Impact (required) - New permissions/capabilities? `No` - Secrets/tokens handling changed? `No` - New/changed network calls? `No` - Command/tool execution surface changed? `No` - Data access scope changed? `No` ## Repro + Verification ### Environment - OS: macOS (Darwin arm64) - Runtime: Node.js - QMD searchMode: `search` (default) - Collections: `memory-root` (1 doc), `memory-alt` (1 doc), `memory-dir` (38 docs), `custom-1` (432 docs) ### Steps 1. Configure QMD with default settings (searchMode=search) and multiple collections of varying sizes 2. Run `memory_search("context explosion")` 3. Observe results ### Expected Results from both `custom-1` and `memory-dir` collections appear ### Actual Before fix: Only `custom-1` results returned; `memory-dir` results completely absent After fix: Both collections contribute results, merged by best score per docid ## Evidence - [x] Failing test/log before + passing after Verified via direct qmd CLI: ``` # All collections together → only custom-1 results (memory-dir drowned out) qmd query "context explosion" --json -n 5 -c memory-root -c memory-alt -c memory-dir -c custom-1 → 5/5 results from custom-1, 0 from memory-dir # memory-dir alone → results exist qmd query "context explosion" --json -n 5 -c memory-dir → 5/5 results from memory-dir (score 0.9, 0.51, 0.38, 0.35, 0.32) ``` 37/37 unit tests passing after changes. ## Human Verification (required) - Verified scenarios: Confirmed via production QMD index (~/.openclaw/agents/main/qmd) that per-collection search returns memory-dir results that were previously invisible - Edge cases checked: Single collection (no behavior change), unsupported flag fallback (search → query), missing collection recovery - What you did **not** verify: vsearch mode (no vsearch-capable qmd binary available for testing) ## Compatibility / Migration - Backward compatible? `Yes` - Config/env changes? `No` - Migration needed? `No` ## Failure Recovery (if this breaks) - How to disable/revert this change quickly: Set `searchMode: "query"` in config (query mode already had per-collection logic) - Files/config to restore: `src/memory/qmd-manager.ts` - Known bad symptoms reviewers should watch for: Increased qmd process spawns (N collections × 1 search per query instead of 1 combined search) ## Risks and Mitigations - Risk: More qmd subprocess invocations per search (one per collection instead of one total) - Mitigation: Typical setups have 3-5 collections; each per-collection query is faster than a combined query over a large index. Net latency impact is minimal. <!-- greptile_comment --> <h3>Greptile Summary</h3> This PR fixes a real problem where large QMD collections (e.g. 432 docs) dominated top-N results and prevented smaller collections (e.g. 38 docs) from contributing any results. The core change generalizes `runQueryAcrossCollections` → `runSearchAcrossCollections` to support `search`, `vsearch`, and `query` commands, querying each collection independently and merging results by best score per docid. Additional improvements include: - `--no-expand` optimization for query mode via `buildSearchArgs`, with a static capability flag and automatic fallback - Self-healing recovery for missing collections during search (`recoverMissingCollection`) - `resolveDocLocation` now scoped to managed collections only (prevents returning results from unmanaged collections) - Non-blocking bootstrap: when `waitForBootSync` is false, `create()` no longer blocks on `ensureCollections()` - Boot update no longer blocks search via `waitForPendingUpdateBeforeSearch` skipping when reason is "boot" Issues found: - `runSearchAcrossCollections` in `query` mode does not handle `--no-expand` rejection. When `buildSearchArgs("query", ...)` includes `--no-expand` and the qmd binary rejects it, the error is caught as an "unsupported option" and the loop breaks, but the fallback guard (`command !== "query"`) prevents retry. This silently returns partial/empty results, and `noExpandSupported` is never set to `false` since that logic only exists in the `runQuery` inner function. This affects the first multi-collection search on systems where qmd doesn't support `--no-expand`. <h3>Confidence Score: 3/5</h3> - The PR solves a genuine recall problem and is mostly well-implemented, but has one logic gap in --no-expand fallback handling for multi-collection query paths that should be addressed before merge. - Score of 3 reflects solid overall implementation with good test coverage, but one confirmed logic bug: runSearchAcrossCollections in query mode silently returns partial/empty results when --no-expand is rejected, with no retry or flag update. This bug affects the common multi-collection query code path on systems with older qmd binaries. - Pay close attention to `src/memory/qmd-manager.ts`, specifically the `runSearchAcrossCollections` method's error handling when `command === "query"` and `--no-expand` is unsupported. <sub>Last reviewed commit: 7bd347b</sub> <!-- greptile_other_comments_section --> <!-- /greptile_comment -->

Most Similar PRs