Skip to content

Commit 9d0dee2

Browse files
committed
feat(tools): Rust embedding provider with direct GGUF loading (embellama)
New tools/embedding-provider-rs/: - Cargo.toml: embellama dependency, optional 'cuda' feature - src/main.rs: CLI with clap, auto-discovers GGUF from Desktop\model, batch embedding generation, SQLite BLOB storage (little-endian f32) - README.md: build prerequisites (CMake + VS Build Tools), model selection guide (7B vs 14B for RTX 4060 8GB), troubleshooting Design: zero external services, pure local inference via llama.cpp. Contrast with Python provider: no Ollama dependency, memory-native batch processing, longer initial compile time. Note: compilation requires CMake and Visual Studio C++ tools (not present in current environment). User must install these before building.
1 parent b4bb5b1 commit 9d0dee2

3 files changed

Lines changed: 361 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
[package]
2+
name = "embedding-provider-rs"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
embellama = "0.10"
8+
anyhow = "1"
9+
clap = { version = "4", features = ["derive"] }
10+
dirs = "5"
11+
chrono = "0.4"
12+
rusqlite = { version = "0.34", features = ["bundled"] }
13+
tracing = "0.1"
14+
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
15+
16+
[features]
17+
default = []
18+
# Enable CUDA GPU acceleration. Requires CUDA toolkit and compatible GPU.
19+
cuda = ["embellama/cuda"]
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
# embedding-provider-rs
2+
3+
Pure Rust embedding provider for devbase. Loads GGUF models directly via llama.cpp (through `embellama`) — no Ollama server required.
4+
5+
## Prerequisites
6+
7+
> ⚠️ **Windows 编译需要 C++ 工具链**。如果以下工具缺失,请先安装。
8+
9+
| 工具 | 用途 | 安装方式 |
10+
|------|------|---------|
11+
| **CMake** ≥ 3.14 | 编译 llama.cpp C++ 代码 | [cmake.org/download](https://cmake.org/download/) 或 `pip install cmake` |
12+
| **Visual Studio 2022 Build Tools** | C++ 编译器 (cl.exe) | [Visual Studio 下载页](https://visualstudio.microsoft.com/downloads/) → 选择 "Desktop development with C++" |
13+
| **CUDA Toolkit** 12.x (可选) | GPU 加速 | 已安装于本机 (CUDA 12.6) |
14+
15+
验证安装:
16+
```powershell
17+
cmake --version # 应输出 ≥ 3.14
18+
cl.exe # 应找到 Visual C++ 编译器
19+
nvcc --version # 应输出 CUDA 12.x
20+
```
21+
22+
## Build
23+
24+
```powershell
25+
cd tools\embedding-provider-rs
26+
27+
# CPU only (推荐先验证基础功能)
28+
cargo build --release
29+
30+
# With CUDA acceleration (需要 CMake + VS Build Tools + CUDA)
31+
cargo build --release --features cuda
32+
```
33+
34+
> **Note**: RTX 4060 有 8GB VRAM。Qwen2.5-7B Q4_K_M (~4.5GB) 可以完整加载到 GPU;14B 模型 (~8.5GB) 可能超出显存,会自动回退到 CPU offload。
35+
36+
## Usage
37+
38+
```powershell
39+
# 自动发现模型 (优先 7B,其次 14B)
40+
.\target\release\embedding-provider-rs --repo-id claude-code-rust
41+
42+
# 指定模型路径
43+
.\target\release\embedding-provider-rs `
44+
--model-path "C:\Users\22414\Desktop\model\Qwen2.5-7B-Instruct.Q4_K_M.gguf" `
45+
--repo-id claude-code-rust
46+
47+
# 跳过已存在的 embedding
48+
.\target\release\embedding-provider-rs --repo-id claude-code-rust --skip-existing
49+
50+
# 自定义 registry 路径
51+
.\target\release\embedding-provider-rs `
52+
--repo-id claude-code-rust `
53+
--db-path "C:\Users\22414\AppData\Local\devbase\registry.db"
54+
```
55+
56+
## Architecture
57+
58+
```
59+
GGUF model file (Qwen2.5-7B/14B)
60+
│
61+
▼
62+
[embellama / llama.cpp] ──► embedding vector (f32[])
63+
│
64+
▼
65+
devbase registry.db ──► code_embeddings BLOB
66+
│
67+
▼
68+
devkit_hybrid_search(repo_id, query_text, query_embedding?)
69+
```
70+
71+
## Model Selection Guide
72+
73+
| 模型 | 文件大小 | VRAM 需求 | 推荐场景 |
74+
|------|---------|----------|---------|
75+
| Qwen2.5-7B Q4_K_M | ~4.5 GB | ~5 GB | **首选** — RTX 4060 8GB 可完整加载 |
76+
| Qwen2.5-14B Q4_K_M | ~8.5 GB | ~9 GB | 需 CPU offload 部分层,速度较慢 |
77+
78+
## Troubleshooting
79+
80+
### `cmake not found`
81+
安装 CMake 并确保其在 PATH 中:
82+
```powershell
83+
# 方法 1: pip
84+
pip install cmake
85+
86+
# 方法 2: 官方安装包 (推荐)
87+
# https://cmake.org/download/
88+
```
89+
90+
### `cl.exe not found`
91+
需要安装 Visual Studio Build Tools:
92+
1. 下载 [Visual Studio Build Tools](https://visualstudio.microsoft.com/downloads/#build-tools-for-visual-studio-2022)
93+
2. 安装 "Desktop development with C++" 工作负载
94+
3. 从 "x64 Native Tools Command Prompt" 运行 cargo build
95+
96+
### CUDA out of memory
97+
14B 模型超出 RTX 4060 8GB 显存时,llama.cpp 会自动将部分层 offload 到系统内存。可以通过降低 `--batch-size` 减少峰值显存占用。
98+
99+
## Differences from Python Provider
100+
101+
| 维度 | Python (`tools/embedding-provider/`) | Rust (`tools/embedding-provider-rs/`) |
102+
|------|--------------------------------------|---------------------------------------|
103+
| 依赖 | `requests` (HTTP) → Ollama | `embellama` (本地 GGUF 直接加载) |
104+
| 运行时 | 需要 Ollama 服务 | 零外部服务,纯本地推理 |
105+
| 性能 | 受 HTTP 往返影响 | 内存内 batch 处理,更快 |
106+
| 编译 | 零编译,pip 安装 | 首次编译 10-30 分钟(llama.cpp C++) |
107+
| 适用场景 | 快速验证、已有 Ollama 环境 | 生产部署、无网络环境、GPU 最大化利用 |
Lines changed: 235 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,235 @@
1+
//! Local Rust embedding provider for devbase.
2+
//!
3+
//! Loads a GGUF model directly via llama.cpp (through `embellama`) and generates
4+
//! f32 embeddings for code symbols stored in the devbase SQLite registry.
5+
//!
6+
//! No Ollama server required — pure local inference with optional CUDA.
7+
//!
8+
//! ## Prerequisites
9+
//! - CMake 3.14+
10+
//! - Visual Studio 2022 Build Tools (or full VS) with "Desktop development with C++"
11+
//! - CUDA Toolkit 12.x (optional, only if `--features cuda` is used)
12+
//!
13+
//! ## Build
14+
//! ```powershell
15+
//! # CPU only
16+
//! cargo build --release
17+
//!
18+
//! # With CUDA acceleration
19+
//! cargo build --release --features cuda
20+
//! ```
21+
//!
22+
//! ## Run
23+
//! ```powershell
24+
//! # Auto-discover model from Desktop\model
25+
//! .\target\release\embedding-provider-rs --repo-id claude-code-rust
26+
//!
27+
//! # Explicit model path
28+
//! .\target\release\embedding-provider-rs `
29+
//! --model-path "C:\Users\22414\Desktop\model\Qwen2.5-7B-Instruct.Q4_K_M.gguf" `
30+
//! --repo-id claude-code-rust
31+
//! ```
32+
33+
use std::path::PathBuf;
34+
35+
use clap::Parser;
36+
use embellama::{EngineConfig, EmbeddingEngine, ModelConfig, NormalizationMode};
37+
use tracing::{info, warn};
38+
39+
#[derive(Parser, Debug)]
40+
#[command(name = "embedding-provider-rs")]
41+
#[command(about = "Local Rust embedding provider for devbase (GGUF → SQLite)")]
42+
struct Args {
43+
/// Repository ID to generate embeddings for
44+
#[arg(long)]
45+
repo_id: String,
46+
47+
/// Path to GGUF model file. If omitted, auto-discovers from Desktop\model.
48+
#[arg(long)]
49+
model_path: Option<PathBuf>,
50+
51+
/// Batch size for embedding generation
52+
#[arg(long, default_value_t = 16)]
53+
batch_size: usize,
54+
55+
/// Skip symbols that already have embeddings
56+
#[arg(long, default_value_t = false)]
57+
skip_existing: bool,
58+
59+
/// Registry database path. Defaults to devbase's standard location.
60+
#[arg(long)]
61+
db_path: Option<PathBuf>,
62+
}
63+
64+
fn main() -> anyhow::Result<()> {
65+
tracing_subscriber::fmt::init();
66+
let args = Args::parse();
67+
68+
// 1. Resolve model path
69+
let model_path = resolve_model_path(args.model_path)?;
70+
info!("Using model: {}", model_path.display());
71+
72+
// 2. Load embedding engine
73+
let model_config = ModelConfig::builder()
74+
.with_model_path(model_path.to_string_lossy().as_ref())
75+
.with_model_name("local-embedding")
76+
.with_normalization_mode(NormalizationMode::L2)
77+
.build()?;
78+
79+
let engine_config = EngineConfig::builder()
80+
.with_model_config(model_config)
81+
.build()?;
82+
83+
let engine = EmbeddingEngine::new(engine_config)?;
84+
info!("Embedding engine loaded successfully");
85+
86+
// 3. Connect to devbase registry
87+
let db_path = args.db_path.unwrap_or_else(|| {
88+
dirs::data_local_dir()
89+
.expect("Could not find local data dir")
90+
.join("devbase")
91+
.join("registry.db")
92+
});
93+
info!("Registry DB: {}", db_path.display());
94+
95+
let mut conn = rusqlite::Connection::open(&db_path)?;
96+
97+
// 4. Read function symbols
98+
let symbols = read_symbols(&conn, &args.repo_id, args.skip_existing)?;
99+
if symbols.is_empty() {
100+
info!("No symbols to process for repo '{}'", args.repo_id);
101+
return Ok(());
102+
}
103+
info!("Found {} function symbols to embed", symbols.len());
104+
105+
// 5. Generate & store embeddings in batches
106+
let mut total = 0usize;
107+
for (idx, chunk) in symbols.chunks(args.batch_size).enumerate() {
108+
let texts: Vec<String> = chunk
109+
.iter()
110+
.map(|(name, file, sig)| {
111+
let sig_text = sig.as_deref().unwrap_or(name);
112+
format!("{} in {}: {}", name, file, sig_text)
113+
})
114+
.collect();
115+
116+
let embeddings = engine.embed_batch(None, &texts)?;
117+
if embeddings.len() != chunk.len() {
118+
warn!(
119+
"Batch {}: expected {} embeddings, got {}. Skipping batch.",
120+
idx,
121+
chunk.len(),
122+
embeddings.len()
123+
);
124+
continue;
125+
}
126+
127+
let dim = embeddings.first().map(|e| e.len()).unwrap_or(0);
128+
let mut pairs: Vec<(String, Vec<f32>)> = Vec::with_capacity(chunk.len());
129+
for ((name, _file, _sig), emb) in chunk.iter().zip(embeddings.iter()) {
130+
let vec: Vec<f32> = emb.iter().map(|&v| v).collect();
131+
pairs.push((name.clone(), vec));
132+
}
133+
134+
save_embeddings(&mut conn, &args.repo_id, &pairs)?;
135+
total += chunk.len();
136+
info!("Batch {}/{}: {} embeddings stored (dim={})",
137+
idx + 1,
138+
(symbols.len() + args.batch_size - 1) / args.batch_size,
139+
chunk.len(),
140+
dim
141+
);
142+
}
143+
144+
info!("Done! {} embeddings stored for '{}'", total, args.repo_id);
145+
Ok(())
146+
}
147+
148+
/// Auto-discover GGUF model from known locations.
149+
fn resolve_model_path(explicit: Option<PathBuf>) -> anyhow::Result<PathBuf> {
150+
if let Some(p) = explicit {
151+
if p.exists() {
152+
return Ok(p);
153+
}
154+
anyhow::bail!("Specified model path does not exist: {}", p.display());
155+
}
156+
157+
let candidates = [
158+
PathBuf::from(r"C:\Users\22414\Desktop\model\Qwen2.5-7B-Instruct.Q4_K_M.gguf"),
159+
PathBuf::from(r"C:\Users\22414\Desktop\model\Qwen2.5-14B-Instruct.Q4_K_M.gguf"),
160+
];
161+
162+
for c in &candidates {
163+
if c.exists() {
164+
return Ok(c.clone());
165+
}
166+
}
167+
168+
anyhow::bail!(
169+
"Could not auto-discover GGUF model. Please specify --model-path. \
170+
Searched: {:?}",
171+
candidates
172+
)
173+
}
174+
175+
/// Read function symbols from code_symbols table.
176+
fn read_symbols(
177+
conn: &rusqlite::Connection,
178+
repo_id: &str,
179+
skip_existing: bool,
180+
) -> anyhow::Result<Vec<(String, String, Option<String>)>> {
181+
let existing: std::collections::HashSet<String> = if skip_existing {
182+
let mut stmt = conn.prepare(
183+
"SELECT symbol_name FROM code_embeddings WHERE repo_id = ?1"
184+
)?;
185+
let rows = stmt.query_map([repo_id], |row| row.get::<_, String>(0))?;
186+
rows.collect::<Result<std::collections::HashSet<_>, _>>()?
187+
} else {
188+
std::collections::HashSet::new()
189+
};
190+
191+
let mut stmt = conn.prepare(
192+
"SELECT name, file_path, signature FROM code_symbols
193+
WHERE repo_id = ?1 AND symbol_type = 'function'"
194+
)?;
195+
let rows = stmt.query_map([repo_id], |row| {
196+
Ok((
197+
row.get::<_, String>(0)?,
198+
row.get::<_, String>(1)?,
199+
row.get::<_, Option<String>>(2)?,
200+
))
201+
})?;
202+
203+
let mut symbols = Vec::new();
204+
for row in rows {
205+
let (name, file, sig) = row?;
206+
if skip_existing && existing.contains(&name) {
207+
continue;
208+
}
209+
symbols.push((name, file, sig));
210+
}
211+
Ok(symbols)
212+
}
213+
214+
/// Save embeddings to code_embeddings table (little-endian f32 BLOB).
215+
fn save_embeddings(
216+
conn: &mut rusqlite::Connection,
217+
repo_id: &str,
218+
pairs: &[(String, Vec<f32>)],
219+
) -> anyhow::Result<()> {
220+
let tx = conn.transaction()?;
221+
let now = chrono::Utc::now().to_rfc3339();
222+
for (symbol_name, vec) in pairs {
223+
let blob: Vec<u8> = vec.iter().flat_map(|f| f.to_le_bytes()).collect();
224+
tx.execute(
225+
"INSERT INTO code_embeddings (repo_id, symbol_name, embedding, generated_at)
226+
VALUES (?1, ?2, ?3, ?4)
227+
ON CONFLICT(repo_id, symbol_name) DO UPDATE SET
228+
embedding = excluded.embedding,
229+
generated_at = excluded.generated_at",
230+
rusqlite::params![repo_id, symbol_name, blob, &now],
231+
)?;
232+
}
233+
tx.commit()?;
234+
Ok(())
235+
}

0 commit comments

Comments
 (0)