{"id":17130270,"url":"https://github.com/aparo/opensearch-client-rs","last_synced_at":"2025-10-12T05:31:02.447Z","repository":{"id":223017849,"uuid":"758986308","full_name":"aparo/opensearch-client-rs","owner":"aparo","description":"Strong Opinionated OpenSearch Client for Rust","archived":false,"fork":false,"pushed_at":"2024-11-15T19:29:20.000Z","size":1409,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-01-01T16:41:37.670Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Rust","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/aparo.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"licenserc.toml","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2024-02-17T16:49:48.000Z","updated_at":"2024-11-19T17:03:34.000Z","dependencies_parsed_at":null,"dependency_job_id":"df372760-cf65-40e2-81eb-9b9836e2651f","html_url":"https://github.com/aparo/opensearch-client-rs","commit_stats":null,"previous_names":["aparo/opensearch-client-rs"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aparo%2Fopensearch-client-rs","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aparo%2Fopensearch-client-rs/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aparo%2Fopensearch-client-rs/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/aparo%2Fopensearch-client-rs/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/aparo","download_url":"https://codeload.github.com/aparo/opensearch-client-rs/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":236169074,"owners_count":19106100,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-14T19:11:55.025Z","updated_at":"2025-10-12T05:31:02.441Z","avatar_url":"https://github.com/aparo.png","language":"Rust","funding_links":[],"categories":[],"sub_categories":[],"readme":"# OpenSearch Client for Rust\n\n[![Crates.io](https://img.shields.io/crates/v/opensearch-client)](https://crates.io/crates/opensearch-client)\n[![License](https://img.shields.io/crates/l/opensearch-client)](https://crates.io/crates/opensearch-client)\n[![Documentation](https://docs.rs/opensearch-client/badge.svg)](https://docs.rs/opensearch-client)\n\nA comprehensive Rust client library for OpenSearch with a strongly typed DSL, CLI tools, and extensive API coverage.\n\n## 🚀 Features\n\n- **Strongly Typed DSL**: Type-safe query building with compile-time guarantees\n- **Comprehensive API Coverage**: Support for search, indices, cluster management, and more\n- **CLI Tools**: Command-line interface for cluster management and data operations\n- **Async/Await Support**: Built on modern async Rust with tokio\n- **Production Ready**: Includes retry logic, connection pooling, and error handling\n- **Extensible**: Modular design with feature flags for optional functionality\n\n## 📦 Project Structure\n\nThis workspace contains several crates:\n\n- **`opensearch-client`**: Core client library with API bindings\n- **`opensearch-dsl`**: Strongly typed query DSL\n- **`opensearch-cli`**: Command-line tools for cluster management\n- **`opensearch-testcontainer`**: Testing utilities with container support\n\n## 🛠 Installation\n\nAdd the dependencies to your `Cargo.toml`:\n\n```toml\n[dependencies]\nopensearch-client = \"0.3\"\nopensearch-dsl = \"0.3\"\n```\n\nThe macro support is included by default - no additional features needed!\n\n## 🔧 Quick Start\n\n### Basic Client Usage\n\n```rust\nuse opensearch_client::{ConfigurationBuilder, OsClient};\nuse url::Url;\n\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    // Create client configuration\n    let url = Url::parse(\"http://localhost:9200\")?;\n    let config = ConfigurationBuilder::new()\n        .base_url(url)\n        .basic_auth(\"admin\".to_string(), \"admin\".to_string())\n        .build();\n    \n    let client = OsClient::new(config);\n    \n    // Get cluster health\n    let health = client.cluster().health().await?;\n    println!(\"Cluster status: {:?}\", health);\n    \n    Ok(())\n}\n```\n\n### Query Building with DSL\n\n```rust\nuse opensearch_dsl::*;\n\nlet query = Search::new()\n    .source(false)\n    .from(0)\n    .size(10)\n    .query(\n        Query::bool()\n            .must(Query::term(\"status\", \"published\"))\n            .filter(Query::range(\"date\").gte(\"2023-01-01\"))\n    )\n    .aggregations([\n        (\"status_count\", Aggregation::terms(\"status\")),\n        (\"avg_score\", Aggregation::avg(\"score\"))\n    ]);\n\n// Execute the search\nlet response = client.search(\u0026query).index(\"my_index\").await?;\n```\n\n### Document Modeling with Macros\n\nThe `opensearch-client` provides a powerful macro system for creating strongly-typed document models that automatically implement the `Document` trait.\n\n**Quick Navigation:**\n- [Basic Document Definition](#basic-document-definition)\n- [Working with Documents](#working-with-document-models)\n- [CRUD Operations](#crud-operations)\n- [Querying and Search](#querying-and-search)\n- [Field Type Mapping](#field-type-mapping)\n- [Macro Attributes](#macro-attributes)\n- [Field Introspection](#field-introspection)\n- [Nested Documents](#nested-documents)\n- [Best Practices](#best-practices)\n- [API Reference](#-document-trait-api-reference)\n\n#### Basic Document Definition\n\n```rust\nuse opensearch_client::{Document, OpenSearch};\nuse serde::{Deserialize, Serialize};\n\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n#[os(index = \"users\")]\npub struct User {\n    #[os(id)]\n    pub id: String,\n    pub name: String,\n    pub email: String,\n    pub age: u32,\n    pub active: bool,\n    pub profile: UserProfile,  // Nested document\n    pub tags: Vec\u003cString\u003e,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n#[os(index = \"user_profiles\")]\npub struct UserProfile {\n    #[os(id)]\n    pub id: String,\n    pub bio: String,\n    pub website: Option\u003cString\u003e,\n    pub location: Address,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n#[os(index = \"addresses\")]\npub struct Address {\n    pub street: String,\n    pub city: String,\n    pub country: String,\n    pub zipcode: u32,\n}\n```\n\n### Working with Document Models\n\nOnce you've defined your models, you can use them with full type safety:\n\n```rust\n#[tokio::main]\nasync fn main() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    // Create a new user\n    let user = User {\n        id: \"user123\".to_string(),\n        name: \"John Doe\".to_string(),\n        email: \"john@example.com\".to_string(),\n        age: 30,\n        active: true,\n        profile: UserProfile {\n            id: \"profile123\".to_string(),\n            bio: \"Software developer\".to_string(),\n            website: Some(\"https://johndoe.dev\".to_string()),\n            location: Address {\n                street: \"123 Main St\".to_string(),\n                city: \"San Francisco\".to_string(),\n                country: \"USA\".to_string(),\n                zipcode: 94105,\n            },\n        },\n        tags: vec![\"developer\".to_string(), \"rust\".to_string()],\n    };\n\n    // Save to OpenSearch\n    let response = user.save().await?;\n    println!(\"Saved user with ID: {}\", response.id);\n\n    // Retrieve by ID\n    let retrieved_user = User::get(\"user123\").await?;\n    println!(\"Retrieved: {}\", retrieved_user.name);\n\n    // Update the user\n    User::update(\"user123\", \u0026json!({\n        \"age\": 31,\n        \"active\": false\n    })).await?;\n\n    // Search for users\n    let search_results = User::find(\n        Search::new()\n            .query(Query::term(\"active\", true))\n            .size(10)\n    ).await?;\n\n    for hit in search_results.hits.hits {\n        if let Some(user) = hit.source {\n            println!(\"Found user: {}\", user.name);\n        }\n    }\n\n    // Count active users\n    let count = User::count(Some(Query::term(\"active\", true))).await?;\n    println!(\"Active users: {}\", count);\n\n    Ok(())\n}\n```\n\n### Document Trait Features\n\nThe `Document` trait provides a comprehensive API for working with OpenSearch documents:\n\n#### Core Methods\n\n```rust\n// Get index name and field metadata\nlet index = User::index_name();              // \"users\"\nlet fields = User::columns();                // Vec\u003cField\u003e with type info\n\n// Instance methods\nlet user = User::get(\"123\").await?;\nlet user_id = user.id();                     // Get document ID\n```\n\n#### CRUD Operations\n\n```rust\n// Create/Update\nlet response = user.save().await?;\n\n// Read\nlet user = User::get(\"123\").await?;\nlet maybe_user = User::find_one(\n    Search::new().query(Query::term(\"email\", \"john@example.com\"))\n).await?;\n\n// Update with partial data\nUser::update(\"123\", \u0026json!({\"age\": 31})).await?;\n\n// Refresh instance from database\nuser.refresh().await?;\n\n// Delete\nUser::delete(\"123\").await?;\n```\n\n#### Querying and Search\n\n```rust\n// Find with complex queries\nlet results = User::find(\n    Search::new()\n        .query(\n            Query::bool()\n                .must(Query::term(\"active\", true))\n                .filter(Query::range(\"age\").gte(18).lte(65))\n        )\n        .sort([(\"name.keyword\", \"asc\")])\n        .from(0)\n        .size(20)\n).await?;\n\n// Find all documents\nlet all_users = User::find_all(Some(100)).await?;\n\n// Count documents\nlet active_count = User::count(Some(Query::term(\"active\", true))).await?;\nlet total_count = User::count(None).await?;\n```\n\n### Field Type Mapping\n\nThe macro automatically maps Rust types to OpenSearch field types:\n\n| Rust Type | OpenSearch Type | Field Type | Notes |\n|-----------|-----------------|------------|-------|\n| `String`, `\u0026str` | `text` | `string` | Full-text searchable |\n| `u32`, `i32`, `u64`, `i64` | `long` | `number` | Aggregatable, sortable |\n| `f32`, `f64` | `double` | `number` | Aggregatable, sortable |\n| `bool` | `boolean` | `boolean` | Aggregatable, sortable |\n| `Vec\u003cT\u003e` | (inner type) | (inner type) | Arrays of the inner type |\n| `Option\u003cT\u003e` | (inner type) | (inner type) | Nullable fields |\n| Custom structs | `object` | `object` | Nested documents |\n\n### Macro Attributes\n\n#### Index Configuration\n\n```rust\n#[derive(OpenSearch)]\n#[os(index = \"my_index\")]  // Required: specify the index name\npub struct MyDocument {\n    // fields...\n}\n```\n\n#### ID Field\n\n```rust\n#[derive(OpenSearch)]\n#[os(index = \"users\")]\npub struct User {\n    #[os(id)]              // Mark this field as the document ID\n    pub id: String,\n    // other fields...\n}\n```\n\nThe ID field must be of type `String` and will be used as the document's unique identifier in OpenSearch.\n\n### Field Introspection\n\nThe generated `columns()` method provides detailed field metadata:\n\n```rust\nlet fields = User::columns();\nfor field in fields {\n    println!(\"Field: {}\", field.name);\n    println!(\"  Type: {}\", field.field_type);\n    println!(\"  OpenSearch Type: {}\", field.os_type);\n    println!(\"  Aggregatable: {}\", field.aggregatable);\n    println!(\"  Searchable: {}\", field.searchable);\n}\n```\n\nThis metadata can be used for:\n- Dynamic query building\n- Index mapping generation\n- API documentation\n- Query validation\n\n### Nested Documents\n\nWhen using custom types as fields, the macro recognizes them as nested documents:\n\n```rust\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n#[os(index = \"users\")]\npub struct User {\n    #[os(id)]\n    pub id: String,\n    pub profile: UserProfile,  // This becomes a nested object\n}\n\n// The UserProfile fields are accessible through its own columns() method\nlet profile_fields = UserProfile::columns();\n```\n\nThis enables complex document structures while maintaining type safety and field introspection capabilities.\n\n### Best Practices\n\n#### Required Derives\nAlways include these derives for full functionality:\n\n```rust\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n```\n\n- `Debug` - For debugging and logging\n- `Clone` - Required by the Document trait\n- `Serialize`/`Deserialize` - For JSON conversion with serde\n- `OpenSearch` - The derive macro that implements Document\n\n#### ID Field Guidelines\n\n```rust\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n#[os(index = \"my_index\")]\npub struct MyDoc {\n    #[os(id)]\n    pub id: String,  // Must be String type\n    // other fields...\n}\n```\n\n- The ID field must be of type `String`\n- Exactly one field must be marked with `#[os(id)]`\n- The ID field is used for document identification in OpenSearch\n\n#### Index Naming\n\n```rust\n#[os(index = \"users\")]           // Good: lowercase, descriptive\n#[os(index = \"blog_posts\")]      // Good: snake_case for multi-word\n#[os(index = \"product-reviews\")] // Good: kebab-case alternative\n```\n\n- Use lowercase names\n- Avoid special characters except hyphens and underscores\n- Choose descriptive, consistent naming patterns\n\n#### Type Compatibility\n\n```rust\n// Supported primitive types\npub struct MyDoc {\n    pub text_field: String,\n    pub number_field: u32,\n    pub flag_field: bool,\n    pub optional_field: Option\u003cString\u003e,\n    pub list_field: Vec\u003cString\u003e,\n    \n    // Nested documents (must also derive OpenSearch)\n    pub nested_doc: OtherDoc,\n    \n    // Custom types for complex data\n    pub metadata: serde_json::Value,  // For dynamic content\n}\n```\n\n### Index Management\n\n```rust\n// Create an index\nclient.indices()\n    .create(\"my_index\")\n    .mappings(json!({\n        \"properties\": {\n            \"title\": {\"type\": \"text\"},\n            \"timestamp\": {\"type\": \"date\"}\n        }\n    }))\n    .await?;\n\n// Index a document\nclient.index(\"my_index\")\n    .id(\"1\")\n    .body(json!({\n        \"title\": \"Hello OpenSearch\",\n        \"timestamp\": \"2023-10-08T12:00:00Z\"\n    }))\n    .await?;\n```\n\n## 🖥 CLI Tools\n\nThe project includes a powerful CLI tool for cluster management:\n\n```bash\n# Install the CLI\ncargo install opensearch-cli\n\n# List all indices\nopensearch-cli list-indices\n\n# Dump cluster metadata\nopensearch-cli dump-metadata --output ./backup\n\n# Restore metadata\nopensearch-cli restore-metadata --input ./backup\n\n# Copy index between clusters\nopensearch-cli copy-index --remote my_index --target-index new_index\n```\n\n### CLI Configuration\n\nSet environment variables or use command-line flags:\n\n```bash\nexport OPENSEARCH_URL=\"https://my-cluster.example.com:9200\"\nexport OPENSEARCH_USER=\"admin\"\nexport OPENSEARCH_PASSWORD=\"password\"\n\n# Or use flags\nopensearch-cli --server https://my-cluster.example.com:9200 --user admin list-indices\n```\n\n## 🏗 Architecture\n\n### Client Library (`opensearch-client`)\n\nThe core client provides:\n- **HTTP Transport**: Built on reqwest with middleware support\n- **Authentication**: Basic auth, API keys, and custom auth\n- **API Modules**: Organized by OpenSearch API categories\n- **Error Handling**: Comprehensive error types and retry logic\n- **Connection Management**: Connection pooling and keep-alive\n\n### DSL Library (`opensearch-dsl`)\n\nThe DSL provides type-safe query building:\n- **Query Types**: Match, term, bool, range, and more\n- **Aggregations**: Bucket, metric, and pipeline aggregations\n- **Response Parsing**: Strongly typed response structures\n- **Validation**: Compile-time query validation\n\n### CLI Tools (`opensearch-cli`)\n\nCommand-line utilities for:\n- **Metadata Management**: Index templates, pipelines, components\n- **Data Operations**: Dump, restore, and copy indices\n- **Cluster Management**: Health checks and monitoring\n- **Remote Operations**: Multi-cluster support\n\n## 🔧 Configuration\n\n### Client Configuration\n\n```rust\nuse opensearch_client::ConfigurationBuilder;\n\nlet config = ConfigurationBuilder::new()\n    .base_url(url)\n    .basic_auth(username, password)\n    .timeout(Duration::from_secs(30))\n    .retry_attempts(3)\n    .build();\n```\n\n### Feature Flags\n\nEnable only the features you need:\n\n```toml\n[dependencies]\nopensearch-client = { version = \"0.3\", features = [\n    \"search\",\n    \"indices\", \n    \"cluster\",\n    \"ml\"\n] }\n```\n\nAvailable features:\n- `search` - Search APIs (default)\n- `indices` - Index management (default)\n- `cluster` - Cluster APIs (default)\n- `ml` - Machine learning APIs (default)\n- `security` - Security APIs\n- `tools` - Utility tools\n\n## 📚 Examples\n\n### Bulk Operations\n\n```rust\nuse opensearch_client::bulk::*;\n\nlet mut bulk = BulkOperation::new();\nbulk.index(\"my_index\", \"1\", json!({\"field\": \"value1\"}));\nbulk.index(\"my_index\", \"2\", json!({\"field\": \"value2\"}));\nbulk.delete(\"my_index\", \"3\");\n\nlet response = client.bulk(bulk).await?;\n```\n\n### Document Modeling Examples\n\n```rust\nuse opensearch_client::{Document, OpenSearch};\nuse serde::{Deserialize, Serialize};\n\n// Blog post with tags and metadata\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n#[os(index = \"blog_posts\")]\npub struct BlogPost {\n    #[os(id)]\n    pub id: String,\n    pub title: String,\n    pub content: String,\n    pub author: Author,\n    pub tags: Vec\u003cString\u003e,\n    pub published: bool,\n    pub created_at: String, // ISO 8601 datetime\n    pub view_count: u32,\n}\n\n#[derive(Debug, Clone, Serialize, Deserialize, OpenSearch)]\n#[os(index = \"authors\")]\npub struct Author {\n    #[os(id)]\n    pub id: String,\n    pub name: String,\n    pub email: String,\n    pub bio: String,\n}\n\n// Example of a non-Document struct for complex data\n#[derive(Debug, Clone, Serialize, Deserialize)]\npub struct AuthorProfile {\n    pub bio: String,\n    pub website: Option\u003cString\u003e,\n    pub social_links: Vec\u003cString\u003e,\n}\n\n// Using the models\nasync fn blog_example() -\u003e Result\u003c(), Box\u003cdyn std::error::Error\u003e\u003e {\n    let post = BlogPost {\n        id: \"post-123\".to_string(),\n        title: \"Getting Started with OpenSearch in Rust\".to_string(),\n        content: \"In this post, we'll explore...\".to_string(),\n        author: Author {\n            id: \"author-456\".to_string(),\n            name: \"Jane Developer\".to_string(),\n            email: \"jane@example.com\".to_string(),\n            bio: \"Full-stack developer passionate about Rust\".to_string(),\n        },\n        tags: vec![\"rust\".to_string(), \"opensearch\".to_string(), \"tutorial\".to_string()],\n        published: true,\n        created_at: \"2023-10-08T12:00:00Z\".to_string(),\n        view_count: 0,\n    };\n\n    // Save the blog post\n    post.save().await?;\n\n    // Search for published posts by tag\n    let rust_posts = BlogPost::find(\n        Search::new()\n            .query(\n                Query::bool()\n                    .must(Query::term(\"published\", true))\n                    .must(Query::term(\"tags\", \"rust\"))\n            )\n            .sort([(\"created_at\", \"desc\")])\n    ).await?;\n\n    Ok(())\n}\n```\n\n### Aggregations\n\n```rust\nlet search = Search::new()\n    .aggregations([\n        (\"sales_over_time\", \n            Aggregation::date_histogram(\"timestamp\", \"month\")\n                .sub_aggregation(\"total_sales\", Aggregation::sum(\"amount\"))\n        ),\n        (\"top_products\",\n            Aggregation::terms(\"product_id\")\n                .size(10)\n                .order([(\"total_sales\", \"desc\")])\n                .sub_aggregation(\"total_sales\", Aggregation::sum(\"amount\"))\n        )\n    ]);\n```\n\n### Stream Processing\n\n```rust\nlet mut stream = client.search_stream(query).index(\"logs\").scroll(\"1m\");\nwhile let Some(response) = stream.next().await {\n    for hit in response?.hits.hits {\n        // Process each document\n        println!(\"{:?}\", hit.source);\n    }\n}\n```\n\n## 📖 Document Trait API Reference\n\nThe `Document` trait provides a complete ORM-like interface for working with OpenSearch documents. All methods are automatically implemented when you use the `#[derive(OpenSearch)]` macro.\n\n### Static Methods\n\n| Method | Signature | Description |\n|--------|-----------|-------------|\n| `index_name()` | `fn index_name() -\u003e \u0026'static str` | Returns the index name configured with `#[os(index = \"...\")]` |\n| `columns()` | `fn columns() -\u003e Vec\u003cField\u003e` | Returns field metadata for introspection and mapping |\n| `get(id)` | `async fn get(id: \u0026str) -\u003e Result\u003cSelf, Error\u003e` | Fetch a document by ID |\n| `delete(id)` | `async fn delete(id: \u0026str) -\u003e Result\u003cDocumentDeleteResponse, Error\u003e` | Delete a document by ID |\n| `update(id, doc)` | `async fn update(id: \u0026str, partial_doc: \u0026Value) -\u003e Result\u003cIndexResponse, Error\u003e` | Update document with partial data |\n| `find(search)` | `async fn find(search: Search) -\u003e Result\u003cTypedSearchResult\u003cSelf\u003e, Error\u003e` | Search with custom query |\n| `find_all(limit)` | `async fn find_all(limit: Option\u003cusize\u003e) -\u003e Result\u003cTypedSearchResult\u003cSelf\u003e, Error\u003e` | Find all documents with optional limit |\n| `find_one(search)` | `async fn find_one(search: Search) -\u003e Result\u003cOption\u003cSelf\u003e, Error\u003e` | Find single document matching query |\n| `count(query)` | `async fn count(query: Option\u003cQuery\u003e) -\u003e Result\u003cu32, Error\u003e` | Count documents matching query |\n\n### Instance Methods\n\n| Method | Signature | Description |\n|--------|-----------|-------------|\n| `id()` | `fn id(\u0026self) -\u003e \u0026str` | Get the document's ID (from field marked with `#[os(id)]`) |\n| `save()` | `async fn save(\u0026self) -\u003e Result\u003cIndexResponse, Error\u003e` | Create or update this document |\n| `refresh()` | `async fn refresh(\u0026mut self) -\u003e Result\u003c(), Error\u003e` | Reload this instance from OpenSearch |\n\n### Field Metadata\n\nThe `Field` struct returned by `columns()` contains:\n\n```rust\npub struct Field {\n    pub name: String,           // Field name\n    pub field_type: String,     // Human-readable type (string, number, boolean, object)\n    pub os_type: String,        // OpenSearch mapping type (text, long, boolean, object)\n    pub aggregatable: bool,     // Can be used in aggregations\n    pub searchable: bool,       // Can be searched/filtered\n    pub sub_fields: Vec\u003cBox\u003cField\u003e\u003e, // Nested fields (for object types)\n}\n```\n\n## 🧪 Testing\n\nRun the test suite:\n\n```bash\n# Unit tests\ncargo test\n\n# Integration tests (requires OpenSearch running)\ncargo test --features integration-tests\n\n# Test with specific OpenSearch version\ndocker run -d -p 9200:9200 opensearchproject/opensearch:3.2.0\ncargo test\n```\n\n## 🤝 Contributing\n\nWe welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for details.\n\n### Development Setup\n\n1. Clone the repository:\n```bash\ngit clone https://github.com/aparo/opensearch-client-rs.git\ncd opensearch-client-rs\n```\n\n2. Install dependencies:\n```bash\ncargo build\n```\n\n3. Run tests:\n```bash\ncargo test\n```\n\n4. Start OpenSearch for integration tests:\n```bash\ndocker run -d -p 9200:9200 \\\n  -e \"discovery.type=single-node\" \\\n  -e \"DISABLE_SECURITY_PLUGIN=true\" \\\n  opensearchproject/opensearch:latest\n```\n\n## 📄 License\n\nThis project is licensed under the Apache 2.0 License - see the [LICENSE](LICENSE) file for details.\n\n## 🔗 Related Projects\n\n- [OpenSearch](https://opensearch.org/) - The OpenSearch search engine\n- [elasticsearch-dsl-rs](https://github.com/vinted/elasticsearch-dsl-rs) - Original Elasticsearch DSL inspiration\n- [opensearch-rs](https://github.com/opensearch-project/opensearch-rs) - Alternative Rust client\n\n## 📞 Support\n\n- [Documentation](https://docs.rs/opensearch-client)\n- [GitHub Issues](https://github.com/aparo/opensearch-client-rs/issues)\n- [Discussions](https://github.com/aparo/opensearch-client-rs/discussions)\n\n---\n\nMade with ❤️ by the OpenSearch Rust community","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faparo%2Fopensearch-client-rs","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faparo%2Fopensearch-client-rs","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faparo%2Fopensearch-client-rs/lists"}