// STRYKE-SEARCH — ENGINEERING REPORT

Transport

The client talks to the cluster over its REST API using ureq — a synchronous, pure-Rust HTTP client backed by rustls. There is no async runtime and no OpenSSL, which keeps the dependency tree vendorable and the build reproducible. Elasticsearch and OpenSearch expose the same _search, _bulk, _doc, _cat, and _cluster endpoints, so a single client covers both without version-locked SDK types.

Connection cache

A ureq::Agent owns an HTTP keep-alive connection pool. One Agent is cached per (base_url, auth) tuple in a OnceCell<Mutex<HashMap>> for the life of the stryke process, so repeated Search::* calls reuse pooled sockets instead of reconnecting. Auth precedence is api_key (sent as ApiKey <key>) over username + password (HTTP Basic), resolved once when the key is built.

FFI contract

Each #[no_mangle] extern "C" fn search__* is a JSON-string-in / JSON-string-out wrapper. stryke's FFI bridge resolves the symbols at first use Search, passes a JSON-encoded args dict on each call, and copies the returned JSON into a stryke string; stryke_free_cstring frees the allocation. Handlers run inside catch_unwind, so a panic becomes an { "error": … } result rather than unwinding across the FFI boundary.

Request building

Every network wrapper builds its request hash through one helper, Search::_req. It copies the connection keys (url, host, port, username, password, api_key, tls, params) from the caller's %opts, filtering out undefined values so the cdylib applies its own defaults (127.0.0.1:9200, scheme from tls). When neither url nor host is supplied it falls back to the $SEARCH_URL environment variable. Per-operation fields (index, id, body, query, ops, …) are merged in by each wrapper before the hash is JSON-encoded and handed to the FFI symbol.

Error dispatch

Every wrapper routes its FFI return through Search::_decode, which parses the JSON and, when the result is a hash carrying an error key, raises die "Search::<fn>: <reason>". Callers see a stryke exception instead of a silent error hash. Boolean-style wrappers (ping, index_exists, doc_exists, index_template_exists) unwrap the value field; list wrappers (index_list, cat) dereference the returned array.

Pure builders

The query-DSL builders (match, term, range, bool, multi_match, geo_bounding_box, function_score, more_like_this, …), the aggregation builders (agg_terms, agg_date_histogram, agg_top_hits, agg_global, …), the body and field composers (query_body, sort, highlight, bulk_ndjson, escape), and the URL helpers (build_url, parse_url, redact_url, valid_index_name) take no connection. They return bare clauses that nest directly inside other builders. They are unit-tested in the crate and pinned by the surface test, so they validate in CI with no cluster present.

Test + build matrix

The crate is cdylib-only, so the integration test asserts only that the crate type-checks and links. The real coverage is in src/lib.rs's #[cfg(test)] module (NDJSON build, Lucene escaping, URL parse / redact, basic-auth header, percent-encoding), the t/test_stryke_search_surface.stk surface pin (every Search::* wrapper resolves and every builder produces the right shape, no cluster), and t/test_search.stk (end-to-end CRUD + search against a live cluster, short-circuited when none answers). CI runs cargo check, fmt, clippy -D warnings, cargo test on Ubuntu and macOS, cargo doc -D warnings, release builds for the four host triples, and the docs / README gate suite.