Core implementation: - Added Chroma MCP client integration to search-server.ts - Implemented queryChroma() helper with Python dict parsing - Added VECTOR_DB_DIR constant to paths.ts - Added SessionStore.getObservationsByIds() method Search handlers updated: - search_observations: Semantic-first with 90-day temporal filter - find_by_concept/type/file: Metadata-first, semantic-enhanced ranking - All handlers fall back to FTS5 if Chroma unavailable Technical details: - Direct MCP client usage (no abstractions) - Regex parsing of Chroma Python dict responses - Semantic ranking preserved in final results - Graceful degradation to FTS5-only search 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
14 KiB
Feature Implementation Plan: Hybrid Search (Chroma + SQLite)
Status: Experimental validation complete, ready for production implementation
Experiment Results Summary
Branch: experiment/chroma-mcp
Validation: Semantic search (Chroma) + Temporal filtering (SQLite) working correctly
Collection: cm__claude-mem with 2,800+ documents synced
Decision: Proceed with production implementation
Implementation Plan
Phase 1: Clean Start
1.1 Create Feature Branch
# Start from clean main branch
git checkout main
git pull origin main
# Create new feature branch
git branch feature/hybrid-search
git checkout feature/hybrid-search
1.2 Port Working Experiment Scripts
Files to keep (these work correctly):
experiment/chroma-sync-experiment.ts- Syncs SQLite → Chromaexperiment/chroma-search-test.ts- Validates search qualityexperiment/README.md- Experiment documentationexperiment/RESULTS.md- Update with accurate current results
Actions:
# Cherry-pick only the experiment files from experiment/chroma-mcp
git checkout experiment/chroma-mcp -- experiment/
# Remove any experiment artifacts that reference old implementation
# (test-chroma-connection.ts uses broken ChromaOrchestrator)
git rm experiment/../test-chroma-connection.ts 2>/dev/null || true
# Commit clean experiment baseline
git commit -m "Add validated Chroma search experiments"
Phase 2: Production Architecture
2.1 Design Principles
Core Rules:
- ✅ Direct MCP client usage (no wrapper abstractions)
- ✅ Inline helper functions (no ChromaOrchestrator)
- ✅ Each search workflow is deterministic (no fallbacks)
- ✅ Temporal boundaries prevent stale results
- ✅ Chroma handles semantic ranking, SQLite handles recency
File Structure:
src/
├── servers/
│ └── search-server.ts # Hybrid MCP server (SQLite + Chroma)
├── services/
│ ├── sqlite/
│ │ ├── SessionStore.ts # SQLite CRUD (unchanged)
│ │ └── SessionSearch.ts # FTS5 search (fallback if Chroma fails)
│ └── sync/
│ └── ChromaSync.ts # NEW: Sync SQLite → Chroma on observation save
└── shared/
└── paths.ts # Add VECTOR_DB_DIR constant
2.2 Search Workflows
Workflow 1: search_observations (Semantic-First, Temporally-Bounded)
User Query → Chroma Semantic Search (top 100)
→ Filter: created_at_epoch > (now - 90 days)
→ SQLite: Hydrate full records
→ Sort: created_at_epoch DESC
→ Return: Recent + semantically relevant
Workflow 2: find_by_concept/type/file (Metadata-First, Semantic-Enhanced)
User Query → SQLite: Filter by metadata (type/concept/file)
→ Chroma: Rank filtered IDs by semantic relevance
→ SQLite: Hydrate in semantic rank order
→ Return: Metadata-filtered + semantically ranked
Workflow 3: search_sessions (SQLite FTS5 only)
User Query → SQLite FTS5 search (sessions are already summarized)
→ Return: Keyword matches
Workflow 4: get_recent_context (Temporal-First, No Semantic)
Hook Request → SQLite: Last 50 observations ORDER BY created_at_epoch DESC
→ Return: Most recent context (no semantic ranking needed)
Phase 3: Implementation Steps
3.1 Add Chroma Support to search-server.ts
File: src/servers/search-server.ts
Changes:
-
Add Chroma MCP client initialization (lines 20-26):
let chromaClient: Client; const COLLECTION_NAME = 'cm__claude-mem'; -
Add
queryChroma()helper function with proper Python dict parsing:async function queryChroma( query: string, limit: number, whereFilter?: Record<string, any> ): Promise<{ ids: number[]; distances: number[]; metadatas: any[] }> -
Initialize Chroma client in
main():const chromaTransport = new StdioClientTransport({ command: 'uvx', args: ['chroma-mcp', '--client-type', 'persistent', '--data-dir', VECTOR_DB_DIR] }); chromaClient = new Client({...}); await chromaClient.connect(chromaTransport); -
Update
search_observationshandler (lines 350-427):- Replace FTS5 search with Chroma semantic search
- Add 90-day temporal filter
- Hydrate from SQLite in temporal order
-
Update
find_by_concepthandler (lines 501-575):- SQLite metadata filter first
- Chroma semantic ranking second
- Preserve semantic rank order in final results
-
Update
find_by_typehandler (lines 720-797):- Same pattern as find_by_concept
-
Update
find_by_filehandler (lines 592-700):- Same pattern as find_by_concept
IMPORTANT:
- Keep
SessionSearchas fallback (if Chroma client fails to connect) - Add error handling: if Chroma query fails, fall back to FTS5
- Log all Chroma operations to stderr for debugging
3.2 Add VECTOR_DB_DIR Path Constant
File: src/shared/paths.ts
export const VECTOR_DB_DIR = path.join(DATA_DIR, 'vector-db');
3.3 Add Automatic Sync Service
NEW File: src/services/sync/ChromaSync.ts
Purpose: Automatically sync new observations to Chroma when worker saves them
Key Methods:
class ChromaSync {
async syncObservation(obs: Observation): Promise<void>
async syncBatch(observations: Observation[]): Promise<void>
async ensureCollection(): Promise<void>
}
Integration Point:
worker-service.ts- After saving observation to SQLite, callchromaSync.syncObservation()- Batch sync on startup: sync any observations not yet in Chroma
Document Format (per experiment):
// Each observation creates multiple Chroma documents (one per semantic chunk)
id: `obs_${obs.id}_title`
document: obs.title
metadata: { sqlite_id: obs.id, type: obs.type, created_at_epoch: obs.created_at_epoch }
id: `obs_${obs.id}_narrative`
document: obs.narrative
metadata: { sqlite_id: obs.id, type: obs.type, created_at_epoch: obs.created_at_epoch }
// Facts become individual searchable chunks
id: `obs_${obs.id}_fact_${i}`
document: fact
metadata: { sqlite_id: obs.id, type: obs.type, created_at_epoch: obs.created_at_epoch }
Phase 4: Build and Validation
4.1 Build Process
# Build all scripts
npm run build
# Verify outputs
ls -lh plugin/scripts/search-server.js # Should exist (ESM)
ls -lh plugin/scripts/search-server.cjs # Should NOT exist (delete if present)
# Check build format
head -1 plugin/scripts/search-server.js # Should show: #!/usr/bin/env node
4.2 Validation Checklist
✅ Pre-deployment checks:
-
Run sync experiment:
npx tsx experiment/chroma-sync-experiment.ts- Verify collection created
- Verify documents synced
- Check document count matches observations
-
Run search test:
npx tsx experiment/chroma-search-test.ts- Verify semantic queries return results
- Compare quality vs FTS5
- Document results in RESULTS.md
-
Test MCP server standalone:
# Start server manually node plugin/scripts/search-server.js # In another terminal, test with MCP inspector npx @modelcontextprotocol/inspector node plugin/scripts/search-server.js -
Test with Claude Code:
# Deploy to plugin directory cp -r plugin/* ~/.claude/plugins/marketplaces/thedotmack/ # Restart worker pm2 restart claude-mem-worker # Start new Claude session and test search tools
✅ Smoke tests:
- Search for recent work: Should return last 90 days
- Search for old concepts: Should filter by recency
- Search by file: Should return file-specific observations
- Search by type: Should return only that type
Phase 5: Documentation
5.1 Update CLAUDE.md
Add to "What It Does" section:
### Hybrid Search Architecture
Claude-mem uses a hybrid search system combining:
- **Semantic Search (Chroma)**: Vector embeddings for conceptual understanding
- **Keyword Search (SQLite FTS5)**: Full-text search for exact matches
- **Temporal Filtering**: 90-day recency boundary prevents stale results
Search workflows automatically choose the optimal combination:
- Conceptual queries → Semantic-first, temporally-bounded
- Metadata queries → Metadata-first, semantically-enhanced
- Recent context → Temporal-first (no semantic ranking)
5.2 Update Architecture Section
### Vector Database Layer
**Technology**: ChromaDB via Chroma MCP server
**Location**: `~/.claude-mem/vector-db/`
**Collection**: `cm__claude-mem`
**Sync Strategy**:
- Worker service syncs observations to Chroma after SQLite save
- Each observation creates multiple vector documents (title, narrative, facts)
- Metadata includes `sqlite_id` for cross-reference
**Search Strategy**:
- Semantic queries use Chroma with 90-day temporal filter
- Metadata queries filter SQLite first, then semantic rank
- Fallback to FTS5 if Chroma unavailable
5.3 Write Release Notes
File: EXPERIMENTAL_RELEASE_NOTES.md
# Hybrid Search Release (v4.4.0)
## Breaking Changes
None - Search MCP tools maintain same interface
## New Features
### Semantic Search via Chroma
- Added ChromaDB integration for vector-based semantic search
- Observations automatically synced to vector database
- Search understands conceptual queries (not just keywords)
### Hybrid Search Workflows
- `search_observations`: Semantic search with 90-day recency filter
- `find_by_concept/type/file`: Metadata filtering + semantic ranking
- Automatic fallback to FTS5 if Chroma unavailable
### Sync Automation
- Worker service auto-syncs new observations to Chroma
- Batch sync on startup for any missing observations
- Collection: `cm__claude-mem` in `~/.claude-mem/vector-db/`
## Technical Details
**New Dependencies:**
- `@modelcontextprotocol/sdk` (already present)
- External: `uvx chroma-mcp` (Python package via uvx)
**New Files:**
- `src/services/sync/ChromaSync.ts` - Auto-sync service
- `experiment/chroma-sync-experiment.ts` - Manual sync tool
- `experiment/chroma-search-test.ts` - Search quality validator
**Modified Files:**
- `src/servers/search-server.ts` - Hybrid search implementation
- `src/services/worker-service.ts` - Auto-sync integration
- `src/shared/paths.ts` - Added VECTOR_DB_DIR constant
**Design Rationale:**
- Temporal boundaries prevent old semantically-perfect matches from outranking recent updates
- Metadata-first filtering eliminates irrelevant categories before semantic ranking
- Direct MCP client usage avoids abstraction overhead
- Inline helpers keep parsing logic close to usage
Phase 6: Deployment
6.1 Pre-merge Validation
# Ensure all tests pass
npm run build
npm run test:parser # If applicable
# Validate experiment results
npx tsx experiment/chroma-sync-experiment.ts
npx tsx experiment/chroma-search-test.ts
# Test production MCP server
node plugin/scripts/search-server.js &
# Send test queries via MCP inspector
# Clean build artifacts
rm -f plugin/scripts/*.cjs # Remove stale CommonJS builds
6.2 Commit Strategy
# Commit 1: Experiment scripts (already done if following plan)
git add experiment/
git commit -m "Add validated Chroma search experiments"
# Commit 2: Core implementation
git add src/servers/search-server.ts src/shared/paths.ts
git commit -m "Implement hybrid search: Chroma semantic + SQLite temporal"
# Commit 3: Auto-sync service
git add src/services/sync/ src/services/worker-service.ts
git commit -m "Add automatic observation sync to Chroma vector DB"
# Commit 4: Documentation
git add CLAUDE.md EXPERIMENTAL_RELEASE_NOTES.md
git commit -m "Document hybrid search architecture and usage"
# Commit 5: Build artifacts
npm run build
git add plugin/scripts/
git commit -m "Build hybrid search implementation"
6.3 Merge to Main
# Push feature branch
git push origin feature/hybrid-search
# Create PR or merge directly (your choice)
git checkout main
git merge feature/hybrid-search
git push origin main
# Tag release
git tag v4.4.0
git push origin v4.4.0
Rollback Plan
If issues arise post-deployment:
# Quick rollback
git checkout main
git revert HEAD~5..HEAD # Revert last 5 commits
git push origin main
# Or cherry-pick the revert
git checkout -b hotfix/rollback-hybrid-search
git revert <commit-sha>
git push origin hotfix/rollback-hybrid-search
Chroma data cleanup (if needed):
# Remove vector database
rm -rf ~/.claude-mem/vector-db/
# Search server will fall back to FTS5 if Chroma unavailable
Success Criteria
Must have before merge:
- ✅ Sync experiment completes without errors
- ✅ Search test shows Chroma returning results
- ✅ MCP server starts and responds to queries
- ✅ Fallback to FTS5 works if Chroma unavailable
- ✅ No breaking changes to existing MCP tool interfaces
- ✅ Documentation updated
- ✅ No uncommitted changes
- ✅ No dead code (ChromaOrchestrator removed)
- ✅ No stale build artifacts (.cjs files)
Nice to have:
- Performance benchmarks (Chroma vs FTS5 query time)
- Search quality metrics (relevance scores)
- Token usage comparison (semantic vs keyword results)
Timeline Estimate
- Phase 1 (Clean Start): 15 minutes
- Phase 2 (Architecture Review): 30 minutes
- Phase 3 (Implementation): 2-3 hours
- Phase 4 (Validation): 1 hour
- Phase 5 (Documentation): 1 hour
- Phase 6 (Deployment): 30 minutes
Total: ~5-6 hours for complete, validated implementation
Notes
- The experiment validated that semantic search works and provides value
- This plan avoids all the mistakes from the previous attempt:
- ✅ Clean branch from main (no baggage)
- ✅ Implementation AFTER experiment validation
- ✅ No dead code (ChromaOrchestrator)
- ✅ Proper commit strategy
- ✅ Complete documentation
- ✅ Validation at every step