This commit is contained in:
2026-08-22 23:31:14 -04:00
parent 631bb539f1
commit e28cfd1c53
7 changed files with 234 additions and 189 deletions
+3
View File
@@ -30,4 +30,7 @@ pub enum Error {
#[error(transparent)]
TonicStatus(#[from] gl_inference::tonic::Status),
#[error("Ontology client is not connected")]
NotConnected,
}
+15 -62
View File
@@ -1,16 +1,12 @@
use rayon::iter::ParallelIterator;
use std::collections::{HashMap, HashSet};
use oxigraph::model::{Graph, NamedNode, TermRef, NamedOrBlankNodeRef, NamedNodeRef};
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput};
use rayon::iter::IntoParallelRefIterator;
use gl_inference::tonic::transport::Channel;
use gl_inference::proto::ontology_client::OntologyClient;
use gl_inference::proto::OntologyQueryRequest;
use gl_search::{Field, OwnedValue, Schema};
use crate::class::Class;
use crate::{helpers, vocab, CurieHelper};
use crate::helpers::{term_as_str, term_to_named_node};
use crate::language::LanguageCondition;
use crate::ontology::ResourceDescription;
pub struct Indexer<'a> {
language: LanguageCondition,
@@ -83,7 +79,7 @@ impl<'a> Indexer<'a> {
])
}
pub fn graph(&self, graph: &Graph) -> crate::Result<Vec<HashMap<Field, OwnedValue>>> {
pub fn graph(&self, graph: &Graph) -> Vec<HashMap<Field, OwnedValue>> {
let mut entities_and_types = HashSet::new();
let triples = graph.triples_for_predicate(vocab::rdf::TYPE);
for triple in triples {
@@ -119,85 +115,42 @@ impl<'a> Indexer<'a> {
})
}).collect();
Ok(results)
results
}
pub async fn ontology(&self, client: &mut OntologyClient<Channel>) -> crate::Result<Vec<HashMap<Field, OwnedValue>>> {
let filter = LanguageCondition::filter([
(String::from("label"), self.language.clone()),
(String::from("description"), self.language.clone()),
]);
pub fn ontology(&self, entities: HashMap<NamedNode, (NamedNode, ResourceDescription)>) -> Vec<HashMap<Field, OwnedValue>> {
let label_field = Schema::field("label", self.language.primary_language());
let definition_field = Schema::field("definition", self.language.primary_language());
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 ?class ?subject ?label ?description WHERE {{
VALUES ?class {{ rdf:Property rdfs:Class skos:Concept }}
?subject a ?class ;
rdfs:label ?label .
OPTIONAL {{ ?subject rdfs:comment ?comment }}
OPTIONAL {{ ?subject skos:definition ?definition }}
BIND(COALESCE(?definition, ?comment) AS ?description)
{filter}
}}"#));
let response = client.query(request).await?;
let parser_output = QueryResultsParser::from_format(QueryResultsFormat::Json)
.for_slice(&response.get_ref().results)?;
let mut results = Vec::new();
if let SliceQueryResultsParserOutput::Solutions(solutions) = parser_output {
let primary_language = self.language.primary_language();
let label_field = Schema::field("label", primary_language);
let definition_field = Schema::field("definition", primary_language);
for solution in solutions.filter_map(Result::ok) {
entities.iter()
.map(|(subject, (class, description))| {
let mut document = HashMap::with_capacity(4);
let discriminant = solution
.get("class")
.and_then(term_to_named_node)
.and_then(Class::try_from_named_node)
let discriminant = Class::try_from_named_node(class)
.map(|doc_type| doc_type as u64)
.map(OwnedValue::U64)
.unwrap_or(OwnedValue::Null);
document.insert(Schema::discriminant_field(), discriminant);
let subject_str = solution
.get("subject")
.and_then(term_to_named_node)
.map(NamedNode::as_str);
let curie = subject_str
.and_then(|subject| self.curie_helper.abbreviate(None, subject))
let curie = self.curie_helper.abbreviate(None, subject.as_str())
.map(OwnedValue::Str)
.unwrap_or(OwnedValue::Null);
document.insert(Schema::curie_field(), curie);
let subject = subject_str
.map(OwnedValue::from)
.unwrap_or(OwnedValue::Null);
let subject = OwnedValue::from(subject.as_str());
document.insert(Schema::iri_field(), subject);
let label = solution
.get("label")
.and_then(term_as_str)
let label = description.label.clone()
.map(OwnedValue::from)
.unwrap_or(OwnedValue::Null);
document.insert(label_field, label);
let definition = solution
.get("description")
.and_then(term_as_str)
let definition = description.description.clone()
.map(OwnedValue::from)
.unwrap_or(OwnedValue::Null);
document.insert(definition_field, definition);
results.push(document);
}
}
Ok(results)
document
}).collect()
}
}
+21 -33
View File
@@ -53,47 +53,35 @@ impl LanguageCondition {
}
}
pub fn filter(conditions: impl IntoIterator<Item = (String, Self)>) -> String {
let exact_expression = |variable: String, language| Expression::FunctionCall(
pub fn filter(&self, variable: impl Into<String>) -> String {
let variable = variable.into();
let exact = |language: &LanguageTag<String>| Expression::FunctionCall(
Function::LangMatches, vec![
Expression::FunctionCall(Function::Lang, vec![
Expression::Variable(Variable::new_unchecked(variable))
Expression::Variable(Variable::new_unchecked(&variable))
]),
Expression::Literal(Literal::new_simple_literal(language))
Expression::Literal(Literal::new_simple_literal(language.to_string()))
],
);
let untagged_expression = |variable| Expression::Not(
let untagged = Expression::Not(
Box::new(Expression::FunctionCall(Function::HasLang, vec![
Expression::Variable(Variable::new_unchecked(variable))
Expression::Variable(Variable::new_unchecked(&variable))
]))
);
let condition_to_expression = |variable, condition| match condition {
LanguageCondition::ExactMatchOnly(language) =>
Some(exact_expression(variable, language.to_string())),
LanguageCondition::ExactMatchOrUntagged(language) => {
Some(Expression::Or(
Box::new(exact_expression(variable.clone(), language.to_string())),
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));
if let Some(expression) = first_expression {
let expr = iter.filter_map(|(variable, condition)| condition_to_expression(variable, condition))
.fold(expression, |acc, next_exp| Expression::Or(Box::new(acc), Box::new(next_exp)));
GraphPattern::Filter {
expr,
inner: Box::new(GraphPattern::Bgp { patterns: vec![] }),
}.to_string()
} else {
String::default()
}
match self {
Self::ExactMatchOnly(language) => Some(exact(language)),
Self::ExactMatchOrUntagged(language) => Some(Expression::Or(
Box::new(exact(language)),
Box::new(untagged),
)),
Self::UntaggedOnly => Some(untagged),
Self::AnyOrNone => None,
}.map(|expr| GraphPattern::Filter {
expr,
inner: Box::new(GraphPattern::Bgp { patterns: vec![] }),
}.to_string()).unwrap_or_default()
}
}
+142 -39
View File
@@ -1,12 +1,13 @@
use std::collections::{HashMap, HashSet};
use std::fmt::Debug;
use std::str::FromStr;
use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
use oxigraph::model::{Graph, NamedNode, Triple, TripleRef};
use oxigraph::model::{Graph, NamedNode, NamedNodeRef, Triple};
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsParser, SliceQueryResultsParserOutput};
use tracing::debug;
use spargebra::algebra::GraphPattern;
use spargebra::term::{GroundTerm, Variable};
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;
@@ -24,31 +25,51 @@ pub enum ReadOnlyEntity {
Class(NamedNode),
}
pub struct OntologyBuilder {
endpoint: Endpoint,
language: LanguageCondition,
}
impl OntologyBuilder {
pub fn from_string(endpoint: &str, language: LanguageCondition) -> crate::Result<Self> {
let endpoint = Endpoint::from_str(endpoint)?
.connect_timeout(std::time::Duration::from_secs(10))
.tcp_keepalive(Some(std::time::Duration::from_secs(30)))
.http2_keep_alive_interval(std::time::Duration::from_secs(15))
.keep_alive_timeout(std::time::Duration::from_secs(20))
.keep_alive_while_idle(true);
Ok(Self {
endpoint,
language,
})
}
pub async fn connect(self) -> crate::Result<ConnectedOntology> {
let client = OntologyClient::connect(self.endpoint)
.await?
.max_decoding_message_size(1024 * 1024 * 1024);
Ok(ConnectedOntology {
client,
language: self.language,
})
}
}
#[derive(Clone)]
pub struct Ontology {
pub struct ConnectedOntology {
client: OntologyClient<Channel>,
language: LanguageCondition,
}
impl Debug for Ontology {
impl Debug for ConnectedOntology {
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,
})
}
impl ConnectedOntology {
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)
@@ -118,23 +139,24 @@ SELECT ?subject ?class WHERE {{
}
pub async fn datatypes(&mut self) -> crate::Result<HashMap<NamedNode, ResourceDescription>> {
let filter = LanguageCondition::filter([
(String::from("label"), self.language.clone()),
(String::from("description"), self.language.clone()),
]);
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: <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 }}
OPTIONAL {{ ?subject rdfs:comment ?comment }}
OPTIONAL {{ ?subject skos:definition ?definition }}
BIND(COALESCE(?definition, ?comment) AS ?description)
{filter}
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?;
@@ -167,12 +189,10 @@ SELECT ?subject ?label ?description WHERE {{
Ok(results)
}
pub async fn resource_description(&mut self, subject: NamedNode) -> crate::Result<ResourceDescription> {
let filter = LanguageCondition::filter([
(String::from("label"), self.language.clone()),
(String::from("description"), self.language.clone()),
]);
debug!("Filter: {filter}");
pub async fn resource_description_from_subject(&mut self, subject: NamedNode) -> crate::Result<ResourceDescription> {
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: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
@@ -180,13 +200,21 @@ 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 }}
OPTIONAL {{ {subject} rdfs:comment ?comment }}
OPTIONAL {{ {subject} skos:definition ?definition }}
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)
{filter}
}}"#));
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)?;
@@ -213,4 +241,79 @@ SELECT ?label ?description WHERE {{
)
} else { unreachable!() }
}
fn named_nodes_to_values_expression<'a>(variable: &str, nodes: impl IntoIterator<Item = NamedNodeRef<'a>>) -> 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)],
bindings,
}.to_string()
}
pub async fn resource_descriptions_from_classes<'a>(&mut self, classes: impl IntoIterator<Item = NamedNodeRef<'a>>) -> crate::Result<HashMap<NamedNode, (NamedNode, ResourceDescription)>> {
let values = Self::named_nodes_to_values_expression("class", classes);
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: <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 ?class ?subject ?label ?description WHERE {{
{values}
?subject a ?class .
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 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 class = solution
.get("class")
.and_then(term_to_named_node);
let subject = solution
.get("subject")
.and_then(term_to_named_node)
.map(|node| node.clone());
if let Some(subject) = subject &&
let Some(class) = class {
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, (class.clone(), ResourceDescription { label, description }));
}
}
}
Ok(results)
}
}