refactor(obikselect): implement Algorithm trait for selection

Shifts the selection logic from a direct method into a dedicated `Select` struct implementing the `Algorithm` trait. Introduces a two-phase initialization pattern with explicit builder methods and closure-based progress callbacks. Updates the crate's dependency graph to include `obikalgorithm`, refactors layer processing with unified matrix dispatch, and implements dual directory routing for partition execution.
This commit is contained in:
Eric Coissac
2026-08-26 09:17:48 +02:00
parent cbf6893f38
commit 5dc16b0127
5 changed files with 241 additions and 371 deletions
+1
View File
@@ -1746,6 +1746,7 @@ name = "obikselect"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"obicompactvec", "obicompactvec",
"obikalgorithm",
"obikindex", "obikindex",
"obisys", "obisys",
"tracing", "tracing",
+1
View File
@@ -5,6 +5,7 @@ edition = "2024"
[dependencies] [dependencies]
obikindex = { path = "../obikindex" } obikindex = { path = "../obikindex" }
obikalgorithm = { path = "../obikalgorithm" }
obicompactvec = { path = "../obicompactvec" } obicompactvec = { path = "../obicompactvec" }
obisys = { path = "../obisys" } obisys = { path = "../obisys" }
tracing = "0.1.44" tracing = "0.1.44"
+7 -4
View File
@@ -1,11 +1,14 @@
//! Genome-column selection/aggregation on an `obikindex::KmerIndex`: //! Genome-column selection/aggregation on an `obikindex::KmerIndex`:
//! projecting/aggregating genome columns (`AggOp`) into new output columns //! projecting/aggregating genome columns (`AggOp`) into new output columns
//! (`OutputCol`), either into a new index (`select`) or in place //! (`OutputCol`) of a brand new index. Orthogonal to per-kmer filtering
//! (`select_in_place`). Orthogonal to per-kmer filtering (`obikfilter`), //! (`obikfilter`), which operates on rows, not columns. Kept as a separate
//! which operates on rows, not columns. Kept as a separate crate — not a //! crate — not a module of `obikindex` — so the dependency runs one way
//! module of `obikindex` — so the dependency runs one way only. //! only. Structured on the model of `obikindexer::algorithms`/
//! `obikmerge`: [`Select`] implements `obikalgorithm::Algorithm` (two-phase
//! `new` + setters, then `run`).
mod select; mod select;
mod select_layer; mod select_layer;
pub use select::Select;
pub use select_layer::{AggOp, OutputCol}; pub use select_layer::{AggOp, OutputCol};
+126 -132
View File
@@ -1,154 +1,148 @@
use std::path::Path; //! Projecting/aggregating the genome columns of a source
use std::sync::Arc; //! `obikindex::KmerIndex` into a brand new one, according to a list of
//! [`crate::OutputCol`] specs. Structured exactly like `obikmerge::Merge`:
//! two-phase construction (`new` + setters), then
//! `obikalgorithm::Algorithm::run`, via `obikindex::IndexBuilder`
//! (`create_skeleton` + `finalize_indexed`) — no in-place variant, same as
//! `Merge`.
use obikindex::IndexBuilder; use std::io;
use crate::OutputCol; use std::path::PathBuf;
use obisys::{Reporter, Stage, progress_bar, PartitionRunner};
use obikalgorithm::Algorithm;
use obikindex::{GenomeInfo, IndexBuilder, IndexState, KmerIndex};
use obisys::{PartitionRunner, Progress, Reporter, Stage};
use tracing::info; use tracing::info;
use obikindex::{OKIError, OKIResult}; use crate::select_layer::select_partition;
use obikindex::KmerIndex; use crate::OutputCol;
use obikindex::{GenomeInfo, IndexMeta};
use obikindex::IndexState;
impl KmerIndex { pub struct Select<'a> {
/// Create a new index at `output` by projecting/aggregating the genome columns src: &'a KmerIndex,
/// of `src` according to `specs`. output: PathBuf,
/// specs: &'a [OutputCol],
/// `output_presence` — if true, output uses bit matrices (0/1), regardless of
/// whether the source stores counts. The caller is responsible for ensuring all
/// specs use logical operators when `output_presence=true` on a count source.
pub fn select<P: AsRef<Path>>(
output: P,
src: &KmerIndex,
specs: &[OutputCol],
threshold: u32, threshold: u32,
output_presence: bool, output_presence: bool,
force: bool, force: bool,
rep: &mut Reporter, sparse: bool,
) -> OKIResult<Self> { reporter: Reporter,
let output = output.as_ref(); on_progress: Option<Box<dyn FnMut(Progress) + Send + 'a>>,
}
if src.state()? != IndexState::Indexed { impl<'a> Select<'a> {
return Err(OKIError::NotIndexed(src.root_path.clone())); pub fn new(
} src: &'a KmerIndex,
output: impl Into<PathBuf>,
KmerIndex::clear_output_for_create(output, force)?; specs: &'a [OutputCol],
let mut config = src.meta.config.clone();
config.with_counts = !output_presence;
let genomes: Vec<GenomeInfo> = specs
.iter()
.map(|s| GenomeInfo::new(s.label.clone()))
.collect();
let n_src_genomes = src.meta.genomes().map_err(OKIError::Io)?.len();
let n_partitions = src.n_partitions();
let dst_partition = KmerIndex::create_skeleton(output, config, genomes)?;
info!(
"select: {} partition(s), {} source genome(s) → {} output column(s)",
n_partitions,
n_src_genomes,
specs.len(),
);
let t = Stage::start("select");
let pb = progress_bar("select", n_partitions as u64, "partitions");
let order: Vec<usize> = (0..n_partitions).collect();
let runner = PartitionRunner::new();
runner
.run(
&order,
|i| {
dst_partition.select_partition(
src,
i,
specs,
n_src_genomes,
threshold,
output_presence,
false,
)
},
|_, _, _| {
pb.inc(1);
},
)
.map_err(OKIError::Partition)?;
pb.finish_and_clear();
rep.push(t.stop());
KmerIndex::finalize_indexed(output, rep, false)
}
/// Rewrite the genome columns of this index in-place according to `specs`.
///
/// The MPHF and unitig files are unchanged; only data matrices are rewritten.
pub fn select_in_place(
&mut self,
specs: &[OutputCol],
threshold: u32,
output_presence: bool, output_presence: bool,
rep: &mut Reporter, ) -> Self {
) -> OKIResult<()> { Self {
if self.state()? != IndexState::Indexed { src,
return Err(OKIError::NotIndexed(self.root_path.clone())); output: output.into(),
specs,
threshold: 0,
output_presence,
force: false,
sparse: true,
reporter: Reporter::new(),
on_progress: None,
}
} }
let n_src_genomes = self.meta.genomes().map_err(OKIError::Io)?.len(); /// Minimum count to consider a genome as "carrying" a k-mer, for
let n_partitions = self.n_partitions(); /// logical (`Any`/`All`/`None`) aggregation ops (default: `0`).
pub fn threshold(mut self, v: u32) -> Self {
self.threshold = v;
self
}
info!( /// Remove a pre-existing index at `output` before creating the
"select (in-place): {} partition(s), {} source genome(s) → {} output column(s)", /// selected one (default: `false`, fails if one already exists).
n_partitions, pub fn force(mut self, v: bool) -> Self {
n_src_genomes, self.force = v;
specs.len(), self
); }
let t = Stage::start("select"); /// Pack the output's presence matrices in the compact sparse format
let pb = progress_bar("select", n_partitions as u64, "partitions"); /// rather than dense (default: `true`).
pub fn sparse(mut self, v: bool) -> Self {
self.sparse = v;
self
}
let order: Vec<usize> = (0..n_partitions).collect(); /// Progress callback, called once per completed partition.
let runner = PartitionRunner::new(); pub fn on_progress(mut self, cb: impl FnMut(Progress) + Send + 'a) -> Self {
runner self.on_progress = Some(Box::new(cb));
.run( self
&order, }
|i| {
self.select_partition(
self,
i,
specs,
n_src_genomes,
threshold,
output_presence,
true,
)
},
|_, _, _| {
pb.inc(1);
},
)
.map_err(OKIError::Partition)?;
pb.finish_and_clear(); /// Per-stage timing report accumulated during `run` — call after `run` returns.
rep.push(t.stop()); pub fn reporter(&self) -> &Reporter {
&self.reporter
}
}
let mut config = self.meta.config.clone(); impl Algorithm for Select<'_> {
config.with_counts = !output_presence; type Output = KmerIndex;
let genomes: Vec<GenomeInfo> = specs
fn run(&mut self) -> obikalgorithm::Result<KmerIndex> {
let src = self.src;
let output = self.output.clone();
if src.state()? != IndexState::Indexed {
return Err(format!("{}: source index is not fully built", src.dir().display()).into());
}
let n_src_genomes = src.meta().genomes()?.len();
info!(
"select: {} partition(s), {} source genome(s) → {} output column(s), output={}",
src.n_partitions(),
n_src_genomes,
self.specs.len(),
if self.output_presence { "presence" } else { "count" },
);
KmerIndex::clear_output_for_create(&output, self.force)?;
let mut config = src.meta().config.clone();
config.with_counts = !self.output_presence;
let genomes: Vec<GenomeInfo> = self
.specs
.iter() .iter()
.map(|s| GenomeInfo::new(s.label.clone())) .map(|s| GenomeInfo::new(s.label.clone()))
.collect(); .collect();
self.meta.rewrite_config(config, genomes).map_err(OKIError::Io)?;
self.meta = Arc::new(IndexMeta::open(self).map_err(OKIError::Io)?); let t = Stage::start("select");
let dst = KmerIndex::create_skeleton(&output, config, genomes)?;
let n_partitions = src.n_partitions();
let order: Vec<usize> = (0..n_partitions).collect();
let runner = PartitionRunner::new();
let on_progress = &mut self.on_progress;
let mut done: u64 = 0;
runner
.run(
&order,
|i| select_partition(&dst, src, i, self.specs, self.threshold, self.output_presence),
|_, _, _| {
done += 1;
if let Some(cb) = on_progress.as_mut() {
cb(Progress {
position: done,
total: Some(n_partitions as u64),
});
}
},
)
.map_err(|e| io::Error::other(e.to_string()))?;
self.reporter.push(t.stop());
let t = Stage::start("finalize");
let dst = KmerIndex::finalize_indexed(&output, &mut self.reporter, self.sparse)?;
self.reporter.push(t.stop());
let t_pack = Stage::start("pack"); Ok(dst)
self.pack_matrices(false)?;
rep.push(t_pack.stop());
Ok(())
} }
} }
+104 -233
View File
@@ -1,14 +1,21 @@
//! Per-layer column projection/aggregation — the mechanics
//! [`crate::select::Select`] runs once per partition-layer. Generic over
//! source content (`Count`/`Presence`) via `obicompactvec::MatrixGroupOps`
//! (object-safe: one `&dyn MatrixGroupOps` covers both matrix kinds, so
//! this never branches on source content beyond the single `match` that
//! opens it) and over destination content via [`DstBuilder`] — no
//! duplicated per-op-per-content code path.
use std::fs; use std::fs;
use std::io; use std::io;
use std::path::{Path, PathBuf}; use std::path::Path;
use obicompactvec::{ use obicompactvec::{
ColGroup, MatrixGroupOps, PersistentBitMatrix, PersistentBitMatrixBuilder, ColGroup, MatrixGroupOps, PersistentBitMatrix, PersistentBitMatrixBuilder,
PersistentCompactIntMatrix, PersistentCompactIntMatrixBuilder, PersistentCompactIntMatrix, PersistentCompactIntMatrixBuilder, TempBitVec, TempCompactIntVec,
}; };
use obikindex::{OKIError, OKIResult}; use obikindex::layer::{KmerLayer, LayerContent};
use obikindex::{KmerIndex, OKIError, OKIResult};
use obikindex::KmerIndex;
// ── AggOp ───────────────────────────────────────────────────────────────────── // ── AggOp ─────────────────────────────────────────────────────────────────────
@@ -36,9 +43,67 @@ pub struct OutputCol {
pub op: AggOp, pub op: AggOp,
} }
// ── Group aggregation, generic over source content ───────────────────────────
/// One output column's aggregated values, still content-typed — the shape
/// `AggOp` naturally produces (`Any`/`All`/`None` → bits, `Sum`/`Min`/`Max`
/// → ints), independent of whether the *source* was itself count or
/// presence data.
enum AggResult {
Bit(TempBitVec),
Int(TempCompactIntVec),
}
fn compute_group(mat: &dyn MatrixGroupOps, spec: &OutputCol, threshold: u32) -> io::Result<AggResult> {
let g = ColGroup::new(spec.label.clone(), spec.indices.clone());
Ok(match spec.op {
AggOp::Any => AggResult::Bit(mat.partial_group_any(&g, threshold)?),
AggOp::All => AggResult::Bit(mat.partial_group_all(&g, threshold)?),
AggOp::None => AggResult::Bit(mat.partial_group_none(&g, threshold)?),
AggOp::Sum => AggResult::Int(mat.partial_group_sum(&g)?),
AggOp::Min => AggResult::Int(mat.partial_group_min(&g)?),
AggOp::Max => AggResult::Int(mat.partial_group_max(&g)?),
})
}
// ── DstBuilder — destination matrix, generic over output content ─────────────
enum DstBuilder {
Bit(PersistentBitMatrixBuilder),
Int(PersistentCompactIntMatrixBuilder),
}
impl DstBuilder {
fn new(output_presence: bool, n: usize, dir: &Path) -> io::Result<Self> {
Ok(if output_presence {
DstBuilder::Bit(PersistentBitMatrixBuilder::new(n, dir)?)
} else {
DstBuilder::Int(PersistentCompactIntMatrixBuilder::new(n, dir)?)
})
}
fn add(&mut self, r: AggResult) -> io::Result<()> {
match (self, r) {
(DstBuilder::Bit(b), AggResult::Bit(v)) => b.add_col_from(&v),
(DstBuilder::Bit(b), AggResult::Int(v)) => b.add_col_from_int(&v),
(DstBuilder::Int(b), AggResult::Bit(v)) => b.add_col_from_bit(&v),
(DstBuilder::Int(b), AggResult::Int(v)) => b.add_col_from(&v),
}
}
fn close(self) -> io::Result<()> {
match self {
DstBuilder::Bit(b) => b.close(),
DstBuilder::Int(b) => b.close(),
}
}
}
// ── Helpers ─────────────────────────────────────────────────────────────────── // ── Helpers ───────────────────────────────────────────────────────────────────
/// Copy all plain files (not subdirectories) from `src_dir` to `dst_dir`. /// Copy all plain files (not subdirectories) from `src_dir` to `dst_dir`
/// carries a layer's kmer identity (mphf/unitigs/evidence/fingerprint)
/// across unchanged; only the data matrix (a subdirectory) is rebuilt.
fn copy_layer_files(src_dir: &Path, dst_dir: &Path) -> io::Result<()> { fn copy_layer_files(src_dir: &Path, dst_dir: &Path) -> io::Result<()> {
for entry in fs::read_dir(src_dir)? { for entry in fs::read_dir(src_dir)? {
let entry = entry?; let entry = entry?;
@@ -50,248 +115,54 @@ fn copy_layer_files(src_dir: &Path, dst_dir: &Path) -> io::Result<()> {
Ok(()) Ok(())
} }
// ── fill_builders ───────────────────────────────────────────────────────────── // ── select_partition ──────────────────────────────────────────────────────────
fn fill_builders( /// Rewrite partition `i`'s data matrices from `src` into `dst` according to
specs: &[OutputCol], /// `specs`. Every layer's kmer identity is copied as-is; only the genome
src_layer_dir: &Path, /// columns are recomputed.
src_is_count: bool, pub(crate) fn select_partition(
threshold: u32, dst: &KmerIndex,
output_presence: bool,
mut dst_bit: Option<&mut PersistentBitMatrixBuilder>,
mut dst_int: Option<&mut PersistentCompactIntMatrixBuilder>,
) -> OKIResult<()> {
if src_is_count {
let mat = PersistentCompactIntMatrix::open(src_layer_dir).map_err(OKIError::Io)?;
for spec in specs {
let g = ColGroup::new(&spec.label, spec.indices.clone());
if output_presence {
let b = dst_bit.as_deref_mut().unwrap();
match spec.op {
AggOp::Any => {
b.add_col_from(&mat.partial_group_any(&g, threshold).map_err(OKIError::Io)?)
}
AggOp::All => {
b.add_col_from(&mat.partial_group_all(&g, threshold).map_err(OKIError::Io)?)
}
AggOp::None => {
b.add_col_from(&mat.partial_group_none(&g, threshold).map_err(OKIError::Io)?)
}
AggOp::Sum => {
b.add_col_from_int(&mat.partial_group_sum(&g).map_err(OKIError::Io)?)
}
AggOp::Min => {
b.add_col_from_int(&mat.partial_group_min(&g).map_err(OKIError::Io)?)
}
AggOp::Max => {
b.add_col_from_int(&mat.partial_group_max(&g).map_err(OKIError::Io)?)
}
}
.map_err(OKIError::Io)?;
} else {
let b = dst_int.as_deref_mut().unwrap();
match spec.op {
AggOp::Sum => b.add_col_from(&mat.partial_group_sum(&g).map_err(OKIError::Io)?),
AggOp::Min => b.add_col_from(&mat.partial_group_min(&g).map_err(OKIError::Io)?),
AggOp::Max => b.add_col_from(&mat.partial_group_max(&g).map_err(OKIError::Io)?),
AggOp::Any => b.add_col_from_bit(
&mat.partial_group_any(&g, threshold).map_err(OKIError::Io)?,
),
AggOp::All => b.add_col_from_bit(
&mat.partial_group_all(&g, threshold).map_err(OKIError::Io)?,
),
AggOp::None => b.add_col_from_bit(
&mat.partial_group_none(&g, threshold).map_err(OKIError::Io)?,
),
}
.map_err(OKIError::Io)?;
}
}
} else {
let mat = PersistentBitMatrix::open(src_layer_dir).map_err(OKIError::Io)?;
for spec in specs {
let g = ColGroup::new(&spec.label, spec.indices.clone());
if output_presence {
let b = dst_bit.as_deref_mut().unwrap();
match spec.op {
AggOp::Any => {
b.add_col_from(&mat.partial_group_any(&g, 1).map_err(OKIError::Io)?)
}
AggOp::All => {
b.add_col_from(&mat.partial_group_all(&g, 1).map_err(OKIError::Io)?)
}
AggOp::None => {
b.add_col_from(&mat.partial_group_none(&g, 1).map_err(OKIError::Io)?)
}
AggOp::Sum => {
b.add_col_from_int(&mat.partial_group_sum(&g).map_err(OKIError::Io)?)
}
AggOp::Min => {
b.add_col_from_int(&mat.partial_group_min(&g).map_err(OKIError::Io)?)
}
AggOp::Max => {
b.add_col_from_int(&mat.partial_group_max(&g).map_err(OKIError::Io)?)
}
}
.map_err(OKIError::Io)?;
} else {
let b = dst_int.as_deref_mut().unwrap();
match spec.op {
AggOp::Sum => b.add_col_from(&mat.partial_group_sum(&g).map_err(OKIError::Io)?),
AggOp::Min => b.add_col_from(&mat.partial_group_min(&g).map_err(OKIError::Io)?),
AggOp::Max => b.add_col_from(&mat.partial_group_max(&g).map_err(OKIError::Io)?),
AggOp::Any => {
b.add_col_from_bit(&mat.partial_group_any(&g, 1).map_err(OKIError::Io)?)
}
AggOp::All => {
b.add_col_from_bit(&mat.partial_group_all(&g, 1).map_err(OKIError::Io)?)
}
AggOp::None => {
b.add_col_from_bit(&mat.partial_group_none(&g, 1).map_err(OKIError::Io)?)
}
}
.map_err(OKIError::Io)?;
}
}
}
Ok(())
}
// ── KmerPartition::select_partition ──────────────────────────────────────────
impl KmerIndex {
/// Rewrite the data matrices of partition `i` in `src` into `self`.
///
/// `specs` defines the output columns (projection/aggregation).
/// `output_presence` — if true, all output builders use bit (0/1) format.
/// `in_place` — `self` and `src` share the same root; write to temp dirs then swap.
pub fn select_partition(
&self,
src: &KmerIndex, src: &KmerIndex,
i: usize, i: usize,
specs: &[OutputCol], specs: &[OutputCol],
_n_src_genomes: usize,
threshold: u32, threshold: u32,
output_presence: bool, output_presence: bool,
in_place: bool, ) -> OKIResult<()> {
) -> OKIResult<()> { let src_partition = src.partition(i)?;
let src_index_dir = src.index_dir(i); let dst_partition = dst.partition(i)?;
if !src_index_dir.exists() { let n_layers = src_partition.n_layers();
return Ok(());
}
let n_src_layers = src.n_layers(i)?; for l in 0..n_layers {
if n_src_layers == 0 { let src_layer = src_partition.layer(l)?.open()?;
return Ok(()); let n = src_layer.n();
}
let dst_index_dir = self.index_dir(i); let dst_layer_dir = KmerLayer::new(&dst_partition, l)
if !in_place { .create()
fs::create_dir_all(&dst_index_dir)?;
}
let data_subdir = if output_presence {
"presence"
} else {
"counts"
};
for l in 0..n_src_layers {
let src_layer_dir = src.layer_dir(i, l);
if !src_layer_dir.exists() {
continue;
}
let dst_layer_dir = self.layer_dir(i, l);
let counts_dir = src_layer_dir.join("counts");
let presence_dir = src_layer_dir.join("presence");
let src_is_count = counts_dir.exists() && !presence_dir.exists();
// Determine number of slots and detect implicit layers.
let n = if counts_dir.exists() {
PersistentCompactIntMatrix::open(&src_layer_dir)
.map_err(OKIError::Io)? .map_err(OKIError::Io)?
.n() .dir()
} else if presence_dir.exists() { .to_path_buf();
PersistentBitMatrix::open(&src_layer_dir) copy_layer_files(src_layer.dir(), &dst_layer_dir).map_err(OKIError::Io)?;
.map_err(OKIError::Io)?
.n() let group_mat: Box<dyn MatrixGroupOps> = match src_layer.content() {
} else { LayerContent::Count => {
// Implicit single-genome layer: no data matrix needed in output either. Box::new(PersistentCompactIntMatrix::open(src_layer.dir()).map_err(OKIError::Io)?)
if !in_place { }
fs::create_dir_all(&dst_layer_dir)?; LayerContent::Presence => {
copy_layer_files(&src_layer_dir, &dst_layer_dir)?; Box::new(PersistentBitMatrix::open(src_layer.dir()).map_err(OKIError::Io)?)
} }
continue;
}; };
// Choose the output data directory (temp name for in-place). let data_subdir = if output_presence { "presence" } else { "counts" };
let (dst_data_dir, final_data_dir): (PathBuf, PathBuf) = if in_place { let data_dir = dst_layer_dir.join(data_subdir);
let tmp = dst_layer_dir.join(format!("{data_subdir}_new")); fs::create_dir_all(&data_dir).map_err(OKIError::Io)?;
let perm = dst_layer_dir.join(data_subdir);
(tmp, perm)
} else {
let perm = dst_layer_dir.join(data_subdir);
(perm.clone(), perm)
};
if !in_place { let mut builder = DstBuilder::new(output_presence, n, &data_dir).map_err(OKIError::Io)?;
fs::create_dir_all(&dst_layer_dir)?; for spec in specs {
copy_layer_files(&src_layer_dir, &dst_layer_dir)?; let r = compute_group(group_mat.as_ref(), spec, threshold).map_err(OKIError::Io)?;
builder.add(r).map_err(OKIError::Io)?;
} }
fs::create_dir_all(&dst_data_dir)?; builder.close().map_err(OKIError::Io)?;
let (mut dst_bit, mut dst_int) = if output_presence {
(
Some(PersistentBitMatrixBuilder::new(n, &dst_data_dir).map_err(OKIError::Io)?),
None,
)
} else {
(
None,
Some(
PersistentCompactIntMatrixBuilder::new(n, &dst_data_dir)
.map_err(OKIError::Io)?,
),
)
};
fill_builders(
specs,
&src_layer_dir,
src_is_count,
threshold,
output_presence,
dst_bit.as_mut(),
dst_int.as_mut(),
)?;
if output_presence {
dst_bit.unwrap().close().map_err(OKIError::Io)?;
} else {
dst_int.unwrap().close().map_err(OKIError::Io)?;
}
// In-place: swap old data dir for new.
if in_place {
let old_data_dir = if src_is_count {
dst_layer_dir.join("counts")
} else {
dst_layer_dir.join("presence")
};
if old_data_dir.exists() {
fs::remove_dir_all(&old_data_dir)?;
}
fs::rename(&dst_data_dir, &final_data_dir)?;
}
}
if !in_place {
src.partition_meta(i)?
.save(&dst_index_dir)?;
} }
Ok(()) Ok(())
}
} }