This commit is contained in:
Alex Wied
2026-06-29 23:10:22 -04:00
parent 59c8dffb01
commit 5cea8e8307
7 changed files with 129 additions and 115 deletions
+1 -1
View File
@@ -27,4 +27,4 @@ thiserror = "2.0"
tokio = { version = "1.52", features = ["rt", "rt-multi-thread", "macros", "fs"] } tokio = { version = "1.52", features = ["rt", "rt-multi-thread", "macros", "fs"] }
tracing = "0.1" tracing = "0.1"
tracing-appender = "0.2" tracing-appender = "0.2"
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
+63 -68
View File
@@ -1,7 +1,7 @@
use crate::rdf::ontology::{LabeledIri, Ontology}; use crate::rdf::ontology::{LabeledIri, Ontology};
use crate::rdf::term_helper::{TermHelper, TermHelperMut}; use crate::rdf::term_helper::{TermHelper, TermHelperMut};
use crate::rdf::vocab::{gl, rda}; use crate::rdf::vocab::{gl, rda};
use gl_search::{Schema, SearchDocument, SearchIndex, NamedFieldDocument, OwnedValue, doc}; use gl_search::{Schema, SearchDocument, SearchIndex, NamedFieldDocument, OwnedValue, doc, IndexWriter};
use http::StatusCode; use http::StatusCode;
use iced::alignment::Horizontal; use iced::alignment::Horizontal;
use iced::widget::button::Style; use iced::widget::button::Style;
@@ -43,6 +43,7 @@ pub(crate) enum Message {
HoverRow(QuadKey), HoverRow(QuadKey),
UnhoverRow(QuadKey), UnhoverRow(QuadKey),
QueryUpdated(String), QueryUpdated(String),
SetSearchResults(Vec<NamedFieldDocument>),
QueryTypeUpdated(NamedNode), QueryTypeUpdated(NamedNode),
SearchResultClicked(NamedNode), SearchResultClicked(NamedNode),
DatatypeUpdated(QuadKey, Option<String>), DatatypeUpdated(QuadKey, Option<String>),
@@ -88,6 +89,7 @@ pub(crate) struct Publisher {
hovered_row: Option<QuadKey>, hovered_row: Option<QuadKey>,
search_state: Option<SearchState>, search_state: Option<SearchState>,
index: SearchIndex, index: SearchIndex,
index_writer: Option<IndexWriter>,
show_overwrite_confirmation: bool, show_overwrite_confirmation: bool,
modified: bool, modified: bool,
show_new_document_buttons: bool, show_new_document_buttons: bool,
@@ -108,26 +110,39 @@ impl Publisher {
.build() .build()
.expect("Failed to build search index"); .expect("Failed to build search index");
if let Some(id) = ontology.catalog_id(&rdf::PROPERTY.into_owned()) { index.remove_all_of_type(id); } let writer = index.writer().expect("Failed to create search index writer");
if let Some(id) = ontology.catalog_id(&rdfs::CLASS.into_owned()) { index.remove_all_of_type(id); }
for (iri, entity) in ontology.entities() { let classes_to_index = [
let mut document = doc!( rdf::PROPERTY,
Schema::type_field() => entity.catalog_id, rdfs::CLASS,
Schema::iri_field() => iri.as_str(), ];
);
if let Some(field) = ontology.field_for_property(&rdfs::LABEL.into_owned()) { for class in &classes_to_index {
document.add_text(Schema::field(&field.name), &entity.label); let class = class.into_owned();
if let Some(id) = ontology.catalog_id(&class) {
index.remove_all_of_type(id).expect("Failed to remove documents from search index");
for individual in ontology.individuals(&class) {
let info = ontology.info(&individual);
if info.label.is_some() || info.comment.is_some() {
let mut document = doc!(
Schema::type_field() => id,
Schema::iri_field() => individual.as_str(),
);
if let Some(field) = ontology.field_for_property(&rdfs::LABEL.into_owned()) {
document.add_text(Schema::field(&field.name), info.label.unwrap_or_default());
}
if let Some(field) = ontology.field_for_property(&rdfs::COMMENT.into_owned()) {
document.add_text(Schema::field(&field.name), info.comment.unwrap_or_default());
}
writer.add_document(document).expect("Failed to add document to search index");
}
}
} }
if let Some(field) = ontology.field_for_property(&rdfs::COMMENT.into_owned()) {
document.add_text(Schema::field(&field.name), entity.comment.clone().unwrap_or(String::from("")));
}
index.add(document).expect("Unable to add annotated IRI to search index");
} }
index.commit().expect("Unable to commit ontology to index");
index index
}); });
@@ -161,6 +176,7 @@ impl Publisher {
hovered_row: None, hovered_row: None,
search_state: None, search_state: None,
index, index,
index_writer: None,
show_overwrite_confirmation: false, show_overwrite_confirmation: false,
modified: false, modified: false,
show_new_document_buttons: false, show_new_document_buttons: false,
@@ -178,6 +194,9 @@ impl Publisher {
let client = self.http_client.clone(); let client = self.http_client.clone();
let root = Url::parse(&self.url_input).expect("Invalid URL"); let root = Url::parse(&self.url_input).expect("Invalid URL");
let stream = Traverse::new(client, root, None); 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 { task = Task::run(stream, |result| match result {
Ok(rdf_source) => Message::IndexRdfSource(rdf_source), Ok(rdf_source) => Message::IndexRdfSource(rdf_source),
Err(err) => Message::ShowError(format!("Unable to fetch RDF Source: {err}")), Err(err) => Message::ShowError(format!("Unable to fetch RDF Source: {err}")),
@@ -203,14 +222,14 @@ impl Publisher {
} }
} }
self.index if let Some(writer) = &self.index_writer {
.add(document) writer.add_document(document).expect("Unable to add document to search index");
.expect("Unable to add document to search index"); }
} }
} }
Message::CommitIndex => { Message::CommitIndex => {
if let Err(err) = self.index.commit() { if let Some(writer) = &mut self.index_writer {
task = Task::done(Message::ShowError(format!("Unable to commit index: {err}"))); writer.commit().expect("Unable to commit updates to search index");
} }
} }
Message::WindowClosed(id) => { Message::WindowClosed(id) => {
@@ -349,13 +368,22 @@ impl Publisher {
SearchResultClickAction::URLInput => self.ontology.catalog_id(&search_state.type_.iri), SearchResultClickAction::URLInput => self.ontology.catalog_id(&search_state.type_.iri),
}; };
search_state.results = self search_state.query = new_query.clone();
.index let index = self.index.clone();
.query(catalog_id, new_query.as_str(), Schema::all_fields()) task = Task::future(async move {
.expect("Error encountered while querying index"); tokio::task::spawn_blocking(move || {
search_state.query = new_query; let result = index.query(catalog_id, new_query.as_str(), Schema::all_fields())
.expect("Error encountered while querying index");
Message::SetSearchResults(result)
}).await.unwrap()
});
}; };
} }
Message::SetSearchResults(results) => {
if let Some(search_state) = &mut self.search_state {
search_state.results = results;
}
}
Message::SearchResultClicked(node) => { Message::SearchResultClicked(node) => {
if let Some(search_state) = &self.search_state { if let Some(search_state) = &self.search_state {
match search_state.action { match search_state.action {
@@ -542,12 +570,12 @@ impl Publisher {
let term = TermHelper::new(&triple.object); let term = TermHelper::new(&triple.object);
let value_label = "bar"; /*term.value_as_named_node().and_then(|node| { let value_label = term.value_as_named_node().and_then(|node| {
self.ontology self.ontology
.info(node) .info(&node.into_owned())
.and_then(|info| info.label.clone()) .label
.map(|label| container(text(label))) .map(|label| container(text(label)))
});*/ });
let value = term let value = term
.value_as_named_node() .value_as_named_node()
@@ -563,43 +591,10 @@ impl Publisher {
}) })
.unwrap_or(Horizontal::Left); .unwrap_or(Horizontal::Left);
/*let value_input_base = text_input("Value", value.as_str())
.align_x(value_alignment);*/
/*let value_input = if !state.read_only {
let is_named_node = term.is_named_node();
value_input_base.on_input(move |input| {
let expanded_input = if is_named_node {
self.ontology
.expand(input.as_str())
.map(|node| node.as_str().to_string())
.unwrap_or(input)
} else {
input
};
Message::ValueUpdated(key, expanded_input)
})
} else {
value_input_base
};*/
let foo = iri_input(self.ontology.prefixes(), "Object", value.as_str()) let foo = iri_input(self.ontology.prefixes(), "Object", value.as_str())
.on_input(move |value| Message::ValueUpdated(key, value)) .on_input(move |value| Message::ValueUpdated(key, value))
.on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key))); .on_control_click(Message::OpenQueryWindow(SearchResultClickAction::Object(key)));
/*let search_launcher = if term.is_named_node() && !state.read_only {
Some(container(
button(text("\u{1f50e}"))
.on_press(Message::OpenQueryWindow(SearchResultClickAction::Object(
key,
)))
.style(button::text),
))
} else {
None
};*/
let selected_datatype = term.datatype().map(|node| self.ontology.abbreviate(node)); let selected_datatype = term.datatype().map(|node| self.ontology.abbreviate(node));
let datatype_selector: Element<Message> = if state.read_only { let datatype_selector: Element<Message> = if state.read_only {
selected_datatype.map(text).into() selected_datatype.map(text).into()
@@ -671,11 +666,11 @@ impl Publisher {
entities: impl Iterator<Item = NamedNode>, entities: impl Iterator<Item = NamedNode>,
) -> Element<'_, Message> { ) -> Element<'_, Message> {
let buttons = entities.map(|entity| { let buttons = entities.map(|entity| {
let label = "baz"; /*self let label = self
.ontology .ontology
.info(entity.as_ref()) .info(&entity)
.and_then(|info| info.label.clone()) .label
.unwrap_or(entity.as_str().to_string());*/ .unwrap_or(entity.as_str().to_string());
button(text(label)) button(text(label))
.on_press(Message::NewDocument(entity)) .on_press(Message::NewDocument(entity))
.into() .into()
+14 -4
View File
@@ -214,18 +214,28 @@ fedora:lastModifiedBy rdf:type owl:NamedIndividual ;
### http://www.w3.org/1999/02/22-rdf-syntax-ns#Property ### http://www.w3.org/1999/02/22-rdf-syntax-ns#Property
rdf:Property rdf:type owl:NamedIndividual ; rdf:Property rdf:type owl:NamedIndividual ;
:associatedProperty :comment , :associatedProperty rdfs:comment ,
:label ; rdfs:label ;
:catalogId "0"^^xsd:nonNegativeInteger . :catalogId "0"^^xsd:nonNegativeInteger .
### http://www.w3.org/2000/01/rdf-schema#Class ### http://www.w3.org/2000/01/rdf-schema#Class
rdfs:Class rdf:type owl:NamedIndividual ; rdfs:Class rdf:type owl:NamedIndividual ;
:associatedProperty :comment , :associatedProperty rdfs:comment ,
:label ; rdfs:label ;
:catalogId "1"^^xsd:nonNegativeInteger . :catalogId "1"^^xsd:nonNegativeInteger .
### http://www.w3.org/2000/01/rdf-schema#comment
rdfs:comment rdf:type owl:NamedIndividual ;
:indexedByField :comment .
### http://www.w3.org/2000/01/rdf-schema#label
rdfs:label rdf:type owl:NamedIndividual ;
:indexedByField :label .
### http://www.w3.org/ns/ldp#BasicContainer ### http://www.w3.org/ns/ldp#BasicContainer
ldp:BasicContainer rdf:type owl:NamedIndividual ; ldp:BasicContainer rdf:type owl:NamedIndividual ;
:readOnly "true"^^xsd:boolean . :readOnly "true"^^xsd:boolean .
+38 -25
View File
@@ -135,10 +135,12 @@ impl OntologyBuilder {
PREFIX gl: <https://graphofliberty.org/2026/04/ont/> PREFIX gl: <https://graphofliberty.org/2026/04/ont/>
SELECT DISTINCT ?subject ?name ?label { SELECT DISTINCT ?subject ?name ?label {
?subject a gl:IndexField ; GRAPH ?graph {
gl:fieldName ?name ; ?subject a gl:IndexDocumentField ;
gl:fieldLabel ?label . gl:fieldName ?name ;
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label)) gl:fieldLabel ?label .
FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label))
}
}"#, }"#,
) )
.expect("Unable to parse field query"); .expect("Unable to parse field query");
@@ -195,7 +197,7 @@ SELECT ?class {{
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct LabeledIri { pub struct LabeledIri {
pub iri: NamedNode, pub iri: NamedNode,
pub label: String, pub label: Option<String>,
pub comment: Option<String>, pub comment: Option<String>,
} }
@@ -207,18 +209,10 @@ impl PartialEq for LabeledIri {
impl Display for LabeledIri { impl Display for LabeledIri {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.label.clone()) write!(f, "{}", self.label.clone().unwrap_or_default())
} }
} }
#[derive(Clone, Debug)]
pub struct IriInformation {
pub type_: NamedNode,
pub label: Option<String>,
pub comment: Option<String>,
pub read_only: bool,
}
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct Entity { pub struct Entity {
pub label: String, pub label: String,
@@ -302,11 +296,39 @@ impl Ontology {
.map(|entity| entity.catalog_id) .map(|entity| entity.catalog_id)
} }
pub fn individuals(&self, class: &NamedNode) -> impl Iterator<Item = NamedNode> {
self.store
.quads_for_pattern(None, Some(rdf::TYPE), Some(class.into()), None)
.filter_map(Result::ok)
.filter_map(|quad|
if let NamedOrBlankNode::NamedNode(subject) = quad.subject {
Some(subject)
} else { None })
}
pub fn info(&self, iri: &NamedNode) -> LabeledIri {
let label = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::LABEL), None, None)
.filter_map(Result::ok)
.filter_map(|quad| conversion::term_to_string(&quad.object).map(String::from))
.next();
let comment = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::COMMENT), None, None)
.filter_map(Result::ok)
.filter_map(|quad| conversion::term_to_string(&quad.object).map(String::from))
.next();
LabeledIri {
iri: iri.clone(),
label,
comment,
}
}
pub fn labeled_entity(&self, class: &NamedNode) -> Option<LabeledIri> { pub fn labeled_entity(&self, class: &NamedNode) -> Option<LabeledIri> {
self.entities.get(class) self.entities.get(class)
.map(|entity| LabeledIri { .map(|entity| LabeledIri {
iri: class.to_owned(), iri: class.to_owned(),
label: entity.label.to_owned(), label: Some(entity.label.to_owned()),
comment: entity.comment.to_owned(), comment: entity.comment.to_owned(),
}) })
} }
@@ -315,15 +337,11 @@ impl Ontology {
self.entities.iter() self.entities.iter()
.map(|(iri, entity)| LabeledIri { .map(|(iri, entity)| LabeledIri {
iri: iri.to_owned(), iri: iri.to_owned(),
label: entity.label.to_owned(), label: Some(entity.label.to_owned()),
comment: entity.comment.to_owned(), comment: entity.comment.to_owned(),
}) })
} }
pub fn entities(&self) -> impl Iterator<Item = (&NamedNode, &Entity)> {
self.entities.iter()
}
pub fn datatypes(&self) -> impl Iterator<Item = NamedNode> { pub fn datatypes(&self) -> impl Iterator<Item = NamedNode> {
self.store self.store
.quads_for_pattern( .quads_for_pattern(
@@ -354,11 +372,6 @@ impl Ontology {
.count() >= 1 .count() >= 1
} }
/*pub fn for_each_annotated_(&self) -> impl Iterator<Item = TripleRef<'_>> {
self.store
.quads_for_pattern()
}*/
pub fn template_triples<'a>( pub fn template_triples<'a>(
&'a self, &'a self,
class: NamedNodeRef<'a>, class: NamedNodeRef<'a>,
-2
View File
@@ -19,7 +19,6 @@ where
{ {
prefixes: &'a BTreeMap<String, String>, prefixes: &'a BTreeMap<String, String>,
on_control_click: Option<Message>, on_control_click: Option<Message>,
on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
text_input: widget::TextInput<'a, Message, Theme, Renderer>, text_input: widget::TextInput<'a, Message, Theme, Renderer>,
} }
@@ -51,7 +50,6 @@ where
Self { Self {
prefixes, prefixes,
on_control_click: None, on_control_click: None,
on_input: None,
text_input, text_input,
} }
} }
+12 -15
View File
@@ -1,5 +1,4 @@
use crate::{error, SearchDocument}; use crate::{error, SearchDocument};
use crate::error::SearchError;
use crate::schema::Schema; use crate::schema::Schema;
use std::path::PathBuf; use std::path::PathBuf;
use tantivy::collector::TopDocs; use tantivy::collector::TopDocs;
@@ -8,6 +7,8 @@ use tantivy::query::{BooleanQuery, Occur, QueryParser, TermQuery};
use tantivy::schema::{Field, IndexRecordOption, NamedFieldDocument, Value}; use tantivy::schema::{Field, IndexRecordOption, NamedFieldDocument, Value};
use tantivy::tokenizer::{LowerCaser, NgramTokenizer, TextAnalyzer, TokenizerManager}; use tantivy::tokenizer::{LowerCaser, NgramTokenizer, TextAnalyzer, TokenizerManager};
use tantivy::{Document, Index, IndexReader, IndexWriter, ReloadPolicy, Score, TantivyDocument, Term}; use tantivy::{Document, Index, IndexReader, IndexWriter, ReloadPolicy, Score, TantivyDocument, Term};
use tantivy::indexer::UserOperation;
use tracing::{info, instrument, span, Level};
#[derive(Default)] #[derive(Default)]
pub struct SearchIndexBuilder { pub struct SearchIndexBuilder {
@@ -46,20 +47,17 @@ impl SearchIndexBuilder {
.reload_policy(ReloadPolicy::OnCommitWithDelay) .reload_policy(ReloadPolicy::OnCommitWithDelay)
.try_into()?; .try_into()?;
let writer = index.writer(50_000_000)?;
Ok(SearchIndex { Ok(SearchIndex {
index, index,
reader, reader,
writer,
}) })
} }
} }
#[derive(Clone)]
pub struct SearchIndex { pub struct SearchIndex {
index: Index, index: Index,
reader: IndexReader, reader: IndexReader,
writer: IndexWriter,
} }
impl SearchIndex { impl SearchIndex {
@@ -67,18 +65,14 @@ impl SearchIndex {
SearchIndexBuilder::default() SearchIndexBuilder::default()
} }
pub fn add<'a>(&self, document: TantivyDocument) -> crate::Result<()> { pub fn writer(&self) -> crate::Result<IndexWriter> {
self.writer.add_document(document)?; Ok(self.index.writer(128_000_000)?)
Ok(())
} }
pub fn remove_all_of_type(&mut self, type_: u64) { pub fn remove_all_of_type(&mut self, type_: u64) -> crate::Result<()> {
self.writer let mut writer: IndexWriter<TantivyDocument> = self.index.writer(64_000_000)?;
.delete_term(Term::from_field_u64(Schema::type_field(), type_)); writer.delete_term(Term::from_field_u64(Schema::type_field(), type_));
} writer.commit()?;
pub fn commit(&mut self) -> crate::Result<()> {
self.writer.commit()?;
Ok(()) Ok(())
} }
@@ -88,6 +82,9 @@ impl SearchIndex {
user_query: &str, user_query: &str,
default_fields: Vec<Field>, default_fields: Vec<Field>,
) -> error::Result<Vec<NamedFieldDocument>> { ) -> error::Result<Vec<NamedFieldDocument>> {
let span = span!(Level::INFO, "Search Query");
let _enter = span.enter();
let parser = QueryParser::for_index(&self.index, default_fields); let parser = QueryParser::for_index(&self.index, default_fields);
let (user_query, _) = parser.parse_query_lenient(user_query); let (user_query, _) = parser.parse_query_lenient(user_query);
+1
View File
@@ -5,6 +5,7 @@ mod schema;
pub use tantivy::TantivyDocument as SearchDocument; pub use tantivy::TantivyDocument as SearchDocument;
pub use tantivy::doc; pub use tantivy::doc;
pub use tantivy::schema::{NamedFieldDocument, OwnedValue}; pub use tantivy::schema::{NamedFieldDocument, OwnedValue};
pub use tantivy::IndexWriter;
pub use error::{Result, SearchError}; pub use error::{Result, SearchError};
pub use index::{SearchIndex, SearchIndexBuilder}; pub use index::{SearchIndex, SearchIndexBuilder};