heroindex_client 0.1.0

Client library for HeroIndex search server
Documentation

HeroIndex Client

Crates.io Documentation License: MIT

A Rust client library for HeroIndex, a high-performance full-text search server built on Tantivy.

Features

  • Async/Await - Built on Tokio for async operations
  • Type-Safe - Strongly typed responses
  • Simple API - Intuitive method names matching RPC calls
  • Connection Pooling Ready - Each client maintains its own connection state

Installation

Add to your Cargo.toml:

[dependencies]
heroindex_client = "0.1"
tokio = { version = "1", features = ["full"] }
serde_json = "1"

Quick Start

use heroindex_client::HeroIndexClient;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), heroindex_client::Error> {
    // Connect to HeroIndex server
    let mut client = HeroIndexClient::connect("/tmp/heroindex.sock").await?;
    
    // Check server health
    let ping = client.ping().await?;
    println!("Server status: {} (v{})", ping.status, ping.version);
    
    Ok(())
}

Complete Example

use heroindex_client::HeroIndexClient;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), heroindex_client::Error> {
    let mut client = HeroIndexClient::connect("/tmp/heroindex.sock").await?;
    
    // Create a database with schema
    client.db_create("products", json!({
        "fields": [
            {"name": "id", "type": "str", "stored": true, "indexed": true},
            {"name": "name", "type": "text", "stored": true, "indexed": true},
            {"name": "description", "type": "text", "stored": true, "indexed": true},
            {"name": "price", "type": "f64", "stored": true, "indexed": true, "fast": true},
            {"name": "in_stock", "type": "bool", "stored": true, "indexed": true}
        ]
    })).await?;
    
    // Select the database
    client.db_select("products").await?;
    
    // Add documents one by one
    client.doc_add(json!({
        "id": "prod-001",
        "name": "Wireless Mouse",
        "description": "Ergonomic wireless mouse with USB receiver",
        "price": 29.99,
        "in_stock": true
    })).await?;
    
    // Or add in batch (more efficient)
    client.doc_add_batch(vec![
        json!({"id": "prod-002", "name": "Mechanical Keyboard", "description": "RGB mechanical keyboard with Cherry MX switches", "price": 149.99, "in_stock": true}),
        json!({"id": "prod-003", "name": "USB-C Hub", "description": "7-port USB-C hub with HDMI output", "price": 49.99, "in_stock": false}),
        json!({"id": "prod-004", "name": "Webcam HD", "description": "1080p HD webcam with microphone", "price": 79.99, "in_stock": true}),
    ]).await?;
    
    // Commit and reload to make documents searchable
    client.commit().await?;
    client.reload().await?;
    
    // Search for products
    let results = client.search(
        json!({"type": "match", "field": "description", "value": "wireless"}),
        10, 0
    ).await?;
    
    println!("Found {} products:", results.total_hits);
    for hit in &results.hits {
        println!("  - {} (score: {:.2})", 
            hit.doc["name"].as_str().unwrap_or("?"),
            hit.score
        );
    }
    
    Ok(())
}

API Reference

Connection

// Connect to server
let mut client = HeroIndexClient::connect("/tmp/heroindex.sock").await?;

Server Operations

// Health check
let ping = client.ping().await?;
println!("Status: {}, Version: {}", ping.status, ping.version);

// Server statistics
let stats = client.stats().await?;
println!("Databases: {}, Total docs: {}", stats.databases, stats.total_docs);

// Get OpenRPC discovery document
let schema = client.discover().await?;

Database Operations

// List all databases
let list = client.db_list().await?;
for db in &list.databases {
    println!("{}: {} docs", db.name, db.doc_count);
}

// Create database
client.db_create("mydb", json!({
    "fields": [
        {"name": "title", "type": "text", "stored": true, "indexed": true}
    ]
})).await?;

// Select database (required before document operations)
client.db_select("mydb").await?;

// Get current database info
let info = client.db_info().await?;
println!("Doc count: {}, Segments: {}", info.doc_count, info.segment_count);

// Delete database
client.db_delete("mydb").await?;

Schema Operations

// Get schema of selected database
let schema = client.schema().await?;
for field in &schema.fields {
    println!("{}: {} (stored: {})", field.name, field.field_type, field.stored);
}

Document Operations

// Add single document
client.doc_add(json!({"title": "Hello World"})).await?;

// Add batch of documents (more efficient)
client.doc_add_batch(vec![
    json!({"title": "Doc 1"}),
    json!({"title": "Doc 2"}),
    json!({"title": "Doc 3"}),
]).await?;

// Delete documents by field value
client.doc_delete("id", json!("doc-to-delete")).await?;

// Commit changes (persist to disk)
client.commit().await?;

// Reload index (make changes searchable)
client.reload().await?;

Search Operations

// Full-text search
let results = client.search(
    json!({"type": "match", "field": "content", "value": "search terms"}),
    10,  // limit
    0    // offset
).await?;

println!("Found {} results in {}ms", results.total_hits, results.took_ms);
for hit in &results.hits {
    println!("Score: {:.2}, Doc: {:?}", hit.score, hit.doc);
}

// Count matching documents
let count = client.count(json!({"type": "all"})).await?;
println!("Total documents: {}", count.count);

Query Examples

Match Query (Full-Text Search)

client.search(json!({
    "type": "match", 
    "field": "content", 
    "value": "rust programming"
}), 10, 0).await?;

Fuzzy Query (Typo-Tolerant)

// Finds "hello" even when searching for "helo"
client.search(json!({
    "type": "fuzzy",
    "field": "title",
    "value": "helo",
    "distance": 1  // Allow 1 character difference
}), 10, 0).await?;

Phrase Query (Exact Phrase)

client.search(json!({
    "type": "phrase",
    "field": "content",
    "value": "exact phrase match"
}), 10, 0).await?;

Range Query (Numeric)

// Products between $50 and $100
client.search(json!({
    "type": "range",
    "field": "price",
    "gte": 50.0,
    "lt": 100.0
}), 10, 0).await?;

Boolean Query (Combined)

client.search(json!({
    "type": "boolean",
    "must": [
        {"type": "match", "field": "category", "value": "electronics"}
    ],
    "should": [
        {"type": "match", "field": "title", "value": "premium"}
    ],
    "must_not": [
        {"type": "term", "field": "status", "value": "discontinued"}
    ]
}), 10, 0).await?;

Prefix Query

// Find all words starting with "auto"
client.search(json!({
    "type": "prefix",
    "field": "title",
    "value": "auto"
}), 10, 0).await?;

Term Query (Exact Match)

// Exact match on keyword field
client.search(json!({
    "type": "term",
    "field": "id",
    "value": "prod-001"
}), 10, 0).await?;

Error Handling

use heroindex_client::{Error, error_codes};

match client.db_select("nonexistent").await {
    Ok(_) => println!("Selected"),
    Err(Error::Rpc { code, message }) => {
        if code == error_codes::DATABASE_NOT_FOUND {
            println!("Database not found: {}", message);
        }
    }
    Err(e) => println!("Other error: {}", e),
}

Response Types

All methods return strongly-typed responses:

  • PingResponse - status, version
  • ServerStats - uptime_secs, databases, total_docs
  • DatabaseList - databases: Vec<DatabaseInfo>
  • DatabaseInfo - name, doc_count, size_bytes, segment_count
  • SchemaInfo - fields: Vec<FieldInfo>
  • SearchResult - total_hits, hits: Vec<SearchHit>, took_ms
  • SearchHit - score, doc: serde_json::Value
  • CountResult - count
  • OpResult - success, opstamp

Related Crates

License

MIT License