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