diff --git a/publish/src/app.rs b/publish/src/app.rs index 888bba5..eed9633 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -1,7 +1,7 @@ use crate::rdf::ontology::Ontology; -use crate::rdf::vocab::{gl, rda}; use crate::rdf::term_helper::{TermHelper, TermHelperMut}; -use gl_search::{doc, Schema, SearchDocument, SearchIndex}; +use crate::rdf::vocab::{gl, rda}; +use gl_search::{Schema, SearchDocument, SearchIndex, doc}; use http::StatusCode; use iced::alignment::Horizontal; use iced::widget::button::Style; @@ -121,7 +121,9 @@ impl Publisher { document.add_text(Schema::field("comment"), comment.to_lowercase()); } - index.add(document).expect("Unable to add annotated IRI to search index"); + index + .add(document) + .expect("Unable to add annotated IRI to search index"); }); index.commit().expect("Unable to commit ontology to index"); @@ -669,8 +671,7 @@ impl Publisher { .on_press(Message::SearchResultClicked(result.clone())) .style(button::text) }); - let results_table = - scrollable(table([type_column, iri_column], &search_state.results)); + let results_table = scrollable(table([type_column, iri_column], &search_state.results)); return column![search_input, results_table].into(); } diff --git a/publish/src/rdf/mod.rs b/publish/src/rdf/mod.rs index 57a04a9..db70147 100644 --- a/publish/src/rdf/mod.rs +++ b/publish/src/rdf/mod.rs @@ -1,3 +1,3 @@ pub(crate) mod ontology; pub(crate) mod term_helper; -pub mod vocab; \ No newline at end of file +pub mod vocab; diff --git a/publish/src/rdf/ontology.rs b/publish/src/rdf/ontology.rs index d72ab1e..0310f68 100644 --- a/publish/src/rdf/ontology.rs +++ b/publish/src/rdf/ontology.rs @@ -1,4 +1,5 @@ use crate::error; +use crate::rdf::vocab::{gl, owl}; use oxigraph::io::{RdfFormat, RdfParser}; use oxigraph::model::vocab::{rdf, rdfs, xsd}; use oxigraph::model::{ @@ -8,7 +9,6 @@ use oxigraph::model::{ use oxigraph::sparql::{QueryResults, SparqlEvaluator}; use std::collections::{HashMap, HashSet}; use tracing::debug; -use crate::rdf::vocab::{gl, owl}; const PREFIXES: &[(&str, &str)] = &[ ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"), @@ -106,7 +106,10 @@ impl<'a> OntologyBuilder<'a> { } } - fn lookup_iri_information<'b>(dataset: &Dataset, classes: impl IntoIterator>) -> HashMap { + fn lookup_iri_information<'b>( + dataset: &Dataset, + classes: impl IntoIterator>, + ) -> HashMap { let class_filter = classes .into_iter() .map(|node| node.to_string()) @@ -114,16 +117,28 @@ impl<'a> OntologyBuilder<'a> { .join(","); let query = SparqlEvaluator::new() - .parse_query(format!(r#"PREFIX gl: + .parse_query( + format!( + r#"PREFIX gl: PREFIX rdfs: SELECT DISTINCT ?subject ?label ?comment ?read_only {{ ?subject a ?class . FILTER(?class IN ({class_filter})) - OPTIONAL {{ ?subject rdfs:label ?label }} - OPTIONAL {{ ?subject rdfs:comment ?comment }} + OPTIONAL {{ + ?subject rdfs:label ?label + FILTER (LANG(?label) = 'en' || LANG(?label) = '') + }} + OPTIONAL {{ + ?subject rdfs:comment ?comment + FILTER (LANG(?comment) = 'en' || LANG(?comment) = '') + }} OPTIONAL {{ ?subject gl:readOnly ?read_only }} -}}"#).as_str()).expect("Unable to parse property query"); +}}"# + ) + .as_str(), + ) + .expect("Unable to parse property query"); let mut results = HashMap::new(); if let QueryResults::Solutions(solutions) = @@ -133,7 +148,10 @@ SELECT DISTINCT ?subject ?label ?comment ?read_only {{ let subject = solution.get("subject").and_then(term_to_named_node); let label = solution.get("label").and_then(term_to_string); let comment = solution.get("comment").and_then(term_to_string); - let read_only = solution.get("read_only").and_then(term_to_boolean).unwrap_or(false); + let read_only = solution + .get("read_only") + .and_then(term_to_boolean) + .unwrap_or(false); if let Some(subject) = subject { let info = IriInformation { @@ -148,37 +166,6 @@ SELECT DISTINCT ?subject ?label ?comment ?read_only {{ results } - fn memoize(ontology: &mut Ontology) { - // Full-text search index field names - for quad in ontology - .dataset - .quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None) - { - if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject - && let TermRef::Literal(literal) = quad.object - { - ontology - .field_map - .insert(subject.into_owned(), literal.value().to_string()); - } - } - - // Catalog IDs (used to quickly filter full-text search results) - for quad in ontology - .dataset - .quads_for_pattern(None, Some(gl::CATALOG_ID), None, None) - { - if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject - && let TermRef::Literal(literal) = quad.object - { - if literal.datatype() == xsd::NON_NEGATIVE_INTEGER { - let value: u64 = literal.value().parse().expect("Failed to parse catalog ID from ontology. It ought to be a non-negative integer."); - ontology.catalog_ids.insert(subject.into_owned(), value); - } - } - } - } - pub fn build(&mut self) -> error::Result { let prefixes = PREFIXES .iter() @@ -194,31 +181,52 @@ SELECT DISTINCT ?subject ?label ?comment ?read_only {{ } Self::materialize_same_as(&mut dataset); - let properties = Self::lookup_iri_information(&dataset, [ - rdf::PROPERTY, - owl::ANNOTATION_PROPERTY, - owl::DATATYPE_PROPERTY, - owl::FUNCTIONAL_PROPERTY, - owl::OBJECT_PROPERTY, - owl::ONTOLOGY_PROPERTY, - ]); + let properties = Self::lookup_iri_information( + &dataset, + [ + rdf::PROPERTY, + rdfs::DATATYPE, + owl::ANNOTATION_PROPERTY, + owl::DATATYPE_PROPERTY, + owl::FUNCTIONAL_PROPERTY, + owl::OBJECT_PROPERTY, + owl::ONTOLOGY_PROPERTY, + ], + ); - let classes = Self::lookup_iri_information(&dataset, [ - rdfs::CLASS, - owl::CLASS, - ]); + let classes = Self::lookup_iri_information(&dataset, [rdfs::CLASS, owl::CLASS]); - let mut ontology = Ontology { + // Full-text search index field names + let mut field_map = HashMap::new(); + for quad in dataset.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None) { + if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject + && let TermRef::Literal(literal) = quad.object + { + field_map.insert(subject.into_owned(), literal.value().to_string()); + } + } + + // Catalog IDs (used to quickly filter full-text search results) + let mut catalog_ids = HashMap::new(); + for quad in dataset.quads_for_pattern(None, Some(gl::CATALOG_ID), None, None) { + if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject + && let TermRef::Literal(literal) = quad.object + { + if literal.datatype() == xsd::NON_NEGATIVE_INTEGER { + let value: u64 = literal.value().parse().expect("Failed to parse catalog ID from ontology. It ought to be a non-negative integer."); + catalog_ids.insert(subject.into_owned(), value); + } + } + } + + Ok(Ontology { dataset, prefixes, properties, classes, - field_map: HashMap::new(), - catalog_ids: HashMap::new(), - }; - - Self::memoize(&mut ontology); - Ok(ontology) + field_map, + catalog_ids, + }) } } @@ -241,24 +249,34 @@ pub struct Ontology { fn term_to_named_node(term: &Term) -> Option<&NamedNode> { if let Term::NamedNode(node) = term { Some(node) - } else { None } + } else { + None + } } fn term_to_string(term: &Term) -> Option<&str> { if let Term::Literal(literal) = term { match literal.datatype() { - xsd::STRING | xsd::NORMALIZED_STRING => Some(literal.value()), + xsd::STRING | xsd::NORMALIZED_STRING | rdf::LANG_STRING | rdf::DIR_LANG_STRING => { + Some(literal.value()) + } _ => None, } - } else { None } + } else { + None + } } fn term_to_boolean(term: &Term) -> Option { if let Term::Literal(literal) = term { if literal.datatype() == xsd::BOOLEAN { literal.value().parse().ok() - } else { None } - } else { None } + } else { + None + } + } else { + None + } } impl Ontology { @@ -269,22 +287,17 @@ impl Ontology { } pub fn label<'a>(&'a self, node: NamedNodeRef<'a>) -> Option<&'a str> { - let quads = self.dataset.quads_for_pattern( - Some(NamedOrBlankNodeRef::NamedNode(node)), - Some(rdfs::LABEL), - None, - None, - ); - for quad in quads { - if let TermRef::Literal(literal) = quad.object { - match literal.language() { - None | Some("en") => return Some(literal.value()), - _ => {} - } - } - } - - None + let node = node.into_owned(); + self.classes + .get(&node) + .map(|info| info.label.as_deref()) + .flatten() + .or_else(|| { + self.properties + .get(&node) + .map(|info| info.label.as_deref()) + .flatten() + }) } pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String { @@ -372,12 +385,11 @@ SELECT ?subject ?label ?comment WHERE { .unwrap_or(false); let read_only_class = match (triple.predicate, triple.object) { - (rdf::TYPE, TermRef::NamedNode(node)) => { - self.classes - .get(&node.into_owned()) - .map(|info| info.read_only) - .unwrap_or(false) - } + (rdf::TYPE, TermRef::NamedNode(node)) => self + .classes + .get(&node.into_owned()) + .map(|info| info.read_only) + .unwrap_or(false), _ => false, }; @@ -388,12 +400,14 @@ SELECT ?subject ?label ?comment WHERE { let classes = self.classes.clone(); let properties = self.properties.clone(); move |triple| { - let read_only_property = properties.get(&triple.predicate.into_owned()) + let read_only_property = properties + .get(&triple.predicate.into_owned()) .map(|info| info.read_only) .unwrap_or(false); let read_only_class = match (triple.predicate, triple.object) { - (rdf::TYPE, TermRef::NamedNode(node)) => classes.get(&node.into_owned()) + (rdf::TYPE, TermRef::NamedNode(node)) => classes + .get(&node.into_owned()) .map(|info| info.read_only) .unwrap_or(false), _ => false, diff --git a/publish/src/rdf/vocab.rs b/publish/src/rdf/vocab.rs index aab594d..688e4eb 100644 --- a/publish/src/rdf/vocab.rs +++ b/publish/src/rdf/vocab.rs @@ -1,8 +1,6 @@ pub mod gl { use oxigraph::model::NamedNodeRef; - pub const READ_ONLY: NamedNodeRef = - NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/ReadOnly"); pub const TEMPLATE: NamedNodeRef = NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/template"); pub const INDEXED_BY_FIELD: NamedNodeRef = @@ -17,13 +15,20 @@ pub mod gl { pub mod owl { use oxigraph::model::NamedNodeRef; - pub const SAME_AS: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#sameAs"); - pub const CLASS: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#Class"); - pub const OBJECT_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#ObjectProperty"); - pub const DATATYPE_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#DatatypeProperty"); - pub const ANNOTATION_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#AnnotationProperty"); - pub const FUNCTIONAL_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#FunctionalProperty"); - pub const ONTOLOGY_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#OntologyProperty"); + pub const SAME_AS: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#sameAs"); + pub const CLASS: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#Class"); + pub const OBJECT_PROPERTY: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#ObjectProperty"); + pub const DATATYPE_PROPERTY: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#DatatypeProperty"); + pub const ANNOTATION_PROPERTY: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#AnnotationProperty"); + pub const FUNCTIONAL_PROPERTY: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#FunctionalProperty"); + pub const ONTOLOGY_PROPERTY: NamedNodeRef = + NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#OntologyProperty"); } pub mod rda { @@ -31,4 +36,4 @@ pub mod rda { pub const ENTITY: NamedNodeRef = NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/c/C10013"); -} \ No newline at end of file +} diff --git a/search/src/schema.rs b/search/src/schema.rs index a5d8530..fa423a0 100644 --- a/search/src/schema.rs +++ b/search/src/schema.rs @@ -60,8 +60,6 @@ impl Schema { } pub fn all_fields() -> Vec { - Self::schema().fields() - .map(|(field, _)| field) - .collect() + Self::schema().fields().map(|(field, _)| field).collect() } }