Skip to content

Sync commits Feat/vector engine and fix critical issues#35

Merged
pilotspacex-byte merged 158 commits into
mainfrom
feat/vector-engine
Apr 1, 2026
Merged

Sync commits Feat/vector engine and fix critical issues#35
pilotspacex-byte merged 158 commits into
mainfrom
feat/vector-engine

Conversation

@TinDang97

@TinDang97 TinDang97 commented Apr 1, 2026

Copy link
Copy Markdown
Collaborator

#27

Summary by CodeRabbit

  • New Features

    • Added broader end-to-end vector search tests and moon-only consistency checks.
  • Bug Fixes / Stability

    • DIM parameter validated to 1–65536.
    • Safer initialization paths and deterministic error messages to avoid undefined behavior.
    • Quantization and vector indexing hardened with fallbacks.
    • Refined auto-delete logic to avoid removing keys on field-only operations.
  • Known Limitations

    • FILTER is not supported in multi-shard vector searches.

- Add src/vector/ module tree with aligned_buffer and distance submodules
- Implement AlignedBuffer<T> with 64-byte aligned allocation via std::alloc
- Support new(), from_vec(), Deref/DerefMut, and safe Drop
- Add pub mod vector to lib.rs
- 6 unit tests passing (alignment, read/write, from_vec, empty, deref)
- Implement l2_f32, l2_i8, dot_f32, cosine_f32 scalar kernels
- DistanceTable struct with function pointers for runtime dispatch
- OnceLock init() with x86_64/aarch64 feature detection stubs
- table() returns &'static DistanceTable via unsafe unwrap_unchecked
- 12 unit tests passing (all metrics + edge cases + table init)
- Zero clippy warnings
- AVX2+FMA kernels with 4x unrolled FMA for l2_f32, l2_i8, dot_f32, cosine_f32
- AVX-512 kernels with 2x unrolled 512-bit ops and reduce intrinsics
- AVX-512BW l2_i8_vnni using cvtepi8_epi16 widening (VNNI intrinsic not yet stable)
- Scalar tail loops for non-SIMD-aligned vector lengths
- SAFETY comments on all unsafe blocks
- Comprehensive tests: scalar equivalence, tail handling, empty vectors
- ARM NEON kernels with 4x unrolled FMA for l2_f32, l2_i8, dot_f32, cosine_f32
- DistanceTable init dispatches AVX-512 > AVX2+FMA > NEON > scalar via runtime detection
- Closure wrappers bridge unsafe SIMD fns to safe fn pointers with SAFETY comments
- All 19 distance tests pass including NEON scalar equivalence and dispatch verification
- Fixed redundant parentheses in test helpers across all SIMD modules
- Benchmark all 4 distance metrics (l2_f32, l2_i8, dot_f32, cosine_f32)
- Cover dimensions 128, 384, 768, 1024 plus tail sizes 1, 3, 13, 97, 100
- Compare scalar vs SIMD dispatch at each dimension
- NEON achieves 9.2x speedup on l2_f32@768d, validates VEC-SIMD-02
- Verify SIMD == scalar across 17 dimension sizes for all 4 metrics
- Test edge cases: identical vectors, zero vectors, single element
- Use deterministic LCG PRNG with approx_eq_f32 relative tolerance
- All 1175 project tests pass with zero regressions (VEC-SIMD-01)
- Add VectorId, DistanceMetric, SearchResult newtypes (src/vector/types.rs)
- Add missing // SAFETY: comments on 6 test-only unsafe blocks
- 35 vector module tests passing
- Scalar FWHT in-place O(d log d) butterfly pattern, self-inverse
- AVX2 FWHT with 8-wide SIMD butterflies for h>=8, scalar fallback
- OnceLock dispatch (AVX2 > scalar) matching distance/ pattern
- Lloyd-Max 16-centroid codebook for N(0,1/sqrt(768)), symmetric
- 15 midpoint boundaries, quantize_scalar with fixed-point property
- Fix pre-existing conn/tests.rs compilation (try_inline_dispatch import, write_db lock)
- encode_tq_mse: normalize -> pad -> randomized FWHT -> quantize -> nibble pack
- decode_tq_mse: unpack -> centroids -> inverse FWHT -> unpad -> scale by norm
- Nibble pack/unpack: 2 indices per byte, lossless round-trip
- Round-trip distortion 0.000010 avg, 0.000012 max (well within 0.009 bound)
- 23 tests covering FWHT, codebook, and encoder
- SUMMARY.md with metrics, deviations, decisions
- STATE.md updated with position and decisions
- ROADMAP.md updated with plan progress
…lar kernel

- CollectionMetadata stores materialized sign_flips as AlignedBuffer<f32>
- XXHash64 checksum computed at creation, verified by verify_checksum()
- Checksum mismatch returns CollectionMetadataError (no panic)
- tq_l2_adc_scalar computes asymmetric L2 between rotated query and TQ code
- 13 tests covering checksum integrity, corruption detection, ADC correctness
- Add tq_l2 field to DistanceTable (all tiers use scalar ADC for now)
- Initialize FWHT dispatch during distance::init()
- Add TQ ADC smoke test to existing distance table test
- Compiles under both runtime-tokio and runtime-monoio
- Contiguous AlignedBuffer<u32> layer-0 neighbor storage
- SmallVec upper-layer storage indexed by original node ID
- BFS reorder produces cache-friendly traversal order
- Dual prefetch for x86_64 (neighbor list + vector data)
- 12 unit tests covering BFS reorder, neighbor accessors, TQ code slicing
- Single-threaded HNSW construction via incremental insertion
- Pairwise distance function Fn(u32, u32)->f32 for proper neighbor pruning
- search_layer with BinaryHeap beam search and HashSet visited tracking
- select_neighbors_simple heuristic (nearest M/M0)
- add_neighbor_with_prune replaces farthest existing neighbor when full
- random_level with LCG PRNG following exponential distribution
- 8 unit tests: empty/single/100/500 graphs, level distribution, BFS permutation
- BitVec with test_and_set and memset clear (64x more cache-efficient than HashSet)
- SearchScratch with pre-allocated candidates/results heaps and visited BitVec
- OrdF32Pair with IEEE 754 total ordering for BinaryHeap
- All structures reusable across queries with zero per-search allocation
- hnsw_search: upper-layer greedy descent + layer-0 ef-bounded beam search
- 2-hop dual prefetch of neighbor lists and TQ vector data
- TQ-ADC distance via DistanceTable::tq_l2 kernel
- BitVec visited tracking (64x cache-efficient vs HashSet)
- SearchScratch reuse: zero heap allocation per search
- Recall@10 = 1.0 at ef=128 on 1000 vectors (TQ-ADC ground truth)
- SmallVec<[SearchResult; 32]> keeps results on stack for k <= 32
…nt, and SegmentHolder

- MutableSegment: parking_lot::RwLock, brute-force l2_i8 search, append, freeze, mark_deleted
- ImmutableSegment: HnswGraph+TQ codes, delegated hnsw_search, dead_fraction tracking
- SegmentHolder: ArcSwap lock-free segment list, atomic swap, fan-out search merge
- No HNSW code in MutableSegment (VEC-SEG-01 compile-time enforced)
- No search visibility gap via ArcSwap snapshot (VEC-SEG-02)
- arc-swap dependency added to Cargo.toml
- 18 tests covering search, freeze, snapshot isolation, dead fraction
@TinDang97

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Apr 1, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@TinDang97

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Apr 1, 2026

Copy link
Copy Markdown

Claude finished @TinDang97's task in 15m 14s —— View job


Code Review

Tasks:

  • Gather context
  • Read key files
  • Analyze issues
  • Post review feedback

This is a large, technically ambitious PR adding a complete vector search engine (HNSW + IVF + TurboQuant). The algorithmic work is impressive, but there are several critical correctness/safety issues and rule violations that should be addressed before merging.


Critical Bugs

1. Deadlock: Lock Order Inversion (DB ↔ VectorStore)

This is the most serious issue. There are two concurrent paths that acquire the same two locks in opposite orders:

Local write path (handler_sharded.rs:1355-1379): DB write lock → vector_store lock

let mut guard = shard_databases.write_db(shard_id, db_idx);  // DB lock acquired
// ... dispatch ...
let mut vs = shard_databases.vector_store(shard_id);          // vector_store lock acquired WHILE DB still held

Shard event loop (event_loop.rs:424): vector_store lock → DB write lock

&mut *shard_databases.vector_store(shard_id),  // vector_store lock acquired
// drain_spsc_shared → write_db() inside spsc_handler.rs:204

Under concurrent cross-shard traffic, these two paths can deadlock any shard. The fix is to drop guard before acquiring vector_store, or establish a global lock order and enforce it everywhere. Fix this →

