Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions benchmarks/arxiv-precomputed/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@

use clap::Parser;
use ordinaldb::manifest::{CreateManifestOptions, VerifyOptions};
use ordinaldb::{DenseLoadOptions, DenseSearchOptions, OrdinalIndex};
use ordinaldb::{DenseLoadOptions, DenseSearchOptions, OrdinalIndex, SignLoadPolicy};
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Write};
use std::path::{Path, PathBuf};
Expand Down Expand Up @@ -322,7 +322,7 @@ fn main() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(dim),
expected_bits: Some(BITS),
},
Expand Down
8 changes: 5 additions & 3 deletions cookbook/supportsearch/src/index.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use std::path::{Path, PathBuf};
use ordinaldb::artifacts::{MANIFEST_FILE, SPARSE_BM25_AUX_NAME};
use ordinaldb::hybrid::{Bm25MmapIndex, SparseIndexBuilder, TokenizerKind};
use ordinaldb::manifest::{AuxiliaryArtifactDeclaration, CreateManifestOptions, VerifyOptions};
use ordinaldb::{BuildOptions, DenseLoadOptions, IdMapIndex, OrdinalIndexBuilder, SignPolicy};
use ordinaldb::{
BuildOptions, DenseLoadOptions, IdMapIndex, OrdinalIndexBuilder, SignLoadPolicy, SignPolicy,
};

