Cross-repo search — design
Class:
design· Function:search(usesregistryfor each backend'slist()/fetch()/archive_files(), and the in-processHolgerfacade for the CLI adapter). The whole-system view isdesign.md; the parity gap this closes isfeature-complete-gaps.md§8 (search / metadata / REST — previously all ❌, the cheapest high-value operability win) andartifactory-parity-design.md§H.
What this is
The cross-repo search answers the Artifactory/Nexus "search" table stakes: find an artifact across every configured repository by name, coordinate, checksum, or path. It ships as server/lib/src/search.rs — a pure function over the repo list it is handed:
search_repos(repos: &[(String, Arc<dyn RepositoryBackendTrait>)], query: &SearchQuery)
-> SearchResults
No transport, no scheduler, no mutation. The only I/O is the backend calls it is given, so it is unit-testable with in-memory mocks and reused verbatim by all three surfaces (gRPC RPC, HTTP door, CLI verb).
The matching axes (AND-composed)
| Axis | Field | Match | Backed by |
| Name | name |
case-insensitive substring on artifact name | list() |
| Namespace / GAVC group | namespace |
case-insensitive substring (a namespace-less artifact never matches) | list() |
| Version | version |
exact (versions are exact tokens) | list() |
| Checksum | checksum |
lowercase-hex sha256 of the content, case-insensitive | fetch() + sha256 |
| Path | path |
case-insensitive substring on a raw archive path → PathHits |
archive_files() |
name + namespace + version are the GAVC (GroupId·ArtifactId·Version) coordinate axes mapped onto holger's ArtifactId { namespace, name, version }. All supplied criteria are AND-ed. A bare query lists everything (bounded by limit); a path-only query yields only path hits.
The chosen design: one pure engine over list(), three thin surfaces
The gap doc names the shape exactly: "backends expose list_files(); a thin cross-repo search verb (gRPC + a read-only HTTP endpoint) is the cheapest high-value operability win." So:
- Engine (
search::search_repos) — the pure matcher. Iterates the repo list, applies the axis predicates, collectsArtifactHit/PathHit. Best-effort: a backend that errors is logged and skipped, never sinking the whole search. - gRPC —
SearchService.Search(added toproto/holger.proto, served through the sharednornir-serviceserve loop + auth interceptor alongside the other four services). Reads are open (anonymous), likefetch/list; audited as aList. - HTTP — a read-only door
GET /-/search?name=…&namespace=…&version=…&checksum=…&path=…&repo=…&limit=…, reserved under/-/so it never collides with a repository name. Returns the engine'sSearchResultsas JSON. This is the "gRPC + read-only HTTP" the gap doc frames. - CLI —
holger-server search --config <ron> [--name …] [--namespace …] [--version …] [--checksum …] [--path …] [--repo … --repo …] [--limit N], drivingHolger::search_allin-process.
Why checksum is the one expensive axis
ArtifactEntry (the list() row) carries no digest, so a content-checksum search must read the bytes and hash them. The engine only does this for candidates that already pass the cheap axes, and only when a checksum is supplied — so the common name/coordinate search never fetches a body. Searching by digest is genuinely useful (dedup / provenance lookup: "which coordinate holds these bytes?"), and it is honest — it verifies the real content, not a stored label.
What is deliberately NOT here (honest scope)
- Custom properties (§8) — holger artifacts carry no user-defined properties today, so there is no property axis to search. Added as a follow-up when artifact properties land.
- A ranked / scored index — this is a linear scan over
list(), fine for the archive sizes holger serves; a pushed-down index is the scale-out follow-up.
Testing (Law #4)
- Engine (
search.rs) — 9 unit tests over two mock repos: name find + exclude non-matches (RED-when-broken), case-insensitive substring, GAVC namespace/version narrowing, checksum match and exclusion of non-matching content (RED-when-broken), repo scoping, path-only hits, empty-query list-all, and limit/truncation. - gRPC (
grpc.rs) —search_rpc_finds_hits_excludes_non_matches_and_audits: drivesSearchService::searchover two backends, asserts the hits, assertstokiois EXCLUDED from aserdesearch (RED-when-broken), asserts each hit carries its repo, and asserts theListaudit row with the hit count. - HTTP (
exposed/http.rs) — theparse_search_queryquery-string parser: all axes, percent-decode,+→space, empty/unknown-key handling. - Every surface emits a
functional_statuscell gated#[cfg(feature="testmatrix")](holger-grpc/search,holger-http/search,holger-cli/search).