Skip to main content

Rust API Client

A comprehensive guide to using the hlquery Rust API client library. The Rust client provides a modern, async interface for interacting with hlquery servers.

Overview

The hlquery Rust client is a fully-featured, async client library built on Tokio. It provides type-safe access to all hlquery API endpoints with comprehensive error handling and input validation.

Key Features

  • Async/Await Support: Built on Tokio for modern async Rust
  • Type-safe: Strong typing throughout with serde for JSON handling
  • Consistent API Design: Familiar structure for developers
  • Authentication Support: Bearer token and X-API-Key authentication
  • Comprehensive Validation: Input validation for all operations
  • Error Handling: Rich error types with thiserror
  • Minimal Dependencies: Optimized for performance

Installation

Add to Cargo.toml

Add the hlquery Rust client to your project:

[dependencies]
hlquery-rust-client = { path = "../api/rust" }
tokio = { version = "1", features = ["full"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

Or if published to crates.io:

[dependencies]
hlquery-rust-client = "1.0"
tokio = { version = "1", features = ["full"] }

Requirements

  • Rust 1.70+ (Edition 2021)
  • Tokio runtime (for async support)

Quick Start

Basic Example

use hlquery_rust_client::Client;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
// Initialize client
let client = Client::new("http://localhost:9200", None)?;

// Health check
let health = client.health().await?;
println!("Server Status: {}", health.get_status_code());

// List collections
let collections = client.list_collections(0, 10).await?;
if collections.is_success() {
let body = collections.get_body();
if let Some(collections_array) = body.get("collections").and_then(|c| c.as_array()) {
println!("Found {} collections", collections_array.len());
}
}

Ok(())
}

Client Initialization

Creating a Client

use hlquery_rust_client::Client;
use std::collections::HashMap;

// Basic client (no authentication)
let client = Client::new("http://localhost:9200", None)?;

// Client with authentication
let mut options = HashMap::new();
options.insert("token".to_string(), "your_token_here".to_string());
options.insert("auth_method".to_string(), "bearer".to_string());
let client = Client::new("http://localhost:9200", Some(options))?;

Client Options

The client accepts an optional HashMap<String, String> for configuration:

  • token: Authentication token (required if authentication enabled)
  • auth_method: Authentication method ("bearer" or "api-key")
  • timeout: Request timeout in milliseconds (default: 30000)

Dynamic Authentication

You can set or change authentication after client creation:

let client = Client::new("http://localhost:9200", None)?;

// Set bearer token
client.set_auth_token("your_token".to_string(), "bearer".to_string());

// Or use X-API-Key
client.set_auth_token("your_key".to_string(), "api-key".to_string());

// Clear authentication
client.clear_auth();

Health & Status APIs

Health Check

let health = client.health().await?;
println!("Status Code: {}", health.get_status_code());
println!("Response: {}", health.get_body());

Server Statistics

let stats = client.stats().await?;
if stats.is_success() {
let body = stats.get_body();
println!("Uptime: {:?}", body.get("uptime"));
println!("Memory: {:?}", body.get("memory"));
}

Server Information

let info = client.info().await?;
println!("Server Info: {}", info.get_body());

Collections API

List Collections

// List first 10 collections
let collections = client.list_collections(0, 10).await?;

// Using the collections API handler
let collections_api = client.collections();
let result = collections_api.list(0, 10).await?;

if result.is_success() {
let body = result.get_body();
if let Some(cols) = body.get("collections").and_then(|c| c.as_array()) {
for collection in cols {
println!("Collection: {:?}", collection.get("name"));
}
}
}

Get Collection Details

let collection = client.get_collection("my_collection").await?;

// Or using collections API
let result = client.collections().get("my_collection").await?;

if result.is_success() {
let body = result.get_body();
println!("Collection Schema: {:?}", body.get("schema"));
}

Get Collection Fields

let fields = client.get_collection_fields("my_collection").await?;
println!("Fields: {}", fields.get_body());

Create Collection

use serde_json::json;

let schema = json!({
"fields": [
{
"name": "title",
"type": "string",
"index": true
},
{
"name": "price",
"type": "float",
"index": true
},
{
"name": "description",
"type": "string",
"index": true
}
]
});

let result = client.collections().create("products", schema).await?;

if result.is_success() {
println!("Collection created successfully!");
} else {
println!("Error: {:?}", result.get_error());
}

Update Collection Schema

let updated_schema = json!({
"fields": [
// ... updated field definitions
]
});

let result = client.collections().update("products", updated_schema).await?;

Delete Collection

let result = client.collections().delete("products").await?;

if result.is_success() {
println!("Collection deleted successfully");
} else {
println!("Error: {:?}", result.get_error());
}

Documents API

List Documents

use std::collections::HashMap;

let mut params = HashMap::new();
params.insert("offset".to_string(), "0".to_string());
params.insert("limit".to_string(), "10".to_string());

let docs = client.list_documents("products", Some(params)).await?;

// Or using documents API
let result = client.documents().list("products", Some(params)).await?;

if result.is_success() {
let body = result.get_body();
if let Some(documents) = body.get("documents").and_then(|d| d.as_array()) {
println!("Found {} documents", documents.len());
}
}

Get Document

let doc = client.get_document("products", "doc_123").await?;

// Or using documents API
let result = client.documents().get("products", "doc_123").await?;

if result.is_success() {
let body = result.get_body();
println!("Document: {}", body);
}

Add Document

use serde_json::json;

let new_doc = json!({
"id": "prod_1",
"title": "Laptop",
"price": 999.99,
"description": "High-performance laptop"
});

let result = client.documents().add("products", new_doc).await?;

if result.is_success() {
println!("Document added successfully");
} else {
println!("Error: {:?}", result.get_error());
}

Update Document

let updated_doc = json!({
"title": "Updated Laptop",
"price": 899.99
});

let result = client.documents().update("products", "prod_1", updated_doc).await?;

Delete Document

let result = client.documents().delete("products", "prod_1").await?;

if result.is_success() {
println!("Document deleted");
}

Bulk Import Documents

let bulk_docs = vec![
json!({ "id": "prod_1", "title": "Laptop", "price": 999.99 }),
json!({ "id": "prod_2", "title": "Mouse", "price": 29.99 }),
json!({ "id": "prod_3", "title": "Keyboard", "price": 79.99 })
];

let result = client.documents().import("products", bulk_docs).await?;

if result.is_success() {
let body = result.get_body();
println!("Imported: {:?}", body.get("imported"));
println!("Failed: {:?}", body.get("failed"));
}

Delete Documents by Filter

let result = client.documents()
.delete_by_filter("products", "price:[0 TO 50]")
.await?;

if result.is_success() {
let body = result.get_body();
println!("Deleted: {:?}", body.get("deleted"));
}

Search API

use std::collections::HashMap;

let mut params = HashMap::new();
params.insert("q".to_string(), "laptop".to_string());
params.insert("query_by".to_string(), "title,description".to_string());
params.insert("limit".to_string(), "10".to_string());

let results = client.search("products", params).await?;

if results.is_success() {
let body = results.get_body();
if let Some(hits) = body.get("hits").and_then(|h| h.as_array()) {
println!("Found {} results", hits.len());
for hit in hits {
println!("Document: {:?}", hit.get("document"));
println!("Score: {:?}", hit.get("_text_match"));
}
}
}

Search with Filters

let mut params = HashMap::new();
params.insert("q".to_string(), "laptop".to_string());
params.insert("query_by".to_string(), "title".to_string());
params.insert("filter_by".to_string(), "price:[500 TO 1000]".to_string());
params.insert("sort_by".to_string(), "price:asc".to_string());
params.insert("limit".to_string(), "20".to_string());

let results = client.search("products", params).await?;

Using Search API Handler

let search_api = client.search_api();
let mut params = HashMap::new();
params.insert("q".to_string(), "search term".to_string());
params.insert("query_by".to_string(), "title".to_string());

let results = search_api.search("products", params).await?;
let mut params = HashMap::new();
params.insert("vector_query".to_string(), "[0.1,0.2,0.3,0.4,0.5]".to_string());
params.insert("limit".to_string(), "10".to_string());
params.insert("threshold".to_string(), "0.5".to_string());

let results = client.vector_search("products", params).await?;

// Or using search API
let results = client.search_api().vector_search("products", params).await?;
use serde_json::json;

let searches = vec![
json!({
"collection": "products",
"q": "laptop",
"query_by": "title"
}),
json!({
"collection": "reviews",
"q": "laptop",
"query_by": "content"
})
];

let results = client.search_api().multi_search(searches).await?;

if results.is_success() {
let body = results.get_body();
if let Some(results_array) = body.get("results").and_then(|r| r.as_array()) {
for (idx, result) in results_array.iter().enumerate() {
println!("Search {} results: {:?}", idx, result.get("hits"));
}
}
}

Response Handling

Response Object Methods

All API methods return a Result<Response>. The Response struct provides helper methods:

let response = client.health().await?;

// Check status code
let status_code = response.get_status_code();
println!("Status: {}", status_code);

// Get response body (serde_json::Value)
let body = response.get_body();
println!("Body: {}", body);

// Check if successful (2xx status codes)
if response.is_success() {
println!("Request succeeded");
}

// Check if error (4xx or 5xx status codes)
if response.is_error() {
println!("Request failed");
}

// Get error message (if any)
if let Some(error) = response.get_error() {
println!("Error: {}", error);
}

Working with Response Data

let collections = client.list_collections(0, 10).await?;

if collections.is_success() {
let body = collections.get_body();

// Access nested JSON values
if let Some(cols) = body.get("collections").and_then(|c| c.as_array()) {
for collection in cols {
if let Some(name) = collection.get("name").and_then(|n| n.as_str()) {
println!("Collection: {}", name);
}
if let Some(count) = collection.get("num_documents").and_then(|c| c.as_u64()) {
println!("Documents: {}", count);
}
}
}
}

Error Handling

Error Types

The client uses Rust's Result type with custom error types:

use hlquery_rust_client::{Client, HlqueryError};

match client.search("collection", params).await {
Ok(response) => {
// Handle success
if response.is_success() {
println!("Search successful");
} else {
println!("Search returned error status: {}", response.get_status_code());
}
}
Err(HlqueryError::RequestError(e)) => {
println!("Request failed: {}", e);
}
Err(HlqueryError::ValidationError(msg)) => {
println!("Validation failed: {}", msg);
}
Err(HlqueryError::AuthenticationError(msg)) => {
println!("Authentication failed: {}", msg);
}
Err(HlqueryError::InvalidUrl(url)) => {
println!("Invalid URL: {}", url);
}
Err(HlqueryError::JsonError(e)) => {
println!("JSON error: {}", e);
}
Err(e) => {
println!("Other error: {}", e);
}
}

Error Handling Best Practices

async fn search_products(client: &Client, query: &str) -> Result<Vec<serde_json::Value>, Box<dyn std::error::Error>> {
let mut params = HashMap::new();
params.insert("q".to_string(), query.to_string());
params.insert("query_by".to_string(), "title".to_string());
params.insert("limit".to_string(), "10".to_string());

let response = client.search("products", params).await?;

if !response.is_success() {
return Err(format!("Search failed with status {}: {:?}",
response.get_status_code(),
response.get_error()
).into());
}

let body = response.get_body();
let hits = body.get("hits")
.and_then(|h| h.as_array())
.ok_or("Invalid response format")?;

let documents: Vec<serde_json::Value> = hits.iter()
.filter_map(|hit| hit.get("document").cloned())
.collect();

Ok(documents)
}

Input Validation

The client includes comprehensive input validation that runs before making API requests:

Collection Name Validation

use hlquery_rust_client::utils::validator;

// Valid collection names
validator::validate_collection_name("my_collection")?;
validator::validate_collection_name("products_2024")?;

// Invalid collection names (will return ValidationError)
// validator::validate_collection_name("")?; // Empty
// validator::validate_collection_name("123collection")?; // Starts with number
// validator::validate_collection_name("collection name")?; // Contains space

Validation Rules:

  • Must be 1-64 characters
  • Must start with a letter or underscore
  • Can only contain letters, numbers, underscores, and hyphens

Document ID Validation

// Valid document IDs
validator::validate_document_id("doc_123")?;
validator::validate_document_id("product-1")?;

// Invalid document IDs
// validator::validate_document_id("")?; // Empty
// validator::validate_document_id("doc with spaces")?; // Contains spaces

Validation Rules:

  • Must be 1-64 characters
  • Can only contain letters, numbers, underscores, and hyphens

Pagination Validation

// Valid pagination
validator::validate_pagination(Some(0), Some(10))?;

// Invalid pagination
// validator::validate_pagination(Some(-1), Some(10))?; // Negative offset
// validator::validate_pagination(Some(0), Some(0))?; // Zero limit

Search Parameters Validation

use serde_json::json;

let params = json!({
"q": "search term",
"query_by": "title",
"limit": 10,
"offset": 0
});

validator::validate_search_params(&params)?;

Complete Examples

Example 1: Full CRUD Workflow

use hlquery_rust_client::Client;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new("http://localhost:9200", None)?;

// 1. Create collection
let schema = json!({
"fields": [
{ "name": "title", "type": "string", "index": true },
{ "name": "price", "type": "float", "index": true }
]
});
client.collections().create("products", schema).await?;

// 2. Add document
let doc = json!({
"id": "prod_1",
"title": "Laptop",
"price": 999.99
});
client.documents().add("products", doc).await?;

// 3. Get document
let result = client.documents().get("products", "prod_1").await?;
println!("Document: {}", result.get_body());

// 4. Update document
let updated = json!({ "price": 899.99 });
client.documents().update("products", "prod_1", updated).await?;

// 5. Search
let mut params = std::collections::HashMap::new();
params.insert("q".to_string(), "laptop".to_string());
params.insert("query_by".to_string(), "title".to_string());
let results = client.search("products", params).await?;
println!("Search results: {}", results.get_body());

// 6. Delete document
client.documents().delete("products", "prod_1").await?;

// 7. Delete collection
client.collections().delete("products").await?;

Ok(())
}