use crate::corpus::{corpus, Doc};
use crate::embedder::{Embedder, DIM};
Expand Down Expand Up @@ -87,7 +89,7 @@ pub fn build_and_persist(

// Dense (OrdVec) side.
// DIM=384 is sign-capable; Required fails fast instead of silently
// writing a bundle the require_sign load below would reject.
// writing a bundle the Require-policy load below would reject.
let mut dense_builder = OrdinalIndexBuilder::new(
DIM,
BITS,
Expand Down Expand Up @@ -121,7 +123,7 @@ pub fn build_and_persist(
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(DIM),
expected_bits: Some(BITS),
},
Expand Down
8 changes: 5 additions & 3 deletions examples/downstream-smoke/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use ordinaldb::hybrid::{
rrf_fuse_batch, Bm25MmapIndex, RrfConfig, SparseIndexBuilder, TokenizerKind,
};
use ordinaldb::manifest::{AuxiliaryArtifactDeclaration, CreateManifestOptions, VerifyOptions};
use ordinaldb::{BuildOptions, DenseLoadOptions, IdMapIndex, OrdinalIndexBuilder, SignPolicy};
use ordinaldb::{
BuildOptions, DenseLoadOptions, IdMapIndex, OrdinalIndexBuilder, SignLoadPolicy, SignPolicy,
};
use std::fs;
use std::path::{Path, PathBuf};

Expand Down Expand Up @@ -37,7 +39,7 @@ fn main() {
.expect("write sparse mmap");

// dim=64 is sign-capable; Required fails fast instead of silently
// writing a bundle the require_sign load below would reject.
// writing a bundle the Require-policy load below would reject.
let mut dense_builder = OrdinalIndexBuilder::new(
dim,
2,
Expand Down Expand Up @@ -70,7 +72,7 @@ fn main() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(dim),
expected_bits: Some(2),
},
Expand Down
17 changes: 13 additions & 4 deletions ordinaldb/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -251,10 +251,9 @@ pub enum DenseError {
/// constructed; a lower-level error than
/// [`Self::ManifestVerification`].
Manifest(ordvec_manifest::ManifestError),
/// A required auxiliary artifact declared on the manifest (for example
/// a sign sidecar requested via
/// [`crate::ordinal::DenseLoadOptions::require_sign`]) was missing or
/// invalid.
/// An auxiliary artifact declared on the manifest and required by the
/// load (for example a sign sidecar the [`crate::SignLoadPolicy`]
/// load policy requires) was declared but not loadable.
Auxiliary(ordvec_manifest::RequireAuxiliaryError),
/// The flat `queries` buffer length is not a multiple of `dim` (or
/// `dim` is `0`, which only occurs when searching a lazily-constructed,
Expand Down Expand Up @@ -283,7 +282,13 @@ pub enum DenseError {
/// [`crate::SignPolicy`] build policy allows one).
/// `DenseSearchMode::Auto` never produces this error — it silently
/// falls back to an exact scan instead.
///
/// Also returned by a verified load whose [`crate::SignLoadPolicy`]
/// requires a sign sidecar the bundle does not declare.
MissingSignSidecar,
/// The load policy is [`crate::SignLoadPolicy::Forbid`] but the
/// verified bundle declares a sign sidecar.
SignSidecarForbidden,
/// An internal consistency check failed with a human-readable
/// `message` — for example, a verified bundle's manifest metadata
/// (dim/bits/row count) disagreeing with the artifact actually loaded
Expand Down Expand Up @@ -348,6 +353,9 @@ impl fmt::Display for DenseError {
"invalid query value at query {query_index}, coord {coord_index}: {value}"
),
Self::MissingSignSidecar => f.write_str("verified dense load requires a sign sidecar"),
Self::SignSidecarForbidden => f.write_str(
"verified dense load forbids a sign sidecar, but the bundle declares one",
),
Comment on lines 353 to +358

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

1. Misleading sign-sidecar error 🐞 Bug ◔ Observability

DenseError::MissingSignSidecar is emitted by both verified-load and SignTwoStage search paths, but
its Display text claims it is a verified-load failure, which misleads callers when the error
originates from search APIs.
Agent Prompt
## Issue description
`DenseError::MissingSignSidecar` is used in multiple contexts (verified load policy and SignTwoStage search), but its `Display` message is load-specific ("verified dense load requires a sign sidecar"). This makes error output misleading in search contexts and in any downstream code that logs `{err}`.

## Issue Context
- The enum variant is explicitly documented as used by search (`DenseSearchMode::SignTwoStage`) and now also by verified load.
- Tests print the error via `{err}` in a search context, so the message is user-visible.

## Fix Focus Areas
- ordinaldb/src/error.rs[332-370]

## Suggested fix
- Change the `Display` string for `DenseError::MissingSignSidecar` to be context-agnostic, e.g. "missing required sign sidecar" or "operation requires a sign sidecar".
- (Optional) If you want richer diagnostics, consider splitting into two variants (search vs load) or carrying context, but the minimal fix is to generalize the message.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Self::MetadataMismatch(message) => f.write_str(message),
Self::RowIdentity(message) => f.write_str(message),
Self::AllowlistRowIdMissing(id) => {
Expand All @@ -374,6 +382,7 @@ impl Error for DenseError {
Self::InvalidQueryDim { .. }
| Self::InvalidQueryValue { .. }
| Self::MissingSignSidecar
| Self::SignSidecarForbidden
| Self::MetadataMismatch(_)
| Self::RowIdentity(_)
| Self::AllowlistRowIdMissing(_)
Expand Down
7 changes: 5 additions & 2 deletions ordinaldb/src/id_map.rs
Original file line number Diff line number Diff line change
Expand Up @@ -700,13 +700,16 @@ impl IdMapIndex {

/// Load a bundle directory previously written by [`Self::write`] or
/// [`Self::write_verified_bundle`], with default manifest
/// verification.
/// verification and the default sign-sidecar load policy
/// ([`crate::SignLoadPolicy::RequireIfSupported`]).
///
/// # Errors
/// Returns an `InvalidData` [`std::io::Error`] if the bundle is
/// missing its ID sidecar (it was written by a bare [`OrdinalIndex`] —
/// load it with [`OrdinalIndex::load`] instead), fails manifest
/// verification, or is otherwise malformed.
/// verification, is a sign-capable bundle missing its sign sidecar
/// (use [`Self::open_verified`] with [`crate::SignLoadPolicy::Any`] to
/// load it without one), or is otherwise malformed.
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let artifacts = crate::io::load_id_map_bundle(path)?;
let inner = OrdinalIndex::from_loaded_parts(artifacts.rankquant, artifacts.sign)?;
Expand Down
11 changes: 11 additions & 0 deletions ordinaldb/src/io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -611,8 +611,19 @@ fn load_bundle(path: &Path) -> io::Result<LoadedBundle> {
));
}

// This backs the convenience `load()` entry points, which take no
// `DenseLoadOptions`; they inherit the same default sign policy as
// `open_verified` (`SignLoadPolicy::RequireIfSupported`).
// TODO(PR2): type this error surface as `DenseError` instead of an
// `InvalidData` `io::Error`.
let sign = match auxiliary_path(&plan, SIGN_AUX_NAME)? {
Some(path) => Some(SignBitmap::load(path)?),
None if crate::ordinal::sign_compatible(metadata.dim, metadata_bits) => {
return Err(invalid_data(
"sign-capable bundle (bits=2, dim divisible by 64) has no sign sidecar; \
open it with open_verified and SignLoadPolicy::Any to load without one",
));
}
Comment on lines +614 to +626

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

2. Stringly typed load error 🐞 Bug ⚙ Maintainability

The convenience load() path enforces the new default sign-sidecar policy but reports a missing
sign sidecar as a generic io::Error with a formatted string, forcing downstream callers to
string-match to distinguish it from other InvalidData failures.
Agent Prompt
## Issue description
`OrdinalIndex::load` / `IdMapIndex::load` fail a sign-capable bundle missing its sign sidecar, but the error is currently returned as `io::ErrorKind::InvalidData` with a string message. This prevents reliable programmatic handling (without brittle substring checks) and creates inconsistency with `open_verified`, which returns a typed `DenseError::MissingSignSidecar`.

## Issue Context
- This new failure mode is part of the PR’s safe-by-default behavior, so it is more likely to be encountered by callers using the convenience APIs.
- The repository already has a TODO noting the error surface should be typed.

## Fix Focus Areas
- ordinaldb/src/io.rs[577-636]
- ordinaldb/tests/sign_load_policy.rs[185-211]

## Suggested fix
- Instead of `invalid_data("...")`, construct an `io::Error` that carries a typed inner error, e.g.:
  - `return Err(std::io::Error::new(std::io::ErrorKind::InvalidData, crate::DenseError::MissingSignSidecar));`
  This keeps the public return type as `io::Result` while allowing callers to downcast via `err.get_ref().and_then(|e| e.downcast_ref::<DenseError>())`.
- Update the test to assert on the downcasted `DenseError::MissingSignSidecar` rather than substring-matching.
- If you prefer a dedicated typed error for convenience loads, introduce a small `LoadError` enum and wrap that instead.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

None => None,
};
let ids_path = auxiliary_path(&plan, IDS_AUX_NAME)?;
Expand Down
11 changes: 8 additions & 3 deletions ordinaldb/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,14 +53,19 @@
//! a bundle back:
//!
//! - `OrdinalIndex::load` / `IdMapIndex::load` open a bundle *directory*
//! with default verification.
//! with default verification and default load options.
//! - `OrdinalIndex::open_verified` / `IdMapIndex::open_verified` open a
//! bundle from an explicit `manifest.json` path with caller-controlled
//! [`manifest::VerifyOptions`] (size limits, path-escape policy) and
//! [`ordinal::DenseLoadOptions`] (require a sign sidecar, assert an
//! [`ordinal::DenseLoadOptions`] (sign-sidecar load policy, assert an
//! expected `dim`/`bits`), reporting mismatches as a [`DenseError`]
//! instead of a generic I/O error.
//!
//! Both paths default to [`SignLoadPolicy::RequireIfSupported`]: a bundle
//! whose `(dim, bits)` can carry a sign sidecar must have one to load.
//! Pass [`SignLoadPolicy::Any`] to `open_verified` to load such a bundle
//! without its sidecar.
//!
//! Loading an `OrdinalIndex` bundle that actually carries an ID sidecar
//! (or vice versa) is a load-time error that names the correct type to use.
//!
Expand Down Expand Up @@ -135,6 +140,6 @@ pub use ordinal::{
rankquant_compatible, rankquant_required_multiple, sign_compatible, sign_required_multiple,
BuildOptions, DenseBundleInspectReport, DenseLoadOptions, DenseSearchExecution,
DenseSearchMode, DenseSearchOptions, DenseSearchPlan, DenseSearchReport, DenseSearchTimings,
OrdinalIndex, OrdinalIndexBuilder, SearchResults, SignPolicy, TwoStageOptions,
OrdinalIndex, OrdinalIndexBuilder, SearchResults, SignLoadPolicy, SignPolicy, TwoStageOptions,
VerifiedBundleReport,
};
84 changes: 69 additions & 15 deletions ordinaldb/src/ordinal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -82,13 +82,36 @@ impl Default for BuildOptions {
}
}

/// Policy controlling whether a verified load requires, tolerates, or
/// rejects a sign sidecar on the bundle (see [`DenseLoadOptions::sign`]).
/// This is the load-side counterpart of the build-side [`SignPolicy`]: it
/// never changes what was written, only whether the load accepts it.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SignLoadPolicy {
/// Require the sidecar whenever the bundle's `(dim, bits)` can carry
/// one (see [`sign_compatible`]), failing with
/// [`DenseError::MissingSignSidecar`] when it is absent; bundles that
/// cannot carry a sidecar load without one. This is the default.
RequireIfSupported,
/// Fail the load with [`DenseError::MissingSignSidecar`] unless the
/// bundle declares a sign sidecar.
Require,
/// Load the sidecar when the bundle declares one and proceed without
/// it otherwise; never fail over sidecar presence.
Any,
/// Fail the load with [`DenseError::SignSidecarForbidden`] when the
/// bundle declares a sign sidecar.
Forbid,
}

/// Options controlling how a verified bundle is loaded (see
/// [`OrdinalIndex::open_verified`] / [`crate::IdMapIndex::open_verified`]).
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct DenseLoadOptions {
/// If `true`, fail the load with [`DenseError::MissingSignSidecar`]
/// when the bundle has no sign sidecar, instead of loading without one.
pub require_sign: bool,
/// Sign-sidecar load policy — see [`SignLoadPolicy`]. Defaults to
/// [`SignLoadPolicy::RequireIfSupported`]: a bundle whose `(dim, bits)`
/// can carry a sidecar must have one to load.
pub sign: SignLoadPolicy,
/// If `Some`, fail the load with [`DenseError::MetadataMismatch`] when
/// the bundle's manifest dim does not match.
pub expected_dim: Option<usize>,
Expand All @@ -97,6 +120,16 @@ pub struct DenseLoadOptions {
pub expected_bits: Option<u8>,
}

impl Default for DenseLoadOptions {
fn default() -> Self {
Self {
sign: SignLoadPolicy::RequireIfSupported,
expected_dim: None,
expected_bits: None,
}
}
}

/// Which search strategy to use.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum DenseSearchMode {
Expand Down Expand Up @@ -1152,13 +1185,16 @@ impl OrdinalIndex {

/// Load a bundle directory previously written by [`Self::write`] or
/// [`Self::write_verified_bundle`], with default manifest
/// verification.
/// verification and the default sign-sidecar load policy
/// ([`SignLoadPolicy::RequireIfSupported`]).
///
/// # Errors
/// Returns an `InvalidData` [`std::io::Error`] if the bundle carries an
/// ID sidecar (it was written by [`crate::IdMapIndex`] — load it with
/// [`crate::IdMapIndex::load`] instead), fails manifest verification,
/// or is otherwise malformed.
/// is a sign-capable bundle missing its sign sidecar (use
/// [`Self::open_verified`] with [`SignLoadPolicy::Any`] to load it
/// without one), or is otherwise malformed.
pub fn load(path: impl AsRef<std::path::Path>) -> std::io::Result<Self> {
let artifacts = crate::io::load_ordinal_bundle(path)?;
Self::from_loaded_parts(artifacts.rankquant, artifacts.sign)
Expand Down Expand Up @@ -1718,16 +1754,19 @@ fn load_verified_bundle(
));
}

let sign_path = if load_options.require_sign {
match plan.require_auxiliary(crate::io::SIGN_AUX_NAME) {
Ok(path) => Some(path.to_path_buf()),
Err(crate::manifest::RequireAuxiliaryError::MissingDeclaration { .. }) => {
return Err(DenseError::MissingSignSidecar);
}
Err(error) => return Err(DenseError::Auxiliary(error)),
let sign_declared = plan.auxiliary_by_name(crate::io::SIGN_AUX_NAME).is_some();
let sign_path = match load_options.sign {
SignLoadPolicy::Forbid if sign_declared => {
return Err(DenseError::SignSidecarForbidden);
}
SignLoadPolicy::Forbid => None,
SignLoadPolicy::Require => required_sign_path(&plan)?,
SignLoadPolicy::RequireIfSupported if sign_compatible(metadata.dim, metadata_bits) => {
required_sign_path(&plan)?
}
SignLoadPolicy::RequireIfSupported | SignLoadPolicy::Any => {
crate::io::auxiliary_path(&plan, crate::io::SIGN_AUX_NAME)?
}
} else {
crate::io::auxiliary_path(&plan, crate::io::SIGN_AUX_NAME)?
};
let sign = match sign_path {
Some(path) => {
Expand All @@ -1753,6 +1792,21 @@ fn load_verified_bundle(
})
}

/// The verified path of the bundle's sign sidecar, required: a missing
/// declaration is [`DenseError::MissingSignSidecar`]; a declared but
/// unloadable sidecar is [`DenseError::Auxiliary`].
fn required_sign_path(
plan: &ordvec_manifest::VerifiedLoadPlan,
) -> Result<Option<PathBuf>, DenseError> {
match plan.require_auxiliary(crate::io::SIGN_AUX_NAME) {
Ok(path) => Ok(Some(path.to_path_buf())),
Err(crate::manifest::RequireAuxiliaryError::MissingDeclaration { .. }) => {
Err(DenseError::MissingSignSidecar)
}
Err(error) => Err(DenseError::Auxiliary(error)),
}
}

impl OrdinalIndexBuilder {
/// Construct a new builder for a `dim`/`bits`/`options`-configured
/// index.
Expand Down
16 changes: 8 additions & 8 deletions ordinaldb/tests/boundary_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use ordinaldb::hybrid::{
use ordinaldb::manifest::{AuxiliaryArtifactDeclaration, CreateManifestOptions, VerifyOptions};
use ordinaldb::{
BuildOptions, DenseError, DenseLoadOptions, IdMapIndex, OrdinalIndex, OrdinalIndexBuilder,
SignPolicy,
SignLoadPolicy, SignPolicy,
};
use std::fs;
use std::path::{Path, PathBuf};
Expand All @@ -24,7 +24,7 @@ fn verified_idmap_bundle_loads_dense_and_sparse_with_stable_row_ids() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(64),
expected_bits: Some(2),
},
Expand Down Expand Up @@ -83,7 +83,7 @@ fn verified_dense_load_can_require_sign_sidecar() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(64),
expected_bits: Some(2),
},
Expand Down Expand Up @@ -208,7 +208,7 @@ fn declared_but_unloadable_sign_sidecar_is_not_reported_missing() {
bundle.join(MANIFEST_FILE),
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(64),
expected_bits: Some(2),
},
Expand All @@ -230,7 +230,7 @@ fn batch_dense_sparse_and_rrf_preserve_per_query_allowlists() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(64),
expected_bits: Some(2),
},
Expand Down Expand Up @@ -319,7 +319,7 @@ fn hybrid_bundle_open_verified_returns_dense_and_sparse_from_one_manifest() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(64),
expected_bits: Some(2),
},
Expand Down Expand Up @@ -362,7 +362,7 @@ fn hybrid_bundle_open_verified_reports_dense_and_sparse_failures_distinctly() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(32),
expected_bits: Some(2),
},
Expand All @@ -380,7 +380,7 @@ fn hybrid_bundle_open_verified_reports_dense_and_sparse_failures_distinctly() {
&manifest_path,
VerifyOptions::default(),
DenseLoadOptions {
require_sign: true,
sign: SignLoadPolicy::Require,
expected_dim: Some(64),
expected_bits: Some(2),
},
Expand Down
Loading