Skip to main content

qdrant_datafusion/arrow/
deserialize.rs

1//! Schema-driven [`RecordBatch`] builder for `Qdrant` data
2use std::collections::HashMap;
3use std::sync::Arc;
4
5use datafusion::arrow::array::*;
6use datafusion::arrow::datatypes::SchemaRef;
7use datafusion::arrow::record_batch::RecordBatch;
8use datafusion::error::{DataFusionError, Result as DataFusionResult};
9use qdrant_client::qdrant::{
10    ScoredPoint, SparseVector, VectorOutput, VectorsOutput, point_id, vector_output, vectors_output,
11};
12
13use super::schema::is_multi_vector_field;
14
15/// Convert flat vector data into multi-vector format with proper validation.
16///
17/// This function implements the same conversion logic as `Qdrant`'s Rust client `try_into_multi()`
18/// method. It takes a flat array of floats and splits it into multiple sub-vectors based on the
19/// vectors count. This handles Qdrant's deprecated protobuf format for multi-vectors.
20///
21/// # Arguments
22/// * `data` - Flat array of float values representing concatenated vectors
23/// * `vectors_count` - Number of vectors to split the data into
24///
25/// # Returns
26/// A vector of vectors, where each sub-vector represents one embedding.
27///
28/// # Errors
29/// Returns a `DataFusionError` if the data length is not evenly divisible by the vectors count,
30/// which indicates malformed multi-vector data.
31///
32/// # Examples
33/// ```rust,ignore
34/// use qdrant_datafusion::arrow::deserialize::convert_to_multi_vector;
35///
36/// // Convert [1.0, 2.0, 3.0, 4.0] into 2 vectors of length 2
37/// let data = vec![1.0, 2.0, 3.0, 4.0];
38/// let result = convert_to_multi_vector(&data, 2).unwrap();
39/// assert_eq!(result, vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
40/// ```
41pub fn convert_to_multi_vector(
42    data: &[f32],
43    vectors_count: u32,
44) -> DataFusionResult<Vec<Vec<f32>>> {
45    if data.len() % vectors_count as usize != 0 {
46        return Err(DataFusionError::External(Box::new(std::io::Error::new(
47            std::io::ErrorKind::InvalidData,
48            format!(
49                "Malformed multi vector: data length {} is not divisible by vectors count {}",
50                data.len(),
51                vectors_count
52            ),
53        ))));
54    }
55
56    let chunk_size = data.len() / vectors_count as usize;
57    Ok(data.chunks(chunk_size).map(<[f32]>::to_vec).collect())
58}
59
60/// Internal vector content representation for efficient processing.
61///
62/// This enum provides a clean abstraction over `Qdrant`'s various vector formats,
63/// normalizing them for consistent processing in the record batch builder.
64/// It handles both newer and deprecated protobuf formats from Qdrant.
65#[derive(Debug)]
66pub enum Vector {
67    Dense(Vec<f32>),
68    Sparse(SparseVector),
69    MultiDense(Vec<Vec<f32>>),
70}
71
72impl Vector {
73    /// Extract vector content from `VectorOutput`
74    fn from_vector_output(vector_output: VectorOutput) -> Option<Self> {
75        // Check newer format first
76        if let Some(vector) = vector_output.vector {
77            return match vector {
78                vector_output::Vector::Dense(dense) => Some(Self::Dense(dense.data)),
79                vector_output::Vector::Sparse(sparse) => Some(Self::Sparse(sparse)),
80                vector_output::Vector::MultiDense(multi) => {
81                    Some(Self::MultiDense(multi.vectors.into_iter().map(|v| v.data).collect()))
82                }
83            };
84        }
85
86        // Fall back to deprecated format
87        if let Some(vectors_count) = vector_output.vectors_count
88            && let Ok(multi_vectors) = convert_to_multi_vector(&vector_output.data, vectors_count)
89        {
90            // Multi-vector in deprecated format
91            return Some(Self::MultiDense(multi_vectors));
92        }
93
94        // Check for sparse in deprecated format
95        if let Some(indices) = vector_output.indices {
96            return Some(Self::Sparse(SparseVector {
97                indices: indices.data,
98                values:  vector_output.data,
99            }));
100        }
101
102        // No vectors found
103        if vector_output.data.is_empty() {
104            return None;
105        }
106
107        // Regular dense vector in deprecated format
108        Some(Self::Dense(vector_output.data))
109    }
110}
111
112/// Schema-driven field extractor - one per schema field
113enum FieldExtractor {
114    Id(StringBuilder),
115    Payload(StringBuilder),
116    DenseVector { name: String, builder: ListBuilder<Float32Builder> },
117    MultiVector { name: String, builder: ListBuilder<ListBuilder<Float32Builder>> },
118    SparseIndices { name: String, builder: ListBuilder<UInt32Builder> },
119    SparseValues { name: String, builder: ListBuilder<Float32Builder> },
120}
121
122impl FieldExtractor {
123    /// Create field extractor from schema field
124    fn from_schema_field(field: &datafusion::arrow::datatypes::Field, capacity: usize) -> Self {
125        match field.name().as_str() {
126            "id" => Self::Id(StringBuilder::with_capacity(capacity, capacity * 16)),
127            "payload" => Self::Payload(StringBuilder::with_capacity(capacity, capacity * 64)),
128            name if name.ends_with("_indices") => Self::SparseIndices {
129                name:    name.to_string(),
130                builder: ListBuilder::with_capacity(UInt32Builder::new(), capacity),
131            },
132            name if name.ends_with("_values") => Self::SparseValues {
133                name:    name.to_string(),
134                builder: ListBuilder::with_capacity(Float32Builder::new(), capacity),
135            },
136            name if is_multi_vector_field(field) => Self::MultiVector {
137                name:    name.to_string(),
138                builder: ListBuilder::with_capacity(
139                    ListBuilder::new(Float32Builder::new()),
140                    capacity,
141                ),
142            },
143            name => Self::DenseVector {
144                name:    name.to_string(),
145                builder: ListBuilder::with_capacity(Float32Builder::new(), capacity),
146            },
147        }
148    }
149}
150
151/// Schema-driven [`RecordBatch`] builder for `Qdrant` data.
152///
153/// This is the core component that converts `Qdrant` `ScoredPoint` data into Arrow `RecordBatch`es
154/// for `DataFusion` consumption. It uses a clean, schema-driven architecture that eliminates the
155/// complex nested matching logic of previous implementations.
156///
157/// # Architecture
158/// The builder is initialized with an Arrow schema and creates one `FieldExtractor` per schema
159/// field in the exact same order. During processing, each point is processed once with all fields
160/// updated in a single pass, achieving O(F) performance where F is the number of fields.
161///
162/// # Performance Features
163/// - **Single-Pass Processing**: Each point is destructured once and all fields updated
164/// - **Owned Iteration**: No unnecessary borrowing or reference management
165/// - **Pre-Allocated Builders**: Capacity is allocated upfront for optimal memory usage
166/// - **Inline Logic**: All processing logic is inline with no hidden function calls
167///
168/// # Examples
169/// ```rust,ignore
170/// use qdrant_datafusion::arrow::deserialize::QdrantRecordBatchBuilder;
171/// use datafusion::arrow::datatypes::{Schema, Field, DataType};
172/// use std::sync::Arc;
173///
174/// // Create schema and builder
175/// let schema = Arc::new(Schema::new(vec![
176///     Field::new("id", DataType::Utf8, false),
177///     Field::new("vector", DataType::List(
178///         Arc::new(Field::new("item", DataType::Float32, true))
179///     ), true),
180/// ]));
181///
182/// let mut builder = QdrantRecordBatchBuilder::new(schema, 1000);
183///
184/// // Process points (would normally come from Qdrant query)
185/// // for point in qdrant_points {
186/// //     builder.append_point(point);
187/// // }
188///
189/// // Create final record batch
190/// // let batch = builder.finish()?;
191/// ```
192pub struct QdrantRecordBatchBuilder {
193    schema:           SchemaRef,
194    field_extractors: Vec<FieldExtractor>, // 1:1 with schema fields, in schema order
195}
196
197impl QdrantRecordBatchBuilder {
198    /// Create builder from schema with proper capacity allocation
199    pub fn new(schema: SchemaRef, point_count: usize) -> Self {
200        // Schema-driven initialization - one extractor per field, in schema order
201        let field_extractors = schema
202            .fields()
203            .iter()
204            .map(|field| FieldExtractor::from_schema_field(field, point_count))
205            .collect();
206
207        Self { schema, field_extractors }
208    }
209
210    /// Append a point using owned destructuring - defines its own invariants
211    pub fn append_point(&mut self, point: ScoredPoint) {
212        // Single destructuring
213        let ScoredPoint { id, payload, vectors, .. } = point;
214
215        // Build lookup once per point
216        let vector_lookup = build_vector_lookup(vectors);
217
218        // Schema-driven extraction - inline logic, no hidden functions
219        for extractor in &mut self.field_extractors {
220            match extractor {
221                FieldExtractor::Id(builder) => {
222                    if let Some(id) = &id {
223                        match &id.point_id_options {
224                            Some(point_id::PointIdOptions::Num(n)) => {
225                                builder.append_value(n.to_string());
226                            }
227                            Some(point_id::PointIdOptions::Uuid(s)) => builder.append_value(s),
228                            None => builder.append_value(""),
229                        }
230                    } else {
231                        builder.append_null();
232                    }
233                }
234
235                FieldExtractor::Payload(builder) => {
236                    if !payload.is_empty()
237                        && let Ok(json) = serde_json::to_string(&payload)
238                    {
239                        builder.append_value(json);
240                    } else {
241                        builder.append_null();
242                    }
243                }
244
245                FieldExtractor::DenseVector { name, builder } => {
246                    if let Some(Vector::Dense(data)) = vector_lookup.get(name) {
247                        builder.values().append_slice(data);
248                        builder.append(true);
249                    } else {
250                        builder.append(false);
251                    }
252                }
253
254                FieldExtractor::MultiVector { name, builder } => {
255                    if let Some(Vector::MultiDense(vectors)) = vector_lookup.get(name) {
256                        for vector in vectors {
257                            builder.values().values().append_slice(vector);
258                            builder.values().append(true);
259                        }
260                        builder.append(true);
261                    } else {
262                        builder.append(false);
263                    }
264                }
265
266                FieldExtractor::SparseIndices { name, builder } => {
267                    let sparse_name = name.trim_end_matches("_indices");
268                    if let Some(Vector::Sparse(sparse)) = vector_lookup.get(sparse_name) {
269                        builder.values().append_slice(&sparse.indices);
270                        builder.append(true);
271                    } else {
272                        builder.append(false);
273                    }
274                }
275
276                FieldExtractor::SparseValues { name, builder } => {
277                    let sparse_name = name.trim_end_matches("_values");
278                    if let Some(Vector::Sparse(sparse)) = vector_lookup.get(sparse_name) {
279                        builder.values().append_slice(&sparse.values);
280                        builder.append(true);
281                    } else {
282                        builder.append(false);
283                    }
284                }
285            }
286        }
287    }
288
289    /// Finish building and create the final `RecordBatch`
290    ///
291    /// # Errors
292    /// - Returns an error if `RecordBatch` creation fails.
293    pub fn finish(self) -> DataFusionResult<RecordBatch> {
294        let mut arrays: Vec<ArrayRef> = Vec::with_capacity(self.schema.fields().len());
295
296        // Extract arrays from field extractors in schema order
297        for extractor in self.field_extractors {
298            let array: ArrayRef = match extractor {
299                FieldExtractor::Id(mut builder) | FieldExtractor::Payload(mut builder) => {
300                    Arc::new(builder.finish())
301                }
302                FieldExtractor::DenseVector { mut builder, .. }
303                | FieldExtractor::SparseValues { mut builder, .. } => Arc::new(builder.finish()),
304                FieldExtractor::MultiVector { mut builder, .. } => Arc::new(builder.finish()),
305                FieldExtractor::SparseIndices { mut builder, .. } => Arc::new(builder.finish()),
306            };
307            arrays.push(array);
308        }
309
310        RecordBatch::try_new(self.schema, arrays)
311            .map_err(|e| DataFusionError::ArrowError(Box::new(e), None))
312    }
313}
314
315/// Simple helper - builds flat lookup map once per point
316fn build_vector_lookup(vectors: Option<VectorsOutput>) -> HashMap<String, Vector> {
317    let mut lookup = HashMap::new();
318
319    if let Some(vectors) = vectors {
320        match vectors.vectors_options {
321            Some(vectors_output::VectorsOptions::Vector(vector_output)) => {
322                // Unnamed case - use "vector" as key
323                if let Some(content) = Vector::from_vector_output(vector_output) {
324                    drop(lookup.insert("vector".to_string(), content));
325                }
326            }
327            Some(vectors_output::VectorsOptions::Vectors(named_vectors)) => {
328                // Named case - use actual names
329                for (name, vector_output) in named_vectors.vectors {
330                    if let Some(content) = Vector::from_vector_output(vector_output) {
331                        drop(lookup.insert(name, content));
332                    }
333                }
334            }
335            None => {}
336        }
337    }
338
339    lookup
340}
341
342#[cfg(test)]
343mod tests {
344    use super::*;
345
346    #[test]
347    fn test_convert_to_multi_vector_error() {
348        // Test the error path when data length is not divisible by vectors count
349        let data = vec![1.0, 2.0, 3.0]; // length = 3
350        let vectors_count = 2; // 3 % 2 != 0
351
352        let result = convert_to_multi_vector(&data, vectors_count);
353
354        assert!(result.is_err());
355        if let Err(DataFusionError::External(boxed_error)) = result {
356            let error_msg = boxed_error.to_string();
357            assert!(error_msg.contains("Malformed multi vector"));
358            assert!(error_msg.contains("data length 3 is not divisible by vectors count 2"));
359        } else {
360            panic!("Expected DataFusionError::External");
361        }
362    }
363
364    #[test]
365    fn test_vector_from_new_format() {
366        use qdrant_client::qdrant::{DenseVector, MultiDenseVector, SparseVector, vector_output};
367
368        // Test newer format dense vector (lines 55-59)
369        let dense_vector_output = VectorOutput {
370            vector:        Some(vector_output::Vector::Dense(DenseVector {
371                data: vec![1.0, 2.0, 3.0],
372            })),
373            data:          vec![], // Should be ignored when vector.is_some()
374            indices:       None,
375            vectors_count: None,
376        };
377
378        let result = Vector::from_vector_output(dense_vector_output);
379        if let Some(Vector::Dense(data)) = result {
380            assert_eq!(data, vec![1.0, 2.0, 3.0]);
381        } else {
382            panic!("Expected Dense vector");
383        }
384
385        // Test newer format sparse vector
386        let sparse_vector_output = VectorOutput {
387            vector:        Some(vector_output::Vector::Sparse(SparseVector {
388                indices: vec![0, 2, 5],
389                values:  vec![0.1, 0.2, 0.3],
390            })),
391            data:          vec![], // Should be ignored
392            indices:       None,
393            vectors_count: None,
394        };
395
396        let result = Vector::from_vector_output(sparse_vector_output);
397        if let Some(Vector::Sparse(sparse)) = result {
398            assert_eq!(sparse.indices, vec![0, 2, 5]);
399            assert_eq!(sparse.values, vec![0.1, 0.2, 0.3]);
400        } else {
401            panic!("Expected Sparse vector");
402        }
403
404        // Test newer format multi-dense vector
405        let multi_vector_output = VectorOutput {
406            vector:        Some(vector_output::Vector::MultiDense(MultiDenseVector {
407                vectors: vec![DenseVector { data: vec![1.0, 2.0] }, DenseVector {
408                    data: vec![3.0, 4.0],
409                }],
410            })),
411            data:          vec![], // Should be ignored
412            indices:       None,
413            vectors_count: None,
414        };
415
416        let result = Vector::from_vector_output(multi_vector_output);
417        if let Some(Vector::MultiDense(multi)) = result {
418            assert_eq!(multi, vec![vec![1.0, 2.0], vec![3.0, 4.0]]);
419        } else {
420            panic!("Expected MultiDense vector");
421        }
422    }
423}