2. Undefined Behavior When distance::init() Not Called

src/vector/distance/mod.rs:157 and src/vector/distance/fastscan.rs:56 both use unwrap_unchecked() protected only by debug_assert!:

unsafe { DISTANCE_TABLE.get().unwrap_unchecked() }  // UB in release if not initialized

Any test or benchmark that uses vector code without going through main() will trigger UB in release builds. Use get_or_init(...) or OnceLock::get_or_init to make initialization automatic and safe. Fix this →

3. Unbounded DIM — DoS via OOM/Panic

ft_create only validates DIM > 0 then calls padded_dimension(dim) which uses next_power_of_two(). For dim > 2^31, this overflows and panics. For very large dims, it triggers massive allocations. The docs say 1–65536 is the intended range; enforce it in code. Fix this →

4. FILTER Silently Ignored in Multi-Shard Mode

handler_sharded.rs:1261 parses the filter expression but discards it (_filter) — it is never forwarded to scatter_vector_search_remote. Queries return different results in single-shard vs multi-shard mode with the same FILTER clause. Either propagate the filter through ShardMessage::VectorSearch or return an explicit error for filtered multi-shard queries. Fix this →

5. DEL k1 k2 Only Marks First Key as Deleted

handler_sharded.rs:1375 calls cmd_args.first() for DEL/UNLINK, so only the first key's vectors are removed. The SPSC path (spsc_handler.rs:290-294) correctly iterates all keys, creating an inconsistency. Fix this →

6. HDEL on Any Field Drops Vector from Index

Both spsc_handler.rs:285-288 and handler_sharded.rs:1373-1378 mark a key's vector as deleted on any HDEL, even when the deleted field is unrelated to the vector (e.g., HDEL doc:1 title). The fix requires checking whether the deleted fields include the index's source_field for that key's matching indexes. Fix this →


Rule Violations (CLAUDE.md)

7. unwrap() in Library Code

Multiple violations of the no-unwrap() rule outside tests:

  • hnsw/search.rs:290: sub_table.unwrap() — panics if use_subcent is true but sub_centroid_table is None
  • tq_adc.rs:596,676: partial_cmp(&b.0).unwrap() — panics on NaN distances. Use f32::total_cmp instead.
  • codebook.rs:141,156: panic!("unsupported bit width: {bits}") — should return Result<Vec<f32>, Error>
  • collection.rs:284-304: assert_eq! + try_into().unwrap() in codebook_16() / codebook_boundaries_15() — should return Option<&[f32; N]>

8. Hot-Path Allocations in src/command/

ft_info() at vector_search.rs:307-344 uses format!(), to_string(), and clone() — all prohibited in src/command/:

format!("{}", idx.meta.hnsw_ef_runtime)   // heap alloc
"auto".to_string()                          // heap alloc
format!("{:?}", idx.meta.quantization)      // heap alloc
Frame::BulkString(idx.meta.name.clone())    // clone

Use itoa for integer formatting and static Bytes for string constants.

Also: format!("{}", r.distance) in result building (line ~562) and to_string() at line 1191.

9. Instant::now() on the Command Path

ft_search() at vector_search.rs:402-405 calls std::time::Instant::now() + elapsed() on every search. This adds syscall overhead on the hot path. Use the shard-cached timestamp instead.

10. vector_search.rs Exceeds 1500 Lines

The file is 1526 lines — over the project limit. Split into submodules: parsing (parse.rs), execution (exec.rs), response building (response.rs), and tests.

11. FT.* Commands Missing from Test Scripts

scripts/test-consistency.sh and scripts/test-commands.sh contain no FT.* coverage. Per CLAUDE.md, new commands require entries in both scripts.

12. CUDA FWHT Kernel Not Wired to build.rs

src/vector/gpu/fwht_kernel.rs documents that the .cu kernel exists at src/gpu/kernels/turbo_quant_wht.cu but is not compiled via build.rs and always returns CudaNotAvailable. Per CLAUDE.md, kernels must be built via build.rs when --features gpu-cuda is enabled.


Minor Observations

  • src/vector/segment/holder.rs (825 lines) and src/vector/segment/mutable.rs (792 lines) are approaching the limit and will likely need splitting as they grow.
  • The VectorStore uses Arc<Mutex<>> in server/listener.rs per the Qodo review; the CLAUDE.md coding rules prefer parking_lot::Mutex.
  • search.rs:740 has the same partial_cmp(...).unwrap() issue inside a test function — while tests are exempt from the no-unwrap rule, using f32::total_cmp would be more robust.

