Skip to content

Releases: dualeai/seek

v0.14.0

Choose a tag to compare

@clemlesne clemlesne released this 07 Jul 11:23

seek gc gains a --sort flag and hardened table rendering. seek gc --dry-run --sort=size shows what is eating your disk.

Upgrade. No re-index, no cache-layout change, no removed flags. Automatic cache GC is unchanged — sorting is manual-only.

What changes for you

  • Sortable GC table. seek gc --sort=name|age|size: name (default, hash order), age (oldest .used first — the eviction order), size (largest first). Invalid --sort exits non-zero (was a silent no-op); shell completion lists the values.
  • Disk audit in one command. seek gc --dry-run --sort=size prints the plan by size, mutates nothing. Add --force to skip the daily throttle, --all to evict every corpus not in use.
  • Terabyte sizes. The SIZE column now renders TB caches (was GB-capped), still within its 6-char width.
  • Sane ages under clock skew. A future .used (NTP step, cp -p restore) renders 0s, not a negative raw-second blob.
  • Safe table cells. Control characters and tabs are stripped from displayed paths; left-truncation lands on rune boundaries, so multibyte paths never render orphan continuation bytes.
  • Documented maintenance. New README "Cache maintenance" section: auto-GC (14d TTL, once per day, off on NFS), the SEEK_GC_MAX_AGE / SEEK_GC_INTERVAL knobs, manual commands.

Dev/test

  • Dry-run and live GC paths unified so the tables cannot drift: shared gcTableStats, emitGCRow, predictGCAction/evictIfExpired, sweepCorpusOrphans, buildGCRows/sortGCRows. --sort values come from one gcSortValues list feeding both the parse error and completion.
  • Sorted views (age/size) measure all rows before rendering; name and the silent opportunistic path stream incrementally, keeping Ctrl-C prefix semantics.
  • releaseLock is now nil-safe, replacing hand-rolled unlock+close in runGC and evictCorpus.
  • Coverage: gc_sort_test.go (sort keys, tiebreaks, cancel), gc_format_test.go (TB, sanitize, rune boundaries, negative duration), a guard that the empty-cache live run still stamps .last-gc, and BenchmarkGC_RunGC_StreamingSorted_N100.
  • TODO(gc-size-cap): future SEEK_GC_MAX_TOTAL_SIZE (journalctl --vacuum-size style, LRU-by-age).

Full changelog: v0.13.2...v0.14.0

v0.13.2

Choose a tag to compare

@clemlesne clemlesne released this 28 Jun 16:34

Indexing no longer blocks searches, and scoped Git searches reuse the whole-repo index instead of building one per scope.

No flag changes. First search after upgrade may rebuild the index once; later searches reuse it.

What changed

  • Lock-free builds. Builds run in a temp dir, validate HEAD, then publish atomically under a brief lock (.build.lock build, .lock publish). Readers hold a shared lock across glob+open — never blocked by a build, never see a torn shard set. A .swapping marker recovers a crash mid-swap.
  • Scopes share one index. A scoped search (seek 'x' ./cmd/seek) reuses the combined whole-repo index and filters at search time — no per-scope layers, less rebuild churn.
  • Over-cap fallback. If the whole repo exceeds index caps, a scoped search builds a per-scope index instead, so large siblings can't cap small scopes. A .git_cap_exceeded marker (HEAD + cap limits) caches the verdict; HEAD or cap-limit change re-evaluates.
  • Faster file scopes. ≤64 file operands compile to trigram-assisted anchored regexps instead of an O(docs) scan; above 64 falls back to set lookup.
  • GC hardening. Sweeps orphaned temp build dirs, skips eviction during a build, excludes temp dirs from reported size.

Dev/test

  • New swap.go (temp build, atomic publish, crash recovery); reworked lock.go (acquireBuildLock/acquirePublishLock/acquireReadLock).
  • Scoped path collapsed onto the shared index in corpus.go/git_scope.go/main.go; per-scope index kept only as the over-cap fallback. Removed gitRepoStateInScope and the committed/dirty/shared-committed layers.
  • Coverage: build/search concurrency, mid-swap recovery, orphan-dir GC, shared-index reuse, over-cap fallback, file-operand regexp vs set.