Example 2: Bulk Operations

use hlquery_rust_client::Client;
use serde_json::json;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new("http://localhost:9200", None)?;

// Bulk import
let products = (1..=100).map(|i| {
json!({
"id": format!("prod_{}", i),
"title": format!("Product {}", i),
"price": (i * 10) as f64
})
}).collect::<Vec<_>>();

let result = client.documents().import("products", products).await?;
println!("Import result: {}", result.get_body());

// List all documents with pagination
let mut offset = 0;
let limit = 20;

loop {
let mut params = std::collections::HashMap::new();
params.insert("offset".to_string(), offset.to_string());
params.insert("limit".to_string(), limit.to_string());

let result = client.documents().list("products", Some(params)).await?;
let body = result.get_body();

if let Some(docs) = body.get("documents").and_then(|d| d.as_array()) {
if docs.is_empty() {
break;
}

println!("Fetched {} documents (offset: {})", docs.len(), offset);
offset += limit;
} else {
break;
}
}

Ok(())
}
use hlquery_rust_client::Client;
use std::collections::HashMap;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new("http://localhost:9200", None)?;

// Complex search with filters and sorting
let mut params = HashMap::new();
params.insert("q".to_string(), "laptop OR computer".to_string());
params.insert("query_by".to_string(), "title,description".to_string());
params.insert("filter_by".to_string(), "price:[500 TO 1500]".to_string());
params.insert("sort_by".to_string(), "price:asc,created_at:desc".to_string());
params.insert("limit".to_string(), "20".to_string());
params.insert("offset".to_string(), "0".to_string());

