feat(api): on-write search reindex after catalogue writes (#17)

Wire best-effort Meilisearch index sync into the admin write paths
(create/update/delete/set_fields/set_visibility). Adds
SearchClient::sync_object (reindex if the object exists, remove if gone —
one uniform path), an optional AppState.search client, and a reindex
helper that logs failures via tracing without failing the committed
write. Server gains MEILI_URL/MEILI_MASTER_KEY/MEILI_INDEX config;
search stays disabled (no-op) when unset. reindex_all remains the
recovery path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-02 23:25:43 +02:00
parent c4e0c4c834
commit d15afda9b2
17 changed files with 299 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
use db::{Db, catalog};
use domain::{AuditActor, ObjectInput, Visibility};
use search::SearchClient;
use sqlx::PgPool;
fn meili() -> (String, String) {
(
std::env::var("MEILI_URL").expect("MEILI_URL must be set"),
std::env::var("MEILI_MASTER_KEY").expect("MEILI_MASTER_KEY must be set"),
)
}
fn unique_index() -> String {
format!("sync_test_{}", uuid::Uuid::new_v4().simple())
}
fn object(number: &str, name: &str) -> ObjectInput {
ObjectInput {
object_number: number.into(),
object_name: name.into(),
number_of_objects: 1,
brief_description: None,
current_location: None,
current_owner: None,
recorder: None,
recording_date: None,
visibility: Visibility::Draft,
}
}
#[sqlx::test(migrations = "../db/migrations")]
async fn sync_object_indexes_then_removes(pool: PgPool) {
let db = Db::from_pool(pool);
let mut tx = db.pool().begin().await.unwrap();
let id = catalog::create_object(&mut tx, AuditActor::System, &object("S-1", "lamp"))
.await
.unwrap();
tx.commit().await.unwrap();
let (url, key) = meili();
let client = SearchClient::connect(&url, &key, &unique_index()).unwrap();
client.ensure_index().await.unwrap();
// object exists -> sync indexes it
client.sync_object(&db, id).await.unwrap();
assert_eq!(client.search("lamp").await.unwrap(), vec![id]);
// object deleted -> sync removes it from the index
let mut tx = db.pool().begin().await.unwrap();
let existed = catalog::delete_object(&mut tx, AuditActor::System, id)
.await
.unwrap();
assert!(existed);
tx.commit().await.unwrap();
client.sync_object(&db, id).await.unwrap();
assert!(client.search("lamp").await.unwrap().is_empty());
}