.
This commit is contained in:
+6
-5
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
+92
-78
@@ -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(
|
||||||
|
&dataset,
|
||||||
|
[
|
||||||
rdf::PROPERTY,
|
rdf::PROPERTY,
|
||||||
|
rdfs::DATATYPE,
|
||||||
owl::ANNOTATION_PROPERTY,
|
owl::ANNOTATION_PROPERTY,
|
||||||
owl::DATATYPE_PROPERTY,
|
owl::DATATYPE_PROPERTY,
|
||||||
owl::FUNCTIONAL_PROPERTY,
|
owl::FUNCTIONAL_PROPERTY,
|
||||||
owl::OBJECT_PROPERTY,
|
owl::OBJECT_PROPERTY,
|
||||||
owl::ONTOLOGY_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,
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user