let results = client.search("products", params).await?;

if results.is_success() {
let body = results.get_body();

if let Some(hits) = body.get("hits").and_then(|h| h.as_array()) {
println!("Found {} results", hits.len());

for hit in hits {
if let Some(doc) = hit.get("document") {
println!("Document: {}", doc);
}
if let Some(score) = hit.get("_text_match") {
println!("Relevance Score: {}", score);
}
}
}

if let Some(found) = body.get("found") {
println!("Total found: {}", found);
}
}

Ok(())
}

Best Practices

1. Error Handling

Always handle errors properly:

match client.health().await {
Ok(response) => {
if response.is_success() {
// Handle success
} else {
// Handle API error
eprintln!("API error: {}", response.get_status_code());
}
}
Err(e) => {
// Handle client error
eprintln!("Client error: {}", e);
}
}

2. Resource Management

The client is thread-safe and can be shared:

use std::sync::Arc;

let client = Arc::new(Client::new("http://localhost:9200", None)?);

// Share across tasks
let client1 = Arc::clone(&client);
let client2 = Arc::clone(&client);

tokio::spawn(async move {
client1.health().await?;
Ok::<(), Box<dyn std::error::Error>>(())
});

tokio::spawn(async move {
client2.stats().await?;
Ok::<(), Box<dyn std::error::Error>>(())
});

