searchcraft 0.1.0

Async Rust client for the Searchcraft search API
Documentation

Features

  • Fully typed, async API built on reqwest.
  • Composable, immutable query builder.
  • Support for all Searchcraft query kinds (fuzzy, exact, dynamic, term, more-like-this).
  • Full index, document, federation, synonym, and stopword management.
  • Streaming AI search summaries over Server-Sent Events.
  • Analytics via the Measure API, including the dashboard reports.
  • rustls by default, with an optional native-tls backend.
  • API keys held in secrecy wrappers so they are not logged by accident.

Installation

Add the crate to your project:

cargo add searchcraft

Or add it manually to your Cargo.toml:

[dependencies]
searchcraft = "0.1"

Quick start

use searchcraft::SearchcraftClient;
use searchcraft::search::query::QueryBuilder;

#[tokio::main]
async fn main() -> Result<(), searchcraft::Error> {
    let client = SearchcraftClient::new(
        "https://my-instance.searchcraft.io",
        Some("sc-read-key"),
        None::<String>,
    )?;

    let request = QueryBuilder::fuzzy()
        .term("laptop")
        .limit(10)
        .build_request();

    let response = client
        .search_index::<serde_json::Value>("products", &request)
        .await?;

    for hit in &response.data.hits {
        // `score` is None when results are ordered by a field rather than
        // by relevance.
        match hit.score {
            Some(score) => println!("{} (score: {score:.2})", hit.document_id),
            None => println!("{}", hit.document_id),
        }
    }

    Ok(())
}

Feature flags

Feature Default Description
rustls Use rustls for TLS
native-tls Use the platform's native TLS

To use native TLS instead of rustls:

[dependencies]
searchcraft = { version = "0.1", default-features = false, features = ["native-tls"] }

API overview

Search

Search a single index or a federation of indices:

# async fn example() -> Result<(), searchcraft::Error> {
# let client = searchcraft::SearchcraftClient::new("https://x.io", Some("k"), None::<String>)?;
use searchcraft::search::query::QueryBuilder;

// Fuzzy search with boolean composition
let request = QueryBuilder::fuzzy()
    .term("laptop")
    .and("gaming")
    .not("refurbished")
    .limit(20)
    .build_request();

let results = client.search_index::<serde_json::Value>("products", &request).await?;

// Federation search
let results = client.search_federation::<serde_json::Value>("global", &request).await?;
# Ok(())
# }

The QueryBuilder supports fuzzy, exact, and dynamic query modes, field queries, range queries, comparisons, boolean composition (and/or/not), grouping, pagination (limit/offset), and sorting (order_by). limit is capped at 200 and is checked client-side before the request is sent.

AI search summaries

Engine 0.10.0+ can stream an LLM-generated summary of a query's results over Server-Sent Events. Check get_index_capabilities first — the endpoint requires AI features to be enabled on the index and a key with summary permissions:

# async fn example() -> Result<(), searchcraft::Error> {
# let client = searchcraft::SearchcraftClient::new("https://x.io", Some("k"), None::<String>)?;
use searchcraft::search::query::QueryBuilder;
use searchcraft::search::types::SummaryStreamEvent;
use searchcraft::search::StreamExt;

let capabilities = client.get_index_capabilities("products").await?;
if capabilities.ai.enabled {
    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::Delta(d) => print!("{}", d.content),
            SummaryStreamEvent::Done(d) => println!("\n({} results)", d.results_count),
            SummaryStreamEvent::Error(e) => eprintln!("summary failed: {}", e.message),
            SummaryStreamEvent::Metadata(_) => {}
        }
    }
}
# Ok(())
# }

Malformed frames and mid-stream transport failures arrive as Error events rather than ending the stream. The configured timeout bounds only connection setup for this endpoint, so a slow generation is never cut short.

Index management

