// STRYKE-SEARCH — ELASTICSEARCH / OPENSEARCH CLIENT FOR STRYKE

What it is

stryke-search is an Elasticsearch / OpenSearch client for stryke. It covers index administration, document CRUD, bulk indexing, the full query DSL, aggregations, scroll and point-in-time pagination, aliases, templates, ingest pipelines, snapshots, stored scripts, tasks, and cluster / node health against any Elasticsearch 7+/8+ or OpenSearch 1+/2+ cluster. Both engines speak the same REST API, so one client covers both without version-locked SDK types.

It ships as a Rust cdylib that stryke dlopens in-process on the first use Search and registers every search__* export. A ureq HTTP keep-alive connection pool (a ureq::Agent) is cached per (base_url, auth) tuple for the life of the process, so repeated calls reuse pooled sockets instead of reconnecting. There is no tokio and no OpenSSL — the transport is pure-Rust over rustls, which keeps the dependency tree vendorable and the build reproducible. The public surface is 135 functions in the Search namespace.

Install

s add github.com/MenkeTechnologies/stryke-search

This records the dependency in your project's stryke.toml and downloads the prebuilt cdylib for your host triple from the GitHub release (SHA-256 verified). On first use Search stryke dlopens the cdylib in-process. Prebuilt targets are x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu, x86_64-apple-darwin, and aarch64-apple-darwin. Then use Search in any stryke script.

Quick start

use Search

var %conn
$conn{url} = "http://127.0.0.1:9200"

# create an index with a mapping
Search::index_create(
    "books",
    body => { mappings => { properties => { title => { type => "text" }, year => { type => "integer" } } } },
    %conn,
)

# index a document and make it searchable
Search::doc_index("books", { title => "rust in action", year => 2019 }, id => "1", %conn)
Search::index_refresh(index => "books", %conn)

# search it
val $res = Search::search("books", Search::match("title", "rust"), %conn)
p $res->{hits}{total}{value}      # 1

# count with a range query
val $cnt = Search::count("books", Search::range("year", gte => 2019), %conn)
p $cnt->{count}                   # 1

Connecting

Connection params come from the %conn opts hash on every network call. When neither url nor host is given, the client falls back to the $SEARCH_URL environment variable. The pure query-DSL builders take no connection.

urlFull base URL, e.g. https://es.example.com:9243. No default.
hostUsed with port / tls when url is absent. Default 127.0.0.1.
portDefault 9200.
tlstrue selects the https scheme. Default false.
username / passwordHTTP Basic auth.
api_keySent as Authorization: ApiKey <key>; wins over Basic when both are set.
paramsHash of extra URL query params merged into the request.
# explicit host/port/tls with basic auth
var %conn
$conn{host}     = "es.example.com"
$conn{port}     = 9243
$conn{tls}      = true
$conn{username} = "elastic"
$conn{password} = "changeme"

# or one URL, or an API key
$conn{url}      = "https://es.example.com:9243"
$conn{api_key}  = "BASE64_ENCODED_KEY"

# or rely on $SEARCH_URL: SEARCH_URL=http://127.0.0.1:9200 s run app.stk

API surface

The 135-function FFI surface, grouped. Network ops take a trailing %conn opts hash; the query-DSL, aggregation, body, and URL builders are pure and take no connection.

Version + livenessversion, ping, info, health
Cluster + nodescluster_stats, cluster_state, cluster_settings_get, cluster_settings_put, nodes_info, nodes_stats, pending_tasks, allocation_explain, cat, raw
Index adminindex_create, index_delete, index_exists, index_list, index_refresh, index_open, index_close, index_stats, forcemerge, flush, clear_cache, settings_get, settings_update, mapping_get, mapping_put, field_caps
Aliasesalias_add, alias_remove, alias_get
Templatesindex_template_put, index_template_get, index_template_delete, index_template_exists, component_template_put, component_template_get, component_template_delete
Documentsdoc_index, doc_get, doc_exists, doc_update, doc_delete, mget, bulk, termvectors, mtermvectors
Searchsearch, count, msearch, search_aggs, search_shards, scroll_start, scroll_next, scroll_clear, delete_by_query, update_by_query, reindex, analyze, explain, pit_open, pit_close
Query DSL buildersmatch_all, match, match_phrase, match_phrase_prefix, term, terms, range, prefix, wildcard, regexp, fuzzy, exists, ids, query_string, simple_query_string, multi_match, geo_distance, geo_bounding_box, nested, constant_score, dis_max, function_score, more_like_this, bool
Aggregationsagg, agg_terms, agg_avg, agg_sum, agg_min, agg_max, agg_stats, agg_extended_stats, agg_cardinality, agg_value_count, agg_percentiles, agg_histogram, agg_date_histogram, agg_range, agg_filter, agg_missing, agg_nested, agg_top_hits, agg_global
Body + fieldsquery_body, sort, highlight, bulk_ndjson, escape
Ingest pipelinesingest_put, ingest_get, ingest_delete, ingest_simulate
Snapshot + reporepo_create, repo_get, repo_delete, snapshot_create, snapshot_get, snapshot_delete, snapshot_restore
Taskstasks_list, tasks_get, tasks_cancel
Stored scripts + templatesscript_put, script_get, script_delete, search_template, render_template
URL helpersbuild_url, parse_url, redact_url
Validationvalid_index_name

