This commit is contained in:
Alex Wied
2026-07-01 17:42:23 -04:00
parent 617c4465fb
commit 6b8f4ccb3d
9 changed files with 250 additions and 234 deletions
+12 -28
View File
@@ -2,38 +2,10 @@ use oxigraph::model::{NamedNode, Quad, Term};
use oxigraph::model::vocab::{rdf, xsd};
use oxilangtag::LanguageTag;
pub enum LanguageCondition {
ExactMatch(LanguageTag<String>),
RelaxedMatch(LanguageTag<String>),
Untagged,
}
pub fn quad_into_term(quad: Quad) -> Term {
quad.object
}
pub fn english() -> LanguageCondition {
LanguageCondition::RelaxedMatch(LanguageTag::parse("en".to_string()).unwrap())
}
pub fn language_matches(term: &Term, condition: &LanguageCondition) -> bool {
if let Term::Literal(literal) = term {
let tag = literal.language()
.map(LanguageTag::parse_and_normalize)
.and_then(Result::ok);
match (tag, condition) {
(Some(language), LanguageCondition::ExactMatch(expectation)) |
(Some(language), LanguageCondition::RelaxedMatch(expectation)) => language == *expectation,
(None, LanguageCondition::RelaxedMatch(_)) => true,
(None, LanguageCondition::Untagged) => true,
_ => false,
}
} else {
false
}
}
pub fn term_to_named_node(term: &Term) -> Option<&NamedNode> {
if let Term::NamedNode(node) = term {
Some(node)
@@ -59,6 +31,18 @@ pub fn term_as_str(term: &Term) -> Option<&str> {
}
}
/*pub fn term_to_tagged_string(term: &Term) -> Option<(String, Option<LanguageTag<String>>)> {
term_as_str(&term).and_then(|value| {
if let Term::Literal(literal) = term {
literal.language()
.map(|language| {
let tag = LanguageTag::parse(language.to_string()).ok();
(value.to_string(), tag)
})
} else { None }
})
}*/
pub fn term_to_boolean(term: &Term) -> Option<bool> {
if let Term::Literal(literal) = term {
if literal.datatype() == xsd::BOOLEAN {
+46
View File
@@ -0,0 +1,46 @@
use std::sync::LazyLock;
use oxigraph::model::Term;
use oxilangtag::LanguageTag;
pub const ENGLISH_PRIMARY: &str = "en";
pub static ENGLISH_OR_UNTAGGED: LazyLock<LanguageCondition> = LazyLock::new(|| {
LanguageCondition::ExactMatchOrUntagged(LanguageTag::parse(ENGLISH_PRIMARY.to_string()).unwrap())
});
pub enum LanguageCondition {
ExactMatchOnly(LanguageTag<String>),
ExactMatchOrUntagged(LanguageTag<String>),
UntaggedOnly,
AnyOrNone,
}
impl LanguageCondition {
pub fn primary_matches_term(&self, term: &Term) -> bool {
if let Term::Literal(literal) = term {
let tag = literal.language()
.map(LanguageTag::parse_and_normalize)
.and_then(Result::ok);
match (tag, self) {
(Some(language), LanguageCondition::ExactMatchOnly(expectation)) |
(Some(language), LanguageCondition::ExactMatchOrUntagged(expectation)) => language.primary_language() == expectation.primary_language(),
(None, LanguageCondition::ExactMatchOrUntagged(_)) => true,
(None, LanguageCondition::UntaggedOnly) => true,
(_, LanguageCondition::AnyOrNone) => true,
_ => false,
}
} else {
false
}
}
pub fn to_filter_expression(&self, var: &str) -> String {
match self {
LanguageCondition::ExactMatchOnly(language) => format!(r#"FILTER(langMATCHES(LANG(?{var}), "{language}"))"#),
LanguageCondition::ExactMatchOrUntagged(language) => format!(r#"FILTER(langMATCHES(LANG(?{var}), "{language}") || !hasLANG(?{var}))"#),
LanguageCondition::UntaggedOnly => format!("FILTER(!hasLANG(?{var}))"),
LanguageCondition::AnyOrNone => "".to_string(),
}
}
}
+1
View File
@@ -3,3 +3,4 @@ pub(crate) mod term_helper;
pub mod vocab;
pub(crate) mod materialize;
pub(crate) mod conversion;
pub(crate) mod language;
+114 -126
View File
@@ -1,3 +1,4 @@
use std::borrow::Borrow;
use crate::error;
use crate::rdf::vocab::gl;
use oxigraph::model::vocab::{rdf, rdfs, xsd};
@@ -7,10 +8,12 @@ use std::collections::{BTreeMap, HashMap, HashSet};
use std::fmt::Display;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;
use iced::widget::sensor::Key;
use oxigraph::store::Store;
use tracing::debug_span;
use crate::rdf::{conversion, materialize};
use crate::rdf::conversion::LanguageCondition;
use oxilangtag::LanguageTag;
use tracing::{debug, debug_span};
use crate::rdf::{conversion, language, materialize};
use crate::rdf::language::LanguageCondition;
static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
BTreeMap::from_iter([
@@ -30,14 +33,15 @@ static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
("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/"),
("rdaf", "http://rdaregistry.info/Elements/rof/"),
("rdat", "http://rdaregistry.info/Elements/t/"),
("rdau", "http://rdaregistry.info/Elements/u/"),
("rdaw", "http://rdaregistry.info/Elements/w/"),
("rdax", "http://rdaregistry.info/Elements/x/"),
("schema", "https://schema.org/"),
@@ -69,17 +73,14 @@ impl OntologyBuilder {
.map(|(k, v)| (k.to_string(), v.to_string()))
.collect::<HashMap<String, String>>();
materialize::same_as(&mut store)?;
/*materialize::same_as(&mut store)?;
materialize::super_properties(&mut store)?;
materialize::super_classes(&mut store)?;
debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;
// Full-text search index field names
let fields = Self::fields(&store)?;
debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;*/
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)
/*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)
@@ -112,7 +113,7 @@ impl OntologyBuilder {
properties,
});
}
}
}*/
let mut indexed_by = HashMap::new();
for quad in store.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None)
@@ -127,80 +128,21 @@ impl OntologyBuilder {
Ok(Ontology {
store,
prefixes,
fields,
entities,
indexed_by,
})
}
}
fn fields(store: &Store) -> error::Result<HashMap<NamedNode, IndexField>> {
let query = SparqlEvaluator::new()
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")?
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
.parse_query(
r#"SELECT DISTINCT ?subject ?name ?label {
GRAPH ?graph {
?subject a gl:IndexDocumentField ;
gl:fieldName ?name ;
gl:fieldLabel ?label .
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label))
}
}"#)?;
let mut results = HashMap::new();
if let QueryResults::Solutions(solutions) = query.on_store(store).execute()?
{
for solution in solutions.filter_map(Result::ok) {
let subject = solution.get("subject").and_then(conversion::term_to_named_node);
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(subject) = subject && let Some(name) = name {
let field = IndexField {
name: name.to_string(),
label: label.map(|l| l.to_string()),
};
results.insert(subject.to_owned(), field);
}
}
}
Ok(results)
}
/*fn subclass_of(dataset: &Dataset, class: NamedNodeRef<'_>) -> impl Iterator<Item = NamedNode> {
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!()
}
}*/
pub struct IndexEntry {
pub catalog_id: u64,
pub fields: HashMap<String, String>,
}
#[derive(Clone, Debug)]
pub struct LabeledIri {
pub iri: NamedNode,
pub label: Option<String>,
pub comment: Option<String>,
pub label: String,
}
impl PartialEq for LabeledIri {
@@ -211,7 +153,7 @@ impl PartialEq for LabeledIri {
impl Display for LabeledIri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label.clone().unwrap_or_default())
write!(f, "{}", self.label)
}
}
@@ -226,19 +168,13 @@ pub struct Entity {
#[derive(Clone, Debug)]
pub struct IndexField {
pub name: String,
pub label: Option<String>,
pub label: String,
}
pub struct Ontology {
store: Store,
prefixes: HashMap<String, String>,
// Resource (Property or Class) -> Rust Type
//iri_info: HashMap<NamedNode, IriInformation>,
// NamedIndividual of class IndexField -> Rust Type
fields: HashMap<NamedNode, IndexField>,
// NamedIndividual of class Entity -> Rust Type
entities: HashMap<NamedNode, Entity>,
@@ -257,6 +193,81 @@ impl Ontology {
&*PREFIXES
}
pub fn index(&self, language: &LanguageCondition) -> error::Result<HashMap<NamedNode, IndexEntry>> {
let language_filter = language.to_filter_expression("fieldValue");
let query = format!(r#"SELECT ?individual ?catalogId ?fieldName ?fieldValue
WHERE {{
?entity a gl:Entity ;
gl:catalogId ?catalogId ;
gl:associatedProperty ?property .
?property gl:indexedByField/gl:fieldName ?fieldName .
?individual a ?entity ;
?property ?fieldValue .
{language_filter}
}}"#);
let mut sparql = SparqlEvaluator::new()
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
.parse_query(&query)?;
sparql.dataset_mut().set_default_graph_as_union();
let mut results = HashMap::new();
if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? {
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("catalogId").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 {
catalog_id,
fields: HashMap::from_iter([(field_name.to_owned(), field_value.to_owned())]),
});
}
}
}
Ok(results)
}
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#")?
.with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")?
.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)
}
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) {
@@ -277,56 +288,24 @@ impl Ontology {
.map(|base| NamedNode::new_unchecked(format!("{base}{name}")))
}
pub fn field_for_property(&self, property: &NamedNode) -> Option<&IndexField> {
self.indexed_by.get(property)
.and_then(|node| self.fields.get(node))
}
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())
}
pub fn catalog_id(&self, class: &NamedNode) -> Option<u64> {
self.entities
.get(class)
.map(|entity| entity.catalog_id)
}
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 })
}
pub fn info(&self, iri: &NamedNode, language: &LanguageCondition) -> LabeledIri {
let label = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::LABEL), None, None)
.filter_map(Result::ok)
.map(conversion::quad_into_term)
.filter(|term| conversion::language_matches(term, language))
.filter(|term| language.primary_matches_term(term))
.filter_map(conversion::term_into_string)
.next();
let comment = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::COMMENT), None, None)
.filter_map(Result::ok)
.map(conversion::quad_into_term)
.filter(|term| conversion::language_matches(term, language))
.filter_map(conversion::term_into_string)
.next();
.next()
.unwrap_or_default();
LabeledIri {
iri: iri.clone(),
label,
comment,
}
}
@@ -334,17 +313,26 @@ impl Ontology {
self.entities.get(class)
.map(|entity| LabeledIri {
iri: class.to_owned(),
label: Some(entity.label.to_owned()),
comment: entity.comment.to_owned(),
label: entity.label.to_owned(),
})
}
pub fn labeled_entities(&self) -> impl Iterator<Item = LabeledIri> {
self.entities.iter()
.map(|(iri, entity)| LabeledIri {
iri: iri.to_owned(),
label: Some(entity.label.to_owned()),
comment: entity.comment.to_owned(),
pub fn labeled_entities(&self, language: &LanguageCondition) -> impl Iterator<Item = LabeledIri> {
self.store.quads_for_pattern(None, Some(rdf::TYPE), Some(gl::ENTITY.into()), None)
.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)
.map(conversion::quad_into_term)
.filter(|term| language.primary_matches_term(term))
.filter_map(conversion::term_into_string)
.next();
if let Some(label) = label && let NamedOrBlankNode::NamedNode(subject) = quad.subject {
Some(LabeledIri {
iri: subject,
label,
})
} else { None }
})
}