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
Basic Search
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?;
Vector Search
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?;
Multi-Collection Search
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(¶ms)?;
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(())
}
Example 3: Advanced Search
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 clientset_auth_token(token, method)- Set authenticationclear_auth()- Clear authenticationhealth()- Health checkstats()- Server statisticsinfo()- Server informationlist_collections(offset, limit)- List collectionsget_collection(name)- Get collectionget_collection_fields(name)- Get collection fieldslist_documents(collection, params)- List documentsget_document(collection, id)- Get documentsearch(collection, params)- Searchvector_search(collection, params)- Vector search
Collections API
list(offset, limit)- List collectionsget(name)- Get collectionget_fields(name)- Compatibility alias for collection metadata (including fields)language(name)- Get collection language informationcreate(name, schema)- Create collectionupdate(name, schema)- Update collectiondelete(name)- Delete collection
Documents API
list(collection, params)- List documentsget(collection, id)- Get documentadd(collection, document)- Add documentupdate(collection, id, document)- Update documentdelete(collection, id)- Delete documentimport(collection, documents)- Bulk importdelete_by_filter(collection, filter)- Delete by filter
Search API
search(collection, params)- Searchvector_search(collection, params)- Vector searchmulti_search(searches)- Multi-collection search using POSTmulti_search_with_method(searches, method)- Multi-collection search using GET or POST
Additional Resources
- Rust Client Documentation - Detailed client library docs
- API Overview - Complete API reference
- Examples - More code examples
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