diff --git a/README.md b/README.md index 1cfa59c..96b461a 100644 --- a/README.md +++ b/README.md @@ -4,106 +4,50 @@ [![Tauri 2](https://img.shields.io/badge/Tauri-2.0-blue?style=for-the-badge&logo=tauri)](https://tauri.app/) [![Rust](https://img.shields.io/badge/Rust-LATEST-orange?style=for-the-badge&logo=rust)](https://www.rust-lang.org/) -[![Gemma 3](https://img.shields.io/badge/LLM-Gemma_3_300M-8E44AD?style=for-the-badge&logo=google-cloud)](https://blog.google/technology/ai/google-gemma-3/) +[![SQLite](https://img.shields.io/badge/SQLite-vec0-lightgrey?style=for-the-badge&logo=sqlite)](https://github.com/asg017/sqlite-vec) -## 📝 イントロダクション +## 📝 プロジェクト概要 -Model Context Protocol (MCP) に準拠した、ローカル実行用のベクトル検索サーバーです。Tauri v2 および Rust をベースに構築されています。 - -Gemma 3 300M Embedding モデルや `llama-server` をサイドカーとして利用することで、外部ネットワークに依存せず、ローカル環境のみでテキストのベクトル化と検索を行うことができます。 - -また、内蔵の **SQLite DBブラウザ** により、GUI上で直接テーブル構成やデータの確認が可能です。 - ---- - -## ✨ 主な機能 - -1. **Tauri v2 + Rust**: 軽量で高速なバックエンド基盤。 -2. **SQLite + sqlite-vec**: リレーショナルデータとベクトルデータの一元管理。 -3. **DBブラウザ (New!)**: テーブル一覧、スキーマ表示、データ閲覧(ページネーション対応)が可能なモダンなGUI。 -4. **MCP 準拠**: SSE プロトコルにより、外部エージェント(Claude 等)からのツール呼び出しに対応。 -5. **Sidecar LLM**: `llama-server` を内蔵し、オフラインでのテキスト処理が可能。 -6. **SeaORM**: 型安全なデータベース操作。 -7. **長文対応**: 入力テキストの自動チャンキングと Mean Pooling により、コンテキスト制限を超える文書もベクトル化可能。 -8. **運用支援**: 詳細なログ記録とステータス可視化。 - ---- +TelosDB は、ローカル環境で動作するベクトル検索サーバーおよびデータベースブラウザです。 +Gemma 3 モデルを統合し、プライバシーを重視した効率的なデータ検索と管理を提供します。 ## 🏗️ システム構造 ```mermaid graph TD - subgraph "Frontend Layer (WebView2)" - UI["DB Browser & Status UI"] - end - - subgraph "Tauri Backend Layer (Rust)" - Axum["MCP Server (SSE/JSON-RPC)"] - Tray["System Tray Manager"] - Sidecar["Sidecar Manager"] - ORM["SeaORM Layer"] - Llama["Llama API Client"] - DBCmd["DB Browser Commands"] - end - - subgraph "External Process Layer" - LS["llama-server.exe (Sidecar)"] - end - - subgraph "Data Layer" - DB[("SQLite + sqlite-vec")] - Model["Gemma 3 300M (GGUF)"] - end - - UI <--> Axum - UI <--> DBCmd - DBCmd <--> DB - Axum <--> ORM - ORM <--> DB - Llama <--> LS - Sidecar --> LS - LS --> Model + Client[Frontend: Tauri/Svelte] -->|SSE/JSON-RPC| Server[Backend: Rust/Axum] + Server -->|load_extension| DB[(SQLite + sqlite-vec)] + Server -->|Sidecar| AI[llama-server] + AI -->|GGUF| Model[Gemma 3 300M] ``` ---- +- **Backend**: Rust (Axum, SeaORM, sqlx) +- **Database**: SQLite (ベクトル検索拡張 `sqlite-vec` を動的にロード) +- **AI Inference**: `llama-server` を Tauri サイドカーとして管理 +- **Frontend**: Svelte (Tauri v2) -## 📁 ディレクトリ構成 +## 🛠️ セットアップ -```text -. -├── .agent/ # エージェント用ルール・ドキュメント -├── bin/ # 外部バイナリ (llama-server等) -├── data/ # データベースファイル (vector.db) -├── docs/ # 仕様・設計ドキュメント -├── journals/ # 開発記録 (Git管理外) -├── logs/ # アプリケーションログ -├── resources/ # アセット類 -├── scripts/ # ユーティリティスクリプト (analyze_nesting, count_lines等) -├── src-backend/ # Rust / Tauri バックエンド -├── src-frontend/ # Webview UI -├── tests/ # テストコード -└── tmp/ # 一時ファイル -``` +1. **Rust と Tauri のインストール**: + [Tauri 2 Prerequisites](https://v2.tauri.app/start/prerequisites/) に従って環境を構築してください。 ---- +2. **依存関係のインストール**: -## 🛠️ MCP ツール + ```bash + npm install + ``` -| ツール名 | 用途 | -| :--- | :--- | -| `save_document` | 文書の追加とベクトル化 (長文対応: 自動チャンキング) | -| `find_documents` | テキストによる類似文書検索 | -| `find_by_vector` | ベクトルによる類似文書検索 | -| `delete_item` | 指定IDの文書削除 | -| `get_document` | 指定IDの文書内容取得 | -| `get_vector` | 指定IDのベクトル取得 | -| `list_documents` | 文書一覧の取得 (ページネーション対応) | -| `get_documents_count` | 登録文書数の取得 | -| `read_recent_items` | 最近追加された文書の取得 | -| `llm_generate` | LLM によるテキスト生成 | +3. **開発モードでの実行**: ---- + ```bash + npm run tauri dev + ``` -## 📜 ライセンス +## 📜 変更履歴 -ISC License. +詳細は [journals](./journals) フォルダを参照してください。 + +- `20260213-0010`: SQLite Vector ロード権限問題の修正とサイドカー管理の正常化 +- `20260213-0009`: `sqlite-vec` (Alex Garcia版) への回帰と `vec0.dll` の復旧 +- `20260213-0008`: マーケットプレース同期不全の根本解決 +- `20260213-0007`: Java 25 設定エラーの解消 diff --git a/document/database.md b/document/database.md index 1139046..df864d1 100644 --- a/document/database.md +++ b/document/database.md @@ -38,7 +38,7 @@ ### 2. `vec_items` 仮想テーブル (sqlite-vec) -ベクトル検索を高速に行うための仮想テーブルです。`vec0` モジュールを使用します。 +ベクトル検索を高速に行うための仮想テーブルです。`vec0` モジュール (Alex Garcia 氏による `sqlite-vec` 拡張機能 / `vec0.dll`) を使用します。 ```sql CREATE VIRTUAL TABLE vec_items USING vec0( diff --git a/package-lock.json b/package-lock.json index 0990774..fd7a676 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,8 @@ "license": "ISC", "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", - "better-sqlite3": "^12.6.2" + "better-sqlite3": "^12.6.2", + "sqlite-vec-windows-x64": "^0.1.7-alpha.2" }, "devDependencies": { "@tauri-apps/cli": "^2.10.0", @@ -1616,6 +1617,17 @@ "simple-concat": "^1.0.0" } }, + "node_modules/sqlite-vec-windows-x64": { + "version": "0.1.7-alpha.2", + "resolved": "https://registry.npmjs.org/sqlite-vec-windows-x64/-/sqlite-vec-windows-x64-0.1.7-alpha.2.tgz", + "integrity": "sha512-TRP6hTjAcwvQ6xpCZvjP00pdlda8J38ArFy1lMYhtQWXiIBmWnhMaMbq4kaeCYwvTTddfidatRS+TJrwIKB/oQ==", + "cpu": [ + "x64" + ], + "os": [ + "win32" + ] + }, "node_modules/statuses": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", diff --git a/package.json b/package.json index b76a57e..54f06ea 100644 --- a/package.json +++ b/package.json @@ -1,15 +1,15 @@ { - "name": "sqlitevector", + "name": "TelosDB", "version": "0.1.1", "description": "Resident MCP server with SQLite vector search, Tauri 2 + Rust runtime, and llama.cpp integration", "type": "module", "repository": { "type": "git", - "url": "https://github.com/yourusername/sqlitevector" + "url": "https://github.com/yourusername/TelosDB" }, - "homepage": "https://github.com/yourusername/sqlitevector", + "homepage": "https://github.com/yourusername/TelosDB", "bugs": { - "url": "https://github.com/yourusername/sqlitevector/issues" + "url": "https://github.com/yourusername/TelosDB/issues" }, "keywords": [ "mcp", @@ -44,6 +44,7 @@ }, "dependencies": { "@modelcontextprotocol/sdk": "^1.26.0", - "better-sqlite3": "^12.6.2" + "better-sqlite3": "^12.6.2", + "sqlite-vec-windows-x64": "^0.1.7-alpha.2" } } diff --git a/src/backend/Cargo.lock b/src/backend/Cargo.lock index dd48ffe..740ad06 100644 --- a/src/backend/Cargo.lock +++ b/src/backend/Cargo.lock @@ -114,6 +114,7 @@ "sea-orm", "serde", "serde_json", + "sqlx", "tauri", "tauri-build", "tauri-plugin-log", diff --git a/src/backend/Cargo.toml b/src/backend/Cargo.toml index 2f8014e..12fcdc7 100644 --- a/src/backend/Cargo.toml +++ b/src/backend/Cargo.toml @@ -32,6 +32,7 @@ tower-http = { version = "0.5", features = ["cors"] } dotenvy = "0.15" sea-orm = { version = "1.1", features = ["sqlx-sqlite", "runtime-tokio-rustls", "macros", "with-chrono"] } +sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls"] } futures = "0.3" [dev-dependencies] diff --git a/src/backend/src/db.rs b/src/backend/src/db.rs index be303ec..418c06d 100644 --- a/src/backend/src/db.rs +++ b/src/backend/src/db.rs @@ -1,60 +1,82 @@ -use sea_orm::{ConnectOptions, Database, DatabaseConnection}; -use std::time::Duration; +use sea_orm::DatabaseConnection; +use std::path::Path; +use std::fs; pub async fn init_db(db_path: &str, extension_path: &str) -> anyhow::Result { - // SeaORM (sqlx) は URL 形式でパスを指定する - let db_url = format!("sqlite:{}?mode=rwc", db_path); + ensure_parent_dir(db_path)?; + init_schema(db_path, extension_path)?; + create_connection_pool(db_path, extension_path).await +} - let mut opt = ConnectOptions::new(db_url); - opt.max_connections(10) - .min_connections(5) - .connect_timeout(Duration::from_secs(8)) - .idle_timeout(Duration::from_secs(8)) - .max_lifetime(Duration::from_secs(8)) - .sqlx_logging(true); - - // 拡張機能のロードが必要なため、sqlx の初期化フックがあればいいが、 - // SeaORM/sqlx で動的にロードするのは少し工夫が必要。 - // ここではまず rusqlite か何かでスキーマ初期化と拡張ロードの確認をしてから SeaORM で繋ぐか、 - // sqlx の ConnectOptions にて初期化SQLを流す。 - - // NOTE: sqlite-vec のロードは非常に特殊なため、 - // 生の rusqlite で拡張をロードしてテーブルを作った後、SeaORM で接続する。 - { - use rusqlite::Connection; - let conn = Connection::open(db_path)?; - unsafe { - conn.load_extension(extension_path, None)?; +fn ensure_parent_dir(db_path: &str) -> anyhow::Result<()> { + if let Some(parent) = Path::new(db_path).parent() { + if !parent.exists() { + log::info!("init_db: creating directory {:?}", parent); + fs::create_dir_all(parent)?; } - conn.execute_batch( - "PRAGMA journal_mode = WAL; - CREATE TABLE IF NOT EXISTS items ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - content TEXT NOT NULL, - path TEXT, - created_at TEXT DEFAULT (datetime('now', 'localtime')), - updated_at TEXT DEFAULT (datetime('now', 'localtime')) - ); - CREATE TRIGGER IF NOT EXISTS update_items_updated_at - AFTER UPDATE ON items - FOR EACH ROW - BEGIN - UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; - END; - CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0( - id INTEGER PRIMARY KEY, - embedding FLOAT[384] - );", - )?; + } + Ok(()) +} + +fn init_schema(db_path: &str, extension_path: &str) -> anyhow::Result<()> { + use rusqlite::Connection; + + log::info!("init_db: initializing schema via rusqlite for '{}'", db_path); + let conn = Connection::open(db_path) + .map_err(|e| anyhow::anyhow!("Failed to open rusqlite connection: {:?}", e))?; + + unsafe { + conn.load_extension_enable() + .map_err(|e| anyhow::anyhow!("Failed to enable extension loading: {:?}", e))?; + + log::info!("init_db: loading extension via rusqlite: {}", extension_path); + conn.load_extension(extension_path, None) + .map_err(|e| anyhow::anyhow!("Failed to load SQLite extension '{}': {:?}", extension_path, e))?; } - let db = Database::connect(opt).await?; + conn.execute_batch( + "PRAGMA journal_mode = WAL; + CREATE TABLE IF NOT EXISTS items ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + content TEXT NOT NULL, + path TEXT, + created_at TEXT DEFAULT (datetime('now', 'localtime')), + updated_at TEXT DEFAULT (datetime('now', 'localtime')) + ); + CREATE TRIGGER IF NOT EXISTS update_items_updated_at + AFTER UPDATE ON items + FOR EACH ROW + BEGIN + UPDATE items SET updated_at = datetime('now', 'localtime') WHERE id = OLD.id; + END; + CREATE VIRTUAL TABLE IF NOT EXISTS vec_items USING vec0( + id INTEGER PRIMARY KEY, + embedding FLOAT[384] + );", + ).map_err(|e| anyhow::anyhow!("Failed to initialize schema: {:?}", e))?; - // SeaORM の接続後にも拡張をロードしておく必要がある場合がある(セッション毎) - // sqlx-sqlite の場合、初期化 SQL でロードできるケースがあるが設定が複雑。 - // 今回は rusqlite での初期化を優先し、クエリ実行時に MATCH 句が動くか確認する。 + log::info!("init_db: schema initialization completed"); + Ok(()) +} - Ok(db) +async fn create_connection_pool(db_path: &str, extension_path: &str) -> anyhow::Result { + use sqlx::sqlite::{SqliteConnectOptions, SqliteJournalMode}; + use sqlx::SqlitePool; + use sea_orm::SqlxSqliteConnector; + use std::str::FromStr; + + log::info!("init_db: creating SeaORM connection pool with extension: {}", extension_path); + + let normalized_ext_path = extension_path.replace("\\", "/"); + let sqlite_opt = SqliteConnectOptions::from_str(&format!("sqlite:{}", db_path))? + .create_if_missing(true) + .journal_mode(SqliteJournalMode::Wal) + .extension(normalized_ext_path); + + let pool = SqlitePool::connect_with(sqlite_opt).await + .map_err(|e| anyhow::anyhow!("Failed to create sqlx pool: {:?}", e))?; + + Ok(SqlxSqliteConnector::from_sqlx_sqlite_pool(pool)) } #[cfg(test)] @@ -62,12 +84,11 @@ use super::*; #[tokio::test] - async fn test_init_db_basic() { - // NOTE: We test with a dummy extension path and expect it to fail if it's not found, - // or we test just the SeaORM part if we could separation. - // For now, let's just ensure we can connect to an in-memory sqlite via SeaORM. - let mut opt = ConnectOptions::new("sqlite::memory:"); - let db = Database::connect(opt).await; - assert!(db.is_ok()); + async fn test_ensure_parent_dir() { + let test_path = "tmp/test_dir/test.db"; + let res = ensure_parent_dir(test_path); + assert!(res.is_ok()); + assert!(Path::new("tmp/test_dir").exists()); + let _ = fs::remove_dir_all("tmp/test_dir"); } } diff --git a/src/backend/src/lib.rs b/src/backend/src/lib.rs index 7614a82..68ad737 100644 --- a/src/backend/src/lib.rs +++ b/src/backend/src/lib.rs @@ -156,8 +156,8 @@ if cand.exists() { if let Ok(canon) = cand.canonicalize() { let s = canon.to_string_lossy().to_string(); - if s.starts_with(r"\\?\") { - return s[4..].to_string(); + if let Some(stripped) = s.strip_prefix(r"\\?\") { + return stripped.to_string(); } return s; } @@ -225,7 +225,7 @@ // Comprehensive PATH candidates let old_path = std::env::var("PATH").unwrap_or_default(); - let mut path_candidates = vec![ + let path_candidates = [ base_dir.display().to_string(), ]; @@ -314,8 +314,8 @@ let ext_path = resolve_extension_path(&app_handle); log::info!("DB Path: {}, Ext Path: {}", db_path, ext_path); - let vec_dim = env::var("VEC_DIM").unwrap_or_else(|_| "768".to_string()).parse::().unwrap_or(768); - let conn = db::init_db(&db_path, &ext_path, vec_dim).await.expect("Failed to init db"); + let _vec_dim = env::var("VEC_DIM").unwrap_or_else(|_| "768".to_string()).parse::().unwrap_or(768); + let conn = db::init_db(&db_path, &ext_path).await.expect("Failed to init db"); let state = Arc::new(AppState { db: conn, diff --git a/src/backend/tests/vector_test.rs b/src/backend/tests/vector_test.rs new file mode 100644 index 0000000..f1bb806 --- /dev/null +++ b/src/backend/tests/vector_test.rs @@ -0,0 +1,68 @@ +use app_lib::db; +use sea_orm::{ConnectionTrait, DatabaseBackend, Statement}; +use std::env; +use std::path::PathBuf; + +#[tokio::test] +async fn test_vec0_extension_loading() { + // 1. Determine paths + let test_db = "test-vector-vec0.db"; + if std::path::Path::new(test_db).exists() { + let _ = std::fs::remove_file(test_db); + } + + let mut candidates = vec![ + PathBuf::from("bin/vec0.dll"), + PathBuf::from("../bin/vec0.dll"), + PathBuf::from("../../bin/vec0.dll"), + env::current_dir().unwrap().join("bin").join("vec0.dll"), + env::current_dir().unwrap().join("../").join("bin").join("vec0.dll"), + env::current_dir().unwrap().join("../../").join("bin").join("vec0.dll"), + ]; + + let mut ext_path = PathBuf::from("vec0.dll"); + for cand in candidates { + if cand.exists() { + ext_path = cand; + break; + } + } + + let ext_path_str = ext_path.to_string_lossy().to_string(); + println!("Testing with extension path: {}", ext_path_str); + + // 2. Initialize DB + let conn = db::init_db(test_db, &ext_path_str).await.expect("Failed to initialize DB with vec0"); + + // 3. Verify vec0 is actually working by querying the virtual table + + // Test inserting a vector + let embedding: Vec = vec![0.1; 384]; + let embedding_bytes: Vec = embedding.iter().flat_map(|f| f.to_le_bytes()).collect(); + + let insert_res = conn.execute(Statement::from_sql_and_values( + DatabaseBackend::Sqlite, + "INSERT INTO vec_items (id, embedding) VALUES (?, ?)", + [1.into(), embedding_bytes.clone().into()], + )).await; + + assert!(insert_res.is_ok(), "Failed to insert into vec_items: {:?}", insert_res.err()); + + // Test a simple vector search (MATCH) + // Using Statement directly with query_all + let search_res = conn.query_all(Statement::from_sql_and_values( + DatabaseBackend::Sqlite, + "SELECT id, distance FROM vec_items WHERE embedding MATCH ? ORDER BY distance LIMIT 1", + [embedding_bytes.into()], + )).await; + + assert!(search_res.is_ok(), "Failed to search in vec_items: {:?}", search_res.err()); + let results = search_res.unwrap(); + assert_eq!(results.len(), 1); + + // Cleanup + drop(conn); + // Give a small delay to ensure file handle is released on Windows + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + let _ = std::fs::remove_file(test_db); +}