Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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, legacy indexer.yaml) exists, SAI prints note: 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 --config to 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)TypeDefaultResolutionCLI flag?
backendstringqdrantCLI > config > defaulthas --backend
embedderort | ollama | qdrantbackend-aware: qdrantqdrant, duckdbortCLI > config > backend-aware defaulthas --embedder
chunkerstringsmart: ast for ts/tsx/rs/go/py when built --features ast, else linesCLI > config > smart defaulthas --chunker
collectionstringsource_codeCLI > config > defaulthas --collection
modelstringortjinaai/jina-embeddings-v2-base-code; otherwise intfloat/multilingual-e5-smallCLI > config > embedder-aware defaulthas --model
vector_dimintegerort768; otherwise 384config > embedder-aware defaultconfig-only (runtime-validated)
max_chunk_charsintegermodel-aware (see below)config > model-aware defaultconfig-only
prefix_stylee5 | qwen | noneauto-detected from model nameconfig > auto-detectconfig-only (no CLI flag)
duckdb.pathstring.index/code.duckdbconfig > defaultconfig-only
duckdb.model_cachestringunset (None)configconfig-only
duckdb.model_repostringortjinaai/jina-embeddings-v2-base-code; otherwise Xenova/multilingual-e5-smallconfig > embedder-aware defaultconfig-only
ollama.urlstringhttp://localhost:11434config > defaultconfig-only
ollama.modelstringmxbai-embed-largeconfig > defaultconfig-only
qdrant.urlstringunset (None)QDRANT_URL env > configconfig or env (see Environment variables)
exclude_dirslist of strings[] (merged with hard-pruned dirs)config (additive)config-only
includelist of glob strings[] (inactive = match everything)configconfig-only
excludelist of glob strings[]configconfig-only
skip_generated_markerboolfalse when omittedconfigconfig-only
strip_commentsbooltrueconfig > defaultconfig-only
honor_noindex_markerbooltrueconfig > defaultconfig-only
honor_noduplicate_markerbooltrueconfig > defaultconfig-only
similarity.find_similar_min_scorefloat0.85CLI/MCP arg > config > defaulthas CLI flag / MCP arg
similarity.duplicate_min_scorefloat0.93CLI/MCP arg > config > defaulthas CLI flag / MCP arg
similarity.duplicate_min_cluster_sizeinteger2CLI/MCP arg > config > defaulthas CLI flag / MCP arg
similarity.top_kinteger10CLI/MCP arg > config > defaulthas CLI flag / MCP arg
logging.levelerror | warn | info | debug | traceinfo (unknown → info)RUST_LOG > -v/--silent > config > defaulthas -v/-vv/--silent + RUST_LOG
logging.formatpretty | jsonpretty--log-format > config > defaulthas --log-format
logging.timingboolfalse--timing (--silent forces off) > config > defaulthas --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 from duckdb.model_repo. The default on the duckdb backend.
  • ollama — remote Ollama HTTP server (see ollama).
  • qdrant — Qdrant Cloud server-side inference (the Document API). Only valid with backend: qdrant (a duckdb backend with embedder: qdrant is a config error); it is that backend’s default.

How the two combine:

  • backend: qdrant, embedder: qdrant (default) — code/queries are sent as Documents and the cluster embeds them. Requires Cloud (or an inference-enabled deployment) with model enabled 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 (or qdrant,ollama); selecting a local embedder without that feature fails with a clear rebuild hint. vector_dim must 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 pathDefault modelDefault vector_dim
ortjinaai/jina-embeddings-v2-base-code768
any other (Qdrant, Ollama)intfloat/multilingual-e5-small384

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 qwen32000
model contains e51400
model contains jina32000
backend duckdb + embedder ollama32000
otherwise1400

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 e5e5, containing qwenqwen, 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_dirs is additive — its entries are merged on top of the always-pruned dirs (see below). Listing a hard-pruned dir here is harmless.
  • include is 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.
  • exclude globs 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:

  1. include — if active, the file must match an include glob, else it is skipped.
  2. exclude — if any exclude glob matches, the file is skipped (exclude wins over include).
  3. skip_generated_marker, then strip_comments are 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 --config path 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).

VariablePurposeYAML equivalent
QDRANT_API_KEYQdrant API key (secret — keep it out of version control)none (env-only by design)
QDRANT_URLQdrant cluster gRPC URL; overrides qdrant.url if setqdrant.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