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

408 lines
15 KiB
Rust
Raw Normal View History

2026-06-08 19:33:49 -04:00
use crate::error;
2026-08-05 11:21:25 -04:00
use crate::rdf::{conversion, materialize};
2026-08-05 18:33:30 -04:00
use gl_graph::vocab::gl;
2026-08-05 11:21:25 -04:00
use gl_search::language::LanguageCondition;
use iced::futures::TryFutureExt;
2026-06-08 19:33:49 -04:00
use oxigraph::model::vocab::{rdf, rdfs, xsd};
2026-07-14 21:31:15 -04:00
use oxigraph::model::{Dataset, Graph, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef};
2026-07-20 23:53:33 -04:00
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
2026-08-05 11:21:25 -04:00
use oxigraph::store::Store;
2026-07-13 14:05:40 -04:00
use std::collections::{BTreeMap, BTreeSet, HashMap};
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-07-22 20:34:15 -04:00
use tracing::{debug_span, field};
2026-06-08 19:33:49 -04:00
2026-07-13 14:05:40 -04:00
const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/";
const ONTOLOGY_GRAPH_NAME: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont"));
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#",
),
2026-07-24 19:25:07 -04:00
("prov", "http://www.w3.org/ns/prov#"),
("locid", "http://id.loc.gov/vocabulary/identifiers/"),
2026-07-25 23:08:19 -04:00
("loclang", "http://id.loc.gov/vocabulary/languages/"),
2026-06-26 11:28:44 -04:00
("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/"),
("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/"),
2026-07-01 17:42:23 -04:00
("rdaf", "http://rdaregistry.info/Elements/rof/"),
2026-06-26 11:28:44 -04:00
("rdat", "http://rdaregistry.info/Elements/t/"),
2026-07-01 17:42:23 -04:00
("rdau", "http://rdaregistry.info/Elements/u/"),
2026-06-26 11:28:44 -04:00
("rdaw", "http://rdaregistry.info/Elements/w/"),
("rdax", "http://rdaregistry.info/Elements/x/"),
2026-07-25 23:08:19 -04:00
("rdaco", "http://rdaregistry.info/termList/RDAContentType/"),
("rdact", "http://rdaregistry.info/termList/RDACarrierType/"),
2026-08-05 11:21:25 -04:00
("rdamt", "http://rdaregistry.info/termList/RDAMediaType/"),
2026-07-25 23:08:19 -04:00
("rdaft", "http://rdaregistry.info/termList/fileType/"),
2026-06-26 11:28:44 -04:00
("schema", "https://schema.org/"),
2026-07-25 23:08:19 -04:00
("gl", "http://fedora.quill.lan/rest/"),
("glo", ONTOLOGY_PREFIX),
2026-06-26 11:28:44 -04:00
].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-07-13 14:05:40 -04:00
materialize_inferences: bool,
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-07-13 14:05:40 -04:00
pub fn materialize_inferences(mut self) -> Self {
self.materialize_inferences = true;
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-07-13 14:05:40 -04:00
if self.materialize_inferences {
Store::open(path)
} else {
Store::open_read_only(path)
}
2026-06-23 21:55:52 -04:00
} 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-07-13 14:05:40 -04:00
if self.materialize_inferences {
materialize::same_as(&mut store)?;
materialize::super_properties(&mut store)?;
materialize::super_classes(&mut store)?;
debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;
}
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
2026-07-01 21:28:12 -04:00
&& let Term::NamedNode(field) = quad.object {
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-09 15:40:28 -04:00
}
2026-07-01 17:42:23 -04:00
}
2026-06-09 15:40:28 -04:00
2026-07-13 14:05:40 -04:00
#[derive(Clone, Debug)]
2026-07-01 17:42:23 -04:00
pub struct IndexEntry {
2026-07-20 23:53:33 -04:00
pub category_id: u64,
2026-07-01 17:42:23 -04:00
pub fields: HashMap<String, String>,
2026-06-08 19:33:49 -04:00
}
2026-07-08 21:07:52 -04:00
#[derive(Clone, Debug, Eq)]
2026-06-17 20:50:26 -04:00
pub struct LabeledIri {
pub iri: NamedNode,
2026-07-01 17:42:23 -04:00
pub label: String,
2026-06-17 20:50:26 -04:00
}
impl PartialEq for LabeledIri {
fn eq(&self, other: &Self) -> bool {
self.iri == other.iri
}
}
2026-07-08 21:07:52 -04:00
impl PartialOrd for LabeledIri {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
self.label.partial_cmp(&other.label)
}
}
impl Ord for LabeledIri {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.label.cmp(&other.label)
}
}
2026-06-17 20:50:26 -04:00
impl Display for LabeledIri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2026-07-01 17:42:23 -04:00
write!(f, "{}", self.label)
2026-06-17 20:50:26 -04:00
}
}
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-07-01 17:42:23 -04:00
pub label: String,
2026-06-15 19:10:55 -04:00
}
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>,
}
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-07-13 14:05:40 -04:00
materialize_inferences: false,
2026-06-08 19:33:49 -04:00
}
}
2026-07-13 14:05:40 -04:00
pub fn prefixes() -> &'static BTreeMap<String, String> {
2026-06-26 11:28:44 -04:00
&*PREFIXES
}
2026-07-13 14:05:40 -04:00
pub fn to_dataset(&self) -> Dataset {
self.store
.quads_for_pattern(None, None, None, Some(ONTOLOGY_GRAPH_NAME))
.filter_map(Result::ok)
.collect::<Dataset>()
}
2026-08-05 11:21:25 -04:00
pub fn store(&self) -> Store {
self.store.clone()
}
2026-07-22 20:34:15 -04:00
pub fn query_for_indexable_triples(&self, language: &LanguageCondition, source: Option<Dataset>) -> impl Future<Output = error::Result<HashMap<NamedNode, IndexEntry>>> + 'static {
2026-07-01 17:42:23 -04:00
let language_filter = language.to_filter_expression("fieldValue");
2026-07-20 23:53:33 -04:00
let query = format!(r#"SELECT ?individual ?categoryId ?fieldName ?fieldValue
2026-07-01 17:42:23 -04:00
WHERE {{
2026-07-07 22:06:39 -04:00
?class a gl:SearchableClass ;
2026-07-20 23:53:33 -04:00
gl:categoryId ?categoryId ;
2026-07-01 17:42:23 -04:00
gl:associatedProperty ?property .
?property gl:indexedByField/gl:fieldName ?fieldName .
2026-07-07 22:06:39 -04:00
?individual a ?class ;
2026-07-01 17:42:23 -04:00
?property ?fieldValue .
{language_filter}
}}"#);
let mut sparql = SparqlEvaluator::new()
2026-07-13 14:05:40 -04:00
.with_prefix("gl", ONTOLOGY_PREFIX).unwrap()
.parse_query(&query)
2026-07-20 23:53:33 -04:00
.expect("Unable to parse query");
2026-07-01 17:42:23 -04:00
sparql.dataset_mut().set_default_graph_as_union();
2026-07-20 23:53:33 -04:00
let store = self.store.clone();
tokio::task::spawn_blocking(move || {
2026-07-22 20:34:15 -04:00
let span = debug_span!("Indexable Triples Query", solutions = field::Empty).entered();
2026-07-20 23:53:33 -04:00
let query_results = if let Some(source) = &source {
sparql.on_queryable_dataset(source).execute()
} else {
sparql.on_store(&store).execute()
}.expect("Unable to execute indexing query");
2026-07-13 14:05:40 -04:00
2026-07-20 23:53:33 -04:00
let mut results = HashMap::new();
if let QueryResults::Solutions(solutions) = query_results {
2026-07-22 20:34:15 -04:00
let mut counter = 0usize;
2026-07-20 23:53:33 -04:00
for solution in solutions.filter_map(Result::ok) {
let individual = solution.get("individual").and_then(conversion::term_to_named_node);
let catalog_id = solution.get("categoryId").and_then(conversion::term_to_u64);
let field_name = solution.get("fieldName").and_then(conversion::term_as_str);
let field_value = solution.get("fieldValue").and_then(conversion::term_as_str);
if let (Some(individual), Some(catalog_id), Some(field_name), Some(field_value)) = (individual, catalog_id, field_name, field_value) {
results.entry(individual.to_owned())
.and_modify(|entry: &mut IndexEntry| {
entry.fields.insert(field_name.to_owned(), field_value.to_owned());
}).or_insert(IndexEntry {
category_id: catalog_id,
2026-07-01 17:42:23 -04:00
fields: HashMap::from_iter([(field_name.to_owned(), field_value.to_owned())]),
});
2026-07-20 23:53:33 -04:00
}
2026-07-22 20:34:15 -04:00
counter += 1;
2026-07-01 17:42:23 -04:00
}
2026-07-22 20:34:15 -04:00
span.record("solutions", counter);
2026-07-20 23:53:33 -04:00
} else {
unreachable!()
2026-07-01 17:42:23 -04:00
}
2026-07-20 23:53:33 -04:00
results
}).map_err(error::Error::from)
2026-07-01 17:42:23 -04:00
}
2026-07-20 23:53:33 -04:00
pub fn category_id(&self, class: &NamedNode) -> Option<u64> {
self.store.quads_for_pattern(Some(class.into()), Some(gl::CATEGORY_ID), None, None)
2026-07-01 21:28:12 -04:00
.filter_map(Result::ok)
.map(conversion::quad_into_term)
.filter_map(conversion::term_into_u64)
.next()
}
2026-07-01 17:42:23 -04:00
pub fn fields_for_class(&self, class: &NamedNode, language: &LanguageCondition) -> error::Result<Vec<IndexField>> {
let language_filter = language.to_filter_expression("label");
let query = format!(r#"SELECT DISTINCT ?name ?label {{
{class} gl:associatedProperty/gl:indexedByField ?field .
?field gl:fieldName ?name ;
gl:fieldLabel ?label .
{language_filter}
}}"#);
let mut sparql = SparqlEvaluator::new()
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")?
2026-07-13 14:05:40 -04:00
.with_prefix("gl", ONTOLOGY_PREFIX)?
2026-07-01 17:42:23 -04:00
.parse_query(&query)?;
sparql.dataset_mut().set_default_graph_as_union();
let mut results = Vec::new();
if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? {
for solution in solutions.filter_map(Result::ok) {
let name = solution.get("name").and_then(conversion::term_as_str);
let label = solution.get("label").and_then(conversion::term_as_str);
if let Some(name) = name && let Some(label) = label {
results.push(IndexField {
name: name.to_string(),
label: label.to_string(),
});
}
}
}
Ok(results)
}
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-07-13 14:05:40 -04:00
.map(conversion::quad_into_term)
2026-07-01 17:42:23 -04:00
.filter(|term| language.primary_matches_term(term))
2026-07-13 14:05:40 -04:00
.filter_map(conversion::term_into_string)
2026-07-01 17:42:23 -04:00
.next()
.unwrap_or_default();
2026-06-29 23:10:22 -04:00
LabeledIri {
iri: iri.clone(),
label,
}
}
2026-07-08 21:07:52 -04:00
pub fn searchable_classes(&self, language: &LanguageCondition) -> BTreeSet<LabeledIri> {
self.store.quads_for_pattern(None, Some(rdf::TYPE), Some(gl::SEARCHABLE_CLASS.into()), None)
2026-07-01 17:42:23 -04:00
.filter_map(Result::ok)
.filter_map(|quad| {
let label = self.store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::LABEL), None, None)
.filter_map(Result::ok)
2026-07-13 14:05:40 -04:00
.map(conversion::quad_into_term)
2026-07-01 17:42:23 -04:00
.filter(|term| language.primary_matches_term(term))
2026-07-13 14:05:40 -04:00
.filter_map(conversion::term_into_string)
2026-07-01 17:42:23 -04:00
.next();
if let Some(label) = label && let NamedOrBlankNode::NamedNode(subject) = quad.subject {
Some(LabeledIri {
iri: subject,
label,
})
} else { None }
2026-07-08 21:07:52 -04:00
}).collect()
2026-06-17 20:50:26 -04:00
}
2026-07-07 22:06:39 -04:00
pub fn subclasses_of(&self, class: &NamedNode) -> BTreeSet<NamedNode> {
self.store.quads_for_pattern(None, Some(rdfs::SUB_CLASS_OF), Some(class.into()), None)
.filter_map(Result::ok)
.filter_map(|quad| if let NamedOrBlankNode::NamedNode(subject) = quad.subject {
Some(subject)
} else { None })
.collect()
}
2026-07-10 23:14:50 -04:00
pub fn datatypes(&self) -> BTreeSet<NamedNode> {
2026-06-23 21:55:52 -04:00
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-07-10 23:14:50 -04:00
}).collect()
2026-06-08 19:33:49 -04:00
}
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-07-07 22:06:39 -04:00
pub fn exclude_read_only(&self) -> impl Fn(TripleRef<'_>) -> bool + 'static {
let true_term = TermRef::Literal(LiteralRef::new_typed_literal("true", xsd::BOOLEAN));
2026-07-12 13:52:13 -04:00
let read_only_graph = self.store
2026-07-07 22:06:39 -04:00
.quads_for_pattern(None, Some(gl::READ_ONLY), Some(true_term), None)
.filter_map(Result::ok)
2026-07-12 13:52:13 -04:00
.map(Triple::from)
.collect::<Graph>();
2026-07-07 22:06:39 -04:00
move |triple| {
2026-07-12 13:52:13 -04:00
if triple.predicate == rdf::TYPE && let TermRef::NamedNode(class) = triple.object {
read_only_graph.triples_for_subject(class).count() == 0
} else {
read_only_graph.triples_for_subject(triple.predicate).count() == 0
}
2026-07-07 22:06:39 -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
}