Skip to main content

couchbase_core/
searchcomponent.rs

1/*
2 *
3 *  * Copyright (c) 2025 Couchbase, Inc.
4 *  *
5 *  * Licensed under the Apache License, Version 2.0 (the "License");
6 *  * you may not use this file except in compliance with the License.
7 *  * You may obtain a copy of the License at
8 *  *
9 *  *    http://www.apache.org/licenses/LICENSE-2.0
10 *  *
11 *  * Unless required by applicable law or agreed to in writing, software
12 *  * distributed under the License is distributed on an "AS IS" BASIS,
13 *  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 *  * See the License for the specific language governing permissions and
15 *  * limitations under the License.
16 *
17 */
18
19use crate::authenticator::Authenticator;
20use crate::componentconfigs::NetworkAndCanonicalEndpoint;
21use crate::diagnosticscomponent::PingSearchReportOptions;
22use crate::error::ErrorKind;
23use crate::httpcomponent::{HttpComponent, HttpComponentState};
24use crate::httpx::client::Client;
25use crate::httpx::request::Auth;
26use crate::mgmtx::node_target::NodeTarget;
27use crate::options::search::SearchOptions;
28use crate::options::search_management::{
29    AllowQueryingOptions, AnalyzeDocumentOptions, DeleteIndexOptions, DisallowQueryingOptions,
30    EnsureIndexOptions, FreezePlanOptions, GetAllIndexesOptions, GetIndexOptions,
31    GetIndexedDocumentsCountOptions, PauseIngestOptions, ResumeIngestOptions, UnfreezePlanOptions,
32    UpsertIndexOptions,
33};
34use crate::results::pingreport::{EndpointPingReport, PingState};
35use crate::results::search::SearchResultStream;
36use crate::retry::{orchestrate_retries, RetryManager, RetryRequest, RetryStrategy};
37use crate::retrybesteffort::ExponentialBackoffCalculator;
38use crate::searchx::document_analysis::DocumentAnalysis;
39use crate::searchx::ensure_index_helper::EnsureIndexHelper;
40use crate::searchx::index::Index;
41use crate::searchx::mgmt_options::{EnsureIndexPollOptions, PingOptions};
42use crate::searchx::search::Search;
43use crate::service_type::ServiceType;
44use crate::tracingcomponent::TracingComponent;
45use crate::{error, httpx};
46use arc_swap::ArcSwap;
47use futures::future::join_all;
48use futures::StreamExt;
49use std::collections::HashMap;
50use std::future::Future;
51use std::ops::Sub;
52use std::sync::Arc;
53use std::time::Duration;
54use tokio::select;
55use tracing::debug;
56
57pub(crate) struct SearchComponent<C: Client> {
58    id: String,
59    http_component: HttpComponent<C>,
60    tracing: Arc<TracingComponent>,
61
62    retry_manager: Arc<RetryManager>,
63
64    state: ArcSwap<SearchComponentState>,
65}
66
67#[derive(Debug)]
68pub(crate) struct SearchComponentState {
69    pub vector_search_enabled: bool,
70}
71
72pub(crate) struct SearchComponentConfig {
73    pub endpoints: HashMap<String, NetworkAndCanonicalEndpoint>,
74    pub authenticator: Authenticator,
75
76    pub vector_search_enabled: bool,
77}
78
79#[derive(Debug)]
80pub(crate) struct SearchComponentOptions {
81    pub id: String,
82    pub user_agent: String,
83}
84
85impl<C: Client + 'static> SearchComponent<C> {
86    pub fn new(
87        retry_manager: Arc<RetryManager>,
88        http_client: Arc<C>,
89        tracing: Arc<TracingComponent>,
90        config: SearchComponentConfig,
91        opts: SearchComponentOptions,
92    ) -> Self {
93        Self {
94            id: opts.id,
95            http_component: HttpComponent::new(
96                ServiceType::SEARCH,
97                opts.user_agent,
98                http_client,
99                HttpComponentState::new(config.endpoints, config.authenticator),
100            ),
101            tracing,
102            retry_manager,
103            state: ArcSwap::new(Arc::new(SearchComponentState {
104                vector_search_enabled: config.vector_search_enabled,
105            })),
106        }
107    }
108
109    pub fn reconfigure(&self, config: SearchComponentConfig) {
110        debug!(
111            "Search component {} updating endpoints to {:?}",
112            self.id,
113            &config.endpoints.keys().collect::<Vec<_>>()
114        );
115
116        self.http_component.reconfigure(HttpComponentState::new(
117            config.endpoints,
118            config.authenticator,
119        ));
120
121        self.state.swap(Arc::new(SearchComponentState {
122            vector_search_enabled: config.vector_search_enabled,
123        }));
124    }
125
126    pub async fn query(&self, opts: SearchOptions) -> error::Result<SearchResultStream> {
127        if (opts.knn.is_some() || opts.knn_operator.is_some())
128            && !self.state.load().vector_search_enabled
129        {
130            return Err(ErrorKind::FeatureNotAvailable {
131                feature: "Vector Search".to_string(),
132                msg: "vector queries are available from Couchbase Server 7.6.0 and above"
133                    .to_string(),
134            }
135            .into());
136        }
137        let retry_info = RetryRequest::new("search_query", true);
138
139        let retry = opts.retry_strategy.clone();
140        let endpoint = opts.endpoint.clone();
141        let copts = opts.into();
142
143        orchestrate_retries(self.retry_manager.clone(), retry, retry_info, async || {
144            self.http_component
145                .orchestrate_endpoint(
146                    endpoint.clone(),
147                    async |client: Arc<C>,
148                           endpoint_id: String,
149                           endpoint: String,
150                           canonical_endpoint: String,
151                           auth: Auth| {
152                        let res = match (Search::<C> {
153                            http_client: client,
154                            user_agent: self.http_component.user_agent().to_string(),
155                            endpoint: endpoint.clone(),
156                            canonical_endpoint,
157                            auth,
158
159                            vector_search_enabled: self.state.load().vector_search_enabled,
160                            tracing: self.tracing.clone(),
161                        }
162                        .query(&copts)
163                        .await)
164                        {
165                            Ok(r) => r,
166                            Err(e) => return Err(ErrorKind::Search(e).into()),
167                        };
168
169                        Ok(SearchResultStream {
170                            inner: res,
171                            endpoint,
172                        })
173                    },
174                )
175                .await
176        })
177        .await
178    }
179
180    pub async fn get_index(&self, opts: &GetIndexOptions<'_>) -> error::Result<Index> {
181        let retry_info = RetryRequest::new("search_get_index", true);
182        let retry = opts.retry_strategy.clone();
183        let endpoint = opts.endpoint;
184        let copts = opts.into();
185
186        self.orchestrate_mgmt_call(
187            retry,
188            retry_info,
189            endpoint.map(|e| e.to_string()),
190            async |search| {
191                search
192                    .get_index(&copts)
193                    .await
194                    .map_err(|e| ErrorKind::Search(e).into())
195            },
196        )
197        .await
198    }
199
200    pub async fn get_all_indexes(
201        &self,
202        opts: &GetAllIndexesOptions<'_>,
203    ) -> error::Result<Vec<Index>> {
204        let retry_info = RetryRequest::new("search_get_all_indexes", true);
205        let retry = opts.retry_strategy.clone();
206        let endpoint = opts.endpoint;
207        let copts = opts.into();
208
209        self.orchestrate_mgmt_call(
210            retry,
211            retry_info,
212            endpoint.map(|e| e.to_string()),
213            async |search| {
214                search
215                    .get_all_indexes(&copts)
216                    .await
217                    .map_err(|e| ErrorKind::Search(e).into())
218            },
219        )
220        .await
221    }
222
223    pub async fn upsert_index(&self, opts: &UpsertIndexOptions<'_>) -> error::Result<()> {
224        let retry_info = RetryRequest::new("search_upsert_index", true);
225        let retry = opts.retry_strategy.clone();
226        let endpoint = opts.endpoint;
227        let copts = opts.into();
228
229        self.orchestrate_no_res_mgmt_call(
230            retry,
231            retry_info,
232            endpoint.map(|e| e.to_string()),
233            async |search| {
234                search
235                    .upsert_index(&copts)
236                    .await
237                    .map_err(|e| ErrorKind::Search(e).into())
238            },
239        )
240        .await
241    }
242
243    pub async fn delete_index(&self, opts: &DeleteIndexOptions<'_>) -> error::Result<()> {
244        let retry_info = RetryRequest::new("search_delete_index", true);
245        let retry = opts.retry_strategy.clone();
246        let endpoint = opts.endpoint;
247        let copts = opts.into();
248
249        self.orchestrate_no_res_mgmt_call(
250            retry,
251            retry_info,
252            endpoint.map(|e| e.to_string()),
253            async |search| {
254                search
255                    .delete_index(&copts)
256                    .await
257                    .map_err(|e| ErrorKind::Search(e).into())
258            },
259        )
260        .await
261    }
262
263    pub async fn analyze_document(
264        &self,
265        opts: &AnalyzeDocumentOptions<'_>,
266    ) -> error::Result<DocumentAnalysis> {
267        let retry_info = RetryRequest::new("search_analyze_document", true);
268        let retry = opts.retry_strategy.clone();
269        let endpoint = opts.endpoint;
270        let copts = opts.into();
271
272        self.orchestrate_mgmt_call(
273            retry,
274            retry_info,
275            endpoint.map(|e| e.to_string()),
276            async |search| {
277                search
278                    .analyze_document(&copts)
279                    .await
280                    .map_err(|e| ErrorKind::Search(e).into())
281            },
282        )
283        .await
284    }
285
286    pub async fn get_indexed_documents_count(
287        &self,
288        opts: &GetIndexedDocumentsCountOptions<'_>,
289    ) -> error::Result<u64> {
290        let retry_info = RetryRequest::new("search_get_indexed_documents_count", true);
291        let retry = opts.retry_strategy.clone();
292        let endpoint = opts.endpoint;
293        let copts = opts.into();
294
295        self.orchestrate_mgmt_call(
296            retry,
297            retry_info,
298            endpoint.map(|e| e.to_string()),
299            async |search| {
300                search
301                    .get_indexed_documents_count(&copts)
302                    .await
303                    .map_err(|e| ErrorKind::Search(e).into())
304            },
305        )
306        .await
307    }
308
309    pub async fn pause_ingest(&self, opts: &PauseIngestOptions<'_>) -> error::Result<()> {
310        let retry_info = RetryRequest::new("search_pause_ingest", true);
311        let retry = opts.retry_strategy.clone();
312        let endpoint = opts.endpoint;
313        let copts = opts.into();
314
315        self.orchestrate_no_res_mgmt_call(
316            retry,
317            retry_info,
318            endpoint.map(|e| e.to_string()),
319            async |search| {
320                search
321                    .pause_ingest(&copts)
322                    .await
323                    .map_err(|e| ErrorKind::Search(e).into())
324            },
325        )
326        .await
327    }
328
329    pub async fn resume_ingest(&self, opts: &ResumeIngestOptions<'_>) -> error::Result<()> {
330        let retry_info = RetryRequest::new("search_resume_ingest", true);
331        let retry = opts.retry_strategy.clone();
332        let endpoint = opts.endpoint;
333        let copts = opts.into();
334
335        self.orchestrate_no_res_mgmt_call(
336            retry,
337            retry_info,
338            endpoint.map(|e| e.to_string()),
339            async |search| {
340                search
341                    .resume_ingest(&copts)
342                    .await
343                    .map_err(|e| ErrorKind::Search(e).into())
344            },
345        )
346        .await
347    }
348
349    pub async fn allow_querying(&self, opts: &AllowQueryingOptions<'_>) -> error::Result<()> {
350        let retry_info = RetryRequest::new("search_allow_querying", true);
351        let retry = opts.retry_strategy.clone();
352        let endpoint = opts.endpoint;
353        let copts = opts.into();
354
355        self.orchestrate_no_res_mgmt_call(
356            retry,
357            retry_info,
358            endpoint.map(|e| e.to_string()),
359            async |search| {
360                search
361                    .allow_querying(&copts)
362                    .await
363                    .map_err(|e| ErrorKind::Search(e).into())
364            },
365        )
366        .await
367    }
368
369    pub async fn disallow_querying(&self, opts: &DisallowQueryingOptions<'_>) -> error::Result<()> {
370        let retry_info = RetryRequest::new("search_disallow_querying", true);
371        let retry = opts.retry_strategy.clone();
372        let endpoint = opts.endpoint;
373        let copts = opts.into();
374
375        self.orchestrate_no_res_mgmt_call(
376            retry,
377            retry_info,
378            endpoint.map(|e| e.to_string()),
379            async |search| {
380                search
381                    .disallow_querying(&copts)
382                    .await
383                    .map_err(|e| ErrorKind::Search(e).into())
384            },
385        )
386        .await
387    }
388
389    pub async fn freeze_plan(&self, opts: &FreezePlanOptions<'_>) -> error::Result<()> {
390        let retry_info = RetryRequest::new("search_freeze_plan", true);
391        let retry = opts.retry_strategy.clone();
392        let endpoint = opts.endpoint;
393        let copts = opts.into();
394
395        self.orchestrate_no_res_mgmt_call(
396            retry,
397            retry_info,
398            endpoint.map(|e| e.to_string()),
399            async |search| {
400                search
401                    .freeze_plan(&copts)
402                    .await
403                    .map_err(|e| ErrorKind::Search(e).into())
404            },
405        )
406        .await
407    }
408
409    pub async fn unfreeze_plan(&self, opts: &UnfreezePlanOptions<'_>) -> error::Result<()> {
410        let retry_info = RetryRequest::new("search_unfreeze_plan", true);
411        let retry = opts.retry_strategy.clone();
412        let endpoint = opts.endpoint;
413        let copts = opts.into();
414
415        self.orchestrate_no_res_mgmt_call(
416            retry,
417            retry_info,
418            endpoint.map(|e| e.to_string()),
419            async |search| {
420                search
421                    .unfreeze_plan(&copts)
422                    .await
423                    .map_err(|e| ErrorKind::Search(e).into())
424            },
425        )
426        .await
427    }
428
429    pub async fn ensure_index(&self, opts: &EnsureIndexOptions<'_>) -> error::Result<()> {
430        let mut helper = EnsureIndexHelper::new(
431            self.http_component.user_agent(),
432            opts.index_name,
433            opts.bucket_name,
434            opts.scope_name,
435            opts.on_behalf_of_info,
436        );
437
438        let backoff = ExponentialBackoffCalculator::new(
439            Duration::from_millis(100),
440            Duration::from_millis(1000),
441            1.5,
442        );
443
444        self.http_component
445            .ensure_resource(backoff, async |client: Arc<C>, targets: Vec<NodeTarget>| {
446                helper
447                    .clone()
448                    .poll(&EnsureIndexPollOptions {
449                        client,
450                        targets,
451                        desired_state: opts.desired_state,
452                    })
453                    .await
454                    .map_err(error::Error::from)
455            })
456            .await
457    }
458
459    pub async fn ping_all_endpoints(
460        &self,
461        on_behalf_of: Option<&httpx::request::OnBehalfOfInfo>,
462    ) -> error::Result<Vec<error::Result<()>>> {
463        let (client, targets) = self.http_component.get_all_targets::<NodeTarget>(&[])?;
464
465        let copts = PingOptions { on_behalf_of };
466
467        let mut handles = Vec::with_capacity(targets.len());
468        let user_agent = self.http_component.user_agent().to_string();
469        for target in targets {
470            let user_agent = user_agent.clone();
471            let client = Search::<C> {
472                http_client: client.clone(),
473                user_agent,
474                endpoint: target.endpoint,
475                canonical_endpoint: target.canonical_endpoint,
476                auth: target.auth,
477                vector_search_enabled: false,
478                tracing: self.tracing.clone(),
479            };
480
481            let handle = self.ping_one(client, copts.clone());
482
483            handles.push(handle);
484        }
485
486        let results = join_all(handles).await;
487
488        Ok(results)
489    }
490
491    pub async fn create_ping_report(
492        &self,
493        opts: PingSearchReportOptions<'_>,
494    ) -> error::Result<Vec<EndpointPingReport>> {
495        let (client, targets) = self.http_component.get_all_targets::<NodeTarget>(&[])?;
496
497        let copts = PingOptions {
498            on_behalf_of: opts.on_behalf_of,
499        };
500        let timeout = opts.timeout;
501
502        let mut handles = Vec::with_capacity(targets.len());
503        let user_agent = self.http_component.user_agent().to_string();
504        for target in targets {
505            let user_agent = user_agent.clone();
506            let client = Search::<C> {
507                http_client: client.clone(),
508                user_agent,
509                endpoint: target.endpoint,
510                canonical_endpoint: target.canonical_endpoint,
511                auth: target.auth,
512
513                vector_search_enabled: self.state.load().vector_search_enabled,
514                tracing: self.tracing.clone(),
515            };
516
517            let handle = self.create_one_report(client, timeout, copts.clone());
518
519            handles.push(handle);
520        }
521
522        let reports = join_all(handles).await;
523
524        Ok(reports)
525    }
526
527    async fn ping_one(&self, client: Search<C>, opts: PingOptions<'_>) -> error::Result<()> {
528        client.ping(&opts).await.map_err(error::Error::from)
529    }
530
531    async fn create_one_report(
532        &self,
533        client: Search<C>,
534        timeout: Duration,
535        opts: PingOptions<'_>,
536    ) -> EndpointPingReport {
537        let start = std::time::Instant::now();
538        let res = select! {
539            e = tokio::time::sleep(timeout) => {
540                return EndpointPingReport {
541                    remote: client.endpoint,
542                    error: None,
543                    latency: std::time::Instant::now().sub(start),
544                    id: None,
545                    namespace: None,
546                    state: PingState::Timeout,
547                }
548            }
549            r = client.ping(&opts) => r.map_err(error::Error::from),
550        };
551        let end = std::time::Instant::now();
552
553        let (error, state) = match res {
554            Ok(_) => (None, PingState::Ok),
555            Err(e) => (Some(e), PingState::Error),
556        };
557
558        EndpointPingReport {
559            remote: client.endpoint,
560            error,
561            latency: end.sub(start),
562            id: None,
563            namespace: None,
564            state,
565        }
566    }
567
568    async fn orchestrate_mgmt_call<Fut, Resp>(
569        &self,
570        retry_strategy: Arc<dyn RetryStrategy>,
571        retry_info: RetryRequest,
572        endpoint: Option<String>,
573        operation: impl Fn(Search<C>) -> Fut + Send + Sync,
574    ) -> error::Result<Resp>
575    where
576        Resp: Send + Sync,
577        Fut: Future<Output = error::Result<Resp>> + Send,
578        C: Client,
579    {
580        orchestrate_retries(
581            self.retry_manager.clone(),
582            retry_strategy,
583            retry_info,
584            async || {
585                self.http_component
586                    .orchestrate_endpoint(
587                        endpoint.clone(),
588                        async |client: Arc<C>,
589                               endpoint_id: String,
590                               endpoint: String,
591                               canonical_endpoint: String,
592                               auth: Auth| {
593                            operation(Search::<C> {
594                                http_client: client,
595                                user_agent: self.http_component.user_agent().to_string(),
596                                endpoint,
597                                canonical_endpoint,
598                                auth,
599
600                                vector_search_enabled: self.state.load().vector_search_enabled,
601                                tracing: self.tracing.clone(),
602                            })
603                            .await
604                        },
605                    )
606                    .await
607            },
608        )
609        .await
610    }
611
612    async fn orchestrate_no_res_mgmt_call<Fut>(
613        &self,
614        retry_strategy: Arc<dyn RetryStrategy>,
615        retry_info: RetryRequest,
616        endpoint: Option<String>,
617        operation: impl Fn(Search<C>) -> Fut + Send + Sync,
618    ) -> error::Result<()>
619    where
620        Fut: Future<Output = error::Result<()>> + Send,
621        C: Client,
622    {
623        orchestrate_retries(
624            self.retry_manager.clone(),
625            retry_strategy,
626            retry_info,
627            async || {
628                self.http_component
629                    .orchestrate_endpoint(
630                        endpoint.clone(),
631                        async |client: Arc<C>,
632                               endpoint_id: String,
633                               endpoint: String,
634                               canonical_endpoint: String,
635                               auth: Auth| {
636                            operation(Search::<C> {
637                                http_client: client,
638                                user_agent: self.http_component.user_agent().to_string(),
639                                endpoint,
640                                canonical_endpoint,
641                                auth,
642
643                                vector_search_enabled: self.state.load().vector_search_enabled,
644                                tracing: self.tracing.clone(),
645                            })
646                            .await
647                        },
648                    )
649                    .await
650            },
651        )
652        .await
653    }
654}