qail_qdrant/encoder.rs
1//! Zero-copy Protobuf encoder for Qdrant gRPC protocol.
2//!
3//! This module implements direct wire format encoding without intermediate
4//! struct allocations. Key optimizations:
5//! - Pre-computed field tag bytes
6//! - Buffer reuse via BytesMut
7//! - Direct memcpy for vectors (no per-element loop)
8
9use bytes::{BufMut, BytesMut};
10
11// ============================================================================
12// Protobuf Wire Type Constants
13// ============================================================================
14
15/// Wire type for varints (int32, int64, uint32, uint64, bool, enum)
16#[allow(dead_code)]
17const WIRE_VARINT: u8 = 0;
18/// Wire type for length-delimited (string, bytes, embedded messages, packed repeated)
19#[allow(dead_code)]
20const WIRE_LEN: u8 = 2;
21/// Wire type for 32-bit fixed (float, fixed32)
22#[allow(dead_code)]
23const WIRE_FIXED32: u8 = 5;
24
25// ============================================================================
26// SearchPoints Field Tags (pre-computed)
27// ============================================================================
28// Tag = (field_number << 3) | wire_type
29
30/// Field 1: collection_name (string) -> (1 << 3) | 2 = 0x0A
31const SEARCH_COLLECTION: u8 = 0x0A;
32/// Field 2: vector (repeated float, packed) -> (2 << 3) | 2 = 0x12
33const SEARCH_VECTOR: u8 = 0x12;
34/// Field 3: filter (message) -> (3 << 3) | 2 = 0x1A
35#[allow(dead_code)]
36const SEARCH_FILTER: u8 = 0x1A;
37/// Field 4: limit (uint64) -> (4 << 3) | 0 = 0x20
38const SEARCH_LIMIT: u8 = 0x20;
39/// Field 6: with_payload (message) -> (6 << 3) | 2 = 0x32
40const SEARCH_WITH_PAYLOAD: u8 = 0x32;
41/// Field 8: score_threshold (float) -> (8 << 3) | 5 = 0x45
42const SEARCH_SCORE_THRESHOLD: u8 = 0x45;
43/// Field 10: vector_name (string) -> (10 << 3) | 2 = 0x52
44const SEARCH_VECTOR_NAME: u8 = 0x52;
45
46// ============================================================================
47// UpsertPoints Field Tags
48// ============================================================================
49
50/// Field 1: collection_name (string) -> 0x0A
51const UPSERT_COLLECTION: u8 = 0x0A;
52/// Field 2: wait (bool) -> (2 << 3) | 0 = 0x10
53const UPSERT_WAIT: u8 = 0x10;
54/// Field 3: points (repeated PointStruct) -> (3 << 3) | 2 = 0x1A
55const UPSERT_POINTS: u8 = 0x1A;
56
57// ============================================================================
58// PointStruct Field Tags
59// ============================================================================
60
61/// Field 1: id (PointId) -> 0x0A
62const POINT_ID: u8 = 0x0A;
63/// Field 4: vectors (Vectors) -> (4 << 3) | 2 = 0x22 (field 2 is deprecated)
64const POINT_VECTORS: u8 = 0x22;
65/// Field 3: payload (map) -> (3 << 3) | 2 = 0x1A
66#[allow(dead_code)]
67const POINT_PAYLOAD: u8 = 0x1A;
68
69// ============================================================================
70// PointId Field Tags
71// ============================================================================
72
73/// Field 1: num (uint64) -> (1 << 3) | 0 = 0x08
74const POINT_ID_NUM: u8 = 0x08;
75/// Field 2: uuid (string) -> (2 << 3) | 2 = 0x12
76const POINT_ID_UUID: u8 = 0x12;
77
78// ============================================================================
79// Varint Encoding
80// ============================================================================
81
82/// Encode a varint (variable-length integer) into the buffer.
83/// Uses 7 bits per byte, MSB indicates continuation.
84#[inline]
85pub fn encode_varint(buf: &mut BytesMut, mut value: usize) {
86 loop {
87 let byte = (value & 0x7F) as u8;
88 value >>= 7;
89 if value == 0 {
90 buf.put_u8(byte);
91 break;
92 } else {
93 buf.put_u8(byte | 0x80);
94 }
95 }
96}
97
98/// Encode a u64 varint.
99#[inline]
100pub fn encode_varint_u64(buf: &mut BytesMut, mut value: u64) {
101 loop {
102 let byte = (value & 0x7F) as u8;
103 value >>= 7;
104 if value == 0 {
105 buf.put_u8(byte);
106 break;
107 } else {
108 buf.put_u8(byte | 0x80);
109 }
110 }
111}
112
113// ============================================================================
114// SearchPoints Encoder
115// ============================================================================
116
117/// Encode a SearchPoints request directly to protobuf wire format.
118///
119/// # Arguments
120/// * `buf` - Reusable buffer (cleared before writing)
121/// * `collection` - Collection name
122/// * `vector` - Query vector (directly memcpy'd)
123/// * `limit` - Max results
124/// * `score_threshold` - Optional minimum score
125/// * `vector_name` - Optional named vector field
126///
127/// # Zero-Copy Optimization
128/// The vector is written via direct memory copy, avoiding per-element encoding.
129pub fn encode_search_proto(
130 buf: &mut BytesMut,
131 collection: &str,
132 vector: &[f32],
133 limit: u64,
134 score_threshold: Option<f32>,
135 vector_name: Option<&str>,
136) {
137 buf.clear();
138
139 // Field 1: collection_name (string)
140 buf.put_u8(SEARCH_COLLECTION);
141 encode_varint(buf, collection.len());
142 buf.extend_from_slice(collection.as_bytes());
143
144 // Field 2: vector (packed repeated float)
145 // This is the key optimization - direct memcpy of float bytes!
146 buf.put_u8(SEARCH_VECTOR);
147 let vector_bytes_len = vector.len() * 4; // f32 = 4 bytes
148 encode_varint(buf, vector_bytes_len);
149
150 // ZERO-COPY: Write floats directly as bytes
151 // Safe because f32 is 4 bytes on all platforms
152 let float_bytes = unsafe {
153 std::slice::from_raw_parts(vector.as_ptr() as *const u8, vector_bytes_len)
154 };
155 buf.extend_from_slice(float_bytes);
156
157 // Field 4: limit (varint)
158 buf.put_u8(SEARCH_LIMIT);
159 encode_varint_u64(buf, limit);
160
161 // Field 8: score_threshold (float, optional)
162 if let Some(threshold) = score_threshold {
163 buf.put_u8(SEARCH_SCORE_THRESHOLD);
164 buf.put_f32_le(threshold);
165 }
166
167 // Field 10: vector_name (string, optional)
168 if let Some(name) = vector_name {
169 buf.put_u8(SEARCH_VECTOR_NAME);
170 encode_varint(buf, name.len());
171 buf.extend_from_slice(name.as_bytes());
172 }
173}
174
175/// Encode with_payload = true as a sub-message.
176pub fn encode_with_payload_true(buf: &mut BytesMut) {
177 // WithPayloadSelector { enable = true }
178 // Field 1: enable (bool) = 0x08, value = 1
179 buf.put_u8(SEARCH_WITH_PAYLOAD);
180 encode_varint(buf, 2); // submessage length
181 buf.put_u8(0x08); // field 1, varint
182 buf.put_u8(0x01); // true
183}
184
185// ============================================================================
186// UpsertPoints Encoder
187// ============================================================================
188
189/// Encode an UpsertPoints request to protobuf wire format.
190pub fn encode_upsert_proto(
191 buf: &mut BytesMut,
192 collection: &str,
193 points: &[crate::Point],
194 wait: bool,
195) {
196 buf.clear();
197
198 // Field 1: collection_name
199 buf.put_u8(UPSERT_COLLECTION);
200 encode_varint(buf, collection.len());
201 buf.extend_from_slice(collection.as_bytes());
202
203 // Field 2: wait (bool)
204 if wait {
205 buf.put_u8(UPSERT_WAIT);
206 buf.put_u8(0x01);
207 }
208
209 // Field 3: points (repeated PointStruct)
210 for point in points {
211 encode_point_struct(buf, point);
212 }
213}
214
215/// Encode a single PointStruct.
216fn encode_point_struct(buf: &mut BytesMut, point: &crate::Point) {
217 // We need to encode into a temp buffer first to get length,
218 // since PointStruct is length-delimited
219 let mut point_buf = BytesMut::with_capacity(point.vector.len() * 4 + 64);
220
221 // Field 1: id (PointId oneof)
222 match &point.id {
223 crate::PointId::Num(n) => {
224 // Nested message: PointId { num: n }
225 point_buf.put_u8(POINT_ID);
226 let id_len = 1 + varint_len(*n); // tag + varint
227 encode_varint(&mut point_buf, id_len);
228 point_buf.put_u8(POINT_ID_NUM);
229 encode_varint_u64(&mut point_buf, *n);
230 }
231 crate::PointId::Uuid(s) => {
232 point_buf.put_u8(POINT_ID);
233 let id_len = 1 + varint_len(s.len() as u64) + s.len();
234 encode_varint(&mut point_buf, id_len);
235 point_buf.put_u8(POINT_ID_UUID);
236 encode_varint(&mut point_buf, s.len());
237 point_buf.extend_from_slice(s.as_bytes());
238 }
239 }
240
241 // Field 4: vectors (Vectors -> Vector)
242 // Using the simpler deprecated approach: Vector.data = repeated float (field 1)
243 // This encodes as packed floats
244 let vector_bytes_len = point.vector.len() * 4;
245
246 // Vector message: field 1 = data (packed floats)
247 // Tag for field 1, wire type 2 (length-delimited) = 0x0A
248 let vector_inner_len = 1 + varint_len(vector_bytes_len as u64) + vector_bytes_len;
249
250 // Vectors message: field 1 = vector (Vector message)
251 // Tag for field 1, wire type 2 (length-delimited) = 0x0A
252 let vectors_len = 1 + varint_len(vector_inner_len as u64) + vector_inner_len;
253
254 // Write Vectors field (field 4 of PointStruct)
255 point_buf.put_u8(POINT_VECTORS); // 0x22 = field 4, wire type 2
256 encode_varint(&mut point_buf, vectors_len);
257
258 // Vectors.vector (field 1)
259 point_buf.put_u8(0x0A); // field 1, wire type 2
260 encode_varint(&mut point_buf, vector_inner_len);
261
262 // Vector.data (field 1, packed floats) - deprecated but still works
263 point_buf.put_u8(0x0A); // field 1, wire type 2
264 encode_varint(&mut point_buf, vector_bytes_len);
265 let float_bytes = unsafe {
266 std::slice::from_raw_parts(point.vector.as_ptr() as *const u8, vector_bytes_len)
267 };
268 point_buf.extend_from_slice(float_bytes);
269
270 // TODO: Field 3: payload (map) - skipped for now
271
272 // Write to main buffer with length prefix
273 buf.put_u8(UPSERT_POINTS);
274 encode_varint(buf, point_buf.len());
275 buf.extend_from_slice(&point_buf);
276}
277
278// ============================================================================
279// CreateCollection Field Tags
280// ============================================================================
281
282/// Field 1: collection_name (string) -> 0x0A
283const CREATE_COLLECTION_NAME: u8 = 0x0A;
284/// Field 10: vectors_config (VectorsConfig) -> (10 << 3) | 2 = 0x52
285const CREATE_VECTORS_CONFIG: u8 = 0x52;
286/// Field 8: on_disk_payload (bool) -> (8 << 3) | 0 = 0x40
287const CREATE_ON_DISK: u8 = 0x40;
288
289// ============================================================================
290// DeleteCollection Field Tags
291// ============================================================================
292
293/// Field 1: collection_name (string) -> 0x0A
294const DELETE_COLLECTION_NAME: u8 = 0x0A;
295
296/// Encode CreateCollection request to protobuf wire format.
297pub fn encode_create_collection_proto(
298 buf: &mut BytesMut,
299 collection_name: &str,
300 vector_size: u64,
301 distance: qail_core::ast::Distance,
302 on_disk: bool,
303) {
304 buf.clear();
305
306 // Field 1: collection_name
307 buf.put_u8(CREATE_COLLECTION_NAME);
308 encode_varint(buf, collection_name.len());
309 buf.extend_from_slice(collection_name.as_bytes());
310
311 // Field 2: vectors_config (VectorsConfig -> VectorParams)
312 // Need to construct nested messages for VectorParams
313 let mut params_buf = BytesMut::with_capacity(32);
314
315 // VectorParams.size (field 1, uint64)
316 params_buf.put_u8(0x08);
317 encode_varint_u64(&mut params_buf, vector_size);
318
319 // VectorParams.distance (field 2, enum)
320 // Per Qdrant proto: UnknownDistance=0, Cosine=1, Euclid=2, Dot=3
321 params_buf.put_u8(0x10);
322 let distance_val = match distance {
323 qail_core::ast::Distance::Cosine => 1,
324 qail_core::ast::Distance::Euclid => 2,
325 qail_core::ast::Distance::Dot => 3,
326 };
327 encode_varint(&mut params_buf, distance_val);
328
329 // VectorParams.on_disk (field 5, bool) - optional but useful
330 if on_disk {
331 params_buf.put_u8(0x28); // field 5
332 params_buf.put_u8(0x01);
333 }
334
335 // VectorsConfig.params (field 1) wraps VectorParams
336 let mut config_buf = BytesMut::with_capacity(params_buf.len() + 4);
337 config_buf.put_u8(0x0A); // field 1
338 encode_varint(&mut config_buf, params_buf.len());
339 config_buf.extend_from_slice(¶ms_buf);
340
341 // Write to main buffer
342 buf.put_u8(CREATE_VECTORS_CONFIG);
343 encode_varint(buf, config_buf.len());
344 buf.extend_from_slice(&config_buf);
345
346 // Field 5: on_disk_payload (bool) - optional
347 if on_disk {
348 buf.put_u8(CREATE_ON_DISK);
349 buf.put_u8(0x01);
350 }
351}
352
353/// Encode DeleteCollection request.
354pub fn encode_delete_collection_proto(buf: &mut BytesMut, collection_name: &str) {
355 buf.clear();
356 buf.put_u8(DELETE_COLLECTION_NAME);
357 encode_varint(buf, collection_name.len());
358 buf.extend_from_slice(collection_name.as_bytes());
359}
360
361/// Calculate the byte length of a varint.
362#[inline]
363fn varint_len(value: u64) -> usize {
364 if value == 0 {
365 1
366 } else {
367 let bits = 64 - value.leading_zeros() as usize;
368 bits.div_ceil(7)
369 }
370}
371
372// ============================================================================
373// Tests
374// ============================================================================
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379
380 #[test]
381 fn test_varint_encoding() {
382 let mut buf = BytesMut::new();
383
384 // Single byte
385 encode_varint(&mut buf, 1);
386 assert_eq!(&buf[..], &[0x01]);
387
388 buf.clear();
389 encode_varint(&mut buf, 127);
390 assert_eq!(&buf[..], &[0x7F]);
391
392 // Two bytes
393 buf.clear();
394 encode_varint(&mut buf, 128);
395 assert_eq!(&buf[..], &[0x80, 0x01]);
396
397 buf.clear();
398 encode_varint(&mut buf, 300);
399 assert_eq!(&buf[..], &[0xAC, 0x02]);
400 }
401
402 #[test]
403 fn test_encode_search_basic() {
404 let mut buf = BytesMut::with_capacity(1024);
405 let vector = vec![0.1f32, 0.2, 0.3, 0.4];
406
407 encode_search_proto(&mut buf, "test_collection", &vector, 10, None, None);
408
409 // Verify starts with collection name field
410 assert_eq!(buf[0], SEARCH_COLLECTION);
411
412 // Verify buffer is not empty
413 assert!(buf.len() > 20);
414 }
415
416 #[test]
417 fn test_zero_copy_vector() {
418 let mut buf = BytesMut::with_capacity(1024);
419 let vector = vec![1.0f32, 2.0, 3.0, 4.0];
420
421 encode_search_proto(&mut buf, "test", &vector, 5, None, None);
422
423 // Find where vector data starts (after collection name + vector tag + length)
424 // collection: 0x0A, len(4), "test" = 6 bytes
425 // vector tag: 0x12 = 1 byte
426 // vector len: 16 (4 floats * 4 bytes) = 1 byte varint
427 // Total header: 8 bytes
428 let vector_start = 8;
429 let vector_bytes = &buf[vector_start..vector_start + 16];
430
431 // Verify floats are correctly encoded as little-endian bytes
432 let float_bytes: [u8; 4] = 1.0f32.to_le_bytes();
433 assert_eq!(&vector_bytes[0..4], &float_bytes);
434 }
435
436 #[test]
437 fn test_varint_len() {
438 assert_eq!(varint_len(0), 1);
439 assert_eq!(varint_len(1), 1);
440 assert_eq!(varint_len(127), 1);
441 assert_eq!(varint_len(128), 2);
442 assert_eq!(varint_len(16383), 2);
443 assert_eq!(varint_len(16384), 3);
444 }
445}
446
447// ============================================================================
448// DeletePoints Encoder
449// ============================================================================
450
451/// Encode a DeletePoints request.
452///
453/// DeletePoints message:
454/// - Field 1: collection_name (string)
455/// - Field 2: wait (bool)
456/// - Field 4: points (PointsSelector message)
457///
458/// PointsSelector (oneof):
459/// - Field 1: points (PointsIdsList message)
460///
461/// PointsIdsList:
462/// - Field 1: ids (repeated PointId)
463pub fn encode_delete_points_proto(
464 buf: &mut BytesMut,
465 collection_name: &str,
466 point_ids: &[u64],
467) {
468 // Pre-calculate sizes for efficient allocation
469
470 // Calculate PointsIdsList size (field 1 repeated PointId)
471 let mut ids_list_size = 0usize;
472 for &id in point_ids {
473 // Each PointId is a message with field 2 (num) as varint
474 // Tag for field 2 (varint): 0x10, then varint value
475 let point_id_inner_size = 1 + varint_len(id);
476 // PointId message: tag 0x0A (field 1, len-delimited) + len + content
477 ids_list_size += 1 + varint_len(point_id_inner_size as u64) + point_id_inner_size;
478 }
479
480 // PointsSelector size: field 1 (points) = tag + len + ids_list_size
481 let selector_inner_size = 1 + varint_len(ids_list_size as u64) + ids_list_size;
482
483 // DeletePoints fields:
484 // Field 1: collection_name
485 let collection_size = 1 + varint_len(collection_name.len() as u64) + collection_name.len();
486 // Field 2: wait = true (tag 0x10, value 1)
487 let wait_size = 2;
488 // Field 4: points (PointsSelector) tag 0x22 + len + content
489 let points_field_size = 1 + varint_len(selector_inner_size as u64) + selector_inner_size;
490
491 let total_size = collection_size + wait_size + points_field_size;
492 buf.reserve(total_size);
493
494 // Field 1: collection_name
495 buf.put_u8(0x0A);
496 encode_varint(buf, collection_name.len());
497 buf.put_slice(collection_name.as_bytes());
498
499 // Field 2: wait = true
500 buf.put_u8(0x10);
501 buf.put_u8(1);
502
503 // Field 4: points (PointsSelector message)
504 buf.put_u8(0x22);
505 encode_varint(buf, selector_inner_size);
506
507 // PointsSelector.points (field 1): PointsIdsList
508 buf.put_u8(0x0A);
509 encode_varint(buf, ids_list_size);
510
511 // PointsIdsList.ids (field 1): repeated PointId
512 for &id in point_ids {
513 let point_id_inner_size = 1 + varint_len(id);
514 buf.put_u8(0x0A); // PointId message tag
515 encode_varint(buf, point_id_inner_size);
516 buf.put_u8(0x10); // PointId.num field tag (field 2, varint)
517 encode_varint(buf, id as usize);
518 }
519}
520