v0.13.1

Choose a tag to compare

@clemlesne clemlesne released this 27 Jun 21:52

Scoped Git searches now share one whole-repo committed index instead of building a separate one per scope. Clean trees skip the dirty layer.

No flag changes. First scoped search after upgrade may rebuild the index once; later scopes reuse it.

What changed

  • Scopes share one committed index. A scoped search (seek 'needle' ./cmd/seek) reuses the repo's whole-repo index and filters by scope at search time — less rebuild churn across subtrees of the same repo.
  • Clean trees do no dirty work. A clean in-scope tree skips the dirty layer — no dir, no state writes.
  • Over-cap repos fall back. When the whole repo exceeds the index caps, search uses the per-scope committed layer instead. A .git_cap_exceeded marker (keyed by HEAD + cap limits) caches that verdict so the budget isn't re-scanned every search; a HEAD or cap change re-evaluates.

Dev/test

  • Shared committed_shared layer on the plan; per-scope committed* kept as over-cap fallback. resolveCommittedLayer picks the layer; prepareAndSearchCorpusOnce repoints plan.committed* and zeros plan.dirty* on a clean tree.
  • Cap marker: read/write/removeGitCapMarker + gitCapMarkerValue (treeish + cap limits), best-effort (write failure only warns).
  • Coverage: shared reuse across scopes, over-cap fallback, clean-tree elision, cap-marker invalidation, state validation skipping the elided dirty layer.

v0.13.0

Choose a tag to compare

@clemlesne clemlesne released this 27 Jun 19:22

File operands inside a Git worktree now route through the repo's scoped Git index instead of a separate per-file corpus. seek 'needle' ./cmd/seek/searcher.go keeps Git ignore and local-change behavior, reuses the repo's index, and reads back as the file you selected.

Upgrade. No flag changes. Exact files inside a Git repo now route to the repo's Git corpus (scoped to your selection), not a standalone single-file corpus — files and directories take the same routing decision. .gitignore-excluded files and folders, and anything outside a worktree, fall back to a plain file/folder corpus. The Git corpus identity is unchanged; a file newly routed to it may build a scoped committed/dirty layer on first use, and an ignored fallback may build a folder/file corpus.

What changes for you

  • In-repo files reuse the Git index. A tracked or untracked-visible file routes to its worktree's Git corpus, scoped to the selection — no per-file corpus, consistent ignore + dirty-layer behavior.
  • Ignored folders are searchable. An exact .gitignore-excluded file or folder now searches via a fallback corpus. Previously only ignored files.
  • Selected files read as what you picked. Single-corpus mode shows the basename; multi-corpus mode keeps the absolute, openable path.
  • Files follow nested-Git ownership. A file routes through its parent dir: into the inner repo when nested, collapsed to one plan when multiple files share a repo or a selected directory already covers them.
  • Predictable at boundaries. git check-ignore runs index-aware (tracked-but-ignored stays tracked); submodule-interior paths fall back per path; case/normalization-insensitive filesystems are corrected to the real on-disk name so a mistyped-case operand isn't silently missed.
  • Pathspec-safe operands. Paths are fed as literals (./-prefixed), so a :-leading name isn't misparsed as pathspec magic; a symlink to a gitignored in-worktree file falls back instead of vanishing.

Dev/test

  • 3-phase operand router (resolve → classify visibility → route), one git check-ignore batch per repo root, core.fsmonitor=false to skip the fsmonitor IPC tax.
  • Extracted git_scope.go, git_visibility.go, corpus_id.go.
  • realCaseWithin corrects case/NFC-NFD names by os.SameFile identity before they drive the byte-exact Git scope.
  • Regression coverage: classify gap (tracked/untracked/ignored/nested/collapse), visibility matrix (submodule-interior, leading-colon, all-prefiltered), case-mismatch, symlink-to-ignored, operand-order independence, frozen golden guard on the dirty-scope cache key. Refreshed README and CLI help.

