fix(python): expose optimize compaction options

This commit is contained in:
Gatefixer
2026-08-06 05:01:42 +00:00
parent c7ea91f3ea
commit e0499de959
9 changed files with 184 additions and 11 deletions
+2 -1
View File
@@ -21,7 +21,7 @@ from .remote.db import RemoteDBConnection
from .expr import Expr, col, lit, func
from .schema import blob, vector, BlobType
from .job import AsyncJob, Job
from .table import AsyncTable, Table
from .table import AsyncTable, CompactionOptions, Table
from .types import BaseTokenizerType
from ._lancedb import Session
from .namespace import (
@@ -504,6 +504,7 @@ __all__ = [
"AsyncJob",
"AsyncLanceNamespaceDBConnection",
"AsyncTable",
"CompactionOptions",
"FtsToken",
"col",
"Expr",
+1
View File
@@ -347,6 +347,7 @@ class Table:
*,
cleanup_since_ms: Optional[int] = None,
delete_unverified: Optional[bool] = None,
compaction_options: Optional[Dict[str, Any]] = None,
) -> OptimizeStats: ...
async def uri(self) -> str: ...
async def initial_storage_options(self) -> Optional[Dict[str, str]]: ...
+11 -1
View File
@@ -64,7 +64,16 @@ from ..query import (
LanceTakeQueryBuilder,
LanceVectorQueryBuilder,
)
from ..table import AsyncTable, BlobMode, Branches, IndexStatistics, Query, Table, Tags
from ..table import (
AsyncTable,
BlobMode,
Branches,
CompactionOptions,
IndexStatistics,
Query,
Table,
Tags,
)
from ..types import BaseTokenizerType
@@ -942,6 +951,7 @@ class RemoteTable(Table):
*,
cleanup_older_than: Optional[timedelta] = None,
delete_unverified: bool = False,
compaction_options: Optional[CompactionOptions] = None,
):
"""
optimize() is a no-op on LanceDB Cloud.
+81
View File
@@ -22,6 +22,7 @@ from typing import (
Optional,
Sequence,
Tuple,
TypedDict,
Union,
overload,
)
@@ -211,6 +212,69 @@ IndexConfigType = Union[
FTS,
]
class CompactionOptions(TypedDict, total=False):
"""Options that control file compaction during table optimization.
Unspecified options use Lance's defaults.
Examples
--------
Limit fragment size and compaction concurrency for tables with large rows:
>>> options: CompactionOptions = {
... "target_rows_per_fragment": 500,
... "batch_size": 1024,
... "num_threads": 1,
... }
>>> await table.optimize(compaction_options=options) # doctest: +SKIP
"""
target_rows_per_fragment: int
"""Target number of rows per fragment (default: 1,048,576)."""
max_rows_per_group: int
"""Maximum number of rows per row group (default: 1,024)."""
max_bytes_per_file: Optional[int]
"""Maximum number of bytes per data file."""
materialize_deletions: bool
"""Whether to rewrite fragments containing deleted rows (default: True)."""
materialize_deletions_threshold: float
"""Minimum deleted-row fraction that makes a fragment eligible (default: 0.1)."""
num_threads: Optional[int]
"""Number of compaction tasks to run in parallel."""
batch_size: Optional[int]
"""Number of rows per input scan batch."""
io_buffer_size: Optional[int]
"""Maximum number of bytes queued in the input scan I/O buffer."""
defer_index_remap: bool
"""Whether to defer index remapping during compaction (default: False)."""
index_remap_mode: Literal["direct", "compact"]
"""How to construct the old-to-new row-address mapping."""
compaction_mode: Optional[
Literal["reencode", "try_binary_copy", "force_binary_copy"]
]
"""Whether compaction re-encodes data or uses binary copying."""
binary_copy_read_batch_bytes: Optional[int]
"""Number of bytes read per batch during binary-copy compaction."""
max_source_fragments: Optional[int]
"""Maximum number of source fragments compacted in one run."""
max_overlays_per_fragment: Optional[int]
"""Maximum overlays before a fragment is fully compacted."""
# Known distance metrics for legacy API detection
KNOWN_METRICS = {"l2", "cosine", "dot", "hamming"}
@@ -1820,6 +1884,7 @@ class Table(ABC):
cleanup_older_than: Optional[timedelta] = None,
delete_unverified: bool = False,
retrain: bool = False,
compaction_options: Optional[CompactionOptions] = None,
):
"""
Optimize the on-disk data and indices for better performance.
@@ -1852,6 +1917,10 @@ class Table(ABC):
retrain: bool, default False
This parameter is no longer used and is deprecated.
compaction_options: CompactionOptions, optional
Options that control file compaction. This can be used to bound
memory usage by reducing ``target_rows_per_fragment``, ``batch_size``,
or ``num_threads``.
Notes
-----
@@ -3825,6 +3894,7 @@ class LanceTable(Table):
cleanup_older_than: Optional[timedelta] = None,
delete_unverified: bool = False,
retrain: bool = False,
compaction_options: Optional[CompactionOptions] = None,
):
"""
Optimize the on-disk data and indices for better performance.
@@ -3857,6 +3927,10 @@ class LanceTable(Table):
retrain: bool, default False
This parameter is no longer used and is deprecated.
compaction_options: CompactionOptions, optional
Options that control file compaction. This can be used to bound
memory usage by reducing ``target_rows_per_fragment``, ``batch_size``,
or ``num_threads``.
Notes
-----
@@ -3871,6 +3945,7 @@ class LanceTable(Table):
cleanup_older_than=cleanup_older_than,
delete_unverified=delete_unverified,
retrain=retrain,
compaction_options=compaction_options,
)
)
@@ -6019,6 +6094,7 @@ class AsyncTable:
cleanup_older_than: Optional[timedelta] = None,
delete_unverified: bool = False,
retrain=False,
compaction_options: Optional[CompactionOptions] = None,
) -> OptimizeStats:
"""
Optimize the on-disk data and indices for better performance.
@@ -6051,6 +6127,10 @@ class AsyncTable:
retrain: bool, default False
This parameter is no longer used and is deprecated.
compaction_options: CompactionOptions, optional
Options that control file compaction. This can be used to bound
memory usage by reducing ``target_rows_per_fragment``, ``batch_size``,
or ``num_threads``.
Notes
-----
@@ -6077,6 +6157,7 @@ class AsyncTable:
return await self._inner.optimize(
cleanup_since_ms=cleanup_since_ms,
delete_unverified=delete_unverified,
compaction_options=compaction_options,
)
async def list_indices(self) -> Iterable[IndexConfig]:
+23
View File
@@ -3381,6 +3381,29 @@ async def test_optimize(mem_db_async: AsyncConnection):
assert await table.query().to_arrow() == pa.table({"x": [[1], [2]]})
@pytest.mark.asyncio
async def test_optimize_compaction_options(mem_db_async: AsyncConnection):
table = await mem_db_async.create_table("test", data=[{"x": 1}])
await table.add([{"x": 2}])
stats = await table.optimize(
compaction_options={
"target_rows_per_fragment": 1,
"batch_size": 1,
"num_threads": 1,
}
)
assert stats.compaction.fragments_removed == 0
assert stats.compaction.fragments_added == 0
stats = await table.optimize(compaction_options={"target_rows_per_fragment": 3})
assert stats.compaction.fragments_removed == 2
assert stats.compaction.fragments_added == 1
with pytest.raises(ValueError, match="Invalid compaction option: unknown"):
await table.optimize(compaction_options={"unknown": 1})
@pytest.mark.asyncio
async def test_optimize_delete_unverified(tmp_db_async: AsyncConnection, tmp_path):
table = await tmp_db_async.create_table(
+60 -7
View File
@@ -20,8 +20,9 @@ use arrow::{
use lancedb::blob::{BlobFile, BlobRangeRequest};
use lancedb::index::scalar::FtsIndexBuilder;
use lancedb::table::{
AddDataMode, ColumnAlteration, Duration, FieldMetadataUpdate, FtsToken as LanceDbFtsToken,
NewColumnTransform, OptimizeAction, OptimizeOptions, Ref, Table as LanceDbTable,
AddDataMode, ColumnAlteration, CompactionMode, CompactionOptions, Duration,
FieldMetadataUpdate, FtsToken as LanceDbFtsToken, IndexRemapMode, NewColumnTransform,
OptimizeAction, OptimizeOptions, Ref, Table as LanceDbTable,
};
use lancedb::tokenize as lancedb_tokenize;
use pyo3::{
@@ -39,6 +40,56 @@ enum PredicateArg {
Sql(String),
}
fn parse_compaction_options(options: Option<&Bound<'_, PyDict>>) -> PyResult<CompactionOptions> {
let mut parsed = CompactionOptions::default();
let Some(options) = options else {
return Ok(parsed);
};
for (key, value) in options.iter() {
let key: String = key.extract()?;
match key.as_str() {
"target_rows_per_fragment" => parsed.target_rows_per_fragment = value.extract()?,
"max_rows_per_group" => parsed.max_rows_per_group = value.extract()?,
"max_bytes_per_file" => parsed.max_bytes_per_file = value.extract()?,
"materialize_deletions" => parsed.materialize_deletions = value.extract()?,
"materialize_deletions_threshold" => {
parsed.materialize_deletions_threshold = value.extract()?
}
"num_threads" => parsed.num_threads = value.extract()?,
"batch_size" => parsed.batch_size = value.extract()?,
"io_buffer_size" => parsed.io_buffer_size = value.extract()?,
"defer_index_remap" => parsed.defer_index_remap = value.extract()?,
"index_remap_mode" => {
let mode: String = value.extract()?;
parsed.index_remap_mode = IndexRemapMode::try_from(mode.as_str())
.map_err(|err| PyValueError::new_err(err.to_string()))?;
}
"compaction_mode" => {
let mode: Option<String> = value.extract()?;
parsed.compaction_mode = mode
.map(|mode| {
CompactionMode::try_from(mode.as_str())
.map_err(|err| PyValueError::new_err(err.to_string()))
})
.transpose()?;
}
"binary_copy_read_batch_bytes" => {
parsed.binary_copy_read_batch_bytes = value.extract()?
}
"max_source_fragments" => parsed.max_source_fragments = value.extract()?,
"max_overlays_per_fragment" => parsed.max_overlays_per_fragment = value.extract()?,
_ => {
return Err(PyValueError::new_err(format!(
"Invalid compaction option: {key}"
)));
}
}
}
Ok(parsed)
}
/// Statistics about a compaction operation.
#[pyclass(get_all, from_py_object)]
#[derive(Clone, Debug)]
@@ -1195,13 +1246,15 @@ impl Table {
}
/// Optimize the on-disk data by compacting and pruning old data, for better performance.
#[pyo3(signature = (cleanup_since_ms=None, delete_unverified=None))]
pub fn optimize(
self_: PyRef<'_, Self>,
#[pyo3(signature = (cleanup_since_ms=None, delete_unverified=None, compaction_options=None))]
pub fn optimize<'py>(
self_: PyRef<'py, Self>,
cleanup_since_ms: Option<u64>,
delete_unverified: Option<bool>,
) -> PyResult<Bound<'_, PyAny>> {
compaction_options: Option<&Bound<'py, PyDict>>,
) -> PyResult<Bound<'py, PyAny>> {
let inner = self_.inner_ref()?.clone();
let compaction_options = parse_compaction_options(compaction_options)?;
let older_than = if let Some(ms) = cleanup_since_ms {
if ms > i64::MAX as u64 {
return Err(PyValueError::new_err(format!(
@@ -1217,7 +1270,7 @@ impl Table {
future_into_py(self_.py(), async move {
let compaction_stats = inner
.optimize(OptimizeAction::Compact {
options: lancedb::table::CompactionOptions::default(),
options: compaction_options,
remap_options: None,
})
.await