The semantic index service

Status: Bus wired. The engine, reconnecting daemon, bridge, CLI and system unit are implemented. Corpus parity and production cutover are separate steps. The tokenizer golden records the legacy 0.21.4/onig token IDs.

indexd stores text chunks with their embeddings and returns the nearest matches for a query. Embeddings are computed locally on the CPU with nomic-ai/nomic-embed-text-v1.5 (768 dimensions), through the pure-Rust candle stack. The model files are loaded from a local directory and never downloaded at run time. Vectors live in SQLite (sqlite-vec), and search runs over an in-memory copy, re-ranked by feedback and staleness.

It runs on one workstation host as a system service, registered on that host's own node. Agents usually reach it through mixos-mcp (context_search, index_workspace, the skills_* tools). A desk on the same machine reaches it over the Bus, or over ssh through indexd bridge, which reads newline-delimited JSON on stdin and makes each request as a local Bus call.

Asking it

On the Bus, the service is indexd:

send indexd indexd.stats
send indexd indexd.search query="how are service dependencies declared" limit=3

Each body is a JSON object. Clients may include an action equal to the verb suffix; a conflicting value is rejected. Replies contain the verb's JSON directly. Errors use rc 10 and a string error field. Metadata is always a JSON-encoded string, including each element of store.metadata.

VerbRequest fieldsReply
indexd.embedtexts, optional prefixembeddings
indexd.storetexts, source, metadatastored, duplicates, ids
indexd.searchquery, limit, optional source, metadata_filterresults
indexd.updateid, optional content, metadata, sourceupdated, re_embedded
indexd.deleteoptional ids, optional filter (at least one non-empty)deleted
indexd.listoptional source, limit, offsetitems, total
indexd.feedbackid, usefulid, feedback_score
indexd.supersedeold_id, new_id, optional reasonupdated
indexd.staleoptional source, never_retrieved_age_days, low_value_min_retrievals, long_dormant_days, per_bucket_limitthree buckets, total_chunks
indexd.statsnonecounts, model/cache/queue statistics, by_source
indexd.index_fileabsolute path, optional content, source, domain, backgroundindexed, sections; background acknowledgement when queued

search excludes superseded chunks before applying its limit. Filter entries have field, op (eq, gt, lt, gte, lte, contains) and value.

Delete filters use the same predicate shape, for example {"filter":[{"field":"path","op":"eq","value":"/srv/project/a.md"}]}. Predicates are ANDed; when IDs are also supplied they restrict the matching set. DELETE accepts only eq, with a non-empty string, number or boolean value. Each predicate also requires the matching JSON type (integers and reals share the numeric type); null, arrays and objects are refused. Search retains all operators listed above. Deletion updates the durable vectors and in-memory index in one operation.

CLI and bridge

indexd -c /etc/mixos/indexd/config.conf.mix call indexd.stats opens the configured engine in-process and prints one JSON reply, with exit status 0 for success or 1 for an error. Bare verb names also work. For example:

indexd -c /etc/mixos/indexd/config.conf.mix call indexd.search '{"query":"service dependencies","limit":3}'

Writes (store, update, delete, feedback, supersede, index_file) require call --offline with the daemon stopped: otherwise its in-memory index would desynchronise. Read verbs need no flag. call always refuses background:true because no process remains to run the queued job.

indexd bridge connects to the local node's Bus once and forwards one call per stdin line, flushing each reply immediately. It does not open the index or require model configuration. Keep one process alive for repeated requests, including through ssh -T user@example.test /opt/mixos/bin/indexd bridge. The bridge honours non-empty MIXOS_NODED_URL first. Otherwise it reads --node-config PATH, then non-empty MIXOS_NODE_CONFIG, defaulting to /opt/mixos/etc/node.conf.mix, the same file pinned by the system unit. It derives the broker address from that file rather than user config discovery.

{"id":1,"verb":"indexd.search","body":{"query":"service dependencies","limit":3}}

