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}