Vyakti is a high-performance Rust implementation of LEANN (Low-Storage Vector Index), a vector database that achieves 97% storage savings through graph-based selective recomputation.
✅ PHASE 9D COMPLETE - 171 tests passing with LEANN compact mode
What Works Now:
In Development:
Vyakti is designed as a complete product that can be used in four modes:
✨ Zero Configuration Required!
Vyakti uses llama.cpp for embeddings with automatic model download. No external services needed!
System Requirements:
What Happens on First Run:
mxbai-embed-large-v1 (Q4_K_M, ~500MB) from HuggingFace~/.vyakti/models/ for future useGPU Acceleration (Optional):
--gpu-layers 32 flag to enable GPU acceleration# Install from crates.io
cargo install vyakti
# Or build from source
git clone https://github.com/yourusername/vyakti.git
cd vyakti
cargo install --path crates/vyakti-cli
# Create an index from documents (hybrid search enabled by default!)
# First run will auto-download mxbai-embed-large model (~500MB)
vyakti build my-docs --input ./documents
# Build with LEANN compact mode (93% storage savings + 60% faster!)
vyakti build my-docs --input ./documents --compact
# GPU acceleration (offload 32 layers to GPU for faster embeddings)
vyakti build my-docs --input ./documents --gpu-layers 32
# Custom model and GPU
vyakti build my-docs --input ./documents --model-path ./my-model.gguf --gpu-layers 64
# Disable hybrid search (vector-only mode)
vyakti build my-docs --input ./documents --no-hybrid
# Search the index (works transparently with both hybrid and vector-only)
vyakti search my-docs "vector database concepts" --top-k 10
# Search with GPU (uses same flags as build)
vyakti search my-docs "machine learning" --gpu-layers 32
# List all indexes
vyakti list
# Remove an index
vyakti remove my-docs --yes
Storage Savings:
Performance Gains:
Search results include a score representing the distance between your query and each result. Lower scores indicate higher relevance.
Score Interpretation (Cosine Distance):
Example:
vyakti search my-docs "machine learning" -k 5
Output:
Results for "machine learning":
1. Score: 0.12, Text: "Introduction to machine learning algorithms" # Highly relevant
2. Score: 0.28, Text: "Deep learning and neural networks" # Highly relevant
3. Score: 0.45, Text: "Data science best practices" # Moderately relevant
4. Score: 0.68, Text: "Software engineering patterns" # Weakly relevant
5. Score: 0.89, Text: "Cooking recipes database" # Unrelated
Tips for Better Results:
--top-k with a higher value, then filter by scoreuse vyakti_core::{VyaktiBuilder, VyaktiSearcher};
use vyakti_backend_hnsw::HnswBackend;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider, ensure_model};
use vyakti_common::BackendConfig;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Ensure model is available (auto-downloads if needed)
let model_path = ensure_model(None).await?;
// Create embedding provider (llama.cpp with mxbai-embed-large)
let embedding_config = LlamaCppConfig {
model_path,
n_gpu_layers: 0, // CPU-only (use 32 for GPU)
n_ctx: 512,
n_threads: num_cpus::get() as u32,
dimension: 1024,
normalize: true,
};
let embedding_provider = Arc::new(LlamaCppProvider::new(embedding_config)?);
// Create backend
let backend_config = BackendConfig::default();
let backend = Box::new(HnswBackend::with_config(backend_config.clone()));
// Create builder and add documents
let mut builder = VyaktiBuilder::new(backend, embedding_provider.clone());
builder.add_text("LEANN is a vector database", None);
builder.add_text("Rust is fast and memory-safe", None);
// Build and save index (normal mode)
builder.build_index(".vyakti/my-index").await?;
// OR: Build in compact mode for 93% storage savings + faster search!
// let (path, stats) = builder.build_index_compact(".vyakti/my-index", None).await?;
// println!("Storage savings: {:.1}%", stats.savings_percent);
// Load and search
let backend = Box::new(HnswBackend::with_config(backend_config));
let searcher = VyaktiSearcher::load(
".vyakti/my-index",
backend,
embedding_provider,
).await?;
let results = searcher.search("database", 5).await?;
for result in results {
println!("Score: {:.4}, Text: {}", result.score, result.text);
}
Ok(())
}
use vyakti_core::{VyaktiBuilder, VyaktiSearcher};
use vyakti_backend_hnsw::HnswBackend;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider, ensure_model};
use vyakti_common::BackendConfig;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = BackendConfig::default();
let model_path = ensure_model(None).await?;
let embedding_config = LlamaCppConfig {
model_path,
n_gpu_layers: 0,
n_ctx: 512,
n_threads: num_cpus::get() as u32,
dimension: 1024,
normalize: true,
};
let embedding_provider = Arc::new(LlamaCppProvider::new(embedding_config)?);
// Load searcher
let backend = Box::new(HnswBackend::with_config(config));
let searcher = VyaktiSearcher::load(".vyakti/my-index", backend, embedding_provider).await?;
// Search with large k to get more candidates
let results = searcher.search("machine learning", 20).await?;
// Filter for highly relevant results only (score < 0.3)
let relevant_results: Vec<_> = results
.into_iter()
.filter(|r| r.score < 0.3)
.collect();
println!("Found {} highly relevant results:", relevant_results.len());
for result in relevant_results {
println!(" Score: {:.4}, Text: {}", result.score, result.text);
}
Ok(())
}
brew install llvm libomp cmake
sudo apt-get install build-essential cmake clang libomp-dev pkg-config
# Using chocolatey
choco install llvm cmake
git clone https://github.com/yourusername/vyakti.git
cd vyakti
# Build all crates
cargo build --release --workspace
# Run tests
cargo test --workspace
# Install CLI globally
cargo install --path crates/vyakti-cli
Add to your Cargo.toml:
[dependencies]
vyakti-core = "0.1.0"
vyakti-backend-hnsw = "0.1.0" # Or other backends
tokio = { version = "1", features = ["full"] }
use vyakti_core::{VyaktiBuilder, VyaktiSearcher};
use vyakti_backend_hnsw::HnswBackend;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider, ensure_model};
use vyakti_common::BackendConfig;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Setup
let backend_config = BackendConfig::default();
let model_path = ensure_model(None).await?;
let embedding_config = LlamaCppConfig {
model_path,
n_gpu_layers: 0, // Use 32 for GPU acceleration
n_ctx: 512,
n_threads: num_cpus::get() as u32,
dimension: 1024,
normalize: true,
};
let embedding_provider = Arc::new(LlamaCppProvider::new(embedding_config)?);
// Build index
let backend = Box::new(HnswBackend::with_config(backend_config.clone()));
let mut builder = VyaktiBuilder::new(backend, embedding_provider.clone());
builder.add_text("Document 1 content", None);
builder.add_text("Document 2 content", None);
builder.build_index(".vyakti/my-index").await?;
// Search index
let backend = Box::new(HnswBackend::with_config(backend_config));
let searcher = VyaktiSearcher::load(
".vyakti/my-index",
backend,
embedding_provider,
).await?;
let results = searcher.search("query", 10).await?;
Ok(())
}
use vyakti_common::BackendConfig;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider};
use std::path::PathBuf;
// Customize backend configuration
let backend_config = BackendConfig {
graph_degree: 32,
build_complexity: 64,
search_complexity: 32,
};
// Use custom model with GPU acceleration
let embedding_config = LlamaCppConfig {
model_path: PathBuf::from("/path/to/custom-model.gguf"),
n_gpu_layers: 32, // GPU acceleration
n_ctx: 512,
n_threads: 8,
dimension: 1024, // Match your model's dimension
normalize: true,
};
let embedding_provider = Arc::new(
LlamaCppProvider::new(embedding_config)?
);
let backend = Box::new(HnswBackend::with_config(backend_config));
let mut builder = VyaktiBuilder::new(backend, embedding_provider);
// Add documents with metadata
use std::collections::HashMap;
let mut metadata = HashMap::new();
metadata.insert("category".to_string(), serde_json::json!("technology"));
metadata.insert("year".to_string(), serde_json::json!("2024"));
builder.add_text("Machine learning advances", Some(metadata));
Note: Metadata filtering is now fully implemented! See Metadata Filtering section for examples.
The Vyakti CLI provides a complete interface for building and searching vector indexes with extensive configuration options.
Build a new index from documents with automatic chunking and embedding:
# Basic usage - builds index with default settings
vyakti build my-docs --input ./documents
# Full example with all options
vyakti build my-docs \
--input ./documents \
--output .vyakti \
--chunk-size 256 \
--chunk-overlap 128 \
--embedding-model mxbai-embed-large \
--embedding-dimension 1024 \
--graph-degree 16 \
--build-complexity 64 \
--verbose
Build Parameters:
| Parameter | Description | Default | Example |
|---|---|---|---|
<name> | Index name (required) | - | my-docs |
-i, --input <PATH> | Input file or directory (required) | - | ./documents |
-o, --output <DIR> | Output directory for index | .vyakti | .vyakti |
--chunk-size <SIZE> | Chunk size in tokens | 256 | 512 |
--chunk-overlap <SIZE> | Overlap between chunks in tokens | 128 | 64 |
--enable-code-chunking | Enable AST-aware code chunking and index code files (.py, .rs, .java, .ts, .tsx, .cs, .js, .jsx, .go, .c, .cpp, .swift, .kt, .rb, .php) | false | - |
--no-chunking | Disable chunking (use whole docs) | false | - |
--embedding-model <MODEL> | Model name (for display only) | mxbai-embed-large | - |
--embedding-dimension <DIM> | Embedding vector dimension | 1024 | - |
--model-path <PATH> | Path to custom GGUF model file | Auto-download | ./model.gguf |
--gpu-layers <N> | GPU layers to offload (0=CPU only) | 0 | 32 |
--model-threads <N> | Threads for inference | Auto-detect | 8 |
--graph-degree <N> | Max connections per node | 16 | 32 |
--build-complexity <N> | Build quality (higher = better) | 64 | 128 |
-v, --verbose | Verbose output | false | - |
Chunking Examples:
# Default chunking (256 tokens, 128 overlap) - indexes all supported text and document formats
vyakti build docs --input ./files
# Custom chunk sizes for larger context
vyakti build docs --input ./files --chunk-size 512 --chunk-overlap 256
# AST-aware code chunking - indexes all text/document formats AND code files
# Preserves function/class boundaries for better code search
vyakti build code --input ./src --enable-code-chunking
# Index a mixed project (documentation + code)
# Supports: .txt, .md, .json, .yaml, .toml, .csv, .html, .pdf, .ipynb, .docx, .xlsx, .pptx
# Plus code files when --enable-code-chunking is used
vyakti build my-project --input ./project --enable-code-chunking
# No chunking (index whole documents)
vyakti build docs --input ./files --no-chunking
GPU and Model Examples:
# Default: CPU-only with auto-downloaded model
vyakti build docs --input ./files
# GPU acceleration (offload 32 layers to GPU)
vyakti build docs --input ./files --gpu-layers 32
# Custom GGUF model
vyakti build docs --input ./files \
--model-path ./my-model.gguf \
--embedding-dimension 768
# Maximum GPU offload with custom threads
vyakti build docs --input ./files \
--gpu-layers 999 \
--model-threads 16
Note: On first run, Vyakti automatically downloads mxbai-embed-large-v1 (~500MB) from HuggingFace. The model is cached in ~/.vyakti/models/ for future use.
Search an existing index with customizable parameters:
# Basic search
vyakti search my-docs "vector database concepts"
# Advanced search with custom model and more results
vyakti search my-docs "machine learning" \
--top-k 20 \
--embedding-model mxbai-embed-large \
--embedding-dimension 1024 \
--verbose
Search Parameters:
| Parameter | Description | Default | Example |
|---|---|---|---|
<name> | Index name (required) | - | my-docs |
<query> | Search query (required) | - | "vector database" |
-k, --top-k <N> | Number of results to return (before filtering) | 10 | 20 |
-i, --index-dir <DIR> | Index directory | .vyakti | .vyakti |
--embedding-model <MODEL> | Must match build model | mxbai-embed-large | nomic-embed-text |
--embedding-dimension <DIM> | Must match build dimension | 1024 | 768 |
--max-score <THRESHOLD> | Filter results by maximum score (lower = more relevant) | - | 0.5 |
--min-relevance <LEVEL> | Filter by relevance level (highly/moderately/weakly) | - | highly |
--show-relevance | Show relevance labels with each result | false | - |
-v, --verbose | Verbose output | false | - |
Important: The --embedding-model and --embedding-dimension parameters must match the values used when building the index.
Search Examples:
# Search with default model
vyakti search docs "machine learning algorithms" -k 10
# Search index built with nomic-embed-text
vyakti search docs "neural networks" \
--embedding-model nomic-embed-text \
--embedding-dimension 768 \
-k 15
# Verbose output shows model info and timing
vyakti search docs "deep learning" -v
# Filter by maximum score (only results with score ≤ 0.5)
vyakti search docs "machine learning" --max-score 0.5
# Filter by relevance level (highly relevant: score < 0.3)
vyakti search docs "neural networks" --min-relevance highly
# Show relevance labels with each result
vyakti search docs "vector database" --show-relevance
# Combine filtering with relevance labels
vyakti search docs "AI algorithms" \
--min-relevance moderately \
--show-relevance \
-k 20
Score Filtering:
The CLI supports two ways to filter results by relevance:
Direct Score Threshold (--max-score): Keep only results with score ≤ threshold
--max-score 0.5 keeps results with score 0.5 or lowerSemantic Relevance Levels (--min-relevance): Filter by user-friendly relevance categories
highly: score < 0.3 (highly relevant results only)moderately: score < 0.7 (moderately and highly relevant)weakly: score < 1.0 (all but completely unrelated results)Tip: Use --show-relevance to see relevance labels (Highly/Moderately/Weakly relevant) colored by relevance level.
List all available indexes:
# List all indexes in default directory
vyakti list
# List indexes in custom directory
vyakti list --index-dir /path/to/indexes
# Verbose mode shows file sizes and paths
vyakti list --verbose
Remove an index:
# Remove with confirmation prompt
vyakti remove my-docs
# Remove without confirmation
vyakti remove my-docs --yes
# Remove from custom directory
vyakti remove my-docs --index-dir /path/to/indexes --yes
# 1. Build an index with custom settings (auto-downloads model on first run)
vyakti build my-docs \
--input ./documents \
--chunk-size 256 \
--chunk-overlap 128 \
--gpu-layers 32 \
--verbose
# 2. Search the index
vyakti search my-docs "vector database storage optimization" -k 10
# 3. Build compact index for 93% storage savings
vyakti build my-docs-compact \
--input ./documents \
--compact \
--gpu-layers 32
# 4. List all indexes
vyakti list
# 5. Remove when done
vyakti remove my-docs --yes
Vyakti supports the following file types when building indexes:
Always Indexed:
.txt - Plain text filesText & Configuration Formats:
.md, .markdown - Markdown documentation.json - JSON files.yaml, .yml - YAML configuration.toml - TOML configuration.csv - CSV data files.html, .htm - HTML contentDocument Formats:
.pdf - PDF documents.ipynb - Jupyter notebooks.docx - Microsoft Word documents.xlsx - Microsoft Excel spreadsheets.pptx - Microsoft PowerPoint presentationsCode Files (with --enable-code-chunking flag):
Core Languages:
.py - Python (AST-aware chunking).rs - Rust (AST-aware chunking).java - Java (AST-aware chunking).ts, .tsx - TypeScript (AST-aware chunking).cs - C# (AST-aware chunking)Extended Languages:
.js, .jsx, .mjs, .cjs - JavaScript (AST-aware chunking).go - Go (AST-aware chunking).c, .h - C (AST-aware chunking).cpp, .cc, .cxx, .hpp, .hxx, .hh - C++ (AST-aware chunking).swift - Swift (AST-aware chunking).kt, .kts - Kotlin (AST-aware chunking).rb - Ruby (AST-aware chunking).php - PHP (AST-aware chunking)Total: 35+ file extensions supported
Example Usage:
# Index only text files (default)
vyakti build my-docs --input ./documents
# Index text files AND code files with AST-aware chunking
vyakti build my-code --input ./src --enable-code-chunking
# Index mixed directory (text + code)
vyakti build my-project --input ./project \
--enable-code-chunking \
--chunk-size 512
Note: When indexing code files, AST-aware chunking preserves function and class boundaries, resulting in more meaningful search results compared to simple text chunking.
Vyakti uses llama.cpp for embedding generation with automatic model download from HuggingFace.
mxbai-embed-large-v1 (Q4_K_M quantized)
~/.vyakti/models/mxbai-embed-large-v1.q4_k_m.ggufVyakti automatically downloads the default model on first use:
# First time running Vyakti
$ vyakti build docs --input ./files
# Output shows:
# → Initializing llama.cpp embedding provider...
# → Using model: mxbai-embed-large
# → Downloading model from HuggingFace Hub...
# → Model downloaded successfully to ~/.vyakti/models/
# ✓ Embedding provider initialized
# → Building index...
Use --gpu-layers to offload computation to GPU for faster embeddings:
# CPU-only (default)
vyakti build docs --input ./files
# GPU acceleration (offload 32 layers)
vyakti build docs --input ./files --gpu-layers 32
# Maximum GPU offload
vyakti build docs --input ./files --gpu-layers 999
Performance Impact:
You can use custom GGUF models:
# Use custom GGUF model
vyakti build docs --input ./files \
--model-path ./path/to/your-model.gguf \
--embedding-dimension <model_dimension> \
--gpu-layers 32
Important: Ensure you specify the correct dimension for your custom model. Check the model card on HuggingFace for dimension information.
Vyakti includes built-in RAG (Retrieval-Augmented Generation) capabilities for question-answering over documents.
Architecture:
Library Usage:
use vyakti_core::{ChatSession, ask_question};
use vyakti_common::GenerationConfig;
// One-shot Q&A
let answer = ask_question(
&searcher,
llm_provider, // Your TextGenerationProvider (OpenAI, Claude, etc.)
"What is vector search?",
5, // top-k documents
&GenerationConfig::default()
).await?;
// Multi-turn chat
let mut session = ChatSession::new(searcher, llm_provider, 5);
let response = session.ask("What is LEANN?", &config).await?;
LLM Integration: Implement the TextGenerationProvider trait for your preferred LLM (OpenAI, Anthropic Claude, Ollama, etc.)
See GPU_AND_CHAT_FEATURES.md for detailed documentation.
# Start with default settings
vyakti-server --port 8080 --storage-dir .vyakti
# With authentication
vyakti-server --port 8080 --storage-dir .vyakti --auth-token your-secret-token
# View all options
vyakti-server --help
Example API requests:
# Health check
curl http://localhost:8080/health
# Create a new index
curl -X POST http://localhost:8080/api/v1/indexes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{
"name": "my-docs",
"config": {
"dimension": 768,
"graph_degree": 32,
"build_complexity": 64
}
}'
# Add documents to an index
curl -X POST http://localhost:8080/api/v1/indexes/my-docs/documents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{
"documents": [
{"text": "Document 1", "metadata": {"category": "tech"}},
{"text": "Document 2", "metadata": {"category": "science"}}
]
}'
# Search an index
curl -X POST http://localhost:8080/api/v1/indexes/my-docs/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{
"query": "vector database",
"k": 10
}'
# List all indexes
curl http://localhost:8080/api/v1/indexes \
-H "Authorization: Bearer your-secret-token"
# Delete an index
curl -X DELETE http://localhost:8080/api/v1/indexes/my-docs \
-H "Authorization: Bearer your-secret-token"
Status: Planned for Phase 9 (not yet implemented)
The gRPC API will provide high-performance binary protocol access to all index operations once implemented.
Model Context Protocol (MCP) integration enables Claude Code to perform semantic searches across your codebase and documents.
# 1. Build the MCP server
cargo build --release -p vyakti-mcp
# 2. Configure Claude Code (~/.claude/claude_desktop_config.json)
{
"mcpServers": {
"vyakti": {
"command": "/Users/vijay/01-all-my-code-repos/vyakti/target/release/vyakti-mcp",
"env": {
"INDEX_DIR": "/Users/vijay/.vyakti",
"VYAKTI_BIN": "/Users/vijay/.cargo/bin/vyakti"
}
}
}
}
# 3. Restart Claude Code and test
# Ask Claude: "List my Vyakti indexes"
In Claude Code chat:
You: "Index my codebase with Vyakti using code chunking and compact mode"
Claude: [Calls vyakti_build] ✓ Index 'codebase' built successfully
You: "Search for authentication logic in the codebase"
Claude: [Calls vyakti_search] Found 10 results:
1. (score: 0.12) auth/login.rs - Authentication middleware
2. (score: 0.28) api/users.rs - User login endpoint
...
For detailed setup and usage, see mcp-server/README.md
Vyakti includes a comprehensive evaluation framework to measure and optimize search quality using industry-standard metrics.
The framework supports the following metrics:
Create a JSON file with test queries and ground truth relevance:
{
"name": "my_test_dataset",
"description": "Test queries for my domain",
"queries": [
{
"query": "machine learning basics",
"relevant_docs": ["1", "5", "7"],
"graded_relevance": {
"1": 3, // Highly relevant
"5": 2, // Relevant
"7": 1 // Somewhat relevant
}
}
]
}
Relevance Scale:
0: Not relevant1: Somewhat relevant2: Relevant3: Highly relevant# Basic evaluation
cargo run --release --bin vyakti-evaluate -- \
--index my-index \
--dataset ./evaluation/datasets/my_test.json
# With custom K values and verbose output
cargo run --release --bin vyakti-evaluate -- \
--index my-index \
--dataset ./evaluation/datasets/my_test.json \
--k-values 1,3,5,10,20,50 \
--verbose \
--output ./results.json
# Quick evaluation
./evaluation/scripts/evaluate.sh \
--index my-index \
--dataset ./evaluation/datasets/my_test.json
# With all options
./evaluation/scripts/evaluate.sh \
--index my-index \
--dataset ./evaluation/datasets/my_test.json \
--k-values 1,3,5,10,20 \
--output ./evaluation/results/ \
--verbose
Run grid search to find optimal parameters:
# Optimize for NDCG@10
./evaluation/scripts/optimize.sh \
--dataset ./evaluation/datasets/my_test.json \
--input ./documents \
--optimize-for ndcg@10 \
--graph-degree 16,32,64 \
--search-complexity 16,32,64,128 \
--chunk-size 128,256,512 \
--output ./evaluation/optimization/
The script will:
Compare two indexes side-by-side:
./evaluation/scripts/compare.sh \
--index-a baseline-index \
--index-b optimized-index \
--dataset ./evaluation/datasets/my_test.json \
--output ./evaluation/comparison/
Shows:
Good Metrics:
Optimization Recommendations:
| Problem | Solution |
|---|---|
| Low Precision | Increase search_complexity, better chunking |
| Low Recall | Increase graph_degree, increase top-K |
| Slow Search | Decrease search_complexity, use compact mode |
| Poor Ranking (MAP) | Better embedding model, optimize chunk_size |
| First Result Poor (MRR) | Tune search_complexity, metadata filtering |
# 1. Build test index
vyakti build test-index --input ./test_docs --compact
# 2. Create evaluation dataset (see format above)
cat > test_dataset.json << 'EOF'
{
"name": "quick_test",
"description": "Quick sanity check",
"queries": [
{"query": "test query 1", "relevant_docs": ["1", "2"]},
{"query": "test query 2", "relevant_docs": ["3"]}
]
}
EOF
# 3. Run baseline evaluation
./evaluation/scripts/evaluate.sh \
--index test-index \
--dataset test_dataset.json
# 4. Optimize parameters
./evaluation/scripts/optimize.sh \
--dataset test_dataset.json \
--input ./test_docs \
--optimize-for ndcg@10
# 5. Build production index with best params
vyakti build prod-index \
--input ./test_docs \
--graph-degree 32 \
--chunk-size 256 \
--compact
# 6. Verify improvement
./evaluation/scripts/compare.sh \
--index-a test-index \
--index-b prod-index \
--dataset test_dataset.json
For detailed documentation, see evaluation/README.md
vyakti/
├── crates/
│ ├── vyakti-core/ # Core library (Builder, Searcher, API)
│ ├── vyakti-backend-hnsw/ # HNSW backend implementation
│ ├── vyakti-backend-diskann/ # DiskANN backend implementation
│ ├── vyakti-embedding/ # Embedding computation layer
│ ├── vyakti-server/ # REST & gRPC server
│ ├── vyakti-cli/ # Command-line interface
│ ├── vyakti-storage/ # Storage layer (CSR, memory mapping)
│ ├── vyakti-proto/ # Protocol buffers definitions
│ └── vyakti-common/ # Shared utilities and types
├── mcp-server/ # MCP server for Claude Code integration
├── benches/ # Performance benchmarks
├── examples/ # Example applications
├── docs/ # Documentation
└── tests/ # Integration tests
| Crate | Purpose | Exports |
|---|---|---|
vyakti-core | Main API surface | LeannBuilder, LeannSearcher, LeannChat |
leann-backend-* | Vector search backends | Backend implementations |
vyakti-embedding | Embedding models | EmbeddingModel, EmbeddingServer |
vyakti-server | Network server | REST/gRPC endpoints |
vyakti-cli | CLI interface | Binary executable |
vyakti-storage | Persistence layer | CSR format, memory mapping |
vyakti-common | Shared utilities | Error types, config, traits |
Based on the original Python LEANN implementation, Vyakti aims to achieve:
| Operation | Python LEANN | Target (Rust) | Expected Speedup |
|---|---|---|---|
| Index Build (1M docs) | 180s | 12s | 15x |
| Search (Top-10) | 45ms | 0.8ms | 56x |
| Embedding Compute | 120ms | 8ms | 15x |
| Index Load Time | 2.3s | 0.05s | 46x |
| Memory Usage | 4.2GB | 0.8GB | 5.2x less |
Note: Formal benchmarks are in development. Performance numbers from Python LEANN paper.
| Backend | Full Vectors | LEANN Compact | Savings |
|---|---|---|---|
| HNSW | 512MB | 15MB | 96.7% |
| DiskANN* | 512MB | 28MB | 94.5% |
*DiskANN backend implementation in progress
Vyakti supports powerful metadata filtering with SQL-like operators that can be applied to search results. Filters use AND logic, meaning all filter conditions must be satisfied for a result to be included.
| Operator | Description | Example |
|---|---|---|
== | Equal to | {"category": {"==": "tech"}} |
!= | Not equal to | {"status": {"!=": "draft"}} |
< | Less than | {"price": {"<": 100}} |
<= | Less than or equal | {"rating": {"<=": 5}} |
> | Greater than | {"views": {">": 1000}} |
>= | Greater than or equal | {"year": {">=": 2024}} |
in | Value is in list | {"tag": {"in": ["rust", "python"]}} |
not_in | Value not in list | {"status": {"not_in": ["draft", "archived"]}} |
contains | String contains substring | {"title": {"contains": "machine learning"}} |
starts_with | String starts with prefix | {"filename": {"starts_with": "test_"}} |
ends_with | String ends with suffix | {"url": {"ends_with": ".pdf"}} |
is_true | Value is truthy | {"published": {"is_true": true}} |
is_false | Value is falsy | {"archived": {"is_false": false}} |
use vyakti_core::VyaktiSearcher;
use vyakti_common::{FilterOperator, FilterValue, MetadataFilters};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup searcher (backend + embedding provider)
let searcher = setup_searcher().await?;
// Create metadata filters
let mut filters = MetadataFilters::new();
// Filter by category == "technology"
let mut category_filter = HashMap::new();
category_filter.insert(
FilterOperator::Eq,
FilterValue::String("technology".to_string()),
);
filters.insert("category".to_string(), category_filter);
// Search with filters
let results = searcher.search_with_filters(
"machine learning trends",
10,
Some(&filters),
).await?;
println!("Found {} results", results.len());
for result in results {
println!(" {}: {}", result.score, result.text);
}
Ok(())
}
// Filter by multiple conditions - ALL must be satisfied
let mut filters = MetadataFilters::new();
// category == "technology"
let mut category_filter = HashMap::new();
category_filter.insert(
FilterOperator::Eq,
FilterValue::String("technology".to_string()),
);
filters.insert("category".to_string(), category_filter);
// year >= 2024
let mut year_filter = HashMap::new();
year_filter.insert(FilterOperator::Ge, FilterValue::Integer(2024));
filters.insert("year".to_string(), year_filter);
// published == true
let mut published_filter = HashMap::new();
published_filter.insert(FilterOperator::IsTrue, FilterValue::Bool(true));
filters.insert("published".to_string(), published_filter);
// Search - only results matching ALL filters will be returned
let results = searcher.search_with_filters(
"AI advances",
20,
Some(&filters),
).await?;
// Filter by price range: 100 <= price <= 500
let mut filters = MetadataFilters::new();
let mut price_filter = HashMap::new();
price_filter.insert(FilterOperator::Ge, FilterValue::Integer(100)); // >= 100
price_filter.insert(FilterOperator::Le, FilterValue::Integer(500)); // <= 500
filters.insert("price".to_string(), price_filter);
let results = searcher.search_with_filters(
"laptop recommendations",
15,
Some(&filters),
).await?;
// Find documents where author is in a specific list
let mut filters = MetadataFilters::new();
let mut author_filter = HashMap::new();
author_filter.insert(
FilterOperator::In,
FilterValue::List(vec![
FilterValue::String("Alice".to_string()),
FilterValue::String("Bob".to_string()),
FilterValue::String("Charlie".to_string()),
]),
);
filters.insert("author".to_string(), author_filter);
let results = searcher.search_with_filters(
"research papers",
10,
Some(&filters),
).await?;
// Filter by file extension and content
let mut filters = MetadataFilters::new();
// filename ends with ".rs"
let mut filename_filter = HashMap::new();
filename_filter.insert(
FilterOperator::EndsWith,
FilterValue::String(".rs".to_string()),
);
filters.insert("filename".to_string(), filename_filter);
// content contains "async"
let mut content_filter = HashMap::new();
content_filter.insert(
FilterOperator::Contains,
FilterValue::String("async".to_string()),
);
filters.insert("description".to_string(), content_filter);
let results = searcher.search_with_filters(
"async Rust code examples",
10,
Some(&filters),
).await?;
The filter engine automatically handles type coercion for numeric comparisons:
// These will work even if metadata values are stored as different types
let mut filters = MetadataFilters::new();
// Works if "age" is stored as integer, float, or string "25"
let mut age_filter = HashMap::new();
age_filter.insert(FilterOperator::Gt, FilterValue::Integer(25));
filters.insert("age".to_string(), age_filter);
let mut filters = MetadataFilters::new();
// published == true
let mut published_filter = HashMap::new();
published_filter.insert(FilterOperator::IsTrue, FilterValue::Bool(true));
filters.insert("published".to_string(), published_filter);
// year >= 2023
let mut year_filter = HashMap::new();
year_filter.insert(FilterOperator::Ge, FilterValue::Integer(2023));
filters.insert("year".to_string(), year_filter);
// category in ["AI", "ML", "Data Science"]
let mut category_filter = HashMap::new();
category_filter.insert(
FilterOperator::In,
FilterValue::List(vec![
FilterValue::String("AI".to_string()),
FilterValue::String("ML".to_string()),
FilterValue::String("Data Science".to_string()),
]),
);
filters.insert("category".to_string(), category_filter);
// First search with semantic similarity
let all_results = searcher.search("machine learning", 50).await?;
// Then apply metadata filters
let engine = MetadataFilterEngine::new();
let filtered_results = engine.apply_filters(all_results, &filters);
// Further filter by relevance score
let high_quality_results: Vec<_> = filtered_results
.into_iter()
.filter(|r| r.score < 0.3) // High relevance only
.collect();
Vyakti supports hybrid search that combines the strengths of both semantic vector search and keyword-based (BM25) search. This is especially useful for code search, technical documentation, and scenarios where exact keyword matches are important alongside semantic understanding.
Semantic Vector Search excels at:
Keyword (BM25) Search excels at:
Hybrid Search combines both approaches to get the best of both worlds!
# CLI: Hybrid search is enabled by default
vyakti build my-code --input ./src
# Hybrid + Compact mode (93% storage savings!) - RECOMMENDED
vyakti build my-code --input ./src --compact
# Disable hybrid search (vector-only mode)
vyakti build my-code --input ./src --no-hybrid
# Custom BM25 parameters (with hybrid enabled)
vyakti build my-code --input ./src --bm25-k1 1.5 --bm25-b 0.6
# RRF (Reciprocal Rank Fusion) - default, balanced
vyakti search my-code "authentication handler" --fusion rrf
# Weighted fusion (configurable balance)
vyakti search my-code "database connection" --fusion weighted --fusion-param 0.7
# Cascade (keyword first, fallback to vector)
vyakti search my-code "login function" --fusion cascade --fusion-param 5
# Vector-only (disable keyword search)
vyakti search my-code "error handling" --fusion vector-only
# Keyword-only (BM25 only)
vyakti search my-code "handleRequest" --fusion keyword-only
Combines results based on their ranks, not raw scores. Simple and effective.
use vyakti_core::{HybridSearcher, FusionStrategy};
let strategy = FusionStrategy::RRF { k: 60 };
let searcher = HybridSearcher::load(
&index_path,
backend,
embedding_provider,
strategy,
documents,
)?;
let results = searcher.search("vector database", 10).await?;
Formula: score(doc) = Σ 1/(k + rank_in_result_set)
Best for: General-purpose hybrid search, no tuning needed
Parameter k: Higher values (default: 60) give more equal weight to both modes
Combines normalized scores with configurable weight parameter α.
let strategy = FusionStrategy::Weighted { alpha: 0.7 };
Formula: score(doc) = α * norm(bm25_score) + (1-α) * norm(vector_score)
Best for: When you want explicit control over vector vs keyword balance
Parameter alpha:
0.0 = Pure vector search0.5 = Equal weight to both1.0 = Pure keyword searchTry keyword search first, fallback to vector search if insufficient results.
let strategy = FusionStrategy::Cascade { threshold: 5 };
Best for: Technical documentation where exact matches should be prioritized
Parameter threshold: Minimum keyword results needed before using vector search
Disable hybrid mode, use only semantic vector search.
let strategy = FusionStrategy::VectorOnly;
Best for: Natural language queries, conceptual searches
Use only BM25 keyword search (fastest).
let strategy = FusionStrategy::KeywordOnly;
Best for: Exact identifier lookup, very fast search
use vyakti_core::{VyaktiBuilder, HybridSearcher, FusionStrategy};
use vyakti_keyword::KeywordConfig;
use vyakti_backend_hnsw::HnswBackend;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Build hybrid index
let backend = Box::new(HnswBackend::new());
let embedding_provider = Arc::new(/* your provider */);
let config = BackendConfig::default();
let mut builder = VyaktiBuilder::with_config(backend, embedding_provider.clone(), config);
// Add documents
builder.add_text("Rust async programming guide", None);
builder.add_text("Python asyncio tutorial", None);
// Build with keyword indexing
let keyword_config = KeywordConfig {
enabled: true,
k1: 1.2, // Term frequency saturation
b: 0.75, // Length normalization
};
let index_path = builder.build_index_hybrid("my-index", Some(keyword_config)).await?;
// Search with hybrid fusion
let backend = Box::new(HnswBackend::new());
let documents = /* load documents */;
let searcher = HybridSearcher::load(
&index_path,
backend,
embedding_provider,
FusionStrategy::RRF { k: 60 },
documents,
)?;
let results = searcher.search("async programming patterns", 10).await?;
Ok(())
}
The BM25 algorithm has two main parameters:
k1 (term frequency saturation):
b (document length normalization):
# Tune for code search (less length normalization)
vyakti build my-code --input ./src --hybrid --bm25-k1 1.5 --bm25-b 0.6
# Tune for natural language docs (more length normalization)
vyakti build my-docs --input ./docs --hybrid --bm25-k1 1.2 --bm25-b 0.85
The Vyakti MCP server fully supports hybrid search:
{
"name": "vyakti_build",
"arguments": {
"name": "my-code",
"input_path": "./src",
"hybrid": true,
"bm25_k1": 1.2,
"bm25_b": 0.75
}
}
{
"name": "vyakti_search",
"arguments": {
"name": "my-code",
"query": "authentication middleware",
"top_k": 10,
"fusion": "rrf",
"fusion_param": 60
}
}
| Operation | Latency | Notes |
|---|---|---|
| Vector-only | ~53 µs | Baseline semantic search |
| Keyword-only | ~5 µs | Fastest, BM25 only |
| Hybrid RRF | ~95 µs | Balanced fusion |
| Hybrid Weighted | ~80 µs | Slightly faster than RRF |
| Hybrid + Compact | ~85 µs | 93% storage savings + fast search |
Storage Overhead:
✅ Use Hybrid Search when:
❌ Skip Hybrid Search when:
Start with RRF fusion - Works well out of the box, no tuning needed
Use compact mode - Hybrid + compact gives you both accuracy AND efficiency
Tune BM25 for your domain:
k1=1.5, b=0.6 (less length normalization)k1=1.2, b=0.75 (balanced)k1=1.2, b=0.5 (minimal length norm)Experiment with fusion strategies - Different queries may benefit from different strategies
Profile your workload - Use benchmarks to find the best strategy for your use case
# llama.cpp settings (optional - uses auto-download by default)
VYAKTI_MODEL_PATH=~/.vyakti/models/mxbai-embed-large-v1.q4_k_m.gguf # Custom model path
VYAKTI_GPU_LAYERS=0 # Number of GPU layers to offload (0 = CPU only)
VYAKTI_MODEL_THREADS=8 # Number of threads for inference (default: auto-detect)
# Server settings
VYAKTI_PORT=8080
VYAKTI_STORAGE_DIR=.vyakti
VYAKTI_AUTH_TOKEN=your-secret-token
# Logging
RUST_LOG=info # Options: trace, debug, info, warn, error
RUST_BACKTRACE=1 # Enable backtraces for debugging
Most configuration is done via CLI parameters rather than environment variables:
Chunking Configuration:
--chunk-size - Default: 256 tokens--chunk-overlap - Default: 128 tokens--enable-code-chunking - Enable AST-aware code chunking--no-chunking - Disable chunking entirelyEmbedding Configuration:
--embedding-model - Default: mxbai-embed-large--embedding-dimension - Default: 1024Backend Configuration:
--graph-degree - Default: 16 (max connections per node)--build-complexity - Default: 64 (higher = better quality)See vyakti build --help for all options.
Customize HNSW backend behavior programmatically:
use vyakti_common::BackendConfig;
let config = BackendConfig {
dimension: 768, // Must match embedding model
graph_degree: 32, // Higher = better recall, more storage
build_complexity: 64, // Higher = better graph quality
search_complexity: 32, // Higher = more accurate search
compact: true, // Enable 97% storage savings
..Default::default()
};
Status: Planned for Phase 9
TOML/YAML configuration file support is planned for future releases.
# Install Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install development tools
cargo install cargo-watch cargo-edit cargo-outdated
# Development build
cargo build
# Release build with optimizations
cargo build --release
# Run tests
cargo test --workspace
# Run with examples
cargo run --example basic_search
# Watch mode for development
cargo watch -x "test --workspace"
# Format code
cargo fmt --all
# Lint code
cargo clippy --all-targets --all-features
# Check for security vulnerabilities
cargo audit
# Generate documentation
cargo doc --no-deps --open
# Run all benchmarks
cargo bench --workspace
# Specific benchmark
cargo bench --bench search_performance
# With flamegraph profiling
cargo flamegraph --bench search_performance
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
git checkout -b feature/amazing-featurecargo test && cargo clippygit commit -m 'Add amazing feature'git push origin feature/amazing-featureThis project is licensed under the MIT License - see the LICENSE file for details.
If you use Vyakti in your research, please cite:
@article{leann2024,
title={LEANN: Low-Storage Vector Index with Graph-Based Selective Recomputation},
author={Wang, Yichuan},
journal={arXiv preprint arXiv:2506.08276},
year={2024}
}
6 commits
4 commits
Python
67.8%
Rust
30.4%
Shell
1.4%
Vyakti is a high-performance Rust implementation of LEANN (Low-Storage Vector Index), a vector database that achieves 97% storage savings through graph-based selective recomputation.
✅ PHASE 9D COMPLETE - 171 tests passing with LEANN compact mode
What Works Now:
In Development:
Vyakti is designed as a complete product that can be used in four modes:
✨ Zero Configuration Required!
Vyakti uses llama.cpp for embeddings with automatic model download. No external services needed!
System Requirements:
What Happens on First Run:
mxbai-embed-large-v1 (Q4_K_M, ~500MB) from HuggingFace~/.vyakti/models/ for future useGPU Acceleration (Optional):
--gpu-layers 32 flag to enable GPU acceleration# Install from crates.io
cargo install vyakti
# Or build from source
git clone https://github.com/yourusername/vyakti.git
cd vyakti
cargo install --path crates/vyakti-cli
# Create an index from documents (hybrid search enabled by default!)
# First run will auto-download mxbai-embed-large model (~500MB)
vyakti build my-docs --input ./documents
# Build with LEANN compact mode (93% storage savings + 60% faster!)
vyakti build my-docs --input ./documents --compact
# GPU acceleration (offload 32 layers to GPU for faster embeddings)
vyakti build my-docs --input ./documents --gpu-layers 32
# Custom model and GPU
vyakti build my-docs --input ./documents --model-path ./my-model.gguf --gpu-layers 64
# Disable hybrid search (vector-only mode)
vyakti build my-docs --input ./documents --no-hybrid
# Search the index (works transparently with both hybrid and vector-only)
vyakti search my-docs "vector database concepts" --top-k 10
# Search with GPU (uses same flags as build)
vyakti search my-docs "machine learning" --gpu-layers 32
# List all indexes
vyakti list
# Remove an index
vyakti remove my-docs --yes
Storage Savings:
Performance Gains:
Search results include a score representing the distance between your query and each result. Lower scores indicate higher relevance.
Score Interpretation (Cosine Distance):
Example:
vyakti search my-docs "machine learning" -k 5
Output:
Results for "machine learning":
1. Score: 0.12, Text: "Introduction to machine learning algorithms" # Highly relevant
2. Score: 0.28, Text: "Deep learning and neural networks" # Highly relevant
3. Score: 0.45, Text: "Data science best practices" # Moderately relevant
4. Score: 0.68, Text: "Software engineering patterns" # Weakly relevant
5. Score: 0.89, Text: "Cooking recipes database" # Unrelated
Tips for Better Results:
--top-k with a higher value, then filter by scoreuse vyakti_core::{VyaktiBuilder, VyaktiSearcher};
use vyakti_backend_hnsw::HnswBackend;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider, ensure_model};
use vyakti_common::BackendConfig;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Ensure model is available (auto-downloads if needed)
let model_path = ensure_model(None).await?;
// Create embedding provider (llama.cpp with mxbai-embed-large)
let embedding_config = LlamaCppConfig {
model_path,
n_gpu_layers: 0, // CPU-only (use 32 for GPU)
n_ctx: 512,
n_threads: num_cpus::get() as u32,
dimension: 1024,
normalize: true,
};
let embedding_provider = Arc::new(LlamaCppProvider::new(embedding_config)?);
// Create backend
let backend_config = BackendConfig::default();
let backend = Box::new(HnswBackend::with_config(backend_config.clone()));
// Create builder and add documents
let mut builder = VyaktiBuilder::new(backend, embedding_provider.clone());
builder.add_text("LEANN is a vector database", None);
builder.add_text("Rust is fast and memory-safe", None);
// Build and save index (normal mode)
builder.build_index(".vyakti/my-index").await?;
// OR: Build in compact mode for 93% storage savings + faster search!
// let (path, stats) = builder.build_index_compact(".vyakti/my-index", None).await?;
// println!("Storage savings: {:.1}%", stats.savings_percent);
// Load and search
let backend = Box::new(HnswBackend::with_config(backend_config));
let searcher = VyaktiSearcher::load(
".vyakti/my-index",
backend,
embedding_provider,
).await?;
let results = searcher.search("database", 5).await?;
for result in results {
println!("Score: {:.4}, Text: {}", result.score, result.text);
}
Ok(())
}
use vyakti_core::{VyaktiBuilder, VyaktiSearcher};
use vyakti_backend_hnsw::HnswBackend;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider, ensure_model};
use vyakti_common::BackendConfig;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let config = BackendConfig::default();
let model_path = ensure_model(None).await?;
let embedding_config = LlamaCppConfig {
model_path,
n_gpu_layers: 0,
n_ctx: 512,
n_threads: num_cpus::get() as u32,
dimension: 1024,
normalize: true,
};
let embedding_provider = Arc::new(LlamaCppProvider::new(embedding_config)?);
// Load searcher
let backend = Box::new(HnswBackend::with_config(config));
let searcher = VyaktiSearcher::load(".vyakti/my-index", backend, embedding_provider).await?;
// Search with large k to get more candidates
let results = searcher.search("machine learning", 20).await?;
// Filter for highly relevant results only (score < 0.3)
let relevant_results: Vec<_> = results
.into_iter()
.filter(|r| r.score < 0.3)
.collect();
println!("Found {} highly relevant results:", relevant_results.len());
for result in relevant_results {
println!(" Score: {:.4}, Text: {}", result.score, result.text);
}
Ok(())
}
brew install llvm libomp cmake
sudo apt-get install build-essential cmake clang libomp-dev pkg-config
# Using chocolatey
choco install llvm cmake
git clone https://github.com/yourusername/vyakti.git
cd vyakti
# Build all crates
cargo build --release --workspace
# Run tests
cargo test --workspace
# Install CLI globally
cargo install --path crates/vyakti-cli
Add to your Cargo.toml:
[dependencies]
vyakti-core = "0.1.0"
vyakti-backend-hnsw = "0.1.0" # Or other backends
tokio = { version = "1", features = ["full"] }
use vyakti_core::{VyaktiBuilder, VyaktiSearcher};
use vyakti_backend_hnsw::HnswBackend;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider, ensure_model};
use vyakti_common::BackendConfig;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Setup
let backend_config = BackendConfig::default();
let model_path = ensure_model(None).await?;
let embedding_config = LlamaCppConfig {
model_path,
n_gpu_layers: 0, // Use 32 for GPU acceleration
n_ctx: 512,
n_threads: num_cpus::get() as u32,
dimension: 1024,
normalize: true,
};
let embedding_provider = Arc::new(LlamaCppProvider::new(embedding_config)?);
// Build index
let backend = Box::new(HnswBackend::with_config(backend_config.clone()));
let mut builder = VyaktiBuilder::new(backend, embedding_provider.clone());
builder.add_text("Document 1 content", None);
builder.add_text("Document 2 content", None);
builder.build_index(".vyakti/my-index").await?;
// Search index
let backend = Box::new(HnswBackend::with_config(backend_config));
let searcher = VyaktiSearcher::load(
".vyakti/my-index",
backend,
embedding_provider,
).await?;
let results = searcher.search("query", 10).await?;
Ok(())
}
use vyakti_common::BackendConfig;
use vyakti_embedding::{LlamaCppConfig, LlamaCppProvider};
use std::path::PathBuf;
// Customize backend configuration
let backend_config = BackendConfig {
graph_degree: 32,
build_complexity: 64,
search_complexity: 32,
};
// Use custom model with GPU acceleration
let embedding_config = LlamaCppConfig {
model_path: PathBuf::from("/path/to/custom-model.gguf"),
n_gpu_layers: 32, // GPU acceleration
n_ctx: 512,
n_threads: 8,
dimension: 1024, // Match your model's dimension
normalize: true,
};
let embedding_provider = Arc::new(
LlamaCppProvider::new(embedding_config)?
);
let backend = Box::new(HnswBackend::with_config(backend_config));
let mut builder = VyaktiBuilder::new(backend, embedding_provider);
// Add documents with metadata
use std::collections::HashMap;
let mut metadata = HashMap::new();
metadata.insert("category".to_string(), serde_json::json!("technology"));
metadata.insert("year".to_string(), serde_json::json!("2024"));
builder.add_text("Machine learning advances", Some(metadata));
Note: Metadata filtering is now fully implemented! See Metadata Filtering section for examples.
The Vyakti CLI provides a complete interface for building and searching vector indexes with extensive configuration options.
Build a new index from documents with automatic chunking and embedding:
# Basic usage - builds index with default settings
vyakti build my-docs --input ./documents
# Full example with all options
vyakti build my-docs \
--input ./documents \
--output .vyakti \
--chunk-size 256 \
--chunk-overlap 128 \
--embedding-model mxbai-embed-large \
--embedding-dimension 1024 \
--graph-degree 16 \
--build-complexity 64 \
--verbose
Build Parameters:
| Parameter | Description | Default | Example |
|---|---|---|---|
<name> | Index name (required) | - | my-docs |
-i, --input <PATH> | Input file or directory (required) | - | ./documents |
-o, --output <DIR> | Output directory for index | .vyakti | .vyakti |
--chunk-size <SIZE> | Chunk size in tokens | 256 | 512 |
--chunk-overlap <SIZE> | Overlap between chunks in tokens | 128 | 64 |
--enable-code-chunking | Enable AST-aware code chunking and index code files (.py, .rs, .java, .ts, .tsx, .cs, .js, .jsx, .go, .c, .cpp, .swift, .kt, .rb, .php) | false | - |
--no-chunking | Disable chunking (use whole docs) | false | - |
--embedding-model <MODEL> | Model name (for display only) | mxbai-embed-large | - |
--embedding-dimension <DIM> | Embedding vector dimension | 1024 | - |
--model-path <PATH> | Path to custom GGUF model file | Auto-download | ./model.gguf |
--gpu-layers <N> | GPU layers to offload (0=CPU only) | 0 | 32 |
--model-threads <N> | Threads for inference | Auto-detect | 8 |
--graph-degree <N> | Max connections per node | 16 | 32 |
--build-complexity <N> | Build quality (higher = better) | 64 | 128 |
-v, --verbose | Verbose output | false | - |
Chunking Examples:
# Default chunking (256 tokens, 128 overlap) - indexes all supported text and document formats
vyakti build docs --input ./files
# Custom chunk sizes for larger context
vyakti build docs --input ./files --chunk-size 512 --chunk-overlap 256
# AST-aware code chunking - indexes all text/document formats AND code files
# Preserves function/class boundaries for better code search
vyakti build code --input ./src --enable-code-chunking
# Index a mixed project (documentation + code)
# Supports: .txt, .md, .json, .yaml, .toml, .csv, .html, .pdf, .ipynb, .docx, .xlsx, .pptx
# Plus code files when --enable-code-chunking is used
vyakti build my-project --input ./project --enable-code-chunking
# No chunking (index whole documents)
vyakti build docs --input ./files --no-chunking
GPU and Model Examples:
# Default: CPU-only with auto-downloaded model
vyakti build docs --input ./files
# GPU acceleration (offload 32 layers to GPU)
vyakti build docs --input ./files --gpu-layers 32
# Custom GGUF model
vyakti build docs --input ./files \
--model-path ./my-model.gguf \
--embedding-dimension 768
# Maximum GPU offload with custom threads
vyakti build docs --input ./files \
--gpu-layers 999 \
--model-threads 16
Note: On first run, Vyakti automatically downloads mxbai-embed-large-v1 (~500MB) from HuggingFace. The model is cached in ~/.vyakti/models/ for future use.
Search an existing index with customizable parameters:
# Basic search
vyakti search my-docs "vector database concepts"
# Advanced search with custom model and more results
vyakti search my-docs "machine learning" \
--top-k 20 \
--embedding-model mxbai-embed-large \
--embedding-dimension 1024 \
--verbose
Search Parameters:
| Parameter | Description | Default | Example |
|---|---|---|---|
<name> | Index name (required) | - | my-docs |
<query> | Search query (required) | - | "vector database" |
-k, --top-k <N> | Number of results to return (before filtering) | 10 | 20 |
-i, --index-dir <DIR> | Index directory | .vyakti | .vyakti |
--embedding-model <MODEL> | Must match build model | mxbai-embed-large | nomic-embed-text |
--embedding-dimension <DIM> | Must match build dimension | 1024 | 768 |
--max-score <THRESHOLD> | Filter results by maximum score (lower = more relevant) | - | 0.5 |
--min-relevance <LEVEL> | Filter by relevance level (highly/moderately/weakly) | - | highly |
--show-relevance | Show relevance labels with each result | false | - |
-v, --verbose | Verbose output | false | - |
Important: The --embedding-model and --embedding-dimension parameters must match the values used when building the index.
Search Examples:
# Search with default model
vyakti search docs "machine learning algorithms" -k 10
# Search index built with nomic-embed-text
vyakti search docs "neural networks" \
--embedding-model nomic-embed-text \
--embedding-dimension 768 \
-k 15
# Verbose output shows model info and timing
vyakti search docs "deep learning" -v
# Filter by maximum score (only results with score ≤ 0.5)
vyakti search docs "machine learning" --max-score 0.5
# Filter by relevance level (highly relevant: score < 0.3)
vyakti search docs "neural networks" --min-relevance highly
# Show relevance labels with each result
vyakti search docs "vector database" --show-relevance
# Combine filtering with relevance labels
vyakti search docs "AI algorithms" \
--min-relevance moderately \
--show-relevance \
-k 20
Score Filtering:
The CLI supports two ways to filter results by relevance:
Direct Score Threshold (--max-score): Keep only results with score ≤ threshold
--max-score 0.5 keeps results with score 0.5 or lowerSemantic Relevance Levels (--min-relevance): Filter by user-friendly relevance categories
highly: score < 0.3 (highly relevant results only)moderately: score < 0.7 (moderately and highly relevant)weakly: score < 1.0 (all but completely unrelated results)Tip: Use --show-relevance to see relevance labels (Highly/Moderately/Weakly relevant) colored by relevance level.
List all available indexes:
# List all indexes in default directory
vyakti list
# List indexes in custom directory
vyakti list --index-dir /path/to/indexes
# Verbose mode shows file sizes and paths
vyakti list --verbose
Remove an index:
# Remove with confirmation prompt
vyakti remove my-docs
# Remove without confirmation
vyakti remove my-docs --yes
# Remove from custom directory
vyakti remove my-docs --index-dir /path/to/indexes --yes
# 1. Build an index with custom settings (auto-downloads model on first run)
vyakti build my-docs \
--input ./documents \
--chunk-size 256 \
--chunk-overlap 128 \
--gpu-layers 32 \
--verbose
# 2. Search the index
vyakti search my-docs "vector database storage optimization" -k 10
# 3. Build compact index for 93% storage savings
vyakti build my-docs-compact \
--input ./documents \
--compact \
--gpu-layers 32
# 4. List all indexes
vyakti list
# 5. Remove when done
vyakti remove my-docs --yes
Vyakti supports the following file types when building indexes:
Always Indexed:
.txt - Plain text filesText & Configuration Formats:
.md, .markdown - Markdown documentation.json - JSON files.yaml, .yml - YAML configuration.toml - TOML configuration.csv - CSV data files.html, .htm - HTML contentDocument Formats:
.pdf - PDF documents.ipynb - Jupyter notebooks.docx - Microsoft Word documents.xlsx - Microsoft Excel spreadsheets.pptx - Microsoft PowerPoint presentationsCode Files (with --enable-code-chunking flag):
Core Languages:
.py - Python (AST-aware chunking).rs - Rust (AST-aware chunking).java - Java (AST-aware chunking).ts, .tsx - TypeScript (AST-aware chunking).cs - C# (AST-aware chunking)Extended Languages:
.js, .jsx, .mjs, .cjs - JavaScript (AST-aware chunking).go - Go (AST-aware chunking).c, .h - C (AST-aware chunking).cpp, .cc, .cxx, .hpp, .hxx, .hh - C++ (AST-aware chunking).swift - Swift (AST-aware chunking).kt, .kts - Kotlin (AST-aware chunking).rb - Ruby (AST-aware chunking).php - PHP (AST-aware chunking)Total: 35+ file extensions supported
Example Usage:
# Index only text files (default)
vyakti build my-docs --input ./documents
# Index text files AND code files with AST-aware chunking
vyakti build my-code --input ./src --enable-code-chunking
# Index mixed directory (text + code)
vyakti build my-project --input ./project \
--enable-code-chunking \
--chunk-size 512
Note: When indexing code files, AST-aware chunking preserves function and class boundaries, resulting in more meaningful search results compared to simple text chunking.
Vyakti uses llama.cpp for embedding generation with automatic model download from HuggingFace.
mxbai-embed-large-v1 (Q4_K_M quantized)
~/.vyakti/models/mxbai-embed-large-v1.q4_k_m.ggufVyakti automatically downloads the default model on first use:
# First time running Vyakti
$ vyakti build docs --input ./files
# Output shows:
# → Initializing llama.cpp embedding provider...
# → Using model: mxbai-embed-large
# → Downloading model from HuggingFace Hub...
# → Model downloaded successfully to ~/.vyakti/models/
# ✓ Embedding provider initialized
# → Building index...
Use --gpu-layers to offload computation to GPU for faster embeddings:
# CPU-only (default)
vyakti build docs --input ./files
# GPU acceleration (offload 32 layers)
vyakti build docs --input ./files --gpu-layers 32
# Maximum GPU offload
vyakti build docs --input ./files --gpu-layers 999
Performance Impact:
You can use custom GGUF models:
# Use custom GGUF model
vyakti build docs --input ./files \
--model-path ./path/to/your-model.gguf \
--embedding-dimension <model_dimension> \
--gpu-layers 32
Important: Ensure you specify the correct dimension for your custom model. Check the model card on HuggingFace for dimension information.
Vyakti includes built-in RAG (Retrieval-Augmented Generation) capabilities for question-answering over documents.
Architecture:
Library Usage:
use vyakti_core::{ChatSession, ask_question};
use vyakti_common::GenerationConfig;
// One-shot Q&A
let answer = ask_question(
&searcher,
llm_provider, // Your TextGenerationProvider (OpenAI, Claude, etc.)
"What is vector search?",
5, // top-k documents
&GenerationConfig::default()
).await?;
// Multi-turn chat
let mut session = ChatSession::new(searcher, llm_provider, 5);
let response = session.ask("What is LEANN?", &config).await?;
LLM Integration: Implement the TextGenerationProvider trait for your preferred LLM (OpenAI, Anthropic Claude, Ollama, etc.)
See GPU_AND_CHAT_FEATURES.md for detailed documentation.
# Start with default settings
vyakti-server --port 8080 --storage-dir .vyakti
# With authentication
vyakti-server --port 8080 --storage-dir .vyakti --auth-token your-secret-token
# View all options
vyakti-server --help
Example API requests:
# Health check
curl http://localhost:8080/health
# Create a new index
curl -X POST http://localhost:8080/api/v1/indexes \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{
"name": "my-docs",
"config": {
"dimension": 768,
"graph_degree": 32,
"build_complexity": 64
}
}'
# Add documents to an index
curl -X POST http://localhost:8080/api/v1/indexes/my-docs/documents \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{
"documents": [
{"text": "Document 1", "metadata": {"category": "tech"}},
{"text": "Document 2", "metadata": {"category": "science"}}
]
}'
# Search an index
curl -X POST http://localhost:8080/api/v1/indexes/my-docs/search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your-secret-token" \
-d '{
"query": "vector database",
"k": 10
}'
# List all indexes
curl http://localhost:8080/api/v1/indexes \
-H "Authorization: Bearer your-secret-token"
# Delete an index
curl -X DELETE http://localhost:8080/api/v1/indexes/my-docs \
-H "Authorization: Bearer your-secret-token"
Status: Planned for Phase 9 (not yet implemented)
The gRPC API will provide high-performance binary protocol access to all index operations once implemented.
Model Context Protocol (MCP) integration enables Claude Code to perform semantic searches across your codebase and documents.
# 1. Build the MCP server
cargo build --release -p vyakti-mcp
# 2. Configure Claude Code (~/.claude/claude_desktop_config.json)
{
"mcpServers": {
"vyakti": {
"command": "/Users/vijay/01-all-my-code-repos/vyakti/target/release/vyakti-mcp",
"env": {
"INDEX_DIR": "/Users/vijay/.vyakti",
"VYAKTI_BIN": "/Users/vijay/.cargo/bin/vyakti"
}
}
}
}
# 3. Restart Claude Code and test
# Ask Claude: "List my Vyakti indexes"
In Claude Code chat:
You: "Index my codebase with Vyakti using code chunking and compact mode"
Claude: [Calls vyakti_build] ✓ Index 'codebase' built successfully
You: "Search for authentication logic in the codebase"
Claude: [Calls vyakti_search] Found 10 results:
1. (score: 0.12) auth/login.rs - Authentication middleware
2. (score: 0.28) api/users.rs - User login endpoint
...
For detailed setup and usage, see mcp-server/README.md
Vyakti includes a comprehensive evaluation framework to measure and optimize search quality using industry-standard metrics.
The framework supports the following metrics:
Create a JSON file with test queries and ground truth relevance:
{
"name": "my_test_dataset",
"description": "Test queries for my domain",
"queries": [
{
"query": "machine learning basics",
"relevant_docs": ["1", "5", "7"],
"graded_relevance": {
"1": 3, // Highly relevant
"5": 2, // Relevant
"7": 1 // Somewhat relevant
}
}
]
}
Relevance Scale:
0: Not relevant1: Somewhat relevant2: Relevant3: Highly relevant# Basic evaluation
cargo run --release --bin vyakti-evaluate -- \
--index my-index \
--dataset ./evaluation/datasets/my_test.json
# With custom K values and verbose output
cargo run --release --bin vyakti-evaluate -- \
--index my-index \
--dataset ./evaluation/datasets/my_test.json \
--k-values 1,3,5,10,20,50 \
--verbose \
--output ./results.json
# Quick evaluation
./evaluation/scripts/evaluate.sh \
--index my-index \
--dataset ./evaluation/datasets/my_test.json
# With all options
./evaluation/scripts/evaluate.sh \
--index my-index \
--dataset ./evaluation/datasets/my_test.json \
--k-values 1,3,5,10,20 \
--output ./evaluation/results/ \
--verbose
Run grid search to find optimal parameters:
# Optimize for NDCG@10
./evaluation/scripts/optimize.sh \
--dataset ./evaluation/datasets/my_test.json \
--input ./documents \
--optimize-for ndcg@10 \
--graph-degree 16,32,64 \
--search-complexity 16,32,64,128 \
--chunk-size 128,256,512 \
--output ./evaluation/optimization/
The script will:
Compare two indexes side-by-side:
./evaluation/scripts/compare.sh \
--index-a baseline-index \
--index-b optimized-index \
--dataset ./evaluation/datasets/my_test.json \
--output ./evaluation/comparison/
Shows:
Good Metrics:
Optimization Recommendations:
| Problem | Solution |
|---|---|
| Low Precision | Increase search_complexity, better chunking |
| Low Recall | Increase graph_degree, increase top-K |
| Slow Search | Decrease search_complexity, use compact mode |
| Poor Ranking (MAP) | Better embedding model, optimize chunk_size |
| First Result Poor (MRR) | Tune search_complexity, metadata filtering |
# 1. Build test index
vyakti build test-index --input ./test_docs --compact
# 2. Create evaluation dataset (see format above)
cat > test_dataset.json << 'EOF'
{
"name": "quick_test",
"description": "Quick sanity check",
"queries": [
{"query": "test query 1", "relevant_docs": ["1", "2"]},
{"query": "test query 2", "relevant_docs": ["3"]}
]
}
EOF
# 3. Run baseline evaluation
./evaluation/scripts/evaluate.sh \
--index test-index \
--dataset test_dataset.json
# 4. Optimize parameters
./evaluation/scripts/optimize.sh \
--dataset test_dataset.json \
--input ./test_docs \
--optimize-for ndcg@10
# 5. Build production index with best params
vyakti build prod-index \
--input ./test_docs \
--graph-degree 32 \
--chunk-size 256 \
--compact
# 6. Verify improvement
./evaluation/scripts/compare.sh \
--index-a test-index \
--index-b prod-index \
--dataset test_dataset.json
For detailed documentation, see evaluation/README.md
vyakti/
├── crates/
│ ├── vyakti-core/ # Core library (Builder, Searcher, API)
│ ├── vyakti-backend-hnsw/ # HNSW backend implementation
│ ├── vyakti-backend-diskann/ # DiskANN backend implementation
│ ├── vyakti-embedding/ # Embedding computation layer
│ ├── vyakti-server/ # REST & gRPC server
│ ├── vyakti-cli/ # Command-line interface
│ ├── vyakti-storage/ # Storage layer (CSR, memory mapping)
│ ├── vyakti-proto/ # Protocol buffers definitions
│ └── vyakti-common/ # Shared utilities and types
├── mcp-server/ # MCP server for Claude Code integration
├── benches/ # Performance benchmarks
├── examples/ # Example applications
├── docs/ # Documentation
└── tests/ # Integration tests
| Crate | Purpose | Exports |
|---|---|---|
vyakti-core | Main API surface | LeannBuilder, LeannSearcher, LeannChat |
leann-backend-* | Vector search backends | Backend implementations |
vyakti-embedding | Embedding models | EmbeddingModel, EmbeddingServer |
vyakti-server | Network server | REST/gRPC endpoints |
vyakti-cli | CLI interface | Binary executable |
vyakti-storage | Persistence layer | CSR format, memory mapping |
vyakti-common | Shared utilities | Error types, config, traits |
Based on the original Python LEANN implementation, Vyakti aims to achieve:
| Operation | Python LEANN | Target (Rust) | Expected Speedup |
|---|---|---|---|
| Index Build (1M docs) | 180s | 12s | 15x |
| Search (Top-10) | 45ms | 0.8ms | 56x |
| Embedding Compute | 120ms | 8ms | 15x |
| Index Load Time | 2.3s | 0.05s | 46x |
| Memory Usage | 4.2GB | 0.8GB | 5.2x less |
Note: Formal benchmarks are in development. Performance numbers from Python LEANN paper.
| Backend | Full Vectors | LEANN Compact | Savings |
|---|---|---|---|
| HNSW | 512MB | 15MB | 96.7% |
| DiskANN* | 512MB | 28MB | 94.5% |
*DiskANN backend implementation in progress
Vyakti supports powerful metadata filtering with SQL-like operators that can be applied to search results. Filters use AND logic, meaning all filter conditions must be satisfied for a result to be included.
| Operator | Description | Example |
|---|---|---|
== | Equal to | {"category": {"==": "tech"}} |
!= | Not equal to | {"status": {"!=": "draft"}} |
< | Less than | {"price": {"<": 100}} |
<= | Less than or equal | {"rating": {"<=": 5}} |
> | Greater than | {"views": {">": 1000}} |
>= | Greater than or equal | {"year": {">=": 2024}} |
in | Value is in list | {"tag": {"in": ["rust", "python"]}} |
not_in | Value not in list | {"status": {"not_in": ["draft", "archived"]}} |
contains | String contains substring | {"title": {"contains": "machine learning"}} |
starts_with | String starts with prefix | {"filename": {"starts_with": "test_"}} |
ends_with | String ends with suffix | {"url": {"ends_with": ".pdf"}} |
is_true | Value is truthy | {"published": {"is_true": true}} |
is_false | Value is falsy | {"archived": {"is_false": false}} |
use vyakti_core::VyaktiSearcher;
use vyakti_common::{FilterOperator, FilterValue, MetadataFilters};
use std::collections::HashMap;
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Setup searcher (backend + embedding provider)
let searcher = setup_searcher().await?;
// Create metadata filters
let mut filters = MetadataFilters::new();
// Filter by category == "technology"
let mut category_filter = HashMap::new();
category_filter.insert(
FilterOperator::Eq,
FilterValue::String("technology".to_string()),
);
filters.insert("category".to_string(), category_filter);
// Search with filters
let results = searcher.search_with_filters(
"machine learning trends",
10,
Some(&filters),
).await?;
println!("Found {} results", results.len());
for result in results {
println!(" {}: {}", result.score, result.text);
}
Ok(())
}
// Filter by multiple conditions - ALL must be satisfied
let mut filters = MetadataFilters::new();
// category == "technology"
let mut category_filter = HashMap::new();
category_filter.insert(
FilterOperator::Eq,
FilterValue::String("technology".to_string()),
);
filters.insert("category".to_string(), category_filter);
// year >= 2024
let mut year_filter = HashMap::new();
year_filter.insert(FilterOperator::Ge, FilterValue::Integer(2024));
filters.insert("year".to_string(), year_filter);
// published == true
let mut published_filter = HashMap::new();
published_filter.insert(FilterOperator::IsTrue, FilterValue::Bool(true));
filters.insert("published".to_string(), published_filter);
// Search - only results matching ALL filters will be returned
let results = searcher.search_with_filters(
"AI advances",
20,
Some(&filters),
).await?;
// Filter by price range: 100 <= price <= 500
let mut filters = MetadataFilters::new();
let mut price_filter = HashMap::new();
price_filter.insert(FilterOperator::Ge, FilterValue::Integer(100)); // >= 100
price_filter.insert(FilterOperator::Le, FilterValue::Integer(500)); // <= 500
filters.insert("price".to_string(), price_filter);
let results = searcher.search_with_filters(
"laptop recommendations",
15,
Some(&filters),
).await?;
// Find documents where author is in a specific list
let mut filters = MetadataFilters::new();
let mut author_filter = HashMap::new();
author_filter.insert(
FilterOperator::In,
FilterValue::List(vec![
FilterValue::String("Alice".to_string()),
FilterValue::String("Bob".to_string()),
FilterValue::String("Charlie".to_string()),
]),
);
filters.insert("author".to_string(), author_filter);
let results = searcher.search_with_filters(
"research papers",
10,
Some(&filters),
).await?;
// Filter by file extension and content
let mut filters = MetadataFilters::new();
// filename ends with ".rs"
let mut filename_filter = HashMap::new();
filename_filter.insert(
FilterOperator::EndsWith,
FilterValue::String(".rs".to_string()),
);
filters.insert("filename".to_string(), filename_filter);
// content contains "async"
let mut content_filter = HashMap::new();
content_filter.insert(
FilterOperator::Contains,
FilterValue::String("async".to_string()),
);
filters.insert("description".to_string(), content_filter);
let results = searcher.search_with_filters(
"async Rust code examples",
10,
Some(&filters),
).await?;
The filter engine automatically handles type coercion for numeric comparisons:
// These will work even if metadata values are stored as different types
let mut filters = MetadataFilters::new();
// Works if "age" is stored as integer, float, or string "25"
let mut age_filter = HashMap::new();
age_filter.insert(FilterOperator::Gt, FilterValue::Integer(25));
filters.insert("age".to_string(), age_filter);
let mut filters = MetadataFilters::new();
// published == true
let mut published_filter = HashMap::new();
published_filter.insert(FilterOperator::IsTrue, FilterValue::Bool(true));
filters.insert("published".to_string(), published_filter);
// year >= 2023
let mut year_filter = HashMap::new();
year_filter.insert(FilterOperator::Ge, FilterValue::Integer(2023));
filters.insert("year".to_string(), year_filter);
// category in ["AI", "ML", "Data Science"]
let mut category_filter = HashMap::new();
category_filter.insert(
FilterOperator::In,
FilterValue::List(vec![
FilterValue::String("AI".to_string()),
FilterValue::String("ML".to_string()),
FilterValue::String("Data Science".to_string()),
]),
);
filters.insert("category".to_string(), category_filter);
// First search with semantic similarity
let all_results = searcher.search("machine learning", 50).await?;
// Then apply metadata filters
let engine = MetadataFilterEngine::new();
let filtered_results = engine.apply_filters(all_results, &filters);
// Further filter by relevance score
let high_quality_results: Vec<_> = filtered_results
.into_iter()
.filter(|r| r.score < 0.3) // High relevance only
.collect();
Vyakti supports hybrid search that combines the strengths of both semantic vector search and keyword-based (BM25) search. This is especially useful for code search, technical documentation, and scenarios where exact keyword matches are important alongside semantic understanding.
Semantic Vector Search excels at:
Keyword (BM25) Search excels at:
Hybrid Search combines both approaches to get the best of both worlds!
# CLI: Hybrid search is enabled by default
vyakti build my-code --input ./src
# Hybrid + Compact mode (93% storage savings!) - RECOMMENDED
vyakti build my-code --input ./src --compact
# Disable hybrid search (vector-only mode)
vyakti build my-code --input ./src --no-hybrid
# Custom BM25 parameters (with hybrid enabled)
vyakti build my-code --input ./src --bm25-k1 1.5 --bm25-b 0.6
# RRF (Reciprocal Rank Fusion) - default, balanced
vyakti search my-code "authentication handler" --fusion rrf
# Weighted fusion (configurable balance)
vyakti search my-code "database connection" --fusion weighted --fusion-param 0.7
# Cascade (keyword first, fallback to vector)
vyakti search my-code "login function" --fusion cascade --fusion-param 5
# Vector-only (disable keyword search)
vyakti search my-code "error handling" --fusion vector-only
# Keyword-only (BM25 only)
vyakti search my-code "handleRequest" --fusion keyword-only
Combines results based on their ranks, not raw scores. Simple and effective.
use vyakti_core::{HybridSearcher, FusionStrategy};
let strategy = FusionStrategy::RRF { k: 60 };
let searcher = HybridSearcher::load(
&index_path,
backend,
embedding_provider,
strategy,
documents,
)?;
let results = searcher.search("vector database", 10).await?;
Formula: score(doc) = Σ 1/(k + rank_in_result_set)
Best for: General-purpose hybrid search, no tuning needed
Parameter k: Higher values (default: 60) give more equal weight to both modes
Combines normalized scores with configurable weight parameter α.
let strategy = FusionStrategy::Weighted { alpha: 0.7 };
Formula: score(doc) = α * norm(bm25_score) + (1-α) * norm(vector_score)
Best for: When you want explicit control over vector vs keyword balance
Parameter alpha:
0.0 = Pure vector search0.5 = Equal weight to both1.0 = Pure keyword searchTry keyword search first, fallback to vector search if insufficient results.
let strategy = FusionStrategy::Cascade { threshold: 5 };
Best for: Technical documentation where exact matches should be prioritized
Parameter threshold: Minimum keyword results needed before using vector search
Disable hybrid mode, use only semantic vector search.
let strategy = FusionStrategy::VectorOnly;
Best for: Natural language queries, conceptual searches
Use only BM25 keyword search (fastest).
let strategy = FusionStrategy::KeywordOnly;
Best for: Exact identifier lookup, very fast search
use vyakti_core::{VyaktiBuilder, HybridSearcher, FusionStrategy};
use vyakti_keyword::KeywordConfig;
use vyakti_backend_hnsw::HnswBackend;
use std::sync::Arc;
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Build hybrid index
let backend = Box::new(HnswBackend::new());
let embedding_provider = Arc::new(/* your provider */);
let config = BackendConfig::default();
let mut builder = VyaktiBuilder::with_config(backend, embedding_provider.clone(), config);
// Add documents
builder.add_text("Rust async programming guide", None);
builder.add_text("Python asyncio tutorial", None);
// Build with keyword indexing
let keyword_config = KeywordConfig {
enabled: true,
k1: 1.2, // Term frequency saturation
b: 0.75, // Length normalization
};
let index_path = builder.build_index_hybrid("my-index", Some(keyword_config)).await?;
// Search with hybrid fusion
let backend = Box::new(HnswBackend::new());
let documents = /* load documents */;
let searcher = HybridSearcher::load(
&index_path,
backend,
embedding_provider,
FusionStrategy::RRF { k: 60 },
documents,
)?;
let results = searcher.search("async programming patterns", 10).await?;
Ok(())
}
The BM25 algorithm has two main parameters:
k1 (term frequency saturation):
b (document length normalization):
# Tune for code search (less length normalization)
vyakti build my-code --input ./src --hybrid --bm25-k1 1.5 --bm25-b 0.6
# Tune for natural language docs (more length normalization)
vyakti build my-docs --input ./docs --hybrid --bm25-k1 1.2 --bm25-b 0.85
The Vyakti MCP server fully supports hybrid search:
{
"name": "vyakti_build",
"arguments": {
"name": "my-code",
"input_path": "./src",
"hybrid": true,
"bm25_k1": 1.2,
"bm25_b": 0.75
}
}
{
"name": "vyakti_search",
"arguments": {
"name": "my-code",
"query": "authentication middleware",
"top_k": 10,
"fusion": "rrf",
"fusion_param": 60
}
}
| Operation | Latency | Notes |
|---|---|---|
| Vector-only | ~53 µs | Baseline semantic search |
| Keyword-only | ~5 µs | Fastest, BM25 only |
| Hybrid RRF | ~95 µs | Balanced fusion |
| Hybrid Weighted | ~80 µs | Slightly faster than RRF |
| Hybrid + Compact | ~85 µs | 93% storage savings + fast search |
Storage Overhead:
✅ Use Hybrid Search when:
❌ Skip Hybrid Search when:
Start with RRF fusion - Works well out of the box, no tuning needed
Use compact mode - Hybrid + compact gives you both accuracy AND efficiency
Tune BM25 for your domain:
k1=1.5, b=0.6 (less length normalization)k1=1.2, b=0.75 (balanced)k1=1.2, b=0.5 (minimal length norm)Experiment with fusion strategies - Different queries may benefit from different strategies
Profile your workload - Use benchmarks to find the best strategy for your use case
# llama.cpp settings (optional - uses auto-download by default)
VYAKTI_MODEL_PATH=~/.vyakti/models/mxbai-embed-large-v1.q4_k_m.gguf # Custom model path
VYAKTI_GPU_LAYERS=0 # Number of GPU layers to offload (0 = CPU only)
VYAKTI_MODEL_THREADS=8 # Number of threads for inference (default: auto-detect)
# Server settings
VYAKTI_PORT=8080
VYAKTI_STORAGE_DIR=.vyakti
VYAKTI_AUTH_TOKEN=your-secret-token
# Logging
RUST_LOG=info # Options: trace, debug, info, warn, error
RUST_BACKTRACE=1 # Enable backtraces for debugging
Most configuration is done via CLI parameters rather than environment variables:
Chunking Configuration:
--chunk-size - Default: 256 tokens--chunk-overlap - Default: 128 tokens--enable-code-chunking - Enable AST-aware code chunking--no-chunking - Disable chunking entirelyEmbedding Configuration:
--embedding-model - Default: mxbai-embed-large--embedding-dimension - Default: 1024Backend Configuration:
--graph-degree - Default: 16 (max connections per node)--build-complexity - Default: 64 (higher = better quality)See vyakti build --help for all options.
Customize HNSW backend behavior programmatically:
use vyakti_common::BackendConfig;
let config = BackendConfig {
dimension: 768, // Must match embedding model
graph_degree: 32, // Higher = better recall, more storage
build_complexity: 64, // Higher = better graph quality
search_complexity: 32, // Higher = more accurate search
compact: true, // Enable 97% storage savings
..Default::default()
};
Status: Planned for Phase 9
TOML/YAML configuration file support is planned for future releases.
# Install Rust toolchain
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
# Install development tools
cargo install cargo-watch cargo-edit cargo-outdated
# Development build
cargo build
# Release build with optimizations
cargo build --release
# Run tests
cargo test --workspace
# Run with examples
cargo run --example basic_search
# Watch mode for development
cargo watch -x "test --workspace"
# Format code
cargo fmt --all
# Lint code
cargo clippy --all-targets --all-features
# Check for security vulnerabilities
cargo audit
# Generate documentation
cargo doc --no-deps --open
# Run all benchmarks
cargo bench --workspace
# Specific benchmark
cargo bench --bench search_performance
# With flamegraph profiling
cargo flamegraph --bench search_performance
We welcome contributions! Please see CONTRIBUTING.md for guidelines.
git checkout -b feature/amazing-featurecargo test && cargo clippygit commit -m 'Add amazing feature'git push origin feature/amazing-featureThis project is licensed under the MIT License - see the LICENSE file for details.
If you use Vyakti in your research, please cite:
@article{leann2024,
title={LEANN: Low-Storage Vector Index with Graph-Based Selective Recomputation},
author={Wang, Yichuan},
journal={arXiv preprint arXiv:2506.08276},
year={2024}
}
6 commits
4 commits
Python
67.8%
Rust
30.4%
Shell
1.4%