Full changelog: v0.12.0...v0.13.0

v0.12.0

Choose a tag to compare

@clemlesne clemlesne released this 21 Jun 20:23

Git directory operands now use scoped Git indexes. seek 'needle' ./platform keeps Git ignore and local-change behavior, but no longer lets unrelated repo files drive indexing, caps, or results.

Upgrade. No flag changes. Compared with v0.11.1, directories inside a Git repo now use Git rules instead of the literal folder pipeline. Exact files remain literal, folders outside Git remain folder corpora, and root/file/outside-folder cache identities are unchanged. Git directory operands may build new scoped committed/dirty layers on first use.

What changes for you

  • Scoped Git directories. Selected Git directories get committed and dirty layers for that subtree, so large ignored artifacts, dirty siblings, or tracked siblings outside the path no longer trip Git file/byte caps.
  • Nested Git handled once. No-path searches, Git-root operands, and selected Git directories discover visible nested repos, submodules, gitlinks, and linked worktrees, while parents exclude them to avoid duplicate results and ignored-content leaks.
  • Ignore and explicit operands stay predictable. Ignored directories inside the selected repo stay ignored; exact ignored files and explicit nested Git roots still search what you asked for.
  • Fewer Git edge-case misses. Seek forces --untracked-files=all, treats pathspec metacharacters literally, strips pathspec environment overrides, and can reuse scoped committed layers after commits outside the selected path.

Dev/test

  • Added scoped committed/dirty Git layers with separate caches, locks, empty-layer markers, state validation, and git ls-tree / git cat-file committed indexing.
  • Recursed nested-Git discovery through visible repos, submodules, gitlinks, and linked worktrees; removed the old fixed discovery cap.
  • Hardened Git boundary detection for symlink escapes, common-dir escapes, and linked-worktree backrefs.
  • Added regression coverage for scoped budgets, ignored nested Git, pathspec handling, nested untracked files, layer drift, exact-file ownership, and refreshed README/benchmark docs.

Full changelog: v0.11.1...v0.12.0

v0.11.1

Choose a tag to compare

@clemlesne clemlesne released this 20 Jun 13:29

Small follow-up to v0.11.0. Path operands now do what they say: exact files search only that file, plain folders search that folder, nested Git roots stay separate, and local Git repos without remotes index reliably.

Upgrade. No flag changes and no cache-layout change. Existing Git indexes stay valid; explicit file and non-Git-root directory operands may build a new literal corpus on first use.

What changes for you

  • File operands are exact. seek 'TODO' ./src/foo.go searches only that file, even inside a Git worktree. Result context now uses the file's parent directory.
  • Plain folder operands stay plain. Passing a non-Git-root directory searches that folder literally, including ignored files when passed directly. For Git-aware subtree search, search the worktree root and filter inside the query, for example seek 'needle file:src'.
  • Git roots keep Git behavior. Worktree-root operands keep Git ignore handling and [uncommitted] labels.
  • Nested Git roots stay separate. Discovered nested Git roots get their own Git corpus. If discovery is disabled or capped, seek falls back to parent-folder traversal.
  • Mixed parent/child operands deduplicate. A Git parent plus explicit child operand no longer returns child results twice.
  • Symlink operands are consistent. Direct symlink operands resolve to their target; broken symlinks error; symlinks found during folder walks remain skipped.
  • No-remote Git repos index reliably. Local Git repos without a remote use a stable opaque fallback repository name, including repos named uncommitted.
  • Cold-cache failures report the real cause. Git indexing failures now return the indexer error instead of no index shards.

Dev/test

  • Shared Go setup action with Ubuntu 26.04 linker workaround.
  • CodSpeed action updated to v4.17.6.
  • Go directive updated to 1.26.4.
  • Delta pressure test now uses channel synchronization.
  • Regression coverage for exact files, ignored directories, nested Git roots, no-remote repos, symlink operands, cold-cache errors, and duplicate suppression.

