Skip to content

Guides

Local development

The one-binary yolosearch-local setup, the portable installation directory, and the Docker Swarm fleet that gives you a two-worker topology.


There are two local setups, and the choice between them is about what you need to observe.

One binary. yolosearch-local runs the complete data plane plus the admin UI in a single process, with no flags required. Use it for schema work, ingest, queries, and anything that does not depend on more than one node.

A Docker Swarm fleet. just swarm-up deploys a coordinator, two workers with separate caches, a writable server, an aggregator, a compactor, MinIO, and the admin console. Use it to exercise worker placement, remote execution, and cross-node failures.

Testing placement, remote workers, and cross-node failure requires separate node processes. Swarm and Kubernetes provide deployment setups for these tests.

Prerequisites

Go 1.27 or newer, Node.js and npm, just, and zsh. The Swarm path adds Docker Engine or Docker Desktop with a running Linux-container daemon.

The shortest path

From the repository root:

zsh
just build
./bin/yolosearch-local

Or compile and run in one step with just local. Once the ready block appears, open http://127.0.0.1:8787:

yolosearch-local: ready
  data:  127.0.0.1:9500
  admin: http://127.0.0.1:8787
  root:  <cwd>/yolosearch-local-data
  objects: <cwd>/yolosearch-local-data/objects
  cache:   <cwd>/yolosearch-local-data/cache
  ingest:  <cwd>/yolosearch-local-data/ingest
  models:  <cwd>/yolosearch-local-data/models

The native gRPC API is 127.0.0.1:9500, which is the address the ordinary yolosearch client already dials. In another terminal:

zsh
./bin/yolosearch schema apply deploy/swarm/articles.proto
./bin/yolosearch push articles deploy/swarm/articles.jsonl
./bin/yolosearch search articles search --fields title,url
./bin/yolosearch stats articles

What is in the process

One OS process containing the production direct multi-namespace data path, the builder, the publisher, the follower and cache, the idle-aware compactor, the optional GC scheduler, the embedding runtime, the native gRPC server, and the admin HTTP/Connect server. The admin bridge still calls the public gRPC API; there is no local-only search adapter, so what the console does here is what it does everywhere.

The default persistent layout:

yolosearch-local-data/
├── config.yaml   optional configuration
├── objects/      authoritative schemas, segments, commits, and catalogs
├── ingest/       durable spool and build scratch
├── cache/        disposable segment/block cache
└── models/       checksum-addressed internal embedding models

Data persists across restarts; restarting the same command reopens the same schemas and documents. Ctrl-C or SIGTERM drains both endpoints and joins every background goroutine. Logs go to the inherited stdout and stderr only; the process creates no hidden log file.

Settings work exactly as configuration describes — flags, YOLOSEARCH_* variables, yolosearch-local-data/config.yaml, or the runtime config API. Only server.data_dir has a local command default, so every explicit setting wins. Secrets remain environment-only.

zsh
./bin/yolosearch-local --data-dir ./scratch/search \
  --listen '[::1]:9500' --admin-listen '[::1]:8787'

Local mode refuses S3 as authoritative storage. Internal model packages live under models/; external HTTP and gRPC embedders use the same embedding.* settings as yolosearch serve, so vector search works here. A provider being unavailable affects the requests that need it and does not prevent lexical-only startup.

The idle-aware compaction scheduler is available in-process: set compaction.enabled=true when the local process should run it. Safe GC stays off unless gc.sweep_enabled=true, and enabling it retains the existing dry-run, quarantine, and fresh reachability proof.

Resetting

Stop the process and run this guarded, path-resolved reset from the repository root:

zsh
local_root="$(realpath ./yolosearch-local-data)"
test "$local_root" = "$(pwd -P)/yolosearch-local-data" && rm -rf -- "$local_root"

A portable installation: --dir

--dir turns a directory into a self-contained installation and runs it:

zsh
./bin/yolosearch-local --dir ~/search/articles
# yolosearch-local: created installation /home/you/search/articles
# config: loaded /home/you/search/articles/yolosearch.yml (--config)
# yolosearch-local: ready

It creates the directory and objects/, cache/, ingest/, and models/ beneath it — the same names --data-dir derives — so the layout exists and is inspectable before anything starts. When the directory holds no configuration file yet (yolosearch.yml, yolosearch.yaml, or config.yaml), it writes yolosearch.yml with the object backend, the four directories, the listen address, and the log level. A file that is already there is never rewritten: --dir means "run this installation", not "reset it".