Summary: The vector engine implementation is architecturally sound, but the deadlock risk (issue #1) and UB (issue #2) are blocking issues. The HDEL/DEL correctness bugs (#5, #6) and silent FILTER discard (#4) will cause silent data integrity problems. The unwrap() violations should be systematically addressed. I'd recommend fixing the critical bugs before merging.

Bugs fixed:
- Deadlock: move vector_store lock AFTER DB write lock release in handler_sharded
- Unbounded DIM: enforce DIM 1-65536 in FT.CREATE (prevents OOM/panic)
- DEL multi-key: iterate ALL keys for DEL/UNLINK vector deletion, not just first
- HDEL: remove from auto-delete trigger (field deletion != key deletion)
- FILTER cross-shard: return ERR if FILTER used with multi-shard (was silent ignore)
- UB on uninit: replace unsafe unwrap_unchecked with .expect() in distance/fastscan

Rule violations fixed:
- Split vector_search.rs (1531→855 lines) into mod.rs + tests.rs submodule
- Add FT.* to test-commands.sh and test-consistency.sh
- Replace format!/to_string() in ft_info() with itoa + static Bytes
- Remove Instant::now() from ft_search hot path
- Replace sub_table.unwrap() with safe fallback in HNSW search
- Replace partial_cmp().unwrap() with f32::total_cmp in tq_adc
- Replace panic! with tracing::warn + empty Vec in codebook
- Replace assert_eq!/try_into().unwrap() with safe match in collection

@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: 11

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@scripts/test-commands.sh`:
- Around line 682-698: The test blocks updating PASS/FAIL for the vector
assertions do not increment TOTAL, causing the final summary to be wrong; update
the script so each assertion that modifies PASS or FAIL also increments TOTAL
(e.g., before or immediately after each conditional that updates PASS/FAIL for
the FT.INFO check, the FT.SEARCH check, and the FT.INFO after drop check),
ensuring TOTAL is incremented alongside PASS/FAIL for the checks involving
FT.INFO, FT.SEARCH, and FT.INFO after drop (symbols: TOTAL, PASS, FAIL, and the
FT-related checks/assert_moon usage).
- Around line 685-691: The test uses positional FT.SEARCH and command
substitution which strips null bytes; change the test to call FT.SEARCH using
the server's expected PARAMS form so extract_param_blob() is exercised: build
the 16‑byte binary vector into a temporary file with printf '\x...' (to preserve
null bytes), then invoke mcli FT.SEARCH with a query that references a parameter
name (e.g. "'@embedding:$p'") followed by PARAMS <count> <param-name> `@tempfile`
KNN 2 so the server reads the raw blob from the file instead of losing null
bytes via "$(…)" substitution.

In `@scripts/test-consistency.sh`:
- Around line 499-500: The grep -q call is causing the script to exit under set
-e before the PASS/FAIL accounting runs; replace the two-line pattern using $?
with a proper if/then wrapper that runs grep in the conditional (e.g., if echo
"$FT_INFO" | grep -q "vecidx"; then PASS=$((PASS + 1)); else FAIL=$((FAIL + 1));
echo "  FAIL: FT.INFO should show vecidx"; fi) so the grep exit status is
handled by the conditional instead of triggering set -e; update the block that
references FT_INFO, PASS, and FAIL accordingly.
- Around line 494-495: The two redis-cli HSET lines using bash command
substitution with printf containing NUL bytes (e.g., the lines with redis-cli -p
"$PORT_RUST" HSET vec:1 embedding "$(printf '\x00\x00\x80\x3f...')") corrupt
binary data because Bash truncates at NUL; replace each of these with a
redis-cli -x invocation that reads binary from stdin and generate the binary
payload with a short Python one-liner (via python -c writing struct.pack bytes
to stdout) piped into redis-cli -x HSET vec:1 embedding -p "$PORT_RUST" (and
similarly for vec:2), and make the same substitution for the identical pattern
in the other script (the line that matches the redis-cli ... HSET vec:*
embedding "$(printf ...)") so binary FLOAT32 blobs are preserved.

In `@src/command/vector_search/tests.rs`:
- Around line 636-680: The test test_vector_metrics_increment_decrement is flaky
because shared atomics VECTOR_INDEXES and VECTOR_SEARCH_TOTAL are modified by
other tests; wrap the metric snapshot+operation+assertion sequences with a
process-wide mutex to serialize access. Add a static global lock (e.g., a
once_cell::sync::Lazy< std::sync::Mutex<()> > METRICS_LOCK) in the tests module
and acquire METRICS_LOCK.lock().unwrap() around the FT.CREATE section
(before_create → ft_create → after_create → assert), around the FT.SEARCH
section (before_search → ft_search → after_search → assert), and around the
FT.DROPINDEX section (before_drop → ft_dropindex → after_drop → assert) so the
before/after snapshots cannot be disturbed by other tests; keep ft_create,
ft_search, ft_dropindex calls and the VECTOR_INDEXES/VECTOR_SEARCH_TOTAL loads
as-is.
- Around line 241-289: Add a duplicate of the existing FT.SEARCH tests that
explicitly forces the scalar distance backend before running the same
assertions: for each test (e.g. test_ft_search_dimension_mismatch and
test_ft_search_empty_index) call the scalar-backend initializer (for example
crate::vector::distance::init_scalar() or
crate::vector::distance::set_backend(Backend::Scalar) /
crate::vector::distance::force_scalar(true) depending on your API) instead of
crate::vector::distance::init(), then run ft_search(&mut store, &search_args)
and assert the same results; ensure the new tests reference ft_search and
crate::vector::distance so both SIMD and scalar paths are covered.

In `@src/server/conn/handler_monoio.rs`:
- Around line 1419-1437: The monoio handler is incorrectly rejecting FILTER and
always calling scatter_vector_search_remote even when num_shards == 1; update
the match branch that handles Ok((index_name, query_blob, k, filter)) so that
when num_shards == 1 it calls the local ft_search(...) path (the same way
handler_sharded.rs does) and preserves FILTER handling, otherwise keep the
existing scatter_vector_search_remote(...) call; use parse_ft_search_args,
ft_search, scatter_vector_search_remote, shard_databases, dispatch_tx and
spsc_notifiers to locate and implement the conditional routing.

In `@src/vector/distance/fastscan.rs`:
- Around line 55-57: Replace the panic-on-missing-initialization in
fastscan_dispatch by using FASTSCAN_DISPATCH.get_or_init(...) so the dispatch is
initialized automatically; implement the closure to detect AVX2 (cfg target_arch
= "x86_64" + is_x86_feature_detected!("avx2")) and return a FastScanDispatch
with scan_block calling fastscan_block_avx2 when available, otherwise return a
FastScanDispatch with scan_block = fastscan_block_scalar; update the
fastscan_dispatch function to return the reference from get_or_init and remove
the expect()/panic path and any safety precondition related to init_fastscan().

In `@src/vector/distance/mod.rs`:
- Around line 149-151: The table() accessor currently calls
DISTANCE_TABLE.get().expect(...), which can panic; replace that path with a
non-panicking access by using DISTANCE_TABLE.get_or_init(...) (or simply
DISTANCE_TABLE.get_or_init(|| /* previously initialized value or call to init
*/) and return the reference it yields) so library code no longer uses expect();
update the table() function to call get_or_init on the OnceLock<DistanceTable>
named DISTANCE_TABLE to retrieve the DistanceTable instance safely (matching
other uses in this module).

In `@src/vector/turbo_quant/codebook.rs`:
- Around line 141-144: The match arm that currently returns Vec::new() for
unsupported centroid bit widths should fail fast instead: change the function to
return a Result (e.g., Result<Vec<CentroidType>, Error>) and replace the
fallback arm(s) that return Vec::new() with an Err containing a descriptive
error like "unsupported centroid bit width {bits}" (do the same for the other
occurrence at the second match), update all successful branches to return
Ok(...), and propagate the Result to callers; mirror the stricter behavior used
by code_bytes_per_vector so configuration/programming errors are surfaced rather
than silently producing empty codebooks.

In `@src/vector/turbo_quant/collection.rs`:
- Around line 284-291: The current fallback returns a ZERO array when
self.codebook_boundaries.as_slice().try_into() fails, which hides metadata
corruption; change this to fail fast by removing the silent ZERO fallback and
instead return an Err (or propagate a Result) with a clear error message (e.g.,
"invalid codebook_boundaries length for 4-bit quantization") or panic with that
message if changing the signature isn’t feasible; apply the same change to the
other occurrence around lines 299-306 so any invalid
codebook_lengths/codebook_boundaries on methods using self.codebook_boundaries
and try_into are reported immediately rather than producing all-zero
quantization metadata.
🪄 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: f072b5bf-a9e3-4336-8f43-24fcb2016361

📥 Commits

Reviewing files that changed from the base of the PR and between 65224d1 and 7dee89f.

📒 Files selected for processing (13)
  • scripts/test-commands.sh
  • scripts/test-consistency.sh
  • src/command/vector_search/mod.rs
  • src/command/vector_search/tests.rs
  • src/server/conn/handler_monoio.rs
  • src/server/conn/handler_sharded.rs
  • src/shard/spsc_handler.rs
  • src/vector/distance/fastscan.rs
  • src/vector/distance/mod.rs
  • src/vector/hnsw/search.rs
  • src/vector/turbo_quant/codebook.rs
  • src/vector/turbo_quant/collection.rs
  • src/vector/turbo_quant/tq_adc.rs

Comment thread scripts/test-commands.sh Outdated
Comment thread scripts/test-commands.sh Outdated
Comment on lines +685 to +691
# Insert vectors via HSET (auto-indexed)
mcli HSET doc:1 embedding "$(printf '\x00\x00\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')" >/dev/null 2>&1
mcli HSET doc:2 embedding "$(printf '\x00\x00\x00\x00\x00\x00\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00')" >/dev/null 2>&1

# FT.SEARCH — basic vector search
FT_SEARCH=$(mcli FT.SEARCH myidx 4 "$(printf '\x00\x00\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')" KNN 2 2>&1)
if echo "$FT_SEARCH" | grep -q "doc:"; then PASS=$((PASS + 1)); echo " PASS: FT.SEARCH returns results"; else FAIL=$((FAIL + 1)); echo " FAIL: FT.SEARCH returns results"; fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

❓ Verification inconclusive

Script executed:

#!/bin/bash
set -euo pipefail

echo "Current vector smoke-test snippet:"
sed -n '685,691p' scripts/test-commands.sh

echo
echo "Current FT.SEARCH parser in the server:"
sed -n '660,699p' src/command/vector_search/mod.rs

echo
blob="$(printf '\x00\x00\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')"
printf 'bytes preserved by command substitution: %s\n' "$(printf '%s' "$blob" | wc -c)"

Repository: pilotspace/moon


Repository: pilotspace/moon
Exit code: 0

stdout:

Current vector smoke-test snippet:
    # Insert vectors via HSET (auto-indexed)
    mcli HSET doc:1 embedding "$(printf '\x00\x00\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')" >/dev/null 2>&1
    mcli HSET doc:2 embedding "$(printf '\x00\x00\x00\x00\x00\x00\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00')" >/dev/null 2>&1

    # FT.SEARCH — basic vector search
    FT_SEARCH=$(mcli FT.SEARCH myidx 4 "$(printf '\x00\x00\x80\x3f\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00')" KNN 2 2>&1)
    if echo "$FT_SEARCH" | grep -q "doc:"; then PASS=$((PASS + 1)); echo "  PASS: FT.SEARCH returns results"; else FAIL=$((FAIL + 1)); echo "  FAIL: FT.SEARCH returns results"; fi

Current FT.SEARCH parser in the server:
pub fn parse_ft_search_args(
    args: &[Frame],
) -> Result<(Bytes, Bytes, usize, Option<FilterExpr>), Frame> {
    if args.len() < 2 {
        return Err(Frame::Error(Bytes::from_static(
            b"ERR wrong number of arguments for 'FT.SEARCH' command",
        )));
    }

    let index_name = match extract_bulk(&args[0]) {
        Some(b) => b,
        None => return Err(Frame::Error(Bytes::from_static(b"ERR invalid index name"))),
    };

    let query_str = match extract_bulk(&args[1]) {
        Some(b) => b,
        None => return Err(Frame::Error(Bytes::from_static(b"ERR invalid query"))),
    };

    let (k, param_name) = match parse_knn_query(&query_str) {
        Some(parsed) => parsed,
        None => {
            return Err(Frame::Error(Bytes::from_static(
                b"ERR invalid KNN query syntax",
            )));
        }
    };

    let query_blob = match extract_param_blob(args, &param_name) {
        Some(blob) => blob,
        None => {
            return Err(Frame::Error(Bytes::from_static(
                b"ERR query vector parameter not found in PARAMS",
            )));
        }
    };

    let filter = parse_filter_clause(args);
    Ok((index_name, query_blob, k, filter))
}

bytes preserved by command substitution: 2

stderr:

/bin/bash: line 12: warning: command substitution: ignored null byte in input

Vector smoke test uses incompatible command format and loses binary data.

The server's FT.SEARCH parser (lines 660–699 in src/command/vector_search/mod.rs) expects the format:

FT.SEARCH <index> '<query>' PARAMS <count> <key> <vector-blob>

where the query string is parsed to extract a parameter name, then the vector blob is retrieved via extract_param_blob().

The test at lines 685–691 uses the incompatible positional form:

FT.SEARCH myidx 4 "$(printf '...')" KNN 2

Additionally, command substitution strips null bytes from the binary payload. The bash warning confirms this: ignored null byte in input, and the payload is reduced from 16 bytes to 2 bytes before being sent to the server.

The test cannot exercise the intended code path and will fail to parse correctly.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@scripts/test-commands.sh` around lines 685 - 691, The test uses positional
FT.SEARCH and command substitution which strips null bytes; change the test to
call FT.SEARCH using the server's expected PARAMS form so extract_param_blob()
is exercised: build the 16‑byte binary vector into a temporary file with printf
'\x...' (to preserve null bytes), then invoke mcli FT.SEARCH with a query that
references a parameter name (e.g. "'@embedding:$p'") followed by PARAMS <count>
<param-name> `@tempfile` KNN 2 so the server reads the raw blob from the file
instead of losing null bytes via "$(…)" substitution.

Comment thread scripts/test-consistency.sh Outdated
Comment thread scripts/test-consistency.sh Outdated
Comment on lines +241 to +289
fn test_ft_search_dimension_mismatch() {
let mut store = VectorStore::new();
let args = ft_create_args();
ft_create(&mut store, &args);

// Build a query with wrong dimension (4 bytes instead of 128*4)
let search_args = vec![
bulk(b"myidx"),
bulk(b"*=>[KNN 10 @vec $query]"),
bulk(b"PARAMS"),
bulk(b"2"),
bulk(b"query"),
bulk(b"tooshort"),
];
let result = ft_search(&mut store, &search_args);
match &result {
Frame::Error(e) => assert!(
e.starts_with(b"ERR query vector dimension"),
"expected dimension mismatch error, got {:?}",
std::str::from_utf8(e)
),
other => panic!("expected error, got {other:?}"),
}
}

#[test]
fn test_ft_search_empty_index() {
let mut store = VectorStore::new();
let args = ft_create_args();
ft_create(&mut store, &args);

// Build valid query for dim=128
let query_vec: Vec<u8> = vec![0u8; 128 * 4]; // 128 floats, all zero
let search_args = vec![
bulk(b"myidx"),
bulk(b"*=>[KNN 5 @vec $query]"),
bulk(b"PARAMS"),
bulk(b"2"),
bulk(b"query"),
Frame::BulkString(Bytes::from(query_vec)),
];
crate::vector::distance::init();
let result = ft_search(&mut store, &search_args);
match result {
Frame::Array(items) => {
assert_eq!(items[0], Frame::Integer(0)); // no results
}
other => panic!("expected Array, got {other:?}"),
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Search coverage still misses the scalar backend.

These FT.SEARCH tests only call crate::vector::distance::init(), so x86 CI will exercise the runtime-selected SIMD path and leave the scalar fallback untested. Please run at least one end-to-end search assertion with the scalar backend forced on.
Based on learnings: Applies to **/*.rs : Add unit tests for both SIMD and scalar paths

Also applies to: 360-444, 599-623

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/vector_search/tests.rs` around lines 241 - 289, Add a duplicate
of the existing FT.SEARCH tests that explicitly forces the scalar distance
backend before running the same assertions: for each test (e.g.
test_ft_search_dimension_mismatch and test_ft_search_empty_index) call the
scalar-backend initializer (for example crate::vector::distance::init_scalar()
or crate::vector::distance::set_backend(Backend::Scalar) /
crate::vector::distance::force_scalar(true) depending on your API) instead of
crate::vector::distance::init(), then run ft_search(&mut store, &search_args)
and assert the same results; ensure the new tests reference ft_search and
crate::vector::distance so both SIMD and scalar paths are covered.

Comment thread src/server/conn/handler_monoio.rs Outdated
Comment thread src/vector/distance/fastscan.rs Outdated
Comment on lines +55 to +57
FASTSCAN_DISPATCH
.get()
.expect("init_fastscan() must be called before fastscan_dispatch()")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the file and the specific lines mentioned
cat -n src/vector/distance/fastscan.rs | head -70

Repository: pilotspace/moon

Length of output: 3214


🏁 Script executed:

# Let's also search for FASTSCAN_DISPATCH definition to understand its type
rg "FASTSCAN_DISPATCH" -B 3 -A 3

Repository: pilotspace/moon

Length of output: 1485


🏁 Script executed:

# Check if get_or_init is available and what the type signature looks like
rg "get_or_init" src/

Repository: pilotspace/moon

Length of output: 252


🏁 Script executed:

# Let's also check if there are other .expect() calls in the library code to understand the pattern
rg "\.expect\(" src/ --type rs | head -20

Repository: pilotspace/moon

Length of output: 85


Replace .expect() with get_or_init() for automatic initialization.

This code violates the library guideline against .expect() in non-test code. Instead of panicking on missing initialization, call get_or_init() with the scalar fallback as default:

pub fn fastscan_dispatch() -> &'static FastScanDispatch {
    FASTSCAN_DISPATCH.get_or_init(|| {
        #[cfg(target_arch = "x86_64")]
        {
            if is_x86_feature_detected!("avx2") {
                return FastScanDispatch {
                    scan_block: |codes, lut, dim_half, results| {
                        unsafe { fastscan_block_avx2(codes, lut, dim_half, results) }
                    },
                };
            }
        }
        FastScanDispatch {
            scan_block: fastscan_block_scalar,
        }
    })
}

This eliminates the panic path, removes the safety contract requirement, and follows the guideline: "No unwrap() or expect() in library code outside tests; use pattern matching or if let."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/vector/distance/fastscan.rs` around lines 55 - 57, Replace the
panic-on-missing-initialization in fastscan_dispatch by using
FASTSCAN_DISPATCH.get_or_init(...) so the dispatch is initialized automatically;
implement the closure to detect AVX2 (cfg target_arch = "x86_64" +
is_x86_feature_detected!("avx2")) and return a FastScanDispatch with scan_block
calling fastscan_block_avx2 when available, otherwise return a FastScanDispatch
with scan_block = fastscan_block_scalar; update the fastscan_dispatch function
to return the reference from get_or_init and remove the expect()/panic path and
any safety precondition related to init_fastscan().

Comment thread src/vector/distance/mod.rs Outdated
Comment on lines +149 to +151
DISTANCE_TABLE
.get()
.expect("distance::init() must be called before table()")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

cat -n src/vector/distance/mod.rs | sed -n '140,160p'

Repository: pilotspace/moon

Length of output: 811


🏁 Script executed:

# First, let's understand the file structure and find DISTANCE_TABLE definition
rg -n "DISTANCE_TABLE" src/vector/distance/mod.rs -A 3 -B 3

Repository: pilotspace/moon

Length of output: 767


🏁 Script executed:

# Check the full context around lines 149-151
head -n 160 src/vector/distance/mod.rs | tail -n 25 | cat -n

Repository: pilotspace/moon

Length of output: 894


Replace .expect() with .get_or_init() to maintain library code compliance.

The table() function uses .expect() in library code, violating the guideline requiring non-panicking access patterns. Switch to get_or_init() since it's already available on OnceLock<DistanceTable> and used elsewhere in the module.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/vector/distance/mod.rs` around lines 149 - 151, The table() accessor
currently calls DISTANCE_TABLE.get().expect(...), which can panic; replace that
path with a non-panicking access by using DISTANCE_TABLE.get_or_init(...) (or
simply DISTANCE_TABLE.get_or_init(|| /* previously initialized value or call to
init */) and return the reference it yields) so library code no longer uses
expect(); update the table() function to call get_or_init on the
OnceLock<DistanceTable> named DISTANCE_TABLE to retrieve the DistanceTable
instance safely (matching other uses in this module).

Comment thread src/vector/turbo_quant/codebook.rs Outdated
Comment thread src/vector/turbo_quant/collection.rs
Scripts:
- test-commands.sh: increment TOTAL for FT.* inline checks, fix binary
  blob handling via python3 pipe (bash strips null bytes)
- test-consistency.sh: fix grep -q under set -e, same binary blob fix

Safety:
- distance::table() and fastscan_dispatch() auto-init on first use
  (eliminates UB risk if init() not called explicitly)
- codebook: scaled_centroids_n/scaled_boundaries_n return Result<>
  instead of silently returning empty Vec on unsupported bit width
- collection: codebook_16/boundaries_15 log tracing::error on invariant
  violation before zero fallback (makes corruption visible)

Correctness:
- handler_monoio: add single-shard fast path for FT.* with FILTER
  support (was always using scatter path even for num_shards==1)
- tests: add METRICS_LOCK mutex for test_vector_metrics to prevent
  flaky failures from parallel test interference on global atomics

@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: 1

♻️ Duplicate comments (1)
src/vector/turbo_quant/collection.rs (1)

220-233: ⚠️ Potential issue | 🟠 Major

Silent failure in constructor creates unusable metadata that passes checksum verification.

When scaled_centroids_n or scaled_boundaries_n fails, the constructor logs an error but continues building metadata with an empty codebook. This metadata:

  1. Passes verify_checksum() (checksum computed on invalid state)
  2. Causes downstream codebook_16() / codebook_boundaries_15() to return zero arrays
  3. Produces silently wrong search results per the relevant code snippets

The constructor should propagate errors rather than create a metadata object that appears valid but is fundamentally broken.

🔧 Suggested approach: make constructor fallible
-    pub fn with_build_mode(
+    pub fn with_build_mode(
         collection_id: u64,
         dimension: u32,
         metric: DistanceMetric,
         quantization: QuantizationConfig,
         seed: u64,
         build_mode: BuildMode,
-    ) -> Self {
+    ) -> Result<Self, &'static str> {
         // ... sign flip generation unchanged ...

         let mut meta = Self {
             // ...
             codebook: if quantization.is_turbo_quant() {
-                scaled_centroids_n(padded, quantization.bits()).unwrap_or_else(|e| {
-                    tracing::error!("failed to compute codebook centroids: {e}");
-                    Vec::new()
-                })
+                scaled_centroids_n(padded, quantization.bits())?
             } else {
                 Vec::new()
             },
             codebook_boundaries: if quantization.is_turbo_quant() {
-                scaled_boundaries_n(padded, quantization.bits()).unwrap_or_else(|e| {
-                    tracing::error!("failed to compute codebook boundaries: {e}");
-                    Vec::new()
-                })
+                scaled_boundaries_n(padded, quantization.bits())?
             } else {
                 Vec::new()
             },
             // ...
         };
         meta.metadata_checksum = meta.compute_checksum();
-        meta
+        Ok(meta)
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/vector/turbo_quant/collection.rs` around lines 220 - 233, The constructor
currently swallows errors from scaled_centroids_n and scaled_boundaries_n and
substitutes empty Vecs, producing invalid metadata that still passes
verify_checksum; change the constructor (the function building the struct that
calls scaled_centroids_n and scaled_boundaries_n) to be fallible and propagate
those errors (return a Result) instead of logging and returning Vec::new(), so
failures bubble up to callers; update call sites to handle the Result and ensure
downstream callers like codebook_16 and codebook_boundaries_15 never receive
empty/invalid codebooks.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/vector/turbo_quant/collection.rs`:
- Around line 303-306: The docstring for the accessor (the method that returns
the codebook as a `&[f32; 16]`, e.g., codebook_as_array) is misleading—update it
to state clearly that this accessor is intended only for 4-bit quantization
implementations (TurboQuant4 / TurboQuantProd4) and that returning a zeroed
array is an error fallback for invariant violations rather than expected
behavior; also add a defensive check (debug_assert! or unreachable! / panic) in
codebook_as_array to catch non-4-bit usage in debug builds so callers are
notified of the invariant breach.

---

Duplicate comments:
In `@src/vector/turbo_quant/collection.rs`:
- Around line 220-233: The constructor currently swallows errors from
scaled_centroids_n and scaled_boundaries_n and substitutes empty Vecs, producing
invalid metadata that still passes verify_checksum; change the constructor (the
function building the struct that calls scaled_centroids_n and
scaled_boundaries_n) to be fallible and propagate those errors (return a Result)
instead of logging and returning Vec::new(), so failures bubble up to callers;
update call sites to handle the Result and ensure downstream callers like
codebook_16 and codebook_boundaries_15 never receive empty/invalid codebooks.
🪄 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: 7e723bc7-090d-47d5-9470-02d02a58be45

📥 Commits

Reviewing files that changed from the base of the PR and between 7dee89f and c74e379.

📒 Files selected for processing (11)
  • scripts/test-commands.sh
  • scripts/test-consistency.sh
  • src/command/vector_search/tests.rs
  • src/server/conn/handler_monoio.rs
  • src/vector/distance/fastscan.rs
  • src/vector/distance/mod.rs
  • src/vector/turbo_quant/codebook.rs
  • src/vector/turbo_quant/collection.rs
  • src/vector/turbo_quant/encoder.rs
  • src/vector/turbo_quant/inner_product.rs
  • src/vector/turbo_quant/tq_adc.rs
✅ Files skipped from review due to trivial changes (3)
  • scripts/test-consistency.sh
  • src/vector/distance/fastscan.rs
  • src/command/vector_search/tests.rs
🚧 Files skipped from review as they are similar to previous changes (4)
  • src/vector/distance/mod.rs
  • src/server/conn/handler_monoio.rs
  • scripts/test-commands.sh
  • src/vector/turbo_quant/codebook.rs

Comment thread src/vector/turbo_quant/collection.rs Outdated
@pilotspacex-byte pilotspacex-byte changed the title Sync commits Feat/vector engine Sync commits Feat/vector engine and fix critical issues Apr 1, 2026
…sage

- codebook_16() and codebook_boundaries_15(): update docstrings to clarify
  these are only valid for 4-bit quantization and the zero fallback is an
  error path, not expected behavior
- Add debug_assert_eq! to catch non-4-bit usage in debug/test builds

@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: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/vector/turbo_quant/collection.rs`:
- Around line 220-233: CollectionMetadata is being constructed with empty
codebook and codebook_boundaries when scaled_centroids_n or scaled_boundaries_n
fail, producing an invalid collection; change the constructor to fail instead of
swallowing errors: when quantization.is_turbo_quant() and
scaled_centroids_n(...) or scaled_boundaries_n(...) returns Err, propagate that
error (or panic) so construction returns Result<Self, CollectionMetadataError>
(or panics) rather than inserting Vec::new(); update the constructor signature
and all call sites that create CollectionMetadata (and any tests) to handle the
Result, and ensure callers that rely on codebook_16() / codebook_boundaries_15()
won’t see silent all-zero data.
🪄 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: 1b66c7ab-5118-4955-9fe4-92315b479fbd

📥 Commits

Reviewing files that changed from the base of the PR and between c74e379 and 777be2b.

📒 Files selected for processing (1)
  • src/vector/turbo_quant/collection.rs

Comment thread src/vector/turbo_quant/collection.rs Outdated
…! on command path

- tq_adc.rs: replace panic! on unsupported bit width and try_into().unwrap_or_else
  with tracing::error + f32::MAX fallback (no panic in production)
- codebook.rs: replace panic! in code_bytes_per_vector with tracing::error + 0
- vector_search/mod.rs: replace format!("ERR {msg}") with pre-allocated Vec,
  replace format!("{}", distance) with write! to pre-allocated String
… errors

CollectionMetadata constructor now uses .expect() for scaled_centroids_n/
scaled_boundaries_n results. Invalid bit widths are programming invariants
(QuantizationConfig guarantees 1-4), not user input — silent empty Vec
would produce all-zero quantization data downstream.

Accessors (codebook_16, codebook_boundaries_15) keep defense-in-depth:
debug_assert + tracing::error + zero fallback, since these are on the
search hot path and the invariant is already enforced at construction.

@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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/command/vector_search/mod.rs (1)

421-424: ⚠️ Potential issue | 🟡 Minor

Latency metric removal leaves INFO command reporting stale data.

Per the code change, per-search latency tracking was removed—search_local_filtered() no longer calls record_search_latency(). However, src/command/connection.rs:174 still reads and exposes VECTOR_SEARCH_LATENCY_US in the INFO output. This will report either zero or a stale value from before this change.

Consider either:

  1. Removing the metric from the INFO output and deleting VECTOR_SEARCH_LATENCY_US / record_search_latency() from src/vector/metrics.rs, or
  2. Retaining latency tracking if this metric is valuable for observability.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/command/vector_search/mod.rs` around lines 421 - 424, The INFO output is
still reading VECTOR_SEARCH_LATENCY_US even though search_local_filtered no
longer records per-search latency; either remove the stale metric usage or
restore latency recording. Option A: delete VECTOR_SEARCH_LATENCY_US and
record_search_latency() from src/vector/metrics.rs and remove references to that
metric in the INFO handler in connection.rs (stop exposing
VECTOR_SEARCH_LATENCY_US). Option B: reintroduce latency recording by calling
record_search_latency(...) inside search_local_filtered (or any wrapper that
runs each search) so VECTOR_SEARCH_LATENCY_US remains valid; update
search_local_filtered and ensure metrics::record_search_latency is imported and
called with the measured duration. Use the symbols search_local_filtered,
record_search_latency, and VECTOR_SEARCH_LATENCY_US to locate and change the
code.
♻️ Duplicate comments (2)
src/vector/turbo_quant/collection.rs (1)

289-332: ⚠️ Potential issue | 🟠 Major

The zero-filled fallback still masks corrupted metadata.

src/vector/hnsw/search.rs, src/vector/segment/mutable.rs, and src/vector/persistence/segment_io.rs all consume these accessors as valid 4-bit tables. Returning [0.0; N] here silently degrades scoring/encoding instead of surfacing the invariant break. Please fail the operation rather than substitute zeros.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/vector/turbo_quant/collection.rs` around lines 289 - 332, The accessors
codebook_boundaries_15 and codebook_16 currently log an error and return
zero-filled fallbacks which masks corrupted metadata; instead, make them fail
hard when the slice -> array conversion fails by replacing the Err(_) branches
with a panic (or unreachable!) that includes the actual len and a clear message
about violated construction invariants (remove the static ZERO fallback). Keep
the existing debug_asserts but ensure the panic is used in release builds so
callers like code in hnsw::search, segment::mutable, and persistence::segment_io
observe the failure rather than proceeding with zeros; update the doc comments
to state that these functions will panic if invariants are violated.
src/vector/turbo_quant/codebook.rs (1)

193-196: ⚠️ Potential issue | 🟠 Major

Don't use 0 as the unsupported-bit fallback here.

This turns an invariant failure into bad layout math. CollectionMetadata::code_bytes_per_vector() forwards self.quantization.bits(), so a valid QuantizationConfig::Sq8 metadata object now reports 0 bytes, and src/vector/turbo_quant/tq_adc.rs uses that size to compute bytes_per_code and slice the buffer. Please fail fast or return a Result here instead of a sentinel length.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/vector/turbo_quant/codebook.rs` around lines 193 - 196, The match arm
that returns 0 for unsupported bit widths must not return a sentinel 0; change
it to fail fast by replacing the "0" fallback with a panic (or a Result::Err)
that includes the unsupported bit width so callers (e.g.,
CollectionMetadata::code_bytes_per_vector() and consumers in
turbo_quant::tq_adc.rs) never get a bogus length; update the code in codebook.rs
where the match occurs to call panic!("unsupported bit width for
code_bytes_per_vector: {}", bits) (or change the surrounding function to return
Result and propagate an Err with that message) so the invalid
QuantizationConfig::Sq8 path fails immediately.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/vector/turbo_quant/collection.rs`:
- Around line 220-231: The code currently calls scaled_centroids_n(...)
.expect(...) and scaled_boundaries_n(...) .expect(...); replace each .expect()
with explicit pattern matching: call scaled_centroids_n(padded,
quantization.bits()) and match the Result — on Ok(v) return v for the codebook
field, on Err(e) call unreachable!() (or unreachable!("codebook centroids:
invalid bit width: {}", quantization.bits())) to express the invariant; do the
same for scaled_boundaries_n(...) used for codebook_boundaries, referencing
quantization.is_turbo_quant(), scaled_centroids_n, and scaled_boundaries_n to
locate the spots.

In `@src/vector/turbo_quant/tq_adc.rs`:
- Around line 320-333: The per-vector logging and f32::MAX sentinel in the
tq_adc hot path must be replaced by a one-time validation and abort: move
validation of centroid slice length (e.g., exactly 16 centroids for 4-bit) and
supported bit widths out of the inner branch in tq_l2_adc_scaled()/tq_adc.rs and
into the caller (brute_force_tq_adc_multibit()), perform these checks once
before scanning candidates, and return an error/abort the search if invariants
fail; then remove the per-vector tracing::error + f32::MAX fallback in the match
arms so the hot path no longer emits logs or produces sentinel distances. Ensure
you reference and validate the same symbols used in the diff (centroids, bits,
tq_l2_adc_scaled, brute_force_tq_adc_multibit) and propagate a non-OK result
instead of continuing.

---

Outside diff comments:
In `@src/command/vector_search/mod.rs`:
- Around line 421-424: The INFO output is still reading VECTOR_SEARCH_LATENCY_US
even though search_local_filtered no longer records per-search latency; either
remove the stale metric usage or restore latency recording. Option A: delete
VECTOR_SEARCH_LATENCY_US and record_search_latency() from src/vector/metrics.rs
and remove references to that metric in the INFO handler in connection.rs (stop
exposing VECTOR_SEARCH_LATENCY_US). Option B: reintroduce latency recording by
calling record_search_latency(...) inside search_local_filtered (or any wrapper
that runs each search) so VECTOR_SEARCH_LATENCY_US remains valid; update
search_local_filtered and ensure metrics::record_search_latency is imported and
called with the measured duration. Use the symbols search_local_filtered,
record_search_latency, and VECTOR_SEARCH_LATENCY_US to locate and change the
code.

---

Duplicate comments:
In `@src/vector/turbo_quant/codebook.rs`:
- Around line 193-196: The match arm that returns 0 for unsupported bit widths
must not return a sentinel 0; change it to fail fast by replacing the "0"
fallback with a panic (or a Result::Err) that includes the unsupported bit width
so callers (e.g., CollectionMetadata::code_bytes_per_vector() and consumers in
turbo_quant::tq_adc.rs) never get a bogus length; update the code in codebook.rs
where the match occurs to call panic!("unsupported bit width for
code_bytes_per_vector: {}", bits) (or change the surrounding function to return
Result and propagate an Err with that message) so the invalid
QuantizationConfig::Sq8 path fails immediately.

In `@src/vector/turbo_quant/collection.rs`:
- Around line 289-332: The accessors codebook_boundaries_15 and codebook_16
currently log an error and return zero-filled fallbacks which masks corrupted
metadata; instead, make them fail hard when the slice -> array conversion fails
by replacing the Err(_) branches with a panic (or unreachable!) that includes
the actual len and a clear message about violated construction invariants
(remove the static ZERO fallback). Keep the existing debug_asserts but ensure
the panic is used in release builds so callers like code in hnsw::search,
segment::mutable, and persistence::segment_io observe the failure rather than
proceeding with zeros; update the doc comments to state that these functions
will panic if invariants are violated.
🪄 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: b72a43bf-84bc-4a49-b127-1ff5ad81f8c0

📥 Commits

Reviewing files that changed from the base of the PR and between 777be2b and 6054c64.

📒 Files selected for processing (4)
  • src/command/vector_search/mod.rs
  • src/vector/turbo_quant/codebook.rs
  • src/vector/turbo_quant/collection.rs
  • src/vector/turbo_quant/tq_adc.rs

Comment on lines 220 to +231
codebook: if quantization.is_turbo_quant() {
// Fail fast on invalid bit width — this is a programming invariant,
// not user input. Valid bit widths (1-4) are guaranteed by QuantizationConfig.
scaled_centroids_n(padded, quantization.bits())
.expect("codebook centroids: invalid bit width is a programming bug")
} else {
// SQ8 doesn't use codebooks -- store empty Vec
Vec::new()
},
codebook_boundaries: if quantization.is_turbo_quant() {
scaled_boundaries_n(padded, quantization.bits())
.expect("codebook boundaries: invalid bit width is a programming bug")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

Replace .expect() with an explicit invariant branch.

The fail-fast behavior is fine, but src/**/*.rs explicitly disallows unwrap()/expect() in library code. Use match + unreachable! (or return a typed error) so the invariant stays explicit without violating the repo rule.

♻️ Suggested adjustment
             codebook: if quantization.is_turbo_quant() {
-                scaled_centroids_n(padded, quantization.bits())
-                    .expect("codebook centroids: invalid bit width is a programming bug")
+                match scaled_centroids_n(padded, quantization.bits()) {
+                    Ok(v) => v,
+                    Err(_) => unreachable!("codebook centroids: invalid bit width is a programming bug"),
+                }
             } else {
                 // SQ8 doesn't use codebooks -- store empty Vec
                 Vec::new()
             },
             codebook_boundaries: if quantization.is_turbo_quant() {
-                scaled_boundaries_n(padded, quantization.bits())
-                    .expect("codebook boundaries: invalid bit width is a programming bug")
+                match scaled_boundaries_n(padded, quantization.bits()) {
+                    Ok(v) => v,
+                    Err(_) => unreachable!("codebook boundaries: invalid bit width is a programming bug"),
+                }
             } else {
                 Vec::new()
             },

As per coding guidelines, src/**/*.rs: No unwrap() or expect() in library code outside tests; use pattern matching or if let.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/vector/turbo_quant/collection.rs` around lines 220 - 231, The code
currently calls scaled_centroids_n(...) .expect(...) and
scaled_boundaries_n(...) .expect(...); replace each .expect() with explicit
pattern matching: call scaled_centroids_n(padded, quantization.bits()) and match
the Result — on Ok(v) return v for the codebook field, on Err(e) call
unreachable!() (or unreachable!("codebook centroids: invalid bit width: {}",
quantization.bits())) to express the invariant; do the same for
scaled_boundaries_n(...) used for codebook_boundaries, referencing
quantization.is_turbo_quant(), scaled_centroids_n, and scaled_boundaries_n to
locate the spots.

Comment on lines +320 to +333
if let Ok(c) = centroids.try_into() {
tq_l2_adc_scaled(q_rotated, code, norm, c)
} else {
// Invariant violated — return max distance rather than panic
tracing::error!(
"4-bit ADC requires exactly 16 centroids, got {}",
centroids.len()
)
});
tq_l2_adc_scaled(q_rotated, code, norm, c)
);
f32::MAX
}
}
_ => {
tracing::error!("unsupported bit width: {bits}");
f32::MAX

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Don't log-and-continue inside the per-vector ADC hot path.

brute_force_tq_adc_multibit() calls this once per candidate. A bad centroid slice or unsupported bits will emit one error per vector and still return a heap of f32::MAX sentinels, which means arbitrary top-k output plus log spam. Validate these invariants once before the scan and abort the whole search instead.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/vector/turbo_quant/tq_adc.rs` around lines 320 - 333, The per-vector
logging and f32::MAX sentinel in the tq_adc hot path must be replaced by a
one-time validation and abort: move validation of centroid slice length (e.g.,
exactly 16 centroids for 4-bit) and supported bit widths out of the inner branch
in tq_l2_adc_scaled()/tq_adc.rs and into the caller
(brute_force_tq_adc_multibit()), perform these checks once before scanning
candidates, and return an error/abort the search if invariants fail; then remove
the per-vector tracing::error + f32::MAX fallback in the match arms so the hot
path no longer emits logs or produces sentinel distances. Ensure you reference
and validate the same symbols used in the diff (centroids, bits,
tq_l2_adc_scaled, brute_force_tq_adc_multibit) and propagate a non-OK result
instead of continuing.

@pilotspacex-byte
pilotspacex-byte added this pull request to the merge queue Apr 1, 2026
Merged via the queue into main with commit be6797d Apr 1, 2026
11 checks passed
@pilotspacex-byte
pilotspacex-byte deleted the feat/vector-engine branch April 1, 2026 07:57
pilotspacex-byte added a commit that referenced this pull request Jul 11, 2026
…ilent replica drop, SELECT leak, AOF db attribution) (#282)

* feat(replication): R1 real WAIT/ACK — replica ACK sender, master ACK reader, WAIT wire

Task #19, the v0.7 R1 milestone item: WAIT/ACK was dead in production.
Three gaps closed (the fourth, per-shard ack floors, keeps the existing
Vec<AtomicU64> shape and rides the R2 multi-shard redesign):

1. Replica ACK send (replica.rs, both runtimes): the replication socket
   is split (monoio::io::Splitable / tokio into_split); a dedicated 1s
   ticker task owns the write half and sends REPLCONF ACK
   <master_repl_offset> — Redis's replicationCron cadence, which also
   serves as an idle keepalive for master-side lag detection. A
   timeout-wrapped read was rejected by design: cancelling an in-flight
   io_uring read whose CQE already completed DISCARDS those bytes —
   silent replication-stream corruption. The read/apply loop is
   unchanged, extracted as stream_commands_read_loop; the ticker stops
   via a per-connection flag when the read loop exits, and the write
   half drops with it.

2. Master ACK read (master.rs): drain_replica_inline_single_shard
   splits the hijacked PSYNC socket; a same-thread reader task
   (ack_read_loop) parses REPLCONF ACK frames and records them into the
   replica's ack_offsets/last_ack_time. fetch_max — a reordered or
   duplicate ACK can never regress the offset. Parsing is a dedicated
   drain_ack_offsets over protocol::parse: the shared replication
   drainer (drain_replicated_commands) deliberately DROPS REPLCONF as
   chatter, which is exactly the frame this loop exists to read (first
   implementation reused it and swallowed every ACK — caught by the red
   e2e staying red). Malformed bytes close the connection loudly; the
   replica then reconnects with a fresh resync.

3. WAIT wire (handler_monoio + handler_sharded dispatch): WAIT answered
   ':0' unconditionally from the synchronous generic dispatch table on
   the monoio runtime (only handler_single ever called
   wait_for_replicas). New connection-layer try_handle_wait intercept
   awaits wait_for_replicas (10ms poll, early exit). Redis parity:
   timeout 0 = block until satisfied (capped at one year — the poll
   loop is cooperative so the shard thread is never starved). The
   dispatch-table arm remains as a truthful fallback for paths without
   repl_state (no replicas can be attached there).

Red/green: new e2e wait_returns_acked_replica_count — WAIT with no
replicas returns 0 fast; WAIT 1 after a write returns 1 within the ACK
cadence; WAIT 2 with one replica times out reporting 1. RED before
(returned :0), GREEN after; the intermediate drainer-reuse bug kept it
red until the dedicated parser landed.

Verification: lib 4111 green, replication_streaming 6/6 (incl. new
test), fmt clean; clippy + tokio matrix + consistency/durability/bench
gates run before PR (see PR description).

author: Tin Dang

* fix(replication): consistency/durability defects caught by the R1 gates

Task #35. The user-mandated consistency/durability gates for the R1
WAIT/ACK work (multi-db load parity + kill-9 recovery, A/B'd against
main) exposed three PRE-EXISTING data-integrity bugs. All three
reproduced on main with the same harness; none are R1 regressions.

1. Replication silently dropped records for lagging replicas.
   wal_append_and_fanout / ReplicaLiveFanout used try_send and SKIPPED
   the record on a full channel — one pipelined burst left the replica
   with ~2k of 40k keys, master_link_status:up, offsets diverged
   forever (WAIT correctly reported 0, which is how R1 exposed it).
   Fix: fanout_send_or_kick — overflow sets a shared `kicked` flag
   (ShardMessage::RegisterReplica now carries it) and drops the shard's
   sender; the inline drain loop polls the flag (monoio::select! with a
   250ms timer — the kick cannot arrive in-band on a full channel, and
   ReplicaInfo.shard_txs holds a sender clone so channel closure cannot
   signal it either) and closes the socket. The replica reconnects and
   resyncs from the backlog — Redis's output-buffer-limit policy:
   divergence becomes a loud, self-healing resync. Channel capacity
   1024 -> 16384 records so ordinary bursts kick rarely.

2. Client SELECT commands leaked into BOTH persistence planes.
   SELECT is W-flagged for routing, so every handler AOF'd/replicated
   the literal client SELECT while bare writes carried no db context.
   Interleaved connections then corrupted db attribution downstream —
   conn A (db0) SET after conn B's SELECT 2 handshake replayed into
   db2. New metadata::is_persisted_write (is_write minus SELECT) gates
   all four handler persist legs; SELECT is connection-state only.

3. The AOF had no per-record db attribution at all.
   Kill-9 recovery of a 20k-db0 + 20k-db2 load restored 0/40000 (every
   key into db2 — the last literal client SELECT won). Fix mirrors
   Redis's aof_selected_db: AofMessage::{Append,AppendSync} carry the
   executing db; each writer task tracks the stream's current db and
   injects a `SELECT <db>` record exactly when it changes, in the same
   on-disk format (raw RESP for TopLevel, [lsn=0][len] framed for
   PerShard). Context resets per fresh incr segment (replay starts each
   segment at db0) and threads through every BGREWRITEAOF fold drain;
   drains that leave the context ambiguous set a usize::MAX force-emit
   sentinel — a redundant SELECT is harmless, a missing one is
   corruption. Zero-length fsync-barrier payloads never trigger nor
   observe a db switch. Replay engines already executed SELECT records;
   only the writer was blind.

Red/green:
- NEW tests/aof_multidb_kill9.rs (TopLevel shards=1 + PerShard
  shards=2): interleaved two-conn multi-db writes, kill -9, restart,
  exact per-db placement. RED before (db0=0, all keys in db2), GREEN
  after, on BOTH runtimes.
- NEW replica_converges_under_interleaved_multidb_load
  (replication_streaming): 2x10k interleaved pipelined SETs across
  db0/db2 with a replica attached; exact DBSIZE parity + no cross-db
  leaks after settle. RED before (5116/10001, 5115/10000), GREEN after.
- Writer-level unit test: Append db0 -> db2 -> db0 produces framed
  SELECT records at exactly the two switches.

Verification: gate script 11/11 PASS (WAIT-under-load 1, 20000/20000
parity both dbs, AOF recovery 20000/20000, post-restart reconvergence);
replication e2e matrix 17/17 (streaming 7, graph 3, hardening 5,
aof_multidb 2); crash_matrix_per_shard_aof 3/3 +
crash_matrix_per_shard_bgrewriteaof 2/2 (rewrite-fold canary); lib
4114 monoio / 3315 tokio; clippy -D warnings both matrices; fmt clean.

author: Tin Dang

* fix(replication): per-entry db attribution for MULTI/EXEC AOF entries

PR #282 review (CodeRabbit, Major): the txn executors collapsed every
body entry to ONE db at EXEC time and still persisted the literal
queued SELECT (they gated on is_write — missed by the top-level
is_persisted_write sweep). Deterministic corruption:
`MULTI; SELECT 2; SET t2; EXEC` then a plain db0 write — the literal
SELECT 2 shifts the replay/replica stream to db2, but the next db0
record is tagged 0 == the writer's tracker 0, so no corrective SELECT
is injected: every post-EXEC db0 write recovers into db2 (and applies
into the replica's db2). RED-proven by the new e2e before the fix.

Per-executor semantics (they genuinely differ):
- execute_transaction_sharded re-dispatches each body command against
  the CURRENT `selected`, so a queued SELECT really redirects the
  commands after it. aof_entries is now Vec<(usize, Bytes)> with the
  db captured per entry BEFORE dispatch; persist_txn_aof and the
  monoio EXEC replication leg (record_local_write_db) consume it per
  entry, as does the TxnExecute owner-routing arm's
  wal_append_and_fanout loop.
- execute_transaction (handler_single) holds ONE guard on the db
  selected at EXEC time — every body write physically lands there even
  if a queued SELECT mutates conn.selected_db mid-body. The caller now
  captures `txn_db` BEFORE the call and attributes all entries to it
  (the post-EXEC conn.selected_db is the wrong answer there).
- Both executors now use is_persisted_write: a queued literal SELECT
  is never persisted on any path.

Red/green: new e2e aof_txn_select_inside_multi_kill9_{toplevel,
per_shard} — MULTI body with a queued SELECT, post-EXEC db0 probe,
kill -9, restart; recovery placement must equal memory placement. RED
before (post lands in db2), GREEN after, on BOTH runtimes.

Regression: aof_multidb_kill9 4/4 both runtimes, replication_streaming
7/7, replication_hardening 5/5, crash_matrix_per_shard_aof 3/3,
crash_matrix_per_shard_bgrewriteaof 2/2, lib 4114, clippy -D warnings
both matrices, fmt clean.

Known residue (unchanged semantics, noted for R2): in-txn SELECT does
not persist to the connection after EXEC on the sharded paths
(`selected` mutations are local to the executor), and OrderedAcrossShards
merged entries still replay per-entry at db0.

author: Tin Dang

* test(aof): pin txn-SELECT e2e baseline — t2 must exist in exactly one db

PR #282 review (Minor): the memory baseline for t2 could be `$-1` in
BOTH dbs if the queued `SET t2` silently failed, letting every
recovery assertion pass vacuously. Assert exactly-one-db placement
before the kill so the test can never green on a no-op transaction.

author: Tin Dang

---------

Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
pilotspacex-byte added a commit that referenced this pull request Jul 18, 2026
…#133) (#385)

* fix(shard): multi-shard SWAPDB — durable + replica-visible before +OK (#133)

coordinate_swapdb had two defects. Defect 1 (the issue's report): no
fsync rendezvous anywhere — the local leg fired its record into the
generic wal_append_txs channel and remote legs acked right after
enqueueing, so under --appendfsync always the client saw +OK before
any shard had fsynced. Defect 2 (found during scoping, empirically
confirmed): the local leg's channel drains into WAL v3 ONLY, but for
--shards >= 2 --appendonly yes recovery wipes every shard and replays
ONLY the per-shard AOF manifest — the coordinator shard's own SWAPDB
record never reached the plane recovery reads. A kill-9 after +OK
permanently lost the LOCAL shard's half of the swap while remote
shards' halves survived: cross-shard keyspace divergence, strictly
worse than a fsync-timing gap. The local leg also never reached the
replication plane (no backlog append, no offset advance, no live
fan-out), unlike remote legs.

Fix:

- Local leg: durable AOF append via send_append_group (the same v3-5
  group-commit primitive persist_local_leg uses) + one fsync_barrier
  under Always, BEFORE the swap — any failure aborts with no local
  mutation, mirroring the single-shard handler_single contract. The
  WAL v3 write is kept (other record types share the channel; it is a
  documented non-authority for this deployment shape). Replication:
  record_local_swapdb_repl appends to the per-shard backlog and
  advances the offset synchronously, then defers the live-replica
  send via ShardMessage::ReplicaLiveFanout — the same
  backlog-then-defer contract wal_append_and_fanout and
  record_local_write_db use — with a SELECT 0 prefix (R2 multi-shard
  framing; SWAPDB has no single-db context, task #35 precedent).

- Remote legs: the SwapDb SPSC arm already enqueues to that shard's
  AOF writer strictly before acking; the coordinator now issues one
  fsync_barrier(target) after observing each ack (H1-BARRIER
  happens-before ordering: barrier queues behind the append in the
  same FIFO channel, so an acked barrier proves the record durable).
  Barriers no-op under everysec/no — no cost off the Always policy.

- Failure semantics: local durability failure aborts before any
  mutation; a remote barrier failure lands AFTER that shard's swap
  (SWAPDB has no rollback) and is reported truthfully as
  durability-unconfirmed instead of a false +OK. All remote legs are
  drained even after one leg errors (coordinate_mset pattern).

Red/green: crash_133_swapdb_multishard_durability_after_sigkill
(2-shard, appendfsync always, distinct db0/db1 content on both shards
via hash tags, SWAPDB 0 1, +OK, zero-delay SIGKILL, restart) — RED on
unfixed code with the local shard's entire swap half lost (db0 keys
kept db0 values), GREEN 8/8 with the fix. Unit tests prove
exactly-once backlog append + offset advance with SELECT-0 framing,
no-op without repl_state, and inactive-fanout gating.

Fixes #133
author: Tin Dang <tindang.ht97@gmail.com>

* fix(shard): SWAPDB local leg durably persists BEFORE remote dispatch; drop repl-plane recording (#133 review round)

Adversarial review of the previous commit refuted two parts of it while
confirming the core durability fix sound:

- Vector 2 (new hazard, confirmed): record_local_swapdb_repl committed
  backlog + offset + a queued live fan-out BEFORE the AOF durability
  gate; a send_append_group/fsync_barrier failure then aborted the
  local swap but could not retract the already-queued replication
  record -> the replica applies a swap the master never performed.
- Vector 5 (pre-existing, falsifies the "replica-visible" claim):
  replicas cannot execute streamed SWAPDB at all -
  replication::apply::apply_local falls through to cmd_dispatch, which
  hard-errors, and the record silently no-ops with no resync. The new
  plumbing faithfully delivered bytes the replica cannot apply.
- Vector 3 (pre-existing, widened): remote SwapDb messages were
  dispatched before the local durability gate, so every local abort
  point left N-1 shards swapped and the coordinator shard not.

This commit:

- Removes swapdb_repl_active / record_local_swapdb_repl and their unit
  tests. The local leg no longer touches the replication plane; wiring
  replica-side SWAPDB application properly (including the remote legs'
  pre-existing no-op fan-out) is issue #386, with the ordering
  constraints from the review recorded there.
- Reorders coordinate_swapdb: the local durable-append + fsync barrier
  + local swap now run BEFORE any remote dispatch. Every abort point
  (WAL backpressure, AOF enqueue failure, fsync failure) fires while
  the whole cluster is still unmutated - a local abort now leaves all
  shards consistent instead of N-1 swapped.

The confirmed-sound parts are unchanged: durable AOF append on the
recovery-authority plane for the local leg (defect 2), post-ack
fsync_barrier per remote shard (defect 1, H1-BARRIER ordering),
truthful durability-unconfirmed error on post-swap remote barrier
failure, lsn=0 PerShard sentinel parity with the remote legs
(review Vector 1), no double-replay across WAL v3 + AOF (Vector 4).

Refs #133 #386
author: Tin Dang <tindang.ht97@gmail.com>

---------

Co-authored-by: Tin Dang <tindang.ht97@gmail.com>
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.

2 participants