Full changelog: v0.11.0...v0.11.1

v0.11.0

Choose a tag to compare

@clemlesne clemlesne released this 17 Jun 15:15

Output rendering pass: long lines window around the match, color on a terminal and plain when piped, control bytes and Trojan-Source overrides stripped, truncation now states what it hid.

Upgrade. No re-index, no flag changes, no cache-layout change. Text output changed: denser gutter and a truncation notice — re-check anything that parses seek's output.

What changes for you

  • Color, auto-gated. File cyan, line numbers dim, matches bold red — terminal only. Piped output (agents, CI, | cat) stays plain, zero config. Precedence: NO_COLOR (non-empty) off → CLICOLOR_FORCE on (even piped) → else on when TERM set/≠dumb and stdout is a TTY.
  • Long lines windowed. Match line over 1024 B → cropped to ±512 B around its matches (never bisects a match), …+N bytes markers for dropped head/tail. Context lines over the cap tail-trimmed. Cuts on rune boundaries. No new flags.
  • Control-char & Trojan-Source sanitisation. C0/C1/DEL (tab kept) and the bidi / line-paragraph-separator code points behind CVE-2021-42574 stripped from output. Zero-width joiners kept (emoji, scripts).
  • Truncation announced. File cap → … N more files (showing X of Y); per-file match cap → … N more matches in this file. Was silent.
  • Denser gutter. Dropped line-number padding and two-space indent — now <lineNo> <content>. Fewer tokens per line.
  • Directly-openable multi-corpus headers. Header carries the absolute path (displayRoot + file name); tag collapses to bare [git] / [folder], was [git: /abs/root].

Breaking

  • Text output format changed — denser gutter, announced truncation, reshaped multi-corpus headers. Re-check output-scraping tooling. CLI flags, subcommands, and cache layout unchanged.

Dev/test

  • Deps: golang.org/x/term added; golang.org/x/sys → v0.46.0; cobra/pflag now direct.
  • Tests: color_gate_test.go, formatter_color_test.go (~490 lines), plus cli_root_test.go / main_test.go additions.
  • cicd/bench-field.sh: new Tok(content) / Tok(sym) columns — piped-output token count (tiktoken o200k_base via uv).
  • CI: ubuntu-26.04 runners (matrix expanded to 24.04/26.04 + macOS), coverage upload, govulncheck SARIF scan, dependency submission; test job timeout 25 min, make test-unit coverage profile under 18m.

Full changelog: v0.10.0...v0.11.0

v0.10.0

Choose a tag to compare

@clemlesne clemlesne released this 16 Jun 15:52

Search a parent dir holding many repos: each indexes as its own corpus. Multi-GiB folder corpora no longer wedge. CLI errors actually tell you what went wrong. seek gc shows you what it evicted.

Upgrade. Folder corpora indexed under v0.9.x re-index once on first search (cache layout v2 → v3). Git corpora untouched. CLI flags and on-disk paths unchanged.

What changes for you

  • Multi-repo parent folders. seek 'foo' ~/dev now discovers nested git repos inside the parent and indexes each one separately — gitignore honored per repo, results disambiguated by corpus. Capped at 64 nested repos per query (Warn on cap).
  • No more wedged folder searches. The deadlock that hung indefinitely on multi-GiB folder corpora is fixed. Per-corpus content cap raised 5 GiB → 10 GiB; peak RSS still bounded.
  • Multi-corpus searches run in parallel. Up to 4 corpora index concurrently. Same physical repo reached via symlinks or multiple operands indexes once.
  • CLI errors that help.
    • seek gccunknown subcommand "gcc" (did you mean "gc"?)
    • seek --verbos foounknown flag: --verbos (did you mean --verbose?)
    • seek -file:test (single-dash Zoekt query) still passes through unchanged.
  • seek gc --force is no longer silent. Live banner + per-corpus table + summary, same shape as --dry-run. ACTION column: evicted / kept / locked / gone / trashed: <err> / failed: <err>.
  • Wedged-indexer signal. A stuck indexer used to silently degrade every search to stale shards forever. Now you get a Warn naming the lock so you can find and kill it.
  • seek gc subcommand. Promoted to a real subcommand with --force, --dry-run, --all. Alias: garbage-collect.