The generated file spells its directories relative to itself, and a relative path in a configuration file resolves against that file's directory. So the installation moves:

zsh
mv ~/search/articles /media/stick/articles
./bin/yolosearch-local --dir /media/stick/articles   # nothing to edit

--dir and --data-dir both name a root, so giving both is an error. An explicit --config still wins over the installation's own file, and every flag still outranks both.

Finding a flag

--help does not print every catalog flag. It opens on an ESSENTIALS block — the dozen flags that decide where the data lives, what is listening, and what is switched on — followed by an index of configuration topics and how many flags each holds:

zsh
./bin/yolosearch-local --help            # essentials and the topic index
./bin/yolosearch-local --help cache      # one topic, every flag in it
./bin/yolosearch-local --help all        # every topic

Each topic screen shows the flag, its first sentence of documentation, its constraint, its default, and the catalog key to write in yolosearch.yml. The full paragraph for every setting is in yolosearch manual and the CLI reference. The screen wraps to the terminal's width between 60 and 120 columns and is colored when it is going to one — NO_COLOR, TERM=dumb, a pipe, or --color never each turn that off, and --color always forces it on through a pager.

Six flags are specific to this binary:

Flag Default Meaning
--dir — portable installation directory: create it, write yolosearch.yml, and run from it
--color auto color in help and diagnostics: auto, always, never
--admin-listen 127.0.0.1:8787 admin HTTP/Connect listen address
--admin-request-timeout 15s admin bridge request timeout
--admin-shutdown-timeout 30s admin HTTP graceful shutdown timeout
--version — print build identity

If port 8787 or 9500 is taken, the process reports the conflicting address and exits. Choose others with --admin-listen and --listen, using bracketed IPv6 literals where needed.

The Docker Swarm fleet

zsh
just swarm-up

That is the whole bootstrap. The first run downloads dependencies and container base images, builds the current checkout, initializes a single-manager Swarm if Docker is not already in Swarm mode, deploys the fleet, creates an articles namespace, indexes six sample documents, and verifies a distributed query.

When it returns:

zsh
./bin/yolosearch --server 127.0.0.1:9600 \
  search articles 'title:search' --fields title,url
Address Purpose
127.0.0.1:9500 writable all-in-one server: schema changes and ingestion
127.0.0.1:9600 distributed coordinator: normal searches and fleet stats
http://127.0.0.1:8787 admin UI, /livez, /readyz, /metrics
http://127.0.0.1:9001 MinIO object-store console

The MinIO login is yolosearchdev / yolosearch-dev-only-secret. These are public credentials for local development.

What is deployed

Service What it owns
minio authoritative S3-compatible object data and catalog generations
minio-init idempotent creation of the yolosearch-dev bucket
server schema service, ingest service, direct publisher, fallback search
aggregator soft worker capacity, cache-residency, and hotness summaries
worker-a, worker-b query execution and separate disposable local caches
coordinator query planning, dispatch, merging, and the distributed public API
compactor isolated size-tiered segment compaction and GC proposal work
admin the React UI and its same-origin ConnectRPC bridge

The single-process write path is on 9500 and the distributed read path is on 9600. All query workers read the same authoritative object catalog and do not own shards permanently. The aggregator improves placement but is not a correctness dependency — the coordinator can fall back to its explicit worker list. See architecture.

Application roots run read-only as UID/GID 65532. MinIO data uses a normal named volume; builder scratch and query caches use bounded tmpfs-backed named volumes, so they may be cold after a stack stop or a Docker restart.

Lifecycle

zsh
just swarm-status           # services, task placement, endpoints
just swarm-logs             # last 200 timestamped lines from every service
just swarm-logs coordinator # one allowlisted service
just swarm-down             # stop services; preserve MinIO object data
just swarm-up               # rebuild current source and resume or reseed

swarm-up is idempotent. Reapplying the same schema is a no-op and pushing the same six keys creates newer versions that normal searches collapse by key. Existing services are force-refreshed, so rebuilding a local image under the same development tag rolls the tasks.

To delete the local stack and its data:

zsh
just swarm-reset

swarm-reset removes the yolosearch-dev stack and only its six exact named volumes. It does not remove images, unrelated stacks, networks, or other volumes, and it never runs docker swarm leave. Run just swarm-up afterward for a fresh namespace and cold caches.

No lifecycle driver detaches a helper shell, log tail, or find process. Every readiness loop is finite and attached to the command. If startup fails, the stack is left in place for diagnosis instead of being erased.

Writing and reading across the split

