1use std::collections::HashMap;
4
5use async_trait::async_trait;
6use serde::Deserialize;
7
8use crate::{Document, Embeddings, SearchResult, VectorStore, VectorStoreError};
9
10pub struct PineconeStore {
12 api_key: String,
13 host: String,
14 client: reqwest::Client,
15}
16
17impl PineconeStore {
18 pub fn new(api_key: impl Into<String>, host: impl Into<String>) -> Self {
22 Self {
23 api_key: api_key.into(),
24 host: host.into(),
25 client: reqwest::Client::new(),
26 }
27 }
28
29 pub fn build_upsert_body(docs: &[Document], vectors: &[Vec<f32>]) -> serde_json::Value {
31 let vectors_json: Vec<serde_json::Value> = docs
32 .iter()
33 .zip(vectors.iter())
34 .map(|(doc, vec)| {
35 serde_json::json!({
36 "id": doc.id.clone().unwrap_or_else(|| uuid::Uuid::new_v4().to_string()),
37 "values": vec,
38 "metadata": doc.metadata,
39 })
40 })
41 .collect();
42 serde_json::json!({ "vectors": vectors_json })
43 }
44
45 pub fn build_query_body(query_vec: &[f32], top_k: usize) -> serde_json::Value {
47 serde_json::json!({
48 "vector": query_vec,
49 "topK": top_k,
50 "includeMetadata": true,
51 })
52 }
53
54 pub async fn upsert(
56 &self,
57 docs: &[Document],
58 embeddings: &dyn Embeddings,
59 ) -> Result<(), VectorStoreError> {
60 let texts: Vec<&str> = docs.iter().map(|d| d.content.as_str()).collect();
61 let vectors = embeddings
62 .embed_documents(&texts)
63 .await
64 .map_err(|e| VectorStoreError::EmbeddingError(e.to_string()))?;
65 let body = Self::build_upsert_body(docs, &vectors);
66 let url = format!("{}/vectors/upsert", self.host);
67 let resp = self
68 .client
69 .post(&url)
70 .header("Api-Key", &self.api_key)
71 .json(&body)
72 .send()
73 .await
74 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
75 if !resp.status().is_success() {
76 return Err(VectorStoreError::ConnectionError(format!(
77 "Pinecone upsert error: {}",
78 resp.status()
79 )));
80 }
81 Ok(())
82 }
83
84 pub async fn query(
86 &self,
87 query_vec: Vec<f32>,
88 top_k: usize,
89 ) -> Result<Vec<Document>, VectorStoreError> {
90 let body = Self::build_query_body(&query_vec, top_k);
91 let url = format!("{}/query", self.host);
92 let resp = self
93 .client
94 .post(&url)
95 .header("Api-Key", &self.api_key)
96 .json(&body)
97 .send()
98 .await
99 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
100 if !resp.status().is_success() {
101 return Err(VectorStoreError::ConnectionError(format!(
102 "Pinecone query error: {}",
103 resp.status()
104 )));
105 }
106 let query_resp: QueryResponse = resp
107 .json()
108 .await
109 .map_err(|e| VectorStoreError::StorageError(e.to_string()))?;
110 let result = query_resp
111 .matches
112 .into_iter()
113 .map(|m| {
114 let content = m
115 .metadata
116 .as_ref()
117 .and_then(|md| md.get("content").and_then(|v| v.as_str()))
118 .unwrap_or_default()
119 .to_string();
120 Document {
121 content,
122 metadata: m.metadata.unwrap_or_default(),
123 id: Some(m.id),
124 }
125 })
126 .collect();
127 Ok(result)
128 }
129
130 pub async fn describe_index_stats(&self) -> Result<PineconeIndexStats, VectorStoreError> {
134 let url = format!("{}/describe_index_stats", self.host);
135 let resp = self
136 .client
137 .post(&url)
138 .header("Api-Key", &self.api_key)
139 .send()
140 .await
141 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
142 if !resp.status().is_success() {
143 return Err(VectorStoreError::ConnectionError(format!(
144 "Pinecone describe_index_stats error: {}",
145 resp.status()
146 )));
147 }
148 resp.json()
149 .await
150 .map_err(|e| VectorStoreError::StorageError(e.to_string()))
151 }
152
153 pub async fn delete(&self, ids: &[String]) -> Result<(), VectorStoreError> {
155 let url = format!("{}/vectors/delete", self.host);
156 let body = serde_json::json!({ "ids": ids });
157 let resp = self
158 .client
159 .post(&url)
160 .header("Api-Key", &self.api_key)
161 .json(&body)
162 .send()
163 .await
164 .map_err(|e| VectorStoreError::ConnectionError(e.to_string()))?;
165 if !resp.status().is_success() {
166 return Err(VectorStoreError::ConnectionError(format!(
167 "Pinecone delete error: {}",
168 resp.status()
169 )));
170 }
171 Ok(())
172 }
173}
174
175#[async_trait]
176impl VectorStore for PineconeStore {
177 async fn add_documents(
178 &self,
179 documents: Vec<Document>,
180 embeddings: Vec<Vec<f32>>,
181 ) -> Result<Vec<String>, VectorStoreError> {
182 let ids: Vec<String> = documents
183 .iter()
184 .map(|d| {
185 d.id.clone()
186 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
187 })
188 .collect();
189
190 let body = Self::build_upsert_body(&documents, &embeddings);
192 let url = format!("{}/vectors/upsert", self.host);
193 let resp = self
194 .client
195 .post(&url)
196 .header("Api-Key", &self.api_key)
197 .json(&body)
198 .send()
199 .await
200 .map_err(|e| {
201 VectorStoreError::StorageError(format!("Pinecone upsert failed: {}", e))
202 })?;
203
204 if !resp.status().is_success() {
205 return Err(VectorStoreError::StorageError(format!(
206 "Pinecone upsert HTTP error: {}",
207 resp.status()
208 )));
209 }
210
211 Ok(ids)
212 }
213
214 async fn similarity_search(
215 &self,
216 query_embedding: &[f32],
217 k: usize,
218 ) -> Result<Vec<SearchResult>, VectorStoreError> {
219 let body = Self::build_query_body(query_embedding, k);
220 let url = format!("{}/query", self.host);
221 let resp = self
222 .client
223 .post(&url)
224 .header("Api-Key", &self.api_key)
225 .json(&body)
226 .send()
227 .await
228 .map_err(|e| VectorStoreError::StorageError(format!("Pinecone query failed: {}", e)))?;
229
230 if !resp.status().is_success() {
231 return Err(VectorStoreError::StorageError(format!(
232 "Pinecone query HTTP error: {}",
233 resp.status()
234 )));
235 }
236
237 let query_resp: QueryResponse = resp.json().await.map_err(|e| {
238 VectorStoreError::StorageError(format!("Pinecone query parse error: {}", e))
239 })?;
240
241 let results = query_resp
242 .matches
243 .into_iter()
244 .map(|m| {
245 let content = m
246 .metadata
247 .as_ref()
248 .and_then(|md| md.get("content").and_then(|v| v.as_str()))
249 .unwrap_or_default()
250 .to_string();
251 let doc = Document {
252 content,
253 metadata: m.metadata.unwrap_or_default(),
254 id: Some(m.id.clone()),
255 };
256 SearchResult {
257 document: doc,
258 score: m.score as f32,
259 }
260 })
261 .collect();
262
263 Ok(results)
264 }
265
266 async fn get_document(&self, _id: &str) -> Result<Option<Document>, VectorStoreError> {
267 Err(VectorStoreError::StorageError(
270 "Pinecone does not support direct document fetch by ID via HTTP API".to_string(),
271 ))
272 }
273
274 async fn get_embedding(&self, _id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
275 Err(VectorStoreError::StorageError(
276 "Pinecone does not support direct embedding fetch by ID via HTTP API".to_string(),
277 ))
278 }
279
280 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
281 self.delete(&[id.to_string()]).await
282 }
283
284 async fn count(&self) -> usize {
285 match self.describe_index_stats().await {
288 Ok(stats) => stats.total_vector_count,
289 Err(e) => {
290 log::warn!("Pinecone count failed, treating as 0: {}", e);
291 0
292 }
293 }
294 }
295
296 async fn clear(&self) -> Result<(), VectorStoreError> {
297 Err(VectorStoreError::StorageError(
298 "Pinecone does not support clearing all vectors via HTTP API. Delete by namespace or IDs instead.".to_string()
299 ))
300 }
301}
302
303#[derive(Deserialize)]
304struct QueryResponse {
305 matches: Vec<QueryMatch>,
306}
307
308#[derive(Deserialize)]
309struct QueryMatch {
310 id: String,
311 score: f64,
312 metadata: Option<HashMap<String, serde_json::Value>>,
313}
314
315#[derive(Deserialize)]
320pub struct PineconeIndexStats {
321 #[serde(default)]
323 pub total_vector_count: usize,
324}
325
326#[cfg(test)]
327mod tests {
328 use super::*;
329
330 fn doc(id: &str, content: &str) -> Document {
331 Document {
332 content: content.to_string(),
333 metadata: HashMap::new(),
334 id: Some(id.to_string()),
335 }
336 }
337
338 #[test]
339 fn test_build_upsert_body() {
340 let docs = vec![doc("1", "hello"), doc("2", "world")];
341 let vectors = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
342 let body = PineconeStore::build_upsert_body(&docs, &vectors);
343 let vectors_arr = body.get("vectors").unwrap().as_array().unwrap();
344 assert_eq!(vectors_arr.len(), 2);
345 assert_eq!(vectors_arr[0]["id"], "1");
346 assert_eq!(vectors_arr[0]["values"][0], 1.0);
347 }
348
349 #[test]
350 fn test_build_upsert_body_generates_id_if_missing() {
351 let mut d = doc("", "x");
352 d.id = None;
353 let body = PineconeStore::build_upsert_body(&[d], &[vec![0.1]]);
354 let id = body["vectors"][0]["id"].as_str().unwrap();
355 assert!(!id.is_empty());
356 }
357
358 #[test]
359 fn test_build_query_body() {
360 let body = PineconeStore::build_query_body(&[1.0, 2.0, 3.0], 5);
361 assert_eq!(body["topK"], 5);
362 assert_eq!(body["includeMetadata"], true);
363 assert_eq!(body["vector"][2], 3.0);
364 }
365
366 #[test]
367 fn test_new() {
368 let store = PineconeStore::new("key", "https://index.svc.env.pinecone.io");
369 assert_eq!(store.host, "https://index.svc.env.pinecone.io");
370 }
371}