This file is a navigation guide for coding agents working on this repository. Read it first.
semquery is a local-first, full RAG system (retrieval + answer synthesis) written in Rust as a library-first multi-crate workspace:
- Millisecond-latency
searchfor agents (BM25 + dense vectors + RRF + cross-encoder rerank), with zero LLM cost. askfor humans — natural-language answers with inline[N]citations, running on a local GGUF model or an OpenAI-compatible endpoint.- All indexes live in a single SQLite file (
sqlite-vec+FTS5+ plain tables); the whole pipeline works offline. - Chinese-optimized: chunking replicates LlamaIndex's
SentenceSplitter; BM25 uses jieba word-level pre-tokenization.
See README.md for the project overview.
The project uses two names with distinct roles:
semquery= project / package name (developer layer,cargo add semquery).semq= command / runtime name (user layer, the command you type and the directories you see).
So the project is named semquery, the CLI command is semq. Sub-crates keep the semquery-* prefix (semquery-core, semquery-model, ...) following the Rust convention of prefixing sub-crates with the main crate name.
Runtime artifacts (config dir, cache dir, DB file, log file) use semq to match the command name:
~/.config/semq~/.cache/semq/modelssemq.dbsemq.log
# Fastest compile check
cargo check --workspace
# Run all tests
cargo test --workspace
# Run tests for one crate
cargo test -p semquery-storage
# Format check and apply
cargo fmt --all -- --check
cargo fmt --all
# Clippy (project requires -D warnings)
cargo clippy --all-features -- -D warnings
# Run the full pre-commit suite (check + test + fmt + clippy) in one shot
./pre-commit-check.shNotes:
- The Rust toolchain is pinned to
1.95.0stable inrust-toolchain.toml. Do not use nightly. - On macOS,
llama-cpp-2defaults to Metal. This project enables Metal viaGGML_METAL=ONin.cargo/config.toml(for bothaarch64-apple-darwinandx86_64-apple-darwin), plusCMAKE_CXX_FLAGS="-Wno-elaborated-enum-base"to silence a warning. Do not remove these settings. - The first
cargo checkbuilds heavy C/C++ dependencies (libsqlite3-sys,llama-cpp-sys-2,ort-sys); 5–10 minutes is normal. Incremental builds are much faster.
cli (semquery) mcp (future)
└───────┬─────────┘
▼
semquery facade — the only crate library users touch
╱ │ ╲
retrieve index synthesize synthesize is optional (needs semquery-model/llm)
│ ╲ │ │
│ ╲ │ ▼
│ ╲ │ model GGUF / ONNX backends
│ ╲ │ ╱
▼ ▼ ▼ ▼
core types + traits, zero heavy dependencies
| crate | responsibility | depends on |
|---|---|---|
semquery-core |
All core types + traits + error types; zero internal deps | none |
semquery-model |
Model registry, HF download cache, verification, inference backends (Embedder/Reranker/Llm) | core |
semquery-indexer |
File reading, chunking, incremental indexing, content-addressed dedup | core + storage + model(embed) |
semquery-storage |
SQLite Storage impl: documents / chunks / vec_chunks (sqlite-vec) / fts_chunks (FTS5) / model_versions |
core |
semquery-retrieve |
BM25 + vector recall → RRF fusion → rerank; returns SearchHit + ScoreExplain |
core + storage + model(rerank) |
semquery-synth |
Ask: build prompt → LLM → parse [N] citations → Answer |
core + retrieve + model(llm) |
semquery |
CLI binary and Engine facade; exposes init/add/index/search/ask/status plus Engine for library users |
all of the above |
- Upper layers may depend on lower layers; lower layers must not depend on upper layers.
semquery-coredoes not depend on any other internal crate — it defines all traits that other crates implement. This is the key to the library-first promise:cargo add semquery-coredoes not pull in SQLite, llama.cpp, or other heavy stacks.indexerandretrievedo not depend on each other; both operate on data via theStoragetrait.- SQLite details are fully isolated within
semquery-storage. semquery-modeluses feature flags (embed/rerank/llm, all on by default) so consumers enable only the backends they need (avoiding "just want search but must compile llama.cpp").
- The
Storagetrait stays insemquery-core, not insemquery-storage. This is dependency inversion:indexerandretrieveonly needsemquery-core+semquery-modeland do not pullrusqlite/sqlite-vecat compile time. A futuresemquery-storage-pgwould be a drop-in replacement. - No
InMemoryStorage. Tests useSqliteStorage::open_in_memory()(SQLite:memory:mode, millisecond startup). chunks.textis the original text;fts_chunks.textis the jieba-tokenized space-joined text.StorageTx::add_fts_chunks(chunk_ids, tokenized_texts)writes to the FTS table separately —add_chunksonly writes thechunkstable.IndexTxcalls both methods inside onebegin_tx/commitbracket.- All mutations flow through
StorageTx—Storageis read-only (queries +init+begin_tx). This enforces transactional writes at the type level:semquery-retrieveholds&Storageand cannot write;semquery-indexerholds&mut dyn StorageTxand all four indexed tables (documents/chunks/vec_chunks/fts_chunks) plusmodel_versionscommit atomically, so a re-embedding failure cannot leave the store half-written. Document.idis the SHA-256 of the file path — renaming the file changes the id and triggers a reindex, keeping the logic simple.Chunk.idis the SHA-256 oftext— naturally enables content-addressed dedup and change detection.- Embedding model upgrades trigger an explicit reindex: the
model_versionstable records the current model spec for each role; the indexer compares the stored spec with the live one and forces a re-embedding when they differ, avoiding silent staleness. - Invalid citations in the
askflow are filtered out: after the LLM produces[N]markers, only those that actually appear in the provided context are kept. - Not in v0.1: MCP server, xlsx parsing, Python bindings, file-watcher auto-indexing,
semquery modelsubcommand; citation precision is limited to "file + byte range" (not heading/page/row).
rustfmt.toml:max_width=120,tab_spaces=2,chain_width=100,reorder_imports=true,merge_derives=false.- Private struct fields use 2-space indent.
- Error types use
thiserror; do not hand-writeDisplay. - Async traits use
#[async_trait]. - Public APIs get short rustdoc (one-line
//!module description + field comments only for non-obvious conventions, e.g. "SHA-256 oftext", "RFC3339 UTC"). - Never use type-erasure shims like
as any/@ts-ignore(TS concepts). The Rust equivalents areunimplemented!()/todo!()— only allowed in temporary stub methods, and must be removed before commit. - No deep-path references in code: types like
semquery_core::EmbedError::Othermust be flattened toEmbedError::Otherviauseat the top of the file. Never nest more than two::levels inline — import the item and use the short name. This keeps lines short and makes dependencies explicit at the file top.
feat: add sqlite-vec integration
fix: handle missing model file in ModelHub
refactor: split Storage trait into sync methods
doc: update README usage examples
Before committing, make sure:
cargo check --workspacepasses.- New tests pass.
cargo clippy --all-features -- -D warningspasses.cargo fmt --all -- --checkpasses.
- Unit tests prefer stub embedders / stub rerankers / stub LLMs to avoid real model downloads.
- Tests that need a real model (e.g. loading a 4.5GB GGUF) are marked
#[ignore]and run locally viacargo test -- --ignored, not in CI. - Integration tests use
SqliteStorage::open_in_memory()and need no external resources.
sqlite-vecregisters itself process-globally viasqlite3_auto_extension— guarded by astd::sync::Onceto avoid duplicate registration. Seeensure_vec_extension()incrates/semquery-storage/src/sqlite.rs.- Vectors are passed to sqlite-vec as packed native-endian
f32byte streams; KNN queries useWHERE embedding MATCH ?1 AND k = ?2 ORDER BY distance. llama-cpp-2requires cmake to compile thellama.cppC++ source; first build is slow.GGML_METAL=ONenables the Metal GPU backend on macOS (set in.cargo/config.toml).fastembedpulls in the ONNX runtime and model files; on first run it downloads models to~/.cache/fastembedor a similar directory.
| You want to | Look at |
|---|---|
| Add a new Storage backend (e.g. PostgreSQL) | semquery-core's Storage trait + existing SqliteStorage as reference |
| Add a new embedding/rerank/LLM backend | semquery-core's corresponding trait + semquery-model's fastembed/llama-cpp-2 implementations |
| Add a new file format (PDF/xlsx/docx) | semquery-indexer's reader.rs; add a feature flag for each new extractor |
| Add a CLI subcommand | semquery crate's src/main.rs, using clap derive |
| Change the schema | SqliteStorage::init()'s execute_batch + related CRUD methods; consider a migration path. For v0.1 a simple breaking change is fine. |
| Add a unit test | Same module as the code under test, in #[cfg(test)] mod tests; see the existing 5 tests in sqlite.rs for reference |
-
Declare every dependency version once, in the root
Cargo.tomlunder[workspace.dependencies]. This applies todependencies,dev-dependencies, andoptional dependencies. -
Sub-crate
Cargo.tomlfiles must not contain inline version numbers. Always reference workspace-declared dependencies with{ workspace = true }. -
If a sub-crate needs features or wants to mark a dependency as optional, only override those attributes; the version still comes from the workspace. For example:
[dependencies] fastembed = { workspace = true, optional = true } tokio-stream = { workspace = true } [dev-dependencies] tempfile = { workspace = true }
-
When adding a new dependency, first add it to the root
Cargo.toml, then reference it from the sub-crates that need it.
This rule applies to the entire workspace and must be followed for all future dependency changes.