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 describe_index_stats(&self) -> Result<PineconeIndexStats, String> {
120 let url = format!("{}/describe_index_stats", self.host);
121 let resp = self
122 .client
123 .post(&url)
124 .header("Api-Key", &self.api_key)
125 .send()
126 .await
127 .map_err(|e| e.to_string())?;
128 if !resp.status().is_success() {
129 return Err(format!(
130 "Pinecone describe_index_stats error: {}",
131 resp.status()
132 ));
133 }
134 resp.json().await.map_err(|e| e.to_string())
135 }
136
137 pub async fn delete(&self, ids: &[String]) -> Result<(), String> {
139 let url = format!("{}/vectors/delete", self.host);
140 let body = serde_json::json!({ "ids": ids });
141 let resp = self
142 .client
143 .post(&url)
144 .header("Api-Key", &self.api_key)
145 .json(&body)
146 .send()
147 .await
148 .map_err(|e| e.to_string())?;
149 if !resp.status().is_success() {
150 return Err(format!("Pinecone delete error: {}", resp.status()));
151 }
152 Ok(())
153 }
154}
155
156#[async_trait]
157impl VectorStore for PineconeStore {
158 async fn add_documents(
159 &self,
160 documents: Vec<Document>,
161 embeddings: Vec<Vec<f32>>,
162 ) -> Result<Vec<String>, VectorStoreError> {
163 let ids: Vec<String> = documents
164 .iter()
165 .map(|d| {
166 d.id.clone()
167 .unwrap_or_else(|| uuid::Uuid::new_v4().to_string())
168 })
169 .collect();
170
171 let body = Self::build_upsert_body(&documents, &embeddings);
173 let url = format!("{}/vectors/upsert", self.host);
174 let resp = self
175 .client
176 .post(&url)
177 .header("Api-Key", &self.api_key)
178 .json(&body)
179 .send()
180 .await
181 .map_err(|e| {
182 VectorStoreError::StorageError(format!("Pinecone upsert failed: {}", e))
183 })?;
184
185 if !resp.status().is_success() {
186 return Err(VectorStoreError::StorageError(format!(
187 "Pinecone upsert HTTP error: {}",
188 resp.status()
189 )));
190 }
191
192 Ok(ids)
193 }
194
195 async fn similarity_search(
196 &self,
197 query_embedding: &[f32],
198 k: usize,
199 ) -> Result<Vec<SearchResult>, VectorStoreError> {
200 let body = Self::build_query_body(query_embedding, k);
201 let url = format!("{}/query", self.host);
202 let resp = self
203 .client
204 .post(&url)
205 .header("Api-Key", &self.api_key)
206 .json(&body)
207 .send()
208 .await
209 .map_err(|e| VectorStoreError::StorageError(format!("Pinecone query failed: {}", e)))?;
210
211 if !resp.status().is_success() {
212 return Err(VectorStoreError::StorageError(format!(
213 "Pinecone query HTTP error: {}",
214 resp.status()
215 )));
216 }
217
218 let query_resp: QueryResponse = resp.json().await.map_err(|e| {
219 VectorStoreError::StorageError(format!("Pinecone query parse error: {}", e))
220 })?;
221
222 let results = query_resp
223 .matches
224 .into_iter()
225 .map(|m| {
226 let content = m
227 .metadata
228 .as_ref()
229 .and_then(|md| md.get("content").cloned())
230 .unwrap_or_default();
231 let doc = Document {
232 content,
233 metadata: m.metadata.unwrap_or_default(),
234 id: Some(m.id.clone()),
235 };
236 SearchResult {
237 document: doc,
238 score: m.score as f32,
239 }
240 })
241 .collect();
242
243 Ok(results)
244 }
245
246 async fn get_document(&self, _id: &str) -> Result<Option<Document>, VectorStoreError> {
247 Err(VectorStoreError::StorageError(
250 "Pinecone does not support direct document fetch by ID via HTTP API".to_string(),
251 ))
252 }
253
254 async fn get_embedding(&self, _id: &str) -> Result<Option<Vec<f32>>, VectorStoreError> {
255 Err(VectorStoreError::StorageError(
256 "Pinecone does not support direct embedding fetch by ID via HTTP API".to_string(),
257 ))
258 }
259
260 async fn delete_document(&self, id: &str) -> Result<(), VectorStoreError> {
261 self.delete(&[id.to_string()])
262 .await
263 .map_err(VectorStoreError::StorageError)
264 }
265
266 async fn count(&self) -> usize {
267 match self.describe_index_stats().await {
270 Ok(stats) => stats.total_vector_count,
271 Err(e) => {
272 log::warn!("Pinecone count 失败,按 0 处理: {}", e);
273 0
274 }
275 }
276 }
277
278 async fn clear(&self) -> Result<(), VectorStoreError> {
279 Err(VectorStoreError::StorageError(
280 "Pinecone does not support clearing all vectors via HTTP API. Delete by namespace or IDs instead.".to_string()
281 ))
282 }
283}
284
285#[derive(Deserialize)]
286struct QueryResponse {
287 matches: Vec<QueryMatch>,
288}
289
290#[derive(Deserialize)]
291struct QueryMatch {
292 id: String,
293 #[allow(dead_code)]
294 score: f64,
295 metadata: Option<HashMap<String, String>>,
296}
297
298#[derive(Deserialize)]
303pub struct PineconeIndexStats {
304 #[serde(default)]
305 pub total_vector_count: usize,
306}
307
308#[cfg(test)]
309mod tests {
310 use super::*;
311
312 fn doc(id: &str, content: &str) -> Document {
313 Document {
314 content: content.to_string(),
315 metadata: HashMap::new(),
316 id: Some(id.to_string()),
317 }
318 }
319
320 #[test]
321 fn test_build_upsert_body() {
322 let docs = vec![doc("1", "hello"), doc("2", "world")];
323 let vectors = vec![vec![1.0, 2.0], vec![3.0, 4.0]];
324 let body = PineconeStore::build_upsert_body(&docs, &vectors);
325 let vectors_arr = body.get("vectors").unwrap().as_array().unwrap();
326 assert_eq!(vectors_arr.len(), 2);
327 assert_eq!(vectors_arr[0]["id"], "1");
328 assert_eq!(vectors_arr[0]["values"][0], 1.0);
329 }
330
331 #[test]
332 fn test_build_upsert_body_generates_id_if_missing() {
333 let mut d = doc("", "x");
334 d.id = None;
335 let body = PineconeStore::build_upsert_body(&[d], &[vec![0.1]]);
336 let id = body["vectors"][0]["id"].as_str().unwrap();
337 assert!(!id.is_empty());
338 }
339
340 #[test]
341 fn test_build_query_body() {
342 let body = PineconeStore::build_query_body(&[1.0, 2.0, 3.0], 5);
343 assert_eq!(body["topK"], 5);
344 assert_eq!(body["includeMetadata"], true);
345 assert_eq!(body["vector"][2], 3.0);
346 }
347
348 #[test]
349 fn test_new() {
350 let store = PineconeStore::new("key", "https://index.svc.env.pinecone.io");
351 assert_eq!(store.host, "https://index.svc.env.pinecone.io");
352 }
353}