# async fn example() -> Result<(), searchcraft::Error> {
# let client = searchcraft::SearchcraftClient::new("https://x.io", Some("k"), Some("w"))?;
use std::collections::HashMap;
use searchcraft::admin::types::{FieldConfig, FieldType, IndexConfig};

// List indices
let indices = client.list_indices().await?;

// Create an index. Request types have `Default` or a `new` constructor, so you
// only spell out the fields you care about.
let config = IndexConfig {
    language: Some("en".into()),
    search_fields: Some(vec!["title".into()]),
    fields: Some(HashMap::from([(
        "title".to_string(),
        FieldConfig {
            stored: Some(true),
            required: Some(true),
            ..FieldConfig::new(FieldType::Text)
        },
    )])),
    ..Default::default()
};
client.create_index("my-index", &config).await?;

// Update sends only the fields you set, leaving the rest untouched
let patch = IndexConfig { auto_commit_delay: Some(5000), ..Default::default() };
client.update_index("my-index", &patch).await?;

// Get index stats
let stats = client.get_index_stats("my-index").await?;
println!("Documents: {}", stats.document_count);
# Ok(())
# }

Document ingestion

# async fn example() -> Result<(), searchcraft::Error> {
# let client = searchcraft::SearchcraftClient::new("https://x.io", Some("k"), Some("w"))?;
let doc = serde_json::json!({
    "id": "doc-1",
    "title": "Gaming Laptop",
    "price": 999.99
});

client.insert_document("products", &doc).await?;

// Batch insert
let docs = vec![doc];
client.batch_insert_documents("products", &docs).await?;
# Ok(())
# }

Additional APIs

Module Endpoints
Federations List (all, by organization), get, create, update, delete, indices, stats
Auth keys List (all, by application/organization/federation/index), get, create, update, delete, permission check (self-hosted)
Capabilities Per-index AI capability reporting
Health Health check
Stopwords Get, get defaults, add, remove, clear
Synonyms Get, add, remove, clear
Transactions Commit, rollback index transactions
Measure Status, send events, dashboard summary/conversion/usage

This crate targets Searchcraft engine 0.11.0. Every endpoint the engine exposes at that version is covered, except POST /index/:index/hint, which is marked work-in-progress server-side.

Configuration

use std::time::Duration;
use searchcraft::Config;

let config = Config::new(
    "https://my-instance.searchcraft.io",
    Some("read-key"),
    Some("ingest-key"),
)?
.with_admin_key("admin-key")
.with_timeout(Duration::from_secs(10))
.with_header("X-Custom", "value");

let client = searchcraft::SearchcraftClient::from_config(config)?;
# Ok::<(), searchcraft::Error>(())

Error handling

All fallible operations return searchcraft::error::Result<T>. The Error enum covers configuration mistakes, authentication failures (401/403), not-found (404), validation errors (400), other API errors, and network problems:

# async fn example() -> Result<(), searchcraft::Error> {
# let client = searchcraft::SearchcraftClient::new("https://x.io", Some("k"), None::<String>)?;
# let request = searchcraft::search::query::QueryBuilder::fuzzy().term("x").build_request();
match client.search_index::<serde_json::Value>("products", &request).await {
    Ok(response) => println!("Found {} results", response.data.count),
    Err(searchcraft::Error::NotFound(msg)) => eprintln!("Index not found: {msg}"),
    Err(searchcraft::Error::Authentication { status, .. }) => eprintln!("Auth failed ({status})"),
    Err(e) if e.is_retryable() => eprintln!("Transient error, retry: {e}"),
    Err(e) => eprintln!("Error: {e}"),
}
# Ok(())
# }

Minimum supported Rust version

The MSRV is 1.75 (Rust 2021 edition).

Contributing

Please file issues in the Searchcraft Issues repository.

Maintainers: see PUBLISHING.md for the release process.

License

Licensed under the Apache License, Version 2.0. See LICENSE for the full text.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this crate by you shall be licensed as above, without any additional terms or conditions.