Skip to content

feat(index): parallel segment search with rayon - #115

Merged
poyrazK merged 8 commits into
mainfrom
feature/autocomplete-suggest
Jun 13, 2026
Merged

feat(index): parallel segment search with rayon#115
poyrazK merged 8 commits into
mainfrom
feature/autocomplete-suggest

Conversation

@poyrazK

@poyrazK poyrazK commented Jun 11, 2026

Copy link
Copy Markdown
Owner

Summary

  • Add rayon dependency for parallel document scoring in search()
  • Parallelize the scoring hot loop: doc_ids are iterated in parallel, source lookups happen inside each task via &self (avoiding Send+Sync requirement on IndexDocument)
  • Only (score, doc_id) tuples cross thread boundaries; source is looked up sequentially after scoring (O(hits), not O(all_docs))

Changes

  • 2 commits on top of feature/autocomplete-suggest (which is at main-level for the suggest/MLT work)

Summary by CodeRabbit

  • New Features

    • Improved search performance via parallelized document scoring for faster query response on multi-core systems.
  • Documentation

    • Added an architecture decision record outlining the parallel scoring approach and trade-offs.
    • Updated roadmap with "Phase 1b - Vertical Scaling" and related scaling tasks.
  • Tests

    • Added benchmarks measuring search performance across multiple query types and dataset sizes.

@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@poyrazK, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 53 minutes and 26 seconds. Learn how PR review limits work.

Your organization has used up its prepaid credits, and credit purchases are no longer available. Enable the review add-on in the billing tab to keep reviews running — you're only billed for reviews past your plan's rate limits ($0.25/file).

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 5e079f14-a3a5-4f26-9a6d-508cfb4d7b3c

📥 Commits

Reviewing files that changed from the base of the PR and between a98bca3 and f5072a4.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (1)
  • rust/crates/cloudsearch-index/src/lib.rs
📝 Walkthrough

Walkthrough

Parallelizes BM25 document scoring by using Rayon to compute (score, doc_id) tuples in parallel over doc IDs, then sequentially rehydrates document sources for aggregation, sorting, pagination, and hit construction; adds ADR, workspace/crate dependencies, and Criterion benchmark suite.

Changes

Parallel Segment Search Using Rayon

Layer / File(s) Summary
ADR and Roadmap
docs/adr/0005-parallel-segment-search.md, docs/roadmap.md
ADR 0005 documents the parallel scoring approach, design constraints, consequences, alternatives considered, and benchmark observations. Roadmap adds "Phase 1b - Vertical Scaling".
Workspace & Crate Dependency Changes
rust/Cargo.toml, rust/crates/cloudsearch-index/Cargo.toml, rust/crates/cloudsearch-index/src/lib.rs
Adds workspace dependencies (rayon, criterion), wires cloudsearch-index to use rayon and criterion (dev), and declares the search bench target; imports Rayon iterator helpers in lib.rs.
Benchmark Suite and Index Builder
rust/crates/cloudsearch-index/benches/search.rs
Criterion benchmark module with docs, imports, build_index(n_docs) helper that builds temporary on-disk indexes, bench_search exercising MatchAll, term, multi-field, and range queries across 1K/10K docs, and Criterion group/main configuration.
Parallel Search Implementation
rust/crates/cloudsearch-index/src/lib.rs
Refactors IndexHandle::search to collect doc IDs, parallel-score with Rayon producing (score, doc_id) while filtering expired/MLT-excluded docs, then sequentially rehydrate sources, compute aggregations, sort and paginate, and compute highlights/sort values per hit from temporary IndexDocument.

Sequence Diagram(s)

sequenceDiagram
    participant Client
    participant Search as IndexHandle::search
    participant Parallel as Rayon ParallelIterator
    participant Storage as Document Store

    Client->>Search: search(query)
    Search->>Parallel: par_iter(doc_ids) filter and score
    Parallel->>Parallel: BM25 scoring
    Parallel->>Search: scores and doc_ids
    Search->>Storage: Sequential source lookup
    Storage->>Search: sources with scores
    Search->>Search: Aggregations, sort, search_after
    Search->>Client: SearchResponse
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

  • poyrazK/cloudSearch#95: Both PRs modify IndexHandle::search's scoring pipeline; #95 implements BM25 relevance scoring changes that interact with this PR's parallelization.

Poem

🐰 Threads hum like garden bees, scores bloom in rows,

