#21978: perf: parallelize QMD multi-collection queries
size: S
Fixes #21974
## Summary
Converts `runQueryAcrossCollections` from sequential to parallel execution using `Promise.allSettled`.
## Problem
With 8 collections and a 30s per-query timeout, worst-case latency was **240 seconds** (sum of all collection query times), often causing the outer timeout to kill the search and fall back to local embeddings.
## Solution
Use `Promise.allSettled` to query all collections in parallel.
**Before (sequential):**
```typescript
for (const collectionName of collectionNames) {
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
// ...
}
```
**After (parallel):**
```typescript
const results = await Promise.allSettled(
collectionNames.map(async (collectionName) => {
const args = this.buildSearchArgs("query", query, limit);
args.push("-c", collectionName);
const result = await this.runQmd(args, { timeoutMs: this.qmd.limits.timeoutMs });
return parseQmdQueryJson(result.stdout, result.stderr);
})
);
```
## Benefits
- ✅ **Total latency = max(collection times)** instead of sum
- ✅ **Failed collections gracefully skipped** via `allSettled`
- ✅ **No behavior change** for the happy path
- ✅ **Worst-case improvement:** 240s → 30s with 8 collections
## Testing
- Existing tests pass (linting clean)
- Logic unchanged: still merges results by `docid`, keeping highest score
- Error handling improved: individual collection failures don't block others
## Environment
Tested on OpenClaw 2026.2.19-2, 8 QMD collections, searchMode: "query".
<!-- greptile_comment -->
<h3>Greptile Summary</h3>
Converts sequential multi-collection QMD queries to parallel execution using `Promise.allSettled`, reducing worst-case latency from 240s (sum of 8×30s timeouts) to 30s (max timeout). The change maintains identical result-merging logic (keeping highest-scoring result per `docid`) while gracefully skipping failed collections. The implementation correctly uses `Promise.allSettled` to prevent individual collection failures from blocking the entire search operation.
<h3>Confidence Score: 5/5</h3>
- Safe to merge - straightforward performance optimization with no behavior changes
- The parallelization is correctly implemented with `Promise.allSettled` to handle failures gracefully. The result-merging logic remains identical to the sequential version. The change is well-tested (per PR description) and addresses a clear performance issue without introducing new edge cases or changing the API contract.
- No files require special attention
<sub>Last reviewed commit: f0df321</sub>
<!-- greptile_other_comments_section -->
<!-- /greptile_comment -->
Most Similar PRs
#16968: fix(qmd): per-collection search to prevent large collections drowni...
by ProgramCaiCai · 2026-02-15
79.6%
#21868: fix: qmd search/vsearch silently return empty with multiple -c coll...
by jacksclaw · 2026-02-20
78.1%
#22937: fix: remove legacy unsuffixed QMD collections on upgrade
by sud0n1m-ziggy · 2026-02-21
73.8%
#20966: fix(memory/qmd): migrate orphaned unscoped collections on upgrade
by marcodelpin · 2026-02-19
71.6%
#20085: Fix QMD memory_search empty results when docid key changes
by rylena · 2026-02-18
69.4%
#9624: fix(memory): resolve QMD search returning empty results [AI-assisted]
by kowshik24 · 2026-02-05
68.9%
#15307: fix(memory): handle mixed/no-results QMD query output
by MohammadErfan-Jabbari · 2026-02-13
67.4%
#16917: fix(memory): close stale SQLite connection after qmd update
by zerone0x · 2026-02-15
66.9%
#21471: fix: check QMD backend before memory search config
by lbo728 · 2026-02-20
66.5%
#11364: fix(memory/qmd): prevent cascading failure when query fails or retu...
by blazerui · 2026-02-07
66.5%