Performance

Microbenchmarks (Apple M1 Max, benchstat over 5 samples):

Benchmark Δ ns/op
FolderCorpus_ColdIndex −41.0%
FolderCorpus_WarmSearch −20.1%
SmallRepo executeParsedSearchScoped −52.7%
SmallRepo planCurrentGitCorpus −21.3%
SmallRepo ensureUntrackedCache −31.6%
SmallRepo indexUncommitted_1file −12.1%
SmallRepo postVerify_restat −21.5%
PlannedGitCorpus_DirtyReindex −9.2%
geomean (16 benches) −18.4%

Field benchmarks — git + non-git, PR-scale dirty re-index at 1% and 10%:

Kind Workload Files Cold index Warm search Dirty 1% Dirty 10%
git spf13/cobra 66 470ms 80ms 120ms 130ms
git prometheus/prometheus 1,635 1.8s 90ms 150ms 380ms
git kubernetes/kubernetes 30,507 11.0s 180ms 700ms 4.3s
folder synthetic-10k 10,000 6.0s 100ms 250ms 750ms
folder synthetic-100k 100,000 33.5s 250ms 1.3s 7.8s

Reproduce: ./cicd/bench-field.sh [--no-linux] [--keep].

Breaking

  • Cache layout v2 → v3 — folder corpora re-index once on first search.
  • Internal Go API: streamFiles requires context.Context; acquireSearchLock requires indexDir string; fileContent.weight Release contract (see docstring); gitPaths.ExcludePath removed.

