qail-qdrant 0.14.13

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 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(())
    }
}

#[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);
    }
}