This commit is contained in:
2026-08-08 23:50:04 -04:00
parent 9177c8902b
commit 79894f654d
26 changed files with 748 additions and 411 deletions
+13 -8
View File
@@ -1,11 +1,12 @@
use oxigraph::model::{GraphNameRef, NamedNodeRef};
use std::collections::BTreeMap;
use std::sync::LazyLock;
use oxigraph::model::{GraphNameRef, NamedNodeRef};
const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/";
pub static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
BTreeMap::from_iter([
BTreeMap::from_iter(
[
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
("owl", "http://www.w3.org/2002/07/owl#"),
@@ -43,7 +44,9 @@ pub static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
("schema", "https://schema.org/"),
("gl", "http://fedora.quill.lan/rest/"),
("glo", ONTOLOGY_PREFIX),
].map(|(k, v)| (k.to_string(), v.to_string())))
]
.map(|(k, v)| (k.to_string(), v.to_string())),
)
});
#[derive(Clone)]
@@ -53,13 +56,13 @@ pub struct CurieHelper {
impl CurieHelper {
pub fn new(prefixes: BTreeMap<String, String>) -> Self {
Self {
prefixes,
}
Self { prefixes }
}
pub fn abbreviate(&self, base: Option<&str>, iri: &str) -> Option<String> {
if let Some(base) = base && let Some(local_name) = iri.strip_prefix(base) {
if let Some(base) = base
&& let Some(local_name) = iri.strip_prefix(base)
{
return Some(format!(":{local_name}"));
}
@@ -74,7 +77,9 @@ impl CurieHelper {
pub fn expand(&self, base: Option<&str>, abbreviated_iri: &str) -> Option<String> {
let (prefix, name) = abbreviated_iri.split_once(':')?;
if prefix == "" && let Some(base) = base {
if prefix == ""
&& let Some(base) = base
{
Some(format!("{base}{name}"))
} else {
self.prefixes
+1 -1
View File
@@ -17,5 +17,5 @@ pub enum Error {
QueryEvaluation(#[from] oxigraph::sparql::QueryEvaluationError),
#[error(transparent)]
UpdateEvaluation(#[from] oxigraph::sparql::UpdateEvaluationError)
UpdateEvaluation(#[from] oxigraph::sparql::UpdateEvaluationError),
}
+25 -11
View File
@@ -1,13 +1,14 @@
use std::sync::Arc;
use oxigraph::model::{Dataset, GraphNameRef, NamedNodeRef, Quad, QuadRef};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use oxigraph::store::Store;
use tracing::{debug, debug_span, field};
use tracing::{debug_span, field};
const RDF_SCHEMA_PREFIX: &str = "http://www.w3.org/2000/01/rdf-schema#";
const GL_PREFIX: &str = "https://graphofliberty.org/";
const INFERENCE_GRAPH: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked("https://graphofliberty.org/inference"));
const INFERENCE_GRAPH: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(
"https://graphofliberty.org/inference",
));
/// `prp-spo1`
const SUB_PROPERTY_OF_QUERY: &str = r#"CONSTRUCT {
@@ -49,15 +50,23 @@ fn run_query(name: &str, query: &str, dataset: &Dataset) -> crate::Result<Datase
query.dataset_mut().set_default_graph_as_union();
let inferences = if let QueryResults::Graph(result) = query.on_queryable_dataset(dataset).execute()? {
result.filter_map(Result::ok)
.map(|triple| Quad::new(triple.subject, triple.predicate, triple.object, INFERENCE_GRAPH))
let inferences =
if let QueryResults::Graph(result) = query.on_queryable_dataset(dataset).execute()? {
result
.filter_map(Result::ok)
.map(|triple| {
Quad::new(
triple.subject,
triple.predicate,
triple.object,
INFERENCE_GRAPH,
)
})
.collect()
} else {
Dataset::new()
};
Ok(inferences)
}
@@ -86,15 +95,20 @@ impl InferenceEngine {
ontology.extend(store.iter().filter_map(Result::ok));
});
Self {
ontology,
}
Self { ontology }
}
pub fn run(&self, dataset: &Dataset) -> crate::Result<Dataset> {
let span = debug_span!("Inference", inferences = field::Empty).entered();
let input = dataset.iter().map(|triple| QuadRef::new(triple.subject, triple.predicate, triple.object, INFERENCE_GRAPH));
let input = dataset.iter().map(|triple| {
QuadRef::new(
triple.subject,
triple.predicate,
triple.object,
INFERENCE_GRAPH,
)
});
let mut ontology = self.ontology.clone();
ontology.extend(input);
+2 -2
View File
@@ -2,8 +2,8 @@ mod curie;
//mod materialize;
mod error;
pub mod vocab;
pub mod inference;
pub mod vocab;
pub use curie::{PREFIXES, CurieHelper};
pub use curie::{CurieHelper, PREFIXES};
pub use error::{Error, Result};
+123 -72
View File
@@ -3,6 +3,8 @@ use crate::rdf::ontology::{LabeledIri, Ontology};
use crate::rdf::term_helper::{TermHelper, TermHelperMut};
use crate::widget::iri_input::iri_input;
use crate::widget::navigation_area::navigation_area;
use gl_graph::CurieHelper;
use gl_graph::inference::InferenceEngine;
use gl_search::language;
use gl_search::{Schema, SearchDocument, SearchIndex, Value};
use http::StatusCode;
@@ -11,7 +13,10 @@ use iced::keyboard::{Event, key};
use iced::widget::button::Style;
use iced::widget::grid::Sizing;
use iced::widget::text::Wrapping;
use iced::widget::{button, center, column, combo_box, container, grid, mouse_area, opaque, operation, pick_list, row, scrollable, space, stack, table, text, text_input, toggler};
use iced::widget::{
button, center, column, combo_box, container, grid, mouse_area, opaque, operation, pick_list,
row, scrollable, space, stack, table, text, text_input, toggler,
};
use iced::window::Settings;
use iced::{Background, Color, Element, Length, Subscription, Task, color, keyboard, window};
use ldp::middleware::BasicAuthMiddleware;
@@ -21,11 +26,12 @@ use ldp::reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions};
use oxigraph::io::RdfFormat;
use oxigraph::model::vocab::{rdf, rdfs};
use oxigraph::model::{BaseDirection, Dataset, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term, TripleRef};
use oxigraph::model::{
BaseDirection, Dataset, Graph, NamedNode, NamedOrBlankNode, NamedOrBlankNodeRef, Quad, Term,
TripleRef,
};
use oxigraph::store::Store;
use tracing::{debug_span, error, trace};
use gl_graph::CurieHelper;
use gl_graph::inference::InferenceEngine;
#[derive(Clone, Debug)]
pub(crate) enum Message {
@@ -139,9 +145,12 @@ impl Publisher {
let mut abbreviated_datatypes = ontology
.datatypes()
.into_iter()
.map(|node| curie_helper.abbreviate(None, node.as_str())
.map(|node| {
curie_helper
.abbreviate(None, node.as_str())
.unwrap_or(node.as_str().to_string())
).collect::<Vec<_>>();
})
.collect::<Vec<_>>();
abbreviated_datatypes.sort();
let (id, task) = window::open(Settings::default());
@@ -246,14 +255,8 @@ impl Publisher {
});
}
Message::LoadDocument(document) => {
let messages = document
.dataset()
.quads
.iter()
.map(|(key, quad)| {
let datatype_state = combo_box::State::new(
self.abbreviated_datatypes.clone(),
);
let messages = document.dataset().quads.iter().map(|(key, quad)| {
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
let state = RowState {
read_only: self.ontology.is_read_only(quad.as_ref()),
@@ -277,9 +280,7 @@ impl Publisher {
quad.object = Term::NamedNode(empty_node);
let key = self.document.dataset_mut().quads.insert(quad);
let datatype_state = combo_box::State::new(
self.abbreviated_datatypes.clone(),
);
let datatype_state = combo_box::State::new(self.abbreviated_datatypes.clone());
let state = RowState {
read_only: false,
datatype_state,
@@ -306,14 +307,14 @@ impl Publisher {
let selected_entity_class = match action {
SearchResultClickAction::Predicate(_) => Some(rdf::PROPERTY),
SearchResultClickAction::Object(key) => {
self.document
.dataset()
.quads
.get(key)
.and_then(|quad| if quad.predicate == rdf::TYPE {
self.document.dataset().quads.get(key).and_then(|quad| {
if quad.predicate == rdf::TYPE {
Some(rdfs::CLASS)
} else { None })
},
} else {
None
}
})
}
SearchResultClickAction::URLInput => None,
};
@@ -332,7 +333,10 @@ impl Publisher {
}
if let Some(selected_entity_class) = selected_entity_class {
let selection = self.ontology.info(&selected_entity_class.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
let selection = self.ontology.info(
&selected_entity_class.into_owned(),
&*language::ENGLISH_OR_UNTAGGED,
);
task = task.chain(Task::done(Message::QueryTypeUpdated(selection)));
}
}
@@ -344,13 +348,16 @@ impl Publisher {
}
Message::QueryUpdated(new_query) => {
if let Some(search_state) = &mut self.search_state {
let category_id = search_state.entity_selection
let category_id = search_state
.entity_selection
.clone()
.and_then(|info| self.ontology.category_id(&info.iri));
let query = new_query.clone();
search_state.query = new_query;
let results = self.index.query(category_id, query.as_str(), Schema::all_fields(), 25)
let results = self
.index
.query(category_id, query.as_str(), Schema::all_fields(), 25)
.expect("Unable to complete search");
task = Task::done(Message::SetSearchResults(results));
};
@@ -548,7 +555,8 @@ impl Publisher {
self.show_inferred_properties = value;
}
Message::RunInference => {
let dataset = self.document
let dataset = self
.document
.dataset()
.quads
.iter()
@@ -556,9 +564,10 @@ impl Publisher {
.collect();
let engine = self.inference_engine.clone();
task = Task::perform(tokio::task::spawn_blocking(move || {
engine.run(&dataset).unwrap()
}), |dataset| Message::SetInferredTriples(dataset.unwrap()));
task = Task::perform(
tokio::task::spawn_blocking(move || engine.run(&dataset).unwrap()),
|dataset| Message::SetInferredTriples(dataset.unwrap()),
);
}
Message::SetInferredTriples(dataset) => {
self.inferred_triples = Graph::from_iter(&dataset);
@@ -621,22 +630,34 @@ impl Publisher {
subject_input_base.on_input(move |value| Message::SubjectUpdated(key, value))
};
let predicate_input_base = iri_input(&self.curie_helper, "Predicate", Some(&base), triple.predicate.as_str());
let predicate_input_base = iri_input(
&self.curie_helper,
"Predicate",
Some(&base),
triple.predicate.as_str(),
);
let predicate_input = if self.ontology.is_read_only(triple.as_ref()) {
predicate_input_base
} else {
predicate_input_base.on_input(move |value| Message::PredicateUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Predicate(key)))
predicate_input_base
.on_input(move |value| Message::PredicateUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(
SearchResultClickAction::Predicate(key),
))
.on_shift_click(Message::NavigateToPredicate(key))
};
let predicate_info = self.ontology.info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED);
let predicate_info = self
.ontology
.info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED);
let predicate_label = container(text(predicate_info.label));
let term = TermHelper::new(&triple.object);
let value_label = term.value_as_named_node().and_then(|node| {
let info = self.ontology.info(&node.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
let info = self
.ontology
.info(&node.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
Some(container(text(info.label)))
});
@@ -660,7 +681,8 @@ impl Publisher {
if self.ontology.is_read_only(triple.as_ref()) {
base.into()
} else {
base.on_input(move |value| Message::ObjectUpdated(key, value)).into()
base.on_input(move |value| Message::ObjectUpdated(key, value))
.into()
}
} else {
let base = iri_input(&self.curie_helper, "Object", Some(&base), value.as_str())
@@ -670,12 +692,15 @@ impl Publisher {
} else {
base.align_x(value_alignment)
.on_input(move |value| Message::ObjectUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key)))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(
key,
)))
.into()
}
};
let selected_datatype = term.datatype()
let selected_datatype = term
.datatype()
.and_then(|node| self.curie_helper.abbreviate(None, node.as_str()));
let datatype_selector: Element<Message> = if state.read_only {
@@ -690,7 +715,8 @@ impl Publisher {
.on_input(move |input| {
let value = if input.is_empty() { None } else { Some(input) };
Message::DatatypeUpdated(key, value)
}).into()
})
.into()
};
let language = term.language().unwrap_or("en").to_owned();
@@ -748,7 +774,9 @@ impl Publisher {
entities: impl IntoIterator<Item = LabeledIri>,
) -> Element<'_, Message> {
let buttons = entities.into_iter().map(|entity| {
let abbreviation = self.curie_helper.abbreviate(None, entity.iri.as_str())
let abbreviation = self
.curie_helper
.abbreviate(None, entity.iri.as_str())
.unwrap_or_else(|| entity.iri.as_str().to_string());
let label = format!("{} ({})", entity.label, abbreviation);
@@ -766,8 +794,8 @@ impl Publisher {
if let Some(search_state) = &self.search_state
&& search_state.window_id == window
{
let search_input =
text_input("Query", &search_state.query).on_input(Message::QueryUpdated)
let search_input = text_input("Query", &search_state.query)
.on_input(Message::QueryUpdated)
.id("query");
let entities = Vec::new(); /*self.ontology
@@ -775,26 +803,32 @@ impl Publisher {
.into_iter()
.collect::<Vec<_>>();*/
let type_selector = pick_list(search_state.entity_selection.as_ref(), entities, ToString::to_string)
let type_selector = pick_list(
search_state.entity_selection.as_ref(),
entities,
ToString::to_string,
)
.on_select(|selection| Message::QueryTypeUpdated(selection));
let mut columns = vec![
table::column(text("CURIE"), |document: &SearchDocument| {
let iri = document.get_first(Schema::iri_field())
let mut columns = vec![table::column(text("CURIE"), |document: &SearchDocument| {
let iri = document
.get_first(Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let abbreviated_iri = self.curie_helper.abbreviate(None, iri)
let abbreviated_iri = self
.curie_helper
.abbreviate(None, iri)
.unwrap_or_else(|| iri.to_string());
button(text(abbreviated_iri).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
})
];
})];
let fields = if let Some(selection) = search_state.entity_selection.as_ref() {
self.ontology.fields_for_class(&selection.iri, &*language::ENGLISH_OR_UNTAGGED)
self.ontology
.fields_for_class(&selection.iri, &*language::ENGLISH_OR_UNTAGGED)
.expect("Unable to load fields for class")
} else {
vec![]
@@ -804,18 +838,21 @@ impl Publisher {
let field_name = field.name.clone();
columns.push(
table::column(text(field.label), move |document: &SearchDocument| {
let iri = document.get_first(Schema::iri_field())
let iri = document
.get_first(Schema::iri_field())
.and_then(|value| value.as_str())
.unwrap_or_default();
let value = document.get_first(Schema::field(&field_name, Some(language::ENGLISH_PRIMARY)))
let value = document
.get_first(Schema::field(&field_name, Some(language::ENGLISH_PRIMARY)))
.and_then(|value| value.as_str())
.unwrap_or_default();
button(text(value).wrapping(Wrapping::Word))
.on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri)))
.style(button::text)
}).width(Length::Fixed(256.0))
})
.width(Length::Fixed(256.0)),
);
}
@@ -825,10 +862,7 @@ impl Publisher {
Some(scrollable(table(columns, &search_state.results)))
};
return column![
row![search_input, type_selector],
results_table
].into();
return column![row![search_input, type_selector], results_table].into();
}
let back_button = button("\u{1f870}").on_press(Message::NavigateBack);
@@ -841,7 +875,9 @@ impl Publisher {
.on_input(Message::URLInputChanged)
.on_submit(Message::URLInputSubmitted)
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::URLInput))
.style(crate::theme::styles::text_input::required(!self.url_input_valid));
.style(crate::theme::styles::text_input::required(
!self.url_input_valid,
));
let mut rows: Vec<Element<Message>> = vec![];
rows = self
@@ -854,7 +890,9 @@ impl Publisher {
let body: Element<Message> = if self.show_new_document_buttons {
let subclasses = Vec::new(); // TODO self.ontology.subclasses_of(vocab::rda::ENTITY.into_owned());
let labeled_subclasses = subclasses.iter().map(|iri| self.ontology.info(iri, &*language::ENGLISH_OR_UNTAGGED));
let labeled_subclasses = subclasses
.iter()
.map(|iri| self.ontology.info(iri, &*language::ENGLISH_OR_UNTAGGED));
column![self.view_new_entity_buttons(labeled_subclasses)].into()
} else {
column(rows).into()
@@ -864,24 +902,38 @@ impl Publisher {
let subject_column = table::column("Subject", |triple: TripleRef| {
let curie = if let NamedOrBlankNodeRef::NamedNode(subject) = triple.subject {
self.curie_helper.abbreviate(None, subject.as_str())
} else { None };
} else {
None
};
curie.map(text)
});
let predicate_column = table::column("Predicate", |triple: TripleRef| {
let predicate_info = self.ontology.info(&triple.predicate.into_owned(), &*language::ENGLISH_OR_UNTAGGED);
let predicate_info = self.ontology.info(
&triple.predicate.into_owned(),
&*language::ENGLISH_OR_UNTAGGED,
);
let label = if predicate_info.label.is_empty() {
self.curie_helper
.abbreviate(None, triple.predicate.as_str())
.unwrap_or(triple.predicate.as_str().to_string())
} else { predicate_info.label };
} else {
predicate_info.label
};
text(label)
});
let object_column = table::column("Object", |triple: TripleRef| text(triple.object.to_string()));
Some(table([subject_column, predicate_column, object_column], self.inferred_triples.iter()))
} else { None };
let object_column = table::column("Object", |triple: TripleRef| {
text(triple.object.to_string())
});
Some(table(
[subject_column, predicate_column, object_column],
self.inferred_triples.iter(),
))
} else {
None
};
let save_button_base = button(text("\u{1f4be}"));
let save_button = if self.modified {
@@ -890,14 +942,13 @@ impl Publisher {
save_button_base
};
let inferred_type_toggle = toggler(self.show_inferred_types)
.on_toggle(Message::SetInferredTypes);
let inferred_type_toggle =
toggler(self.show_inferred_types).on_toggle(Message::SetInferredTypes);
let inferred_property_toggle = toggler(self.show_inferred_properties)
.on_toggle(Message::SetInferredProperties);
let inferred_property_toggle =
toggler(self.show_inferred_properties).on_toggle(Message::SetInferredProperties);
let read_only_toggle = toggler(self.show_read_only)
.on_toggle(Message::SetReadOnly);
let read_only_toggle = toggler(self.show_read_only).on_toggle(Message::SetReadOnly);
let footer = row![
space::horizontal(),
+1 -1
View File
@@ -1,5 +1,5 @@
use std::path::PathBuf;
use clap::{Args, Parser, Subcommand};
use std::path::PathBuf;
#[derive(Args)]
pub(crate) struct QueryArgs {
+52 -27
View File
@@ -1,14 +1,17 @@
mod app;
mod error;
mod rdf;
mod windows;
mod widget;
mod navigator;
mod theme;
mod args;
mod error;
mod navigator;
mod rdf;
mod theme;
mod widget;
mod windows;
use std::fs::File;
use crate::app::Publisher;
use crate::args::{AppArgs, Command};
use clap::Parser;
use gl_graph::inference::InferenceEngine;
use gl_search::SearchIndex;
use iced::futures::StreamExt;
use ldp::middleware::BasicAuthMiddleware;
use ldp::reqwest::{Client, Url};
@@ -16,23 +19,24 @@ use ldp::reqwest_middleware::ClientBuilder;
use ldp::traverse::Traverse;
use oxigraph::io::{RdfFormat, RdfParser, RdfSerializer};
use oxigraph::model::{Dataset, GraphName, NamedNode, Quad, Triple};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use oxigraph::sparql::results::{QueryResultsFormat, QueryResultsSerializer};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use oxigraph::store::Store;
use std::fs::File;
use tracing::{debug_span, error, field};
use crate::app::Publisher;
use tracing_subscriber::fmt::format::FmtSpan;
use tracing_subscriber::layer::SubscriberExt;
use tracing_subscriber::util::SubscriberInitExt;
use tracing_subscriber::{EnvFilter, fmt};
use tracing_subscriber::fmt::format::FmtSpan;
use gl_graph::inference::InferenceEngine;
use gl_search::SearchIndex;
use crate::args::{AppArgs, Command};
fn main() -> color_eyre::Result<()> {
let appender = tracing_appender::rolling::never("/tmp", "publisher-log");
tracing_subscriber::registry()
.with(fmt::layer().with_span_events(FmtSpan::CLOSE).with_writer(appender))
.with(
fmt::layer()
.with_span_events(FmtSpan::CLOSE)
.with_writer(appender),
)
.with(EnvFilter::from_default_env())
.init();
color_eyre::install()?;
@@ -48,7 +52,10 @@ fn main() -> color_eyre::Result<()> {
let store = Store::open("/home/alex/.local/share/org.graphofliberty.desktop/ontology")?;
let inference_engine = InferenceEngine::new(store.clone());
let graph_name = GraphName::NamedNode(NamedNode::new_unchecked(format!("file://{}", args.dataset_path.to_string_lossy())));
let graph_name = GraphName::NamedNode(NamedNode::new_unchecked(format!(
"file://{}",
args.dataset_path.to_string_lossy()
)));
let dataset = RdfParser::from_format(RdfFormat::Turtle)
.with_default_graph(graph_name)
.for_reader(File::open(&args.dataset_path)?)
@@ -57,11 +64,13 @@ fn main() -> color_eyre::Result<()> {
let dataset_with_inferences = inference_engine.run(&dataset)?;
let raw_query = String::from_utf8(std::fs::read(&args.query_path)?)?;
let mut query = SparqlEvaluator::new()
.parse_query(&raw_query)?;
let mut query = SparqlEvaluator::new().parse_query(&raw_query)?;
query.dataset_mut().set_default_graph_as_union();
match query.on_queryable_dataset(&dataset_with_inferences).execute()? {
match query
.on_queryable_dataset(&dataset_with_inferences)
.execute()?
{
QueryResults::Graph(graph) => {
let mut serializer = RdfSerializer::from_format(RdfFormat::Turtle)
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")?
@@ -78,15 +87,20 @@ fn main() -> color_eyre::Result<()> {
serializer.finish()?;
}
QueryResults::Solutions(solutions) => {
let json_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Json);
let mut writer = json_serializer.serialize_solutions_to_writer(std::io::stdout(), Vec::from_iter(solutions.variables().iter().cloned()))?;
let json_serializer =
QueryResultsSerializer::from_format(QueryResultsFormat::Json);
let mut writer = json_serializer.serialize_solutions_to_writer(
std::io::stdout(),
Vec::from_iter(solutions.variables().iter().cloned()),
)?;
for solution in solutions.filter_map(Result::ok) {
writer.serialize(&solution)?;
}
writer.finish()?;
}
QueryResults::Boolean(result) => {
let json_serializer = QueryResultsSerializer::from_format(QueryResultsFormat::Json);
let json_serializer =
QueryResultsSerializer::from_format(QueryResultsFormat::Json);
json_serializer.serialize_boolean_to_writer(std::io::stdout(), result)?;
}
}
@@ -108,7 +122,11 @@ fn main() -> color_eyre::Result<()> {
{
let span = debug_span!("Index Schema", documents = field::Empty).entered();
let count = gl_search::rdf::index_schema(store, &*gl_search::language::ENGLISH_OR_UNTAGGED, &writer)?;
let count = gl_search::rdf::index_schema(
store,
&*gl_search::language::ENGLISH_OR_UNTAGGED,
&writer,
)?;
span.record("documents", count);
}
@@ -140,8 +158,17 @@ fn main() -> color_eyre::Result<()> {
let inferences = inference_engine.run(&dataset).unwrap();
dataset.extend(&inferences);
let span = debug_span!("Index Repository", rdf_sources = field::Empty, documents = field::Empty).entered();
let document_count = gl_search::rdf::index_entity(&dataset, &*gl_search::language::ENGLISH_OR_UNTAGGED, &writer)?;
let span = debug_span!(
"Index Repository",
rdf_sources = field::Empty,
documents = field::Empty
)
.entered();
let document_count = gl_search::rdf::index_entity(
&dataset,
&*gl_search::language::ENGLISH_OR_UNTAGGED,
&writer,
)?;
span.record("rdf_sources", rdf_source_count);
span.record("documents", document_count);
@@ -149,9 +176,7 @@ fn main() -> color_eyre::Result<()> {
});
let mut writer = runtime.block_on(traversal_task)??;
debug_span!("Commit").in_scope(|| {
writer.commit()
})?;
debug_span!("Commit").in_scope(|| writer.commit())?;
}
None => {
iced::daemon(Publisher::new, Publisher::update, Publisher::view)
+11 -5
View File
@@ -1,5 +1,5 @@
use oxigraph::model::{NamedNode, Quad, Term};
use oxigraph::model::vocab::{rdf, xsd};
use oxigraph::model::{NamedNode, Quad, Term};
pub fn quad_into_term(quad: Quad) -> Term {
quad.object
@@ -51,11 +51,17 @@ pub fn term_to_boolean(term: &Term) -> Option<bool> {
}
pub fn term_to_u64(term: &Term) -> Option<u64> {
if let Term::Literal(literal) = term &&
(literal.datatype() == xsd::NON_NEGATIVE_INTEGER || literal.datatype() == xsd::INTEGER) {
let value: u64 = literal.value().parse().expect("Failed to parse u64 from ontology. It ought to be a non-negative integer.");
if let Term::Literal(literal) = term
&& (literal.datatype() == xsd::NON_NEGATIVE_INTEGER || literal.datatype() == xsd::INTEGER)
{
let value: u64 = literal
.value()
.parse()
.expect("Failed to parse u64 from ontology. It ought to be a non-negative integer.");
Some(value)
} else { None }
} else {
None
}
}
pub fn term_into_u64(term: Term) -> Option<u64> {
+1 -1
View File
@@ -1,3 +1,3 @@
pub(crate) mod conversion;
pub(crate) mod ontology;
pub(crate) mod term_helper;
pub(crate) mod conversion;
+106 -42
View File
@@ -4,7 +4,10 @@ use gl_graph::vocab::gl;
use gl_search::language::LanguageCondition;
use iced::futures::TryFutureExt;
use oxigraph::model::vocab::{rdf, rdfs, xsd};
use oxigraph::model::{Dataset, Graph, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef};
use oxigraph::model::{
Dataset, Graph, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode,
NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef,
};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use oxigraph::store::Store;
use std::collections::{BTreeMap, BTreeSet, HashMap};
@@ -14,10 +17,13 @@ use std::sync::LazyLock;
use tracing::{debug_span, field};
const ONTOLOGY_PREFIX: &str = "https://graphofliberty.org/2026/04/ont/";
const ONTOLOGY_GRAPH_NAME: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont"));
const ONTOLOGY_GRAPH_NAME: GraphNameRef = GraphNameRef::NamedNode(NamedNodeRef::new_unchecked(
"https://graphofliberty.org/2026/04/ont",
));
static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
BTreeMap::from_iter([
BTreeMap::from_iter(
[
("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"),
("rdfs", "http://www.w3.org/2000/01/rdf-schema#"),
("owl", "http://www.w3.org/2002/07/owl#"),
@@ -55,7 +61,9 @@ static PREFIXES: LazyLock<BTreeMap<String, String>> = LazyLock::new(|| {
("schema", "https://schema.org/"),
("gl", "http://fedora.quill.lan/rest/"),
("glo", ONTOLOGY_PREFIX),
].map(|(k, v)| (k.to_string(), v.to_string())))
]
.map(|(k, v)| (k.to_string(), v.to_string())),
)
});
pub struct OntologyBuilder {
@@ -82,18 +90,18 @@ impl OntologyBuilder {
.collect::<HashMap<String, String>>();
let mut indexed_by = HashMap::new();
for quad in store.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None)
.filter_map(Result::ok) {
for quad in store
.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None)
.filter_map(Result::ok)
{
if let NamedOrBlankNode::NamedNode(subject) = quad.subject
&& let Term::NamedNode(field) = quad.object {
&& let Term::NamedNode(field) = quad.object
{
indexed_by.insert(subject, field);
}
}
Ok(Ontology {
store,
prefixes,
})
Ok(Ontology { store, prefixes })
}
}
@@ -146,9 +154,7 @@ pub struct Ontology {
impl Ontology {
pub fn builder() -> OntologyBuilder {
OntologyBuilder {
path: None,
}
OntologyBuilder { path: None }
}
pub fn prefixes() -> &'static BTreeMap<String, String> {
@@ -166,9 +172,14 @@ impl Ontology {
self.store.clone()
}
pub fn query_for_indexable_triples(&self, language: &LanguageCondition, source: Option<Dataset>) -> impl Future<Output = error::Result<HashMap<NamedNode, IndexEntry>>> + 'static {
pub fn query_for_indexable_triples(
&self,
language: &LanguageCondition,
source: Option<Dataset>,
) -> impl Future<Output = error::Result<HashMap<NamedNode, IndexEntry>>> + 'static {
let language_filter = language.to_filter_expression("fieldValue");
let query = format!(r#"SELECT ?individual ?categoryId ?fieldName ?fieldValue
let query = format!(
r#"SELECT ?individual ?categoryId ?fieldName ?fieldValue
WHERE {{
?class a gl:SearchableClass ;
gl:categoryId ?categoryId ;
@@ -180,9 +191,11 @@ WHERE {{
?property ?fieldValue .
{language_filter}
}}"#);
}}"#
);
let mut sparql = SparqlEvaluator::new()
.with_prefix("gl", ONTOLOGY_PREFIX).unwrap()
.with_prefix("gl", ONTOLOGY_PREFIX)
.unwrap()
.parse_query(&query)
.expect("Unable to parse query");
sparql.dataset_mut().set_default_graph_as_union();
@@ -194,23 +207,39 @@ WHERE {{
sparql.on_queryable_dataset(source).execute()
} else {
sparql.on_store(&store).execute()
}.expect("Unable to execute indexing query");
}
.expect("Unable to execute indexing query");
let mut results = HashMap::new();
if let QueryResults::Solutions(solutions) = query_results {
let mut counter = 0usize;
for solution in solutions.filter_map(Result::ok) {
let individual = solution.get("individual").and_then(conversion::term_to_named_node);
let individual = solution
.get("individual")
.and_then(conversion::term_to_named_node);
let catalog_id = solution.get("categoryId").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())
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 {
entry
.fields
.insert(field_name.to_owned(), field_value.to_owned());
})
.or_insert(IndexEntry {
category_id: catalog_id,
fields: HashMap::from_iter([(field_name.to_owned(), field_value.to_owned())]),
fields: HashMap::from_iter([(
field_name.to_owned(),
field_value.to_owned(),
)]),
});
}
counter += 1;
@@ -220,27 +249,35 @@ WHERE {{
unreachable!()
}
results
}).map_err(error::Error::from)
})
.map_err(error::Error::from)
}
pub fn category_id(&self, class: &NamedNode) -> Option<u64> {
self.store.quads_for_pattern(Some(class.into()), Some(gl::CATEGORY_ID), None, None)
self.store
.quads_for_pattern(Some(class.into()), Some(gl::CATEGORY_ID), None, None)
.filter_map(Result::ok)
.map(conversion::quad_into_term)
.filter_map(conversion::term_into_u64)
.next()
}
pub fn fields_for_class(&self, class: &NamedNode, language: &LanguageCondition) -> error::Result<Vec<IndexField>> {
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 {{
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", ONTOLOGY_PREFIX)?
@@ -253,7 +290,9 @@ WHERE {{
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 {
if let Some(name) = name
&& let Some(label) = label
{
results.push(IndexField {
name: name.to_string(),
label: label.to_string(),
@@ -265,7 +304,9 @@ WHERE {{
}
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)
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| language.primary_matches_term(term))
@@ -280,11 +321,16 @@ WHERE {{
}
pub fn subclasses_of(&self, class: &NamedNode) -> BTreeSet<NamedNode> {
self.store.quads_for_pattern(None, Some(rdfs::SUB_CLASS_OF), Some(class.into()), None)
self.store
.quads_for_pattern(None, Some(rdfs::SUB_CLASS_OF), Some(class.into()), None)
.filter_map(Result::ok)
.filter_map(|quad| if let NamedOrBlankNode::NamedNode(subject) = quad.subject {
.filter_map(|quad| {
if let NamedOrBlankNode::NamedNode(subject) = quad.subject {
Some(subject)
} else { None })
} else {
None
}
})
.collect()
}
@@ -300,7 +346,8 @@ WHERE {{
.filter_map(|quad| match quad.subject {
NamedOrBlankNode::NamedNode(subject) => Some(subject),
_ => None,
}).collect()
})
.collect()
}
pub fn is_read_only<'a>(&self, triple: impl Into<TripleRef<'a>>) -> bool {
@@ -313,23 +360,35 @@ WHERE {{
};
self.store
.quads_for_pattern(Some(NamedOrBlankNodeRef::NamedNode(subject)), Some(gl::READ_ONLY), Some(true_term), None)
.quads_for_pattern(
Some(NamedOrBlankNodeRef::NamedNode(subject)),
Some(gl::READ_ONLY),
Some(true_term),
None,
)
.filter_map(Result::ok)
.count() >= 1
.count()
>= 1
}
pub fn exclude_read_only(&self) -> impl Fn(TripleRef<'_>) -> bool + 'static {
let true_term = TermRef::Literal(LiteralRef::new_typed_literal("true", xsd::BOOLEAN));
let read_only_graph = self.store
let read_only_graph = self
.store
.quads_for_pattern(None, Some(gl::READ_ONLY), Some(true_term), None)
.filter_map(Result::ok)
.map(Triple::from)
.collect::<Graph>();
move |triple| {
if triple.predicate == rdf::TYPE && let TermRef::NamedNode(class) = triple.object {
if triple.predicate == rdf::TYPE
&& let TermRef::NamedNode(class) = triple.object
{
read_only_graph.triples_for_subject(class).count() == 0
} else {
read_only_graph.triples_for_subject(triple.predicate).count() == 0
read_only_graph
.triples_for_subject(triple.predicate)
.count()
== 0
}
}
}
@@ -353,7 +412,12 @@ WHERE {{
if let Term::BlankNode(blank_node) = quad.object {
let iter = self
.store
.quads_for_pattern(Some(NamedOrBlankNodeRef::BlankNode(blank_node.as_ref())), None, None, None)
.quads_for_pattern(
Some(NamedOrBlankNodeRef::BlankNode(blank_node.as_ref())),
None,
None,
None,
)
.filter_map(Result::ok)
.map(Triple::from)
.map(move |mut triple| {
+1 -1
View File
@@ -1,7 +1,7 @@
pub mod text_input {
use iced::{color, Theme};
use iced::widget::text_input;
use iced::widget::text_input::{Status, Style};
use iced::{Theme, color};
pub fn required(invalid: bool) -> impl Fn(&Theme, Status) -> Style {
move |theme: &Theme, status| {
+95 -22
View File
@@ -1,12 +1,12 @@
use iced::{keyboard, widget, Element, Event, Length, Rectangle, Size};
use iced::advanced::{mouse, text, Shell, renderer};
use gl_graph::CurieHelper;
use iced::advanced::layout::{Limits, Node};
use iced::advanced::widget::{Operation, Tree, tree};
use iced::advanced::{Layout, Widget};
use iced::advanced::widget::{tree, Tree, Operation};
use iced::advanced::{Shell, mouse, renderer, text};
use iced::clipboard::Content;
use iced::mouse::{Cursor, Interaction};
use iced::widget::text_input::{Catalog, Status, Style, StyleFn};
use gl_graph::CurieHelper;
use iced::{Element, Event, Length, Rectangle, Size, keyboard, widget};
pub struct State {
control: bool,
@@ -31,8 +31,15 @@ where
Theme: Catalog,
Renderer: text::Renderer,
{
pub fn new(curie_helper: &'a CurieHelper, placeholder: &'a str, base: Option<&str>, iri: &str) -> Self {
let display_value = curie_helper.abbreviate(base, iri).unwrap_or_else(|| iri.to_string());
pub fn new(
curie_helper: &'a CurieHelper,
placeholder: &'a str,
base: Option<&str>,
iri: &str,
) -> Self {
let display_value = curie_helper
.abbreviate(base, iri)
.unwrap_or_else(|| iri.to_string());
let text_input = widget::TextInput::new(placeholder, display_value);
Self {
curie_helper,
@@ -107,7 +114,12 @@ where
}
}
pub fn iri_input<'a, Message, Theme, Renderer>(curie_helper: &'a CurieHelper, placeholder: &'a str, base: Option<&str>, iri: &str) -> IriInput<'a, Message, Theme, Renderer>
pub fn iri_input<'a, Message, Theme, Renderer>(
curie_helper: &'a CurieHelper,
placeholder: &'a str,
base: Option<&str>,
iri: &str,
) -> IriInput<'a, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
@@ -116,7 +128,8 @@ where
IriInput::new(curie_helper, placeholder, base, iri)
}
impl <Message, Theme, Renderer> Widget<Message, Theme, Renderer> for IriInput<'_, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for IriInput<'_, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
@@ -127,11 +140,34 @@ where
}
fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
Widget::layout(&mut self.text_input, &mut tree.children[0], renderer, limits)
Widget::layout(
&mut self.text_input,
&mut tree.children[0],
renderer,
limits,
)
}
fn draw(&self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, style: &renderer::Style, layout: Layout<'_>, cursor: Cursor, viewport: &Rectangle) {
Widget::draw(&self.text_input, &tree.children[0], renderer, theme, style, layout, cursor, viewport)
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
Widget::draw(
&self.text_input,
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
)
}
fn tag(&self) -> tree::Tag {
@@ -156,10 +192,20 @@ where
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.text_input.operate(&mut tree.children[0], layout, renderer, operation);
self.text_input
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(&mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: Cursor, renderer: &Renderer, shell: &mut Shell<'_, Message>, viewport: &Rectangle) {
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
let state = tree.state.downcast_mut::<State>();
match event {
@@ -169,12 +215,16 @@ where
}
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if cursor.is_over(layout.bounds()) {
if state.control && let Some(on_control_click) = &self.on_control_click {
if state.control
&& let Some(on_control_click) = &self.on_control_click
{
shell.publish(on_control_click.clone());
shell.capture_event();
}
if state.shift && let Some(on_shift_click) = &self.on_shift_click {
if state.shift
&& let Some(on_shift_click) = &self.on_shift_click
{
shell.publish(on_shift_click.clone());
shell.capture_event();
}
@@ -183,7 +233,16 @@ where
_ => {}
}
Widget::update(&mut self.text_input, &mut tree.children[0], event, layout, cursor, renderer, shell, viewport);
Widget::update(
&mut self.text_input,
&mut tree.children[0],
event,
layout,
cursor,
renderer,
shell,
viewport,
);
let clipboard = shell.clipboard_mut();
if let Some(Content::Text(content)) = &clipboard.write {
@@ -193,15 +252,29 @@ where
}
}
fn mouse_interaction(&self, tree: &Tree, layout: Layout<'_>, cursor: Cursor, viewport: &Rectangle, renderer: &Renderer) -> Interaction {
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> Interaction {
let state = tree.state.downcast_ref::<State>();
let interaction = if cursor.is_over(layout.bounds()) {
if (state.control && self.on_control_click.is_some()) ||
(state.shift && self.on_shift_click.is_some()) {
if (state.control && self.on_control_click.is_some())
|| (state.shift && self.on_shift_click.is_some())
{
Some(Interaction::Pointer)
} else { None }
} else { None };
} else {
None
}
} else {
None
};
interaction.unwrap_or_else(|| Widget::mouse_interaction(&self.text_input, tree, layout, cursor, viewport, renderer))
interaction.unwrap_or_else(|| {
Widget::mouse_interaction(&self.text_input, tree, layout, cursor, viewport, renderer)
})
}
}
+34 -7
View File
@@ -1,10 +1,10 @@
use iced::advanced::{mouse, text, Layout, Shell, Widget, overlay};
use iced::{Element, Event, Length, Rectangle, Size, Vector};
use iced::advanced::layout::{Limits, Node};
use iced::advanced::renderer::Style;
use iced::advanced::widget::{Operation, Tree};
use iced::advanced::{Layout, Shell, Widget, mouse, overlay, text};
use iced::mouse::Cursor;
use iced::widget::text_input::Catalog;
use iced::{Element, Event, Length, Rectangle, Size, Vector};
pub struct NavigationArea<'a, Message, Theme, Renderer>
where
@@ -56,7 +56,9 @@ where
}
}
pub fn navigation_area<'a, Message, Theme, Renderer>(content: impl Into<Element<'a, Message, Theme, Renderer>>) -> NavigationArea<'a, Message, Theme, Renderer>
pub fn navigation_area<'a, Message, Theme, Renderer>(
content: impl Into<Element<'a, Message, Theme, Renderer>>,
) -> NavigationArea<'a, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
@@ -65,7 +67,8 @@ where
NavigationArea::new(content)
}
impl <Message, Theme, Renderer> Widget<Message, Theme, Renderer> for NavigationArea<'_, Message, Theme, Renderer>
impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for NavigationArea<'_, Message, Theme, Renderer>
where
Message: Clone,
Theme: Catalog,
@@ -81,7 +84,16 @@ where
.layout(&mut tree.children[0], renderer, limits)
}
fn draw(&self, tree: &Tree, renderer: &mut Renderer, theme: &Theme, style: &Style, layout: Layout<'_>, cursor: Cursor, viewport: &Rectangle) {
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
style: &Style,
layout: Layout<'_>,
cursor: Cursor,
viewport: &Rectangle,
) {
self.content.as_widget().draw(
&tree.children[0],
renderer,
@@ -97,13 +109,28 @@ where
tree.diff_children(std::slice::from_mut(&mut self.content));
}
fn operate(&mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation) {
fn operate(
&mut self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation,
) {
self.content
.as_widget_mut()
.operate(&mut tree.children[0], layout, renderer, operation);
}
fn update(&mut self, tree: &mut Tree, event: &Event, layout: Layout<'_>, cursor: Cursor, renderer: &Renderer, shell: &mut Shell<'_, Message>, viewport: &Rectangle) {
fn update(
&mut self,
tree: &mut Tree,
event: &Event,
layout: Layout<'_>,
cursor: Cursor,
renderer: &Renderer,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) {
self.content.as_widget_mut().update(
&mut tree.children[0],
event,
+1
View File
@@ -0,0 +1 @@
+7 -13
View File
@@ -1,7 +1,7 @@
use std::collections::HashMap;
use std::fs;
use crate::error;
use crate::schema::Schema;
use std::collections::HashMap;
use std::fs;
use std::path::PathBuf;
use tantivy::collector::TopDocs;
use tantivy::directory::{ManagedDirectory, MmapDirectory};
@@ -24,9 +24,7 @@ impl SearchIndexBuilder {
pub fn build(self) -> error::Result<SearchIndex> {
let ngram_32 = NgramTokenizer::new(1, 32, false)?;
let ngram_32_lowercase = TextAnalyzer::builder(ngram_32)
.filter(LowerCaser)
.build();
let ngram_32_lowercase = TextAnalyzer::builder(ngram_32).filter(LowerCaser).build();
let tokenizer_manager = TokenizerManager::default();
tokenizer_manager.register("ngram_32", ngram_32_lowercase);
@@ -49,10 +47,7 @@ impl SearchIndexBuilder {
.reload_policy(ReloadPolicy::OnCommitWithDelay)
.try_into()?;
Ok(SearchIndex {
index,
reader,
})
Ok(SearchIndex { index, reader })
}
}
@@ -82,9 +77,7 @@ impl SearchIndex {
let parser = QueryParser::for_index(&self.index, default_fields);
let (user_query, _) = parser.parse_query_lenient(user_query);
let mut subqueries = vec![
(Occur::Must, user_query)
];
let mut subqueries = vec![(Occur::Must, user_query)];
if let Some(type_) = type_ {
let doc_type_term = Term::from_field_u64(Schema::discriminant_field(), type_);
@@ -94,7 +87,8 @@ impl SearchIndex {
let query = BooleanQuery::new(subqueries);
let searcher = self.reader.searcher();
let results: Vec<TantivyDocument> = searcher.search(&query, &TopDocs::with_limit(limit).order_by_score())?
let results: Vec<TantivyDocument> = searcher
.search(&query, &TopDocs::with_limit(limit).order_by_score())?
.iter()
.map(|(_, address)| searcher.doc(*address))
.filter_map(Result::ok)
+16 -7
View File
@@ -1,11 +1,13 @@
use std::sync::LazyLock;
use oxigraph::model::{Term, TermRef};
use oxilangtag::LanguageTag;
use std::sync::LazyLock;
pub const ENGLISH_PRIMARY: &str = "en";
pub static ENGLISH_OR_UNTAGGED: LazyLock<LanguageCondition> = LazyLock::new(|| {
LanguageCondition::ExactMatchOrUntagged(LanguageTag::parse(ENGLISH_PRIMARY.to_string()).unwrap())
LanguageCondition::ExactMatchOrUntagged(
LanguageTag::parse(ENGLISH_PRIMARY.to_string()).unwrap(),
)
});
pub enum LanguageCondition {
@@ -18,13 +20,16 @@ pub enum LanguageCondition {
impl LanguageCondition {
pub fn primary_matches_term<'a>(&self, term: impl Into<TermRef<'a>>) -> bool {
if let TermRef::Literal(literal) = term.into() {
let tag = literal.language()
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(),
(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,
@@ -45,8 +50,12 @@ impl LanguageCondition {
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::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(),
}
+3 -3
View File
@@ -1,15 +1,15 @@
mod error;
mod index;
pub mod language;
pub mod rdf;
mod schema;
pub mod subtitles;
pub mod rdf;
pub mod language;
use oxigraph::model::NamedNodeRef;
pub use tantivy::TantivyDocument as SearchDocument;
pub use tantivy::doc;
pub use tantivy::schema::document::{Document, Value};
pub use tantivy::indexer::IndexWriter;
pub use tantivy::schema::document::{Document, Value};
pub use error::{Result, SearchError};
use gl_graph::vocab;
+97 -36
View File
@@ -1,14 +1,14 @@
use std::collections::HashMap;
use oxigraph::model::{Dataset, NamedNode, NamedNodeRef, NamedOrBlankNodeRef, Term, TermRef};
use oxigraph::model::vocab::{rdf, xsd};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use oxigraph::store::Store;
use tantivy::IndexWriter;
use tantivy::schema::{Field, OwnedValue};
use gl_graph::{vocab, CurieHelper};
use crate::DocType;
use crate::language::LanguageCondition;
use crate::schema::Schema;
use gl_graph::{CurieHelper, vocab};
use oxigraph::model::vocab::{rdf, xsd};
use oxigraph::model::{Dataset, NamedNode, NamedNodeRef, NamedOrBlankNodeRef, Term, TermRef};
use oxigraph::sparql::{QueryResults, SparqlEvaluator};
use oxigraph::store::Store;
use std::collections::HashMap;
use tantivy::IndexWriter;
use tantivy::schema::{Field, OwnedValue};
fn term_to_named_node(term: &Term) -> Option<&NamedNode> {
if let Term::NamedNode(node) = term {
@@ -52,13 +52,18 @@ fn term_ref_as_str(term: TermRef<'_>) -> Option<&str> {
}
}
pub fn index_entity(dataset: &Dataset, language: &LanguageCondition, writer: &IndexWriter<HashMap<Field, OwnedValue>>) -> crate::Result<usize> {
pub fn index_entity(
dataset: &Dataset,
language: &LanguageCondition,
writer: &IndexWriter<HashMap<Field, OwnedValue>>,
) -> crate::Result<usize> {
let curie_helper = CurieHelper::new((&*gl_graph::PREFIXES).clone());
let mut subject_type_map = HashMap::new();
for quad in dataset.quads_for_pattern(None, Some(rdf::TYPE), None, None) {
if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject {
subject_type_map.entry(subject)
subject_type_map
.entry(subject)
.and_modify(|types: &mut Vec<_>| types.push(quad.object))
.or_insert_with(|| vec![quad.object]);
}
@@ -67,19 +72,28 @@ pub fn index_entity(dataset: &Dataset, language: &LanguageCondition, writer: &In
let mut counter = 0usize;
for (subject, types) in subject_type_map {
for type_ in types {
if let Some(doc_type) = term_ref_as_named_node(type_).and_then(DocType::try_from_named_node) {
if let Some(doc_type) =
term_ref_as_named_node(type_).and_then(DocType::try_from_named_node)
{
let mut doc = match doc_type {
DocType::Person => index_person(dataset, subject, language),
DocType::CorporateBody => index_corporate_body(dataset, subject, language),
_ => continue
_ => continue,
};
let subject_str = subject.as_str();
doc.insert(Schema::discriminant_field(), OwnedValue::U64(doc_type as u64));
doc.insert(Schema::iri_field(), OwnedValue::Str(subject_str.to_string()));
doc.insert(
Schema::discriminant_field(),
OwnedValue::U64(doc_type as u64),
);
doc.insert(
Schema::iri_field(),
OwnedValue::Str(subject_str.to_string()),
);
let curie = curie_helper.abbreviate(None, subject_str)
let curie = curie_helper
.abbreviate(None, subject_str)
.map(OwnedValue::Str)
.unwrap_or(OwnedValue::Null);
doc.insert(Schema::curie_field(), curie);
@@ -93,8 +107,18 @@ pub fn index_entity(dataset: &Dataset, language: &LanguageCondition, writer: &In
Ok(counter)
}
fn index_person(dataset: &Dataset, subject: NamedNodeRef<'_>, language: &LanguageCondition) -> HashMap<Field, OwnedValue> {
let given_name = dataset.quads_for_pattern(Some(subject.into()), Some(vocab::rdaad::GIVEN_NAME), None, None)
fn index_person(
dataset: &Dataset,
subject: NamedNodeRef<'_>,
language: &LanguageCondition,
) -> HashMap<Field, OwnedValue> {
let given_name = dataset
.quads_for_pattern(
Some(subject.into()),
Some(vocab::rdaad::GIVEN_NAME),
None,
None,
)
.map(|q| q.object)
.filter(|term| language.primary_matches_term(*term))
.filter_map(term_ref_as_str)
@@ -103,7 +127,13 @@ fn index_person(dataset: &Dataset, subject: NamedNodeRef<'_>, language: &Languag
.next()
.unwrap_or_else(|| OwnedValue::Null);
let surname = dataset.quads_for_pattern(Some(subject.into()), Some(vocab::rdaad::SURNAME), None, None)
let surname = dataset
.quads_for_pattern(
Some(subject.into()),
Some(vocab::rdaad::SURNAME),
None,
None,
)
.map(|q| q.object)
.filter(|term| language.primary_matches_term(*term))
.filter_map(term_ref_as_str)
@@ -113,13 +143,29 @@ fn index_person(dataset: &Dataset, subject: NamedNodeRef<'_>, language: &Languag
.unwrap_or_else(|| OwnedValue::Null);
HashMap::from_iter([
(Schema::field("given_name", language.primary_language()), given_name),
(Schema::field("surname", language.primary_language()), surname),
(
Schema::field("given_name", language.primary_language()),
given_name,
),
(
Schema::field("surname", language.primary_language()),
surname,
),
])
}
fn index_corporate_body(dataset: &Dataset, subject: NamedNodeRef<'_>, language: &LanguageCondition) -> HashMap<Field, OwnedValue> {
let name = dataset.quads_for_pattern(Some(subject.into()), Some(vocab::rdaad::NAME_OF_CORPORATE_BODY), None, None)
fn index_corporate_body(
dataset: &Dataset,
subject: NamedNodeRef<'_>,
language: &LanguageCondition,
) -> HashMap<Field, OwnedValue> {
let name = dataset
.quads_for_pattern(
Some(subject.into()),
Some(vocab::rdaad::NAME_OF_CORPORATE_BODY),
None,
None,
)
.map(|q| q.object)
.filter(|term| language.primary_matches_term(*term))
.filter_map(term_ref_as_str)
@@ -128,18 +174,24 @@ fn index_corporate_body(dataset: &Dataset, subject: NamedNodeRef<'_>, language:
.next()
.unwrap_or_else(|| OwnedValue::Null);
HashMap::from_iter([
(Schema::field("corporate_name", language.primary_language()), name),
])
HashMap::from_iter([(
Schema::field("corporate_name", language.primary_language()),
name,
)])
}
pub fn index_schema(store: Store, language: &LanguageCondition, writer: &IndexWriter<HashMap<Field, OwnedValue>>) -> crate::Result<usize> {
pub fn index_schema(
store: Store,
language: &LanguageCondition,
writer: &IndexWriter<HashMap<Field, OwnedValue>>,
) -> crate::Result<usize> {
let curie_helper = CurieHelper::new((&*gl_graph::PREFIXES).clone());
let label_filter = language.to_filter_expression("label");
let description_filter = language.to_filter_expression("description");
let query = format!(r#"SELECT ?class ?subject ?label ?description WHERE {{
let query = format!(
r#"SELECT ?class ?subject ?label ?description WHERE {{
VALUES ?class {{ rdf:Property rdfs:Class skos:Concept }}
?subject a ?class ;
rdfs:label ?label .
@@ -149,11 +201,15 @@ pub fn index_schema(store: Store, language: &LanguageCondition, writer: &IndexWr
OPTIONAL {{ ?subject skos:definition ?definition }}
BIND(COALESCE(?definition, ?comment) AS ?description)
{description_filter}
}}"#);
}}"#
);
let mut sparql = SparqlEvaluator::new()
.with_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#").unwrap()
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#").unwrap()
.with_prefix("skos", "http://www.w3.org/2004/02/skos/core#").unwrap()
.with_prefix("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#")
.unwrap()
.with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")
.unwrap()
.with_prefix("skos", "http://www.w3.org/2004/02/skos/core#")
.unwrap()
.parse_query(&query)?;
sparql.dataset_mut().set_default_graph_as_union();
@@ -167,7 +223,8 @@ pub fn index_schema(store: Store, language: &LanguageCondition, writer: &IndexWr
for solution in solutions.filter_map(Result::ok) {
let mut document = HashMap::with_capacity(4);
let discriminant = solution.get("class")
let discriminant = solution
.get("class")
.and_then(term_to_named_node)
.and_then(DocType::try_from_named_node)
.map(|doc_type| doc_type as u64)
@@ -175,7 +232,8 @@ pub fn index_schema(store: Store, language: &LanguageCondition, writer: &IndexWr
.unwrap_or(OwnedValue::Null);
document.insert(Schema::discriminant_field(), discriminant);
let subject_str = solution.get("subject")
let subject_str = solution
.get("subject")
.and_then(term_to_named_node)
.map(NamedNode::as_str);
@@ -185,17 +243,20 @@ pub fn index_schema(store: Store, language: &LanguageCondition, writer: &IndexWr
.unwrap_or(OwnedValue::Null);
document.insert(Schema::curie_field(), curie);
let subject = subject_str.map(OwnedValue::from)
let subject = subject_str
.map(OwnedValue::from)
.unwrap_or(OwnedValue::Null);
document.insert(Schema::iri_field(), subject);
let label = solution.get("label")
let label = solution
.get("label")
.and_then(term_as_str)
.map(OwnedValue::from)
.unwrap_or(OwnedValue::Null);
document.insert(label_field, label);
let definition = solution.get("description")
let definition = solution
.get("description")
.and_then(term_as_str)
.map(OwnedValue::from)
.unwrap_or(OwnedValue::Null);
+8 -4
View File
@@ -11,17 +11,21 @@ pub struct Schema;
impl Schema {
pub fn schema() -> &'static TantivySchema {
SCHEMA.get_or_init(|| {
let stored_ngram32 = TextOptions::default().set_indexing_options(
let stored_ngram32 = TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_index_option(IndexRecordOption::WithFreqsAndPositions)
.set_tokenizer("ngram_32"),
).set_stored();
)
.set_stored();
let stored_en_stem = TextOptions::default().set_indexing_options(
let stored_en_stem = TextOptions::default()
.set_indexing_options(
TextFieldIndexing::default()
.set_index_option(IndexRecordOption::WithFreqsAndPositions)
.set_tokenizer("en_stem"),
).set_stored();
)
.set_stored();
let mut schema_builder = TantivySchema::builder();
schema_builder.add_u64_field("discriminant", schema::FAST | schema::INDEXED);
+6 -3
View File
@@ -1,7 +1,7 @@
use crate::{Schema, SearchDocument};
use subtitler;
use subtitler::SubtitleFormat;
use tantivy::doc;
use crate::{Schema, SearchDocument};
pub fn bytes_to_documents(data: &[u8]) -> crate::Result<Vec<SearchDocument>> {
let subtitle_start = Schema::field("subtitle_start", None);
@@ -10,12 +10,15 @@ pub fn bytes_to_documents(data: &[u8]) -> crate::Result<Vec<SearchDocument>> {
let subtitle_file = subtitler::parse_bytes(data)?;
let subtitles = subtitle_file.subtitles();
let documents = subtitles.iter().map(|subtitle| {
let documents = subtitles
.iter()
.map(|subtitle| {
doc!(
subtitle_start => subtitle.start,
subtitle_end => subtitle.end,
subtitle_content => subtitle.text,
)
}).collect::<Vec<_>>();
})
.collect::<Vec<_>>();
Ok(documents)
}