Files
tools/publish/src/rdf/ontology.rs
T

415 lines
14 KiB
Rust
Raw Normal View History

2026-06-08 19:33:49 -04:00
use crate::error;
2026-06-30 19:25:15 -04:00
use crate::rdf::vocab::gl;
2026-06-08 19:33:49 -04:00
use oxigraph::model::vocab::{rdf, rdfs, xsd};
2026-06-30 19:25:15 -04:00
use oxigraph::model::{LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef};
2026-06-08 19:33:49 -04:00
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
2026-06-30 19:25:15 -04:00
use std::collections::{BTreeMap, HashMap, HashSet};
2026-06-17 20:50:26 -04:00
use std::fmt::Display;
2026-06-23 21:55:52 -04:00
use std::path::{Path, PathBuf};
2026-06-26 11:28:44 -04:00
use std::sync::LazyLock;
2026-06-23 21:55:52 -04:00
use oxigraph::store::Store;
2026-06-30 19:25:15 -04:00
use tracing::debug_span;
2026-06-29 15:20:02 -04:00
use crate::rdf::{conversion, materialize};
2026-06-30 19:25:15 -04:00
use crate::rdf::conversion::LanguageCondition;
2026-06-08 19:33:49 -04:00
2026-06-26 11:28:44 -04:00
static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
BTreeMap::from_iter([
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
("owl", "http://www.w3.org/2002/07/owl#"),
("xsd", "http://www.w3.org/2001/XMLSchema#"),
("ldp", "http://www.w3.org/ns/ldp#"),
("dc", "http://purl.org/dc/elements/1.1/"),
("posix", "http://www.w3.org/ns/posix/stat#"),
(
"ebucore",
"http://www.ebu.ch/metadata/ontologies/ebucore/ebucore#",
),
("premis", "http://www.loc.gov/premis/rdf/v1#"),
("premis3", "http://www.loc.gov/premis/rdf/v3/"),
("dcterms", "http://purl.org/dc/terms/"),
("fedora", "http://fedora.info/definitions/v4/repository#"),
("rdaa", "http://rdaregistry.info/Elements/a/"),
("rdaad", "http://rdaregistry.info/Elements/a/datatype/"),
("rdac", "http://rdaregistry.info/Elements/c/"),
("rdae", "http://rdaregistry.info/Elements/e/"),
("rdai", "http://rdaregistry.info/Elements/i/"),
("rdam", "http://rdaregistry.info/Elements/m/"),
("rdan", "http://rdaregistry.info/Elements/n/"),
("rdap", "http://rdaregistry.info/Elements/p/"),
("rdat", "http://rdaregistry.info/Elements/t/"),
("rdaw", "http://rdaregistry.info/Elements/w/"),
("rdax", "http://rdaregistry.info/Elements/x/"),
("schema", "https://schema.org/"),
("quill", "http://fedora.quill.lan/rest/"),
("gl", "https://graphofliberty.org/2026/04/ont/"),
].map(|(k, v)| (k.to_string(), v.to_string())))
});
2026-06-08 19:33:49 -04:00
2026-06-23 21:55:52 -04:00
pub struct OntologyBuilder {
path: Option<PathBuf>,
2026-06-08 19:33:49 -04:00
}
2026-06-23 21:55:52 -04:00
impl OntologyBuilder {
pub fn with_path(mut self, path: impl AsRef<Path>) -> Self {
let path = path.as_ref().to_owned();
self.path = Some(path);
2026-06-08 19:33:49 -04:00
self
}
2026-06-23 21:55:52 -04:00
pub fn build(self) -> error::Result<Ontology> {
2026-06-29 15:20:02 -04:00
let mut store = if let Some(path) = self.path {
2026-06-23 21:55:52 -04:00
Store::open(path)
} else {
Store::new()
}?;
2026-06-08 19:33:49 -04:00
2026-06-23 21:55:52 -04:00
let prefixes = PREFIXES
.iter()
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>();
2026-06-08 19:33:49 -04:00
2026-06-29 15:20:02 -04:00
materialize::same_as(&mut store)?;
2026-06-30 19:25:15 -04:00
materialize::super_properties(&mut store)?;
2026-06-29 15:20:02 -04:00
materialize::super_classes(&mut store)?;
2026-06-30 19:25:15 -04:00
debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;
2026-06-23 21:55:52 -04:00
// Full-text search index field names
2026-06-29 15:20:02 -04:00
let fields = Self::fields(&store)?;
2026-06-23 21:55:52 -04:00
2026-06-29 15:20:02 -04:00
let mut entities: HashMap<NamedNode, Entity> = HashMap::new();
// All Entities have exactly one catalog ID.
for quad in store.quads_for_pattern(None, Some(gl::CATALOG_ID), None, None)
.filter_map(Result::ok) {
let label = store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::LABEL), None, None)
.filter_map(Result::ok)
2026-06-30 19:25:15 -04:00
.map(conversion::quad_into_term)
.filter(|term| conversion::language_matches(term, &conversion::english()))
.filter_map(conversion::term_into_string)
2026-06-23 21:55:52 -04:00
.next();
2026-06-29 15:20:02 -04:00
let comment = store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::COMMENT), None, None)
.filter_map(Result::ok)
2026-06-30 19:25:15 -04:00
.map(conversion::quad_into_term)
.filter(|term| conversion::language_matches(term, &conversion::english()))
.filter_map(conversion::term_into_string)
2026-06-23 21:55:52 -04:00
.next();
2026-06-29 15:20:02 -04:00
if let Some(catalog_id) = conversion::term_to_u64(&quad.object) &&
let NamedOrBlankNode::NamedNode(subject) = quad.subject &&
let Some(label) = label {
2026-06-23 21:55:52 -04:00
let mut properties = HashSet::new();
2026-06-29 15:20:02 -04:00
for quad in store.quads_for_pattern(Some(subject.as_ref().into()), Some(gl::ASSOCIATED_PROPERTY), None, None)
.filter_map(Result::ok) {
if let Term::NamedNode(property) = quad.object {
properties.insert(property);
2026-06-08 19:33:49 -04:00
}
}
2026-06-29 15:20:02 -04:00
entities.insert(subject, Entity {
label,
comment,
2026-06-23 21:55:52 -04:00
catalog_id,
properties,
});
2026-06-09 15:40:28 -04:00
}
}
2026-06-23 21:55:52 -04:00
let mut indexed_by = HashMap::new();
2026-06-29 15:20:02 -04:00
for quad in store.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None)
.filter_map(Result::ok) {
if let NamedOrBlankNode::NamedNode(subject) = quad.subject
&& let Term::NamedNode(field) = quad.object
2026-06-23 21:55:52 -04:00
{
2026-06-29 15:20:02 -04:00
indexed_by.insert(subject, field);
2026-06-23 21:55:52 -04:00
}
2026-06-29 15:20:02 -04:00
}
2026-06-23 21:55:52 -04:00
Ok(Ontology {
store,
prefixes,
2026-06-29 15:20:02 -04:00
fields,
entities,
indexed_by,
2026-06-23 21:55:52 -04:00
})
2026-06-09 15:40:28 -04:00
}
2026-06-29 15:20:02 -04:00
fn fields(store: &Store) -> error::Result<HashMap<NamedNode, IndexField>> {
2026-06-15 19:10:55 -04:00
let query = SparqlEvaluator::new()
2026-06-30 19:25:15 -04:00
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")?
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
2026-06-15 19:10:55 -04:00
.parse_query(
2026-06-30 19:25:15 -04:00
r#"SELECT DISTINCT ?subject ?name ?label {
2026-06-29 23:10:22 -04:00
GRAPH ?graph {
?subject a gl:IndexDocumentField ;
gl:fieldName ?name ;
gl:fieldLabel ?label .
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label))
}
2026-06-30 19:25:15 -04:00
}"#)?;
2026-06-15 19:10:55 -04:00
let mut results = HashMap::new();
2026-06-29 15:20:02 -04:00
if let QueryResults::Solutions(solutions) = query.on_store(store).execute()?
2026-06-15 19:10:55 -04:00
{
for solution in solutions.filter_map(Result::ok) {
2026-06-29 15:20:02 -04:00
let subject = solution.get("subject").and_then(conversion::term_to_named_node);
2026-06-30 19:25:15 -04:00
let name = solution.get("name").and_then(conversion::term_as_str);
let label = solution.get("label").and_then(conversion::term_as_str);
2026-06-15 19:10:55 -04:00
2026-06-17 20:50:26 -04:00
if let Some(subject) = subject && let Some(name) = name {
2026-06-15 19:10:55 -04:00
let field = IndexField {
2026-06-17 20:50:26 -04:00
name: name.to_string(),
label: label.map(|l| l.to_string()),
2026-06-15 19:10:55 -04:00
};
results.insert(subject.to_owned(), field);
}
}
}
2026-06-29 15:20:02 -04:00
Ok(results)
2026-06-15 19:10:55 -04:00
}
2026-06-29 15:20:02 -04:00
/*fn subclass_of(dataset: &Dataset, class: NamedNodeRef<'_>) -> impl Iterator<Item = NamedNode> {
2026-06-17 20:50:26 -04:00
let query = SparqlEvaluator::new()
.parse_query(
format!(
"PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT ?class {{
?class (rdfs:subClassOf|^rdfs:subClassOf)* {class} .
}}"
)
.as_str(),
)
.expect("Unable to parse subclass_of query");
if let QueryResults::Solutions(solutions) =
query.on_queryable_dataset(dataset).execute().unwrap()
{
solutions.filter_map(Result::ok).filter_map(|solution| {
if let Some(Term::NamedNode(class)) = solution.get("class") {
Some(class.clone())
} else {
None
}
})
} else {
unreachable!()
}
2026-06-23 21:55:52 -04:00
}*/
2026-06-08 19:33:49 -04:00
}
2026-06-17 20:50:26 -04:00
#[derive(Clone, Debug)]
pub struct LabeledIri {
pub iri: NamedNode,
2026-06-29 23:10:22 -04:00
pub label: Option<String>,
2026-06-29 15:20:02 -04:00
pub comment: Option<String>,
2026-06-17 20:50:26 -04:00
}
impl PartialEq for LabeledIri {
fn eq(&self, other: &Self) -> bool {
self.iri == other.iri
}
}
impl Display for LabeledIri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2026-06-29 23:10:22 -04:00
write!(f, "{}", self.label.clone().unwrap_or_default())
2026-06-17 20:50:26 -04:00
}
}
#[derive(Clone, Debug)]
pub struct Entity {
pub label: String,
2026-06-29 15:20:02 -04:00
pub comment: Option<String>,
2026-06-17 20:50:26 -04:00
pub catalog_id: u64,
pub properties: HashSet<NamedNode>,
}
2026-06-15 19:10:55 -04:00
#[derive(Clone, Debug)]
pub struct IndexField {
2026-06-17 20:50:26 -04:00
pub name: String,
2026-06-15 19:10:55 -04:00
pub label: Option<String>,
}
2026-06-08 19:33:49 -04:00
pub struct Ontology {
2026-06-23 21:55:52 -04:00
store: Store,
2026-06-08 19:33:49 -04:00
prefixes: HashMap<String, String>,
2026-06-17 20:50:26 -04:00
// Resource (Property or Class) -> Rust Type
2026-06-23 21:55:52 -04:00
//iri_info: HashMap<NamedNode, IriInformation>,
2026-06-17 20:50:26 -04:00
// NamedIndividual of class IndexField -> Rust Type
2026-06-15 19:10:55 -04:00
fields: HashMap<NamedNode, IndexField>,
2026-06-17 20:50:26 -04:00
// NamedIndividual of class Entity -> Rust Type
entities: HashMap<NamedNode, Entity>,
// Property -> NamedIndividual of class IndexField
2026-06-15 19:10:55 -04:00
indexed_by: HashMap<NamedNode, NamedNode>,
2026-06-08 19:33:49 -04:00
}
impl Ontology {
2026-06-23 21:55:52 -04:00
pub fn builder() -> OntologyBuilder {
2026-06-08 19:33:49 -04:00
OntologyBuilder {
2026-06-23 21:55:52 -04:00
path: None,
2026-06-08 19:33:49 -04:00
}
}
2026-06-26 11:28:44 -04:00
pub fn prefixes(&self) -> &BTreeMap<String, String> {
&*PREFIXES
}
2026-06-08 19:33:49 -04:00
pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String {
for (prefix_name, prefix_iri) in &self.prefixes {
if let Some(local_name) = node.as_str().strip_prefix(prefix_iri) {
return if local_name.is_empty() {
format!("{prefix_name}:")
} else {
format!("{prefix_name}:{local_name}")
};
}
}
node.as_str().to_string()
}
pub fn expand(&self, prefixed_iri: &str) -> Option<NamedNode> {
let (prefix, name) = prefixed_iri.split_once(':')?;
self.prefixes
.get(prefix)
.map(|base| NamedNode::new_unchecked(format!("{base}{name}")))
}
2026-06-15 19:10:55 -04:00
pub fn field_for_property(&self, property: &NamedNode) -> Option<&IndexField> {
self.indexed_by.get(property)
.and_then(|node| self.fields.get(node))
2026-06-08 19:33:49 -04:00
}
2026-06-17 20:50:26 -04:00
pub fn fields_for_class(&self, class: &NamedNode) -> Vec<&IndexField> {
self.entities.get(class)
.and_then(|entity| {
entity.properties.iter()
.map(|property| self.field_for_property(property))
.filter(Option::is_some)
.collect()
}).unwrap_or(Vec::new())
2026-06-08 19:33:49 -04:00
}
2026-06-17 20:50:26 -04:00
pub fn catalog_id(&self, class: &NamedNode) -> Option<u64> {
self.entities
.get(class)
.map(|entity| entity.catalog_id)
}
2026-06-29 23:10:22 -04:00
pub fn individuals(&self, class: &NamedNode) -> impl Iterator<Item = NamedNode> {
self.store
.quads_for_pattern(None, Some(rdf::TYPE), Some(class.into()), None)
.filter_map(Result::ok)
.filter_map(|quad|
if let NamedOrBlankNode::NamedNode(subject) = quad.subject {
Some(subject)
} else { None })
}
2026-06-30 19:25:15 -04:00
pub fn info(&self, iri: &NamedNode, language: &LanguageCondition) -> LabeledIri {
2026-06-29 23:10:22 -04:00
let label = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::LABEL), None, None)
.filter_map(Result::ok)
2026-06-30 19:25:15 -04:00
.map(conversion::quad_into_term)
.filter(|term| conversion::language_matches(term, language))
.filter_map(conversion::term_into_string)
2026-06-29 23:10:22 -04:00
.next();
let comment = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::COMMENT), None, None)
.filter_map(Result::ok)
2026-06-30 19:25:15 -04:00
.map(conversion::quad_into_term)
.filter(|term| conversion::language_matches(term, language))
.filter_map(conversion::term_into_string)
2026-06-29 23:10:22 -04:00
.next();
LabeledIri {
iri: iri.clone(),
label,
comment,
}
}
2026-06-17 20:50:26 -04:00
pub fn labeled_entity(&self, class: &NamedNode) -> Option<LabeledIri> {
self.entities.get(class)
.map(|entity| LabeledIri {
iri: class.to_owned(),
2026-06-29 23:10:22 -04:00
label: Some(entity.label.to_owned()),
2026-06-29 15:20:02 -04:00
comment: entity.comment.to_owned(),
2026-06-17 20:50:26 -04:00
})
}
pub fn labeled_entities(&self) -> impl Iterator<Item = LabeledIri> {
self.entities.iter()
.map(|(iri, entity)| LabeledIri {
iri: iri.to_owned(),
2026-06-29 23:10:22 -04:00
label: Some(entity.label.to_owned()),
2026-06-29 15:20:02 -04:00
comment: entity.comment.to_owned(),
2026-06-17 20:50:26 -04:00
})
}
2026-06-23 21:55:52 -04:00
pub fn datatypes(&self) -> impl Iterator<Item = NamedNode> {
self.store
2026-06-08 19:33:49 -04:00
.quads_for_pattern(
None,
Some(rdf::TYPE),
Some(TermRef::NamedNode(rdfs::DATATYPE)),
None,
)
2026-06-23 21:55:52 -04:00
.filter_map(Result::ok)
2026-06-08 19:33:49 -04:00
.filter_map(|quad| match quad.subject {
2026-06-23 21:55:52 -04:00
NamedOrBlankNode::NamedNode(subject) => Some(subject),
2026-06-08 19:33:49 -04:00
_ => None,
})
}
2026-06-10 14:16:25 -04:00
pub fn is_read_only<'a>(&self, triple: impl Into<TripleRef<'a>>) -> bool {
2026-06-23 21:55:52 -04:00
let triple = triple.into();
let true_term = TermRef::Literal(LiteralRef::new_typed_literal("true", xsd::BOOLEAN));
let subject = match (triple.predicate, triple.object) {
(rdf::TYPE, TermRef::NamedNode(class)) => class,
(predicate, _) => predicate,
};
self.store
.quads_for_pattern(Some(NamedOrBlankNodeRef::NamedNode(subject)), Some(gl::READ_ONLY), Some(true_term), None)
.filter_map(Result::ok)
.count() >= 1
2026-06-10 14:16:25 -04:00
}
2026-06-08 19:33:49 -04:00
pub fn template_triples<'a>(
&'a self,
class: NamedNodeRef<'a>,
subject: NamedNodeRef<'_>,
) -> Option<impl Iterator<Item = Triple>> {
if let Some(quad) = self
2026-06-23 21:55:52 -04:00
.store
2026-06-08 19:33:49 -04:00
.quads_for_pattern(
Some(NamedOrBlankNodeRef::NamedNode(class)),
2026-06-09 15:40:28 -04:00
Some(gl::TEMPLATE),
2026-06-08 19:33:49 -04:00
None,
None,
)
2026-06-23 21:55:52 -04:00
.filter_map(Result::ok)
2026-06-08 19:33:49 -04:00
.next()
{
2026-06-23 21:55:52 -04:00
if let Term::BlankNode(blank_node) = quad.object {
2026-06-08 19:33:49 -04:00
let iter = self
2026-06-23 21:55:52 -04:00
.store
.quads_for_pattern(Some(NamedOrBlankNodeRef::BlankNode(blank_node.as_ref())), None, None, None)
.filter_map(Result::ok)
2026-06-08 19:33:49 -04:00
.map(Triple::from)
.map(move |mut triple| {
triple.subject = NamedOrBlankNode::NamedNode(subject.into_owned());
triple
});
Some(iter)
} else {
None
}
} else {
None
}
}
2026-06-15 19:10:55 -04:00
}