1use std::sync::Arc;
8
9use arrow::{array::AsArray, compute::sort_to_indices};
10use arrow_array::{RecordBatch, UInt32Array};
11use arrow_schema::Schema;
12use futures::{future::try_join_all, prelude::*};
13use lance_arrow::{RecordBatchExt, SchemaExt};
14use lance_core::{
15 Error, Result,
16 cache::LanceCache,
17 utils::tokio::{get_num_compute_intensive_cpus, spawn_cpu},
18};
19use lance_encoding::decoder::{DecoderPlugins, FilterExpression};
20use lance_file::reader::{FileReader, FileReaderOptions};
21use lance_file::writer::FileWriter;
22use lance_io::{
23 object_store::ObjectStore,
24 scheduler::{ScanScheduler, SchedulerConfig},
25 stream::{RecordBatchStream, RecordBatchStreamAdapter},
26 utils::CachedFileSize,
27};
28use object_store::path::Path;
29
30use crate::vector::{LOSS_METADATA_KEY, PART_ID_COLUMN};
31
32#[async_trait::async_trait]
33pub trait ShuffleReader: Send + Sync {
35 async fn read_partition(
39 &self,
40 partition_id: usize,
41 ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>>;
42
43 fn partition_size(&self, partition_id: usize) -> Result<usize>;
45
46 fn total_loss(&self) -> Option<f64>;
51}
52
53#[async_trait::async_trait]
54pub trait Shuffler: Send + Sync {
57 async fn shuffle(
60 &self,
61 data: Box<dyn RecordBatchStream + Unpin + 'static>,
62 ) -> Result<Box<dyn ShuffleReader>>;
63}
64
65pub struct IvfShuffler {
66 object_store: Arc<ObjectStore>,
67 output_dir: Path,
68 num_partitions: usize,
69
70 precomputed_shuffle_buffers: Option<Vec<String>>,
72 progress: Arc<dyn crate::progress::IndexBuildProgress>,
73}
74
75impl IvfShuffler {
76 pub fn new(output_dir: Path, num_partitions: usize) -> Self {
77 Self {
78 object_store: Arc::new(ObjectStore::local()),
79 output_dir,
80 num_partitions,
81 precomputed_shuffle_buffers: None,
82 progress: crate::progress::noop_progress(),
83 }
84 }
85
86 pub fn with_progress(mut self, progress: Arc<dyn crate::progress::IndexBuildProgress>) -> Self {
87 self.progress = progress;
88 self
89 }
90
91 pub fn with_precomputed_shuffle_buffers(
92 mut self,
93 precomputed_shuffle_buffers: Option<Vec<String>>,
94 ) -> Self {
95 self.precomputed_shuffle_buffers = precomputed_shuffle_buffers;
96 self
97 }
98}
99
100#[async_trait::async_trait]
101impl Shuffler for IvfShuffler {
102 async fn shuffle(
103 &self,
104 data: Box<dyn RecordBatchStream + Unpin + 'static>,
105 ) -> Result<Box<dyn ShuffleReader>> {
106 let num_partitions = self.num_partitions;
107 let mut partition_sizes = vec![0; num_partitions];
108 let schema = data.schema().without_column(PART_ID_COLUMN);
109 let mut writers = stream::iter(0..num_partitions)
110 .map(|partition_id| {
111 let part_path = self.output_dir.child(format!("ivf_{}.lance", partition_id));
112 let spill_path = self.output_dir.child(format!("ivf_{}.spill", partition_id));
113 let object_store = self.object_store.clone();
114 let schema = schema.clone();
115 async move {
116 let writer = object_store.create(&part_path).await?;
117 let file_writer = FileWriter::try_new(
118 writer,
119 lance_core::datatypes::Schema::try_from(&schema)?,
120 Default::default(),
121 )?
122 .with_page_metadata_spill(object_store.clone(), spill_path);
123 Result::Ok(file_writer)
124 }
125 })
126 .buffered(self.object_store.io_parallelism())
127 .try_collect::<Vec<_>>()
128 .await?;
129 let mut parallel_sort_stream = data
130 .map(|batch| {
131 spawn_cpu(move || {
132 let batch = batch?;
133
134 let loss = batch
135 .metadata()
136 .get(LOSS_METADATA_KEY)
137 .map(|s| s.parse::<f64>().unwrap_or_default())
138 .unwrap_or_default();
139
140 let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
141
142 let indices = sort_to_indices(&part_ids, None, None)?;
143 let batch = batch.take(&indices)?;
144
145 let part_ids: &UInt32Array = batch[PART_ID_COLUMN].as_primitive();
146 let batch = batch.drop_column(PART_ID_COLUMN)?;
147
148 let mut partition_buffers = vec![Vec::new(); num_partitions];
149
150 let mut start = 0;
151 while start < batch.num_rows() {
152 let part_id: u32 = part_ids.value(start);
153 let mut end = start + 1;
154 while end < batch.num_rows() && part_ids.value(end) == part_id {
155 end += 1;
156 }
157
158 let part_batches = &mut partition_buffers[part_id as usize];
159 part_batches.push(batch.slice(start, end - start));
160 start = end;
161 }
162
163 Ok::<(Vec<Vec<RecordBatch>>, f64), Error>((partition_buffers, loss))
164 })
165 })
166 .buffered(get_num_compute_intensive_cpus());
167
168 let mut total_loss = 0.0;
169 let mut counter: u64 = 0;
170 while let Some(shuffled) = parallel_sort_stream.next().await {
171 let (shuffled, loss) = shuffled?;
172 total_loss += loss;
173
174 let mut futs = Vec::new();
175 for (part_id, (writer, batches)) in writers.iter_mut().zip(shuffled.iter()).enumerate()
176 {
177 if !batches.is_empty() {
178 partition_sizes[part_id] += batches.iter().map(|b| b.num_rows()).sum::<usize>();
179 futs.push(writer.write_batches(batches.iter()));
180 }
181 }
182 try_join_all(futs).await?;
183
184 counter += 1;
185 self.progress.stage_progress("shuffle", counter).await?;
186 }
187
188 for writer in writers.iter_mut() {
190 writer.finish().await?;
191 }
192
193 Ok(Box::new(IvfShufflerReader::new(
194 self.object_store.clone(),
195 self.output_dir.clone(),
196 partition_sizes,
197 total_loss,
198 )))
199 }
200}
201
202pub struct IvfShufflerReader {
203 scheduler: Arc<ScanScheduler>,
204 output_dir: Path,
205 partition_sizes: Vec<usize>,
206 loss: f64,
207}
208
209impl IvfShufflerReader {
210 pub fn new(
211 object_store: Arc<ObjectStore>,
212 output_dir: Path,
213 partition_sizes: Vec<usize>,
214 loss: f64,
215 ) -> Self {
216 let scheduler_config = SchedulerConfig::max_bandwidth(&object_store);
217 let scheduler = ScanScheduler::new(object_store, scheduler_config);
218 Self {
219 scheduler,
220 output_dir,
221 partition_sizes,
222 loss,
223 }
224 }
225}
226
227#[async_trait::async_trait]
228impl ShuffleReader for IvfShufflerReader {
229 async fn read_partition(
230 &self,
231 partition_id: usize,
232 ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>> {
233 if partition_id >= self.partition_sizes.len() {
234 return Ok(None);
235 }
236
237 let partition_path = self.output_dir.child(format!("ivf_{}.lance", partition_id));
238
239 let reader = FileReader::try_open(
240 self.scheduler
241 .open_file(&partition_path, &CachedFileSize::unknown())
242 .await?,
243 None,
244 Arc::<DecoderPlugins>::default(),
245 &LanceCache::no_cache(),
246 FileReaderOptions::default(),
247 )
248 .await?;
249 let schema: Schema = reader.schema().as_ref().into();
250 Ok(Some(Box::new(RecordBatchStreamAdapter::new(
251 Arc::new(schema),
252 reader.read_stream(
253 lance_io::ReadBatchParams::RangeFull,
254 u32::MAX,
255 16,
256 FilterExpression::no_filter(),
257 )?,
258 ))))
259 }
260
261 fn partition_size(&self, partition_id: usize) -> Result<usize> {
262 Ok(self.partition_sizes.get(partition_id).copied().unwrap_or(0))
263 }
264
265 fn total_loss(&self) -> Option<f64> {
266 Some(self.loss)
267 }
268}
269
270pub struct EmptyReader;
271
272#[async_trait::async_trait]
273impl ShuffleReader for EmptyReader {
274 async fn read_partition(
275 &self,
276 _partition_id: usize,
277 ) -> Result<Option<Box<dyn RecordBatchStream + Unpin + 'static>>> {
278 Ok(None)
279 }
280
281 fn partition_size(&self, _partition_id: usize) -> Result<usize> {
282 Ok(0)
283 }
284
285 fn total_loss(&self) -> Option<f64> {
286 None
287 }
288}