This commit is contained in:
2026-08-20 20:43:18 -04:00
parent 025392b129
commit fcb1da7e0d
21 changed files with 637 additions and 178 deletions
+1
View File
@@ -10,5 +10,6 @@ gl-search.workspace = true
oxigraph.workspace = true
oxilangtag.workspace = true
rayon.workspace = true
spargebra.workspace = true
thiserror.workspace = true
tracing.workspace = true
+74 -1
View File
@@ -1,6 +1,12 @@
use oxigraph::model::NamedNodeRef;
use std::collections::HashMap;
use std::fmt::Display;
use oxigraph::model::{NamedNode, NamedNodeRef};
use oxilangtag::LanguageTag;
use gl_search::Schema;
use crate::language::LanguageCondition;
use crate::vocab;
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum Class {
RdfProperty = 0,
RdfsClass = 1,
@@ -12,7 +18,33 @@ pub enum Class {
Manifestation = 10007,
}
impl Display for Class {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Class::RdfProperty => f.write_str("RDF Property"),
Class::RdfsClass => f.write_str("RDFS Class"),
Class::SkosConcept => f.write_str("SKOS Concept"),
Class::Work => f.write_str("Work"),
Class::Person => f.write_str("Person"),
Class::CorporateBody => f.write_str("Corporate Body"),
Class::Expression => f.write_str("Expression"),
Class::Manifestation => f.write_str("Manifestation"),
}
}
}
impl Class {
pub const ALL: &[Class] = &[
Class::RdfProperty,
Class::RdfsClass,
Class::SkosConcept,
Class::Work,
Class::Person,
Class::CorporateBody,
Class::Expression,
Class::Manifestation,
];
pub fn to_named_node(&self) -> NamedNodeRef<'_> {
match self {
Class::RdfProperty => vocab::rdf::PROPERTY,
@@ -40,4 +72,45 @@ impl Class {
_ => None,
}
}
pub fn fields(&self, language: LanguageTag<&str>) -> Vec<(gl_search::Field, String)> {
match self {
Class::RdfProperty => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("label", Some(language.primary_language())), String::from("Label")),
(Schema::field("definition", Some(language.primary_language())), String::from("Definition")),
],
Class::RdfsClass => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("label", Some(language.primary_language())), String::from("Label")),
(Schema::field("definition", Some(language.primary_language())), String::from("Definition")),
],
Class::SkosConcept => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("label", Some(language.primary_language())), String::from("Label")),
(Schema::field("definition", Some(language.primary_language())), String::from("Definition")),
],
Class::Work => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("title", Some(language.primary_language())), String::from("Title")),
],
Class::Person => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("given_name", Some(language.primary_language())), String::from("Given Name")),
(Schema::field("surname", Some(language.primary_language())), String::from("Surname")),
],
Class::CorporateBody => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("corporate_body", Some(language.primary_language())), String::from("Corporate Body")),
],
Class::Expression => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("title", Some(language.primary_language())), String::from("Title")),
],
Class::Manifestation => vec![
(Schema::curie_field(), String::from("CURIE")),
(Schema::field("title", Some(language.primary_language())), String::from("Title")),
],
}
}
}
+6
View File
@@ -4,6 +4,9 @@ pub type Result<R> = std::result::Result<R, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
#[error(transparent)]
IriParse(#[from] oxigraph::model::IriParseError),
@@ -22,6 +25,9 @@ pub enum Error {
#[error(transparent)]
UpdateEvaluation(#[from] oxigraph::sparql::UpdateEvaluationError),
#[error(transparent)]
TonicTransport(#[from] gl_inference::tonic::transport::Error),
#[error(transparent)]
TonicStatus(#[from] gl_inference::tonic::Status),
}
+1 -1
View File
@@ -1,4 +1,4 @@
use oxigraph::model::{NamedNode, NamedNodeRef, Term, TermRef};
use oxigraph::model::{NamedNode, Term, TermRef};
use oxigraph::model::vocab::xsd;
use crate::vocab::rdf;
+10 -1
View File
@@ -74,6 +74,15 @@ impl<'a> Indexer<'a> {
])
}
fn manifestation(&self, graph: &Graph, subject: NamedNodeRef) -> HashMap<Field, OwnedValue> {
HashMap::from_iter([
(
Schema::field("title", self.language.primary_language()),
self.extract_string(graph, subject, vocab::rdamd::TITLE_OF_MANIFESTATION),
),
])
}
pub fn graph(&self, graph: &Graph) -> crate::Result<Vec<HashMap<Field, OwnedValue>>> {
let mut entities_and_types = HashSet::new();
let triples = graph.triples_for_predicate(vocab::rdf::TYPE);
@@ -95,7 +104,7 @@ impl<'a> Indexer<'a> {
Class::Person => Some(self.person(graph, *subject)),
Class::CorporateBody => Some(self.corporate_body(graph, *subject)),
Class::Expression => Some(self.expression(graph, *subject)),
Class::Manifestation => Some(self.person(graph, *subject)),
Class::Manifestation => Some(self.manifestation(graph, *subject)),
}.map(|mut document| {
document.insert(Schema::discriminant_field(), OwnedValue::from(class as u64));
document.insert(Schema::iri_field(), OwnedValue::Str(subject.as_str().to_string()));
+50 -3
View File
@@ -1,6 +1,9 @@
use oxigraph::model::TermRef;
use oxigraph::model::{Literal, TermRef};
use oxilangtag::LanguageTag;
use std::sync::LazyLock;
use spargebra::algebra::{Expression, Function, GraphPattern};
use spargebra::term::{NamedNode, NamedNodePattern, TermPattern, TriplePattern, Variable};
use tracing::debug;
pub const ENGLISH_PRIMARY: &str = "en";
@@ -10,7 +13,7 @@ pub static ENGLISH_OR_UNTAGGED: LazyLock<LanguageCondition> = LazyLock::new(|| {
)
});
#[derive(Clone)]
#[derive(Clone, Debug)]
pub enum LanguageCondition {
ExactMatchOnly(LanguageTag<String>),
ExactMatchOrUntagged(LanguageTag<String>),
@@ -49,7 +52,51 @@ impl LanguageCondition {
}
}
pub fn to_filter_expression(&self, var: &str) -> String {
pub fn filter(conditions: impl IntoIterator<Item = (String, Self)>) -> String {
let exact_expression = |language, variable| Expression::FunctionCall(
Function::LangMatches, vec![
Expression::FunctionCall(Function::Lang, vec![
Expression::Variable(Variable::new_unchecked(variable))
]),
Expression::Literal(Literal::new_simple_literal(language))
],
);
let untagged_expression = |variable| Expression::Not(
Box::new(Expression::FunctionCall(Function::HasLang, vec![
Expression::Variable(Variable::new_unchecked(variable))
]))
);
let condition_to_expression = |variable, condition| match condition {
LanguageCondition::ExactMatchOnly(language) =>
Some(exact_expression(language.to_string(), variable)),
LanguageCondition::ExactMatchOrUntagged(language) => {
Some(Expression::Or(
Box::new(exact_expression(language.to_string(), variable.clone())),
Box::new(untagged_expression(variable)),
))
}
LanguageCondition::UntaggedOnly => Some(untagged_expression(variable)),
LanguageCondition::AnyOrNone => None,
};
let mut iter = conditions.into_iter();
let first_expression = iter.next()
.and_then(|(variable, condition)| condition_to_expression(variable, condition));
let remaining_expressions = iter.filter_map(|(variable, condition)| condition_to_expression(variable, condition))
.collect::<Vec<_>>();
let concatenated_expressions = if let Some(expression) = first_expression {
if remaining_expressions.is_empty() {
//Expression::
}
} else { String::default() }
}
pub fn to_sparql_expression(&self, var: &str) -> String {
let foo =
debug!("Test: {foo}");
match self {
LanguageCondition::ExactMatchOnly(language) => {
format!(r#"FILTER(langMATCHES(LANG(?{var}), "{language}"))"#)
+1
View File
@@ -7,6 +7,7 @@ pub mod category;
pub mod language;
mod helpers;
pub mod indexer;
pub mod ontology;
pub use curie::{CurieHelper, PREFIXES};
pub use error::{Error, Result};
+214
View File
@@ -0,0 +1,214 @@
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
use oxigraph::model::{Graph, NamedNode, Triple, TripleRef};
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput};
use gl_inference::proto::ontology_client::OntologyClient;
use gl_inference::proto::OntologyQueryRequest;
use gl_inference::tonic::codegen::StdError;
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};
#[derive(Clone, Debug, Default)]
pub struct ResourceDescription {
pub label: Option<String>,
pub description: Option<String>,
}
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
pub enum ReadOnlyEntity {
Property(NamedNode),
Class(NamedNode),
}
#[derive(Clone)]
pub struct Ontology {
client: OntologyClient<Channel>,
language: LanguageCondition,
}
impl Debug for Ontology {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("ontology")
}
}
impl Ontology {
pub async fn new<D>(endpoint: D, language: LanguageCondition) -> crate::Result<Self>
where
D: TryInto<Endpoint>,
D::Error: Into<StdError>,
{
let client = OntologyClient::connect(endpoint).await?;
Ok(Self {
client,
language,
})
}
pub async fn run_inference(&mut self, graph: &Graph) -> crate::Result<Graph> {
let mut output_buffer = Vec::new();
let mut serializer = RdfSerializer::from_format(RdfFormat::Turtle)
.for_writer(output_buffer);
for triple in graph {
serializer.serialize_triple(triple)?;
}
output_buffer = serializer.finish()?;
let turtle = String::from_utf8_lossy(&output_buffer).to_string();
let mut request = OntologyQueryRequest::default();
request.sparql_query = None;
request.turtle = Some(turtle);
request.base = None;
request.inferences_only = true;
let response = self.client.query(request).await?;
Ok(RdfParser::from_format(RdfFormat::Turtle)
.for_slice(&response.get_ref().results)
.filter_map(Result::ok)
.map(Triple::from)
.collect::<Graph>())
}
pub async fn list_read_only(&mut self) -> crate::Result<HashSet<ReadOnlyEntity>> {
let mut request = OntologyQueryRequest::default();
request.sparql_query = Some(format!(r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
PREFIX gl: <https://graphofliberty.org/2026/04/ont/>
SELECT ?subject ?class WHERE {{
VALUES ?class {{ rdf:Property rdfs:Class }}
?subject a ?class ;
gl:readOnly "true"^^xsd:boolean .
}}"#));
let response = self.client.query(request).await?;
let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json)
.for_slice(&response.get_ref().results)?;
let mut results = HashSet::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());
let class = solution
.get("class")
.and_then(term_to_named_node)
.map(|node| node.clone());
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),
_ => continue,
};
results.insert(entry);
}
}
}
Ok(results)
}
pub async fn datatypes(&mut self) -> crate::Result<HashMap<NamedNode, ResourceDescription>> {
let label_filter = self.language.to_filter_expression("label");
let description_filter = self.language.to_filter_expression("description");
let mut request = OntologyQueryRequest::default();
request.sparql_query = Some(format!(r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?subject ?label ?description WHERE {{
?subject a rdfs:Datatype
OPTIONAL {{ ?subject rdfs:label ?label }}
{label_filter}
OPTIONAL {{ ?subject rdfs:comment ?comment }}
OPTIONAL {{ ?subject skos:definition ?definition }}
BIND(COALESCE(?definition, ?comment) AS ?description)
{description_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(&mut self, subject: NamedNode) -> crate::Result<ResourceDescription> {
let label_filter = self.language.to_filter_expression("label");
let description_filter = self.language.to_filter_expression("description");
let mut request = OntologyQueryRequest::default();
request.sparql_query = Some(format!(r#"PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX rdfs: <http://www.w3.org/2000/01/rdf-schema#>
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?label ?description WHERE {{
OPTIONAL {{ {subject} rdfs:label ?label }}
{label_filter}
OPTIONAL {{ {subject} rdfs:comment ?comment }}
OPTIONAL {{ {subject} skos:definition ?definition }}
BIND(COALESCE(?definition, ?comment) AS ?description)
{description_filter}
}}"#));
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!() }
}
}
+7
View File
@@ -69,4 +69,11 @@ pub mod rdaed {
pub const TITLE_OF_EXPRESSION: NamedNodeRef =
NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/e/datatype/P20312");
}
pub mod rdamd {
use oxigraph::model::NamedNodeRef;
pub const TITLE_OF_MANIFESTATION: NamedNodeRef =
NamedNodeRef::new_unchecked("http://rdaregistry.info/Elements/m/datatype/P30335");
}