Configuration reference
SAI reads a single YAML file, sai-cfg.yml, to decide what gets chunked, embedded, and stored. When --config <path> is not passed, SAI looks in the current directory for sai-cfg.yml, then sai-cfg.yaml, then the legacy indexer.yaml — the first one found wins. Generate a fully-commented starter file with semanticastindexer init (interactive; --yes accepts every default). Every value here is resolved in src/config.rs (build_plan), and every key in this page maps to a real field — nothing else is read.
The resolution order for most knobs is:
CLI flag > sai-cfg.yml value > built-in default
The Qdrant API key is the exception — it is a secret read only from the QDRANT_API_KEY environment variable, never from YAML (see Environment variables below). The Qdrant URL is a normal setting: qdrant.url in YAML, overridable by QDRANT_URL.
If none of the default files (
sai-cfg.yml,sai-cfg.yaml, legacyindexer.yaml) exists, SAI printsnote: no config at sai-cfg.yml — using built-in defaults (only hard dirs pruned)and continues with the defaults below. If you pass an explicit--configto a missing file, it is a hard error instead.
For how honor_noindex_marker / honor_noduplicate_marker interact with in-source sai-noindexing / sai-noduplicate comments, see opt-out markers. For the CLI flags that override these keys, see the CLI reference.
Key reference
Type, default, and resolution for every recognized key. “Has CLI flag” means a --flag can override it; “config-only” means the YAML key is the only way to set it (the value otherwise comes from a default or auto-detection).
| Key (full path) | Type | Default | Resolution | CLI flag? |
|---|---|---|---|---|
backend | string | qdrant | CLI > config > default | has --backend |
embedder | ort | ollama | qdrant | backend-aware: qdrant → qdrant, duckdb → ort | CLI > config > backend-aware default | has --embedder |
chunker | string | smart: ast for ts/tsx/rs/go/py when built --features ast, else lines | CLI > config > smart default | has --chunker |
collection | string | source_code | CLI > config > default | has --collection |
model | string | ort → jinaai/jina-embeddings-v2-base-code; otherwise intfloat/multilingual-e5-small | CLI > config > embedder-aware default | has --model |
vector_dim | integer | ort → 768; otherwise 384 | config > embedder-aware default | config-only (runtime-validated) |
max_chunk_chars | integer | model-aware (see below) | config > model-aware default | config-only |
prefix_style | e5 | qwen | none | auto-detected from model name | config > auto-detect | config-only (no CLI flag) |
duckdb.path | string | .index/code.duckdb | config > default | config-only |
duckdb.model_cache | string | unset (None) | config | config-only |
duckdb.model_repo | string | ort → jinaai/jina-embeddings-v2-base-code; otherwise Xenova/multilingual-e5-small | config > embedder-aware default | config-only |
ollama.url | string | http://localhost:11434 | config > default | config-only |
ollama.model | string | mxbai-embed-large | config > default | config-only |
qdrant.url | string | unset (None) | QDRANT_URL env > config | config or env (see Environment variables) |
exclude_dirs | list of strings | [] (merged with hard-pruned dirs) | config (additive) | config-only |
include | list of glob strings | [] (inactive = match everything) | config | config-only |
exclude | list of glob strings | [] | config | config-only |
skip_generated_marker | bool | false when omitted | config | config-only |
strip_comments | bool | true | config > default | config-only |
honor_noindex_marker | bool | true | config > default | config-only |
honor_noduplicate_marker | bool | true | config > default | config-only |
similarity.find_similar_min_score | float | 0.85 | CLI/MCP arg > config > default | has CLI flag / MCP arg |
similarity.duplicate_min_score | float | 0.93 | CLI/MCP arg > config > default | has CLI flag / MCP arg |
similarity.duplicate_min_cluster_size | integer | 2 | CLI/MCP arg > config > default | has CLI flag / MCP arg |
similarity.top_k | integer | 10 | CLI/MCP arg > config > default | has CLI flag / MCP arg |
logging.level | error | warn | info | debug | trace | info (unknown → info) | RUST_LOG > -v/--silent > config > default | has -v/-vv/--silent + RUST_LOG |
logging.format | pretty | json | pretty | --log-format > config > default | has --log-format |
logging.timing | bool | false | --timing (--silent forces off) > config > default | has --timing/--silent |
Backend and embedder
backend selects the vector store: qdrant (default) or duckdb. embedder is the single knob for where and how embeddings are produced. Its default is backend-aware: the qdrant backend defaults to embedder: qdrant (server-side inference); the duckdb backend defaults to embedder: ort (local ONNX).
embedder accepts three values:
ort— local ONNX Runtime; downloads the model fromduckdb.model_repo. The default on theduckdbbackend.ollama— remote Ollama HTTP server (seeollama).qdrant— Qdrant Cloud server-side inference (theDocumentAPI). Only valid withbackend: qdrant(aduckdbbackend withembedder: qdrantis a config error); it is that backend’s default.
How the two combine:
backend: qdrant,embedder: qdrant(default) — code/queries are sent asDocuments and the cluster embeds them. Requires Cloud (or an inference-enabled deployment) withmodelenabled in the Inference tab. This is the unchanged, pre-existing behavior.backend: qdrant,embedder: ort/ollama— embed on-device and upsert raw vectors. Works against self-hosted / OSS Qdrant (no Inference engine, no API key needed) and any local model (e.g.jinaai/jina-embeddings-v2-base-code, 768-d). Requires a binary built with--features qdrant,ort(orqdrant,ollama); selecting a local embedder without that feature fails with a clear rebuild hint.vector_dimmust match the chosen model exactly (validated at runtime).backend: duckdb,embedder: ort/ollama— embed locally and store vectors in a DuckDB file with a VSS/HNSW index.
DuckDB and the local embedders are feature-gated — the binary must be built with --features ort, --features ollama, or --features all.
See Qdrant Cloud → Local-embed mode.
Chunker
chunker is lines or ast. The default is smart: when no chunker is set on the CLI or in config, SAI selects ast if the binary was built with --features ast and any requested extension is in the AST-preferred set (ts, tsx, rs, go); otherwise it falls back to lines. The chunker still dispatches per file, so a mixed walk AST-parses files with a grammar and line-chunks the rest.
chunker: ast # or: lines — CLI --chunker always wins
Model, vector dimension, and chunk size
model is the embedding model label (and, for Qdrant, must match the cluster’s Inference tab). Its default depends on the resolved embedder:
| Embedder path | Default model | Default vector_dim |
|---|---|---|
ort | jinaai/jina-embeddings-v2-base-code | 768 |
| any other (Qdrant, Ollama) | intfloat/multilingual-e5-small | 384 |
vector_dim must equal the embedder model’s output dimensionality. It is config-only and runtime-validated — a mismatch is a clear error. If you change model to one with a different dimensionality, set vector_dim to match (e.g. mxbai-embed-large = 1024, nomic-embed-text = 768).
max_chunk_chars is the character cap both chunkers honor (a ~4-chars/token approximation of the model’s window). When unset, the default is model-aware (default_cap in src/config.rs):
| Condition (checked in order) | Cap (chars) |
|---|---|
model contains qwen | 32000 |
model contains e5 | 1400 |
model contains jina | 32000 |
backend duckdb + embedder ollama | 32000 |
| otherwise | 1400 |
prefix_style (config-only)
prefix_style controls the embedding prefix policy and has no CLI flag. Accepted values are e5, qwen, and none. When omitted, it is auto-detected from the model name: a name containing e5 → e5, containing qwen → qwen, otherwise → none. It is applied by both local embedders and the Qdrant document path.
# prefix_style: e5 # e5 | qwen | none — omit to auto-detect from model
duckdb
Used only when backend: duckdb; ignored by Qdrant.
duckdb:
path: .index/code.duckdb # DuckDB file; created on first index
# model_repo: jinaai/jina-embeddings-v2-base-code # ort: HF repo for model.onnx + tokenizer.json
# model_cache: .model_cache # ort: offline ONNX/HF cache dir (unset by default)
duckdb.model_repo defaults to jinaai/jina-embeddings-v2-base-code for the ort embedder, and to Xenova/multilingual-e5-small otherwise. duckdb.model_cache is unset by default.
ollama
Used only when embedder: ollama.
ollama:
# url: http://localhost:11434 # default
# model: mxbai-embed-large # default (1024-d → set vector_dim: 1024)
ollama.model defaults to mxbai-embed-large (1024-d). Set vector_dim to match the model you pull.
File selection: exclude_dirs, include, exclude
exclude_dirs: # directory NAMES pruned during the walk (never descended into)
- __tests__
include: [] # allow-list globs; empty = consider everything
exclude: # glob patterns matched per file path; '**' spans directories
- "**/*.test.ts"
- "**/*.d.ts"
exclude_dirsis additive — its entries are merged on top of the always-pruned dirs (see below). Listing a hard-pruned dir here is harmless.includeis an allow-list of globs. When non-empty it becomes active: a file must match at least one include glob to be considered. When empty/omitted, everything is considered.excludeglobs are matched against each file path. Exclude always wins over include — the gate is(!include_active || include matches) && !exclude matches.
Selection order (per file)
For each file under the root, after directory pruning and the extension filter:
include— if active, the file must match an include glob, else it is skipped.exclude— if any exclude glob matches, the file is skipped (exclude wins over include).skip_generated_marker, thenstrip_commentsare applied to the surviving content.
skip_generated_marker (defaults to false)
skip_generated_marker is a plain bool — unlike most toggles it has no “absent = true” fallback. When the key is omitted, it defaults to false (generated-marker scanning is off). The shipped sai-cfg.yml sets it to true explicitly:
skip_generated_marker: true # scan file head for @generated / "DO NOT EDIT." markers
When enabled, it skips files whose head carries an autogenerated marker (e.g. @generated, Go’s // Code generated ... DO NOT EDIT.) regardless of filename.
strip_comments
strip_comments defaults to true when omitted. It removes // and /* */ comments from C-family source before embedding so only code reaches the backend; string/template literals are preserved and line numbers stay accurate.
strip_comments: true
honor_noindex_marker / honor_noduplicate_marker
Both default to true when omitted. They are not present in the shipped sai-cfg.yml — they take effect via the defaults. See opt-out markers for the in-source sai-noindexing / sai-noduplicate behavior.
honor_noindex_marker: true # respect sai-noindexing comments (skip chunk entirely)
honor_noduplicate_marker: true # respect sai-noduplicate comments (index, but no clustering)
similarity
Thresholds for the sai_find_similar and sai_find_duplicates MCP tools and the similar / duplicates CLI subcommands. Per-knob resolution is CLI flag / MCP tool arg > config value > built-in default. All fields are optional. These cutoffs are model-specific — tune them per embedder.
similarity:
find_similar_min_score: 0.85 # default 0.85 — drop neighbors below this cosine
duplicate_min_score: 0.93 # default 0.93 — edge cutoff between two chunks
duplicate_min_cluster_size: 2 # default 2 — smallest cluster to report
top_k: 10 # default 10 — nearest-neighbor fan-out per chunk
logging
Diagnostic-logging defaults. All diagnostics go to stderr (stdout stays JSON-RPC / data only — see the CLI logging reference). This block is the lowest-precedence tier: the RUST_LOG env var and the CLI flags (-v/--silent/--log-format/--timing) always override it. All fields are optional.
logging:
level: info # error | warn | info | debug | trace. Unknown → info. -v/-vv/--silent and RUST_LOG override.
format: pretty # pretty (human) | json (one object per line). --log-format overrides.
timing: false # per-operation timing spans (index/embed/query/sync durations). --timing on; --silent off.
Its main audience is the MCP server: the client launches the binary with a fixed command and environment, so setting RUST_LOG per client is awkward — a logging: block in the project’s sai-cfg.yml (which the server already reads) raises verbosity project-wide with no launch-config edits. For ordinary CLI runs, -v/RUST_LOG are usually more convenient and this block can be omitted entirely.
timing toggles only the per-operation span-close durations. The single end-of-command summary line (done … in Ns) is a plain info event and is unaffected — it prints whenever the level is info or lower, regardless of timing. For the precedence details and examples, see the CLI logging reference.
The config is read before the logging subscriber is installed, so a missing or malformed file at that point falls back to these defaults silently (the usual “no config” warning and hard errors on a bad explicit
--configpath still surface later during the normal config load).
Always-pruned directories
Independent of config, these directories are always pruned during the walk (the HARD_PRUNE_DIRS set in src/config.rs):
node_modules .git dist build target .next coverage .turbo
exclude_dirs entries are added on top of this set; you cannot un-prune a hard-pruned dir via config.
AST-preferred extensions
The smart chunker default selects ast (when the ast feature is compiled in and no chunker was set explicitly) for these extensions (AST_PREFERRED_EXTS):
ts tsx rs go
Any other extension falls back to the lines chunker even when ast support is present.
Environment variables
The Qdrant API key is a secret and is read only from the environment — it never belongs in YAML. The cluster URL is not secret: set it in YAML as qdrant.url, or via the QDRANT_URL environment variable (which takes precedence over the YAML value).
| Variable | Purpose | YAML equivalent |
|---|---|---|
QDRANT_API_KEY | Qdrant API key (secret — keep it out of version control) | none (env-only by design) |
QDRANT_URL | Qdrant cluster gRPC URL; overrides qdrant.url if set | qdrant.url |
If your sai-cfg.yml contains qdrant.url it is safe to commit (the URL is not a secret); the key never lives there.
Footgun: unknown keys are silently ignored
The Config struct is deserialized with all fields optional and #[serde(default)], so a partial file still parses — and unrecognized or misspelled YAML keys are silently ignored. A typo like skip_generated_marekr: true or exclude_dir: will not raise an error; the intended setting never takes effect and the default is used instead. Double-check key spelling and nesting (e.g. duckdb.path, similarity.top_k) against the Key reference table above.
See also
- CLI reference — flags that override these keys
- Opt-out markers —
sai-noindexing/sai-noduplicatedetail