Skip to main content

typesense/client/
mod.rs

1//! # A batteries-included, multi-node-aware client for the Typesense API.
2//!
3//! This module provides the main `Client` for interacting with a Typesense cluster.
4//! It is designed for resilience and ease of use, incorporating features like
5//! automatic failover, health checks, and a structured, ergonomic API.
6//!
7//! ## Key Features:
8//! - **Multi-Node Configuration**: Automatically manages connections to multiple Typesense nodes.
9//! - **Health Checks & Failover**: Monitors node health and seamlessly fails over to healthy nodes upon encountering server or network errors.
10//! - **Nearest Node Priority**: Can be configured to always prioritize a specific nearest node to reduce latency.
11//! - **Built-in Retries**: Handles transient network errors with an exponential backoff policy for each node.
12//!
13//! ## Example Usage
14//!
15//! The following example demonstrates how to use the client in a standard
16//! server-side **Tokio** environment.
17//!
18//! ```no_run
19//! #[cfg(not(target_family = "wasm"))]
20//! {
21//! use typesense::{Client, models, ExponentialBackoff};
22//! use std::time::Duration;
23//!
24//! #[tokio::main]
25//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
26//!     let client = Client::builder()
27//!         .nodes(vec!["http://localhost:8108"])
28//!         .api_key("xyz")
29//!         .healthcheck_interval(Duration::from_secs(60))
30//!         .retry_policy(ExponentialBackoff::builder().build_with_max_retries(3))
31//!         .build()
32//!         .unwrap();
33//!
34//!     // Retrieve details for a collection
35//!     let collection = client.collection_schemaless("products").retrieve().await?;
36//!     println!("Collection Name: {}", collection.name);
37//!
38//!     // Search for a document
39//!     let search_params = models::SearchParameters {
40//!         q: Some("phone".into()),
41//!         query_by: Some("name".into()),
42//!         ..Default::default()
43//!     };
44//!
45//!     let search_results = client
46//!         .collection_schemaless("products")
47//!         .documents()
48//!         .search(search_params)
49//!         .await?;
50//!
51//!     println!("Found {} hits.", search_results.found.unwrap_or(0));
52//!     Ok(())
53//! }
54//! }
55//! ```
56//! ---
57//!
58//! ### WebAssembly (Wasm) Usage
59//!
60//! When compiling for a WebAssembly target (`wasm32-unknown-unknown`),
61//! Tokio-based features such as middleware and retries are **not available**.
62//!
63//! Example:
64//!
65//! ```no_run
66//! #[cfg(target_family = "wasm")]
67//! {
68//! use typesense::{Client, models};
69//! use reqwest::Url;
70//! use std::time::Duration;
71//! use wasm_bindgen_futures::spawn_local;
72//!
73//! fn main() {
74//!     spawn_local(async {
75//!         let client = Client::builder()
76//!             .nodes(vec!["http://localhost:8108"])
77//!             .api_key("xyz")
78//!             .healthcheck_interval(Duration::from_secs(60))
79//!             // .retry_policy(...)  <-- not supported in Wasm
80//!             .build()
81//!             .unwrap();
82//!
83//!         // Retrieve details for a collection
84//!         match client.collection_schemaless("products").retrieve().await {
85//!             Ok(collection) => println!("Collection Name: {}", collection.name),
86//!             Err(e) => eprintln!("Error retrieving collection: {}", e),
87//!         }
88//!
89//!         // Search for a document
90//!         let search_params = models::SearchParameters {
91//!             q: Some("phone".into()),
92//!             query_by: Some("name".into()),
93//!             ..Default::default()
94//!         };
95//!
96//!         match client.collection_schemaless("products").documents().search(search_params).await {
97//!             Ok(search_results) => {
98//!                 println!("Found {} hits.", search_results.found.unwrap_or(0));
99//!             }
100//!             Err(e) => eprintln!("Error searching documents: {}", e),
101//!         }
102//!     });
103//! }
104//! }
105//! ```
106mod alias;
107mod aliases;
108mod analytics;
109mod collection;
110mod collections;
111mod conversations;
112mod curation_set;
113mod curation_sets;
114mod key;
115mod keys;
116mod multi_search;
117mod operations;
118mod preset;
119mod presets;
120mod retry_policy;
121mod stemming;
122mod stopword;
123mod stopwords;
124mod synonym_set;
125mod synonym_sets;
126
127use crate::{Error, traits::Document};
128use alias::Alias;
129use aliases::Aliases;
130use analytics::Analytics;
131use collection::Collection;
132use collections::Collections;
133use conversations::Conversations;
134use curation_set::CurationSet;
135use curation_sets::CurationSets;
136use key::Key;
137use keys::Keys;
138use operations::Operations;
139use preset::Preset;
140use presets::Presets;
141use retry_policy::ClientRetryPolicy;
142use stemming::Stemming;
143use stopword::Stopword;
144use stopwords::Stopwords;
145use synonym_set::SynonymSet;
146use synonym_sets::SynonymSets;
147
148#[cfg(not(target_arch = "wasm32"))]
149use reqwest_middleware::ClientBuilder as ReqwestMiddlewareClientBuilder;
150#[cfg(not(target_arch = "wasm32"))]
151use reqwest_retry::RetryTransientMiddleware;
152pub use reqwest_retry::policies::ExponentialBackoff;
153
154use ::std::{
155    borrow::Cow,
156    future::Future,
157    sync::{
158        RwLock,
159        atomic::{AtomicBool, AtomicUsize, Ordering},
160    },
161};
162use serde::{Serialize, de::DeserializeOwned};
163use typesense_codegen::apis::{self, configuration};
164use web_time::{Duration, Instant};
165
166/// Wraps api call in `client::execute()`
167#[macro_export]
168macro_rules! execute_wrapper {
169    ($self:ident, $call:expr) => {
170        $self.client.execute($call).await
171    };
172    ($self:ident, $call:expr, $params:ident) => {
173        $self
174            .client
175            .execute(
176                |config: &typesense_codegen::apis::configuration::Configuration| {
177                    $call(config, &$params)
178                },
179            )
180            .await
181    };
182}
183
184/// Configuration for a single Typesense node.
185///
186/// Use this to customize the HTTP client for specific nodes,
187/// for example to add custom TLS root certificates or configure proxies.
188///
189/// For simple cases, you can pass a plain URL string to the builder's
190/// `.nodes()` method, which will be automatically converted.
191///
192/// # Examples
193///
194/// ```
195/// use typesense::NodeConfig;
196///
197/// // Simple URL (same as passing a string directly)
198/// let node = NodeConfig::new("https://node1.example.com");
199///
200/// // With custom HTTP client configuration
201/// // (add timeouts, headers, TLS, etc. on native targets)
202/// let node = NodeConfig::new("https://node2.example.com")
203///     .http_builder(|builder| {
204///         // This closure receives a `reqwest::ClientBuilder` and must return it.
205///         // You can call any supported builder methods here; for example,
206///         // `builder.connect_timeout(...)` on native targets.
207///         builder
208///     });
209/// ```
210pub struct NodeConfig {
211    url: String,
212    http_builder: Option<Box<dyn FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder>>,
213}
214
215impl std::fmt::Debug for NodeConfig {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        f.debug_struct("NodeConfig")
218            .field("url", &self.url)
219            .field("http_builder", &self.http_builder.as_ref().map(|_| ".."))
220            .finish()
221    }
222}
223
224impl NodeConfig {
225    /// Creates a new `NodeConfig` with the given URL.
226    pub fn new(url: impl Into<String>) -> Self {
227        Self {
228            url: url.into(),
229            http_builder: None,
230        }
231    }
232
233    /// Sets a custom HTTP client builder for this node.
234    ///
235    /// The closure receives a default [`reqwest::ClientBuilder`] and should return
236    /// a configured builder. This is useful for adding custom TLS certificates,
237    /// proxies, or other reqwest settings.
238    ///
239    /// When not set, a default builder with a 5-second connect timeout is used
240    /// (native targets only; WASM uses the browser's defaults).
241    ///
242    /// # Examples
243    ///
244    /// ```no_run
245    /// #[cfg(not(target_family = "wasm"))]
246    /// {
247    /// use typesense::NodeConfig;
248    ///
249    /// # fn cert() -> reqwest::Certificate { unimplemented!() }
250    /// let cert = cert();
251    /// // You can capture arbitrary configuration here (certs, proxies, etc.)
252    /// // and apply it to the `reqwest::ClientBuilder` on platforms that support it.
253    /// let node = NodeConfig::new("https://secure.example.com")
254    ///     .http_builder(move |builder| {
255    ///         builder
256    ///             .add_root_certificate(cert)
257    ///             .connect_timeout(std::time::Duration::from_secs(10))
258    ///     });
259    /// }
260    /// ```
261    ///
262    /// # Multiple nodes with the same configuration
263    ///
264    /// The closure is `FnOnce`, so it is consumed when the HTTP client for that node
265    /// is built. To use the same configuration (e.g. the same TLS certificate) for
266    /// multiple nodes, clone the value once per node when building the configs:
267    ///
268    /// ```no_run
269    /// #[cfg(not(target_family = "wasm"))]
270    /// {
271    /// use typesense::{Client, NodeConfig};
272    ///
273    /// # fn cert() -> reqwest::Certificate { unimplemented!() }
274    /// let cert = cert();
275    /// let nodes = ["https://node1:8108", "https://node2:8108"]
276    ///     .into_iter()
277    ///     .map(|url| {
278    ///         let cert_for_node = cert.clone();
279    ///         NodeConfig::new(url).http_builder(move |b| {
280    ///             b.add_root_certificate(cert_for_node)
281    ///         })
282    ///     })
283    ///     .collect::<Vec<_>>();
284    /// let _client = Client::builder().nodes(nodes).api_key("key").build();
285    /// }
286    /// ```
287    pub fn http_builder(
288        mut self,
289        f: impl FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder + 'static,
290    ) -> Self {
291        self.http_builder = Some(Box::new(f));
292        self
293    }
294}
295
296impl From<String> for NodeConfig {
297    fn from(url: String) -> Self {
298        Self::new(url)
299    }
300}
301
302impl<'a> From<&'a str> for NodeConfig {
303    fn from(url: &'a str) -> Self {
304        Self::new(url)
305    }
306}
307
308impl From<reqwest::Url> for NodeConfig {
309    fn from(url: reqwest::Url) -> Self {
310        Self::new(url)
311    }
312}
313
314// This is an internal detail to track the state of each node.
315#[derive(Debug)]
316struct Node {
317    config: configuration::Configuration,
318    is_healthy: AtomicBool,
319    last_accessed: RwLock<Instant>,
320}
321
322impl Node {
323    /// Sets the health status of the node
324    #[inline]
325    fn set_health(&self, is_healthy: bool) {
326        *self.last_accessed.write().unwrap() = Instant::now();
327        self.is_healthy.store(is_healthy, Ordering::Relaxed);
328    }
329}
330
331/// The main entry point for all interactions with the Typesense API.
332///
333/// The client manages connections to multiple nodes and provides access to different
334/// API resource groups (namespaces) like `collections`, `documents`, and `operations`.
335#[derive(Debug)]
336pub struct Client {
337    nodes: Vec<Node>,
338    is_nearest_node_set: bool,
339    healthcheck_interval: Duration,
340    current_node_index: AtomicUsize,
341}
342
343#[bon::bon]
344impl Client {
345    /// Creates a new `Client`.
346    ///
347    /// Returns an error if the configuration contains no nodes. Default values:
348    /// - **nearest_node**: None.
349    /// - **healthcheck_interval**: 60 seconds.
350    /// - **retry_policy**: Exponential backoff with a maximum of 3 retries. (disabled on WASM)
351    /// - **http_builder**: An `FnOnce(reqwest::ClientBuilder) -> reqwest::ClientBuilder` closure
352    ///   for per-node HTTP client customization (optional, via [`NodeConfig`]).
353    ///
354    /// When no custom `http_builder` is configured, a default `reqwest::ClientBuilder` with
355    /// a 5-second connect timeout is used (native targets only).
356    #[builder]
357    pub fn new(
358        /// The Typesense API key used for authentication.
359        #[builder(into)]
360        api_key: String,
361        /// A list of all nodes in the Typesense cluster.
362        ///
363        /// Accepts plain URL strings or [`NodeConfig`] instances for per-node
364        /// HTTP client customization.
365        #[builder(
366            with = |iter: impl IntoIterator<Item = impl Into<NodeConfig>>|
367                iter.into_iter().map(Into::into).collect::<Vec<NodeConfig>>()
368        )]
369        nodes: Vec<NodeConfig>,
370        #[builder(into)]
371        /// An optional, preferred node to try first for every request.
372        /// This is for your server-side load balancer.
373        /// Do not add this node to all nodes list, should be a separate one.
374        nearest_node: Option<NodeConfig>,
375        #[builder(default = Duration::from_secs(60))]
376        /// The duration after which an unhealthy node will be retried for requests.
377        healthcheck_interval: Duration,
378        #[builder(into, default)]
379        /// The retry policy for transient network errors on a *single* node.
380        retry_policy: ClientRetryPolicy,
381    ) -> Result<Self, &'static str> {
382        let is_nearest_node_set = nearest_node.is_some();
383
384        let nodes: Vec<_> = nodes
385            .into_iter()
386            .chain(nearest_node)
387            .map(|node_config| {
388                let builder = match node_config.http_builder {
389                    Some(f) => f(reqwest::Client::builder()),
390                    None => {
391                        let b = reqwest::Client::builder();
392                        #[cfg(not(target_arch = "wasm32"))]
393                        let b = b.connect_timeout(Duration::from_secs(5));
394                        b
395                    }
396                };
397
398                #[cfg(target_arch = "wasm32")]
399                let http_client = builder.build().expect("Failed to build reqwest client");
400
401                #[cfg(not(target_arch = "wasm32"))]
402                let mw_builder = ReqwestMiddlewareClientBuilder::new(
403                    builder.build().expect("Failed to build reqwest client"),
404                );
405
406                #[cfg(not(target_arch = "wasm32"))]
407                let http_client = match retry_policy {
408                    ClientRetryPolicy::Default(policy) => mw_builder
409                        .with(RetryTransientMiddleware::new_with_policy(policy))
410                        .build(),
411                    ClientRetryPolicy::Timed(policy) => mw_builder
412                        .with(RetryTransientMiddleware::new_with_policy(policy))
413                        .build(),
414                };
415
416                let mut url = node_config.url;
417                if url.len() > 1 && matches!(url.chars().last(), Some('/')) {
418                    url.pop();
419                }
420
421                let config = configuration::Configuration {
422                    base_path: url,
423                    api_key: Some(configuration::ApiKey {
424                        prefix: None,
425                        key: api_key.clone(),
426                    }),
427                    client: http_client,
428                    ..Default::default()
429                };
430
431                Node {
432                    config,
433                    is_healthy: AtomicBool::new(true),
434                    last_accessed: RwLock::new(Instant::now()),
435                }
436            })
437            .collect();
438
439        if nodes.is_empty() {
440            return Err("Configuration must include at least one node or a nearest_node.");
441        }
442
443        Ok(Self {
444            nodes,
445            is_nearest_node_set,
446            healthcheck_interval,
447            current_node_index: AtomicUsize::new(0),
448        })
449    }
450
451    /// Selects the next node to use for a request based on health and priority.
452    fn get_next_node(&self) -> &Node {
453        // if only one node (including nearest)
454        if self.nodes.len() == 1
455            && let Some(first) = self.nodes.first()
456        {
457            return first;
458        }
459
460        let (nodes_len, mut index) = if self.is_nearest_node_set {
461            let last_node_index = self.nodes.len() - 1;
462            (last_node_index, last_node_index)
463        } else {
464            (
465                self.nodes.len(),
466                self.current_node_index.fetch_add(1, Ordering::Relaxed) % self.nodes.len(),
467            )
468        };
469
470        for _ in 0..self.nodes.len() {
471            let node = &self.nodes[index];
472
473            if node.is_healthy.load(Ordering::Relaxed)
474                || node.last_accessed.read().unwrap().elapsed() >= self.healthcheck_interval
475            {
476                return node;
477            }
478            index = self.current_node_index.fetch_add(1, Ordering::Relaxed) % nodes_len;
479        }
480
481        // If all nodes are unhealthy and not due for a check, just pick the next one in the round-robin.
482        // This gives it a chance to prove it has recovered.
483        index = self.current_node_index.load(Ordering::Relaxed) % self.nodes.len();
484        &self.nodes[index]
485    }
486
487    /// For use in legacy APIs.
488    #[inline]
489    pub fn get_legacy_config(&self) -> &configuration::Configuration {
490        &self.get_next_node().config
491    }
492
493    /// The core execution method that handles multi-node failover and retries.
494    /// This internal method is called by all public API methods.
495    pub(super) async fn execute<F, Fut, T, E, 'a>(&'a self, api_call: F) -> Result<T, Error<E>>
496    where
497        F: Fn(&'a configuration::Configuration) -> Fut,
498        Fut: Future<Output = Result<T, apis::Error<E>>>,
499        E: std::fmt::Debug + 'static,
500        apis::Error<E>: std::error::Error + 'static,
501    {
502        let mut last_api_error: Option<apis::Error<E>> = None;
503        // Loop up to the total number of available nodes.
504        for _ in 0..self.nodes.len() {
505            let node = self.get_next_node();
506            match api_call(&node.config).await {
507                Ok(response) => {
508                    node.set_health(true);
509                    return Ok(response);
510                }
511                Err(e) => {
512                    if is_retriable(&e) {
513                        node.set_health(false);
514                        last_api_error = Some(e);
515                    } else {
516                        return Err(e.into());
517                    }
518                }
519            }
520        }
521
522        Err(crate::Error::AllNodesFailed {
523            source: last_api_error
524                .expect("No nodes were available to try, or all errors were non-retriable."),
525        })
526    }
527
528    /// Provides access to the collection aliases-related API endpoints.
529    ///
530    /// # Example
531    /// ```no_run
532    /// # #[cfg(not(target_family = "wasm"))]
533    /// # {
534    /// # use typesense::Client;
535    /// #
536    /// # #[tokio::main]
537    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
538    /// # let client = Client::builder()
539    /// #    .nodes(vec!["http://localhost:8108"])
540    /// #    .api_key("xyz")
541    /// #    .build()
542    /// #    .unwrap();
543    /// let all_aliases = client.aliases().retrieve().await.unwrap();
544    /// # Ok(())
545    /// # }
546    /// # }
547    /// ```
548    #[inline]
549    pub fn aliases(&self) -> Aliases<'_> {
550        Aliases::new(self)
551    }
552
553    /// Provides access to a specific collection alias's-related API endpoints.
554    /// # Example
555    /// ```no_run
556    /// # #[cfg(not(target_family = "wasm"))]
557    /// # {
558    /// # use typesense::Client;
559    /// #
560    /// # #[tokio::main]
561    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
562    /// # let client = Client::builder()
563    /// #    .nodes(vec!["http://localhost:8108"])
564    /// #    .api_key("xyz")
565    /// #    .build()
566    /// #    .unwrap();
567    /// let specific_alias = client.alias("books_alias").retrieve().await.unwrap();
568    /// # Ok(())
569    /// # }
570    /// # }
571    /// ```
572    #[inline]
573    pub fn alias<'a>(&'a self, alias_name: &'a str) -> Alias<'a> {
574        Alias::new(self, alias_name)
575    }
576
577    /// Provides access to the analytics API endpoints.
578    ///
579    /// # Example
580    /// ```no_run
581    /// # #[cfg(not(target_family = "wasm"))]
582    /// # {
583    /// # use typesense::Client;
584    /// #
585    /// # #[tokio::main]
586    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
587    /// # let client = Client::builder()
588    /// #    .nodes(vec!["http://localhost:8108"])
589    /// #    .api_key("xyz")
590    /// #    .build()
591    /// #    .unwrap();
592    /// let all_rules = client.analytics().rules().retrieve(None).await.unwrap();
593    /// # Ok(())
594    /// # }
595    /// # }
596    /// ```
597    #[inline]
598    pub fn analytics(&self) -> Analytics<'_> {
599        Analytics::new(self)
600    }
601
602    /// Provides access to API endpoints for managing collections like `create()` and `retrieve()`.
603    /// # Example
604    /// ```no_run
605    /// # #[cfg(not(target_family = "wasm"))]
606    /// # {
607    /// # use typesense::{Client, models::GetCollectionsParameters};
608    /// #
609    /// # #[tokio::main]
610    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
611    /// # let client = Client::builder()
612    /// #    .nodes(vec!["http://localhost:8108"])
613    /// #    .api_key("xyz")
614    /// #    .build()
615    /// #    .unwrap();
616    /// let all_collections = client.collections().retrieve(GetCollectionsParameters::default()).await.unwrap();
617    /// # Ok(())
618    /// # }
619    /// # }
620    /// ```
621    #[inline]
622    pub fn collections(&self) -> Collections<'_> {
623        Collections::new(self)
624    }
625
626    /// Provides access to API endpoints for a specific collection.
627    ///
628    /// This method returns a `Collection<D>` handle, which is generic over the type of document
629    /// stored in that collection.
630    ///
631    /// # Type Parameters
632    /// * `D` - The type of the documents in the collection. It must be serializable and deserializable.
633    ///
634    /// # Arguments
635    /// * `collection_name` - The name of the collection to interact with.
636    ///
637    /// # Example: Working with a strongly-typed collection
638    ///
639    /// When you want to retrieve or search for documents and have them automatically
640    /// deserialized into your own structs.
641    /// ```no_run
642    /// # #[cfg(not(target_family = "wasm"))]
643    /// # {
644    /// # use typesense::Client;
645    /// # use serde::{Serialize, Deserialize};
646    /// #
647    /// # #[derive(Serialize, Deserialize, Debug)]
648    /// # struct Book { id: String, title: String }
649    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
650    /// # let client = Client::builder()
651    /// #    .nodes(vec!["http://localhost:8108"])
652    /// #    .api_key("xyz")
653    /// #    .build()
654    /// #    .unwrap();
655    /// // Get a typed handle to the "books" collection
656    /// let books_collection = client.collection_named::<Book>("books");
657    ///
658    /// // Retrieve a single book, it returns `Result<Book, ...>`
659    /// let book = books_collection.document("123").retrieve().await?;
660    /// println!("Retrieved book: {:?}", book);
661    /// #
662    /// # Ok(())
663    /// # }
664    /// # }
665    /// ```
666    #[inline]
667    pub fn collection_named<'c, D>(
668        &'c self,
669        collection_name: impl Into<Cow<'c, str>>,
670    ) -> Collection<'c, D>
671    where
672        D: DeserializeOwned + Serialize,
673    {
674        Collection::new(self, collection_name)
675    }
676
677    /// Provides access to API endpoints for a specific collection.
678    ///
679    /// This method returns a `Collection<D>` handle, which is generic over the type of document
680    /// stored in that collection.
681    ///
682    /// # Type Parameters
683    /// * `D` - The type of the documents in the collection. It must be of trait Document.
684    ///
685    /// # Example: Working with a strongly-typed collection
686    ///
687    /// When you want to retrieve or search for documents and have them automatically
688    /// deserialized into your own structs.
689    /// ```no_run
690    /// # #[cfg(not(target_family = "wasm"))]
691    /// # {
692    /// # use typesense::{Client, Typesense};
693    /// # use serde::{Serialize, Deserialize};
694    /// #
695    /// # #[derive(Typesense, Serialize, Deserialize, Debug)]
696    /// # struct Book { id: String, title: String }
697    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
698    /// # let client = Client::builder()
699    /// #    .nodes(vec!["http://localhost:8108"])
700    /// #    .api_key("xyz")
701    /// #    .build()
702    /// #    .unwrap();
703    /// // Get a typed handle to the "books" collection
704    /// let books_collection = client.collection::<Book>();
705    ///
706    /// // Retrieve a single book, it returns `Result<Book, ...>`
707    /// let book = books_collection.document("123").retrieve().await?;
708    /// println!("Retrieved book: {:?}", book);
709    /// #
710    /// # Ok(())
711    /// # }
712    /// # }
713    /// ```
714    #[inline]
715    pub fn collection<'c, D>(&'c self) -> Collection<'c, D>
716    where
717        D: Document,
718    {
719        Collection::new(self, D::COLLECTION_NAME)
720    }
721
722    /// Provides access to API endpoints for a specific collection using schemaless `serde_json::Value` documents.
723    ///
724    /// This is the simplest way to interact with a collection when you do not need strong typing.
725    /// It is a convenient shorthand for `client.collection_named::<serde_json::Value>("...")`.
726    ///
727    /// The returned handle can be used for both document operations (which will return `serde_json::Value`)
728    /// and collection-level operations (like `.delete()` or `.retrieve()`).
729    ///
730    /// # Arguments
731    /// * `collection_name` - The name of the collection to interact with.
732    ///
733    /// # Example
734    /// ```no_run
735    /// # #[cfg(not(target_family = "wasm"))]
736    /// # {
737    /// # use typesense::Client;
738    /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
739    /// # let client = Client::builder()
740    /// #    .nodes(vec!["http://localhost:8108"])
741    /// #    .api_key("xyz")
742    /// #    .build()
743    /// #    .unwrap();
744    /// let products_collection = client.collection_schemaless("products");
745    /// #
746    /// # Ok(())
747    /// # }
748    /// # }
749    /// ```
750    #[inline]
751    pub fn collection_schemaless<'c>(
752        &'c self,
753        collection_name: impl Into<Cow<'c, str>>,
754    ) -> Collection<'c, serde_json::Value> {
755        Collection::new(self, collection_name)
756    }
757
758    /// Returns a `Conversations` instance for managing conversation models.
759    /// # Example
760    /// ```no_run
761    /// # #[cfg(not(target_family = "wasm"))]
762    /// # {
763    /// # use typesense::Client;
764    /// #
765    /// # #[tokio::main]
766    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
767    /// # let client = Client::builder()
768    /// #    .nodes(vec!["http://localhost:8108"])
769    /// #    .api_key("xyz")
770    /// #    .build()
771    /// #    .unwrap();
772    /// let conversation = client.conversations().models().retrieve().await.unwrap();
773    /// # Ok(())
774    /// # }
775    /// # }
776    /// ```
777    #[inline]
778    pub fn conversations(&self) -> Conversations<'_> {
779        Conversations::new(self)
780    }
781
782    /// Provides access to endpoints for managing curation sets.
783    /// # Example
784    /// ```no_run
785    /// # #[cfg(not(target_family = "wasm"))]
786    /// # {
787    /// # use typesense::Client;
788    /// #
789    /// # #[tokio::main]
790    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
791    /// # let client = Client::builder()
792    /// #    .nodes(vec!["http://localhost:8108"])
793    /// #    .api_key("xyz")
794    /// #    .build()
795    /// #    .unwrap();
796    /// let curation_sets = client.curation_sets().retrieve().await.unwrap();
797    /// # Ok(())
798    /// # }
799    /// # }
800    /// ```
801    #[inline]
802    pub fn curation_sets(&self) -> CurationSets<'_> {
803        CurationSets::new(self)
804    }
805
806    /// Provides access to endpoints for managing a specific curation set.
807    /// # Example
808    /// ```no_run
809    /// # #[cfg(not(target_family = "wasm"))]
810    /// # {
811    /// # use typesense::Client;
812    /// #
813    /// # #[tokio::main]
814    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
815    /// # let client = Client::builder()
816    /// #    .nodes(vec!["http://localhost:8108"])
817    /// #    .api_key("xyz")
818    /// #    .build()
819    /// #    .unwrap();
820    /// let curation_set = client.curation_set("curation_set_name").retrieve().await.unwrap();
821    /// # Ok(())
822    /// # }
823    /// # }
824    /// ```
825    #[inline]
826    pub fn curation_set<'a>(&'a self, curation_set_name: &'a str) -> CurationSet<'a> {
827        CurationSet::new(self, curation_set_name)
828    }
829
830    /// Provides access to endpoints for managing the collection of API keys.
831    ///
832    /// # Example
833    /// ```no_run
834    /// # #[cfg(not(target_family = "wasm"))]
835    /// # {
836    /// # use typesense::{Client, models};
837    /// #
838    /// # #[tokio::main]
839    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
840    /// # let client = Client::builder()
841    /// #    .nodes(vec!["http://localhost:8108"])
842    /// #    .api_key("xyz")
843    /// #    .build()
844    /// #    .unwrap();
845    /// # let schema = models::ApiKeySchema {
846    /// #     description: "Search-only key.".into(),
847    /// #     actions: vec!["documents:search".to_owned()],
848    /// #     collections: vec!["*".to_owned()],
849    /// #     ..Default::default()
850    /// # };
851    /// let new_key = client.keys().create(schema).await.unwrap();
852    /// # Ok(())
853    /// # }
854    /// # }
855    /// ```
856    #[inline]
857    pub fn keys(&self) -> Keys<'_> {
858        Keys::new(self)
859    }
860
861    /// Provides access to endpoints for managing a single API key.
862    ///
863    /// # Arguments
864    /// * `key_id` - The ID of the key to manage.
865    ///
866    /// # Example
867    /// ```no_run
868    /// # #[cfg(not(target_family = "wasm"))]
869    /// # {
870    /// # use typesense::Client;
871    /// #
872    /// # #[tokio::main]
873    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
874    /// # let client = Client::builder()
875    /// #    .nodes(vec!["http://localhost:8108"])
876    /// #    .api_key("xyz")
877    /// #    .build()
878    /// #    .unwrap();
879    /// let deleted_key = client.key(123).delete().await.unwrap();
880    /// # Ok(())
881    /// # }
882    /// # }
883    /// ```
884    #[inline]
885    pub fn key(&self, key_id: i64) -> Key<'_> {
886        Key::new(self, key_id)
887    }
888
889    /// Provides access to the multi search endpoint.
890    ///
891    /// # Example
892    /// ```no_run
893    /// # #[cfg(not(target_family = "wasm"))]
894    /// # {
895    /// # use typesense::{Client, models};
896    /// #
897    /// # #[tokio::main]
898    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
899    /// # let client = Client::builder()
900    /// #    .nodes(vec!["http://localhost:8108"])
901    /// #    .api_key("xyz")
902    /// #    .build()
903    /// #    .unwrap();
904    /// # let search_requests = models::MultiSearchBody {
905    /// #     searches: vec![models::MultiSearchCollectionParameters {
906    /// #         collection: Some("products".into()),
907    /// #         q: Some("phone".into()),
908    /// #         query_by: Some("name".into()),
909    /// #         ..Default::default()
910    /// #     }],
911    /// #     ..Default::default()
912    /// # };
913    /// # let common_params = models::MultiSearchParameters::default();
914    /// let results = client.multi_search().perform(search_requests, common_params).await.unwrap();
915    /// # Ok(())
916    /// # }
917    /// # }
918    /// ```
919    #[inline]
920    pub fn multi_search(&self) -> multi_search::MultiSearch<'_> {
921        multi_search::MultiSearch::new(self)
922    }
923
924    /// Provides access to top-level, non-namespaced API endpoints like `health` and `debug`.
925    /// # Example
926    /// ```no_run
927    /// # #[cfg(not(target_family = "wasm"))]
928    /// # {
929    /// # use typesense::Client;
930    /// #
931    /// # #[tokio::main]
932    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
933    /// # let client = Client::builder()
934    /// #    .nodes(vec!["http://localhost:8108"])
935    /// #    .api_key("xyz")
936    /// #    .build()
937    /// #    .unwrap();
938    /// let health = client.operations().health().await.unwrap();
939    /// # Ok(())
940    /// # }
941    /// # }
942    /// ```
943    #[inline]
944    pub fn operations(&self) -> Operations<'_> {
945        Operations::new(self)
946    }
947
948    /// Provides access to endpoints for managing all of your presets.
949    ///
950    /// # Example
951    /// ```no_run
952    /// # #[cfg(not(target_family = "wasm"))]
953    /// # {
954    /// # use typesense::Client;
955    /// #
956    /// # #[tokio::main]
957    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
958    /// # let client = Client::builder()
959    /// #    .nodes(vec!["http://localhost:8108"])
960    /// #    .api_key("xyz")
961    /// #    .build()
962    /// #    .unwrap();
963    /// let list_of_presets = client.presets().retrieve().await.unwrap();
964    /// # Ok(())
965    /// # }
966    /// # }
967    /// ```
968    #[inline]
969    pub fn presets(&self) -> Presets<'_> {
970        Presets::new(self)
971    }
972
973    /// Provides access to endpoints for managing a single preset.
974    ///
975    /// # Arguments
976    /// * `preset_id` - The ID of the preset to manage.
977    ///
978    /// # Example
979    /// ```no_run
980    /// # #[cfg(not(target_family = "wasm"))]
981    /// # {
982    /// # use typesense::Client;
983    /// #
984    /// # #[tokio::main]
985    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
986    /// # let client = Client::builder()
987    /// #    .nodes(vec!["http://localhost:8108"])
988    /// #    .api_key("xyz")
989    /// #    .build()
990    /// #    .unwrap();
991    /// let preset = client.preset("my-preset").retrieve().await.unwrap();
992    /// # Ok(())
993    /// # }
994    /// # }
995    /// ```
996    #[inline]
997    pub fn preset<'a>(&'a self, preset_id: &'a str) -> Preset<'a> {
998        Preset::new(self, preset_id)
999    }
1000
1001    /// Provides access to the stemming-related API endpoints.
1002    ///
1003    /// # Example
1004    ///
1005    /// ```no_run
1006    /// # #[cfg(not(target_family = "wasm"))]
1007    /// # {
1008    /// # use typesense::Client;
1009    /// #
1010    /// # #[tokio::main]
1011    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1012    /// # let client = Client::builder()
1013    /// #    .nodes(vec!["http://localhost:8108"])
1014    /// #    .api_key("xyz")
1015    /// #    .build()
1016    /// #    .unwrap();
1017    /// let response = client.stemming().dictionaries().retrieve().await.unwrap();
1018    /// # Ok(())
1019    /// # }
1020    /// # }
1021    /// ```
1022    #[inline]
1023    pub fn stemming(&self) -> Stemming<'_> {
1024        Stemming::new(self)
1025    }
1026
1027    /// Provides access to endpoints for managing the collection of stopwords sets.
1028    ///
1029    /// # Example
1030    /// ```no_run
1031    /// # #[cfg(not(target_family = "wasm"))]
1032    /// # {
1033    /// # use typesense::Client;
1034    /// #
1035    /// # #[tokio::main]
1036    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1037    /// # let client = Client::builder()
1038    /// #    .nodes(vec!["http://localhost:8108"])
1039    /// #    .api_key("xyz")
1040    /// #    .build()
1041    /// #    .unwrap();
1042    /// let all_stopwords = client.stopwords().retrieve().await.unwrap();
1043    /// # Ok(())
1044    /// # }
1045    /// # }
1046    /// ```
1047    #[inline]
1048    pub fn stopwords(&self) -> Stopwords<'_> {
1049        Stopwords::new(self)
1050    }
1051
1052    /// Provides access to endpoints for managing a single stopwords set.
1053    ///
1054    /// # Arguments
1055    /// * `set_id` - The ID of the stopwords set to manage.
1056    ///
1057    /// # Example
1058    /// ```no_run
1059    /// # #[cfg(not(target_family = "wasm"))]
1060    /// # {
1061    /// # use typesense::Client;
1062    /// #
1063    /// # #[tokio::main]
1064    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1065    /// # let client = Client::builder()
1066    /// #    .nodes(vec!["http://localhost:8108"])
1067    /// #    .api_key("xyz")
1068    /// #    .build()
1069    /// #    .unwrap();
1070    /// let my_stopword_set = client.stopword("common_words").retrieve().await.unwrap();
1071    /// # Ok(())
1072    /// # }
1073    /// # }
1074    /// ```
1075    #[inline]
1076    pub fn stopword<'a>(&'a self, set_id: &'a str) -> Stopword<'a> {
1077        Stopword::new(self, set_id)
1078    }
1079
1080    /// Provides access to endpoints for managing all synonym sets.
1081    ///
1082    /// # Example
1083    /// ```no_run
1084    /// # #[cfg(not(target_family = "wasm"))]
1085    /// # {
1086    /// # use typesense::Client;
1087    /// #
1088    /// # #[tokio::main]
1089    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1090    /// # let client = Client::builder()
1091    /// #    .nodes(vec!["http://localhost:8108"])
1092    /// #    .api_key("xyz")
1093    /// #    .build()
1094    /// #    .unwrap();
1095    /// let all_synonym_sets = client.synonym_sets().retrieve().await.unwrap();
1096    /// # Ok(())
1097    /// # }
1098    /// # }
1099    /// ```
1100    #[inline]
1101    pub fn synonym_sets(&self) -> SynonymSets<'_> {
1102        SynonymSets::new(self)
1103    }
1104
1105    /// Provides access to endpoints for managing a single synonym set.
1106    ///
1107    /// # Arguments
1108    /// * `synonym_set_name` - The name of the synonym set to manage.
1109    ///
1110    /// # Example
1111    /// ```no_run
1112    /// # #[cfg(not(target_family = "wasm"))]
1113    /// # {
1114    /// # use typesense::Client;
1115    /// #
1116    /// # #[tokio::main]
1117    /// # async fn main() -> Result<(), Box<dyn std::error::Error>> {
1118    /// # let client = Client::builder()
1119    /// #    .nodes(vec!["http://localhost:8108"])
1120    /// #    .api_key("xyz")
1121    /// #    .build()
1122    /// #    .unwrap();
1123    /// let my_synonym_set = client.synonym_set("synonym_set_name").retrieve().await.unwrap();
1124    /// # Ok(())
1125    /// # }
1126    /// # }
1127    /// ```
1128    #[inline]
1129    pub fn synonym_set<'a>(&'a self, synonym_set_name: &'a str) -> SynonymSet<'a> {
1130        SynonymSet::new(self, synonym_set_name)
1131    }
1132}
1133
1134/// A helper function to determine if an error is worth retrying on another node.
1135fn is_retriable<E>(error: &apis::Error<E>) -> bool
1136where
1137    E: std::fmt::Debug + 'static,
1138    apis::Error<E>: std::error::Error + 'static,
1139{
1140    match error {
1141        // Server-side errors (5xx) indicate a problem with the node, so we should try another.
1142        apis::Error::ResponseError(content) => content.status.is_server_error(),
1143
1144        // Underlying reqwest errors (e.g., connection refused) are retriable.
1145        apis::Error::Reqwest(_) => true,
1146
1147        // Network-level errors from middleware are always retriable.
1148        #[cfg(not(target_arch = "wasm32"))]
1149        apis::Error::ReqwestMiddleware(_) => true,
1150
1151        // Client-side (4xx) or parsing errors are not retriable as the request is likely invalid.
1152        _ => false,
1153    }
1154}