The full reference lives in the README; the architecture is in the engineering report.

Queries + aggregations

Query builders return bare clauses (e.g. {"match":{…}}), so they nest directly inside bool / nested / constant_score. Search::search and Search::count auto-wrap a top-level clause as {"query": clause}. Pass a full body built with Search::query_body when you need aggs / sort / size.

# bool query: must match title, filter by year range
val $q = Search::bool(
    must   => [ Search::match("title", "rust") ],
    filter => [ Search::range("year", gte => 2018) ],
)
val $hits = Search::search("books", $q, %conn)

# aggregations: average price bucketed by category
val $res = Search::search_aggs(
    "products",
    { by_cat => Search::agg_terms("category", size => 10) },
    %conn,
)
p $res->{aggregations}{by_cat}{buckets}

# full body with query + sort + size
val $body = Search::query_body(
    query => Search::match("title", "rust"),
    sort  => Search::sort("year", order => "desc"),
    size  => 20,
)
val $page = Search::search("books", $body, %conn)

The builders are pure — they construct clauses with no cluster present:

# geo bounding box: docs whose `loc` is inside the box
Search::geo_bounding_box("loc", { lat => 40.73, lon => -74.1 }, { lat => 40.01, lon => -71.12 })

# function_score: re-score with a random function
Search::function_score(Search::match_all(), [{ random_score => {} }], boost_mode => "multiply")

# more_like_this across two fields
Search::more_like_this(["title", "body"], "quick brown fox", min_term_freq => 1)

# top_hits aggregation
Search::agg_top_hits(size => 3)

Bulk + pagination

Bulk ops are an array of op hashes; the cdylib serializes the NDJSON body. Search::bulk_ndjson builds the same body without sending it, for inspection or batching.

Search::bulk(
    [
        { action => "index",  id => "2", document => { title => "the rust book" } },
        { action => "update", id => "1", doc      => { year => 2020 } },
        { action => "delete", id => "3" },
    ],
    index => "books",
    %conn,
)

# scroll through a large result set
val $first = Search::scroll_start("books", Search::match_all(), scroll => "1m", %conn)
val $more  = Search::scroll_next($first->{_scroll_id}, %conn)
Search::scroll_clear($first->{_scroll_id}, %conn)

URL helpers + validation

The URL helpers and valid_index_name are pure and need no cluster. redact_url strips credentials for safe logging.

Search::build_url(host => "h", port => 9200)          # "http://h:9200"
Search::redact_url("https://u:p\@h:9200")           # "https://***\@h:9200"
Search::escape("a+b")                               # "a\\+b" (Lucene-escaped)

val $pu = Search::parse_url("https://es.example.com:9243/prefix")
p $pu->{host}                                        # "es.example.com"
p $pu->{port}                                        # 9243

Search::valid_index_name("Books")                   # { name, valid, reason }

Errors

When the cdylib reports an error, the wrapper dies with a message of the form Search::<fn>: <reason> — e.g. a missing index or a malformed query body surfaces as a raised stryke exception rather than a silent { error: … } hash. FFI handlers run inside catch_unwind, so a panic becomes an error result instead of unwinding across the FFI boundary. Wrap calls in try / catch to handle failures.

Build + test

make debug       # cargo build
make test        # cargo test, then `s test t/` (needs $SEARCH_URL or 127.0.0.1:9200)
make install     # s pkg install -g . (cdylib lands in ~/.stryke/store/search@<ver>/)

cargo test runs the in-crate unit tests (NDJSON build, Lucene escaping, URL parse / redact, basic-auth header, percent-encoding) with no cluster required. t/test_stryke_search_surface.stk pins the wrapper surface and the query-DSL builders; t/test_search.stk runs end-to-end CRUD + search against a live cluster and short-circuits when none answers.

Package family

Part of the stryke connector family under the MenkeTechnologiesMeta umbrella:

Sibling packages

Part of the stryke package family. Browse the others via the MenkeTechnologiesMeta umbrella repo: