Skip to main content

oramacore_client/
collection.rs

1//! Collection management and search functionality.
2
3use std::collections::HashMap;
4use std::sync::Arc;
5
6use reqwest::Client;
7use serde::{Deserialize, Serialize};
8
9use crate::auth::{ApiKeyAuth, Auth, AuthConfig, JwtAuth, Target};
10use crate::client::{ApiKeyPosition, ClientRequest, OramaClient};
11use crate::error::Result;
12use crate::stream_manager::OramaCoreStream;
13use crate::types::*;
14use crate::utils::{current_time_millis, format_duration};
15
16const DEFAULT_READER_URL: &str = "https://collections.orama.com";
17const DEFAULT_JWT_URL: &str = "https://app.orama.com/api/user/jwt";
18
19/// Configuration for CollectionManager
20#[derive(Debug, Clone)]
21pub struct CollectionManagerConfig {
22    pub collection_id: String,
23    pub api_key: String,
24    pub cluster: Option<ClusterConfig>,
25    pub auth_jwt_url: Option<String>,
26}
27
28/// Cluster configuration
29#[derive(Debug, Clone)]
30pub struct ClusterConfig {
31    pub writer_url: Option<String>,
32    pub read_url: Option<String>,
33}
34
35/// NLP search parameters
36#[derive(Debug, Clone, Serialize)]
37pub struct NlpSearchParams {
38    pub query: String,
39    #[serde(rename = "LLMConfig", skip_serializing_if = "Option::is_none")]
40    pub llm_config: Option<LlmConfig>,
41    #[serde(rename = "userID", skip_serializing_if = "Option::is_none")]
42    pub user_id: Option<String>,
43}
44
45/// Index creation parameters
46#[derive(Debug, Clone, Serialize)]
47pub struct CreateIndexParams {
48    #[serde(skip_serializing_if = "Option::is_none")]
49    pub id: Option<String>,
50    #[serde(skip_serializing_if = "Option::is_none")]
51    pub embeddings: Option<serde_json::Value>, /* Can be "automatic", "all_properties", or array
52                                                * of strings */
53}
54
55/// Hook configuration for insertion
56#[derive(Debug, Clone, Serialize)]
57pub struct AddHookConfig {
58    pub name: Hook,
59    pub code: String,
60}
61
62/// Response from hook insertion
63#[derive(Debug, Clone, Deserialize)]
64pub struct NewHookResponse {
65    #[serde(rename = "hookID")]
66    pub hook_id: String,
67    pub code: String,
68}
69
70/// Execute tools request body
71#[derive(Debug, Clone, Serialize)]
72pub struct ExecuteToolsBody {
73    #[serde(skip_serializing_if = "Option::is_none")]
74    pub tool_ids: Option<Vec<String>>,
75    pub messages: Vec<Message>,
76    #[serde(skip_serializing_if = "Option::is_none")]
77    pub llm_config: Option<LlmConfig>,
78}
79
80/// AI operations namespace
81#[derive(Debug, Clone)]
82pub struct AiNamespace {
83    client: OramaClient,
84    collection_id: String,
85}
86
87impl AiNamespace {
88    pub(crate) fn new(client: OramaClient, collection_id: String) -> Self {
89        Self {
90            client,
91            collection_id,
92        }
93    }
94
95    /// Perform NLP-based search
96    pub async fn nlp_search<T>(&self, params: NlpSearchParams) -> Result<Vec<NlpSearchResult<T>>>
97    where
98        T: for<'de> serde::Deserialize<'de>,
99    {
100        let request = ClientRequest::post(
101            format!("/v1/collections/{}/nlp_search", self.collection_id),
102            Target::Reader,
103            ApiKeyPosition::QueryParams,
104            params,
105        );
106
107        self.client.request(request).await
108    }
109
110    /// Create an AI session for streaming conversations
111    pub async fn create_ai_session(&self) -> Result<OramaCoreStream> {
112        OramaCoreStream::new(self.collection_id.clone(), self.client.clone()).await
113    }
114}
115
116/// Collections operations namespace
117#[derive(Debug, Clone)]
118pub struct CollectionsNamespace {
119    client: OramaClient,
120    collection_id: String,
121}
122
123impl CollectionsNamespace {
124    pub(crate) fn new(client: OramaClient, collection_id: String) -> Self {
125        Self {
126            client,
127            collection_id,
128        }
129    }
130
131    /// Get collection statistics
132    pub async fn get_stats(&self) -> Result<serde_json::Value> {
133        let request = ClientRequest::<()>::get(
134            format!("/v1/collections/{}/stats", self.collection_id),
135            Target::Reader,
136            ApiKeyPosition::QueryParams,
137        );
138
139        self.client.request(request).await
140    }
141
142    /// Get all documents in collection
143    pub async fn get_all_docs<T>(&self, id: &str) -> Result<Vec<T>>
144    where
145        T: for<'de> serde::Deserialize<'de>,
146    {
147        let body = serde_json::json!({ "id": id });
148        let request = ClientRequest::post(
149            "/v1/collections/list".to_string(),
150            Target::Writer,
151            ApiKeyPosition::Header,
152            body,
153        );
154
155        self.client.request(request).await
156    }
157}
158
159/// Index operations namespace
160#[derive(Debug, Clone)]
161pub struct IndexNamespace {
162    client: OramaClient,
163    collection_id: String,
164}
165
166impl IndexNamespace {
167    pub(crate) fn new(client: OramaClient, collection_id: String) -> Self {
168        Self {
169            client,
170            collection_id,
171        }
172    }
173
174    /// Create a new index
175    pub async fn create(&self, config: CreateIndexParams) -> Result<()> {
176        let body = serde_json::json!({
177            "id": config.id,
178            "embedding": config.embeddings
179        });
180
181        let request = ClientRequest::post(
182            format!("/v1/collections/{}/indexes/create", self.collection_id),
183            Target::Writer,
184            ApiKeyPosition::Header,
185            body,
186        );
187
188        let _: serde_json::Value = self.client.request(request).await?;
189        Ok(())
190    }
191
192    /// Delete an index
193    pub async fn delete(&self, index_id: &str) -> Result<()> {
194        let body = serde_json::json!({
195            "index_id_to_delete": index_id
196        });
197
198        let request = ClientRequest::post(
199            format!("/v1/collections/{}/indexes/delete", self.collection_id),
200            Target::Writer,
201            ApiKeyPosition::Header,
202            body,
203        );
204
205        let _: serde_json::Value = self.client.request(request).await?;
206        Ok(())
207    }
208
209    /// Get an Index instance for operations
210    pub fn set(&self, id: String) -> Index {
211        Index::new(self.client.clone(), self.collection_id.clone(), id)
212    }
213}
214
215/// Hooks operations namespace
216#[derive(Debug, Clone)]
217pub struct HooksNamespace {
218    client: OramaClient,
219    collection_id: String,
220}
221
222impl HooksNamespace {
223    pub(crate) fn new(client: OramaClient, collection_id: String) -> Self {
224        Self {
225            client,
226            collection_id,
227        }
228    }
229
230    /// Insert a new hook
231    pub async fn insert(&self, config: AddHookConfig) -> Result<NewHookResponse> {
232        let body = serde_json::json!({
233            "name": config.name,
234            "code": config.code
235        });
236
237        let request = ClientRequest::post(
238            format!("/v1/collections/{}/hooks/set", self.collection_id),
239            Target::Writer,
240            ApiKeyPosition::Header,
241            body,
242        );
243
244        let _: serde_json::Value = self.client.request(request).await?;
245
246        Ok(NewHookResponse {
247            hook_id: serde_json::to_string(&config.name)?,
248            code: config.code,
249        })
250    }
251
252    /// List all hooks
253    pub async fn list(&self) -> Result<HashMap<String, Option<String>>> {
254        let request = ClientRequest::<()>::get(
255            format!("/v1/collections/{}/hooks/list", self.collection_id),
256            Target::Writer,
257            ApiKeyPosition::Header,
258        );
259
260        let response: serde_json::Value = self.client.request(request).await?;
261        let empty_map = serde_json::Map::new();
262        let hooks = response["hooks"].as_object().unwrap_or(&empty_map);
263
264        let mut result = HashMap::new();
265        for (key, value) in hooks {
266            let val = value.as_str().map(|s| s.to_string());
267            result.insert(key.clone(), val);
268        }
269
270        Ok(result)
271    }
272
273    /// Delete a hook
274    pub async fn delete(&self, hook: Hook) -> Result<()> {
275        let body = serde_json::json!({
276            "name_to_delete": hook
277        });
278
279        let request = ClientRequest::post(
280            format!("/v1/collections/{}/hooks/delete", self.collection_id),
281            Target::Writer,
282            ApiKeyPosition::Header,
283            body,
284        );
285
286        let _: serde_json::Value = self.client.request(request).await?;
287        Ok(())
288    }
289}
290
291/// System prompts operations namespace
292#[derive(Debug, Clone)]
293pub struct SystemPromptsNamespace {
294    client: OramaClient,
295    collection_id: String,
296}
297
298impl SystemPromptsNamespace {
299    pub(crate) fn new(client: OramaClient, collection_id: String) -> Self {
300        Self {
301            client,
302            collection_id,
303        }
304    }
305
306    /// Insert a system prompt
307    pub async fn insert(&self, system_prompt: InsertSystemPromptBody) -> Result<serde_json::Value> {
308        let request = ClientRequest::post(
309            format!(
310                "/v1/collections/{}/system_prompts/insert",
311                self.collection_id
312            ),
313            Target::Writer,
314            ApiKeyPosition::Header,
315            system_prompt,
316        );
317
318        self.client.request(request).await
319    }
320
321    /// Get a system prompt
322    pub async fn get(&self, id: &str) -> Result<SystemPrompt> {
323        let request = ClientRequest::<()>::get(
324            format!("/v1/collections/{}/system_prompts/get", self.collection_id),
325            Target::Reader,
326            ApiKeyPosition::QueryParams,
327        )
328        .with_param("system_prompt_id", id);
329
330        let response: serde_json::Value = self.client.request(request).await?;
331        let prompt = response["system_prompt"].clone();
332        Ok(serde_json::from_value(prompt)?)
333    }
334
335    /// Get all system prompts
336    pub async fn get_all(&self) -> Result<Vec<SystemPrompt>> {
337        let request = ClientRequest::<()>::get(
338            format!("/v1/collections/{}/system_prompts/all", self.collection_id),
339            Target::Reader,
340            ApiKeyPosition::QueryParams,
341        );
342
343        let response: serde_json::Value = self.client.request(request).await?;
344        let prompts = response["system_prompts"].clone();
345        Ok(serde_json::from_value(prompts)?)
346    }
347
348    /// Delete a system prompt
349    pub async fn delete(&self, id: &str) -> Result<serde_json::Value> {
350        let body = serde_json::json!({ "id": id });
351        let request = ClientRequest::post(
352            format!(
353                "/v1/collections/{}/system_prompts/delete",
354                self.collection_id
355            ),
356            Target::Writer,
357            ApiKeyPosition::Header,
358            body,
359        );
360
361        self.client.request(request).await
362    }
363
364    /// Update a system prompt
365    pub async fn update(&self, system_prompt: SystemPrompt) -> Result<serde_json::Value> {
366        let request = ClientRequest::post(
367            format!(
368                "/v1/collections/{}/system_prompts/update",
369                self.collection_id
370            ),
371            Target::Writer,
372            ApiKeyPosition::Header,
373            system_prompt,
374        );
375
376        self.client.request(request).await
377    }
378
379    /// Validate a system prompt
380    pub async fn validate(
381        &self,
382        system_prompt: SystemPrompt,
383    ) -> Result<SystemPromptValidationResponse> {
384        let request = ClientRequest::post(
385            format!(
386                "/v1/collections/{}/system_prompts/validate",
387                self.collection_id
388            ),
389            Target::Writer,
390            ApiKeyPosition::Header,
391            system_prompt,
392        );
393
394        let response: serde_json::Value = self.client.request(request).await?;
395        let result = response["result"].clone();
396        Ok(serde_json::from_value(result)?)
397    }
398}
399
400/// Tools operations namespace
401#[derive(Debug, Clone)]
402pub struct ToolsNamespace {
403    client: OramaClient,
404    collection_id: String,
405}
406
407impl ToolsNamespace {
408    pub(crate) fn new(client: OramaClient, collection_id: String) -> Self {
409        Self {
410            client,
411            collection_id,
412        }
413    }
414
415    /// Insert a tool
416    pub async fn insert(&self, tool: InsertToolBody) -> Result<()> {
417        let request = ClientRequest::post(
418            format!("/v1/collections/{}/tools/insert", self.collection_id),
419            Target::Writer,
420            ApiKeyPosition::Header,
421            tool,
422        );
423
424        let _: serde_json::Value = self.client.request(request).await?;
425        Ok(())
426    }
427
428    /// Get a tool
429    pub async fn get(&self, id: &str) -> Result<Tool> {
430        let request = ClientRequest::<()>::get(
431            format!("/v1/collections/{}/tools/get", self.collection_id),
432            Target::Reader,
433            ApiKeyPosition::QueryParams,
434        )
435        .with_param("tool_id", id);
436
437        let response: serde_json::Value = self.client.request(request).await?;
438        let tool = response["tool"].clone();
439        Ok(serde_json::from_value(tool)?)
440    }
441
442    /// Get all tools
443    pub async fn get_all(&self) -> Result<Vec<Tool>> {
444        let request = ClientRequest::<()>::get(
445            format!("/v1/collections/{}/tools/all", self.collection_id),
446            Target::Reader,
447            ApiKeyPosition::QueryParams,
448        );
449
450        let response: serde_json::Value = self.client.request(request).await?;
451        let tools = response["tools"].clone();
452        Ok(serde_json::from_value(tools)?)
453    }
454
455    /// Delete a tool
456    pub async fn delete(&self, id: &str) -> Result<serde_json::Value> {
457        let body = serde_json::json!({ "id": id });
458        let request = ClientRequest::post(
459            format!("/v1/collections/{}/tools/delete", self.collection_id),
460            Target::Writer,
461            ApiKeyPosition::Header,
462            body,
463        );
464
465        self.client.request(request).await
466    }
467
468    /// Update a tool
469    pub async fn update(&self, tool: UpdateToolBody) -> Result<serde_json::Value> {
470        let request = ClientRequest::post(
471            format!("/v1/collections/{}/tools/update", self.collection_id),
472            Target::Writer,
473            ApiKeyPosition::Header,
474            tool,
475        );
476
477        self.client.request(request).await
478    }
479
480    /// Execute tools
481    pub async fn execute<T>(&self, tools: ExecuteToolsBody) -> Result<ExecuteToolsParsedResponse<T>>
482    where
483        T: for<'de> serde::Deserialize<'de>,
484    {
485        let request = ClientRequest::post(
486            format!("/v1/collections/{}/tools/run", self.collection_id),
487            Target::Reader,
488            ApiKeyPosition::QueryParams,
489            tools,
490        );
491
492        self.client.request(request).await
493    }
494}
495
496/// Index operations for document management
497#[derive(Debug, Clone)]
498pub struct Index {
499    client: OramaClient,
500    collection_id: String,
501    index_id: String,
502}
503
504impl Index {
505    pub(crate) fn new(client: OramaClient, collection_id: String, index_id: String) -> Self {
506        Self {
507            client,
508            collection_id,
509            index_id,
510        }
511    }
512
513    /// Reindex the collection
514    pub async fn reindex(&self) -> Result<()> {
515        let request = ClientRequest::<()>::post(
516            format!(
517                "/v1/collections/{}/indexes/{}/reindex",
518                self.collection_id, self.index_id
519            ),
520            Target::Writer,
521            ApiKeyPosition::Header,
522            (),
523        );
524
525        let _: serde_json::Value = self.client.request(request).await?;
526        Ok(())
527    }
528
529    /// Insert documents
530    pub async fn insert_documents<T>(&self, documents: Vec<T>) -> Result<()>
531    where
532        T: Serialize,
533    {
534        let body = serde_json::json!({
535            "documents": documents
536        });
537
538        let request = ClientRequest::post(
539            format!(
540                "/v1/collections/{}/indexes/{}/documents/insert",
541                self.collection_id, self.index_id
542            ),
543            Target::Writer,
544            ApiKeyPosition::Header,
545            body,
546        );
547
548        let _: serde_json::Value = self.client.request(request).await?;
549        Ok(())
550    }
551
552    /// Delete documents
553    pub async fn delete_documents(&self, document_ids: Vec<String>) -> Result<()> {
554        let body = serde_json::json!({
555            "document_ids": document_ids
556        });
557
558        let request = ClientRequest::post(
559            format!(
560                "/v1/collections/{}/indexes/{}/documents/delete",
561                self.collection_id, self.index_id
562            ),
563            Target::Writer,
564            ApiKeyPosition::Header,
565            body,
566        );
567
568        let _: serde_json::Value = self.client.request(request).await?;
569        Ok(())
570    }
571
572    /// Upsert documents
573    pub async fn upsert_documents<T>(&self, documents: Vec<T>) -> Result<()>
574    where
575        T: Serialize,
576    {
577        let body = serde_json::json!({
578            "documents": documents
579        });
580
581        let request = ClientRequest::post(
582            format!(
583                "/v1/collections/{}/indexes/{}/documents/upsert",
584                self.collection_id, self.index_id
585            ),
586            Target::Writer,
587            ApiKeyPosition::Header,
588            body,
589        );
590
591        let _: serde_json::Value = self.client.request(request).await?;
592        Ok(())
593    }
594}
595
596/// Main collection manager
597#[derive(Debug, Clone)]
598pub struct CollectionManager {
599    client: OramaClient,
600    collection_id: String,
601    pub ai: AiNamespace,
602    pub collections: CollectionsNamespace,
603    pub index: IndexNamespace,
604    pub hooks: HooksNamespace,
605    pub system_prompts: SystemPromptsNamespace,
606    pub tools: ToolsNamespace,
607}
608
609impl CollectionManager {
610    /// Create a new CollectionManager
611    pub async fn new(config: CollectionManagerConfig) -> Result<Self> {
612        let auth_config = if config.api_key.starts_with("p_") {
613            // Private API Key (JWT flow)
614            AuthConfig::Jwt(
615                JwtAuth::new(
616                    config.auth_jwt_url.as_deref().unwrap_or(DEFAULT_JWT_URL),
617                    &config.collection_id,
618                    &config.api_key,
619                )
620                .with_reader_url(
621                    config
622                        .cluster
623                        .as_ref()
624                        .and_then(|c| c.read_url.as_deref())
625                        .unwrap_or(DEFAULT_READER_URL),
626                )
627                .with_writer_url(
628                    config
629                        .cluster
630                        .as_ref()
631                        .and_then(|c| c.writer_url.as_deref())
632                        .unwrap_or(""),
633                ),
634            )
635        } else {
636            // Regular API Key
637            AuthConfig::ApiKey(
638                ApiKeyAuth::new(&config.api_key)
639                    .with_reader_url(
640                        config
641                            .cluster
642                            .as_ref()
643                            .and_then(|c| c.read_url.as_deref())
644                            .unwrap_or(DEFAULT_READER_URL),
645                    )
646                    .with_writer_url(
647                        config
648                            .cluster
649                            .as_ref()
650                            .and_then(|c| c.writer_url.as_deref())
651                            .unwrap_or(""),
652                    ),
653            )
654        };
655
656        let client = Client::new();
657        let auth = Auth::new(auth_config, Arc::new(client));
658        let orama_client = OramaClient::new(auth)?;
659
660        let collection_id = config.collection_id.clone();
661
662        Ok(Self {
663            ai: AiNamespace::new(orama_client.clone(), collection_id.clone()),
664            collections: CollectionsNamespace::new(orama_client.clone(), collection_id.clone()),
665            index: IndexNamespace::new(orama_client.clone(), collection_id.clone()),
666            hooks: HooksNamespace::new(orama_client.clone(), collection_id.clone()),
667            system_prompts: SystemPromptsNamespace::new(
668                orama_client.clone(),
669                collection_id.clone(),
670            ),
671            tools: ToolsNamespace::new(orama_client.clone(), collection_id.clone()),
672            client: orama_client,
673            collection_id,
674        })
675    }
676
677    /// Perform a search
678    pub async fn search<T>(&self, query: &SearchParams) -> Result<SearchResult<T>>
679    where
680        T: for<'de> serde::Deserialize<'de>,
681    {
682        let start_time = current_time_millis();
683
684        let request = ClientRequest::post(
685            format!("/v1/collections/{}/search", self.collection_id),
686            Target::Reader,
687            ApiKeyPosition::QueryParams,
688            query,
689        );
690
691        let mut result: SearchResult<T> = self.client.request(request).await?;
692
693        let elapsed_time = current_time_millis() - start_time;
694        result.elapsed = Some(Elapsed {
695            raw: elapsed_time,
696            formatted: format_duration(elapsed_time),
697        });
698
699        Ok(result)
700    }
701}
702
703// Builder implementations
704impl CollectionManagerConfig {
705    /// Create a new CollectionManagerConfig
706    pub fn new<S: Into<String>>(collection_id: S, api_key: S) -> Self {
707        Self {
708            collection_id: collection_id.into(),
709            api_key: api_key.into(),
710            cluster: None,
711            auth_jwt_url: None,
712        }
713    }
714
715    /// Set cluster configuration
716    pub fn with_cluster(mut self, cluster: ClusterConfig) -> Self {
717        self.cluster = Some(cluster);
718        self
719    }
720
721    /// Set auth JWT URL
722    pub fn with_auth_jwt_url<S: Into<String>>(mut self, url: S) -> Self {
723        self.auth_jwt_url = Some(url.into());
724        self
725    }
726}
727
728impl ClusterConfig {
729    /// Create a new ClusterConfig
730    pub fn new() -> Self {
731        Self {
732            writer_url: None,
733            read_url: None,
734        }
735    }
736
737    /// Set writer URL
738    pub fn with_writer_url<S: Into<String>>(mut self, url: S) -> Self {
739        self.writer_url = Some(url.into());
740        self
741    }
742
743    /// Set read URL
744    pub fn with_read_url<S: Into<String>>(mut self, url: S) -> Self {
745        self.read_url = Some(url.into());
746        self
747    }
748}
749
750impl Default for ClusterConfig {
751    fn default() -> Self {
752        Self::new()
753    }
754}