SHA256
1
0
This commit is contained in:
Alex Wied
2026-06-09 17:20:46 -04:00
parent 27c92bb2cf
commit 1f8c6201b4
5 changed files with 123 additions and 105 deletions
+6 -5
View File
@@ -1,7 +1,7 @@
use crate::rdf::ontology::Ontology; use crate::rdf::ontology::Ontology;
use crate::rdf::vocab::{gl, rda};
use crate::rdf::term_helper::{TermHelper, TermHelperMut}; 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 http::StatusCode;
use iced::alignment::Horizontal; use iced::alignment::Horizontal;
use iced::widget::button::Style; use iced::widget::button::Style;
@@ -121,7 +121,9 @@ impl Publisher {
document.add_text(Schema::field("comment"), comment.to_lowercase()); 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"); index.commit().expect("Unable to commit ontology to index");
@@ -669,8 +671,7 @@ impl Publisher {
.on_press(Message::SearchResultClicked(result.clone())) .on_press(Message::SearchResultClicked(result.clone()))
.style(button::text) .style(button::text)
}); });
let results_table = let results_table = scrollable(table([type_column, iri_column], &search_state.results));
scrollable(table([type_column, iri_column], &search_state.results));
return column![search_input, results_table].into(); return column![search_input, results_table].into();
} }
+100 -86
View File
@@ -1,4 +1,5 @@
use crate::error; use crate::error;
use crate::rdf::vocab::{gl, owl};
use oxigraph::io::{RdfFormat, RdfParser}; use oxigraph::io::{RdfFormat, RdfParser};
use oxigraph::model::vocab::{rdf, rdfs, xsd}; use oxigraph::model::vocab::{rdf, rdfs, xsd};
use oxigraph::model::{ use oxigraph::model::{
@@ -8,7 +9,6 @@ use oxigraph::model::{
use oxigraph::sparql::{QueryResults, SparqlEvaluator}; use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use std::collections::{HashMap, HashSet}; use std::collections::{HashMap, HashSet};
use tracing::debug; use tracing::debug;
use crate::rdf::vocab::{gl, owl};
const PREFIXES: &[(&str, &str)] = &[ const PREFIXES: &[(&str, &str)] = &[
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"), ("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<Item = NamedNodeRef<'b>>) -> HashMap<NamedNode, IriInformation> { fn lookup_iri_information<'b>(
dataset: &Dataset,
classes: impl IntoIterator<Item = NamedNodeRef<'b>>,
) -> HashMap<NamedNode, IriInformation> {
let class_filter = classes let class_filter = classes
.into_iter() .into_iter()
.map(|node| node.to_string()) .map(|node| node.to_string())
@@ -114,16 +117,28 @@ impl<'a> OntologyBuilder<'a> {
.join(","); .join(",");
let query = SparqlEvaluator::new() let query = SparqlEvaluator::new()
.parse_query(format!(r#"PREFIX gl: <https://graphofliberty.org/2026/04/ont/> .parse_query(
format!(
r#"PREFIX gl: <https://graphofliberty.org/2026/04/ont/>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#> PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
SELECT DISTINCT ?subject ?label ?comment ?read_only {{ SELECT DISTINCT ?subject ?label ?comment ?read_only {{
?subject a ?class . ?subject a ?class .
FILTER(?class IN ({class_filter})) FILTER(?class IN ({class_filter}))
OPTIONAL {{ ?subject rdfs:label ?label }} OPTIONAL {{
OPTIONAL {{ ?subject rdfs:comment ?comment }} ?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 }} 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(); let mut results = HashMap::new();
if let QueryResults::Solutions(solutions) = 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 subject = solution.get("subject").and_then(term_to_named_node);
let label = solution.get("label").and_then(term_to_string); let label = solution.get("label").and_then(term_to_string);
let comment = solution.get("comment").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 { if let Some(subject) = subject {
let info = IriInformation { let info = IriInformation {
@@ -148,37 +166,6 @@ SELECT DISTINCT ?subject ?label ?comment ?read_only {{
results 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<Ontology> { pub fn build(&mut self) -> error::Result<Ontology> {
let prefixes = PREFIXES let prefixes = PREFIXES
.iter() .iter()
@@ -194,31 +181,52 @@ SELECT DISTINCT ?subject ?label ?comment ?read_only {{
} }
Self::materialize_same_as(&mut dataset); Self::materialize_same_as(&mut dataset);
let properties = Self::lookup_iri_information(&dataset, [ let properties = Self::lookup_iri_information(
rdf::PROPERTY, &dataset,
owl::ANNOTATION_PROPERTY, [
owl::DATATYPE_PROPERTY, rdf::PROPERTY,
owl::FUNCTIONAL_PROPERTY, rdfs::DATATYPE,
owl::OBJECT_PROPERTY, owl::ANNOTATION_PROPERTY,
owl::ONTOLOGY_PROPERTY, owl::DATATYPE_PROPERTY,
]); owl::FUNCTIONAL_PROPERTY,
owl::OBJECT_PROPERTY,
owl::ONTOLOGY_PROPERTY,
],
);
let classes = Self::lookup_iri_information(&dataset, [ let classes = Self::lookup_iri_information(&dataset, [rdfs::CLASS, owl::CLASS]);
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, dataset,
prefixes, prefixes,
properties, properties,
classes, classes,
field_map: HashMap::new(), field_map,
catalog_ids: HashMap::new(), catalog_ids,
}; })
Self::memoize(&mut ontology);
Ok(ontology)
} }
} }
@@ -241,24 +249,34 @@ pub struct Ontology {
fn term_to_named_node(term: &Term) -> Option<&NamedNode> { fn term_to_named_node(term: &Term) -> Option<&NamedNode> {
if let Term::NamedNode(node) = term { if let Term::NamedNode(node) = term {
Some(node) Some(node)
} else { None } } else {
None
}
} }
fn term_to_string(term: &Term) -> Option<&str> { fn term_to_string(term: &Term) -> Option<&str> {
if let Term::Literal(literal) = term { if let Term::Literal(literal) = term {
match literal.datatype() { 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, _ => None,
} }
} else { None } } else {
None
}
} }
fn term_to_boolean(term: &Term) -> Option<bool> { fn term_to_boolean(term: &Term) -> Option<bool> {
if let Term::Literal(literal) = term { if let Term::Literal(literal) = term {
if literal.datatype() == xsd::BOOLEAN { if literal.datatype() == xsd::BOOLEAN {
literal.value().parse().ok() literal.value().parse().ok()
} else { None } } else {
} else { None } None
}
} else {
None
}
} }
impl Ontology { impl Ontology {
@@ -269,22 +287,17 @@ impl Ontology {
} }
pub fn label<'a>(&'a self, node: NamedNodeRef<'a>) -> Option<&'a str> { pub fn label<'a>(&'a self, node: NamedNodeRef<'a>) -> Option<&'a str> {
let quads = self.dataset.quads_for_pattern( let node = node.into_owned();
Some(NamedOrBlankNodeRef::NamedNode(node)), self.classes
Some(rdfs::LABEL), .get(&node)
None, .map(|info| info.label.as_deref())
None, .flatten()
); .or_else(|| {
for quad in quads { self.properties
if let TermRef::Literal(literal) = quad.object { .get(&node)
match literal.language() { .map(|info| info.label.as_deref())
None | Some("en") => return Some(literal.value()), .flatten()
_ => {} })
}
}
}
None
} }
pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String { pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String {
@@ -372,12 +385,11 @@ SELECT ?subject ?label ?comment WHERE {
.unwrap_or(false); .unwrap_or(false);
let read_only_class = match (triple.predicate, triple.object) { let read_only_class = match (triple.predicate, triple.object) {
(rdf::TYPE, TermRef::NamedNode(node)) => { (rdf::TYPE, TermRef::NamedNode(node)) => self
self.classes .classes
.get(&node.into_owned()) .get(&node.into_owned())
.map(|info| info.read_only) .map(|info| info.read_only)
.unwrap_or(false) .unwrap_or(false),
}
_ => false, _ => false,
}; };
@@ -388,12 +400,14 @@ SELECT ?subject ?label ?comment WHERE {
let classes = self.classes.clone(); let classes = self.classes.clone();
let properties = self.properties.clone(); let properties = self.properties.clone();
move |triple| { 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) .map(|info| info.read_only)
.unwrap_or(false); .unwrap_or(false);
let read_only_class = match (triple.predicate, triple.object) { 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) .map(|info| info.read_only)
.unwrap_or(false), .unwrap_or(false),
_ => false, _ => false,
+14 -9
View File
@@ -1,8 +1,6 @@
pub mod gl { pub mod gl {
use oxigraph::model::NamedNodeRef; use oxigraph::model::NamedNodeRef;
pub const READ_ONLY: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/ReadOnly");
pub const TEMPLATE: NamedNodeRef = pub const TEMPLATE: NamedNodeRef =
NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/template"); NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/template");
pub const INDEXED_BY_FIELD: NamedNodeRef = pub const INDEXED_BY_FIELD: NamedNodeRef =
@@ -17,13 +15,20 @@ pub mod gl {
pub mod owl { pub mod owl {
use oxigraph::model::NamedNodeRef; use oxigraph::model::NamedNodeRef;
pub const SAME_AS: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#sameAs"); pub const SAME_AS: NamedNodeRef =
pub const CLASS: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#Class"); NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#sameAs");
pub const OBJECT_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#ObjectProperty"); pub const CLASS: NamedNodeRef =
pub const DATATYPE_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#DatatypeProperty"); NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#Class");
pub const ANNOTATION_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#AnnotationProperty"); pub const OBJECT_PROPERTY: NamedNodeRef =
pub const FUNCTIONAL_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#FunctionalProperty"); NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#ObjectProperty");
pub const ONTOLOGY_PROPERTY: NamedNodeRef = NamedNodeRef::new_unchecked("http://www.w3.org/2002/07/owl#OntologyProperty"); 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 { pub mod rda {
+1 -3
View File
@@ -60,8 +60,6 @@ impl Schema {
} }
pub fn all_fields() -> Vec<Field> { pub fn all_fields() -> Vec<Field> {
Self::schema().fields() Self::schema().fields().map(|(field, _)| field).collect()
.map(|(field, _)| field)
.collect()
} }
} }