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:
@@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
use db::{Db, catalog, fields, vocab};
|
||||
use domain::{
|
||||
AuditActor, FieldType, LocalizedLabel, NewFieldDefinition, NewTerm, 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!("reindex_test_{}", uuid::Uuid::new_v4().simple())
|
||||
}
|
||||
|
||||
#[sqlx::test(migrations = "../db/migrations")]
|
||||
async fn reindex_resolves_term_labels_and_finds_by_label(pool: PgPool) {
|
||||
let db = Db::from_pool(pool);
|
||||
|
||||
// a material vocabulary with a "wood" term
|
||||
let material = vocab::create_vocabulary(db.pool(), "material")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let mut tx = db.pool().begin().await.unwrap();
|
||||
|
||||
let wood = vocab::add_term(
|
||||
&mut tx,
|
||||
&NewTerm {
|
||||
vocabulary_id: material.id,
|
||||
external_uri: None,
|
||||
labels: vec![LocalizedLabel {
|
||||
lang: "en".into(),
|
||||
label: "wood".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
fields::create_field_definition(
|
||||
&mut tx,
|
||||
&NewFieldDefinition {
|
||||
key: "material".into(),
|
||||
field_type: FieldType::Term {
|
||||
vocabulary_id: material.id,
|
||||
},
|
||||
required: false,
|
||||
group_key: None,
|
||||
labels: vec![LocalizedLabel {
|
||||
lang: "en".into(),
|
||||
label: "material".into(),
|
||||
}],
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let object_id = catalog::create_object(
|
||||
&mut tx,
|
||||
AuditActor::System,
|
||||
&ObjectInput {
|
||||
object_number: "LM-1".into(),
|
||||
object_name: "vase".into(),
|
||||
number_of_objects: 1,
|
||||
brief_description: None,
|
||||
current_location: None,
|
||||
current_owner: None,
|
||||
recorder: None,
|
||||
recording_date: None,
|
||||
visibility: Visibility::Public,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
// set the material field to the wood term
|
||||
let mut tx = db.pool().begin().await.unwrap();
|
||||
|
||||
catalog::set_object_fields(
|
||||
&mut tx,
|
||||
AuditActor::System,
|
||||
object_id,
|
||||
serde_json::json!({ "material": wood.to_string() })
|
||||
.as_object()
|
||||
.unwrap(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
tx.commit().await.unwrap();
|
||||
|
||||
let (url, key) = meili();
|
||||
let client = SearchClient::connect(&url, &key, &unique_index()).unwrap();
|
||||
|
||||
client.ensure_index().await.unwrap();
|
||||
client.reindex_all(&db).await.unwrap();
|
||||
|
||||
// found by the object name
|
||||
assert_eq!(client.search("vase").await.unwrap(), vec![object_id]);
|
||||
// found by the resolved TERM LABEL (not the uuid)
|
||||
assert_eq!(client.search("wood").await.unwrap(), vec![object_id]);
|
||||
}
|
||||
Reference in New Issue
Block a user