Dev/test

  • 19 new test files: cancellation with goleak, pressure (4 MiB swap), property/fuzz, deadlock guard, discovery fixtures, git-boundary detection, Cobra CLI surface, soak (gated //go:build soak + SEEK_SOAK=1).
  • cicd/bench-field.sh — self-contained field-benchmark harness, sandboxed via SEEK_CACHE_DIR.
  • make test-bench-compare wraps benchstat.
  • New deps: cobra, pflag, goleak (test-only).

Full changelog: v0.9.0...v0.10.0

v0.9.0

Choose a tag to compare

@clemlesne clemlesne released this 14 Jun 22:03

Per-file indexing cap 10 MiB → 100 MiB. New byte-weighted read semaphore caps reader-buffer RAM at ~600 MiB. Drop-in upgrade from v0.8.0 — no CLI or cache-layout changes.

Highlights

  • maxIndexedDocumentBytes 10 MiB → 100 MiB. Vendored libraries, minified bundles, generated JSON/CSV, lockfiles, large protobufs now indexable. Overflow files skipped with slog.Warn.
  • Bounded in-flight memory. A new byte-weighted semaphore caps total bytes resident in reader buffers across all workers at ~600 MiB, so the larger per-file cap does not balloon RSS under bursts of large files.
  • Auto-scaling ceiling. maxInFlightBytes = inFlightHeadroomFiles (6) × maxIndexedDocumentBytes. Bumping the per-file cap scales the in-flight budget for free. Tune inFlightHeadroomFiles for tighter or looser ceilings.

Development

  • New cmd/seek/read_semaphore.go plus lifecycle tests covering leak-free drain on builder errors, panic safety, and ctx cancellation mid-read.
  • Reader weight is released by the consumer after builder.Finish() returns, because Zoekt's shard writers retain Content references until then.
  • New dep: golang.org/x/sync v0.20.0.

Notes

  • Synchronous folder-delta reads stay unbounded by the semaphore (documented as future work).

Full changelog: v0.8.0...v0.9.0

v0.8.0

Choose a tag to compare

@clemlesne clemlesne released this 14 Jun 20:12

Two big wins for steady-state cost. Zoekt delta indexing replaces full shard rebuilds, and a new cache GC keeps ~/Library/Caches/seek (or ${XDG_CACHE_HOME}/seek) bounded over time.

seek gc                 # opportunistic eviction (default)
seek gc --dry-run       # report only
seek gc --force         # ignore throttle gate
seek gc --full          # evict everything

Highlights

  • Zoekt IsDelta enabled for committed and uncommitted shards. Only changed blobs are touched per cycle; renames and removals land as tombstones in the .meta sidecar.
  • Committed indexer flips to delta with a 64-shard fallback threshold.
  • Uncommitted indexer mirrors the folder pattern: per-cycle manifest (path + size + mtime + inode) tagged with state hash, sorted-merge diff against working tree, tombstones on rename/delete.
  • New seek gc subcommand with --force, --dry-run, --full.
  • Opportunistic GC runs after each successful search, gated by throttle and NFS detection (skips network filesystems).
  • Suffix-only shard cleanup preserves Zoekt's contiguous shard numbering (fixes a latent folder-corpus bug too).
  • stateVersion bumps v5 → v6 to force a one-time clean re-index for upgraded binaries.

Performance

Measured on a real kubernetes/kubernetes clone (~17k Go files, 1.6 GB), M1 Max, n=3 medians, via SEEK_BENCH_REPO-gated benches:

Bench Before After Delta
LargeRepo_CommittedAdvance wall 8.97 s 2.43 s 3.7× faster
LargeRepo_CommittedAdvance RAM ~3.2 GB ~250 MB ~13× less
LargeRepo_CommittedAdvance allocs ~19 M ~320 k ~60× fewer
LargeRepo_UncommittedRealistic 97.3 ms 104.1 ms +7% (noise)
Cold index (warm page cache) 10.04 s 9.21 s −8%

Small-fixture benches (1-file, M1 Max, n=5 medians):

  • Win: PlannedGitCorpus_ColdIndex 312 → 272 ms (−13%), PlannedGitCorpus_WarmSearch 28.0 → 25.6 ms (−9%).
  • Regression: PlannedGitCorpus_DirtyReindex 53.6 → 62.0 ms (+16%), indexCommitted_incremental 306 → 364 µs (+19%), indexUncommitted_1file 26.3 → 27.8 ms (+6%), FolderCorpus_DirtyReindex_1File 30.1 → 39.6 ms (+32%).
  • New steady-state: GitCommitted_1CommitAhead 96 ms, GitUncommitted_RapidEdits_N16 912 ms (~57 ms/cycle), GitBranch_Switch_Unrelated 79 ms (80% churn), SearchTombstoneCost 859 µs over 16 delta shards.

Uncommitted is neutral on real repos: rebuilding one dirty file is dominated by ctags + Zoekt builder init (~30 ms), so manifest + per-shard probe overhead cancels the savings. Delta path still helps by keeping each shard small and bounding tombstone growth.

Development

  • New delta.go shared helpers, uncommitted_manifest.go for state hashing.
  • 18 correctness tests covering modify/delete/rename tombstoning, HEAD rewind, rebase, branch switch, threshold fallback, manifest corruption, dirty-to-committed transitions, drift mid-build, concurrent search.
  • 5 delta-focused benches plus LargeRepo_CommittedAdvance and LargeRepo_UncommittedRealistic (gated by SEEK_BENCH_REPO).
  • gc.go, gc_cmd.go, gc_test.go (1217 lines test coverage), platform-specific NFS detection (nfs_darwin.go, nfs_linux.go, nfs_other.go).

Notes

  • An earlier draft claimed "order-of-magnitude" committed-advance gain from extrapolation; actual measured speedup on kubernetes is 3.7×. A 20× number from prior runs came from a reflog oscillation bug (HEAD@{1} alternating between two distant positions). LargeRepo_CommittedAdvance now uses git rev-list --reverse HEAD~10..HEAD and asserts shard count grows under IsDelta so silent fallback cannot pass.

Full changelog: v0.7.1...v0.8.0