feat(search): build documents resolving term/authority labels; reindex_all

Implements build_document in the search crate: resolves Term and Authority
flexible-field values to their human-readable labels so reindex_all produces
documents that Meilisearch can match on label text, not raw UUIDs.
Adds integration test covering the full reindex→search round-trip.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-02 12:08:07 +02:00
parent b8d198f150
commit 7b91989411
3 changed files with 490 additions and 15 deletions
+71 -5
View File
@@ -174,11 +174,77 @@ impl SearchClient {
}
}
/// Build a SearchDocument from an object (implemented in Task 2).
#[allow(clippy::unimplemented)] // Task 2
/// Build a [`SearchDocument`] from a catalogue object, resolving term and authority
/// references to their human-readable labels so Meilisearch can match on them.
pub async fn build_document(
_db: &Db,
_object: &CatalogueObject,
db: &Db,
object: &CatalogueObject,
) -> Result<SearchDocument, SearchError> {
unimplemented!("implemented in Task 2")
let mut fields_text = Vec::new();
if let Some(map) = object.fields.as_object() {
for (key, value) in map {
let Some(def) = db::fields::field_definition_by_key(db.pool(), key).await? else {
// Stale field with no definition — skip.
continue;
};
match def.field_type {
domain::FieldType::Text | domain::FieldType::Date => {
if let Some(s) = value.as_str() {
fields_text.push(s.to_owned());
}
}
domain::FieldType::Integer | domain::FieldType::Boolean => {
fields_text.push(value.to_string());
}
domain::FieldType::LocalizedText => {
if let Some(obj) = value.as_object() {
for v in obj.values() {
if let Some(s) = v.as_str() {
fields_text.push(s.to_owned());
}
}
}
}
domain::FieldType::Term { .. } => {
if let Some(term_id) = value
.as_str()
.and_then(|s| s.parse::<domain::TermId>().ok())
{
if let Some(term) = db::vocab::term_by_id(db.pool(), term_id).await? {
fields_text.extend(term.labels.into_iter().map(|l| l.label));
}
}
}
domain::FieldType::Authority { .. } => {
if let Some(authority_id) = value
.as_str()
.and_then(|s| s.parse::<domain::AuthorityId>().ok())
{
if let Some(authority) =
db::authority::authority_by_id(db.pool(), authority_id).await?
{
fields_text.extend(authority.labels.into_iter().map(|l| l.label));
}
}
}
}
}
}
Ok(SearchDocument {
id: object.id.to_string(),
object_number: object.object_number.clone(),
object_name: object.object_name.clone(),
brief_description: object.brief_description.clone(),
current_owner: object.current_owner.clone(),
recorder: object.recorder.clone(),
visibility: object.visibility.as_str().to_owned(),
fields_text,
})
}