1#![forbid(unsafe_code)]
2#![recursion_limit = "256"]
3use aws_config::SdkConfig;
10use chrono::Utc;
11use docbox_database::{
12 DatabasePoolCache, DbTransaction,
13 models::{
14 document_box::{DocumentBoxScopeRaw, DocumentBoxScopeRawRef},
15 file::FileId,
16 folder::FolderId,
17 tenant::Tenant,
18 tenant_migration::{CreateTenantMigration, TenantMigration},
19 },
20};
21use docbox_secrets::SecretManager;
22use models::{
23 FileSearchRequest, FileSearchResults, SearchIndexData, SearchRequest, SearchResults,
24 UpdateSearchIndexData,
25};
26use serde::{Deserialize, Serialize};
27use std::{ops::DerefMut, sync::Arc};
28use thiserror::Error;
29use uuid::Uuid;
30
31pub mod models;
32
33pub use database::{
34 DatabaseSearchConfig, DatabaseSearchError, DatabaseSearchIndex, DatabaseSearchIndexFactory,
35 DatabaseSearchIndexFactoryError,
36};
37pub use opensearch::{
38 OpenSearchConfig, OpenSearchIndex, OpenSearchIndexFactory, OpenSearchIndexFactoryError,
39 OpenSearchSearchError,
40};
41pub use typesense::{
42 TypesenseApiKey, TypesenseApiKeyProvider, TypesenseApiKeySecret, TypesenseIndex,
43 TypesenseIndexFactory, TypesenseIndexFactoryError, TypesenseSearchConfig, TypesenseSearchError,
44};
45
46mod database;
47mod opensearch;
48mod typesense;
49
50#[derive(Debug, Clone, Deserialize, Serialize)]
51#[serde(tag = "provider", rename_all = "snake_case")]
52pub enum SearchIndexFactoryConfig {
53 Typesense(typesense::TypesenseSearchConfig),
54 OpenSearch(opensearch::OpenSearchConfig),
55 Database(database::DatabaseSearchConfig),
56}
57
58impl Default for SearchIndexFactoryConfig {
59 fn default() -> Self {
60 Self::Database(Default::default())
61 }
62}
63
64#[derive(Debug, Error)]
65pub enum SearchIndexFactoryError {
66 #[error(transparent)]
67 Typesense(#[from] typesense::TypesenseIndexFactoryError),
68 #[error(transparent)]
69 OpenSearch(#[from] opensearch::OpenSearchIndexFactoryError),
70 #[error(transparent)]
71 Database(#[from] database::DatabaseSearchIndexFactoryError),
72 #[error("unknown search index factory type requested")]
73 UnknownIndexFactory,
74}
75
76impl SearchIndexFactoryConfig {
77 pub fn from_env() -> Result<Self, SearchIndexFactoryError> {
78 let variant = std::env::var("DOCBOX_SEARCH_INDEX_FACTORY")
79 .unwrap_or_else(|_| "database".to_string())
80 .to_lowercase();
81 match variant.as_str() {
82 "open_search" | "opensearch" => opensearch::OpenSearchConfig::from_env()
83 .map(Self::OpenSearch)
84 .map_err(SearchIndexFactoryError::OpenSearch),
85
86 "typesense" => typesense::TypesenseSearchConfig::from_env()
87 .map(Self::Typesense)
88 .map_err(SearchIndexFactoryError::Typesense),
89
90 "database" => database::DatabaseSearchConfig::from_env()
91 .map(Self::Database)
92 .map_err(SearchIndexFactoryError::Database),
93
94 _ => Err(SearchIndexFactoryError::UnknownIndexFactory),
96 }
97 }
98}
99
100#[derive(Clone)]
101pub enum SearchIndexFactory {
102 Typesense(typesense::TypesenseIndexFactory),
103 OpenSearch(opensearch::OpenSearchIndexFactory),
104 Database(database::DatabaseSearchIndexFactory),
105}
106
107impl SearchIndexFactory {
108 pub fn from_config(
110 aws_config: &SdkConfig,
111 secrets: SecretManager,
112 db: Arc<DatabasePoolCache>,
113 config: SearchIndexFactoryConfig,
114 ) -> Result<Self, SearchIndexFactoryError> {
115 match config {
116 SearchIndexFactoryConfig::Typesense(config) => {
117 tracing::debug!("using typesense search index");
118 typesense::TypesenseIndexFactory::from_config(secrets, config)
119 .map(SearchIndexFactory::Typesense)
120 .map_err(SearchIndexFactoryError::Typesense)
121 }
122
123 SearchIndexFactoryConfig::OpenSearch(config) => {
124 tracing::debug!("using opensearch search index");
125 opensearch::OpenSearchIndexFactory::from_config(aws_config, config)
126 .map(SearchIndexFactory::OpenSearch)
127 .map_err(SearchIndexFactoryError::OpenSearch)
128 }
129
130 SearchIndexFactoryConfig::Database(config) => {
131 tracing::debug!("using opensearch search index");
132 database::DatabaseSearchIndexFactory::from_config(db, config)
133 .map(SearchIndexFactory::Database)
134 .map_err(SearchIndexFactoryError::Database)
135 }
136 }
137 }
138
139 pub fn create_search_index(&self, tenant: &Tenant) -> TenantSearchIndex {
141 match self {
142 SearchIndexFactory::Typesense(factory) => {
143 let search_index = tenant.os_index_name.clone();
144 TenantSearchIndex::Typesense(factory.create_search_index(search_index))
145 }
146
147 SearchIndexFactory::OpenSearch(factory) => {
148 let search_index = opensearch::TenantSearchIndexName::from_tenant(tenant);
149 TenantSearchIndex::OpenSearch(factory.create_search_index(search_index))
150 }
151
152 SearchIndexFactory::Database(factory) => {
153 TenantSearchIndex::Database(factory.create_search_index(tenant))
154 }
155 }
156 }
157}
158
159#[derive(Clone)]
160pub enum TenantSearchIndex {
161 Typesense(typesense::TypesenseIndex),
162 OpenSearch(opensearch::OpenSearchIndex),
163 Database(database::DatabaseSearchIndex),
164}
165
166#[derive(Debug, Error)]
167pub enum SearchError {
168 #[error(transparent)]
169 Typesense(#[from] typesense::TypesenseSearchError),
170 #[error(transparent)]
171 OpenSearch(#[from] opensearch::OpenSearchSearchError),
172 #[error(transparent)]
173 Database(#[from] database::DatabaseSearchError),
174 #[error("failed to perform migration")]
175 Migration,
176}
177
178impl TenantSearchIndex {
179 #[tracing::instrument(skip(self))]
181 pub async fn create_index(&self) -> Result<(), SearchError> {
182 match self {
183 TenantSearchIndex::Typesense(index) => index.create_index().await,
184 TenantSearchIndex::OpenSearch(index) => index.create_index().await,
185 TenantSearchIndex::Database(index) => index.create_index().await,
186 }
187 }
188
189 #[tracing::instrument(skip(self))]
191 pub async fn index_exists(&self) -> Result<bool, SearchError> {
192 match self {
193 TenantSearchIndex::Typesense(index) => index.index_exists().await,
194 TenantSearchIndex::OpenSearch(index) => index.index_exists().await,
195 TenantSearchIndex::Database(index) => index.index_exists().await,
196 }
197 }
198
199 #[tracing::instrument(skip(self))]
201 pub async fn delete_index(&self) -> Result<(), SearchError> {
202 match self {
203 TenantSearchIndex::Typesense(index) => index.delete_index().await,
204 TenantSearchIndex::OpenSearch(index) => index.delete_index().await,
205 TenantSearchIndex::Database(index) => index.delete_index().await,
206 }
207 }
208
209 #[tracing::instrument(skip(self))]
211 pub async fn search_index(
212 &self,
213 scope: &[DocumentBoxScopeRaw],
214 query: SearchRequest,
215 folder_children: Option<Vec<FolderId>>,
216 ) -> Result<SearchResults, SearchError> {
217 match self {
218 TenantSearchIndex::Typesense(index) => {
219 index.search_index(scope, query, folder_children).await
220 }
221 TenantSearchIndex::OpenSearch(index) => {
222 index.search_index(scope, query, folder_children).await
223 }
224 TenantSearchIndex::Database(index) => {
225 index.search_index(scope, query, folder_children).await
226 }
227 }
228 }
229
230 #[tracing::instrument(skip(self))]
232 pub async fn search_index_file(
233 &self,
234 scope: &DocumentBoxScopeRaw,
235 file_id: FileId,
236 query: FileSearchRequest,
237 ) -> Result<FileSearchResults, SearchError> {
238 match self {
239 TenantSearchIndex::Typesense(index) => {
240 index.search_index_file(scope, file_id, query).await
241 }
242 TenantSearchIndex::OpenSearch(index) => {
243 index.search_index_file(scope, file_id, query).await
244 }
245 TenantSearchIndex::Database(index) => {
246 index.search_index_file(scope, file_id, query).await
247 }
248 }
249 }
250
251 #[tracing::instrument(skip(self))]
253 pub async fn add_data(&self, data: Vec<SearchIndexData>) -> Result<(), SearchError> {
254 match self {
255 TenantSearchIndex::Typesense(index) => index.add_data(data).await,
256 TenantSearchIndex::OpenSearch(index) => index.add_data(data).await,
257 TenantSearchIndex::Database(index) => index.add_data(data).await,
258 }
259 }
260
261 #[tracing::instrument(skip(self))]
263 pub async fn update_data(
264 &self,
265 item_id: Uuid,
266 data: UpdateSearchIndexData,
267 ) -> Result<(), SearchError> {
268 match self {
269 TenantSearchIndex::Typesense(index) => index.update_data(item_id, data).await,
270 TenantSearchIndex::OpenSearch(index) => index.update_data(item_id, data).await,
271 TenantSearchIndex::Database(index) => index.update_data(item_id, data).await,
272 }
273 }
274
275 #[tracing::instrument(skip(self))]
277 pub async fn delete_data(&self, id: Uuid) -> Result<(), SearchError> {
278 match self {
279 TenantSearchIndex::Typesense(index) => index.delete_data(id).await,
280 TenantSearchIndex::OpenSearch(index) => index.delete_data(id).await,
281 TenantSearchIndex::Database(index) => index.delete_data(id).await,
282 }
283 }
284
285 #[tracing::instrument(skip(self))]
287 pub async fn delete_by_scope(
288 &self,
289 scope: DocumentBoxScopeRawRef<'_>,
290 ) -> Result<(), SearchError> {
291 match self {
292 TenantSearchIndex::Typesense(index) => index.delete_by_scope(scope).await,
293 TenantSearchIndex::OpenSearch(index) => index.delete_by_scope(scope).await,
294 TenantSearchIndex::Database(index) => index.delete_by_scope(scope).await,
295 }
296 }
297
298 #[tracing::instrument(skip(self))]
300 pub async fn get_pending_migrations(
301 &self,
302 applied_names: Vec<String>,
303 ) -> Result<Vec<String>, SearchError> {
304 match self {
305 TenantSearchIndex::Typesense(index) => {
306 index.get_pending_migrations(applied_names).await
307 }
308 TenantSearchIndex::OpenSearch(index) => {
309 index.get_pending_migrations(applied_names).await
310 }
311 TenantSearchIndex::Database(index) => index.get_pending_migrations(applied_names).await,
312 }
313 }
314
315 #[tracing::instrument(skip(self))]
317 pub async fn apply_migration(
318 &self,
319 tenant: &Tenant,
320 root_t: &mut DbTransaction<'_>,
321 tenant_t: &mut DbTransaction<'_>,
322 name: &str,
323 ) -> Result<(), SearchError> {
324 match self {
326 TenantSearchIndex::Typesense(index) => {
327 index
328 .apply_migration(tenant, root_t, tenant_t, name)
329 .await?
330 }
331
332 TenantSearchIndex::OpenSearch(index) => {
333 index
334 .apply_migration(tenant, root_t, tenant_t, name)
335 .await?
336 }
337
338 TenantSearchIndex::Database(index) => {
339 index
340 .apply_migration(tenant, root_t, tenant_t, name)
341 .await?
342 }
343 }
344
345 TenantMigration::create(
347 root_t.deref_mut(),
348 CreateTenantMigration {
349 tenant_id: tenant.id,
350 env: tenant.env.clone(),
351 name: name.to_string(),
352 applied_at: Utc::now(),
353 },
354 )
355 .await
356 .map_err(|error| {
357 tracing::error!(?error, "failed to create tenant migration");
358 SearchError::Migration
359 })?;
360
361 Ok(())
362 }
363
364 #[tracing::instrument(skip_all, fields(?tenant, ?target_migration_name))]
369 pub async fn apply_migrations(
370 &self,
371 tenant: &Tenant,
372 root_t: &mut DbTransaction<'_>,
373 tenant_t: &mut DbTransaction<'_>,
374 target_migration_name: Option<&str>,
375 ) -> Result<(), SearchError> {
376 let applied_migrations =
377 TenantMigration::find_by_tenant(root_t.deref_mut(), tenant.id, &tenant.env)
378 .await
379 .map_err(|error| {
380 tracing::error!(?error, "failed to query tenant migrations");
381 SearchError::Migration
382 })?;
383 let pending_migrations = self
384 .get_pending_migrations(
385 applied_migrations
386 .into_iter()
387 .map(|value| value.name)
388 .collect(),
389 )
390 .await?;
391
392 for migration_name in pending_migrations {
393 if target_migration_name
395 .is_some_and(|target_migration_name| target_migration_name.ne(&migration_name))
396 {
397 continue;
398 }
399
400 if let Err(error) = self
402 .apply_migration(tenant, root_t, tenant_t, &migration_name)
403 .await
404 {
405 tracing::error!(%migration_name, ?error, "failed to apply migration");
406 return Err(error);
407 }
408 }
409
410 Ok(())
411 }
412}
413
414pub(crate) trait SearchIndex: Send + Sync + 'static {
415 async fn create_index(&self) -> Result<(), SearchError>;
416
417 async fn index_exists(&self) -> Result<bool, SearchError>;
418
419 async fn delete_index(&self) -> Result<(), SearchError>;
420
421 async fn search_index(
422 &self,
423 scope: &[DocumentBoxScopeRaw],
424 query: SearchRequest,
425 folder_children: Option<Vec<FolderId>>,
426 ) -> Result<SearchResults, SearchError>;
427
428 async fn search_index_file(
429 &self,
430 scope: &DocumentBoxScopeRaw,
431 file_id: FileId,
432 query: FileSearchRequest,
433 ) -> Result<FileSearchResults, SearchError>;
434
435 async fn add_data(&self, data: Vec<SearchIndexData>) -> Result<(), SearchError>;
436
437 async fn update_data(
438 &self,
439 item_id: Uuid,
440 data: UpdateSearchIndexData,
441 ) -> Result<(), SearchError>;
442
443 async fn delete_data(&self, id: Uuid) -> Result<(), SearchError>;
444
445 async fn delete_by_scope(&self, scope: DocumentBoxScopeRawRef<'_>) -> Result<(), SearchError>;
446
447 async fn get_pending_migrations(
448 &self,
449 applied_names: Vec<String>,
450 ) -> Result<Vec<String>, SearchError>;
451
452 async fn apply_migration(
453 &self,
454 tenant: &Tenant,
455 root_t: &mut DbTransaction<'_>,
456 t: &mut DbTransaction<'_>,
457 name: &str,
458 ) -> Result<(), SearchError>;
459}