diff --git a/ontology/graphofliberty.ttl b/ontology/graphofliberty.ttl index fd63a32..9786f29 100644 --- a/ontology/graphofliberty.ttl +++ b/ontology/graphofliberty.ttl @@ -226,6 +226,24 @@ fedora:lastModifiedBy rdf:type owl:NamedIndividual ; :readOnly "true"^^xsd:boolean . +### http://rdaregistry.info/Elements/a/datatype/P50291 + rdf:type owl:NamedIndividual ; + :indexedByField :Surname . + + +### http://rdaregistry.info/Elements/a/datatype/P50292 + rdf:type owl:NamedIndividual ; + :indexedByField :GivenName . + + +### http://rdaregistry.info/Elements/c/C10004 +rdac:C10004 rdf:type owl:NamedIndividual , + :SearchableClass ; + :associatedProperty , + ; + :catalogId "2"^^xsd:nonNegativeInteger . + + ### http://www.w3.org/1999/02/22-rdf-syntax-ns#Property rdf:Property rdf:type owl:NamedIndividual , :SearchableClass ; @@ -292,6 +310,14 @@ ldp:contains rdf:type owl:NamedIndividual ; rdfs:label "Definition Field"@en . +### https://graphofliberty.org/2026/04/ont/GivenName +:GivenName rdf:type owl:NamedIndividual , + :IndexDocumentField ; + :fieldLabel "Given Name"@en ; + :fieldName "given_name" ; + rdfs:label "Given Name Field"@en . + + ### https://graphofliberty.org/2026/04/ont/Label :Label rdf:type owl:NamedIndividual , :IndexDocumentField ; @@ -300,4 +326,12 @@ ldp:contains rdf:type owl:NamedIndividual ; rdfs:label "Label Field"@en . +### https://graphofliberty.org/2026/04/ont/Surname +:Surname rdf:type owl:NamedIndividual , + :IndexDocumentField ; + :fieldLabel "Surname"@en ; + :fieldName "surname" ; + rdfs:label "Surname Field"@en . + + ### Generated by the OWL API (version 4.5.29.2024-05-13T12:11:03Z) https://github.com/owlcs/owlapi diff --git a/publish/src/app.rs b/publish/src/app.rs index 86aa84b..f14aa3e 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -1,4 +1,5 @@ -use crate::rdf::ontology::{LabeledIri, Ontology}; +use std::collections::HashMap; +use crate::rdf::ontology::{IndexEntry, LabeledIri, Ontology}; use crate::rdf::term_helper::{TermHelper, TermHelperMut}; use gl_search::{Schema, SearchDocument, SearchIndex, doc, IndexWriter, Value}; use http::StatusCode; @@ -17,9 +18,10 @@ use ldp::traverse::Traverse; use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions}; use oxigraph::io::RdfFormat; use oxigraph::model::vocab::{rdf, rdfs}; -use oxigraph::model::{BaseDirection, BlankNode, Dataset, NamedNode, NamedNodeRef, NamedOrBlankNode, Quad, Term, Triple}; -use tracing::{debug, debug_span, error, info, trace}; -use crate::app::Message::URLInputSubmitted; +use oxigraph::model::{BaseDirection, Dataset, NamedNode, Quad, Term}; +use tracing::{debug, debug_span, error, trace}; +use tracing::span::EnteredSpan; +use crate::rdf::curie::CurieHelper; use crate::rdf::language; use crate::rdf::vocab::rda; use crate::widget::iri_input::iri_input; @@ -27,9 +29,11 @@ use crate::widget::iri_input::iri_input; #[derive(Clone, Debug)] pub(crate) enum Message { None, + RebuildIndex, Traverse, - IndexRdfSource(RdfSource), - CommitIndex, + AddRdfSource(RdfSource), + ConcludeTraversal, + IndexQueryResults(HashMap), WindowClosed(window::Id), URLInputChanged(String), URLInputSubmitted, @@ -86,6 +90,7 @@ struct SearchState { pub(crate) struct Publisher { http_client: ClientWithMiddleware, + curie_helper: CurieHelper, ontology: Ontology, abbreviated_datatypes: Vec, window_id: window::Id, @@ -94,7 +99,7 @@ pub(crate) struct Publisher { hovered_row: Option, search_state: Option, index: SearchIndex, - index_writer: Option, + traversal: Option<(Dataset, EnteredSpan)>, show_overwrite_confirmation: bool, modified: bool, show_new_document_buttons: bool, @@ -102,6 +107,8 @@ pub(crate) struct Publisher { impl Publisher { pub(crate) fn new() -> (Self, Task) { + let curie_helper = CurieHelper::new(Ontology::prefixes().clone()); + let ontology = debug_span!("Ontology Creation").in_scope(|| { Ontology::builder() .with_path("/home/alex/.local/share/org.graphofliberty.desktop/ontology") @@ -114,31 +121,12 @@ impl Publisher { .build() .expect("Failed to build search index"); - /*debug_span!("Ontology Indexing").in_scope(|| { - let mut counter = 0; - let mut writer = index.writer().expect("Failed to build index writer"); - let index = ontology.index(&*language::ENGLISH_OR_UNTAGGED).expect("Unable to generate index of entities"); - for (individual, entry) in index { - let mut document = doc!( - Schema::type_field() => entry.catalog_id, - Schema::iri_field() => individual.as_str(), - Schema::curie_field() => ontology.abbreviate(individual.as_ref()), - ); - for (key, value) in entry.fields { - document.add_text(Schema::field(&key, language::ENGLISH_PRIMARY), value.as_str()); - } - writer.add(document).expect("Failed to add document to search index"); - counter += 1; - } - writer.commit().expect("Failed to commit changes to search index"); - debug!("Added {counter} documents to search index"); - });*/ - let mut abbreviated_datatypes = ontology .datatypes() .into_iter() - .map(|node| ontology.abbreviate(node.as_ref())) - .collect::>(); + .map(|node| curie_helper.abbreviate(node.as_str()) + .unwrap_or(node.as_str().to_string()) + ).collect::>(); abbreviated_datatypes.sort(); let (id, task) = window::open(Settings::default()); @@ -157,6 +145,7 @@ impl Publisher { ( Self { http_client, + curie_helper, ontology, abbreviated_datatypes, window_id: id, @@ -165,7 +154,7 @@ impl Publisher { hovered_row: None, search_state: None, index, - index_writer: None, + traversal: None, show_overwrite_confirmation: false, modified: false, show_new_document_buttons: false, @@ -179,48 +168,80 @@ impl Publisher { trace!(?message); match message { + Message::RebuildIndex => { + self.index + .writer() + .expect("Unable to create writer") + .remove_all() + .expect("Unable to clear index"); + + let query = Ontology::index_query(&*language::ENGLISH_OR_UNTAGGED); + let query_results = self.ontology.execute_query(query).expect("Unable to generate index of entities"); + let results = Ontology::transform_index_results(query_results); + task = Task::done(Message::IndexQueryResults(results)); + } Message::Traverse => { + self.traversal = Some((self.ontology.to_dataset(), debug_span!("Repository Traversal").entered())); + let client = self.http_client.clone(); let root = Url::parse(&self.url_input).expect("Invalid URL"); let stream = Traverse::new(client, root, None); - self.index_writer = Some(self.index.writer().expect("Unable to create search index writer")); - task = Task::run(stream, |result| match result { - Ok(rdf_source) => Message::IndexRdfSource(rdf_source), + Ok(rdf_source) => Message::AddRdfSource(rdf_source), Err(err) => Message::ShowError(format!("Unable to fetch RDF Source: {err}")), - }) - .chain(Task::done(Message::CommitIndex)); + }).chain(Task::done(Message::ConcludeTraversal)); } - Message::IndexRdfSource(rdf_source) => { - for catalog_id in rdf_source.classes() - .filter_map(|class| self.ontology.catalog_id(&class.into_owned())) { - let mut document = SearchDocument::new(); - document.add_u64(Schema::type_field(), catalog_id); - document.add_text(Schema::iri_field(), rdf_source.origin()); + Message::AddRdfSource(rdf_source) => { + if let Some((traversal, _)) = &mut self.traversal { + traversal.extend(rdf_source.dataset()); + } + } + Message::ConcludeTraversal => { + if let Some((traversal, _)) = &self.traversal { + let query = Ontology::index_query(&*language::ENGLISH_OR_UNTAGGED); + let query_results = query.on_queryable_dataset(traversal) + .execute() + .expect("Unable to generate index of entities"); + let results = Ontology::transform_index_results(query_results); + task = Task::done(Message::IndexQueryResults(results)); + } + self.traversal = None; + } + Message::IndexQueryResults(results) => { + let mut writer = self.index.writer().expect("Failed to build index writer"); + let curie_helper = self.curie_helper.clone(); + let index_task = tokio::task::spawn_blocking(move || { + debug_span!("Indexing").in_scope(|| { + let mut counter = 0; + for (individual, entry) in results { + debug!(%individual, ?entry); + let mut document = doc!( + Schema::type_field() => entry.catalog_id, + Schema::iri_field() => individual.as_str(), + ); - /*for quad in rdf_source.dataset() { - if let Some(field) = - self.ontology.field_for_property(&quad.predicate.into_owned()) - && let TermRef::Literal(literal) = quad.object - { - let field = Schema::schema() - .get_field(field.name.as_str()) - .expect("Field not found in schema"); - document.add_text(field, literal.value()); + if let Some(curie) = curie_helper.abbreviate(individual.as_str()) { + document.add_text(Schema::curie_field(), curie); + } + + for (key, value) in entry.fields { + document.add_text(Schema::field(&key, language::ENGLISH_PRIMARY), value.as_str()); + } + writer.add(document).expect("Failed to add document to search index"); + counter += 1; } - }*/ + writer.commit().expect("Failed to commit changes to search index"); + debug!("Added {counter} documents to search index"); + }); + }); - if let Some(writer) = &self.index_writer { - writer.add(document).expect("Unable to add document to search index"); + task = Task::future(async { + match index_task.await { + Ok(()) => Message::None, + Err(err) => Message::ShowError(err.to_string()), } - } - } - Message::CommitIndex => { - if let Some(writer) = &mut self.index_writer { - writer.commit().expect("Unable to commit updates to search index"); - self.index_writer = None; - } + }); } Message::WindowClosed(id) => { if self.window_id == id { @@ -297,7 +318,9 @@ impl Publisher { let term = TermHelper::new(&quad.object); let datatype = term .datatype() - .map(|datatype| self.ontology.abbreviate(datatype)); + .map(|datatype| self.curie_helper.abbreviate(datatype.as_str()) + .unwrap_or(datatype.as_str().to_string())); + let datatype_state = combo_box::State::with_selection( self.abbreviated_datatypes.clone(), datatype.as_ref(), @@ -417,12 +440,13 @@ impl Publisher { } Message::DatatypeUpdated(key, Some(maybe_prefixed_iri)) => { let node = self - .ontology + .curie_helper .expand(&maybe_prefixed_iri) - .unwrap_or(NamedNode::new_unchecked(&maybe_prefixed_iri)); + .unwrap_or(maybe_prefixed_iri); + if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { let mut term = TermHelperMut::new(&mut quad.object); - term.set_datatype(Some(node)); + term.set_datatype(Some(NamedNode::new_unchecked(node))); self.modified = true; } } @@ -535,14 +559,14 @@ impl Publisher { Message::NavigateToPredicate(key) => { if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { self.url_input = quad.predicate.as_str().to_string(); - task = Task::done(URLInputSubmitted); + task = Task::done(Message::URLInputSubmitted); } } Message::NavigateToObject(key) => { if let Some(quad) = self.document.dataset_mut().quads.get_mut(key) { if let Term::NamedNode(node) = &quad.object { self.url_input = node.as_str().to_string(); - task = Task::done(URLInputSubmitted); + task = Task::done(Message::URLInputSubmitted); } } } @@ -578,7 +602,7 @@ impl Publisher { container(space()).width(BUTTON_WIDTH) }; - let predicate_input_base = iri_input(self.ontology.prefixes(), "Predicate", triple.predicate.as_str()); + let predicate_input_base = iri_input(&self.curie_helper, "Predicate", triple.predicate.as_str()); let predicate_input = if self.ontology.is_read_only(triple.as_ref()) { predicate_input_base } else { @@ -587,6 +611,9 @@ impl Publisher { .on_shift_click(Message::NavigateToPredicate(key)) }; + 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| { @@ -607,7 +634,7 @@ impl Publisher { }) .unwrap_or(Horizontal::Left); - let object_input_base = iri_input(self.ontology.prefixes(), "Object", value.as_str()); + let object_input_base = iri_input(&self.curie_helper, "Object", value.as_str()); let object_input = if self.ontology.is_read_only(triple.as_ref()) { object_input_base } else { @@ -617,7 +644,9 @@ impl Publisher { .on_shift_click(Message::NavigateToObject(key)) }; - let selected_datatype = term.datatype().map(|node| self.ontology.abbreviate(node)); + let selected_datatype = term.datatype() + .and_then(|node| self.curie_helper.abbreviate(node.as_str())); + let datatype_selector: Element = if state.read_only { selected_datatype.map(text).into() } else { @@ -667,8 +696,9 @@ impl Publisher { let row = row![ button_area, predicate_input, - value_label, + predicate_label, object_input, + value_label, datatype_selector, language_input, direction_slider, @@ -685,7 +715,10 @@ impl Publisher { entities: impl IntoIterator, ) -> Element<'_, Message> { let buttons = entities.into_iter().map(|entity| { - let label = format!("{} ({})", entity.label, self.ontology.abbreviate(entity.iri.as_ref())); + let abbreviation = self.curie_helper.abbreviate(entity.iri.as_str()) + .unwrap_or_else(|| entity.iri.as_str().to_string()); + + let label = format!("{} ({})", entity.label, abbreviation); button(text(label)) .on_press(Message::NewDocument(entity.iri)) .into() @@ -717,7 +750,9 @@ impl Publisher { .and_then(|value| value.as_str()) .unwrap_or_default(); - let abbreviated_iri = self.ontology.abbreviate(NamedNodeRef::new_unchecked(iri)); + let abbreviated_iri = self.curie_helper.abbreviate(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) @@ -764,9 +799,11 @@ impl Publisher { let add_row_button = button("Add row").on_press(Message::AddRow(None)); - let index_button = button("Index").on_press(Message::Traverse); + let reindex_ontology_button = button("Rebuild index").on_press(Message::RebuildIndex); - let address_input = iri_input(self.ontology.prefixes(), "URL", &self.url_input) + let traverse_button = button("Traverse").on_press(Message::Traverse); + + let address_input = iri_input(&self.curie_helper, "URL", &self.url_input) .on_input(Message::URLInputChanged) .on_submit(Message::URLInputSubmitted) .on_control_click(Message::OpenQueryWindow(SearchResultClickAction::URLInput)); @@ -797,7 +834,8 @@ impl Publisher { let content = column![ row![ add_row_button, - index_button, + reindex_ontology_button, + traverse_button, address_input, save_button, ], diff --git a/publish/src/main.rs b/publish/src/main.rs index 8c125d0..e444e6d 100644 --- a/publish/src/main.rs +++ b/publish/src/main.rs @@ -18,10 +18,10 @@ fn main() -> color_eyre::Result<()> { .init(); color_eyre::install()?; - let application = iced::daemon(Publisher::new, Publisher::update, Publisher::view) + iced::daemon(Publisher::new, Publisher::update, Publisher::view) .title(Publisher::title) - .subscription(Publisher::subscription); + .subscription(Publisher::subscription) + .run()?; - application.run()?; Ok(()) } diff --git a/publish/src/rdf/curie.rs b/publish/src/rdf/curie.rs new file mode 100644 index 0000000..aa53900 --- /dev/null +++ b/publish/src/rdf/curie.rs @@ -0,0 +1,30 @@ +use std::collections::BTreeMap; + +#[derive(Clone)] +pub struct CurieHelper { + prefixes: BTreeMap, +} + +impl CurieHelper { + pub fn new(prefixes: BTreeMap) -> Self { + Self { + prefixes, + } + } + + pub fn abbreviate(&self, iri: &str) -> Option { + for (name, base) in &self.prefixes { + if let Some(local_name) = iri.strip_prefix(base) { + return Some(format!("{name}:{local_name}")); + } + } + None + } + + pub fn expand(&self, abbreviated_iri: &str) -> Option { + let (prefix, name) = abbreviated_iri.split_once(':')?; + self.prefixes + .get(prefix) + .map(|base| format!("{base}{name}")) + } +} \ No newline at end of file diff --git a/publish/src/rdf/mod.rs b/publish/src/rdf/mod.rs index c97d4c4..36bb896 100644 --- a/publish/src/rdf/mod.rs +++ b/publish/src/rdf/mod.rs @@ -3,4 +3,5 @@ pub(crate) mod term_helper; pub mod vocab; pub(crate) mod materialize; pub(crate) mod conversion; -pub(crate) mod language; \ No newline at end of file +pub(crate) mod language; +pub(crate) mod curie; \ No newline at end of file diff --git a/publish/src/rdf/ontology.rs b/publish/src/rdf/ontology.rs index db175e1..9b13e05 100644 --- a/publish/src/rdf/ontology.rs +++ b/publish/src/rdf/ontology.rs @@ -1,18 +1,20 @@ use crate::error; use crate::rdf::vocab::gl; use oxigraph::model::vocab::{rdf, rdfs, xsd}; -use oxigraph::model::{Dataset, Graph, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef}; -use oxigraph::sparql::{QueryResults, SparqlEvaluator}; -use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use oxigraph::model::{Dataset, Graph, GraphName, GraphNameRef, LiteralRef, NamedNode, NamedNodeRef, NamedOrBlankNode, NamedOrBlankNodeRef, Term, TermRef, Triple, TripleRef}; +use oxigraph::sparql::{PreparedSparqlQuery, QueryResults, SparqlEvaluator}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::fmt::Display; use std::path::{Path, PathBuf}; use std::sync::LazyLock; use oxigraph::store::Store; -use tracing::{debug_span, info}; +use tracing::debug_span; use crate::rdf::{conversion, materialize}; -use crate::rdf::conversion::{quad_into_term, term_into_named_node, term_into_string, term_to_named_node}; use crate::rdf::language::LanguageCondition; +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")); + static PREFIXES: LazyLock> = LazyLock::new(|| { BTreeMap::from_iter([ ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"), @@ -44,12 +46,13 @@ static PREFIXES: LazyLock> = LazyLock::new(|| { ("rdax", "http://rdaregistry.info/Elements/x/"), ("schema", "https://schema.org/"), ("quill", "http://fedora.quill.lan/rest/"), - ("gl", "https://graphofliberty.org/2026/04/ont/"), + ("gl", "ONTOLOGY_PREFIX"), ].map(|(k, v)| (k.to_string(), v.to_string()))) }); pub struct OntologyBuilder { path: Option, + materialize_inferences: bool, } impl OntologyBuilder { @@ -59,9 +62,18 @@ impl OntologyBuilder { self } + pub fn materialize_inferences(mut self) -> Self { + self.materialize_inferences = true; + self + } + pub fn build(self) -> error::Result { let mut store = if let Some(path) = self.path { - Store::open_read_only(path) + if self.materialize_inferences { + Store::open(path) + } else { + Store::open_read_only(path) + } } else { Store::new() }?; @@ -71,10 +83,12 @@ impl OntologyBuilder { .map(|(k, v)| (k.to_string(), v.to_string())) .collect::>(); - /*materialize::same_as(&mut store)?; - materialize::super_properties(&mut store)?; - materialize::super_classes(&mut store)?; - debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;*/ + if self.materialize_inferences { + materialize::same_as(&mut store)?; + materialize::super_properties(&mut store)?; + materialize::super_classes(&mut store)?; + debug_span!("Optimize Ontology").in_scope(|| store.optimize())?; + } let mut indexed_by = HashMap::new(); for quad in store.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None) @@ -92,6 +106,7 @@ impl OntologyBuilder { } } +#[derive(Clone, Debug)] pub struct IndexEntry { pub catalog_id: u64, pub fields: HashMap, @@ -142,14 +157,22 @@ impl Ontology { pub fn builder() -> OntologyBuilder { OntologyBuilder { path: None, + materialize_inferences: false, } } - pub fn prefixes(&self) -> &BTreeMap { + pub fn prefixes() -> &'static BTreeMap { &*PREFIXES } - pub fn index(&self, language: &LanguageCondition) -> error::Result> { + pub fn to_dataset(&self) -> Dataset { + self.store + .quads_for_pattern(None, None, None, Some(ONTOLOGY_GRAPH_NAME)) + .filter_map(Result::ok) + .collect::() + } + + pub fn index_query(language: &LanguageCondition) -> PreparedSparqlQuery { let language_filter = language.to_filter_expression("fieldValue"); let query = format!(r#"SELECT ?individual ?catalogId ?fieldName ?fieldValue WHERE {{ @@ -165,12 +188,20 @@ WHERE {{ {language_filter} }}"#); let mut sparql = SparqlEvaluator::new() - .with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")? - .parse_query(&query)?; + .with_prefix("gl", ONTOLOGY_PREFIX).unwrap() + .parse_query(&query) + .unwrap(); sparql.dataset_mut().set_default_graph_as_union(); + sparql + } + pub fn execute_query(&self, query: PreparedSparqlQuery) -> error::Result> { + Ok(query.on_store(&self.store).execute()?) + } + + pub fn transform_index_results(query_results: QueryResults) -> HashMap { let mut results = HashMap::new(); - if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? { + if let QueryResults::Solutions(solutions) = query_results { for solution in solutions.filter_map(Result::ok) { let individual = solution.get("individual").and_then(conversion::term_to_named_node); let catalog_id = solution.get("catalogId").and_then(conversion::term_to_u64); @@ -187,8 +218,7 @@ WHERE {{ } } } - - Ok(results) + results } pub fn catalog_id(&self, class: &NamedNode) -> Option { @@ -211,7 +241,7 @@ WHERE {{ }}"#); let mut sparql = SparqlEvaluator::new() .with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")? - .with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")? + .with_prefix("gl", ONTOLOGY_PREFIX)? .parse_query(&query)?; sparql.dataset_mut().set_default_graph_as_union(); @@ -232,32 +262,12 @@ WHERE {{ Ok(results) } - pub fn abbreviate(&self, node: NamedNodeRef<'_>) -> String { - for (prefix_name, prefix_iri) in &self.prefixes { - if let Some(local_name) = node.as_str().strip_prefix(prefix_iri) { - return if local_name.is_empty() { - format!("{prefix_name}:") - } else { - format!("{prefix_name}:{local_name}") - }; - } - } - node.as_str().to_string() - } - - pub fn expand(&self, prefixed_iri: &str) -> Option { - let (prefix, name) = prefixed_iri.split_once(':')?; - self.prefixes - .get(prefix) - .map(|base| NamedNode::new_unchecked(format!("{base}{name}"))) - } - pub fn info(&self, iri: &NamedNode, language: &LanguageCondition) -> LabeledIri { let label = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::LABEL), None, None) .filter_map(Result::ok) - .map(quad_into_term) + .map(conversion::quad_into_term) .filter(|term| language.primary_matches_term(term)) - .filter_map(term_into_string) + .filter_map(conversion::term_into_string) .next() .unwrap_or_default(); @@ -273,9 +283,9 @@ WHERE {{ .filter_map(|quad| { let label = self.store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::LABEL), None, None) .filter_map(Result::ok) - .map(quad_into_term) + .map(conversion::quad_into_term) .filter(|term| language.primary_matches_term(term)) - .filter_map(term_into_string) + .filter_map(conversion::term_into_string) .next(); if let Some(label) = label && let NamedOrBlankNode::NamedNode(subject) = quad.subject { Some(LabeledIri { diff --git a/publish/src/widget/iri_input.rs b/publish/src/widget/iri_input.rs index 6c83e69..d3ae003 100644 --- a/publish/src/widget/iri_input.rs +++ b/publish/src/widget/iri_input.rs @@ -7,6 +7,7 @@ use iced::advanced::{Layout, Widget}; use iced::advanced::widget::{tree, Tree}; use iced::mouse::{Cursor, Interaction}; use iced::widget::text_input::Catalog; +use crate::rdf::curie::CurieHelper; pub struct State { control: bool, @@ -18,39 +19,23 @@ where Theme: Catalog, Renderer: text::Renderer, { - prefixes: &'a BTreeMap, + curie_helper: &'a CurieHelper, on_control_click: Option, on_shift_click: Option, text_input: widget::TextInput<'a, Message, Theme, Renderer>, } -fn abbreviate(prefixes: &BTreeMap, iri: &str) -> Option { - for (name, base) in prefixes { - if let Some(local_name) = iri.strip_prefix(base) { - return Some(format!("{name}:{local_name}")); - } - } - None -} - -fn expand(prefixes: &BTreeMap, abbreviated_iri: &str) -> Option { - let (prefix, name) = abbreviated_iri.split_once(':')?; - prefixes - .get(prefix) - .map(|base| format!("{base}{name}")) -} - impl<'a, Message, Theme, Renderer> IriInput<'a, Message, Theme, Renderer> where Message: Clone, Theme: Catalog, Renderer: text::Renderer, { - pub fn new(prefixes: &'a BTreeMap, placeholder: &str, iri: &str) -> Self { - let display_value = abbreviate(prefixes, iri).unwrap_or_else(|| iri.to_string()); + pub fn new(curie_helper: &'a CurieHelper, placeholder: &str, iri: &str) -> Self { + let display_value = curie_helper.abbreviate(iri).unwrap_or_else(|| iri.to_string()); let text_input = widget::TextInput::new(placeholder, &display_value); Self { - prefixes, + curie_helper, on_control_click: None, on_shift_click: None, text_input, @@ -74,7 +59,7 @@ where pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self { let wrapped = move |value: String| { - let expanded_value = expand(self.prefixes, &value); + let expanded_value = self.curie_helper.expand(&value); on_input(expanded_value.unwrap_or_else(|| value)) }; @@ -100,13 +85,13 @@ where } } -pub fn iri_input<'a, Message, Theme, Renderer>(prefixes: &'a BTreeMap, placeholder: &str, iri: &str) -> IriInput<'a, Message, Theme, Renderer> +pub fn iri_input<'a, Message, Theme, Renderer>(curie_helper: &'a CurieHelper, placeholder: &str, iri: &str) -> IriInput<'a, Message, Theme, Renderer> where Message: Clone, Theme: Catalog, Renderer: text::Renderer, { - IriInput::new(prefixes, placeholder, iri) + IriInput::new(curie_helper, placeholder, iri) } impl Widget for IriInput<'_, Message, Theme, Renderer> diff --git a/publish/src/windows/mod.rs b/publish/src/windows/mod.rs index b167737..473a0f4 100644 --- a/publish/src/windows/mod.rs +++ b/publish/src/windows/mod.rs @@ -1,3 +0,0 @@ -//mod search; - -//pub use search::{SearchWindow, SearchWindowMessage}; diff --git a/publish/src/windows/search.rs b/publish/src/windows/search.rs index 70c74a6..473a0f4 100644 --- a/publish/src/windows/search.rs +++ b/publish/src/windows/search.rs @@ -1,64 +0,0 @@ -use iced::{window, Element, Task}; -use iced::widget::{column, mouse_area, space, table, text_input}; -use tracing::info; -use gl_search::{SearchIndex, SearchIndexBuilder}; -use gl_types::CatalogEntryType; - -#[derive(Clone)] -pub enum SearchWindowMessage { - QueryInputUpdated(String), - SearchResultSelected(String), -} - -pub struct SearchWindow { - window_id: window::Id, - index: SearchIndex, - query_input: String, - results: Vec, -} - -impl SearchWindow { - pub fn new(window_id: window::Id) -> Self { - let index = SearchIndex::builder() - .with_path("/tmp/name_index") - .build().expect("Unable to load search index"); - - Self { - window_id, - index, - query_input: String::new(), - results: vec![], - } - } - - pub fn id(&self) -> window::Id { - self.window_id - } - - pub fn title(&self) -> String { - "Graph of Liberty Publisher: Search".to_string() - } - - pub fn update(&mut self, message: SearchWindowMessage) -> Task { - match message { - SearchWindowMessage::QueryInputUpdated(value) => { - self.results = self.index.query(CatalogEntryType::Person, value.as_str()).unwrap(); - self.query_input = value; - } - SearchWindowMessage::SearchResultSelected(value) => { - info!(value); - } - } - Task::none() - } - - pub fn view(&self) -> Element<'_, SearchWindowMessage> { - let name_column = table::column("Name", |name: &String| { - mouse_area(name.as_str()).on_double_click(SearchWindowMessage::SearchResultSelected(name.clone())) - }); - column![ - text_input("Query", &self.query_input).on_input(SearchWindowMessage::QueryInputUpdated), - table(vec![name_column], &self.results), - ].into() - } -} \ No newline at end of file diff --git a/search/src/index.rs b/search/src/index.rs index 3c079be..54a390a 100644 --- a/search/src/index.rs +++ b/search/src/index.rs @@ -8,7 +8,7 @@ use tantivy::query::{BooleanQuery, Occur, QueryParser, TermQuery}; use tantivy::schema::{Field, IndexRecordOption}; use tantivy::tokenizer::{LowerCaser, NgramTokenizer, TextAnalyzer, TokenizerManager}; use tantivy::{Index, IndexReader, ReloadPolicy, TantivyDocument, Term}; -use tracing::{debug, span, Level}; +use tracing::debug_span; #[derive(Default)] pub struct SearchIndexBuilder { @@ -78,8 +78,7 @@ impl SearchIndex { default_fields: Vec, limit: usize, ) -> error::Result> { - let span = span!(Level::DEBUG, "Search Query"); - let _enter = span.enter(); + let _ = debug_span!("Search Query").entered(); let parser = QueryParser::for_index(&self.index, default_fields); let (user_query, _) = parser.parse_query_lenient(user_query); diff --git a/search/src/schema.rs b/search/src/schema.rs index f7f8c61..4f99e42 100644 --- a/search/src/schema.rs +++ b/search/src/schema.rs @@ -31,9 +31,9 @@ impl Schema { schema_builder.add_text_field("label:en", stored_ngram32.clone()); schema_builder.add_text_field("definition:en", stored_en_stem.clone()); - /*schema_builder.add_text_field("title", en_stem.clone()); - schema_builder.add_text_field("description", en_stem.clone()); - schema_builder.add_text_field("content", en_stem);*/ + schema_builder.add_text_field("surname:en", stored_ngram32.clone()); + schema_builder.add_text_field("given_name:en", stored_ngram32.clone()); + schema_builder.build() }) } diff --git a/search/src/update.rs b/search/src/update.rs index 1ea2c2b..0fc7ba0 100644 --- a/search/src/update.rs +++ b/search/src/update.rs @@ -16,6 +16,11 @@ impl IndexWriter { self.inner.commit()?; Ok(()) } + + pub fn remove_all(&mut self) -> crate::Result<()> { + self.inner.delete_all_documents()?; + Ok(()) + } pub fn commit(&mut self) -> crate::Result<()> { self.inner.commit()?;