From 8f6e2b3ac6f4320744f48640aa649cc474876dacd461a3765e2ccbd02443f195 Mon Sep 17 00:00:00 2001 From: Alex Wied <2+alex@noreply.code.graphofliberty.org> Date: Sun, 23 Aug 2026 14:20:48 -0400 Subject: [PATCH] . --- Cargo.lock | 1 + graph/Cargo.toml | 3 +- graph/src/curie.rs | 10 ++- graph/src/indexer.rs | 8 +- graph/src/ontology.rs | 165 ++++++++++++------------------------------ publish/src/app.rs | 11 ++- publish/src/main.rs | 4 +- publish/src/tasks.rs | 12 ++- 8 files changed, 82 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d004b42..47c8d5d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1636,6 +1636,7 @@ dependencies = [ "spargebra", "thiserror 2.0.20", "tracing", + "url", ] [[package]] diff --git a/graph/Cargo.toml b/graph/Cargo.toml index 3e4b7c6..b9eb61d 100644 --- a/graph/Cargo.toml +++ b/graph/Cargo.toml @@ -12,4 +12,5 @@ oxilangtag.workspace = true rayon.workspace = true spargebra.workspace = true thiserror.workspace = true -tracing.workspace = true \ No newline at end of file +tracing.workspace = true +url = "2.5.8" \ No newline at end of file diff --git a/graph/src/curie.rs b/graph/src/curie.rs index 3020ba7..bc6e325 100644 --- a/graph/src/curie.rs +++ b/graph/src/curie.rs @@ -1,5 +1,6 @@ use std::collections::BTreeMap; use std::sync::LazyLock; +use url::Url; const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/"; @@ -19,7 +20,9 @@ pub static PREFIXES: LazyLock> = LazyLock::new(|| { ), ("prov", "http://www.w3.org/ns/prov#"), ("locid", "http://id.loc.gov/vocabulary/identifiers/"), + ("locname", "http://id.loc.gov/authorities/names/"), ("loclang", "http://id.loc.gov/vocabulary/languages/"), + ("viaf", "http://viaf.org/viaf/"), ("premis", "http://www.loc.gov/premis/rdf/v1#"), ("premis3", "http://www.loc.gov/premis/rdf/v3/"), ("dcterms", "http://purl.org/dc/terms/"), @@ -69,7 +72,12 @@ impl CurieHelper { return Some(format!("{name}:{local_name}")); } } - None + + if let Some(base) = base && + let Some(parsed_iri) = Url::parse(iri).ok() && + let Some(parsed_base) = Url::parse(base).ok() { + parsed_base.make_relative(&parsed_iri) + } else { None } } pub fn expand(&self, base: Option<&str>, abbreviated_iri: &str) -> Option { diff --git a/graph/src/indexer.rs b/graph/src/indexer.rs index 14bbb5e..3965a9e 100644 --- a/graph/src/indexer.rs +++ b/graph/src/indexer.rs @@ -118,15 +118,17 @@ impl<'a> Indexer<'a> { results } - pub fn ontology(&self, entities: HashMap) -> Vec> { + pub fn ontology(&self, entities: HashMap) -> Vec> { let label_field = Schema::field("label", self.language.primary_language()); let definition_field = Schema::field("definition", self.language.primary_language()); entities.iter() - .map(|(subject, (class, description))| { + .map(|(subject, description)| { let mut document = HashMap::with_capacity(4); - let discriminant = Class::try_from_named_node(class) + let discriminant = description.class + .as_ref() + .and_then(|node| Class::try_from_named_node(node.as_ref())) .map(|doc_type| doc_type as u64) .map(OwnedValue::U64) .unwrap_or(OwnedValue::Null); diff --git a/graph/src/ontology.rs b/graph/src/ontology.rs index b4bc1c0..1d4835c 100644 --- a/graph/src/ontology.rs +++ b/graph/src/ontology.rs @@ -5,18 +5,24 @@ use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer}; use oxigraph::model::{Graph, NamedNode, NamedNodeRef, Triple}; use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput}; use spargebra::algebra::GraphPattern; -use spargebra::term::{GroundTerm, Variable}; +use spargebra::term::{GroundTerm, NamedNodePattern, TermPattern, TriplePattern, Variable}; use gl_inference::proto::ontology_client::OntologyClient; use gl_inference::proto::OntologyQueryRequest; use gl_inference::tonic::transport::{Channel, Endpoint}; use crate::helpers::{term_as_str, term_to_named_node}; use crate::language::LanguageCondition; -use crate::vocab::{rdf, rdfs}; +use crate::vocab; #[derive(Clone, Debug, Default)] pub struct ResourceDescription { pub label: Option, pub description: Option, + pub class: Option, +} + +pub enum ResourceSelector { + Classes, + Subjects, } #[derive(Clone, Debug, Eq, Hash, PartialEq)] @@ -127,8 +133,8 @@ SELECT ?subject ?class WHERE {{ if let Some(subject) = subject && let Some(class) = class { let entry = match class.as_ref() { - rdf::PROPERTY => ReadOnlyEntity::Property(subject), - rdfs::CLASS => ReadOnlyEntity::Class(subject), + vocab::rdf::PROPERTY => ReadOnlyEntity::Property(subject), + vocab::rdfs::CLASS => ReadOnlyEntity::Class(subject), _ => continue, }; results.insert(entry); @@ -138,125 +144,44 @@ SELECT ?subject ?class WHERE {{ Ok(results) } - pub async fn datatypes(&mut self) -> crate::Result> { - let label_filter = self.language.filter("label"); - let comment_filter = self.language.filter("comment"); - - let mut request = OntologyQueryRequest::default(); - request.sparql_query = Some(format!(r#"PREFIX rdf: -PREFIX rdfs: -PREFIX skos: - -SELECT ?subject ?label ?comment WHERE {{ - OPTIONAL {{ - ?subject a rdfs:Datatype - }} OPTIONAL {{ - ?subject rdfs:label ?label - {label_filter} - }} OPTIONAL {{ - ?subject rdfs:comment ?comment - {comment_filter} - }} -}}"#)); - - let response = self.client.query(request).await?; - let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json) - .for_slice(&response.get_ref().results)?; - - let mut results = HashMap::new(); - if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output { - for solution in solutions.filter_map(Result::ok) { - let subject = solution - .get("subject") - .and_then(term_to_named_node) - .map(|node| node.clone()); - - if let Some(subject) = subject { - let label = solution - .get("label") - .and_then(term_as_str) - .map(String::from); - - let description = solution - .get("description") - .and_then(term_as_str) - .map(String::from); - - results.insert(subject, ResourceDescription { label, description }); - } - } - } - Ok(results) - } - - pub async fn resource_description_from_subject(&mut self, subject: NamedNode) -> crate::Result { - let label_filter = self.language.filter("label"); - let comment_filter = self.language.filter("comment"); - let definition_filter = self.language.filter("definition"); - - let mut request = OntologyQueryRequest::default(); - request.sparql_query = Some(format!(r#"PREFIX rdf: -PREFIX rdfs: -PREFIX skos: - -SELECT ?label ?description WHERE {{ - OPTIONAL {{ - {subject} rdfs:label ?label - {label_filter} - }} OPTIONAL {{ - {subject} rdfs:comment ?comment - {comment_filter} - }} OPTIONAL {{ - {subject} skos:definition ?definition - {definition_filter} - }} - BIND(COALESCE(?definition, ?comment) AS ?description) -}}"#)); - - let foo = request.sparql_query.clone().unwrap(); - - let response = self.client.query(request).await?; - let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json) - .for_slice(&response.get_ref().results)?; - - if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output { - Ok(solutions.filter_map(Result::ok) - .next() - .map(|solution| { - let label = solution - .get("label") - .and_then(term_as_str) - .map(String::from); - - let description = solution - .get("description") - .and_then(term_as_str) - .map(String::from); - - ResourceDescription { - label, - description, - } - }).unwrap_or(ResourceDescription::default()) - ) - } else { unreachable!() } - } - - fn named_nodes_to_values_expression<'a>(variable: &str, nodes: impl IntoIterator>) -> String { + fn selector_to_sparql_pattern<'a>(selector: ResourceSelector, nodes: impl IntoIterator>) -> String { let bindings = nodes.into_iter() .map(|node| GroundTerm::NamedNode(node.into_owned())) .map(Some) .map(|item| vec![item]) .collect(); - GraphPattern::Values { - variables: vec![Variable::new_unchecked(variable)], + let variable = match selector { + ResourceSelector::Classes => Variable::new_unchecked("class"), + ResourceSelector::Subjects => Variable::new_unchecked("subject"), + }; + + let values_pattern = GraphPattern::Values { + variables: vec![variable.clone()], bindings, + }; + + let triple_pattern = match selector { + ResourceSelector::Classes => vec![TriplePattern { + subject: TermPattern::Variable(Variable::new_unchecked("subject")), + predicate: NamedNodePattern::NamedNode(vocab::rdf::TYPE.into_owned()), + object: TermPattern::Variable(variable), + }], + ResourceSelector::Subjects => vec![], + }; + + let basic_pattern = GraphPattern::Bgp { + patterns: triple_pattern, + }; + + GraphPattern::Join { + left: Box::new(basic_pattern), + right: Box::new(values_pattern), }.to_string() } - pub async fn resource_descriptions_from_classes<'a>(&mut self, classes: impl IntoIterator>) -> crate::Result> { - let values = Self::named_nodes_to_values_expression("class", classes); + pub async fn resource_descriptions<'a>(&mut self, selector: ResourceSelector, nodes: impl IntoIterator>) -> crate::Result> { + let pattern = Self::selector_to_sparql_pattern(selector, nodes); let label_filter = self.language.filter("label"); let comment_filter = self.language.filter("comment"); let definition_filter = self.language.filter("definition"); @@ -266,9 +191,8 @@ SELECT ?label ?description WHERE {{ PREFIX rdfs: PREFIX skos: -SELECT ?class ?subject ?label ?description WHERE {{ - {values} - ?subject a ?class . +SELECT ?subject ?label ?description ?class WHERE {{ + {pattern} OPTIONAL {{ ?subject rdfs:label ?label . {label_filter} @@ -298,8 +222,7 @@ SELECT ?class ?subject ?label ?description WHERE {{ .and_then(term_to_named_node) .map(|node| node.clone()); - if let Some(subject) = subject && - let Some(class) = class { + if let Some(subject) = subject { let label = solution .get("label") .and_then(term_as_str) @@ -310,7 +233,11 @@ SELECT ?class ?subject ?label ?description WHERE {{ .and_then(term_as_str) .map(String::from); - results.insert(subject, (class.clone(), ResourceDescription { label, description })); + results.insert(subject, ResourceDescription { + label, + description, + class: class.cloned(), + }); } } } diff --git a/publish/src/app.rs b/publish/src/app.rs index e921653..bd69c1e 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -916,7 +916,7 @@ impl Publisher { let subject_column = table::column("Subject", |triple: TripleRef| { let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject { Some(self.curie_helper - .abbreviate(None, subject.as_str()) + .abbreviate(Some(self.document.origin().as_str()), subject.as_str()) .unwrap_or_else(|| subject.as_str().to_string())) } else { None @@ -942,7 +942,14 @@ impl Publisher { }); let object_column = table::column("Object", |triple: TripleRef| { - text(triple.object.to_string()) + let displayed_text = match triple.object { + TermRef::NamedNode(node) => { + self.curie_helper.abbreviate(Some(self.document.origin().as_str()), node.as_str()) + } + _ => None + }.unwrap_or(triple.object.to_string()); + + text(displayed_text) }); Some(table( [subject_column, predicate_column, object_column], diff --git a/publish/src/main.rs b/publish/src/main.rs index d45a1b1..ca19ed5 100644 --- a/publish/src/main.rs +++ b/publish/src/main.rs @@ -27,7 +27,7 @@ use tracing_subscriber::{EnvFilter, fmt}; use url::Url; use gl_graph::indexer::Indexer; use gl_graph::{language, CurieHelper, vocab}; -use gl_graph::ontology::OntologyBuilder; +use gl_graph::ontology::{OntologyBuilder, ResourceSelector}; use gl_inference::proto::ontology_client::OntologyClient; use gl_inference::proto::OntologyQueryRequest; @@ -111,7 +111,7 @@ fn main() -> color_eyre::Result<()> { let mut client = OntologyBuilder::from_string("http://[::1]:3000", language::ENGLISH_OR_UNTAGGED.clone())? .connect() .await?; - let resource_descriptions = client.resource_descriptions_from_classes([ + let resource_descriptions = client.resource_descriptions(ResourceSelector::Classes, [ vocab::rdf::PROPERTY, vocab::rdfs::CLASS, vocab::skos::CONCEPT, diff --git a/publish/src/tasks.rs b/publish/src/tasks.rs index dd95008..45e0483 100644 --- a/publish/src/tasks.rs +++ b/publish/src/tasks.rs @@ -1,6 +1,7 @@ use iced::Task; use oxigraph::model::{Graph, NamedNode}; -use gl_graph::ontology::{ConnectedOntology, OntologyBuilder}; +use gl_graph::ontology::{ConnectedOntology, OntologyBuilder, ResourceSelector}; +use gl_graph::vocab; use crate::app::Message; pub(crate) fn connect_to_ontology_service(builder: OntologyBuilder) -> Task { @@ -14,16 +15,19 @@ pub(crate) fn connect_to_ontology_service(builder: OntologyBuilder) -> Task Task { let subject_clone = subject.clone(); - Task::perform(async move { ontology.resource_description_from_subject(subject_clone).await }, |result| { + Task::perform(async move { ontology.resource_descriptions(ResourceSelector::Subjects, vec![subject_clone.as_ref()]).await }, |result| { match result { - Ok(description) => Message::CacheResourceDescription(subject, description), + Ok(descriptions) => { + let description = descriptions[&subject].clone(); + Message::CacheResourceDescription(subject, description) + }, Err(err) => Message::ShowError(err.to_string()), } }) } pub(crate) fn list_datatypes(mut ontology: ConnectedOntology) -> Task { - Task::perform(async move { ontology.datatypes().await }, |result| { + Task::perform(async move { ontology.resource_descriptions(ResourceSelector::Classes, vec![vocab::rdfs::DATATYPE]).await }, |result| { match result { Ok(datatypes) => Message::CacheDatatypes(datatypes), Err(err) => Message::ShowError(err.to_string()),