# HeroIndex Client
[](https://crates.io/crates/heroindex_client)
[](https://docs.rs/heroindex_client)
[](https://opensource.org/licenses/MIT)
A Rust client library for [HeroIndex](https://crates.io/crates/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`:
```toml
[dependencies]
heroindex_client = "0.1"
tokio = { version = "1", features = ["full"] }
serde_json = "1"
```
## Quick Start
```rust
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
```rust
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
```rust
// Connect to server
let mut client = HeroIndexClient::connect("/tmp/heroindex.sock").await?;
```
### Server Operations
```rust
// 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
```rust
// 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
```rust
// 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
```rust
// 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
```rust
// 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)
```rust
client.search(json!({
"type": "match",
"field": "content",
"value": "rust programming"
}), 10, 0).await?;
```
### Fuzzy Query (Typo-Tolerant)
```rust
// 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)
```rust
client.search(json!({
"type": "phrase",
"field": "content",
"value": "exact phrase match"
}), 10, 0).await?;
```
### Range Query (Numeric)
```rust
// Products between $50 and $100
client.search(json!({
"type": "range",
"field": "price",
"gte": 50.0,
"lt": 100.0
}), 10, 0).await?;
```
### Boolean Query (Combined)
```rust
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
```rust
// Find all words starting with "auto"
client.search(json!({
"type": "prefix",
"field": "title",
"value": "auto"
}), 10, 0).await?;
```
### Term Query (Exact Match)
```rust
// Exact match on keyword field
client.search(json!({
"type": "term",
"field": "id",
"value": "prod-001"
}), 10, 0).await?;
```
## Error Handling
```rust
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
- [heroindex](https://crates.io/crates/heroindex) - The HeroIndex server
## License
MIT License