qail-qdrant 0.15.0

QAIL driver for Qdrant vector database
Documentation
//! gRPC-based Qdrant driver with zero-copy encoding.
//!
//! This driver uses the proto_encoder for direct protobuf encoding
//! and grpc_transport for HTTP/2 communication, achieving performance
//! that matches or exceeds the official qdrant-client.

use bytes::BytesMut;
use qail_core::ast::Qail;

use crate::error::{QdrantError, QdrantResult};
use crate::transport::GrpcClient;
use crate::point::{Point, ScoredPoint};
use crate::decoder;
use crate::encoder;

/// High-performance gRPC driver for Qdrant.
///
/// Uses gRPC/HTTP2 with zero-copy protobuf encoding:
/// - Encodes protobuf directly with pre-computed headers
/// - Reuses buffers to minimize allocations
/// - Uses memcpy for vector data (no per-element loop)
///
/// # Example
/// ```ignore
/// use qail_qdrant::QdrantDriver;
/// use qail_core::prelude::*;
///
/// let driver = QdrantDriver::connect("localhost", 6334).await?;
///
/// let results = driver.search(
///     "products",
///     &embedding,
///     10,
///     Some(0.5),
/// ).await?;
/// ```
pub struct QdrantDriver {
    /// gRPC client for HTTP/2 transport
    client: GrpcClient,
    /// Reusable encoding buffer
    buffer: BytesMut,
}

impl QdrantDriver {
    /// Connect to Qdrant gRPC endpoint (default port 6334).
    pub async fn connect(host: &str, port: u16) -> QdrantResult<Self> {
        let client = GrpcClient::connect(host, port).await?;
        Ok(Self {
            client,
            buffer: BytesMut::with_capacity(8192),
        })
    }

    /// Connect with address string.
    pub async fn connect_addr(addr: &str) -> QdrantResult<Self> {
        let parts: Vec<&str> = addr.split(':').collect();
        if parts.len() != 2 {
            return Err(QdrantError::Connection(
                "Invalid address format, expected host:port".to_string(),
            ));
        }
        let port: u16 = parts[1]
            .parse()
            .map_err(|_| QdrantError::Connection("Invalid port".to_string()))?;
        Self::connect(parts[0], port).await
    }

    /// Vector similarity search with zero-copy encoding.
    ///
    /// # Arguments
    /// * `collection` - Collection name
    /// * `vector` - Query vector
    /// * `limit` - Max results
    /// * `score_threshold` - Optional minimum score
    ///
    /// # Performance
    /// Vector is encoded via memcpy (no per-element serialization).
    pub async fn search(
        &mut self,
        collection: &str,
        vector: &[f32],
        limit: u64,
        score_threshold: Option<f32>,
    ) -> QdrantResult<Vec<ScoredPoint>> {
        // Clear buffer for reuse
        self.buffer.clear();
        
        // Encode request using zero-copy encoder
        encoder::encode_search_proto(
            &mut self.buffer,
            collection,
            vector,
            limit,
            score_threshold,
            None,
        );

        // Send via gRPC (split to avoid clone - zero allocation!)
        let request_bytes = self.buffer.split().freeze();
        let response = self.client.search(request_bytes).await?;

        // Decode response using zero-copy decoder
        decoder::decode_search_response(&response)
    }

    /// Search multiple vectors concurrently using HTTP/2 pipelining.
    ///
    /// This sends all requests concurrently over a single h2 connection,
    /// achieving 2-3x speedup compared to sequential searches.
    ///
    /// # Example
    /// ```ignore
    /// let vectors = vec![vec1, vec2, vec3];
    /// let results = driver.search_batch("products", &vectors, 10, None).await?;
    /// ```
    pub async fn search_batch(
        &mut self,
        collection: &str,
        vectors: &[Vec<f32>],
        limit: u64,
        score_threshold: Option<f32>,
    ) -> QdrantResult<Vec<Vec<ScoredPoint>>> {
        use futures_util::future::join_all;
        
        // Encode all requests first (reuse buffer for each)
        let mut encoded_requests = Vec::with_capacity(vectors.len());
        
        for vector in vectors {
            self.buffer.clear();
            encoder::encode_search_proto(
                &mut self.buffer,
                collection,
                vector,
                limit,
                score_threshold,
                None,
            );
            encoded_requests.push(self.buffer.split().freeze());
        }

        // Send all requests concurrently using HTTP/2 multiplexing
        let mut futures = Vec::with_capacity(encoded_requests.len());
        for request in encoded_requests {
            futures.push(self.client.search(request));
        }

        // Wait for all responses
        let responses = join_all(futures).await;

        // Decode all responses
        let mut results = Vec::with_capacity(responses.len());
        for response in responses {
            let decoded = decoder::decode_search_response(&response?)?;
            results.push(decoded);
        }

        Ok(results)
    }
    /// Search using QAIL AST.
    ///
    /// Extracts vector, collection, limit from the Qail command.
    pub async fn search_ast(&mut self, cmd: &Qail) -> QdrantResult<Vec<ScoredPoint>> {
        let collection = if cmd.table.is_empty() {
            return Err(QdrantError::Encode("Collection name required".to_string()));
        } else {
            &cmd.table
        };

        let vector = cmd.vector.as_ref().ok_or_else(|| {
            QdrantError::Encode("Vector required for search".to_string())
        })?;

        // Extract limit from cages (default 10)
        let mut limit = 10u64;
        for cage in &cmd.cages {
            if let qail_core::ast::CageKind::Limit(n) = cage.kind {
                limit = n as u64;
            }
        }

        let score_threshold = cmd.score_threshold;

        self.search(collection, vector, limit, score_threshold).await
    }

    /// Upsert points with zero-copy encoding.
    pub async fn upsert(
        &mut self,
        collection: &str,
        points: &[Point],
        wait: bool,
    ) -> QdrantResult<()> {
        // Clear buffer for reuse
        self.buffer.clear();
        
        // Encode request using zero-copy encoder
        encoder::encode_upsert_proto(&mut self.buffer, collection, points, wait);

        // Send via gRPC (split to avoid clone)
        let request_bytes = self.buffer.split().freeze();
        let _response = self.client.upsert(request_bytes).await?;
        Ok(())
    }

    /// Create a collection with specific vector parameters.
    pub async fn create_collection(
        &mut self,
        collection_name: &str,
        vector_size: u64,
        distance: qail_core::ast::Distance,
        on_disk: bool,
    ) -> QdrantResult<()> {
        self.buffer.clear();
        encoder::encode_create_collection_proto(
            &mut self.buffer,
            collection_name,
            vector_size,
            distance,
            on_disk,
        );
        let request = self.buffer.split().freeze();
        self.client.create_collection(request).await?;
        Ok(())
    }

    /// Delete a collection.
    pub async fn delete_collection(&mut self, collection_name: &str) -> QdrantResult<()> {
        self.buffer.clear();
        encoder::encode_delete_collection_proto(&mut self.buffer, collection_name);
        let request = self.buffer.split().freeze();
        self.client.delete_collection(request).await?;
        Ok(())
    }

    /// Delete points by ID from a collection.
    pub async fn delete_points(
        &mut self,
        collection_name: &str,
        point_ids: &[u64],
    ) -> QdrantResult<()> {
        self.buffer.clear();
        encoder::encode_delete_points_proto(&mut self.buffer, collection_name, point_ids);
        let request = self.buffer.split().freeze();
        self.client.delete(request).await?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_grpc_driver_struct() {
        // Verify struct is constructible
        let buffer = BytesMut::with_capacity(1024);
        assert!(buffer.capacity() >= 1024);
    }
}