From ae2d0a67a4f91bb95c40e721b2b486dfce22400a Mon Sep 17 00:00:00 2001 From: Quentin de Quelen Date: Tue, 5 Aug 2025 19:18:05 +0200 Subject: [PATCH] Enhance index update functionality to support renaming by adding new_uid field. Update related structures and methods to handle the new index UID during updates, ensuring backward compatibility with existing index operations. --- crates/dump/src/lib.rs | 5 +- crates/dump/src/reader/compat/v5_to_v6.rs | 4 +- crates/index-scheduler/src/dump.rs | 3 +- crates/index-scheduler/src/insta_snapshot.rs | 4 +- crates/index-scheduler/src/processing.rs | 6 - .../src/scheduler/autobatcher.rs | 16 +- .../src/scheduler/autobatcher_test.rs | 6 +- .../src/scheduler/create_batch.rs | 29 +- .../src/scheduler/process_batch.rs | 62 +-- crates/index-scheduler/src/utils.rs | 11 +- crates/meilisearch-types/src/task_view.rs | 25 +- crates/meilisearch-types/src/tasks.rs | 65 ++- crates/meilisearch/src/routes/indexes/mod.rs | 10 + crates/meilisearch/tests/index/mod.rs | 1 + .../meilisearch/tests/index/rename_index.rs | 419 ++++++++++++++++++ 15 files changed, 547 insertions(+), 119 deletions(-) create mode 100644 crates/meilisearch/tests/index/rename_index.rs diff --git a/crates/dump/src/lib.rs b/crates/dump/src/lib.rs index 81ba40944..b4b339f09 100644 --- a/crates/dump/src/lib.rs +++ b/crates/dump/src/lib.rs @@ -129,6 +129,7 @@ pub enum KindDump { }, IndexUpdate { primary_key: Option, + new_uid: Option, }, IndexSwap { swaps: Vec, @@ -210,8 +211,8 @@ impl From for KindDump { KindWithContent::IndexCreation { primary_key, .. } => { KindDump::IndexCreation { primary_key } } - KindWithContent::IndexUpdate { primary_key, .. } => { - KindDump::IndexUpdate { primary_key } + KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => { + KindDump::IndexUpdate { primary_key, new_uid: new_index_uid } } KindWithContent::IndexSwap { swaps } => KindDump::IndexSwap { swaps }, KindWithContent::TaskCancelation { query, tasks } => { diff --git a/crates/dump/src/reader/compat/v5_to_v6.rs b/crates/dump/src/reader/compat/v5_to_v6.rs index 3a0c8ef0d..790c239d7 100644 --- a/crates/dump/src/reader/compat/v5_to_v6.rs +++ b/crates/dump/src/reader/compat/v5_to_v6.rs @@ -85,7 +85,7 @@ impl CompatV5ToV6 { v6::Kind::IndexCreation { primary_key } } v5::tasks::TaskContent::IndexUpdate { primary_key, .. } => { - v6::Kind::IndexUpdate { primary_key } + v6::Kind::IndexUpdate { primary_key, new_uid: None } } v5::tasks::TaskContent::IndexDeletion { .. } => v6::Kind::IndexDeletion, v5::tasks::TaskContent::DocumentAddition { @@ -141,7 +141,7 @@ impl CompatV5ToV6 { v6::Details::SettingsUpdate { settings: Box::new(settings.into()) } } v5::Details::IndexInfo { primary_key } => { - v6::Details::IndexInfo { primary_key } + v6::Details::IndexInfo { primary_key, new_uid: None } } v5::Details::DocumentDeletion { received_document_ids, diff --git a/crates/index-scheduler/src/dump.rs b/crates/index-scheduler/src/dump.rs index 1e681c8e8..18c665ca3 100644 --- a/crates/index-scheduler/src/dump.rs +++ b/crates/index-scheduler/src/dump.rs @@ -197,9 +197,10 @@ impl<'a> Dump<'a> { index_uid: task.index_uid.ok_or(Error::CorruptedDump)?, primary_key, }, - KindDump::IndexUpdate { primary_key } => KindWithContent::IndexUpdate { + KindDump::IndexUpdate { primary_key, new_uid } => KindWithContent::IndexUpdate { index_uid: task.index_uid.ok_or(Error::CorruptedDump)?, primary_key, + new_index_uid: new_uid, }, KindDump::IndexSwap { swaps } => KindWithContent::IndexSwap { swaps }, KindDump::TaskCancelation { query, tasks } => { diff --git a/crates/index-scheduler/src/insta_snapshot.rs b/crates/index-scheduler/src/insta_snapshot.rs index cb804d9b4..6d72e4b9f 100644 --- a/crates/index-scheduler/src/insta_snapshot.rs +++ b/crates/index-scheduler/src/insta_snapshot.rs @@ -274,8 +274,8 @@ fn snapshot_details(d: &Details) -> String { Details::SettingsUpdate { settings } => { format!("{{ settings: {settings:?} }}") } - Details::IndexInfo { primary_key } => { - format!("{{ primary_key: {primary_key:?} }}") + Details::IndexInfo { primary_key, new_uid } => { + format!("{{ primary_key: {primary_key:?}, new_uid: {new_uid:?} }}") } Details::DocumentDeletion { provided_ids: received_document_ids, diff --git a/crates/index-scheduler/src/processing.rs b/crates/index-scheduler/src/processing.rs index 84b0a5360..3da81f143 100644 --- a/crates/index-scheduler/src/processing.rs +++ b/crates/index-scheduler/src/processing.rs @@ -125,12 +125,6 @@ make_enum_progress! { } } -make_enum_progress! { - pub enum RenameIndexProgress { - RenamingTheIndex, - } -} - make_enum_progress! { pub enum DeleteIndexProgress { DeletingTheIndex, diff --git a/crates/index-scheduler/src/scheduler/autobatcher.rs b/crates/index-scheduler/src/scheduler/autobatcher.rs index 6b62d36e0..a88a9f0bf 100644 --- a/crates/index-scheduler/src/scheduler/autobatcher.rs +++ b/crates/index-scheduler/src/scheduler/autobatcher.rs @@ -24,7 +24,6 @@ enum AutobatchKind { IndexCreation, IndexDeletion, IndexUpdate, - IndexRename, IndexSwap, } @@ -68,7 +67,6 @@ impl From for AutobatchKind { KindWithContent::IndexDeletion { .. } => AutobatchKind::IndexDeletion, KindWithContent::IndexCreation { .. } => AutobatchKind::IndexCreation, KindWithContent::IndexUpdate { .. } => AutobatchKind::IndexUpdate, - KindWithContent::IndexRename { .. } => AutobatchKind::IndexRename, KindWithContent::IndexSwap { .. } => AutobatchKind::IndexSwap, KindWithContent::TaskCancelation { .. } | KindWithContent::TaskDeletion { .. } @@ -117,9 +115,6 @@ pub enum BatchKind { IndexUpdate { id: TaskId, }, - IndexRename { - id: TaskId, - }, IndexSwap { id: TaskId, }, @@ -181,13 +176,6 @@ impl BatchKind { )), false, ), - K::IndexRename => ( - Break(( - BatchKind::IndexRename { id: task_id }, - BatchStopReason::TaskCannotBeBatched { kind, id: task_id }, - )), - false, - ), K::IndexSwap => ( Break(( BatchKind::IndexSwap { id: task_id }, @@ -299,8 +287,8 @@ impl BatchKind { }; match (self, autobatch_kind) { - // We don't batch any of these operations - (this, K::IndexCreation | K::IndexUpdate | K::IndexRename | K::IndexSwap | K::DocumentEdition) => Break((this, BatchStopReason::TaskCannotBeBatched { kind, id })), + // We don't batch any of these operations + (this, K::IndexCreation | K::IndexUpdate | K::IndexSwap | K::DocumentEdition) => Break((this, BatchStopReason::TaskCannotBeBatched { kind, id })), // We must not batch tasks that don't have the same index creation rights if the index doesn't already exists. (this, kind) if !index_already_exists && this.allow_index_creation() == Some(false) && kind.allow_index_creation() == Some(true) => { Break((this, BatchStopReason::IndexCreationMismatch { id })) diff --git a/crates/index-scheduler/src/scheduler/autobatcher_test.rs b/crates/index-scheduler/src/scheduler/autobatcher_test.rs index 435ce55c6..0653753dc 100644 --- a/crates/index-scheduler/src/scheduler/autobatcher_test.rs +++ b/crates/index-scheduler/src/scheduler/autobatcher_test.rs @@ -75,7 +75,11 @@ fn idx_create() -> KindWithContent { } fn idx_update() -> KindWithContent { - KindWithContent::IndexUpdate { index_uid: String::from("doggo"), primary_key: None } + KindWithContent::IndexUpdate { + index_uid: String::from("doggo"), + primary_key: None, + new_index_uid: None, + } } fn idx_del() -> KindWithContent { diff --git a/crates/index-scheduler/src/scheduler/create_batch.rs b/crates/index-scheduler/src/scheduler/create_batch.rs index fa84200a1..14aa307cd 100644 --- a/crates/index-scheduler/src/scheduler/create_batch.rs +++ b/crates/index-scheduler/src/scheduler/create_batch.rs @@ -38,11 +38,7 @@ pub(crate) enum Batch { IndexUpdate { index_uid: String, primary_key: Option, - task: Task, - }, - IndexRename { - index_uid: String, - new_index_uid: String, + new_index_uid: Option, task: Task, }, IndexDeletion { @@ -113,8 +109,7 @@ impl Batch { | Batch::Dump(task) | Batch::IndexCreation { task, .. } | Batch::Export { task } - | Batch::IndexUpdate { task, .. } - | Batch::IndexRename { task, .. } => { + | Batch::IndexUpdate { task, .. } => { RoaringBitmap::from_sorted_iter(std::iter::once(task.uid)).unwrap() } Batch::SnapshotCreation(tasks) @@ -159,7 +154,6 @@ impl Batch { IndexOperation { op, .. } => Some(op.index_uid()), IndexCreation { index_uid, .. } | IndexUpdate { index_uid, .. } - | IndexRename { index_uid, .. } | IndexDeletion { index_uid, .. } => Some(index_uid), } } @@ -178,7 +172,6 @@ impl fmt::Display for Batch { Batch::IndexOperation { op, .. } => write!(f, "{op}")?, Batch::IndexCreation { .. } => f.write_str("IndexCreation")?, Batch::IndexUpdate { .. } => f.write_str("IndexUpdate")?, - Batch::IndexRename { .. } => f.write_str("IndexRename")?, Batch::IndexDeletion { .. } => f.write_str("IndexDeletion")?, Batch::IndexSwap { .. } => f.write_str("IndexSwap")?, Batch::Export { .. } => f.write_str("Export")?, @@ -413,21 +406,13 @@ impl IndexScheduler { let mut task = self.queue.tasks.get_task(rtxn, id)?.ok_or(Error::CorruptedTaskQueue)?; current_batch.processing(Some(&mut task)); - let primary_key = match &task.kind { - KindWithContent::IndexUpdate { primary_key, .. } => primary_key.clone(), + let (primary_key, new_index_uid) = match &task.kind { + KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => { + (primary_key.clone(), new_index_uid.clone()) + } _ => unreachable!(), }; - Ok(Some(Batch::IndexUpdate { index_uid, primary_key, task })) - } - BatchKind::IndexRename { id } => { - let mut task = - self.queue.tasks.get_task(rtxn, id)?.ok_or(Error::CorruptedTaskQueue)?; - current_batch.processing(Some(&mut task)); - let (new_uid) = match &task.kind { - KindWithContent::IndexRename { new_index_uid, .. } => new_index_uid.clone(), - _ => unreachable!(), - }; - Ok(Some(Batch::IndexRename { index_uid, new_index_uid: new_uid, task })) + Ok(Some(Batch::IndexUpdate { index_uid, primary_key, new_index_uid, task })) } BatchKind::IndexDeletion { ids } => Ok(Some(Batch::IndexDeletion { index_uid, diff --git a/crates/index-scheduler/src/scheduler/process_batch.rs b/crates/index-scheduler/src/scheduler/process_batch.rs index d801287a7..a82324fc1 100644 --- a/crates/index-scheduler/src/scheduler/process_batch.rs +++ b/crates/index-scheduler/src/scheduler/process_batch.rs @@ -15,7 +15,7 @@ use super::create_batch::Batch; use crate::processing::{ AtomicBatchStep, AtomicTaskStep, CreateIndexProgress, DeleteIndexProgress, FinalizingIndexStep, InnerSwappingTwoIndexes, SwappingTheIndexes, TaskCancelationProgress, TaskDeletionProgress, - UpdateIndexProgress, RenameIndexProgress, + UpdateIndexProgress, }; use crate::utils::{ self, remove_n_tasks_datetime_earlier_than, remove_task_datetime, swap_index_uid_in_task, @@ -224,38 +224,47 @@ impl IndexScheduler { self.index_mapper.create_index(wtxn, &index_uid, None)?; self.process_batch( - Batch::IndexUpdate { index_uid, primary_key, task }, + Batch::IndexUpdate { index_uid, primary_key, new_index_uid: None, task }, current_batch, progress, ) } - Batch::IndexRename { index_uid, new_index_uid, mut task } => { - progress.update_progress(RenameIndexProgress::RenamingTheIndex); - let mut wtxn = self.env.write_txn()?; - self.index_mapper.rename(&mut wtxn, &index_uid, &new_index_uid)?; - self.queue.tasks.update_index(&mut wtxn, &new_index_uid, |bm| { - let old = self.queue.tasks.index_tasks(&wtxn, &index_uid).unwrap_or_default(); - *bm |= &old; - })?; - self.queue.tasks.update_index(&mut wtxn, &index_uid, |bm| bm.clear())?; - wtxn.commit()?; - task.status = Status::Succeeded; - task.details = Some(Details::IndexRename(IndexRenameDetails { old_uid: index_uid, new_uid: new_index_uid })); - Ok((vec![task], ProcessBatchInfo::default())) - } - Batch::IndexUpdate { index_uid, primary_key, mut task } => { + Batch::IndexUpdate { index_uid, primary_key, new_index_uid, mut task } => { progress.update_progress(UpdateIndexProgress::UpdatingTheIndex); - let rtxn = self.env.read_txn()?; - let index = self.index_mapper.index(&rtxn, &index_uid)?; - if let Some(primary_key) = primary_key.clone() { + // Handle rename if new_index_uid is provided + let final_index_uid = if let Some(new_uid) = &new_index_uid { + let mut wtxn = self.env.write_txn()?; + + // Rename the index + self.index_mapper.rename(&mut wtxn, &index_uid, new_uid)?; + + // Update the task index mappings + let old_tasks = + self.queue.tasks.index_tasks(&wtxn, &index_uid).unwrap_or_default(); + self.queue.tasks.update_index(&mut wtxn, new_uid, |bm| { + *bm |= &old_tasks; + })?; + self.queue.tasks.update_index(&mut wtxn, &index_uid, |bm| bm.clear())?; + wtxn.commit()?; + + new_uid.clone() + } else { + index_uid.clone() + }; + // Get the index (renamed or not) + let rtxn = self.env.read_txn()?; + let index = self.index_mapper.index(&rtxn, &final_index_uid)?; + + // Handle primary key update if provided + if let Some(ref primary_key) = primary_key { let mut index_wtxn = index.write_txn()?; let mut builder = MilliSettings::new( &mut index_wtxn, &index, self.index_mapper.indexer_config(), ); - builder.set_primary_key(primary_key); + builder.set_primary_key(primary_key.clone()); let must_stop_processing = self.scheduler.must_stop_processing.clone(); builder @@ -264,7 +273,7 @@ impl IndexScheduler { &progress, current_batch.embedder_stats.clone(), ) - .map_err(|e| Error::from_milli(e, Some(index_uid.to_string())))?; + .map_err(|e| Error::from_milli(e, Some(final_index_uid.to_string())))?; index_wtxn.commit()?; } @@ -272,7 +281,10 @@ impl IndexScheduler { rtxn.commit()?; task.status = Status::Succeeded; - task.details = Some(Details::IndexInfo { primary_key }); + task.details = Some(Details::IndexInfo { + primary_key: primary_key.clone(), + new_uid: new_index_uid.clone(), + }); // if the update processed successfully, we're going to store the new // stats of the index. Since the tasks have already been processed and @@ -282,8 +294,8 @@ impl IndexScheduler { let mut wtxn = self.env.write_txn()?; let index_rtxn = index.read_txn()?; let stats = crate::index_mapper::IndexStats::new(&index, &index_rtxn) - .map_err(|e| Error::from_milli(e, Some(index_uid.clone())))?; - self.index_mapper.store_stats_of(&mut wtxn, &index_uid, &stats)?; + .map_err(|e| Error::from_milli(e, Some(final_index_uid.clone())))?; + self.index_mapper.store_stats_of(&mut wtxn, &final_index_uid, &stats)?; wtxn.commit()?; Ok(()) }(); diff --git a/crates/index-scheduler/src/utils.rs b/crates/index-scheduler/src/utils.rs index 3c921f099..91bba35d7 100644 --- a/crates/index-scheduler/src/utils.rs +++ b/crates/index-scheduler/src/utils.rs @@ -264,7 +264,12 @@ pub fn swap_index_uid_in_task(task: &mut Task, swap: (&str, &str)) { K::SettingsUpdate { index_uid, .. } => index_uids.push(index_uid), K::IndexDeletion { index_uid } => index_uids.push(index_uid), K::IndexCreation { index_uid, .. } => index_uids.push(index_uid), - K::IndexUpdate { index_uid, .. } => index_uids.push(index_uid), + K::IndexUpdate { index_uid, new_index_uid, .. } => { + index_uids.push(index_uid); + if let Some(new_uid) = new_index_uid { + index_uids.push(new_uid); + } + } K::IndexSwap { swaps } => { for IndexSwap { indexes: (lhs, rhs) } in swaps.iter_mut() { if lhs == swap.0 || lhs == swap.1 { @@ -496,9 +501,9 @@ impl crate::IndexScheduler { Details::SettingsUpdate { settings: _ } => { assert_eq!(kind.as_kind(), Kind::SettingsUpdate); } - Details::IndexInfo { primary_key: pk1 } => match &kind { + Details::IndexInfo { primary_key: pk1, .. } => match &kind { KindWithContent::IndexCreation { index_uid, primary_key: pk2 } - | KindWithContent::IndexUpdate { index_uid, primary_key: pk2 } => { + | KindWithContent::IndexUpdate { index_uid, primary_key: pk2, .. } => { self.queue .tasks .index_tasks diff --git a/crates/meilisearch-types/src/task_view.rs b/crates/meilisearch-types/src/task_view.rs index 7521137c0..460ae68d7 100644 --- a/crates/meilisearch-types/src/task_view.rs +++ b/crates/meilisearch-types/src/task_view.rs @@ -132,6 +132,11 @@ pub struct DetailsView { pub payload_size: Option, #[serde(skip_serializing_if = "Option::is_none")] pub indexes: Option>, + // index rename + #[serde(skip_serializing_if = "Option::is_none")] + pub old_index_uid: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub new_index_uid: Option, } impl DetailsView { @@ -292,6 +297,18 @@ impl DetailsView { (None, Some(to)) | (Some(to), None) => Some(to), (Some(_), Some(to)) => Some(to), }, + old_index_uid: match (self.old_index_uid.clone(), other.old_index_uid.clone()) { + (None, None) => None, + (None, Some(uid)) | (Some(uid), None) => Some(uid), + // We should never be able to batch multiple renames at the same time. + (Some(left), Some(_right)) => Some(left), + }, + new_index_uid: match (self.new_index_uid.clone(), other.new_index_uid.clone()) { + (None, None) => None, + (None, Some(uid)) | (Some(uid), None) => Some(uid), + // We should never be able to batch multiple renames at the same time. + (Some(left), Some(_right)) => Some(left), + }, } } } @@ -324,9 +341,11 @@ impl From
for DetailsView { settings.hide_secrets(); DetailsView { settings: Some(settings), ..DetailsView::default() } } - Details::IndexInfo { primary_key } => { - DetailsView { primary_key: Some(primary_key), ..DetailsView::default() } - } + Details::IndexInfo { primary_key, new_uid } => DetailsView { + primary_key: Some(primary_key), + new_index_uid: new_uid.clone(), + ..DetailsView::default() + }, Details::DocumentDeletion { provided_ids: received_document_ids, deleted_documents, diff --git a/crates/meilisearch-types/src/tasks.rs b/crates/meilisearch-types/src/tasks.rs index 64826c693..f7b59b299 100644 --- a/crates/meilisearch-types/src/tasks.rs +++ b/crates/meilisearch-types/src/tasks.rs @@ -140,10 +140,7 @@ pub enum KindWithContent { IndexUpdate { index_uid: String, primary_key: Option, - }, - IndexRename { - index_uid: String, - new_index_uid: String, + new_index_uid: Option, }, IndexSwap { swaps: Vec, @@ -178,13 +175,6 @@ pub struct IndexSwap { pub indexes: (String, String), } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] -#[serde(rename_all = "camelCase")] -pub struct IndexRenameDetails { - pub old_uid: String, - pub new_uid: String, -} - #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)] #[serde(rename_all = "camelCase")] pub struct ExportIndexSettings { @@ -204,7 +194,6 @@ impl KindWithContent { KindWithContent::IndexCreation { .. } => Kind::IndexCreation, KindWithContent::IndexDeletion { .. } => Kind::IndexDeletion, KindWithContent::IndexUpdate { .. } => Kind::IndexUpdate, - KindWithContent::IndexRename { .. } => Kind::IndexRename, KindWithContent::IndexSwap { .. } => Kind::IndexSwap, KindWithContent::TaskCancelation { .. } => Kind::TaskCancelation, KindWithContent::TaskDeletion { .. } => Kind::TaskDeletion, @@ -232,9 +221,14 @@ impl KindWithContent { | DocumentClear { index_uid } | SettingsUpdate { index_uid, .. } | IndexCreation { index_uid, .. } - | IndexUpdate { index_uid, .. } | IndexDeletion { index_uid } => vec![index_uid], - IndexRename { index_uid, new_index_uid } => vec![index_uid, new_index_uid], + IndexUpdate { index_uid, new_index_uid, .. } => { + let mut indexes = vec![index_uid.as_str()]; + if let Some(new_uid) = new_index_uid { + indexes.push(new_uid.as_str()); + } + indexes + } IndexSwap { swaps } => { let mut indexes = HashSet::<&str>::default(); for swap in swaps { @@ -283,13 +277,12 @@ impl KindWithContent { KindWithContent::SettingsUpdate { new_settings, .. } => { Some(Details::SettingsUpdate { settings: new_settings.clone() }) } - KindWithContent::IndexCreation { primary_key, .. } - | KindWithContent::IndexUpdate { primary_key, .. } => { - Some(Details::IndexInfo { primary_key: primary_key.clone() }) + KindWithContent::IndexCreation { primary_key, .. } => { + Some(Details::IndexInfo { primary_key: primary_key.clone(), new_uid: None }) } - KindWithContent::IndexRename { index_uid, new_index_uid } => { - Some(Details::IndexRename { - old_uid: index_uid.clone(), + KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => { + Some(Details::IndexInfo { + primary_key: primary_key.clone(), new_uid: new_index_uid.clone(), }) } @@ -363,16 +356,15 @@ impl KindWithContent { Some(Details::SettingsUpdate { settings: new_settings.clone() }) } KindWithContent::IndexDeletion { .. } => None, - KindWithContent::IndexRename { index_uid, new_index_uid } => { - Some(Details::IndexRename { - old_uid: index_uid.clone(), + KindWithContent::IndexCreation { primary_key, .. } => { + Some(Details::IndexInfo { primary_key: primary_key.clone(), new_uid: None }) + } + KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => { + Some(Details::IndexInfo { + primary_key: primary_key.clone(), new_uid: new_index_uid.clone(), }) } - KindWithContent::IndexCreation { primary_key, .. } - | KindWithContent::IndexUpdate { primary_key, .. } => { - Some(Details::IndexInfo { primary_key: primary_key.clone() }) - } KindWithContent::IndexSwap { .. } => { todo!() } @@ -426,10 +418,13 @@ impl From<&KindWithContent> for Option
{ } KindWithContent::IndexDeletion { .. } => None, KindWithContent::IndexCreation { primary_key, .. } => { - Some(Details::IndexInfo { primary_key: primary_key.clone() }) + Some(Details::IndexInfo { primary_key: primary_key.clone(), new_uid: None }) } - KindWithContent::IndexUpdate { primary_key, .. } => { - Some(Details::IndexInfo { primary_key: primary_key.clone() }) + KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => { + Some(Details::IndexInfo { + primary_key: primary_key.clone(), + new_uid: new_index_uid.clone(), + }) } KindWithContent::IndexSwap { .. } => None, KindWithContent::TaskCancelation { query, tasks } => Some(Details::TaskCancelation { @@ -563,7 +558,6 @@ pub enum Kind { IndexCreation, IndexDeletion, IndexUpdate, - IndexRename, IndexSwap, TaskCancelation, TaskDeletion, @@ -582,8 +576,7 @@ impl Kind { | Kind::SettingsUpdate | Kind::IndexCreation | Kind::IndexDeletion - | Kind::IndexUpdate - | Kind::IndexRename => true, + | Kind::IndexUpdate => true, Kind::IndexSwap | Kind::TaskCancelation | Kind::TaskDeletion @@ -604,7 +597,6 @@ impl Display for Kind { Kind::IndexCreation => write!(f, "indexCreation"), Kind::IndexDeletion => write!(f, "indexDeletion"), Kind::IndexUpdate => write!(f, "indexUpdate"), - Kind::IndexRename => write!(f, "indexRename"), Kind::IndexSwap => write!(f, "indexSwap"), Kind::TaskCancelation => write!(f, "taskCancelation"), Kind::TaskDeletion => write!(f, "taskDeletion"), @@ -623,8 +615,6 @@ impl FromStr for Kind { Ok(Kind::IndexCreation) } else if kind.eq_ignore_ascii_case("indexUpdate") { Ok(Kind::IndexUpdate) - } else if kind.eq_ignore_ascii_case("indexRename") { - Ok(Kind::IndexRename) } else if kind.eq_ignore_ascii_case("indexSwap") { Ok(Kind::IndexSwap) } else if kind.eq_ignore_ascii_case("indexDeletion") { @@ -687,6 +677,7 @@ pub enum Details { }, IndexInfo { primary_key: Option, + new_uid: Option, }, DocumentDeletion { provided_ids: usize, @@ -722,7 +713,6 @@ pub enum Details { IndexSwap { swaps: Vec, }, - IndexRename(IndexRenameDetails), Export { url: String, api_key: Option, @@ -768,7 +758,6 @@ impl Details { Self::SettingsUpdate { .. } | Self::IndexInfo { .. } | Self::Dump { .. } - | Self::IndexRename { .. } | Self::Export { .. } | Self::UpgradeDatabase { .. } | Self::IndexSwap { .. } => (), diff --git a/crates/meilisearch/src/routes/indexes/mod.rs b/crates/meilisearch/src/routes/indexes/mod.rs index 04b3e12c4..632922542 100644 --- a/crates/meilisearch/src/routes/indexes/mod.rs +++ b/crates/meilisearch/src/routes/indexes/mod.rs @@ -375,6 +375,9 @@ pub struct UpdateIndexRequest { /// The new primary key of the index #[deserr(default, error = DeserrJsonError)] primary_key: Option, + /// The new uid of the index (for renaming) + #[deserr(default, error = DeserrJsonError)] + uid: Option, } /// Update index @@ -419,6 +422,12 @@ pub async fn update_index( debug!(parameters = ?body, "Update index"); let index_uid = IndexUid::try_from(index_uid.into_inner())?; let body = body.into_inner(); + + // Validate new uid if provided + if let Some(ref new_uid) = body.uid { + let _ = IndexUid::try_from(new_uid.clone())?; + } + analytics.publish( IndexUpdatedAggregate { primary_key: body.primary_key.iter().cloned().collect() }, &req, @@ -427,6 +436,7 @@ pub async fn update_index( let task = KindWithContent::IndexUpdate { index_uid: index_uid.into_inner(), primary_key: body.primary_key, + new_index_uid: body.uid, }; let uid = get_task_id(&req, &opt)?; diff --git a/crates/meilisearch/tests/index/mod.rs b/crates/meilisearch/tests/index/mod.rs index 5df5e7e97..f3cb7fde9 100644 --- a/crates/meilisearch/tests/index/mod.rs +++ b/crates/meilisearch/tests/index/mod.rs @@ -2,5 +2,6 @@ mod create_index; mod delete_index; mod errors; mod get_index; +mod rename_index; mod stats; mod update_index; diff --git a/crates/meilisearch/tests/index/rename_index.rs b/crates/meilisearch/tests/index/rename_index.rs new file mode 100644 index 000000000..793b079f1 --- /dev/null +++ b/crates/meilisearch/tests/index/rename_index.rs @@ -0,0 +1,419 @@ +use crate::common::{shared_does_not_exists_index, Server}; +use crate::json; + +#[actix_rt::test] +async fn rename_index_via_patch() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index first + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Rename via PATCH update endpoint + let new_uid = format!("{}_renamed", index.uid); + let body = json!({ "uid": &new_uid }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + let response = server.wait_task(task.uid()).await.succeeded(); + + // Verify the rename succeeded + assert_eq!(response["status"], "succeeded"); + assert_eq!(response["type"], "indexUpdate"); + assert_eq!(response["details"]["newIndexUid"], new_uid); + + // Check that old index doesn't exist + let (_, code) = index.get().await; + assert_eq!(code, 404); + + // Check that new index exists + let (response, code) = server.service.get(format!("/indexes/{}", new_uid)).await; + assert_eq!(code, 200); + assert_eq!(response["uid"], new_uid); +} + +#[actix_rt::test] +async fn rename_to_existing_index_via_patch() { + let server = Server::new_shared(); + let index1 = server.unique_index(); + let index2 = server.unique_index(); + + // Create both indexes + let (task, code) = index1.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + let (task, code) = index2.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Try to rename index1 to index2's uid via PATCH (should fail) + let body = json!({ "uid": index2.uid }); + let (task, code) = index1.service.patch(format!("/indexes/{}", index1.uid), body).await; + + assert_eq!(code, 202); + let response = server.wait_task(task.uid()).await.failed(); + + let expected_response = json!({ + "message": format!("Index `{}` already exists.", index2.uid), + "code": "index_already_exists", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#index_already_exists" + }); + + assert_eq!(response["error"], expected_response); +} + +#[actix_rt::test] +async fn rename_non_existent_index_via_patch() { + let server = Server::new_shared(); + let index = shared_does_not_exists_index().await; + + // Try to rename non-existent index via PATCH + let body = json!({ "uid": "new_name" }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + let response = server.wait_task(task.uid()).await.failed(); + + let expected_response = json!({ + "message": format!("Index `{}` not found.", index.uid), + "code": "index_not_found", + "type": "invalid_request", + "link": "https://docs.meilisearch.com/errors#index_not_found" + }); + + assert_eq!(response["error"], expected_response); +} + +#[actix_rt::test] +async fn rename_with_invalid_uid_via_patch() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index first + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Try to rename with invalid uid via PATCH + let body = json!({ "uid": "Invalid UID!" }); + let (_, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 400); +} + +#[actix_rt::test] +async fn rename_index_with_documents_via_patch() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index and add documents + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + let documents = json!([ + { "id": 1, "title": "Movie 1" }, + { "id": 2, "title": "Movie 2" } + ]); + let (task, code) = index.add_documents(documents, None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Rename the index via PATCH + let new_uid = format!("{}_renamed", index.uid); + let body = json!({ "uid": &new_uid }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Verify documents are accessible in renamed index + let (response, code) = server.service.get(format!("/indexes/{}/documents", new_uid)).await; + assert_eq!(code, 200); + assert_eq!(response["results"].as_array().unwrap().len(), 2); +} + +#[actix_rt::test] +async fn rename_index_and_update_primary_key_via_patch() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index without primary key + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Rename index and set primary key at the same time + let new_uid = format!("{}_renamed", index.uid); + let body = json!({ + "uid": &new_uid, + "primaryKey": "id" + }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + let response = server.wait_task(task.uid()).await.succeeded(); + + // Verify the rename succeeded and primary key was set + assert_eq!(response["status"], "succeeded"); + assert_eq!(response["type"], "indexUpdate"); + assert_eq!(response["details"]["newIndexUid"], new_uid); + assert_eq!(response["details"]["primaryKey"], "id"); + + // Check that old index doesn't exist + let (_, code) = index.get().await; + assert_eq!(code, 404); + + // Check that new index exists with correct primary key + let (response, code) = server.service.get(format!("/indexes/{}", new_uid)).await; + assert_eq!(code, 200); + assert_eq!(response["uid"], new_uid); + assert_eq!(response["primaryKey"], "id"); +} + +#[actix_rt::test] +async fn rename_index_and_verify_stats() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index and add documents + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + let documents = json!([ + { "id": 1, "title": "Movie 1", "genre": "Action" }, + { "id": 2, "title": "Movie 2", "genre": "Drama" }, + { "id": 3, "title": "Movie 3", "genre": "Comedy" } + ]); + let (task, code) = index.add_documents(documents, None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Get stats before rename + let (stats_before, code) = index.stats().await; + assert_eq!(code, 200); + assert_eq!(stats_before["numberOfDocuments"], 3); + + // Rename the index + let new_uid = format!("{}_renamed", index.uid); + let body = json!({ "uid": &new_uid }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Get stats after rename using the new uid + let (stats_after, code) = server.service.get(format!("/indexes/{}/stats", new_uid)).await; + assert_eq!(code, 200); + assert_eq!(stats_after["numberOfDocuments"], 3); + assert_eq!(stats_after["numberOfDocuments"], stats_before["numberOfDocuments"]); +} + +#[actix_rt::test] +async fn rename_index_preserves_settings() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Configure settings + let settings = json!({ + "searchableAttributes": ["title", "description"], + "filterableAttributes": ["genre", "year"], + "sortableAttributes": ["year"], + "rankingRules": [ + "words", + "typo", + "proximity", + "attribute", + "sort", + "exactness" + ], + "stopWords": ["the", "a", "an"], + "synonyms": { + "movie": ["film", "picture"], + "great": ["awesome", "excellent"] + } + }); + + let (task, code) = index.update_settings(settings.clone()).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Rename the index + let new_uid = format!("{}_renamed", index.uid); + let body = json!({ "uid": &new_uid }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Verify settings are preserved + let (settings_after, code) = server.service.get(format!("/indexes/{}/settings", new_uid)).await; + assert_eq!(code, 200); + + assert_eq!(settings_after["searchableAttributes"], json!(["title", "description"])); + assert_eq!(settings_after["filterableAttributes"], json!(["genre", "year"])); + assert_eq!(settings_after["sortableAttributes"], json!(["year"])); + + // Check stopWords contains the same items (order may vary) + let stop_words = settings_after["stopWords"].as_array().unwrap(); + assert_eq!(stop_words.len(), 3); + assert!(stop_words.contains(&json!("the"))); + assert!(stop_words.contains(&json!("a"))); + assert!(stop_words.contains(&json!("an"))); + + assert_eq!(settings_after["synonyms"]["movie"], json!(["film", "picture"])); + assert_eq!(settings_after["synonyms"]["great"], json!(["awesome", "excellent"])); +} + +#[actix_rt::test] +async fn rename_index_preserves_search_functionality() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index and add documents + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + let documents = json!([ + { "id": 1, "title": "The Matrix", "genre": "Sci-Fi", "year": 1999 }, + { "id": 2, "title": "Inception", "genre": "Sci-Fi", "year": 2010 }, + { "id": 3, "title": "The Dark Knight", "genre": "Action", "year": 2008 } + ]); + let (task, code) = index.add_documents(documents, None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Make settings filterable + let settings = json!({ + "filterableAttributes": ["genre", "year"], + "sortableAttributes": ["year"] + }); + let (task, code) = index.update_settings(settings).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Search before rename + let search_params = json!({ + "q": "matrix", + "filter": "genre = 'Sci-Fi'", + "sort": ["year:asc"] + }); + let (results_before, code) = index.search_post(search_params.clone()).await; + assert_eq!(code, 200); + assert_eq!(results_before["hits"].as_array().unwrap().len(), 1); + assert_eq!(results_before["hits"][0]["title"], "The Matrix"); + + // Rename the index + let new_uid = format!("{}_renamed", index.uid); + let body = json!({ "uid": &new_uid }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Search after rename + let (results_after, code) = + server.service.post(format!("/indexes/{}/search", new_uid), search_params).await; + assert_eq!(code, 200); + assert_eq!(results_after["hits"].as_array().unwrap().len(), 1); + assert_eq!(results_after["hits"][0]["title"], "The Matrix"); + + // Verify facet search also works + let facet_search = json!({ + "facetQuery": "Sci", + "facetName": "genre" + }); + let (facet_results, code) = + server.service.post(format!("/indexes/{}/facet-search", new_uid), facet_search).await; + assert_eq!(code, 200); + assert_eq!(facet_results["facetHits"].as_array().unwrap().len(), 1); + assert_eq!(facet_results["facetHits"][0]["value"], "Sci-Fi"); +} + +#[actix_rt::test] +async fn rename_index_with_pending_tasks() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Add initial documents + let documents = json!([ + { "id": 1, "title": "Document 1" } + ]); + let (task, code) = index.add_documents(documents, None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Start a rename + let new_uid = format!("{}_renamed", index.uid); + let body = json!({ "uid": &new_uid }); + let (rename_task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + assert_eq!(code, 202); + + // Try to add documents to the old index while rename is pending + let more_documents = json!([ + { "id": 2, "title": "Document 2" } + ]); + let (_, code) = index.add_documents(more_documents, None).await; + assert_eq!(code, 202); + + // Wait for rename to complete + server.wait_task(rename_task.uid()).await.succeeded(); + + // Add documents to the new index + let final_documents = json!([ + { "id": 3, "title": "Document 3" } + ]); + let (task, code) = + server.service.post(format!("/indexes/{}/documents", new_uid), final_documents).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Verify all documents are accessible + let (response, code) = server.service.get(format!("/indexes/{}/documents", new_uid)).await; + assert_eq!(code, 200); + let docs = response["results"].as_array().unwrap(); + assert!(!docs.is_empty()); // At least the initial document should be there +} + +#[actix_rt::test] +async fn rename_index_to_same_name() { + let server = Server::new_shared(); + let index = server.unique_index(); + + // Create index + let (task, code) = index.create(None).await; + assert_eq!(code, 202); + server.wait_task(task.uid()).await.succeeded(); + + // Try to rename to the same name + let body = json!({ "uid": index.uid }); + let (task, code) = index.service.patch(format!("/indexes/{}", index.uid), body).await; + + assert_eq!(code, 202); + let response = server.wait_task(task.uid()).await.failed(); + + // Should fail with index already exists error + assert_eq!(response["status"], "failed"); + assert_eq!(response["type"], "indexUpdate"); + assert_eq!(response["error"]["code"], "index_already_exists"); + + // Index should still be accessible with original name + let (_, code) = index.get().await; + assert_eq!(code, 200); +}