Skip to main content

vectorizer_sdk/rpc/
commands.rs

1//! Typed wrappers around the v1 RPC command catalog.
2//!
3//! Each method in this module corresponds to one entry in the wire
4//! spec's command catalog (§ 6). The wrapper:
5//!
6//! 1. Builds the positional `args` array per the spec.
7//! 2. Calls [`RpcClient::call`].
8//! 3. Decodes the [`VectorizerValue`] response into a typed Rust
9//!    value with explicit field handling (no `serde_json::from_value`
10//!    detour — the wire is MessagePack, not JSON).
11//!
12//! Adding a new typed wrapper for a v1 command landed on the server
13//! is mechanical: a new method on `RpcClient` here, an entry in the
14//! README, and (ideally) a test in `tests/rpc_integration.rs`.
15
16use super::client::{Result, RpcClient, RpcClientError};
17use super::types::VectorizerValue;
18
19/// Collection metadata returned by `collections.get_info`.
20#[derive(Debug, Clone)]
21pub struct CollectionInfo {
22    /// Collection name as registered on the server.
23    pub name: String,
24    /// Number of vectors currently stored.
25    pub vector_count: i64,
26    /// Number of source documents represented by those vectors.
27    pub document_count: i64,
28    /// Vector dimension.
29    pub dimension: i64,
30    /// Distance metric the collection's index uses (e.g. `"Cosine"`).
31    pub metric: String,
32    /// ISO-8601 timestamp of when the collection was created.
33    pub created_at: String,
34    /// ISO-8601 timestamp of the last mutation.
35    pub updated_at: String,
36}
37
38/// One result from `search.basic`.
39#[derive(Debug, Clone)]
40pub struct SearchHit {
41    /// Vector ID inside the collection.
42    pub id: String,
43    /// Similarity score in `[0.0, 1.0]` for cosine; backend-defined
44    /// otherwise.
45    pub score: f64,
46    /// Optional payload as a JSON string. The server stores payloads
47    /// as `serde_json::Value`; the RPC layer ships them as a string
48    /// because the wire `VectorizerValue` enum doesn't model JSON
49    /// directly. Decode with `serde_json::from_str` if you need
50    /// structured access.
51    pub payload: Option<String>,
52}
53
54impl RpcClient {
55    /// `collections.list` — return every collection name visible to
56    /// the authenticated principal.
57    pub async fn list_collections(&self) -> Result<Vec<String>> {
58        let v = self.call("collections.list", vec![]).await?;
59        let arr = v
60            .as_array()
61            .ok_or_else(|| RpcClientError::Server("collections.list: expected Array".into()))?;
62        Ok(arr
63            .iter()
64            .filter_map(|v| v.as_str().map(str::to_owned))
65            .collect())
66    }
67
68    /// `collections.get_info` — return metadata for one collection.
69    pub async fn get_collection_info(&self, name: &str) -> Result<CollectionInfo> {
70        let v = self
71            .call(
72                "collections.get_info",
73                vec![VectorizerValue::Str(name.to_owned())],
74            )
75            .await?;
76        let need_str = |key: &str| -> Result<String> {
77            v.map_get(key)
78                .and_then(|x| x.as_str().map(str::to_owned))
79                .ok_or_else(|| {
80                    RpcClientError::Server(format!(
81                        "collections.get_info: missing string field '{key}'"
82                    ))
83                })
84        };
85        let need_int = |key: &str| -> Result<i64> {
86            v.map_get(key).and_then(|x| x.as_int()).ok_or_else(|| {
87                RpcClientError::Server(format!("collections.get_info: missing int field '{key}'"))
88            })
89        };
90
91        Ok(CollectionInfo {
92            name: need_str("name")?,
93            vector_count: need_int("vector_count")?,
94            document_count: need_int("document_count")?,
95            dimension: need_int("dimension")?,
96            metric: need_str("metric")?,
97            created_at: need_str("created_at")?,
98            updated_at: need_str("updated_at")?,
99        })
100    }
101
102    /// `vectors.get` — fetch one vector by id. Returns the raw
103    /// `VectorizerValue::Map` so callers can read whichever fields
104    /// they care about (`id`, `data`, `payload`, `document_id`).
105    pub async fn get_vector(&self, collection: &str, vector_id: &str) -> Result<VectorizerValue> {
106        self.call(
107            "vectors.get",
108            vec![
109                VectorizerValue::Str(collection.to_owned()),
110                VectorizerValue::Str(vector_id.to_owned()),
111            ],
112        )
113        .await
114    }
115
116    /// `search.basic` — search `collection` for `query` and return up
117    /// to `limit` hits sorted by descending similarity.
118    pub async fn search_basic(
119        &self,
120        collection: &str,
121        query: &str,
122        limit: usize,
123    ) -> Result<Vec<SearchHit>> {
124        let args = vec![
125            VectorizerValue::Str(collection.to_owned()),
126            VectorizerValue::Str(query.to_owned()),
127            VectorizerValue::Int(limit as i64),
128        ];
129        let v = self.call("search.basic", args).await?;
130        let arr = v
131            .as_array()
132            .ok_or_else(|| RpcClientError::Server("search.basic: expected Array".into()))?;
133        let mut hits = Vec::with_capacity(arr.len());
134        for entry in arr {
135            let id = entry
136                .map_get("id")
137                .and_then(|v| v.as_str())
138                .map(str::to_owned)
139                .ok_or_else(|| RpcClientError::Server("search.basic: hit missing 'id'".into()))?;
140            let score = entry
141                .map_get("score")
142                .and_then(|v| v.as_float())
143                .ok_or_else(|| {
144                    RpcClientError::Server("search.basic: hit missing 'score'".into())
145                })?;
146            let payload = entry
147                .map_get("payload")
148                .and_then(|v| v.as_str())
149                .map(str::to_owned);
150            hits.push(SearchHit { id, score, payload });
151        }
152        Ok(hits)
153    }
154}