diff --git a/.env.example b/.env.example
index 0166e67..615bd7b 100644
--- a/.env.example
+++ b/.env.example
@@ -2,10 +2,16 @@
GROQ_API_KEY=your_groq_key_here
COHERE_API_KEY=your_cohere_key_here
-# Vector DB
+# Vector DB (Qdrant remains the default local backend)
+VECTOR_SEARCH_BACKEND=qdrant
QDRANT_HOST=localhost
QDRANT_PORT=6333
+# Optional Pinecone backend
+PINECONE_API_KEY=
+PINECONE_INDEX_NAME=trojanchat
+PINECONE_NAMESPACE=trojanchat
+
# Inference Cache
REDIS_URL=redis://localhost:6379
CACHE_ENABLED=true
diff --git a/README.md b/README.md
index 7230cbb..8a0cf85 100644
--- a/README.md
+++ b/README.md
@@ -22,6 +22,7 @@
+
@@ -95,6 +96,20 @@ The intended use, excluded use, data/credential handling, model or algorithm lim Use the linked production-readiness issue for this repository as the checklist. Resolve missing tests, deployment instructions, observability, supply-chain controls, and release evidence before attaching a production claim. +## Optional Pinecone semantic search + +TrojanChat keeps Qdrant as the default local vector backend and provides an opt-in Pinecone adapter for hosted retrieval. + +```bash +python -m pip install -r requirements-pinecone.txt +export VECTOR_SEARCH_BACKEND=pinecone +export PINECONE_API_KEY=*** +export PINECONE_INDEX_NAME=trojanchat +export PINECONE_NAMESPACE=trojanchat +``` + +Pinecone quality and operations must be benchmarked independently from the existing bounded in-process storage benchmark: record embedding model, corpus revision, top-k, recall@k, p95/p99 latency, error rate, and cost. Keep credentials in deployment secrets and retain Qdrant/local fallback for offline tests. + ## Engineering evidence | Evidence | Current result | Enforcement | diff --git a/ai/retrieval/pinecone_search.py b/ai/retrieval/pinecone_search.py new file mode 100644 index 0000000..5a1dbe7 --- /dev/null +++ b/ai/retrieval/pinecone_search.py @@ -0,0 +1,52 @@ +from __future__ import annotations + +import os +from typing import Any + +from ai.embeddings.cohere_embedder import CohereEmbedder + + +class PineconeSearch: + """Pinecone alternative to the existing Qdrant semantic-search path.""" + + def __init__(self, index_name: str | None = None): + api_key = os.getenv("PINECONE_API_KEY", "").strip() + self.index_name = ( + index_name or os.getenv("PINECONE_INDEX_NAME", "trojanchat") + ).strip() + self.namespace = os.getenv("PINECONE_NAMESPACE", "trojanchat").strip() + + if not api_key: + raise RuntimeError( + "PINECONE_API_KEY is required when VECTOR_SEARCH_BACKEND=pinecone." + ) + if not self.index_name or not self.namespace: + raise RuntimeError("PINECONE_INDEX_NAME and PINECONE_NAMESPACE are required.") + + from pinecone import Pinecone + + self.index = Pinecone(api_key=api_key).Index(self.index_name) + self.embedder = CohereEmbedder() + + def search( + self, + query: str, + top_k: int = 5, + filter_condition: dict[str, Any] | None = None, + ) -> list[dict[str, Any]]: + vector = self.embedder.embed_text(query) + response = self.index.query( + vector=vector, + top_k=top_k, + include_metadata=True, + namespace=self.namespace, + filter=filter_condition, + ) + return [ + { + "id": match.id, + "score": match.score, + "payload": match.metadata or {}, + } + for match in response.matches + ] diff --git a/requirements-pinecone.txt b/requirements-pinecone.txt new file mode 100644 index 0000000..f00dbab --- /dev/null +++ b/requirements-pinecone.txt @@ -0,0 +1,2 @@ +# Optional hosted semantic-search backend. +pinecone>=6.0.0