zsh
./bin/yolosearch --server 127.0.0.1:9500 schema apply deploy/swarm/articles.proto
./bin/yolosearch --server 127.0.0.1:9500 push articles deploy/swarm/articles.jsonl

./bin/yolosearch --server 127.0.0.1:9600 \
  search articles 'search AND body:cache' --fields title,site,url
./bin/yolosearch --server 127.0.0.1:9600 \
  search articles '[word="cold"] []{0,3} [word="cache"]' --dialect cqp --fields title,url

The workers and coordinator follow the latest catalog generation every second, so a published write becomes searchable through port 9600 without restarting the fleet.

Corpus and fleet statistics:

zsh
./bin/yolosearch --server 127.0.0.1:9600 stats articles
./bin/yolosearch --server 127.0.0.1:9600 stats --node
./bin/yolosearch --server 127.0.0.1:9600 stats --fleet

Tuning the sandbox

The sandbox's non-secret settings are in deploy/swarm/stack.yml. Change cache budgets, builder memory and concurrency, seal thresholds, follower cadence, storage mode, or compaction thresholds there, then rerun just swarm-up. The stack disables configuration discovery so a host's files cannot leak into containers, and its flags override shared environment settings.

Startup-scoped settings require editing the stack and rolling the service; node-runtime settings can also use the ephemeral overlay:

zsh
./bin/yolosearch --server 127.0.0.1:9500 config get ingest.seal_age
./bin/yolosearch --server 127.0.0.1:9500 config set ingest.seal_age 5s

Without Docker: the split form

For the direct-server split, compile and run serve on its own:

zsh
just build
./bin/yolosearch serve --data-dir ./ys --listen 127.0.0.1:9500

Then, in another terminal, the same schema, push, and search commands. This form has no co-located admin UI. Prefer yolosearch-local unless you specifically need to exercise yolosearch serve alone. Stop it with Ctrl-C; nothing is detached. ./ys holds the filesystem object store, ingest scratch, cache, and derived config path.

Common contributor commands:

zsh
just test-unit       # deterministic Go tests
just ui-test         # React tests and TypeScript checking
just bench-short     # quick diagnostic microbenchmark
just ci              # infrastructure-free CI gate
just gate            # CI plus compatibility and integration proofs

Laptop benchmark numbers are diagnostic. Reportable resource and performance receipts run through the repository's remote benchmark commands.

Troubleshooting

failed to connect to the docker API. The CLI exists but the daemon is not running, or the active context is wrong:

zsh
docker context show
docker info

Swarm is locked, pending, or belongs to another cluster. swarm-up initializes only an inactive daemon and reuses only an active one. It will not repair or leave another Swarm:

zsh
docker info --format '{{.Swarm.LocalNodeState}}'
docker node ls

Neither swarm-down nor swarm-reset runs docker swarm leave.

A public port is in use. Ports 8787, 9001, 9500, and 9600 must be available to Swarm. Change the corresponding published value in deploy/swarm/stack.yml and use the new address manually — the lifecycle acceptance probes use the default ports, so changing them also means changing the constants in tools/dev/swarm.go.

A service keeps restarting.

zsh
just swarm-status
docker service ps --no-trunc yolosearch-dev_server
just swarm-logs server
just swarm-logs minio-init

Catalog-bound workers, the coordinator, and the compactor can restart briefly before MinIO and the sample namespace exist. A persistent restart loop usually means the bucket initializer failed, object credentials disagree, a cache mount is not writable, or the local image architecture is wrong.

Search says the index or generation is unavailable. Confirm seed publication on the write endpoint, then allow one follower poll:

zsh
./bin/yolosearch --server 127.0.0.1:9500 stats articles
./bin/yolosearch --server 127.0.0.1:9600 stats articles
just swarm-logs coordinator

A rebuilt service still runs old code. Use just swarm-up, not a raw docker stack deploy. The lifecycle command force-refreshes existing application services after rebuilding the fixed local tags; confirm task creation times with just swarm-status.

Images fail on another Swarm node. Local image tags exist only in one Docker daemon. Push immutable multi-platform images to a registry, authenticate every node, set YOLOSEARCH_SWARM_IMAGE and YOLOSEARCH_SWARM_ADMIN_IMAGE, and deploy with --with-registry-auth. Also replace the node-local volume strategy before treating the deployment as durable — the default placement constrains tasks to manager nodes and every named volume uses node-local storage, which is what makes this a developer topology rather than a highly available one.

Full clean start.

zsh
just swarm-reset
just swarm-up

Next