Each stdout line is {"id":1,"rc":0,"body":{...}}, with the original JSON id preserved. Application errors retain rc 10 and the JSON error body. Transport failures use {"id":1,"rc":-1,"error":"..."} and then exit non-zero, including initial connection failure, so a supervisor can respawn it. EOF exits 0; stdout contains only protocol replies. The bridge never retries a call.

Requests may carry timeout_secs, default 60 and capped at 600 seconds. A timeout returns {"id":1,"rc":-1,"kind":"timeout","error":"..."} and leaves the bridge alive. The operation's outcome is unknown. Empty Bus bodies become JSON null; Bus error headers are forwarded as the top-level error.

Operating the system service

indexd serve registers service indexd using the standard Bus connection helper, advertises the eleven verbs above, and reconnects with backoff. MIXOS_NODE_CONFIG selects the system broker, as for dnsd. The supplied unit pins /opt/mixos/etc/node.conf.mix; no broker URL is embedded in indexd. SIGTERM stops admission and completes accepted Bus calls before exit. The model loads lazily on the blocking pool under the inference permit and unloads after the configured idle interval (300 seconds by default).

Configuration lookup is -c/--config, then /etc/mixos/indexd/config.conf.mix, then $XDG_CONFIG_HOME/mixos/indexd.conf.mix (or the equivalent under $HOME/.config). indexd -c FILE check-config validates the local config.json, tokenizer.json and model.safetensors before loading any weights. HF snapshot symlinks are supported. There is no database environment override; service.vectors_db is authoritative. MIXOS_INDEXD_SEARCH remains the engine's one-release rollback switch (sqlite selects SQLite search).

The default domain map is empty, so unmapped files use general. Configure absolute prefixes; the daemon does not expand ~ in domain keys or file paths. The sample at services/indexd/examples/config.conf.mix includes:

domains: {map: {
  "/home/user/Projects/mixos": "mixos",
  "/home/user/.mix": "mix",
  "/home/user/.ns": "ns",
  "/home/user/.mc": "mc"
}}

Replace /home/user with the operator's absolute home path. Set model_dir to the pre-seeded snapshot; no model download happens at runtime. ProtectHome=yes hides home directories from the service: callers must pass index_file.content for those paths, or use store. index_file rejects a tilde-prefixed path even when content is supplied.

The component owns indexd.service and sysusers.conf; the desktop release bundle carries the standalone binary and these files under share/indexd/units/, with the sample config under share/indexd/examples/config.conf.mix, following the component-owned maild packaging pattern. Install the binary under /opt/mixos/bin, the unit under /etc/systemd/system, sysusers under /usr/lib/sysusers.d/indexd.conf, and a customised config under /etc/mixos/indexd/config.conf.mix. Apply sysusers before starting the service. The unit wants system noded.service, runs as indexd:indexd, limits memory to 2 GiB, sets MALLOC_ARENA_MAX=2, and hardens filesystem, devices and kernel access. TCP address families remain available for the configured broker. MemoryDenyWriteExecute=yes follows the former candle CPU service. systemd creates the state and configuration directories, so no tmpfiles entry is needed. Before cutover, back up existing data and explicitly change its ownership from the legacy account; the new account does not reuse its UID.

Tests

On a CBC worker, cargo test -p indexd runs the engine tests, registry checks, real daemon/bridge pipe tests, and services/indexd/tests/bus_test.mix against libs/test-broker. Set MIXOS_TEST_MIX if Mix is not installed at /opt/mixos/bin/mix. Embeddings are deterministic cache fixtures; no model is needed. The tokenizer parity test compares 0.22.2 token IDs against the committed legacy 0.21.4/onig golden; it stays ignored unless INDEXD_TEST_TOKENIZER names the staged tokenizer file.

Indexd is not exhaustive

indexd answers "what is relevant to X", quickly and approximately, from an index that can lag the files. To prove that something does not occur, use an exact search over the current files instead.

This page as Markdown · This page describes the current main branch of MixOS.