Skip to content

Commit c39719e

Browse files
feat(compaction): introduce RowAddrRemap structure to avoid remap OOM caused by HashMap (#7237)
1 parent 9cf1e00 commit c39719e

37 files changed

Lines changed: 1247 additions & 206 deletions

rust/lance-core/src/utils.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ pub mod hash;
1515
pub mod io_stats;
1616
pub mod parse;
1717
pub mod path;
18+
pub mod row_addr_remap;
1819
pub mod tempfile;
1920
pub mod testing;
2021
pub mod tokio;

rust/lance-core/src/utils/row_addr_remap.rs

Lines changed: 413 additions & 0 deletions
Large diffs are not rendered by default.

rust/lance-index/src/scalar.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ use datafusion::functions::string::contains::ContainsFunc;
1313
use datafusion::functions_nested::array_has;
1414
use datafusion::physical_plan::SendableRecordBatchStream;
1515
use datafusion_common::{Column, scalar::ScalarValue};
16+
use lance_core::utils::row_addr_remap::RowAddrRemap;
1617
use std::collections::{HashMap, HashSet};
1718
use std::fmt::Debug;
1819
use std::pin::Pin;
@@ -1108,7 +1109,7 @@ pub trait ScalarIndex: Send + Sync + std::fmt::Debug + Index + DeepSizeOf {
11081109
/// Remap the row ids, creating a new remapped version of this index in `dest_store`
11091110
async fn remap(
11101111
&self,
1111-
mapping: &HashMap<u64, Option<u64>>,
1112+
mapping: &RowAddrRemap,
11121113
dest_store: &dyn IndexStore,
11131114
) -> Result<CreatedIndex>;
11141115

rust/lance-index/src/scalar/bitmap.rs

Lines changed: 43 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright The Lance Authors
33

4+
use lance_core::utils::row_addr_remap::RowAddrRemap;
45
use std::{
56
any::Any,
67
cmp::Reverse,
@@ -795,7 +796,7 @@ impl ScalarIndex for BitmapIndex {
795796
/// Remap the row ids, creating a new remapped version of this index in `dest_store`
796797
async fn remap(
797798
&self,
798-
mapping: &HashMap<u64, Option<u64>>,
799+
mapping: &RowAddrRemap,
799800
dest_store: &dyn IndexStore,
800801
) -> Result<CreatedIndex> {
801802
let state = self.load_bitmap_index_state().await?;
@@ -1549,18 +1550,15 @@ impl BitmapIndexPlugin {
15491550
/// Remaps every bitmap in a materialized bitmap-index state using row-id mappings.
15501551
pub(crate) fn remap_bitmap_state(
15511552
state: HashMap<ScalarValue, RowAddrTreeMap>,
1552-
mapping: &HashMap<u64, Option<u64>>,
1553+
mapping: &RowAddrRemap,
15531554
) -> HashMap<ScalarValue, RowAddrTreeMap> {
15541555
state
15551556
.into_iter()
15561557
.map(|(key, bitmap)| {
15571558
let remapped_bitmap =
15581559
RowAddrTreeMap::from_iter(bitmap.row_addrs().unwrap().filter_map(|addr| {
15591560
let addr_as_u64 = u64::from(addr);
1560-
mapping
1561-
.get(&addr_as_u64)
1562-
.copied()
1563-
.unwrap_or(Some(addr_as_u64))
1561+
mapping.get(addr_as_u64).unwrap_or(Some(addr_as_u64))
15641562
}));
15651563
(key, remapped_bitmap)
15661564
})
@@ -1909,7 +1907,7 @@ mod tests {
19091907
use lance_core::utils::{address::RowAddress, tempfile::TempObjDir};
19101908
use lance_io::object_store::ObjectStore;
19111909
use lance_select::RowSetOps;
1912-
use std::collections::HashMap;
1910+
use rstest::rstest;
19131911

19141912
fn assert_state_roundtrips(state: &BitmapIndexState) {
19151913
let mut buf = Vec::new();
@@ -2460,8 +2458,44 @@ mod tests {
24602458
assert_eq!(red_rows_2, vec![0, 3, 6, 10, 11]);
24612459
}
24622460

2461+
// frags 1 and 2 (3 rows each) are compacted into frag 3: the 6 rows are
2462+
// rewritten in order to frag 3 offsets 0..6.
2463+
fn bitmap_remap_compact() -> RowAddrRemap {
2464+
use lance_core::utils::row_addr_remap::GroupInput;
2465+
use roaring::RoaringTreemap;
2466+
RowAddrRemap::compact([GroupInput {
2467+
rewritten_old_row_addrs: RoaringTreemap::from_iter(
2468+
(0..3)
2469+
.map(|o| u64::from(RowAddress::new_from_parts(1, o)))
2470+
.chain((0..3).map(|o| u64::from(RowAddress::new_from_parts(2, o)))),
2471+
),
2472+
old_frag_ids: vec![1, 2],
2473+
new_frags: vec![(3, 6)],
2474+
}])
2475+
.unwrap()
2476+
}
2477+
2478+
fn bitmap_remap_explicit() -> RowAddrRemap {
2479+
// The same mapping, listed out explicitly.
2480+
RowAddrRemap::direct(
2481+
(0..6u32)
2482+
.map(|i| {
2483+
let (f, o) = if i < 3 { (1, i) } else { (2, i - 3) };
2484+
(
2485+
u64::from(RowAddress::new_from_parts(f, o)),
2486+
Some(u64::from(RowAddress::new_from_parts(3, i))),
2487+
)
2488+
})
2489+
.collect(),
2490+
)
2491+
}
2492+
2493+
// remap must behave identically whether the mapping is compact or explicit.
2494+
#[rstest]
2495+
#[case(bitmap_remap_compact())]
2496+
#[case(bitmap_remap_explicit())]
24632497
#[tokio::test]
2464-
async fn test_remap_bitmap_with_null() {
2498+
async fn test_remap_bitmap_with_null(#[case] remap: RowAddrRemap) {
24652499
use arrow_array::UInt32Array;
24662500

24672501
// Create a temporary store.
@@ -2526,38 +2560,8 @@ mod tests {
25262560
assert_eq!(index.index_map.len(), 2); // 2 non-null values (1 and 2)
25272561
assert!(!index.null_map.is_empty()); // Should have null values
25282562

2529-
// Create a remap that simulates compaction of frags 1 and 2 into frag 3
2530-
let mut row_addr_map = HashMap::<u64, Option<u64>>::new();
2531-
row_addr_map.insert(
2532-
RowAddress::new_from_parts(1, 0).into(),
2533-
Some(RowAddress::new_from_parts(3, 0).into()),
2534-
);
2535-
row_addr_map.insert(
2536-
RowAddress::new_from_parts(1, 1).into(),
2537-
Some(RowAddress::new_from_parts(3, 1).into()),
2538-
);
2539-
row_addr_map.insert(
2540-
RowAddress::new_from_parts(1, 2).into(),
2541-
Some(RowAddress::new_from_parts(3, 2).into()),
2542-
);
2543-
row_addr_map.insert(
2544-
RowAddress::new_from_parts(2, 0).into(),
2545-
Some(RowAddress::new_from_parts(3, 3).into()),
2546-
);
2547-
row_addr_map.insert(
2548-
RowAddress::new_from_parts(2, 1).into(),
2549-
Some(RowAddress::new_from_parts(3, 4).into()),
2550-
);
2551-
row_addr_map.insert(
2552-
RowAddress::new_from_parts(2, 2).into(),
2553-
Some(RowAddress::new_from_parts(3, 5).into()),
2554-
);
2555-
25562563
// Perform remap
2557-
index
2558-
.remap(&row_addr_map, test_store.as_ref())
2559-
.await
2560-
.unwrap();
2564+
index.remap(&remap, test_store.as_ref()).await.unwrap();
25612565

25622566
// Reload and check
25632567
let reloaded_idx = BitmapIndex::load(test_store, None, &LanceCache::no_cache())

rust/lance-index/src/scalar/bloomfilter.rs

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,12 +20,13 @@ use arrow_schema::{DataType, Field};
2020
use lance_arrow_stats::StatisticsAccumulator;
2121
use lance_core::utils::bloomfilter::as_bytes;
2222
use lance_core::utils::bloomfilter::sbbf::{Sbbf, SbbfBuilder};
23+
use lance_core::utils::row_addr_remap::RowAddrRemap;
2324
use serde::{Deserialize, Serialize};
2425

2526
use std::sync::LazyLock;
2627

2728
use datafusion::execution::SendableRecordBatchStream;
28-
use std::{collections::HashMap, sync::Arc};
29+
use std::sync::Arc;
2930

3031
use crate::scalar::{
3132
AnyQuery, IndexStore, MetricsCollector, RowIdRemapper, ScalarIndex, SearchResult,
@@ -425,7 +426,7 @@ impl ScalarIndex for BloomFilterIndex {
425426

426427
async fn remap(
427428
&self,
428-
_mapping: &HashMap<u64, Option<u64>>,
429+
_mapping: &RowAddrRemap,
429430
_dest_store: &dyn IndexStore,
430431
) -> Result<CreatedIndex> {
431432
Err(Error::invalid_input_source(

rust/lance-index/src/scalar/btree.rs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright The Lance Authors
33

4+
use lance_core::utils::row_addr_remap::RowAddrRemap;
45
use std::{
56
any::Any,
67
cmp::Ordering,
@@ -2227,7 +2228,7 @@ impl ScalarIndex for BTreeIndex {
22272228

22282229
async fn remap(
22292230
&self,
2230-
mapping: &HashMap<u64, Option<u64>>,
2231+
mapping: &RowAddrRemap,
22312232
dest_store: &dyn IndexStore,
22322233
) -> Result<CreatedIndex> {
22332234
// (part_id, path)
@@ -2394,7 +2395,7 @@ pub trait BTreeSubIndex: Debug + Send + Sync + DeepSizeOf {
23942395
async fn remap_subindex(
23952396
&self,
23962397
serialized: RecordBatch,
2397-
mapping: &HashMap<u64, Option<u64>>,
2398+
mapping: &RowAddrRemap,
23982399
) -> Result<RecordBatch>;
23992400
}
24002401

@@ -3355,6 +3356,7 @@ impl ScalarIndexPlugin for BTreeIndexPlugin {
33553356

33563357
#[cfg(test)]
33573358
mod tests {
3359+
use lance_core::utils::row_addr_remap::RowAddrRemap;
33583360
use std::sync::atomic::Ordering;
33593361
use std::{collections::HashMap, sync::Arc};
33603362

@@ -3503,7 +3505,7 @@ mod tests {
35033505

35043506
// Remap with a no-op mapping. The remapped index should be identical to the original
35053507
index
3506-
.remap(&HashMap::default(), remap_store.as_ref())
3508+
.remap(&RowAddrRemap::empty(), remap_store.as_ref())
35073509
.await
35083510
.unwrap();
35093511

@@ -5014,7 +5016,7 @@ mod tests {
50145016

50155017
// Remap with a no-op mapping. The remapped index should be identical to the original
50165018
ranged_index
5017-
.remap(&HashMap::default(), remap_store.as_ref())
5019+
.remap(&RowAddrRemap::empty(), remap_store.as_ref())
50185020
.await
50195021
.unwrap();
50205022

@@ -5342,7 +5344,10 @@ mod tests {
53425344
));
53435345

53445346
// Remap the index with our deletion mapping
5345-
index.remap(&mapping, remap_store.as_ref()).await.unwrap();
5347+
index
5348+
.remap(&RowAddrRemap::direct(mapping), remap_store.as_ref())
5349+
.await
5350+
.unwrap();
53465351

53475352
let remapped_index = BTreeIndex::load(remap_store.clone(), None, &LanceCache::no_cache())
53485353
.await

rust/lance-index/src/scalar/btree/flat.rs

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
// SPDX-License-Identifier: Apache-2.0
22
// SPDX-FileCopyrightText: Copyright The Lance Authors
33

4-
use std::collections::{BTreeSet, HashMap};
4+
use lance_core::utils::row_addr_remap::RowAddrRemap;
5+
use std::collections::BTreeSet;
56
use std::{ops::Bound, sync::Arc};
67

78
use arrow_array::Array;
@@ -133,19 +134,15 @@ impl FlatIndex {
133134
NullableRowAddrSet::new(self.all_addrs_map.clone(), Default::default())
134135
}
135136

136-
pub fn remap_batch(
137-
batch: RecordBatch,
138-
mapping: &HashMap<u64, Option<u64>>,
139-
) -> Result<RecordBatch> {
137+
pub fn remap_batch(batch: RecordBatch, mapping: &RowAddrRemap) -> Result<RecordBatch> {
140138
let row_ids = batch.column(IDS_COL_IDX).as_primitive::<UInt64Type>();
141139
let val_idx_and_new_id = row_ids
142140
.values()
143141
.iter()
144142
.enumerate()
145143
.filter_map(|(idx, old_id)| {
146144
mapping
147-
.get(old_id)
148-
.copied()
145+
.get(*old_id)
149146
.unwrap_or(Some(*old_id))
150147
.map(|new_id| (idx, new_id))
151148
})
@@ -346,7 +343,11 @@ mod tests {
346343
use super::*;
347344
use arrow_array::{record_batch, types::Int32Type};
348345
use datafusion_common::ScalarValue;
346+
use lance_core::utils::row_addr_remap::GroupInput;
349347
use lance_datagen::{RowCount, array, gen_batch};
348+
use roaring::RoaringTreemap;
349+
use rstest::rstest;
350+
use std::collections::HashMap;
350351

351352
fn example_index() -> FlatIndex {
352353
let batch = gen_batch()
@@ -490,9 +491,10 @@ mod tests {
490491
// 3 -> delete
491492
// Keep remaining as is
492493
let mapping = HashMap::<u64, Option<u64>>::from_iter(vec![(0, Some(2000)), (3, None)]);
493-
let remapped =
494-
FlatIndex::try_new(FlatIndex::remap_batch((*index.data).clone(), &mapping).unwrap())
495-
.unwrap();
494+
let remapped = FlatIndex::try_new(
495+
FlatIndex::remap_batch((*index.data).clone(), &RowAddrRemap::direct(mapping)).unwrap(),
496+
)
497+
.unwrap();
496498

497499
let expected = FlatIndex::try_new(
498500
gen_batch()
@@ -505,18 +507,22 @@ mod tests {
505507
assert_eq!(remapped.data, expected.data);
506508
}
507509

508-
// It's possible, during compaction, that an entire page of values is deleted. We just serialize
509-
// it as an empty record batch.
510-
#[tokio::test]
511-
async fn test_remap_to_nothing() {
510+
// An entire page (frag 0) is deleted during compaction. remap_batch must
511+
// drop every row regardless of which RowAddrRemap mode expresses it.
512+
// example_index holds row ids 5, 0, 3, 100, all in frag 0.
513+
#[rstest]
514+
#[case::compact(RowAddrRemap::compact([GroupInput {
515+
rewritten_old_row_addrs: RoaringTreemap::new(),
516+
old_frag_ids: vec![0],
517+
new_frags: vec![],
518+
}])
519+
.unwrap())]
520+
#[case::explicit(RowAddrRemap::direct(
521+
[5u64, 0, 3, 100].into_iter().map(|id| (id, None)).collect(),
522+
))]
523+
fn test_remap_to_nothing(#[case] remap: RowAddrRemap) {
512524
let index = example_index();
513-
let mapping = HashMap::<u64, Option<u64>>::from_iter(vec![
514-
(5, None),
515-
(0, None),
516-
(3, None),
517-
(100, None),
518-
]);
519-
let remapped = FlatIndex::remap_batch((*index.data).clone(), &mapping).unwrap();
525+
let remapped = FlatIndex::remap_batch((*index.data).clone(), &remap).unwrap();
520526
assert_eq!(remapped.num_rows(), 0);
521527
}
522528

rust/lance-index/src/scalar/fmindex.rs

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ use datafusion::execution::SendableRecordBatchStream;
3232
use futures::{StreamExt, TryStreamExt};
3333
use lance_core::cache::LanceCache;
3434
use lance_core::deepsize::DeepSizeOf;
35+
use lance_core::utils::row_addr_remap::RowAddrRemap;
3536
use lance_core::utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu};
3637
use lance_core::{Error, ROW_ADDR, Result};
3738
use roaring::RoaringBitmap;
@@ -1828,11 +1829,7 @@ impl ScalarIndex for FMIndexScalarIndex {
18281829
fn can_remap(&self) -> bool {
18291830
false
18301831
}
1831-
async fn remap(
1832-
&self,
1833-
_: &HashMap<u64, Option<u64>>,
1834-
_: &dyn IndexStore,
1835-
) -> Result<CreatedIndex> {
1832+
async fn remap(&self, _: &RowAddrRemap, _: &dyn IndexStore) -> Result<CreatedIndex> {
18361833
Err(Error::not_supported("Fm does not support remap"))
18371834
}
18381835
async fn update(

0 commit comments

Comments
 (0)