Doc IDs sprint, then home the sources close.
Rayon scatters, rehydrate collects with care,
Benchmarks hum stories of cores that share.
A tiny rabbit cheers: fast searches everywhere!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 66.67% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(index): parallel segment search with rayon' directly and clearly summarizes the main change in the pull request, which is parallelizing document scoring in the search function using the rayon library.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/autocomplete-suggest

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Parallelizes the hot loop in search() that scores documents against the
query. Only doc_id (String, Send+Sync) crosses thread boundaries; source
lookups happen inside each parallel task via &self, avoiding the
Send+Sync requirement on IndexDocument (whose serde_json::Value field
contains RefCell).

The scoring phase now uses into_par_iter() on doc_ids, filtering expired
docs and MLT exclusions in parallel, then lookups source sequentially
after scoring (O(hits) not O(all_docs)).
@poyrazK
poyrazK force-pushed the feature/autocomplete-suggest branch from f4bc042 to 2f4361f Compare June 11, 2026 18:09
poyrazK added 3 commits June 11, 2026 21:12
Benchmarks for search() across MatchAll, term, multi_match, and range
queries at 1k and 10k document counts.

Run with: cargo bench -p cloudsearch-index
@poyrazK
poyrazK changed the base branch from feat/wal-storage-foundation to main June 11, 2026 18:38
poyrazK added a commit that referenced this pull request Jun 11, 2026
poyrazK added a commit that referenced this pull request Jun 11, 2026
@poyrazK
poyrazK force-pushed the feature/autocomplete-suggest branch from ccd38de to 04689be Compare June 11, 2026 19:21
poyrazK added a commit that referenced this pull request Jun 11, 2026
@poyrazK
poyrazK force-pushed the feature/autocomplete-suggest branch from 04689be to b3dcad0 Compare June 11, 2026 19:23
poyrazK added a commit that referenced this pull request Jun 11, 2026
@poyrazK
poyrazK force-pushed the feature/autocomplete-suggest branch from b3dcad0 to 4f164da Compare June 11, 2026 19:31
poyrazK added a commit that referenced this pull request Jun 11, 2026
@poyrazK
poyrazK force-pushed the feature/autocomplete-suggest branch from 4f164da to 6ecd339 Compare June 11, 2026 19:38

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
rust/crates/cloudsearch-index/src/lib.rs (1)

1029-1044: ⚡ Quick win

Avoid full source clones inside sort comparator.

Line 1029-1044 clones full documents for each comparison; compare on borrowed sort field values and doc_id instead to keep sort overhead low.

Suggested refactor
-            scored.sort_by(|(_, l_id, l_src), (_, r_id, r_src)| {
-                let lh = SearchHit {
-                    id: l_id.clone(),
-                    source: l_src.clone(),
-                    score: None,
-                    highlight: None,
-                    sort_values: None,
-                };
-                let rh = SearchHit {
-                    id: r_id.clone(),
-                    source: r_src.clone(),
-                    score: None,
-                    highlight: None,
-                    sort_values: None,
-                };
-                compare_hits(&lh, &rh, sort)
-            });
+            scored.sort_by(|(_, l_id, l_src), (_, r_id, r_src)| {
+                let l_val = l_src.get(&sort.field).and_then(comparable_value);
+                let r_val = r_src.get(&sort.field).and_then(comparable_value);
+
+                let ordering = match (l_val, r_val) {
+                    (Some(l), Some(r)) => compare_sort_values(&l, &r, sort),
+                    (None, Some(_)) => std::cmp::Ordering::Greater,
+                    (Some(_), None) => std::cmp::Ordering::Less,
+                    (None, None) => l_id.cmp(r_id),
+                };
+
+                if ordering == std::cmp::Ordering::Equal {
+                    l_id.cmp(r_id)
+                } else {
+                    ordering
+                }
+            });
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@rust/crates/cloudsearch-index/src/lib.rs` around lines 1029 - 1044, The
comparator passed to scored.sort_by is cloning entire SearchHit.source for every
comparison (in the closure that builds lh/rh and calls compare_hits), which is
expensive; change the comparator to compare only the doc_id and the specific
sort-field values by borrowing them instead of cloning the whole source.
Concretely, in the scored.sort_by closure avoid constructing full SearchHit with
cloned source—either extract the needed sort values and ids from l_src/r_src and
call a new helper like compare_hits_by_values(&l_id, &r_id, l_sort_vals,
r_sort_vals, sort) or change compare_hits to accept borrowed inputs (e.g., &str
id and &Value or &Vec<Value> sort_values) so you only clone the doc_id when
necessary and borrow sort fields from l_src/r_src. Ensure references to the
existing symbols: scored.sort_by closure, SearchHit construction site,
compare_hits (or a new compare_hits_by_values), and the sort variable are
updated accordingly.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/adr/0005-parallel-segment-search.md`:
- Around line 13-14: Replace the incorrect rationale in the ADR: remove the
claim that serde_json::Value “uses RefCell internally” and is not Send/Sync, and
instead state that serde_json::Value does implement Send + Sync; correct the
Rayon bound by noting that into_par_iter() on a Vec<T> requires T: Send (not T:
Send + Sync). Update the discussion around IndexDocument::source to explain the
actual constraint driving the design (e.g., that IndexDocument::source is
intentionally not moved across threads in current architecture or that
cloning/moving the Value is avoided for performance/ownership reasons), and
ensure the text aligns with the later statement that serde_json::Value “never
crosses thread boundaries.”