3. Timeout Configuration

Set appropriate timeouts for your use case:

let mut options = HashMap::new();
options.insert("timeout".to_string(), "60000".to_string()); // 60 seconds
let client = Client::new("http://localhost:9200", Some(options))?;

4. Validation

Validate inputs before making requests:

use hlquery_rust_client::utils::validator;

// Validate before API call
validator::validate_collection_name("my_collection")?;
let result = client.get_collection("my_collection").await?;

5. Async Best Practices

Use async/await properly:

// Good: Sequential operations
let collection = client.get_collection("products").await?;
let documents = client.list_documents("products", None).await?;

// Good: Parallel operations
let (health, stats) = tokio::join!(
client.health(),
client.stats()
);

API Reference Summary

Client Methods

  • new(base_url, options) - Create new client
  • set_auth_token(token, method) - Set authentication
  • clear_auth() - Clear authentication
  • health() - Health check
  • stats() - Server statistics
  • info() - Server information
  • list_collections(offset, limit) - List collections
  • get_collection(name) - Get collection
  • get_collection_fields(name) - Get collection fields
  • list_documents(collection, params) - List documents
  • get_document(collection, id) - Get document
  • search(collection, params) - Search
  • vector_search(collection, params) - Vector search

Collections API

  • list(offset, limit) - List collections
  • get(name) - Get collection
  • get_fields(name) - Compatibility alias for collection metadata (including fields)
  • language(name) - Get collection language information
  • create(name, schema) - Create collection
  • update(name, schema) - Update collection
  • delete(name) - Delete collection

Documents API

  • list(collection, params) - List documents
  • get(collection, id) - Get document
  • add(collection, document) - Add document
  • update(collection, id, document) - Update document
  • delete(collection, id) - Delete document
  • import(collection, documents) - Bulk import
  • delete_by_filter(collection, filter) - Delete by filter

Search API

  • search(collection, params) - Search
  • vector_search(collection, params) - Vector search
  • multi_search(searches) - Multi-collection search using POST
  • multi_search_with_method(searches, method) - Multi-collection search using GET or POST

Additional Resources

Support

For issues, questions, or contributions:

  • GitHub: Report issues in the main hlquery repository
  • Documentation: See API Clients Overview
  • Examples: Check api/rust/examples/ directory