439 lines
12 KiB
Markdown
439 lines
12 KiB
Markdown
|
|
# Vector Search Optimization for ServiceBot
|
||
|
|
|
||
|
|
**Date Created**: 2025-11-06
|
||
|
|
**Context**: Making ServiceBot the "most badass AI chatbot" with optimal vector search
|
||
|
|
**Current State**: SQLite direct read works but isn't optimal for scale
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## The Problem with SQLite Direct Read
|
||
|
|
|
||
|
|
### Current Implementation (servicebot/app/knowledge/rag_engine.py)
|
||
|
|
|
||
|
|
```python
|
||
|
|
def _vector_search(self, query, kb_filter, limit):
|
||
|
|
# 1. Generate query embedding
|
||
|
|
response = self.openai_client.embeddings.create(model=self.embedding_model, input=query)
|
||
|
|
query_embedding = np.array(response.data[0].embedding)
|
||
|
|
|
||
|
|
# 2. Load ALL embeddings from SQLite
|
||
|
|
cursor = self.conn.execute("SELECT * FROM embeddings")
|
||
|
|
|
||
|
|
# 3. Calculate similarity for EVERY document (linear scan)
|
||
|
|
for row in cursor:
|
||
|
|
doc_embedding = np.frombuffer(row['embedding'], dtype=np.float32)
|
||
|
|
similarity = cosine_similarity(query_embedding, doc_embedding)
|
||
|
|
|
||
|
|
# 4. Sort all results and return top N
|
||
|
|
```
|
||
|
|
|
||
|
|
**Time Complexity:** O(n) where n = total documents
|
||
|
|
**Performance:**
|
||
|
|
- 10K docs: ~200-500ms ✅ Acceptable
|
||
|
|
- 100K docs: ~2-5s ⚠️ Slow
|
||
|
|
- 1M docs: 20s+ ❌ Unusable
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Optimization Levels
|
||
|
|
|
||
|
|
### Level 1: sqlite-vec Extension ⚡ FAST WIN
|
||
|
|
|
||
|
|
**What:** Native vector similarity search in SQLite using SIMD optimizations
|
||
|
|
|
||
|
|
**Install:**
|
||
|
|
```bash
|
||
|
|
pip install sqlite-vec
|
||
|
|
```
|
||
|
|
|
||
|
|
**Usage:**
|
||
|
|
```python
|
||
|
|
import sqlite3
|
||
|
|
import sqlite_vec
|
||
|
|
|
||
|
|
conn = sqlite3.connect('data/pistn.db')
|
||
|
|
conn.enable_load_extension(True)
|
||
|
|
sqlite_vec.load(conn)
|
||
|
|
|
||
|
|
# Create virtual table for vector search
|
||
|
|
conn.execute("""
|
||
|
|
CREATE VIRTUAL TABLE vec_embeddings USING vec0(
|
||
|
|
document_id INTEGER PRIMARY KEY,
|
||
|
|
embedding FLOAT[768]
|
||
|
|
)
|
||
|
|
""")
|
||
|
|
|
||
|
|
# Insert embeddings
|
||
|
|
conn.execute("INSERT INTO vec_embeddings SELECT document_id, embedding FROM embeddings")
|
||
|
|
|
||
|
|
# FAST vector search using ANN (Approximate Nearest Neighbors)
|
||
|
|
cursor = conn.execute("""
|
||
|
|
SELECT document_id, distance
|
||
|
|
FROM vec_embeddings
|
||
|
|
WHERE embedding MATCH ?
|
||
|
|
ORDER BY distance
|
||
|
|
LIMIT 10
|
||
|
|
""", [query_embedding.tobytes()])
|
||
|
|
```
|
||
|
|
|
||
|
|
**Performance:**
|
||
|
|
- 10K docs: ~50-100ms (5-10x faster) ✅
|
||
|
|
- 100K docs: ~100-200ms (10-25x faster) ✅
|
||
|
|
- 1M docs: ~200-500ms (40-100x faster) ✅
|
||
|
|
|
||
|
|
**Pros:**
|
||
|
|
- ✅ Same SQLite file
|
||
|
|
- ✅ No infrastructure changes
|
||
|
|
- ✅ 10-100x faster
|
||
|
|
- ✅ Portable
|
||
|
|
|
||
|
|
**Cons:**
|
||
|
|
- ⚠️ Still in SQLite (single file limitations)
|
||
|
|
- ⚠️ Limited concurrent writes
|
||
|
|
|
||
|
|
**Recommendation:** **DO THIS FIRST** - Easy win!
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Level 2: Specialized Vector Database 🚀 ULTIMATE PERFORMANCE
|
||
|
|
|
||
|
|
**Options:**
|
||
|
|
|
||
|
|
#### Option A: Qdrant (Open Source, Self-Hosted)
|
||
|
|
```python
|
||
|
|
from qdrant_client import QdrantClient
|
||
|
|
from qdrant_client.models import Distance, VectorParams, PointStruct
|
||
|
|
|
||
|
|
# Initialize
|
||
|
|
client = QdrantClient(host="localhost", port=6333)
|
||
|
|
|
||
|
|
# Create collection
|
||
|
|
client.create_collection(
|
||
|
|
collection_name="pistn_knowledge",
|
||
|
|
vectors_config=VectorParams(size=768, distance=Distance.COSINE)
|
||
|
|
)
|
||
|
|
|
||
|
|
# Import from SQLite export
|
||
|
|
import sqlite3
|
||
|
|
conn = sqlite3.connect('data/pistn.db')
|
||
|
|
cursor = conn.execute("SELECT id, embedding FROM embeddings")
|
||
|
|
|
||
|
|
points = []
|
||
|
|
for row in cursor:
|
||
|
|
points.append(PointStruct(
|
||
|
|
id=row[0],
|
||
|
|
vector=np.frombuffer(row[1], dtype=np.float32).tolist(),
|
||
|
|
payload={"doc_id": row[0]}
|
||
|
|
))
|
||
|
|
|
||
|
|
client.upsert(collection_name="pistn_knowledge", points=points)
|
||
|
|
|
||
|
|
# ULTRA-FAST search
|
||
|
|
results = client.search(
|
||
|
|
collection_name="pistn_knowledge",
|
||
|
|
query_vector=query_embedding.tolist(),
|
||
|
|
limit=10
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Performance:**
|
||
|
|
- 10K docs: ~10-30ms ✅✅
|
||
|
|
- 100K docs: ~20-50ms ✅✅
|
||
|
|
- 1M docs: ~30-80ms ✅✅
|
||
|
|
- 10M docs: ~50-150ms ✅✅
|
||
|
|
|
||
|
|
**Pros:**
|
||
|
|
- ✅ HNSW index (O(log n) search)
|
||
|
|
- ✅ Handles millions of vectors
|
||
|
|
- ✅ Concurrent queries
|
||
|
|
- ✅ Filtering and metadata search
|
||
|
|
- ✅ Production-grade
|
||
|
|
|
||
|
|
**Cons:**
|
||
|
|
- ⚠️ Separate service to manage
|
||
|
|
- ⚠️ More infrastructure
|
||
|
|
- ⚠️ Memory requirements
|
||
|
|
|
||
|
|
#### Option B: Chroma (Simple, Embedded)
|
||
|
|
```python
|
||
|
|
import chromadb
|
||
|
|
|
||
|
|
# Initialize
|
||
|
|
client = chromadb.Client()
|
||
|
|
collection = client.create_collection("pistn_knowledge")
|
||
|
|
|
||
|
|
# Import from SQLite
|
||
|
|
conn = sqlite3.connect('data/pistn.db')
|
||
|
|
cursor = conn.execute("SELECT d.id, d.content, e.embedding FROM documents d JOIN embeddings e ON d.id = e.document_id")
|
||
|
|
|
||
|
|
for row in cursor:
|
||
|
|
collection.add(
|
||
|
|
ids=[str(row[0])],
|
||
|
|
embeddings=[np.frombuffer(row[2], dtype=np.float32).tolist()],
|
||
|
|
documents=[row[1]]
|
||
|
|
)
|
||
|
|
|
||
|
|
# Query
|
||
|
|
results = collection.query(
|
||
|
|
query_embeddings=[query_embedding.tolist()],
|
||
|
|
n_results=10
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pros:**
|
||
|
|
- ✅ Embedded (no separate server)
|
||
|
|
- ✅ Simple API
|
||
|
|
- ✅ Good performance
|
||
|
|
|
||
|
|
**Cons:**
|
||
|
|
- ⚠️ Less mature than Qdrant
|
||
|
|
- ⚠️ Limited scale (compared to Qdrant)
|
||
|
|
|
||
|
|
#### Option C: Pinecone (Cloud-Hosted)
|
||
|
|
```python
|
||
|
|
import pinecone
|
||
|
|
|
||
|
|
# Initialize
|
||
|
|
pinecone.init(api_key="your_key", environment="us-west1-gcp")
|
||
|
|
index = pinecone.Index("pistn-knowledge")
|
||
|
|
|
||
|
|
# Upsert vectors
|
||
|
|
index.upsert(vectors=[
|
||
|
|
(str(doc_id), embedding.tolist(), metadata)
|
||
|
|
for doc_id, embedding, metadata in documents
|
||
|
|
])
|
||
|
|
|
||
|
|
# Query (BLAZING FAST)
|
||
|
|
results = index.query(
|
||
|
|
vector=query_embedding.tolist(),
|
||
|
|
top_k=10,
|
||
|
|
include_metadata=True
|
||
|
|
)
|
||
|
|
```
|
||
|
|
|
||
|
|
**Pros:**
|
||
|
|
- ✅ Zero infrastructure management
|
||
|
|
- ✅ Scales to billions
|
||
|
|
- ✅ Global CDN
|
||
|
|
- ✅ Enterprise features
|
||
|
|
|
||
|
|
**Cons:**
|
||
|
|
- ❌ Not self-hosted
|
||
|
|
- ❌ Cost (free tier: 1M vectors)
|
||
|
|
- ❌ Vendor lock-in
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
### Level 3: Hybrid Approach 🎯 BEST OF BOTH WORLDS
|
||
|
|
|
||
|
|
**Architecture:**
|
||
|
|
```
|
||
|
|
┌────────────────────────────────────────────────┐
|
||
|
|
│ ServiceBot RAG Engine (Adaptive) │
|
||
|
|
│ │
|
||
|
|
│ IF deployment == "small": │
|
||
|
|
│ └─ Use: SQLite with sqlite-vec │
|
||
|
|
│ • <50K documents │
|
||
|
|
│ • Simple setup │
|
||
|
|
│ • Portable │
|
||
|
|
│ │
|
||
|
|
│ ELIF deployment == "medium": │
|
||
|
|
│ └─ Use: Qdrant (self-hosted) │
|
||
|
|
│ • 50K-1M documents │
|
||
|
|
│ • Local infrastructure │
|
||
|
|
│ • Full control │
|
||
|
|
│ │
|
||
|
|
│ ELIF deployment == "large": │
|
||
|
|
│ └─ Use: Pinecone (cloud) │
|
||
|
|
│ • 1M+ documents │
|
||
|
|
│ • Global scale │
|
||
|
|
│ • Managed service │
|
||
|
|
└────────────────────────────────────────────────┘
|
||
|
|
```
|
||
|
|
|
||
|
|
**Implementation:**
|
||
|
|
```python
|
||
|
|
# servicebot/app/knowledge/vector_backends.py
|
||
|
|
|
||
|
|
class VectorBackend(ABC):
|
||
|
|
@abstractmethod
|
||
|
|
def search(self, query_embedding: np.ndarray, limit: int) -> List[SearchResult]:
|
||
|
|
pass
|
||
|
|
|
||
|
|
class SQLiteVecBackend(VectorBackend):
|
||
|
|
"""Fast for small-medium deployments"""
|
||
|
|
def search(self, query_embedding, limit):
|
||
|
|
# Use sqlite-vec extension
|
||
|
|
...
|
||
|
|
|
||
|
|
class QdrantBackend(VectorBackend):
|
||
|
|
"""Best for self-hosted production"""
|
||
|
|
def search(self, query_embedding, limit):
|
||
|
|
# Use Qdrant client
|
||
|
|
...
|
||
|
|
|
||
|
|
class PineconeBackend(VectorBackend):
|
||
|
|
"""Best for cloud scale"""
|
||
|
|
def search(self, query_embedding, limit):
|
||
|
|
# Use Pinecone client
|
||
|
|
...
|
||
|
|
|
||
|
|
# Auto-select based on config
|
||
|
|
class ServiceBotRAG:
|
||
|
|
def __init__(self, knowledge_db_path: str):
|
||
|
|
backend_type = settings.VECTOR_BACKEND # 'sqlite-vec', 'qdrant', 'pinecone'
|
||
|
|
|
||
|
|
if backend_type == 'sqlite-vec':
|
||
|
|
self.vector_backend = SQLiteVecBackend(knowledge_db_path)
|
||
|
|
elif backend_type == 'qdrant':
|
||
|
|
self.vector_backend = QdrantBackend(settings.QDRANT_URL)
|
||
|
|
elif backend_type == 'pinecone':
|
||
|
|
self.vector_backend = PineconeBackend(settings.PINECONE_API_KEY)
|
||
|
|
else:
|
||
|
|
# Fallback to current implementation
|
||
|
|
self.vector_backend = NumpyBackend(knowledge_db_path)
|
||
|
|
```
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Recommended Implementation Plan
|
||
|
|
|
||
|
|
### Phase 1: Add sqlite-vec Support (QUICK WIN) ⚡
|
||
|
|
|
||
|
|
**Effort:** 1-2 days
|
||
|
|
**Impact:** 10-100x faster vector search
|
||
|
|
|
||
|
|
```bash
|
||
|
|
# 1. Install sqlite-vec
|
||
|
|
pip install sqlite-vec
|
||
|
|
|
||
|
|
# 2. Update ServiceBotRAG to use it
|
||
|
|
# 3. Migrate existing exports to use vec0 tables
|
||
|
|
# 4. Test performance
|
||
|
|
```
|
||
|
|
|
||
|
|
**Files to modify:**
|
||
|
|
- `servicebot/app/knowledge/rag_engine.py` - Add sqlite-vec support
|
||
|
|
- `servicebot/app/knowledge/importer.py` - Migrate embeddings to vec0 table
|
||
|
|
- `servicebot/requirements.txt` - Add sqlite-vec
|
||
|
|
|
||
|
|
**Backward compatible:** Falls back to numpy if sqlite-vec not available
|
||
|
|
|
||
|
|
### Phase 2: Add Qdrant Backend (PRODUCTION SCALE) 🚀
|
||
|
|
|
||
|
|
**Effort:** 3-5 days
|
||
|
|
**Impact:** Handles millions of documents, concurrent queries
|
||
|
|
|
||
|
|
**Benefits:**
|
||
|
|
- Sub-100ms search even with 1M+ docs
|
||
|
|
- Filtering by metadata
|
||
|
|
- Concurrent multi-user support
|
||
|
|
- Production monitoring
|
||
|
|
|
||
|
|
**Implementation:**
|
||
|
|
1. Create `VectorBackend` abstraction
|
||
|
|
2. Implement `QdrantBackend`
|
||
|
|
3. Add importer: SQLite export → Qdrant
|
||
|
|
4. Make backend configurable via environment variable
|
||
|
|
|
||
|
|
### Phase 3: Add Pinecone Option (CLOUD SCALE) ☁️
|
||
|
|
|
||
|
|
**Effort:** 2-3 days
|
||
|
|
**Impact:** Global scale, zero infrastructure
|
||
|
|
|
||
|
|
**For deployments that need:**
|
||
|
|
- Multi-region support
|
||
|
|
- Billions of vectors
|
||
|
|
- Zero ops overhead
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Performance Comparison
|
||
|
|
|
||
|
|
| Backend | 10K Docs | 100K Docs | 1M Docs | 10M Docs | Concurrent Users |
|
||
|
|
|---------|----------|-----------|---------|----------|------------------|
|
||
|
|
| **Current (numpy)** | 200ms | 2-5s | 20s+ | N/A | Low |
|
||
|
|
| **sqlite-vec** | 50ms | 200ms | 500ms | N/A | Low |
|
||
|
|
| **Qdrant** | 20ms | 50ms | 80ms | 150ms | High |
|
||
|
|
| **Pinecone** | 15ms | 40ms | 60ms | 100ms | Very High |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Cost Comparison
|
||
|
|
|
||
|
|
| Backend | Infrastructure | Monthly Cost (100K docs) | Pros |
|
||
|
|
|---------|---------------|------------------------|------|
|
||
|
|
| **Current** | None | $0 | Simple, portable |
|
||
|
|
| **sqlite-vec** | None | $0 | Fast, free, simple |
|
||
|
|
| **Qdrant** | VPS/Server | $10-50 (server) | Full control, scalable |
|
||
|
|
| **Pinecone** | Pinecone Cloud | $70+ | Zero ops, global |
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Recommendation for "Most Badass Chatbot"
|
||
|
|
|
||
|
|
### For Now (Testing Phase 1):
|
||
|
|
✅ **Use current SQLite direct read**
|
||
|
|
- Testing export/import flow
|
||
|
|
- Validating knowledge quality
|
||
|
|
- Widget integration
|
||
|
|
- < 50K documents
|
||
|
|
|
||
|
|
### Next (After Phase 1 Works):
|
||
|
|
🚀 **Add sqlite-vec extension**
|
||
|
|
- Quick performance boost
|
||
|
|
- No infrastructure changes
|
||
|
|
- Production-ready for small-medium deployments
|
||
|
|
|
||
|
|
### Future (Production Scale):
|
||
|
|
💪 **Add Qdrant backend option**
|
||
|
|
- When you have 100K+ documents
|
||
|
|
- When you need multi-user concurrent access
|
||
|
|
- When sub-100ms search matters
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Migration Path
|
||
|
|
|
||
|
|
**The beauty of the export format:**
|
||
|
|
```
|
||
|
|
Claude OS Export (SQLite)
|
||
|
|
↓
|
||
|
|
├─→ ServiceBot (SQLite direct) ← Phase 1: Testing
|
||
|
|
├─→ ServiceBot (SQLite + vec) ← Phase 2: Optimization
|
||
|
|
├─→ ServiceBot (Qdrant) ← Phase 3: Scale
|
||
|
|
└─→ ServiceBot (Pinecone) ← Phase 4: Cloud
|
||
|
|
```
|
||
|
|
|
||
|
|
**Same export, different backends!** The export format is backend-agnostic.
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Implementation Priority
|
||
|
|
|
||
|
|
1. **NOW**: Finish Phase 1 testing with current SQLite read ✅
|
||
|
|
2. **Next Week**: Add sqlite-vec for 10-100x speedup ⚡
|
||
|
|
3. **Next Month**: Add Qdrant backend for production scale 🚀
|
||
|
|
4. **Future**: Add Pinecone for global deployments ☁️
|
||
|
|
|
||
|
|
**Don't optimize prematurely** - SQLite direct read is fine for initial testing!
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
## Key Takeaway
|
||
|
|
|
||
|
|
**For the "most badass chatbot":**
|
||
|
|
- ✅ Current approach: Good for testing and small-medium scale
|
||
|
|
- ⚡ Quick win: Add sqlite-vec (10-100x faster, zero infrastructure)
|
||
|
|
- 🚀 Production: Add Qdrant backend (handles millions, concurrent)
|
||
|
|
- ☁️ Enterprise: Add Pinecone option (global scale, zero ops)
|
||
|
|
|
||
|
|
**The export format supports all of these** - you can choose based on scale!
|
||
|
|
|
||
|
|
---
|
||
|
|
|
||
|
|
**Next Steps:**
|
||
|
|
1. Test current setup with Pistn export
|
||
|
|
2. Validate knowledge quality
|
||
|
|
3. Then optimize based on actual usage patterns
|