use anyhow::Result;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::PathBuf;
pub use oxigraph;
use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
use oxigraph::model::*;
use oxigraph::sparql::{QueryResults, QuerySolutionIter};
use oxigraph::store::Store;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct KnowledgeGraphConfig {
pub storage_path: Option<PathBuf>,
pub default_namespace: String,
pub namespaces: HashMap<String, String>,
pub enable_reasoning: bool,
}
impl Default for KnowledgeGraphConfig {
fn default() -> Self {
let mut namespaces = HashMap::new();
namespaces.insert(
"rdf".to_string(),
"http://www.w3.org/1999/02/22-rdf-syntax-ns#".to_string(),
);
namespaces.insert(
"rdfs".to_string(),
"http://www.w3.org/2000/01/rdf-schema#".to_string(),
);
namespaces.insert(
"owl".to_string(),
"http://www.w3.org/2002/07/owl#".to_string(),
);
namespaces.insert(
"xsd".to_string(),
"http://www.w3.org/2001/XMLSchema#".to_string(),
);
namespaces.insert(
"rk".to_string(),
"https://reasonkit.sh/ontology#".to_string(),
);
Self {
storage_path: None,
default_namespace: "https://reasonkit.sh/ontology#".to_string(),
namespaces,
enable_reasoning: false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Triple {
pub subject: String,
pub predicate: String,
pub object: String,
pub graph: Option<String>,
}
impl Triple {
pub fn new(
subject: impl Into<String>,
predicate: impl Into<String>,
object: impl Into<String>,
) -> Self {
Self {
subject: subject.into(),
predicate: predicate.into(),
object: object.into(),
graph: None,
}
}
pub fn in_graph(mut self, graph: impl Into<String>) -> Self {
self.graph = Some(graph.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct QueryRow {
pub bindings: HashMap<String, String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SparqlResults {
pub variables: Vec<String>,
pub rows: Vec<QueryRow>,
pub execution_time_ms: u64,
}
pub struct KnowledgeGraph {
store: Store,
config: KnowledgeGraphConfig,
}
impl KnowledgeGraph {
pub fn new() -> Result<Self> {
Self::with_config(KnowledgeGraphConfig::default())
}
pub fn with_config(config: KnowledgeGraphConfig) -> Result<Self> {
let store = if let Some(path) = &config.storage_path {
Store::open(path)?
} else {
Store::new()?
};
Ok(Self { store, config })
}
pub fn open(path: impl Into<PathBuf>) -> Result<Self> {
let config = KnowledgeGraphConfig {
storage_path: Some(path.into()),
..Default::default()
};
Self::with_config(config)
}
pub fn add_triple(&self, triple: &Triple) -> Result<()> {
let subject = self.parse_subject(&triple.subject)?;
let predicate = NamedNode::new(&triple.predicate)?;
let object = self.parse_object(&triple.object)?;
let quad = if let Some(graph_name) = &triple.graph {
Quad::new(subject, predicate, object, NamedNode::new(graph_name)?)
} else {
Quad::new(subject, predicate, object, GraphName::DefaultGraph)
};
self.store.insert(&quad)?;
Ok(())
}
pub fn add_triples(&self, triples: &[Triple]) -> Result<()> {
for triple in triples {
self.add_triple(triple)?;
}
Ok(())
}
pub fn remove_triple(&self, triple: &Triple) -> Result<()> {
let subject = self.parse_subject(&triple.subject)?;
let predicate = NamedNode::new(&triple.predicate)?;
let object = self.parse_object(&triple.object)?;
let quad = if let Some(graph_name) = &triple.graph {
Quad::new(subject, predicate, object, NamedNode::new(graph_name)?)
} else {
Quad::new(subject, predicate, object, GraphName::DefaultGraph)
};
self.store.remove(&quad)?;
Ok(())
}
pub fn query(&self, sparql: &str) -> Result<SparqlResults> {
let start = std::time::Instant::now();
let query_with_prefixes = self.add_prefixes(sparql);
let results = self.store.query(&query_with_prefixes)?;
match results {
QueryResults::Solutions(solutions) => {
let variables: Vec<String> = solutions
.variables()
.iter()
.map(|v| v.as_str().to_string())
.collect();
let rows = self.collect_solutions(solutions)?;
Ok(SparqlResults {
variables,
rows,
execution_time_ms: start.elapsed().as_millis() as u64,
})
}
_ => anyhow::bail!("Expected SELECT query results"),
}
}
pub fn ask(&self, sparql: &str) -> Result<bool> {
let query_with_prefixes = self.add_prefixes(sparql);
let results = self.store.query(&query_with_prefixes)?;
match results {
QueryResults::Boolean(b) => Ok(b),
_ => anyhow::bail!("Expected ASK query results"),
}
}
pub fn update(&self, sparql: &str) -> Result<()> {
let update_with_prefixes = self.add_prefixes(sparql);
self.store.update(&update_with_prefixes)?;
Ok(())
}
pub fn load_rdf(&self, data: &str, format: RdfFormat) -> Result<()> {
for quad_result in RdfParser::from_format(format).for_reader(data.as_bytes()) {
let quad = quad_result?;
self.store.insert(&quad)?;
}
Ok(())
}
pub fn export_rdf(&self, format: RdfFormat) -> Result<String> {
let mut writer = RdfSerializer::from_format(format).for_writer(Vec::new());
for quad in self.store.iter() {
writer.serialize_quad(&quad?)?;
}
Ok(String::from_utf8(writer.finish()?)?)
}
pub fn len(&self) -> Result<usize> {
Ok(self.store.len()?)
}
pub fn is_empty(&self) -> Result<bool> {
Ok(self.store.is_empty()?)
}
pub fn clear(&self) -> Result<()> {
self.store.clear()?;
Ok(())
}
pub fn find(
&self,
subject: Option<&str>,
predicate: Option<&str>,
object: Option<&str>,
) -> Result<Vec<Triple>> {
let mut results = Vec::new();
for quad in self.store.iter() {
let quad = quad?;
let subject_str = self.term_to_string(&quad.subject.clone().into());
let s_match = subject.map_or(true, |s| subject_str.as_str() == s);
let p_match = predicate.map_or(true, |p| quad.predicate.as_str() == p);
let o_match = object.map_or(true, |o| self.term_to_string(&quad.object) == o);
if s_match && p_match && o_match {
results.push(Triple {
subject: subject_str,
predicate: quad.predicate.as_str().to_string(),
object: self.term_to_string(&quad.object),
graph: match quad.graph_name {
GraphName::DefaultGraph => None,
GraphName::NamedNode(n) => Some(n.as_str().to_string()),
GraphName::BlankNode(b) => Some(format!("_:{}", b.as_str())),
},
});
}
}
Ok(results)
}
fn parse_subject(&self, s: &str) -> Result<Subject> {
if let Some(stripped) = s.strip_prefix("_:") {
Ok(BlankNode::new(stripped)?.into())
} else {
Ok(NamedNode::new(s)?.into())
}
}
fn parse_object(&self, o: &str) -> Result<Term> {
if let Some(stripped) = o.strip_prefix("_:") {
Ok(BlankNode::new(stripped)?.into())
} else if o.starts_with("http://") || o.starts_with("https://") || o.starts_with("urn:") {
Ok(NamedNode::new(o)?.into())
} else {
Ok(Literal::new_simple_literal(o).into())
}
}
fn term_to_string(&self, term: &Term) -> String {
match term {
Term::NamedNode(n) => n.as_str().to_string(),
Term::BlankNode(b) => format!("_:{}", b.as_str()),
Term::Literal(l) => l.value().to_string(),
Term::Triple(_) => "[triple]".to_string(),
}
}
fn add_prefixes(&self, sparql: &str) -> String {
let mut prefixes = String::new();
for (prefix, uri) in &self.config.namespaces {
prefixes.push_str(&format!("PREFIX {}: <{}>\n", prefix, uri));
}
format!("{}{}", prefixes, sparql)
}
fn collect_solutions(&self, solutions: QuerySolutionIter) -> Result<Vec<QueryRow>> {
let mut rows = Vec::new();
for solution in solutions {
let solution = solution?;
let mut bindings = HashMap::new();
for (var, term) in solution.iter() {
bindings.insert(var.as_str().to_string(), self.term_to_string(term));
}
rows.push(QueryRow { bindings });
}
Ok(rows)
}
}
impl Default for KnowledgeGraph {
fn default() -> Self {
Self::new().expect("Failed to create default knowledge graph")
}
}
pub struct OntologyBuilder {
namespace: String,
triples: Vec<Triple>,
}
impl OntologyBuilder {
pub fn new(namespace: impl Into<String>) -> Self {
Self {
namespace: namespace.into(),
triples: Vec::new(),
}
}
pub fn reasonkit() -> Self {
Self::new("https://reasonkit.sh/ontology#")
}
pub fn class(mut self, name: &str, label: &str, description: &str) -> Self {
let class_iri = format!("{}{}", self.namespace, name);
self.triples.push(Triple::new(
&class_iri,
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"http://www.w3.org/2002/07/owl#Class",
));
self.triples.push(Triple::new(
&class_iri,
"http://www.w3.org/2000/01/rdf-schema#label",
label,
));
self.triples.push(Triple::new(
&class_iri,
"http://www.w3.org/2000/01/rdf-schema#comment",
description,
));
self
}
pub fn subclass(mut self, child: &str, parent: &str) -> Self {
let child_iri = format!("{}{}", self.namespace, child);
let parent_iri = format!("{}{}", self.namespace, parent);
self.triples.push(Triple::new(
child_iri,
"http://www.w3.org/2000/01/rdf-schema#subClassOf",
parent_iri,
));
self
}
pub fn property(mut self, name: &str, label: &str, domain: &str, range: &str) -> Self {
let prop_iri = format!("{}{}", self.namespace, name);
let domain_iri = format!("{}{}", self.namespace, domain);
let range_iri = format!("{}{}", self.namespace, range);
self.triples.push(Triple::new(
&prop_iri,
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"http://www.w3.org/2002/07/owl#ObjectProperty",
));
self.triples.push(Triple::new(
&prop_iri,
"http://www.w3.org/2000/01/rdf-schema#label",
label,
));
self.triples.push(Triple::new(
&prop_iri,
"http://www.w3.org/2000/01/rdf-schema#domain",
domain_iri,
));
self.triples.push(Triple::new(
&prop_iri,
"http://www.w3.org/2000/01/rdf-schema#range",
range_iri,
));
self
}
pub fn build(self) -> Vec<Triple> {
self.triples
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_config_default() {
let config = KnowledgeGraphConfig::default();
assert!(config.namespaces.contains_key("rdf"));
assert!(config.namespaces.contains_key("rk"));
}
#[test]
fn test_triple_creation() {
let triple = Triple::new(
"https://example.org/subject",
"https://example.org/predicate",
"https://example.org/object",
);
assert!(triple.graph.is_none());
let triple_with_graph = triple.in_graph("https://example.org/graph");
assert!(triple_with_graph.graph.is_some());
}
#[test]
fn test_knowledge_graph_basic() {
let kg = KnowledgeGraph::new().unwrap();
let triple = Triple::new(
"https://reasonkit.sh/entity/1",
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"https://reasonkit.sh/ontology#ThinkTool",
);
kg.add_triple(&triple).unwrap();
assert_eq!(kg.len().unwrap(), 1);
kg.remove_triple(&triple).unwrap();
assert!(kg.is_empty().unwrap());
}
#[test]
fn test_sparql_query() {
let kg = KnowledgeGraph::new().unwrap();
kg.add_triple(&Triple::new(
"https://reasonkit.sh/tool/gigathink",
"http://www.w3.org/1999/02/22-rdf-syntax-ns#type",
"https://reasonkit.sh/ontology#ThinkTool",
))
.unwrap();
kg.add_triple(&Triple::new(
"https://reasonkit.sh/tool/gigathink",
"http://www.w3.org/2000/01/rdf-schema#label",
"GigaThink",
))
.unwrap();
let results = kg.query("SELECT ?s WHERE { ?s a rk:ThinkTool }").unwrap();
assert_eq!(results.rows.len(), 1);
}
#[test]
fn test_ontology_builder() {
let triples = OntologyBuilder::reasonkit()
.class("ThinkTool", "ThinkTool", "A reasoning tool")
.class("GigaThink", "GigaThink", "Expansive creative thinking")
.subclass("GigaThink", "ThinkTool")
.build();
assert!(triples.len() >= 7); }
}