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.

This commit is contained in:
Quentin de Quelen
2025-08-05 19:18:05 +02:00
committed by Tamo
parent 0f1c78b185
commit ae2d0a67a4
15 changed files with 547 additions and 119 deletions

View File

@ -129,6 +129,7 @@ pub enum KindDump {
}, },
IndexUpdate { IndexUpdate {
primary_key: Option<String>, primary_key: Option<String>,
new_uid: Option<String>,
}, },
IndexSwap { IndexSwap {
swaps: Vec<IndexSwap>, swaps: Vec<IndexSwap>,
@ -210,8 +211,8 @@ impl From<KindWithContent> for KindDump {
KindWithContent::IndexCreation { primary_key, .. } => { KindWithContent::IndexCreation { primary_key, .. } => {
KindDump::IndexCreation { primary_key } KindDump::IndexCreation { primary_key }
} }
KindWithContent::IndexUpdate { primary_key, .. } => { KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => {
KindDump::IndexUpdate { primary_key } KindDump::IndexUpdate { primary_key, new_uid: new_index_uid }
} }
KindWithContent::IndexSwap { swaps } => KindDump::IndexSwap { swaps }, KindWithContent::IndexSwap { swaps } => KindDump::IndexSwap { swaps },
KindWithContent::TaskCancelation { query, tasks } => { KindWithContent::TaskCancelation { query, tasks } => {

View File

@ -85,7 +85,7 @@ impl CompatV5ToV6 {
v6::Kind::IndexCreation { primary_key } v6::Kind::IndexCreation { primary_key }
} }
v5::tasks::TaskContent::IndexUpdate { 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::IndexDeletion { .. } => v6::Kind::IndexDeletion,
v5::tasks::TaskContent::DocumentAddition { v5::tasks::TaskContent::DocumentAddition {
@ -141,7 +141,7 @@ impl CompatV5ToV6 {
v6::Details::SettingsUpdate { settings: Box::new(settings.into()) } v6::Details::SettingsUpdate { settings: Box::new(settings.into()) }
} }
v5::Details::IndexInfo { primary_key } => { v5::Details::IndexInfo { primary_key } => {
v6::Details::IndexInfo { primary_key } v6::Details::IndexInfo { primary_key, new_uid: None }
} }
v5::Details::DocumentDeletion { v5::Details::DocumentDeletion {
received_document_ids, received_document_ids,

View File

@ -197,9 +197,10 @@ impl<'a> Dump<'a> {
index_uid: task.index_uid.ok_or(Error::CorruptedDump)?, index_uid: task.index_uid.ok_or(Error::CorruptedDump)?,
primary_key, 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)?, index_uid: task.index_uid.ok_or(Error::CorruptedDump)?,
primary_key, primary_key,
new_index_uid: new_uid,
}, },
KindDump::IndexSwap { swaps } => KindWithContent::IndexSwap { swaps }, KindDump::IndexSwap { swaps } => KindWithContent::IndexSwap { swaps },
KindDump::TaskCancelation { query, tasks } => { KindDump::TaskCancelation { query, tasks } => {

View File

@ -274,8 +274,8 @@ fn snapshot_details(d: &Details) -> String {
Details::SettingsUpdate { settings } => { Details::SettingsUpdate { settings } => {
format!("{{ settings: {settings:?} }}") format!("{{ settings: {settings:?} }}")
} }
Details::IndexInfo { primary_key } => { Details::IndexInfo { primary_key, new_uid } => {
format!("{{ primary_key: {primary_key:?} }}") format!("{{ primary_key: {primary_key:?}, new_uid: {new_uid:?} }}")
} }
Details::DocumentDeletion { Details::DocumentDeletion {
provided_ids: received_document_ids, provided_ids: received_document_ids,

View File

@ -125,12 +125,6 @@ make_enum_progress! {
} }
} }
make_enum_progress! {
pub enum RenameIndexProgress {
RenamingTheIndex,
}
}
make_enum_progress! { make_enum_progress! {
pub enum DeleteIndexProgress { pub enum DeleteIndexProgress {
DeletingTheIndex, DeletingTheIndex,

View File

@ -24,7 +24,6 @@ enum AutobatchKind {
IndexCreation, IndexCreation,
IndexDeletion, IndexDeletion,
IndexUpdate, IndexUpdate,
IndexRename,
IndexSwap, IndexSwap,
} }
@ -68,7 +67,6 @@ impl From<KindWithContent> for AutobatchKind {
KindWithContent::IndexDeletion { .. } => AutobatchKind::IndexDeletion, KindWithContent::IndexDeletion { .. } => AutobatchKind::IndexDeletion,
KindWithContent::IndexCreation { .. } => AutobatchKind::IndexCreation, KindWithContent::IndexCreation { .. } => AutobatchKind::IndexCreation,
KindWithContent::IndexUpdate { .. } => AutobatchKind::IndexUpdate, KindWithContent::IndexUpdate { .. } => AutobatchKind::IndexUpdate,
KindWithContent::IndexRename { .. } => AutobatchKind::IndexRename,
KindWithContent::IndexSwap { .. } => AutobatchKind::IndexSwap, KindWithContent::IndexSwap { .. } => AutobatchKind::IndexSwap,
KindWithContent::TaskCancelation { .. } KindWithContent::TaskCancelation { .. }
| KindWithContent::TaskDeletion { .. } | KindWithContent::TaskDeletion { .. }
@ -117,9 +115,6 @@ pub enum BatchKind {
IndexUpdate { IndexUpdate {
id: TaskId, id: TaskId,
}, },
IndexRename {
id: TaskId,
},
IndexSwap { IndexSwap {
id: TaskId, id: TaskId,
}, },
@ -181,13 +176,6 @@ impl BatchKind {
)), )),
false, false,
), ),
K::IndexRename => (
Break((
BatchKind::IndexRename { id: task_id },
BatchStopReason::TaskCannotBeBatched { kind, id: task_id },
)),
false,
),
K::IndexSwap => ( K::IndexSwap => (
Break(( Break((
BatchKind::IndexSwap { id: task_id }, BatchKind::IndexSwap { id: task_id },
@ -299,8 +287,8 @@ impl BatchKind {
}; };
match (self, autobatch_kind) { match (self, autobatch_kind) {
// We don't batch any of these operations // 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 })), (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. // 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) => { (this, kind) if !index_already_exists && this.allow_index_creation() == Some(false) && kind.allow_index_creation() == Some(true) => {
Break((this, BatchStopReason::IndexCreationMismatch { id })) Break((this, BatchStopReason::IndexCreationMismatch { id }))

View File

@ -75,7 +75,11 @@ fn idx_create() -> KindWithContent {
} }
fn idx_update() -> 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 { fn idx_del() -> KindWithContent {

View File

@ -38,11 +38,7 @@ pub(crate) enum Batch {
IndexUpdate { IndexUpdate {
index_uid: String, index_uid: String,
primary_key: Option<String>, primary_key: Option<String>,
task: Task, new_index_uid: Option<String>,
},
IndexRename {
index_uid: String,
new_index_uid: String,
task: Task, task: Task,
}, },
IndexDeletion { IndexDeletion {
@ -113,8 +109,7 @@ impl Batch {
| Batch::Dump(task) | Batch::Dump(task)
| Batch::IndexCreation { task, .. } | Batch::IndexCreation { task, .. }
| Batch::Export { task } | Batch::Export { task }
| Batch::IndexUpdate { task, .. } | Batch::IndexUpdate { task, .. } => {
| Batch::IndexRename { task, .. } => {
RoaringBitmap::from_sorted_iter(std::iter::once(task.uid)).unwrap() RoaringBitmap::from_sorted_iter(std::iter::once(task.uid)).unwrap()
} }
Batch::SnapshotCreation(tasks) Batch::SnapshotCreation(tasks)
@ -159,7 +154,6 @@ impl Batch {
IndexOperation { op, .. } => Some(op.index_uid()), IndexOperation { op, .. } => Some(op.index_uid()),
IndexCreation { index_uid, .. } IndexCreation { index_uid, .. }
| IndexUpdate { index_uid, .. } | IndexUpdate { index_uid, .. }
| IndexRename { index_uid, .. }
| IndexDeletion { index_uid, .. } => Some(index_uid), | IndexDeletion { index_uid, .. } => Some(index_uid),
} }
} }
@ -178,7 +172,6 @@ impl fmt::Display for Batch {
Batch::IndexOperation { op, .. } => write!(f, "{op}")?, Batch::IndexOperation { op, .. } => write!(f, "{op}")?,
Batch::IndexCreation { .. } => f.write_str("IndexCreation")?, Batch::IndexCreation { .. } => f.write_str("IndexCreation")?,
Batch::IndexUpdate { .. } => f.write_str("IndexUpdate")?, Batch::IndexUpdate { .. } => f.write_str("IndexUpdate")?,
Batch::IndexRename { .. } => f.write_str("IndexRename")?,
Batch::IndexDeletion { .. } => f.write_str("IndexDeletion")?, Batch::IndexDeletion { .. } => f.write_str("IndexDeletion")?,
Batch::IndexSwap { .. } => f.write_str("IndexSwap")?, Batch::IndexSwap { .. } => f.write_str("IndexSwap")?,
Batch::Export { .. } => f.write_str("Export")?, Batch::Export { .. } => f.write_str("Export")?,
@ -413,21 +406,13 @@ impl IndexScheduler {
let mut task = let mut task =
self.queue.tasks.get_task(rtxn, id)?.ok_or(Error::CorruptedTaskQueue)?; self.queue.tasks.get_task(rtxn, id)?.ok_or(Error::CorruptedTaskQueue)?;
current_batch.processing(Some(&mut task)); current_batch.processing(Some(&mut task));
let primary_key = match &task.kind { let (primary_key, new_index_uid) = match &task.kind {
KindWithContent::IndexUpdate { primary_key, .. } => primary_key.clone(), KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => {
(primary_key.clone(), new_index_uid.clone())
}
_ => unreachable!(), _ => unreachable!(),
}; };
Ok(Some(Batch::IndexUpdate { index_uid, primary_key, task })) Ok(Some(Batch::IndexUpdate { index_uid, primary_key, new_index_uid, 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 }))
} }
BatchKind::IndexDeletion { ids } => Ok(Some(Batch::IndexDeletion { BatchKind::IndexDeletion { ids } => Ok(Some(Batch::IndexDeletion {
index_uid, index_uid,

View File

@ -15,7 +15,7 @@ use super::create_batch::Batch;
use crate::processing::{ use crate::processing::{
AtomicBatchStep, AtomicTaskStep, CreateIndexProgress, DeleteIndexProgress, FinalizingIndexStep, AtomicBatchStep, AtomicTaskStep, CreateIndexProgress, DeleteIndexProgress, FinalizingIndexStep,
InnerSwappingTwoIndexes, SwappingTheIndexes, TaskCancelationProgress, TaskDeletionProgress, InnerSwappingTwoIndexes, SwappingTheIndexes, TaskCancelationProgress, TaskDeletionProgress,
UpdateIndexProgress, RenameIndexProgress, UpdateIndexProgress,
}; };
use crate::utils::{ use crate::utils::{
self, remove_n_tasks_datetime_earlier_than, remove_task_datetime, swap_index_uid_in_task, 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.index_mapper.create_index(wtxn, &index_uid, None)?;
self.process_batch( self.process_batch(
Batch::IndexUpdate { index_uid, primary_key, task }, Batch::IndexUpdate { index_uid, primary_key, new_index_uid: None, task },
current_batch, current_batch,
progress, progress,
) )
} }
Batch::IndexRename { index_uid, new_index_uid, mut task } => { Batch::IndexUpdate { index_uid, primary_key, 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 } => {
progress.update_progress(UpdateIndexProgress::UpdatingTheIndex); 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 index_wtxn = index.write_txn()?;
let mut builder = MilliSettings::new( let mut builder = MilliSettings::new(
&mut index_wtxn, &mut index_wtxn,
&index, &index,
self.index_mapper.indexer_config(), 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(); let must_stop_processing = self.scheduler.must_stop_processing.clone();
builder builder
@ -264,7 +273,7 @@ impl IndexScheduler {
&progress, &progress,
current_batch.embedder_stats.clone(), 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()?; index_wtxn.commit()?;
} }
@ -272,7 +281,10 @@ impl IndexScheduler {
rtxn.commit()?; rtxn.commit()?;
task.status = Status::Succeeded; 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 // if the update processed successfully, we're going to store the new
// stats of the index. Since the tasks have already been processed and // 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 mut wtxn = self.env.write_txn()?;
let index_rtxn = index.read_txn()?; let index_rtxn = index.read_txn()?;
let stats = crate::index_mapper::IndexStats::new(&index, &index_rtxn) let stats = crate::index_mapper::IndexStats::new(&index, &index_rtxn)
.map_err(|e| Error::from_milli(e, Some(index_uid.clone())))?; .map_err(|e| Error::from_milli(e, Some(final_index_uid.clone())))?;
self.index_mapper.store_stats_of(&mut wtxn, &index_uid, &stats)?; self.index_mapper.store_stats_of(&mut wtxn, &final_index_uid, &stats)?;
wtxn.commit()?; wtxn.commit()?;
Ok(()) Ok(())
}(); }();

View File

@ -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::SettingsUpdate { index_uid, .. } => index_uids.push(index_uid),
K::IndexDeletion { 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::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 } => { K::IndexSwap { swaps } => {
for IndexSwap { indexes: (lhs, rhs) } in swaps.iter_mut() { for IndexSwap { indexes: (lhs, rhs) } in swaps.iter_mut() {
if lhs == swap.0 || lhs == swap.1 { if lhs == swap.0 || lhs == swap.1 {
@ -496,9 +501,9 @@ impl crate::IndexScheduler {
Details::SettingsUpdate { settings: _ } => { Details::SettingsUpdate { settings: _ } => {
assert_eq!(kind.as_kind(), Kind::SettingsUpdate); 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::IndexCreation { index_uid, primary_key: pk2 }
| KindWithContent::IndexUpdate { index_uid, primary_key: pk2 } => { | KindWithContent::IndexUpdate { index_uid, primary_key: pk2, .. } => {
self.queue self.queue
.tasks .tasks
.index_tasks .index_tasks

View File

@ -132,6 +132,11 @@ pub struct DetailsView {
pub payload_size: Option<String>, pub payload_size: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub indexes: Option<BTreeMap<String, DetailsExportIndexSettings>>, pub indexes: Option<BTreeMap<String, DetailsExportIndexSettings>>,
// index rename
#[serde(skip_serializing_if = "Option::is_none")]
pub old_index_uid: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub new_index_uid: Option<String>,
} }
impl DetailsView { impl DetailsView {
@ -292,6 +297,18 @@ impl DetailsView {
(None, Some(to)) | (Some(to), None) => Some(to), (None, Some(to)) | (Some(to), None) => Some(to),
(Some(_), Some(to)) => 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<Details> for DetailsView {
settings.hide_secrets(); settings.hide_secrets();
DetailsView { settings: Some(settings), ..DetailsView::default() } DetailsView { settings: Some(settings), ..DetailsView::default() }
} }
Details::IndexInfo { primary_key } => { Details::IndexInfo { primary_key, new_uid } => DetailsView {
DetailsView { primary_key: Some(primary_key), ..DetailsView::default() } primary_key: Some(primary_key),
} new_index_uid: new_uid.clone(),
..DetailsView::default()
},
Details::DocumentDeletion { Details::DocumentDeletion {
provided_ids: received_document_ids, provided_ids: received_document_ids,
deleted_documents, deleted_documents,

View File

@ -140,10 +140,7 @@ pub enum KindWithContent {
IndexUpdate { IndexUpdate {
index_uid: String, index_uid: String,
primary_key: Option<String>, primary_key: Option<String>,
}, new_index_uid: Option<String>,
IndexRename {
index_uid: String,
new_index_uid: String,
}, },
IndexSwap { IndexSwap {
swaps: Vec<IndexSwap>, swaps: Vec<IndexSwap>,
@ -178,13 +175,6 @@ pub struct IndexSwap {
pub indexes: (String, String), 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)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize, ToSchema)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct ExportIndexSettings { pub struct ExportIndexSettings {
@ -204,7 +194,6 @@ impl KindWithContent {
KindWithContent::IndexCreation { .. } => Kind::IndexCreation, KindWithContent::IndexCreation { .. } => Kind::IndexCreation,
KindWithContent::IndexDeletion { .. } => Kind::IndexDeletion, KindWithContent::IndexDeletion { .. } => Kind::IndexDeletion,
KindWithContent::IndexUpdate { .. } => Kind::IndexUpdate, KindWithContent::IndexUpdate { .. } => Kind::IndexUpdate,
KindWithContent::IndexRename { .. } => Kind::IndexRename,
KindWithContent::IndexSwap { .. } => Kind::IndexSwap, KindWithContent::IndexSwap { .. } => Kind::IndexSwap,
KindWithContent::TaskCancelation { .. } => Kind::TaskCancelation, KindWithContent::TaskCancelation { .. } => Kind::TaskCancelation,
KindWithContent::TaskDeletion { .. } => Kind::TaskDeletion, KindWithContent::TaskDeletion { .. } => Kind::TaskDeletion,
@ -232,9 +221,14 @@ impl KindWithContent {
| DocumentClear { index_uid } | DocumentClear { index_uid }
| SettingsUpdate { index_uid, .. } | SettingsUpdate { index_uid, .. }
| IndexCreation { index_uid, .. } | IndexCreation { index_uid, .. }
| IndexUpdate { index_uid, .. }
| IndexDeletion { index_uid } => vec![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 } => { IndexSwap { swaps } => {
let mut indexes = HashSet::<&str>::default(); let mut indexes = HashSet::<&str>::default();
for swap in swaps { for swap in swaps {
@ -283,13 +277,12 @@ impl KindWithContent {
KindWithContent::SettingsUpdate { new_settings, .. } => { KindWithContent::SettingsUpdate { new_settings, .. } => {
Some(Details::SettingsUpdate { settings: new_settings.clone() }) Some(Details::SettingsUpdate { settings: new_settings.clone() })
} }
KindWithContent::IndexCreation { primary_key, .. } KindWithContent::IndexCreation { primary_key, .. } => {
| KindWithContent::IndexUpdate { primary_key, .. } => { Some(Details::IndexInfo { primary_key: primary_key.clone(), new_uid: None })
Some(Details::IndexInfo { primary_key: primary_key.clone() })
} }
KindWithContent::IndexRename { index_uid, new_index_uid } => { KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => {
Some(Details::IndexRename { Some(Details::IndexInfo {
old_uid: index_uid.clone(), primary_key: primary_key.clone(),
new_uid: new_index_uid.clone(), new_uid: new_index_uid.clone(),
}) })
} }
@ -363,16 +356,15 @@ impl KindWithContent {
Some(Details::SettingsUpdate { settings: new_settings.clone() }) Some(Details::SettingsUpdate { settings: new_settings.clone() })
} }
KindWithContent::IndexDeletion { .. } => None, KindWithContent::IndexDeletion { .. } => None,
KindWithContent::IndexRename { index_uid, new_index_uid } => { KindWithContent::IndexCreation { primary_key, .. } => {
Some(Details::IndexRename { Some(Details::IndexInfo { primary_key: primary_key.clone(), new_uid: None })
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(), new_uid: new_index_uid.clone(),
}) })
} }
KindWithContent::IndexCreation { primary_key, .. }
| KindWithContent::IndexUpdate { primary_key, .. } => {
Some(Details::IndexInfo { primary_key: primary_key.clone() })
}
KindWithContent::IndexSwap { .. } => { KindWithContent::IndexSwap { .. } => {
todo!() todo!()
} }
@ -426,10 +418,13 @@ impl From<&KindWithContent> for Option<Details> {
} }
KindWithContent::IndexDeletion { .. } => None, KindWithContent::IndexDeletion { .. } => None,
KindWithContent::IndexCreation { primary_key, .. } => { 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, .. } => { KindWithContent::IndexUpdate { primary_key, new_index_uid, .. } => {
Some(Details::IndexInfo { primary_key: primary_key.clone() }) Some(Details::IndexInfo {
primary_key: primary_key.clone(),
new_uid: new_index_uid.clone(),
})
} }
KindWithContent::IndexSwap { .. } => None, KindWithContent::IndexSwap { .. } => None,
KindWithContent::TaskCancelation { query, tasks } => Some(Details::TaskCancelation { KindWithContent::TaskCancelation { query, tasks } => Some(Details::TaskCancelation {
@ -563,7 +558,6 @@ pub enum Kind {
IndexCreation, IndexCreation,
IndexDeletion, IndexDeletion,
IndexUpdate, IndexUpdate,
IndexRename,
IndexSwap, IndexSwap,
TaskCancelation, TaskCancelation,
TaskDeletion, TaskDeletion,
@ -582,8 +576,7 @@ impl Kind {
| Kind::SettingsUpdate | Kind::SettingsUpdate
| Kind::IndexCreation | Kind::IndexCreation
| Kind::IndexDeletion | Kind::IndexDeletion
| Kind::IndexUpdate | Kind::IndexUpdate => true,
| Kind::IndexRename => true,
Kind::IndexSwap Kind::IndexSwap
| Kind::TaskCancelation | Kind::TaskCancelation
| Kind::TaskDeletion | Kind::TaskDeletion
@ -604,7 +597,6 @@ impl Display for Kind {
Kind::IndexCreation => write!(f, "indexCreation"), Kind::IndexCreation => write!(f, "indexCreation"),
Kind::IndexDeletion => write!(f, "indexDeletion"), Kind::IndexDeletion => write!(f, "indexDeletion"),
Kind::IndexUpdate => write!(f, "indexUpdate"), Kind::IndexUpdate => write!(f, "indexUpdate"),
Kind::IndexRename => write!(f, "indexRename"),
Kind::IndexSwap => write!(f, "indexSwap"), Kind::IndexSwap => write!(f, "indexSwap"),
Kind::TaskCancelation => write!(f, "taskCancelation"), Kind::TaskCancelation => write!(f, "taskCancelation"),
Kind::TaskDeletion => write!(f, "taskDeletion"), Kind::TaskDeletion => write!(f, "taskDeletion"),
@ -623,8 +615,6 @@ impl FromStr for Kind {
Ok(Kind::IndexCreation) Ok(Kind::IndexCreation)
} else if kind.eq_ignore_ascii_case("indexUpdate") { } else if kind.eq_ignore_ascii_case("indexUpdate") {
Ok(Kind::IndexUpdate) Ok(Kind::IndexUpdate)
} else if kind.eq_ignore_ascii_case("indexRename") {
Ok(Kind::IndexRename)
} else if kind.eq_ignore_ascii_case("indexSwap") { } else if kind.eq_ignore_ascii_case("indexSwap") {
Ok(Kind::IndexSwap) Ok(Kind::IndexSwap)
} else if kind.eq_ignore_ascii_case("indexDeletion") { } else if kind.eq_ignore_ascii_case("indexDeletion") {
@ -687,6 +677,7 @@ pub enum Details {
}, },
IndexInfo { IndexInfo {
primary_key: Option<String>, primary_key: Option<String>,
new_uid: Option<String>,
}, },
DocumentDeletion { DocumentDeletion {
provided_ids: usize, provided_ids: usize,
@ -722,7 +713,6 @@ pub enum Details {
IndexSwap { IndexSwap {
swaps: Vec<IndexSwap>, swaps: Vec<IndexSwap>,
}, },
IndexRename(IndexRenameDetails),
Export { Export {
url: String, url: String,
api_key: Option<String>, api_key: Option<String>,
@ -768,7 +758,6 @@ impl Details {
Self::SettingsUpdate { .. } Self::SettingsUpdate { .. }
| Self::IndexInfo { .. } | Self::IndexInfo { .. }
| Self::Dump { .. } | Self::Dump { .. }
| Self::IndexRename { .. }
| Self::Export { .. } | Self::Export { .. }
| Self::UpgradeDatabase { .. } | Self::UpgradeDatabase { .. }
| Self::IndexSwap { .. } => (), | Self::IndexSwap { .. } => (),

View File

@ -375,6 +375,9 @@ pub struct UpdateIndexRequest {
/// The new primary key of the index /// The new primary key of the index
#[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)] #[deserr(default, error = DeserrJsonError<InvalidIndexPrimaryKey>)]
primary_key: Option<String>, primary_key: Option<String>,
/// The new uid of the index (for renaming)
#[deserr(default, error = DeserrJsonError<InvalidIndexUid>)]
uid: Option<String>,
} }
/// Update index /// Update index
@ -419,6 +422,12 @@ pub async fn update_index(
debug!(parameters = ?body, "Update index"); debug!(parameters = ?body, "Update index");
let index_uid = IndexUid::try_from(index_uid.into_inner())?; let index_uid = IndexUid::try_from(index_uid.into_inner())?;
let body = body.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( analytics.publish(
IndexUpdatedAggregate { primary_key: body.primary_key.iter().cloned().collect() }, IndexUpdatedAggregate { primary_key: body.primary_key.iter().cloned().collect() },
&req, &req,
@ -427,6 +436,7 @@ pub async fn update_index(
let task = KindWithContent::IndexUpdate { let task = KindWithContent::IndexUpdate {
index_uid: index_uid.into_inner(), index_uid: index_uid.into_inner(),
primary_key: body.primary_key, primary_key: body.primary_key,
new_index_uid: body.uid,
}; };
let uid = get_task_id(&req, &opt)?; let uid = get_task_id(&req, &opt)?;

View File

@ -2,5 +2,6 @@ mod create_index;
mod delete_index; mod delete_index;
mod errors; mod errors;
mod get_index; mod get_index;
mod rename_index;
mod stats; mod stats;
mod update_index; mod update_index;

View File

@ -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);
}