Searchcraft API Rust Client Library
The Searchcraft API Rust Client is an async, fully typed client that acts as a language-specific wrapper around the Searchcraft API.
Features
Section titled “Features”- Async throughout, built on
reqwest. - Fully typed requests and responses, with no untyped JSON in the public API.
- Composable, immutable query builder.
- Support for every Searchcraft query kind (fuzzy, exact, dynamic, term, more-like-this) and operation.
- Full index, document, federation, synonym, and stopword management.
- Streaming AI search summaries over Server-Sent Events.
rustlsby default, with an optionalnative-tlsbackend.- API keys held in
secrecywrappers so they are not logged by accident.
Installation
Section titled “Installation”cargo add searchcraftOr add it to your Cargo.toml directly:
[dependencies]searchcraft = "0.1"tokio = { version = "1", features = ["macros", "rt-multi-thread"] }The client is async and does not bundle a runtime, so you need one of your own. The examples here use Tokio.
TLS backends
Section titled “TLS backends”rustls is enabled by default. To use the platform’s native TLS instead:
[dependencies]searchcraft = { version = "0.1", default-features = false, features = ["native-tls"] }The minimum supported Rust version is 1.75.
Quick Start
Section titled “Quick Start”use searchcraft::SearchcraftClient;use searchcraft::search::query::QueryBuilder;
#[tokio::main]async fn main() -> Result<(), searchcraft::Error> { // Initialize the client let client = SearchcraftClient::new( "https://your-searchcraft-instance.com", Some("your-read-key"), None::<String>, )?;
// Perform a search let request = QueryBuilder::fuzzy().term("search query").limit(10).build_request(); let response = client .search_index::<serde_json::Value>("your-index", &request) .await?;
println!("Found {} results", response.data.count); for hit in &response.data.hits { println!("{}: {:?}", hit.document_id, hit.doc); }
Ok(())}Client Initialization
Section titled “Client Initialization”For a read-only client, SearchcraftClient::new takes the endpoint and up to two keys:
use searchcraft::SearchcraftClient;
let client = SearchcraftClient::new( "https://your-instance.com", Some("your-read-key"), Some("your-ingest-key"), // Optional, for document operations)?;At least one of the read or ingest key must be supplied. For an admin key, a custom timeout, or extra headers, build a Config instead:
use std::time::Duration;use searchcraft::{Config, SearchcraftClient};
let config = Config::new( "https://your-instance.com", Some("your-read-key"), Some("your-ingest-key"),)?.with_admin_key("your-admin-key") // Only needed for self-hosted clusters.with_timeout(Duration::from_secs(30)) // Optional, default 30 seconds// Optional Searchcraft headers for analytics and tracking:.with_header("X-Sc-User-Id", "user-123").with_header("X-Sc-Session-Id", "session-abc").with_header("X-Sc-User-Type", "authenticated");
let client = SearchcraftClient::from_config(config)?;The client picks the right key per operation, so you never pass a key to an individual call. SearchcraftClient is cheap to clone and holds a pooled connection — build it once and share it rather than creating one per request.
Note: The admin key is only required for self-hosted Searchcraft clusters. If you’re using Searchcraft Cloud, you do not need to provide one.
Basic Search
Section titled “Basic Search”use searchcraft::search::query::QueryBuilder;
// Fuzzy search (typo-tolerant, uses weight ranking, synonyms and stopwords)let fuzzy_query = QueryBuilder::fuzzy().term("search term").limit(20).build_request();
// Exact searchlet exact_query = QueryBuilder::exact().term("exact match").build_request();
// Dynamic search (adapts based on word count)let dynamic_query = QueryBuilder::dynamic().term("adaptive search").build_request();
let response = client .search_index::<serde_json::Value>("your-index", &fuzzy_query) .await?;Search results deserialize into any type implementing serde::Deserialize, so you can work with your own structs instead of raw JSON:
#[derive(serde::Deserialize)]struct Product { title: String, price: f64,}
let response = client.search_index::<Product>("products", &request).await?;for hit in &response.data.hits { println!("{} — ${}", hit.doc.title, hit.doc.price);}Query Builder
Section titled “Query Builder”The query builder provides a fluent, immutable API for constructing complex queries. Each method returns a new builder, leaving the original unchanged.
📚 Full Query Syntax Documentation: For complete details on the Searchcraft query language, operators, and advanced features, see Using the Searchcraft Query Language.
use searchcraft::search::query::QueryBuilder;use searchcraft::types::SortDirection;
let request = QueryBuilder::fuzzy() .term("laptop") .and("gaming") .not("refurbished") .order_by("price", SortDirection::Asc) .limit(20) .build_request();Field Queries
Section titled “Field Queries”// Match a single fieldlet request = QueryBuilder::exact().field("category", "electronics").build_request();
// Match any of several valueslet request = QueryBuilder::exact() .field_in("status", &["active", "pending"]) .build_request();Range and Comparison Queries
Section titled “Range and Comparison Queries”use searchcraft::search::query::CompareOp;
// Inclusive range: price:[10 TO 100]let request = QueryBuilder::exact().range("price", 10, 100, true).build_request();
// Exclusive range: price:{10 TO 100}let request = QueryBuilder::exact().range("price", 10, 100, false).build_request();
// Comparisons: Gt, Lt, Gte, Ltelet request = QueryBuilder::exact() .compare("price", CompareOp::Gte, 50) .build_request();Boolean Operators
Section titled “Boolean Operators”let request = QueryBuilder::fuzzy() .term("laptop") .and("gaming") // AND .or("notebook") // OR .not("refurbished") // NOT .group("(ssd OR nvme)") // Grouping .build_request();Pagination and Sorting
Section titled “Pagination and Sorting”use searchcraft::types::SortDirection;
let request = QueryBuilder::fuzzy() .term("laptop") .limit(20) .offset(40) // Third page of 20 .order_by("created_at", SortDirection::Desc) .build_request();The engine clamps limit to the instance’s configured maximum (200 unless the operator raised it) rather than rejecting the request.
Note: Ordering by a field means results carry no relevance score, so
hit.scoreisNonefor those queries.
Federated Search
Section titled “Federated Search”Search several indices at once through a federation:
let request = QueryBuilder::fuzzy().term("laptop").limit(10).build_request();let response = client .search_federation::<serde_json::Value>("your-federation", &request) .await?;
for hit in &response.data.hits { // source_index tells you which index each hit came from println!("[{}] {}", hit.source_index, hit.document_id);}Advanced: Raw Query Objects
Section titled “Advanced: Raw Query Objects”The builder covers most cases, but you can construct requests directly for anything it does not express — including the term and more-like-this query kinds, field-scoped fuzzy matching, and minimum_number_should_match:
use searchcraft::search::types::{ FieldSelector, OccurMode, QueryPayload, SearchQuery, SearchRequest,};
let request = SearchRequest { query: QueryPayload::Multiple(vec![ SearchQuery::fuzzy("laptop") .with_fields(FieldSelector::Multi(vec!["title".into(), "body".into()])) .with_occur(OccurMode::Must), SearchQuery::term("electronics", "category").with_occur(OccurMode::Must), SearchQuery::exact("refurbished").with_occur(OccurMode::MustNot), ]), limit: Some(20), offset: None, order_by: None, sort: None, time_decay_field: None, index_weighting: None, minimum_number_should_match: Some(1),};
let response = client.search_index::<serde_json::Value>("products", &request).await?;Find documents similar to an existing one using its internal Searchcraft ID:
let request = SearchRequest { query: QueryPayload::Single(SearchQuery::more_like_this("12345")), limit: Some(10), ..request};AI Search Summaries
Section titled “AI Search Summaries”On engine 0.10.0 and later, search_summary streams an LLM-generated summary of a result set as it is produced. Check the index’s capabilities first — the endpoint requires AI features to be enabled and a key with summary permissions:
use searchcraft::search::StreamExt;use searchcraft::search::types::SummaryStreamEvent;
let capabilities = client.get_index_capabilities("products").await?;
if capabilities.ai.enabled && capabilities.ai.search_summary_configured { let request = QueryBuilder::fuzzy().term("laptop").build_request(); let mut stream = client.search_summary("products", &request).await?;
while let Some(event) = stream.next().await { match event { SummaryStreamEvent::Metadata(m) => { println!("Summarizing {} results (cached: {})", m.results_count, m.cached); } SummaryStreamEvent::Delta(d) => print!("{}", d.content), SummaryStreamEvent::Done(d) => println!("\nDone ({} results)", d.results_count), SummaryStreamEvent::Error(e) => eprintln!("Summary failed: {}", e.message), } }}Malformed frames and mid-stream connection failures arrive as Error events rather than ending the stream. Unlike other requests, the configured timeout bounds only connection setup here, so a slow generation is never cut short.
Document Management
Section titled “Document Management”use serde_json::json;
// Insert a single document — it must include an `id` fieldlet doc = json!({ "id": "doc-1", "title": "Gaming Laptop", "price": 999.99 });client.insert_document("products", &doc).await?;
// Batch insertlet docs = vec![ json!({ "id": "doc-2", "title": "Ultrabook" }), json!({ "id": "doc-3", "title": "Workstation" }),];client.batch_insert_documents("products", &docs).await?;
// Delete by your own `id` fieldlet result = client.delete_document("products", "doc-1").await?;println!("Removed {} documents", result.num_removed);
// Delete several at onceclient.batch_delete_documents("products", &["doc-2", "doc-3"]).await?;
// Delete by Searchcraft's internal document ID (the `document_id` on a search hit)client.delete_document_by_internal_id("products", "12345").await?;
// Delete every document, leaving the index in placeclient.delete_all_documents("products").await?;
// Fetch one document by its internal IDlet hit = client.get_document::<serde_json::Value>("products", "12345").await?;Writes are buffered until committed. An index’s auto_commit_delay commits them on a timer, or you can commit immediately:
client.commit_transaction("products").await?; // Make buffered writes searchableclient.rollback_transaction("products").await?; // Discard them insteadIndex Management
Section titled “Index Management”use std::collections::HashMap;use searchcraft::admin::types::{FieldConfig, FieldType, IndexConfig};
// List every indexlet indices = client.list_indices().await?;println!("{:?}", indices.index_names);
// Create an indexlet config = IndexConfig { language: Some("en".into()), search_fields: Some(vec!["title".into(), "body".into()]), fields: Some(HashMap::from([ ( "title".to_string(), FieldConfig { stored: Some(true), required: Some(true), ..FieldConfig::new(FieldType::Text) }, ), ( "price".to_string(), FieldConfig { fast: Some(true), ..FieldConfig::new(FieldType::F64) }, ), ])), ..Default::default()};client.create_index("products", &config).await?;
// Read the current configurationlet current = client.get_index("products").await?;
// Patch specific settings, leaving everything else alonelet patch = IndexConfig { auto_commit_delay: Some(5000), ..Default::default() };client.update_index("products", &patch).await?;
// Statslet stats = client.get_index_stats("products").await?;println!("Documents: {}", stats.document_count);
let all_stats = client.get_all_index_stats().await?;println!("Total documents: {}", all_stats.total_document_count);
// Delete the index and everything in itclient.delete_index("products").await?;Field types are Text, Facet, Bool, F64, U64, I64, Datetime, and Json. Every option on FieldConfig is optional — anything left unset takes the engine’s default.
Note: The patch endpoint cannot change schema fields, and treats an empty
search_fields,weight_multipliers, orlanguageas “leave unchanged” rather than a reset. Usereplace_indexto change the schema or clear those values. To overwrite an existing index on create, usecreate_index_overwriting.
Federation Management
Section titled “Federation Management”use searchcraft::admin::types::FederationRequest;
// List federationslet federations = client.list_federations().await?;
// Create one over several indices, each weighted equallylet request = FederationRequest::new("global", "Global", ["products", "articles"]);client.create_federation(&request).await?;
// Inspectlet federation = client.get_federation("global").await?;let index_names = client.list_federation_indices("global").await?;
let stats = client.get_federation_stats("global").await?;println!("{} documents across {} indices", stats.num_docs, stats.indices.len());
// Update or deleteclient.update_federation("global", &request).await?;client.delete_federation("global").await?;Synonyms
Section titled “Synonyms”Synonym entries use the format "synonym:original-term".
let synonyms = client.get_synonyms("products").await?;
client.add_synonyms("products", &["notebook:laptop"]).await?;client.delete_synonyms("products", &["notebook:laptop"]).await?;client.delete_all_synonyms("products").await?;Stopwords
Section titled “Stopwords”// The index's current listlet stopwords = client.get_stopwords("products").await?;
// The engine's built-in defaults for the index languagelet defaults = client.get_default_stopwords("products").await?;
client.add_stopwords("products", &["the", "and"]).await?;client.delete_stopwords("products", &["the"]).await?;client.delete_all_stopwords("products").await?;Authentication Keys
Section titled “Authentication Keys”Key management is available on self-hosted clusters and requires an admin key.
use searchcraft::admin::types::{permissions, AuthKeyPermission, CreateAuthKeyRequest};
// Permissions are a bitmask. Named presets cover the common cases.let request = CreateAuthKeyRequest::new("web-frontend", AuthKeyPermission::READ, ["products"]);let created = client.create_auth_key(&request).await?;println!("New key: {}", created.token);
// Or build an exact mask from individual bitslet custom = AuthKeyPermission::from_bits(permissions::SEARCH | permissions::READ_ANALYTICS);assert!(custom.contains(permissions::READ_ANALYTICS));
// List keys, optionally scoped by ownerlet all_keys = client.list_auth_keys().await?;let index_keys = client.list_index_auth_keys("products").await?;let app_keys = client.list_application_auth_keys("app-1").await?;
// Look one up — returns None when it does not existif let Some(key) = client.get_auth_key("sc-abc").await? { println!("{:?}", key.permissions);}
// Ask whether the configured admin key carries a permissionlet can_read_analytics = client .check_auth_key_permissions(permissions::READ_ANALYTICS) .await?;The presets are READ (search only), INGEST (search plus document, synonym, and stopword writes), ADMIN (adds index and key management), and SUPER_USER (everything, including AI and analytics).
Analytics
Section titled “Analytics”Measure records search and click telemetry. Analytics must be configured server-side, so check the status first — the write endpoints silently succeed as no-ops when it is disabled.
use searchcraft::admin::types::{ event_names, MeasureDashboardParams, MeasureEvent, MeasureQueryGranularity, MeasureRequestProperties, MeasureRequestUser,};
let status = client.get_measure_status().await?;if status.enabled { let event = MeasureEvent::new( event_names::DOCUMENT_CLICKED, MeasureRequestProperties { external_document_id: Some("doc-1".into()), document_position: Some(3), ..MeasureRequestProperties::new(["products"]) }, MeasureRequestUser::new("user-42"), ); client.track_measure_event(&event).await?;}
// Dashboard reports accept filters for scope, date range, granularity and paginglet params = MeasureDashboardParams { organization_id: Some("org-1".into()), index_names: vec!["products".into()], date_start: Some(1_700_000_000), granularity: Some(MeasureQueryGranularity::Days), ..Default::default()};let summary = client.get_measure_dashboard_summary(¶ms).await?;track_measure_batch sends several events in one request, and get_measure_dashboard_conversion and get_measure_dashboard_usage return the other two reports.
Health Check
Section titled “Health Check”let health = client.health_check().await?;println!("Status: {}", health.status); // 200println!("Message: {}", health.data); // "Searchcraft is healthy."API Reference
Section titled “API Reference”All methods are async and return searchcraft::error::Result<T>.
Client
Section titled “Client”| Method | Description |
|---|---|
SearchcraftClient::new(url, read_key, ingest_key) | Create a client from an endpoint and up to two keys |
SearchcraftClient::from_config(config) | Create a client from a Config |
client.config() | The configuration the client was built with |
Search
Section titled “Search”| Method | Returns |
|---|---|
search_index::<T>(index, request) | SearchResponse<T> |
search_federation::<T>(federation, request) | SearchResponse<T> |
search_summary(index, request) | A stream of SummaryStreamEvent |
Documents
Section titled “Documents”| Method | Returns |
|---|---|
insert_document(index, document) | String |
batch_insert_documents(index, documents) | String |
delete_document(index, document_id) | DocumentDeleteResponse |
batch_delete_documents(index, document_ids) | DocumentDeleteResponse |
delete_document_by_internal_id(index, internal_id) | String |
delete_all_documents(index) | String |
get_document::<T>(index, internal_id) | SearchHit<T> |
Indices
Section titled “Indices”| Method | Returns |
|---|---|
list_indices() | IndexListResponse |
get_index(index) | IndexConfig |
create_index(index, config) | String |
create_index_overwriting(index, config) | String |
replace_index(index, config) | String |
update_index(index, config) | String |
delete_index(index) | String |
get_index_stats(index) | IndexStats |
get_all_index_stats() | AllIndexStatsResponse |
get_index_capabilities(index) | IndexCapabilities |
Federations
Section titled “Federations”| Method | Returns |
|---|---|
list_federations() | Vec<Federation> |
list_federations_by_organization(org_id) | Vec<Federation> |
get_federation(federation) | Federation |
create_federation(request) | serde_json::Value |
update_federation(federation, request) | serde_json::Value |
delete_federation(federation) | String |
list_federation_indices(federation) | Vec<String> |
get_federation_stats(federation) | FederationStats |
Synonyms and Stopwords
Section titled “Synonyms and Stopwords”| Method | Returns |
|---|---|
get_synonyms(index) | SynonymsMap |
add_synonyms(index, synonyms) | String |
delete_synonyms(index, synonyms) | String |
delete_all_synonyms(index) | String |
get_stopwords(index) | Vec<String> |
get_default_stopwords(index) | Vec<String> |
add_stopwords(index, stopwords) | String |
delete_stopwords(index, stopwords) | String |
delete_all_stopwords(index) | String |
Transactions
Section titled “Transactions”| Method | Returns |
|---|---|
commit_transaction(index) | String |
rollback_transaction(index) | String |
Authentication
Section titled “Authentication”| Method | Returns |
|---|---|
list_auth_keys() | Vec<AuthKey> |
get_auth_key(key) | Option<AuthKey> |
create_auth_key(request) | AuthKey |
update_auth_key(key, request) | AuthKey |
delete_auth_key(key) | String |
delete_all_auth_keys() | String |
list_application_auth_keys(app_id) | Vec<AuthKey> |
list_organization_auth_keys(org_id) | Vec<AuthKey> |
list_federation_auth_keys(federation) | Vec<AuthKey> |
list_index_auth_keys(index) | Vec<AuthKey> |
check_auth_key_permissions(bits) | bool |
Measure and Health
Section titled “Measure and Health”| Method | Returns |
|---|---|
get_measure_status() | MeasureStatus |
track_measure_event(event) | String |
track_measure_batch(events) | String |
get_measure_dashboard_summary(params) | serde_json::Value |
get_measure_dashboard_conversion(params) | serde_json::Value |
get_measure_dashboard_usage(params) | serde_json::Value |
health_check() | HealthCheckResponse |
Error Handling
Section titled “Error Handling”Every fallible call returns searchcraft::error::Result<T>, whose error type separates the cases you are likely to handle differently:
use searchcraft::Error;
match client.search_index::<serde_json::Value>("products", &request).await { Ok(response) => println!("{} hits", response.data.count), Err(Error::Authentication { status, .. }) => eprintln!("Auth failed ({status})"), Err(Error::NotFound(message)) => eprintln!("Missing: {message}"), Err(Error::Validation { message, field }) => { eprintln!("Invalid request: {message} ({field:?})") } Err(e) if e.is_retryable() => eprintln!("Transient failure, retry: {e}"), Err(e) => eprintln!("Failed: {e}"),}is_retryable() is true for network failures and 429/5xx responses, and false for configuration, authentication, not-found, and validation errors, which will fail the same way on a retry. status() returns the HTTP status where one applies.
Error is marked #[non_exhaustive], so match the variants you handle and include a _ arm.
Feature Requests / Issues
Section titled “Feature Requests / Issues”If you encounter any issues with the client library or wish to request additional features please open an issue at the Searchcraft Issues repository.
License and Source code
Section titled “License and Source code”The Searchcraft API Rust client is Apache 2.0 licensed and the source code is available on Github.