feat: add search for the runtime's /v1/search endpoint - #84
Conversation
Adds Client::search, exposing vector, keyword, and hybrid search from Rust. Previously only spice.js could reach /v1/search; users of every other SDK had to hand-roll the HTTP call. SearchRequest follows the DatasetRefreshRequest builder pattern, and SearchError is a typed enum alongside DatasetError and QueryError. Response types are modelled on the runtime's actual wire shape: matches holds a list per column because one column can contribute several chunks to a match, and data / primary_key / metadata are omitted by the runtime when empty. Errors carry the runtime's plain-text explanation alongside the status code, and requests the runtime would reject with a 400 are validated before the request so the error names the field to fix.
|
No checks have reported on this PR because its workflow run is parked awaiting approval, not because CI is failing: run 30697688240 for Sibling PR #81 is parked on the same gate (run 30679500818), so approving both together would give the whole |
|
@copilot review |
Resolves the src/lib.rs module-list conflict: trunk added `mod redirect;` (the same-origin credential fix) while this branch added `pub mod search;`. Both belong; kept in alphabetical order alongside the rest of the list. The new search surface reaches the network through the shared http_client, which trunk now builds via `redirect::credentialed_client_builder()`, so the credential policy covers it with no further wiring.
There was a problem hiding this comment.
Pull request overview
Adds first-class Rust SDK support for the runtime’s /v1/search HTTP endpoint by introducing typed request/response models, error handling, and a new Client::search API that routes through the existing QueryHttpClient (so it inherits auth + configured http_url).
Changes:
- Introduces
SearchRequest(builder-style) plusSearchResponse/SearchMatchdeserialization types andSearchError(SNAFU) in a newsearchmodule. - Adds
QueryHttpClient::searchand exposesSpiceClient::searchas the public entry point. - Updates public exports and README/docs; adds unit + wiremock-backed tests for the new endpoint.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| src/search.rs | New search request/response types, validation, errors, and unit tests for (de)serialization behavior. |
| src/query.rs | Adds QueryHttpClient::search implementation that POSTs to /v1/search and parses results/errors. |
| src/lib.rs | Exposes the new search module and re-exports search types in the public API. |
| src/client.rs | Adds SpiceClient::search method, docs, and wiremock-based tests covering request/response and error surfacing. |
| README.md | Documents search usage with an end-to-end example and explains match payload shape. |
A non-2xx /v1/search response had its body read with unwrap_or_default(), so a failure to read the body collapsed to an empty string and SearchFailed rendered as though the runtime had explained nothing — losing the transport error. Keep the status code, which is already known, and put the read failure in the body instead. The six pre-existing call sites in QueryHttpClient share the pattern and are tracked separately in spiceai#87.
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 5 out of 5 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/client.rs:619
- The
SpiceClient::searchdocs list possible errors but omitSearchError::ParseError, which can be returned if the runtime responds with invalid/unexpected JSON. Documenting this variant helps callers understand the full error surface.
/// - [`SearchError::InvalidRequest`] if the request has empty text or a zero limit
/// - [`SearchError::HttpError`] if the HTTP endpoint is not configured or unreachable
/// - [`SearchError::SearchFailed`] if the runtime rejects the search
src/client.rs:1558
- In
test_search_validates_before_sending,unwrap_or_default()can mask failures to fetch Wiremock’s received requests, making the test pass even if the assertion can’t actually be validated. This should fail loudly so the test remains trustworthy.
server
.received_requests()
.await
.unwrap_or_default()
.is_empty()
|
Update to my 2026-08-02 comment: the workflow run is no longer parked at Every unit test passes on every platform. The only failure is the Mechanism — the fork-secrets boundary. Evidence the diff is not implicated: trunk's own build for the same commit range is green (30971730156) running these same tests with the secret present; This is the same wall as spiceai/spicepy#177 and spiceai/spice.js#311 — the identical failure class in three SDK repos. Nothing on this branch can clear it, so this PR is gated purely on review; the red legs carry no information about the change. The durable fix would be to make |
|
Flagging an overlap I should have caught when this PR was opened: #82 already implements this feature, and it was opened first. @lukekim's #82 ("feat: add search for vector, keyword, and hybrid search", opened 2026-07-27) and this PR (opened 2026-08-01) both add
Neither PR referenced the other, and this PR's description asserts Rust users had no way to reach Where the two differ:
Recommendation, but the call is a maintainer's, not mine: one of the two should be closed rather than both carried. This PR is the one that currently merges cleanly and has been through review; #82 carries the CI change and has precedence by date. I am happy to close this PR in favour of #82 if that is preferred — say the word and I will, and I will port anything from here that #82 lacks. For reference, the same duplication exists in spicepy#174 vs #177, and in gospice it has already resolved — my #79 merged and left @lukekim's #77 conflicting and redundant. Separately, on this PR's red |
|
Filed #88 to own the harness fix, so the permanent red on fork PRs has somewhere to be fixed rather than being re-diagnosed on each PR. Nothing about this branch changes — the ten |
# Conflicts: # README.md # src/client.rs
|
Unblocked the One thing worth a decision before this merges, since it is cheap now and a breaking change once the API ships — raised by the pre-merge adversarial review and filed as #91:
// The runtime rejects an empty dataset list with a 400, so an empty
// vector must be omitted rather than sent.so a runtime rejection that fails closed becomes client behaviour that fails open. A caller computing a scope — a tenant allow-list, a permission filter — that legitimately comes back empty gets an unrestricted search across everything the runtime credential can reach. The root of it is that I have not made that change here — it alters the public API on a PR you have already approved, so it is your call rather than mine. Happy to push it to this PR if you would like it in before merge, or leave it to #91 as a follow-up. #91 also carries three lower-severity findings on the same surface (defaults on required response fields hide malformed 200s, |
What
Adds
Client::search(SearchRequest), wrapping the runtime's/v1/searchendpoint — vector similarity search, with optional keywords for a hybrid lexical + vector ranking.Why
/v1/searchworks on a defaultspice run, but of the six client SDKs only spice.js could reach it. Rust users had to hand-roll the HTTP call, including the response shape.Part of aligning capabilities across the SDKs against the runtime's own API surface.
Shape follows what the crate already does:
SearchRequestuses theDatasetRefreshRequestbuilder pattern (mut self->Self,#[must_use]),SearchErroris a typedsnafuenum alongsideDatasetErrorandQueryError, and the call goes through the existingQueryHttpClientso it picks up auth and the configuredhttp_url.Two details the response types encode, taken from the runtime's wire format rather than the OpenAPI example (which is out of date on the first point):
matchesholds a list of values per column — one column can contribute several chunks to a single match.data,primary_key, andmetadataare omitted by the runtime when empty, so each defaults rather than being required.Errors carry the runtime's plain-text explanation ("No data sources provided") next to the status code, which a status alone loses. Requests the runtime would reject with a 400 — empty text, a zero limit — are validated before sending so the error names the field to fix. An empty
datasetsvector is omitted rather than sent, since the runtime 400s on an empty list.Verification
cargo buildcargo test --lib— 199 passed, 0 failed (12 new: 8 unit insearch.rs, 4 wiremock-backed inclient.rs)cargo test --doc— 28 passed, including the new README examplecargo fmt --all --check— cleancargo clippy --all-features --all-targets— no new warnings. The two that remain are pre-existing:QueryHttpClient::newdead_code in the lib, and 5clone/from_refwarnings intests/client_test.rs.Review gate
Attests the trunk-merge conflict resolution in
e0d621d(composition only — no behaviour change), not the original feature commits.codex— scoped toorigin/trunk...HEAD— verdict needs-attention, 4 findings, all against the pre-existingsearchsurface rather than the merge resolution. Triaged and filed as search: an explicitly empty dataset scope fails open to every dataset, plus three robustness gaps in the new /v1/search surface #91 (an explicitly empty dataset scope fails open; defaults hide malformed responses;cache_controlnot forwarded;with_keywordsdocumented as hybrid ranking but prefilters). Finding 1 is cheap now and breaking once released — see the PR comment.src/client.rsfunction set is exactly the union of both sides (79 = 77 ∪ 74, no missing, no extra); diff vstrunkis 570 insertions and 0 deletions, so nothing from feat: add runtime_status and is_ready #81 was dropped.cargo fmt --all --check,cargo clippy --all-features,cargo test --lib(223 passed),cargo test --doc(34 passed, including both recomposed README fences).