In `@rust/crates/cloudsearch-index/benches/search.rs`:
- Around line 58-67: The benchmark setup currently ignores Results from async
calls (e.g., catalog.initialize(), catalog.create_index(...), and the later
indexing calls), allowing silent failures; change these to propagate or fail
fast by handling the Result (use the ? operator if the surrounding function
returns Result, or call .expect("...")/.unwrap_or_else(|e| panic!(...)) with a
clear message) so any error from catalog.initialize(),
catalog.create_index(...), and the subsequent indexing calls is surfaced
immediately and aborts the benchmark.

---

Nitpick comments:
In `@rust/crates/cloudsearch-index/src/lib.rs`:
- Around line 1029-1044: The comparator passed to scored.sort_by is cloning
entire SearchHit.source for every comparison (in the closure that builds lh/rh
and calls compare_hits), which is expensive; change the comparator to compare
only the doc_id and the specific sort-field values by borrowing them instead of
cloning the whole source. Concretely, in the scored.sort_by closure avoid
constructing full SearchHit with cloned source—either extract the needed sort
values and ids from l_src/r_src and call a new helper like
compare_hits_by_values(&l_id, &r_id, l_sort_vals, r_sort_vals, sort) or change
compare_hits to accept borrowed inputs (e.g., &str id and &Value or &Vec<Value>
sort_values) so you only clone the doc_id when necessary and borrow sort fields
from l_src/r_src. Ensure references to the existing symbols: scored.sort_by
closure, SearchHit construction site, compare_hits (or a new
compare_hits_by_values), and the sort variable are updated accordingly.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a68d3fee-9724-4a58-b2fa-bb23b7915587

📥 Commits

Reviewing files that changed from the base of the PR and between a3fc0e4 and 4f164da.

⛔ Files ignored due to path filters (1)
  • rust/Cargo.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • docs/adr/0005-parallel-segment-search.md
  • docs/roadmap.md
  • rust/Cargo.toml
  • rust/crates/cloudsearch-index/Cargo.toml
  • rust/crates/cloudsearch-index/benches/search.rs
  • rust/crates/cloudsearch-index/src/lib.rs

Comment thread docs/adr/0005-parallel-segment-search.md Outdated
Comment thread rust/crates/cloudsearch-index/benches/search.rs Outdated
poyrazK added a commit that referenced this pull request Jun 11, 2026
@poyrazK
poyrazK force-pushed the feature/autocomplete-suggest branch from 6ecd339 to 0a363b8 Compare June 11, 2026 19:45
@poyrazK
poyrazK force-pushed the feature/autocomplete-suggest branch from 0a363b8 to bb47405 Compare June 11, 2026 19:51
poyrazK added 2 commits June 13, 2026 15:32
…pagation, sort clone overhead

- ADR 0005: correct serde_json::Value Send+Sync claim; explain the actual
  design constraint is avoiding cloning overhead, not a Send+Sync blocker
- benches/search.rs: propagate all async Result errors with expect() so
  catalog init/index/flush failures abort the benchmark immediately
- lib.rs: avoid cloning full SearchHit.source in sort comparator; extract
  sort field values directly from l_src/r_src (serde_json::Value::get borrows)

@poyrazK poyrazK left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's okay to merge

@poyrazK
poyrazK merged commit aece867 into main Jun 13, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant