Skip to content

Guides

Configuration

One catalog of settings, five precedence layers, file discovery and reload, and the scope rule that decides what may change without a restart.


serve and node read their settings through one catalog. Every key has a dotted name, a kind, a scope, a default, an environment variable, and a flag, and the same key means the same thing wherever it is set. Flags, YAML keys, and environment variables resolve to catalog entries.

Use the config commands to inspect and change settings. This site summarizes the generated catalog in the configuration reference and the CLI reference.

A fully-commented example listing every key at its default, with its scope, environment variable, flag, and constraint, is checked in at the repository root as ysearch.example.yml. It is generated from the catalog and checked by continuous integration. Copy it and uncomment the settings you need.

Precedence

A value comes from the highest of five layers that sets it:

default  <  file  <  env  <  flag  <  api
  • default — the catalog.
  • file — every discovered file, in load order.
  • env — every YSEARCH_* variable.
  • flag — only the flags the operator typed, so a flag left at its default never outranks the file.
  • api — the ephemeral overlay ConfigService.SetSetting writes, which a restart drops.

Every layer is validated in full before anything wins. An invalid value in the file is refused at startup even when an environment variable shadows it, so invalid shadowed values are caught before they can take effect.

Where files come from

With config.search at its default of true, serve and node probe, in order, each later file overriding the earlier ones:

  1. system — /etc/ysearch/ysearch.yml (or .yaml), then every *.yml/*.yaml in /etc/ysearch/conf.d/ in name order.
  2. user — the same pair under $XDG_CONFIG_HOME/ysearch, or ~/.config/ysearch when XDG_CONFIG_HOME is unset, including its conf.d/.
  3. workdir — ./ysearch.yml (or .yaml). The working directory has no conf.d.
  4. --config-dir DIR — every *.yml/*.yaml in DIR in name order, a file at a time.
  5. --config FILE — one explicit file, layered last among files so it wins over everything discovered.

--config-search=false turns off steps 1–3, leaving only --config-dir and --config.

The three discovery keys — config.search, config.dir, config.file — may be set by flag or environment only. A file that set them would be honored once and ignored by every reload, so a file that sets one is refused.

Every discovered file is announced on stderr at startup, in load order:

config: loaded /etc/ysearch/ysearch.yml (system)
config: loaded /etc/ysearch/conf.d/10-cache.yml (system)
config: loaded ./ysearch.yml (workdir)
config: loaded /run/ysearch/override.yaml (--config)

When nothing loaded, the places searched are named instead:

config: --config-dir /run/ysearch.d: skipped notes.txt (not a regular file)
config: no configuration file found (searched: /etc/ysearch, ~/.config/ysearch, .)

Paths in a file are relative to the file

server.data_dir, object.dir, cache.dir, ingest.dir, and embedding.model_cache_dir are filesystem locations. A relative value for one of them in a configuration file resolves against the directory holding that file, not against the process working directory:

yaml
# /srv/search/ysearch.yml
object:
  dir: objects          # /srv/search/objects, wherever the process was started

That is what makes a directory holding its own ysearch.yml a portable installation. Only the file layer rebases: a path typed as a flag or exported in the environment resolves against the working directory.

The file

The file is YAML. Nested keys mirror the dots:

yaml
server:
  listen: 127.0.0.1:9500
  grace_period: 10s
index:
  name: articles
object:
  dir: /var/lib/ysearch/objects
cache:
  dir: /var/lib/ysearch/cache
  full_bytes: 16GiB
fleet:
  fallback_workers: [10.0.0.2:9602, 10.0.0.3:9602]
log:
  level: info

The rules, each of which the file reports by line — all problems together, rather than the first one:

  • Every scalar is read as the text the operator typed and typed against the catalog kind, never YAML's implicit schema. 0001 stays 0001 for a string key, 1e6 stays 1e6, a date stays a string. A number key reads 0010 as ten.
  • A null value (~, null, or a key with no value) means "not set here": the key falls through to the layer below. Write "" to mean the empty string.
  • A string-list key takes a YAML sequence of scalars or one comma-separated string; both produce the same value.
  • An unknown key is refused, naming the file, the line, and the nearest catalog key: file:config.yaml:7: unknown key "querry.default_top_k" (did you mean "query.default_top_k"?).
  • A secret is refused by name.
  • The file may not set config.file.
  • A mapping where a value is expected, a duplicate key (server.listen spelled dotted and nested are the same path), a YAML merge key (<<), and a second YAML document are all refused. Anchors and aliases are followed.
  • An empty file, or one holding only comments, sets nothing and is valid.

Environment variables

The variable for a key is YSEARCH_ plus the key in upper case with every dot written as a single underscore, the key's own underscores left alone:

Key Variable
storage.mode YSEARCH_STORAGE_MODE
object.s3.bucket YSEARCH_OBJECT_S3_BUCKET
server.max_send_bytes YSEARCH_SERVER_MAX_SEND_BYTES

Dots and underscores both appear as underscores in environment names. Variables resolve through catalog lookup, and a test checks that the catalog has no environment-name collisions.

A variable set to the empty string is an explicit empty value, not an absence. Unset it to fall through.

Any other YSEARCH_* variable in a serve or node environment refuses startup with an error naming the variable:

$ YSEARCH_LOG_LEVL=debug ysearch serve --config /etc/ysearch/config.yaml
ysearch: serve: config: unknown environment variables: YSEARCH_LOG_LEVL (did you mean YSEARCH_LOG_LEVEL?)

Flags

Every key that may be set has a derived long flag: -- plus the key with dots and underscores as hyphens, so --storage-mode and --server-max-send-bytes. Keys that had a flag before the catalog existed keep it as an alias — --mode, --max-send-bytes, --index, --config — and --help shows the alias while the derived form is hidden but accepted. Setting both forms to different values is refused.

Bool keys are bool flags (--config-watch=false). Every other kind takes text the catalog parses, so sizes and durations carry units: --cache-full-bytes 16GiB, --remote-latency-floor 250ms. An enum matches case-insensitively and renders in its canonical case, so --mode auto is AUTO. A list is comma-separated and refuses an empty item, because a trailing comma is almost always a missing endpoint.

ysearch: serve: config: flag:--grace-period: server.grace_period: "fast" is not a duration (for example 5s, 250ms)

--data-dir

--data-dir DIR is the developer shortcut for a single-directory layout. It derives, wherever nothing more specific than the default was set:

Derived Value
object.backend fs
object.dir DIR/objects — the object store
cache.dir DIR/cache — the disposable read cache
ingest.dir DIR/ingest — the builder's spool
config.file DIR/config.yaml, when that file exists

It creates the three directories and makes DIR absolute against the working directory, so every reported path agrees. Together with the server.listen default of 127.0.0.1:9500, it is what lets a bare command run:

zsh
ysearch serve --index demo --listen 127.0.0.1:9500 --data-dir /tmp/ys
# equals
ysearch serve --index demo --listen 127.0.0.1:9500 --object-dir /tmp/ys/objects \
  --cache-dir /tmp/ys/cache --ingest-dir /tmp/ys/ingest \
  --config /tmp/ys/config.yaml   # only when that file exists

Derived entries report provenance: DERIVED and source: derived from flag:--data-dir; a key the operator set explicitly is left alone.

Provenance

Each resolved key remembers where its value came from, reported as a provenance enum and a source string: default, file:/etc/ysearch/config.yaml:12, env:YSEARCH_STORAGE_MODE, flag:--mode, api, or derived from flag:--data-dir. A file-sourced entry names the line that set it.

zsh
ysearch config get storage.mode
ysearch config list --prefix storage.

rendered is the value in the text form a file or flag would carry; value is the typed form.

Reloading the file

With a config.file and config.watch at its default of true, the server watches the file for its whole lifetime. Two mechanisms see a change: an fsnotify watch on the file's parent directory, which catches an in-place write and an atomic rename alike — including a ConfigMap symlink swap — and a 2-second poll as the backstop for filesystems that deliver no events. A burst of events is debounced for 250 ms so an editor's write-and-rename settles into one reload.

A reload is keyed on content, not events: the bytes are hashed, so a touch without a change is not a reload.

What a reload may change is decided by scope. A node-runtime key takes effect on the next snapshot. A startup key is pinned at its running value — the file's new value is reported on stderr, not applied, and the reload is otherwise accepted:

config: server.listen changed in file:/etc/ysearch/config.yaml:1 to 127.0.0.1:9578 but is a startup setting; the running value 127.0.0.1:9577 (from file:/etc/ysearch/config.yaml:1) stays until restart

A file that fails to parse or validate is refused as a whole and the previous configuration keeps running:

config: reload refused, keeping the previous configuration: config: file:/etc/ysearch/config.yaml:7: unknown key "querry.default_top_k" (did you mean "query.default_top_k"?)

Refusal is once per content: the refused bytes are remembered, so neither the poll nor a repeated touch refuses the same file again and again. Editing the file to something valid — including reverting it — applies and clears the refusal.

The outcome is visible three ways: the stderr line, the counters ysearch_config_reloads_total and ysearch_config_reload_failures_total, and ListSettings, whose last_reload_error carries the most recent refusal until a reload succeeds and whose last_reload_unix_millis is the time of the last successful reload or startup.

A watch that cannot be established degrades rather than failing: one stderr line, config: file watching disabled: ..., and the server keeps the configuration it started with. --config-watch=false turns the watcher off.

Scope: what may change at runtime

Each setting declares one of three runtime scopes:

Scope Meaning
startup read once at boot; a file change is reported and pinned, SetSetting is refused
node-runtime changes on this node through the file or SetSetting, as an overlay a restart drops
compile-time a constant catalogued for visibility; no file, environment, or flag may set it

node-runtime covers log.level, most storage.* tuning keys, ingest.* except ingest.dir, all of follower.*, gc.*, vector.*, and tail.*, and most of builder.*, compaction.*, and query.*. The exceptions in those three sections are startup keys, such as builder.output_format_version and query.scoring_profile, so check the reference for each key. The ysearch additions stats.* and prototype.* are all startup. The two format.* keys are compile-time values.

The scope field of a ListSettings entry is the authority when a written table and the binary disagree.

When settings take effect

node-runtime says a key may change without a restart. Whether the running subsystem re-reads it is per key:

Keys Read
ingest.seal_age, ingest.seal_bytes, ingest.seal_documents, ingest.max_batch_bytes, ingest.max_document_bytes, follower.poll_interval, follower.retired_generation_grace live — the builder checks seal thresholds on every append and tick, the router reads the byte caps per batch, the follower re-reads its interval each poll
builder.* at each seal — the online builder reads the budget from the current snapshot when it builds
compaction.*, gc.* at each maintenance pass
query.* once at startup — snapshotted into the closure the search service reads
storage.* once at startup
log.level by nothing yet — no logger is wired to it

So config set ingest.seal_age 5s changes the next seal, while config set query.regex_max_expansions 1024 is visible to config get and takes effect at the next restart.

The config verbs

zsh
ysearch config list --prefix query. --server 127.0.0.1:9500
ysearch config get ingest.seal_age
ysearch config set log.level debug --server 127.0.0.1:9500
ysearch config unset log.level
ysearch config watch --prefix ingest.

list, get, set, unset, and watch act on the node --server names and talk to ysearch.v1.ConfigService. The overlay set writes is per node and ephemeral: a restart, and unset, return the key to whatever the file, environment, and flags say. Cluster-wide propagation is a later layer.

config export is the offline verb. It needs no server: it layers the defaults, the discovered files, the environment, and the flags exactly as serve would, and prints every setting with its source. --yaml prints the resolved values as one configuration file, leaving out secrets, the discovery keys, and compile-time settings, because a file may not set those:

zsh
ysearch config export --yaml > /etc/ysearch/ysearch.yml

Status codes

Code When
NOT_FOUND the key is not in the catalog
FAILED_PRECONDITION on set or unset, the key is startup or compile-time scoped, or a secret
INVALID_ARGUMENT the value fails the kind or constraint
UNAVAILABLE a watch stream ends because the server is draining

Secrets

object.s3.access_key and object.s3.secret_key are read from the environment only — YSEARCH_OBJECT_S3_ACCESS_KEY and YSEARCH_OBJECT_S3_SECRET_KEY. So are object.s3.session_token and the prototype adapter's prototype.sidecar.password. None of them has a flag, the file refuses them by name, SetSetting answers FAILED_PRECONDITION, and ListSettings marks them secret: true, sends no value, redacts rendered, and names no flag. A writable s3 backend requires the access key and the secret key at startup. A read-only process can use the ambient credential chain when both are absent. Temporary credentials supply object.s3.session_token through YSEARCH_OBJECT_S3_SESSION_TOKEN. See the object store.

A cache.dir reached through a symlink is resolved, not refused: the path is resolved once at startup and the disposable cache anchors at the directory that resolution named, so --cache-dir /tmp/ys-cache on macOS is the cache under /private/tmp/ys-cache. From there the cache follows no link at all — it opens every component with O_NOFOLLOW and compares file identity before each admission, so a component swapped for a symlink afterward fails the open and a root renamed out from under the process fails the identity check. embedding.model_cache_dir is resolved the same way, for the same reason.

Next