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<(), String> {
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| 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| e.to_string())?;
75 if !resp.status().is_success() {
76 return Err(format!("Pinecone upsert error: {}", resp.status()));
77 }
78 Ok(())
79 }
80
81 pub async fn query(&self, query_vec: Vec<f32>, top_k: usize) -> Result<Vec<Document>, String> {
83 let body = Self::build_query_body(&query_vec, top_k);
84 let url = format!("{}/query", self.host);
85 let resp = self
86 .client
87 .post(&url)
88 .header("Api-Key", &self.api_key)
89 .json(&body)
90 .send()
91 .await
92 .map_err(|e| e.to_string())?;
93 if !resp.status().is_success() {
94 return Err(format!("Pinecone query error: {}", resp.status()));
95 }
96 let query_resp: QueryResponse = resp.json().await.map_err(|e| e.to_string())?;
97 let result = query_resp
98 .matches
99 .into_iter()
100 .map(|m| {
101 let content = m
102 .metadata
103 .as_ref()
104 .and_then(|md| md.get("content").cloned())
105 .unwrap_or_default();
106 Document {
107 content,
108 metadata: m.metadata.unwrap_or_default(),
109 id: Some(m.id),
110 }
111 })
112 .collect();
113 Ok(result)
114 }
115
116 pub async fn delete(&self, ids: &[String]) -> Result<(), String> {
118 let url = format!("{}/vectors/delete", self.host);
119 let body = serde_json::json!({ "ids": ids });
120 let resp = self
121 .client
122 .post(&url)
123 .header("Api-Key", &self.api_key)
124 .json(&body)
125 .send()
126 .await
127 .map_err(|e| e.to_string())?;
128 if !resp.status().is_success() {
129 return Err(format!("Pinecone delete error: {}", resp.status()));
130 }
131 Ok(())
132 }
133}
134
135#[async_trait]
136impl VectorStore for PineconeStore {
137 async fn add_documents(
138 &self,
139 documents: Vec<Document>,
140 embeddings: Vec<Vec<f32>>,
141 ) -> Result<Vec<String>, VectorStoreError> {
142 let ids: Vec<String> = documents
143 .iter()
144 .map(|d| {
145 d.id.clone()
146 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
147 })
148 .collect();
149
150 let body = Self::build_upsert_body(&documents, &embeddings);
152 let url = format!("{}/vectors/upsert", self.host);
153 let resp = self
154 .client
155 .post(&url)
156 .header("Api-Key", &self.api_key)
157 .json(&body)
158 .send()
159 .await
160 .map_err(|e| {
161 VectorStoreError::StorageError(format!("Pinecone upsert failed: {}", e))
162 })?;
163
164 if !resp.status().is_success() {
165 return Err(VectorStoreError::StorageError(format!(
166 "Pinecone upsert HTTP error: {}",
167 resp.status()
168 )));
169 }
170
171 Ok(ids)
172 }
173
174 async fn similarity_search(
175 &self,
176 query_embedding: &[f32],
177 k: usize,
178 ) -> Result<Vec<SearchResult>, VectorStoreError> {
179 let body = Self::build_query_body(query_embedding, k);
180 let url = format!("{}/query", self.host);
181 let resp = self
182 .client
183 .post(&url)
184 .header("Api-Key", &self.api_key)
185 .json(&body)
186 .send()
187 .await
188 .map_err(|e| VectorStoreError::StorageError(format!("Pinecone query failed: {}", e)))?;
189
190 if !resp.status().is_success() {
191 return Err(VectorStoreError::StorageError(format!(
192 "Pinecone query HTTP error: {}",
193 resp.status()
194 )));
195 }
196
197 let query_resp: QueryResponse = resp.json().await.map_err(|e| {
198 VectorStoreError::StorageError(format!("Pinecone query parse error: {}", e))
199 })?;
200
201 let results = query_resp
202 .matches
203 .into_iter()
204 .map(|m| {
205 let content = m
206 .metadata
207 .as_ref()
208 .and_then(|md| md.get("content").cloned())
209 .unwrap_or_default();
210 let doc = Document {
211 content,
212 metadata: m.metadata.unwrap_or_default(),
213 id: Some(m.id.clone()),
214 };
215 SearchResult {
216 document: doc,
217 score: m.score as f32,
218 }
219 })
220 .collect();
221
222 Ok(results)
223 }
224
225 async fn get_document(&self, _id: &str) -> Result<Option<Document>, VectorStoreError> {
226 Err(VectorStoreError::StorageError(
229 "Pinecone does not support direct document fetch by ID via HTTP API".to_string(),
230 ))
231 }
232
233 async fn get_embedding(&self, _id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
234 Err(VectorStoreError::StorageError(
235 "Pinecone does not support direct embedding fetch by ID via HTTP API".to_string(),
236 ))
237 }
238
239 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
240 self.delete(&[id.to_string()])
241 .await
242 .map_err(VectorStoreError::StorageError)
243 }
244
245 async fn count(&self) -> usize {
246 0
249 }
250
251 async fn clear(&self) -> Result<(), VectorStoreError> {
252 Err(VectorStoreError::StorageError(
253 "Pinecone does not support clearing all vectors via HTTP API. Delete by namespace or IDs instead.".to_string()
254 ))
255 }
256}
257
258#[derive(Deserialize)]
259struct QueryResponse {
260 matches: Vec<QueryMatch>,
261}
262
263#[derive(Deserialize)]
264struct QueryMatch {
265 id: String,
266 #[allow(dead_code)]
267 score: f64,
268 metadata: Option<HashMap<String, String>>,
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 fn doc(id: &str, content: &str) -> Document {
276 Document {
277 content: content.to_string(),
278 metadata: HashMap::new(),
279 id: Some(id.to_string()),
280 }
281 }
282
283 #[test]
284 fn test_build_upsert_body() {
285 let docs = vec![doc("1", "hello"), doc("2", "world")];
286 let vectors = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
287 let body = PineconeStore::build_upsert_body(&docs, &vectors);
288 let vectors_arr = body.get("vectors").unwrap().as_array().unwrap();
289 assert_eq!(vectors_arr.len(), 2);
290 assert_eq!(vectors_arr[0]["id"], "1");
291 assert_eq!(vectors_arr[0]["values"][0], 1.0);
292 }
293
294 #[test]
295 fn test_build_upsert_body_generates_id_if_missing() {
296 let mut d = doc("", "x");
297 d.id = None;
298 let body = PineconeStore::build_upsert_body(&[d], &[vec![0.1]]);
299 let id = body["vectors"][0]["id"].as_str().unwrap();
300 assert!(!id.is_empty());
301 }
302
303 #[test]
304 fn test_build_query_body() {
305 let body = PineconeStore::build_query_body(&[1.0, 2.0, 3.0], 5);
306 assert_eq!(body["topK"], 5);
307 assert_eq!(body["includeMetadata"], true);
308 assert_eq!(body["vector"][2], 3.0);
309 }
310
311 #[test]
312 fn test_new() {
313 let store = PineconeStore::new("key", "https://index.svc.env.pinecone.io");
314 assert_eq!(store.host, "https://index.svc.env.pinecone.io");
315 }
316}