diff --git a/ontology/graphofliberty.ttl b/ontology/graphofliberty.ttl index 18e557a..564df4a 100644 --- a/ontology/graphofliberty.ttl +++ b/ontology/graphofliberty.ttl @@ -135,6 +135,7 @@ ldp:contains rdf:type owl:DatatypeProperty ; ### https://graphofliberty.org/2026/04/ont/catalogId :catalogId rdf:type owl:DatatypeProperty ; + rdfs:domain :Entity ; rdfs:comment "An integer associated with the class for fast lookup in a database."@en ; rdfs:label "catalog id" . @@ -165,6 +166,7 @@ ldp:contains rdf:type owl:DatatypeProperty ; ### https://graphofliberty.org/2026/04/ont/Entity :Entity rdf:type owl:Class ; + rdfs:comment "An Entity is a first-class citizen of the Graph of Liberty catalog. It is the class of all cultural artifacts which are to be preserved and indexed."@en ; rdfs:label "Graph of Liberty Entity"@en . @@ -214,16 +216,20 @@ fedora:lastModifiedBy rdf:type owl:NamedIndividual ; ### http://www.w3.org/1999/02/22-rdf-syntax-ns#Property -rdf:Property rdf:type owl:NamedIndividual ; +rdf:Property rdf:type owl:NamedIndividual , + :Entity ; :associatedProperty rdfs:comment , - rdfs:label ; + rdfs:label , + skos:definition ; :catalogId "0"^^xsd:nonNegativeInteger . ### http://www.w3.org/2000/01/rdf-schema#Class -rdfs:Class rdf:type owl:NamedIndividual ; +rdfs:Class rdf:type owl:NamedIndividual , + :Entity ; :associatedProperty rdfs:comment , - rdfs:label ; + rdfs:label , + skos:definition ; :catalogId "1"^^xsd:nonNegativeInteger . diff --git a/publish/src/app.rs b/publish/src/app.rs index 3a3559e..0aef864 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -1,6 +1,6 @@ use crate::rdf::ontology::{LabeledIri, Ontology}; use crate::rdf::term_helper::{TermHelper, TermHelperMut}; -use gl_search::{Schema, SearchDocument, SearchIndex, doc, IndexWriter}; +use gl_search::{Schema, SearchDocument, SearchIndex, doc, IndexWriter, Value}; use http::StatusCode; use iced::alignment::Horizontal; use iced::widget::button::Style; @@ -8,6 +8,7 @@ use iced::widget::grid::Sizing; use iced::widget::{button, center, column, combo_box, container, grid, mouse_area, opaque, pick_list, row, scrollable, space, stack, table, text, text_input, toggler}; use iced::window::Settings; use iced::{Background, Color, Element, Length, Subscription, Task, color, window}; +use iced::widget::text::Wrapping; use ldp::middleware::BasicAuthMiddleware; use ldp::model::{KeyedDataset, QuadKey}; use ldp::reqwest::{Client, Url}; @@ -16,9 +17,9 @@ use ldp::traverse::Traverse; use ldp::{RdfSource, RdfSourceUpdateResponse, ResourceRequestBuilder, SerializationOptions}; use oxigraph::io::RdfFormat; use oxigraph::model::vocab::{rdf, rdfs}; -use oxigraph::model::{BaseDirection, Dataset, NamedNode, Quad, Term, TermRef}; -use tracing::{debug_span, error, trace}; -use crate::rdf::conversion; +use oxigraph::model::{BaseDirection, Dataset, NamedNode, NamedNodeRef, Quad, Term, TermRef}; +use tracing::{debug, debug_span, error, trace}; +use crate::rdf::language; use crate::widget::iri_input::iri_input; #[derive(Clone, Debug)] @@ -108,39 +109,23 @@ impl Publisher { .build() .expect("Failed to build search index"); - let classes_to_index = [ - rdf::PROPERTY, - rdfs::CLASS, - ]; - debug_span!("Ontology Indexing").in_scope(|| { + let mut counter = 0; let mut writer = index.writer().expect("Failed to build index writer"); - for class in &classes_to_index { - let class = class.into_owned(); - if let Some(id) = ontology.catalog_id(&class) { - writer.remove_all_of_type(id).expect("Failed to remove documents from search index"); - for individual in ontology.individuals(&class) { - let info = ontology.info(&individual, &conversion::english()); - 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).expect("Failed to add document to search index"); - } - } + 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(), + ); + 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 @@ -207,7 +192,7 @@ impl Publisher { document.add_u64(Schema::type_field(), catalog_id); document.add_text(Schema::iri_field(), rdf_source.origin()); - for quad in rdf_source.dataset() { + /*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 @@ -217,7 +202,7 @@ impl Publisher { .expect("Field not found in schema"); document.add_text(field, literal.value()); } - } + }*/ if let Some(writer) = &self.index_writer { writer.add(document).expect("Unable to add document to search index"); @@ -328,13 +313,13 @@ impl Publisher { if let Some(search_state) = &mut self.search_state { search_state.action = action; } else { - if let Some(type_) = self.ontology.labeled_entities().next() { + if let Some(entity) = self.ontology.labeled_entities(&*language::ENGLISH_OR_UNTAGGED).next() { let (id, window_task) = window::open(Settings::default()); self.search_state = Some(SearchState { window_id: id, action, query: String::new(), - type_, + type_: entity, results: Vec::new(), }); task = window_task.map(|_| Message::None) @@ -350,6 +335,7 @@ impl Publisher { } Message::QueryUpdated(new_query) => { if let Some(search_state) = &mut self.search_state { + let selected_entity_class = &search_state.type_.iri; let catalog_id = match search_state.action { SearchResultClickAction::Predicate(_) => self.ontology.catalog_id(&rdf::PROPERTY.into_owned()), SearchResultClickAction::Object(key) => { @@ -360,10 +346,10 @@ impl Publisher { .and_then(|quad| if quad.predicate == rdf::TYPE { self.ontology.catalog_id(&rdfs::CLASS.into_owned()) } else { - self.ontology.catalog_id(&search_state.type_.iri) + self.ontology.catalog_id(selected_entity_class) }) }, - SearchResultClickAction::URLInput => self.ontology.catalog_id(&search_state.type_.iri), + SearchResultClickAction::URLInput => self.ontology.catalog_id(selected_entity_class), }; let index = self.index.clone(); @@ -373,7 +359,7 @@ impl Publisher { }); task = Task::future(async { match search_task.await { - Ok(Ok(documents)) => Message::SetSearchResults(documents), + Ok(Ok(results)) => Message::SetSearchResults(results), Ok(Err(err)) => Message::ShowError(err.to_string()), Err(err) => Message::ShowError(err.to_string()), } @@ -556,9 +542,8 @@ impl Publisher { let property_label = self .ontology - .info(&triple.predicate, &conversion::english()) - .label - .unwrap_or(self.ontology.abbreviate(triple.predicate.as_ref())); + .info(&triple.predicate, &*language::ENGLISH_OR_UNTAGGED) + .label; let property: Element = if state.read_only { text(property_label).into() @@ -574,10 +559,8 @@ impl Publisher { let term = TermHelper::new(&triple.object); let value_label = term.value_as_named_node().and_then(|node| { - self.ontology - .info(&node.into_owned(), &conversion::english()) - .label - .map(|label| container(text(label))) + let info = self.ontology.info(&node.into_owned(), &*language::ENGLISH_OR_UNTAGGED); + Some(container(text(info.label))) }); let value = term @@ -668,9 +651,8 @@ impl Publisher { let buttons = entities.map(|entity| { let label = self .ontology - .info(&entity, &conversion::english()) - .label - .unwrap_or(entity.as_str().to_string()); + .info(&entity, &*language::ENGLISH_OR_UNTAGGED) + .label; button(text(label)) .on_press(Message::NewDocument(entity)) .into() @@ -689,35 +671,46 @@ impl Publisher { text_input("Query", &search_state.query).on_input(Message::QueryUpdated); let mut entities = self.ontology - .labeled_entities() + .labeled_entities(&*language::ENGLISH_OR_UNTAGGED) .collect::>(); entities.sort_by(|a, b| Ord::cmp(&a.label, &b.label)); let type_selector = pick_list(Some(&search_state.type_), entities, ToString::to_string) .on_select(|selection| Message::QueryTypeUpdated(selection.iri)); - let mut columns = vec![]; - for field in self.ontology.fields_for_class(&search_state.type_.iri) { - let header_text = field.label.as_ref().unwrap_or(&field.name); - /*columns.push(table::column(text(header_text), |document: &QueryResult| { - let cell_value = document.0.get(&field.name) - .and_then(|values| values.first()) - .map(|value| match value { - OwnedValue::Str(string) => string, - _ => "???", - }).unwrap_or(""); + let fields = self.ontology.fields_for_class(&search_state.type_.iri, &*language::ENGLISH_OR_UNTAGGED) + .expect("Unable to load fields for class"); - let iri = document.get("iri") - .and_then(|values| values.first()) - .map(|value| match value { - OwnedValue::Str(string) => string, - _ => "???", - }).unwrap_or(""); + 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(); - button(text(cell_value).wrapping(Wrapping::Word)) + let abbreviated_iri = self.ontology.abbreviate(NamedNodeRef::new_unchecked(iri)); + button(text(abbreviated_iri).wrapping(Wrapping::Word)) .on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri))) .style(button::text) - }).width(Length::Fixed(256.0)));*/ + }) + ]; + + for field in fields.into_iter() { + let field_name = field.name.clone(); + columns.push( + table::column(text(field.label), move |document: &SearchDocument| { + 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, 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)) + ); } let results_table = if columns.is_empty() { diff --git a/publish/src/rdf/conversion.rs b/publish/src/rdf/conversion.rs index 8c03a0f..b930495 100644 --- a/publish/src/rdf/conversion.rs +++ b/publish/src/rdf/conversion.rs @@ -2,38 +2,10 @@ use oxigraph::model::{NamedNode, Quad, Term}; use oxigraph::model::vocab::{rdf, xsd}; use oxilangtag::LanguageTag; -pub enum LanguageCondition { - ExactMatch(LanguageTag), - RelaxedMatch(LanguageTag), - Untagged, -} - pub fn quad_into_term(quad: Quad) -> Term { quad.object } -pub fn english() -> LanguageCondition { - LanguageCondition::RelaxedMatch(LanguageTag::parse("en".to_string()).unwrap()) -} - -pub fn language_matches(term: &Term, condition: &LanguageCondition) -> bool { - if let Term::Literal(literal) = term { - let tag = literal.language() - .map(LanguageTag::parse_and_normalize) - .and_then(Result::ok); - - match (tag, condition) { - (Some(language), LanguageCondition::ExactMatch(expectation)) | - (Some(language), LanguageCondition::RelaxedMatch(expectation)) => language == *expectation, - (None, LanguageCondition::RelaxedMatch(_)) => true, - (None, LanguageCondition::Untagged) => true, - _ => false, - } - } else { - false - } -} - pub fn term_to_named_node(term: &Term) -> Option<&NamedNode> { if let Term::NamedNode(node) = term { Some(node) @@ -59,6 +31,18 @@ pub fn term_as_str(term: &Term) -> Option<&str> { } } +/*pub fn term_to_tagged_string(term: &Term) -> Option<(String, Option>)> { + term_as_str(&term).and_then(|value| { + if let Term::Literal(literal) = term { + literal.language() + .map(|language| { + let tag = LanguageTag::parse(language.to_string()).ok(); + (value.to_string(), tag) + }) + } else { None } + }) +}*/ + pub fn term_to_boolean(term: &Term) -> Option { if let Term::Literal(literal) = term { if literal.datatype() == xsd::BOOLEAN { diff --git a/publish/src/rdf/language.rs b/publish/src/rdf/language.rs new file mode 100644 index 0000000..276f5e0 --- /dev/null +++ b/publish/src/rdf/language.rs @@ -0,0 +1,46 @@ +use std::sync::LazyLock; +use oxigraph::model::Term; +use oxilangtag::LanguageTag; + +pub const ENGLISH_PRIMARY: &str = "en"; + +pub static ENGLISH_OR_UNTAGGED: LazyLock = LazyLock::new(|| { + LanguageCondition::ExactMatchOrUntagged(LanguageTag::parse(ENGLISH_PRIMARY.to_string()).unwrap()) +}); + +pub enum LanguageCondition { + ExactMatchOnly(LanguageTag), + ExactMatchOrUntagged(LanguageTag), + UntaggedOnly, + AnyOrNone, +} + +impl LanguageCondition { + pub fn primary_matches_term(&self, term: &Term) -> bool { + if let Term::Literal(literal) = term { + 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(), + (None, LanguageCondition::ExactMatchOrUntagged(_)) => true, + (None, LanguageCondition::UntaggedOnly) => true, + (_, LanguageCondition::AnyOrNone) => true, + _ => false, + } + } else { + false + } + } + + 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::UntaggedOnly => format!("FILTER(!hasLANG(?{var}))"), + LanguageCondition::AnyOrNone => "".to_string(), + } + } +} \ No newline at end of file diff --git a/publish/src/rdf/mod.rs b/publish/src/rdf/mod.rs index 640c592..c97d4c4 100644 --- a/publish/src/rdf/mod.rs +++ b/publish/src/rdf/mod.rs @@ -3,3 +3,4 @@ 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 diff --git a/publish/src/rdf/ontology.rs b/publish/src/rdf/ontology.rs index b82b1d2..4c195b6 100644 --- a/publish/src/rdf/ontology.rs +++ b/publish/src/rdf/ontology.rs @@ -1,3 +1,4 @@ +use std::borrow::Borrow; use crate::error; use crate::rdf::vocab::gl; use oxigraph::model::vocab::{rdf, rdfs, xsd}; @@ -7,10 +8,12 @@ use std::collections::{BTreeMap, HashMap, HashSet}; use std::fmt::Display; use std::path::{Path, PathBuf}; use std::sync::LazyLock; +use iced::widget::sensor::Key; use oxigraph::store::Store; -use tracing::debug_span; -use crate::rdf::{conversion, materialize}; -use crate::rdf::conversion::LanguageCondition; +use oxilangtag::LanguageTag; +use tracing::{debug, debug_span}; +use crate::rdf::{conversion, language, materialize}; +use crate::rdf::language::LanguageCondition; static PREFIXES: LazyLock> = LazyLock::new(|| { BTreeMap::from_iter([ @@ -30,14 +33,15 @@ static PREFIXES: LazyLock> = LazyLock::new(|| { ("dcterms", "http://purl.org/dc/terms/"), ("fedora", "http://fedora.info/definitions/v4/repository#"), ("rdaa", "http://rdaregistry.info/Elements/a/"), - ("rdaad", "http://rdaregistry.info/Elements/a/datatype/"), ("rdac", "http://rdaregistry.info/Elements/c/"), ("rdae", "http://rdaregistry.info/Elements/e/"), ("rdai", "http://rdaregistry.info/Elements/i/"), ("rdam", "http://rdaregistry.info/Elements/m/"), ("rdan", "http://rdaregistry.info/Elements/n/"), ("rdap", "http://rdaregistry.info/Elements/p/"), + ("rdaf", "http://rdaregistry.info/Elements/rof/"), ("rdat", "http://rdaregistry.info/Elements/t/"), + ("rdau", "http://rdaregistry.info/Elements/u/"), ("rdaw", "http://rdaregistry.info/Elements/w/"), ("rdax", "http://rdaregistry.info/Elements/x/"), ("schema", "https://schema.org/"), @@ -69,17 +73,14 @@ impl OntologyBuilder { .map(|(k, v)| (k.to_string(), v.to_string())) .collect::>(); - materialize::same_as(&mut store)?; + /*materialize::same_as(&mut store)?; materialize::super_properties(&mut store)?; materialize::super_classes(&mut store)?; - debug_span!("Optimize Ontology").in_scope(|| store.optimize())?; - - // Full-text search index field names - let fields = Self::fields(&store)?; + debug_span!("Optimize Ontology").in_scope(|| store.optimize())?;*/ let mut entities: HashMap = HashMap::new(); // All Entities have exactly one catalog ID. - for quad in store.quads_for_pattern(None, Some(gl::CATALOG_ID), None, None) + /*for quad in store.quads_for_pattern(None, Some(gl::CATALOG_ID), None, None) .filter_map(Result::ok) { let label = store.quads_for_pattern(Some(quad.subject.as_ref().into()), Some(rdfs::LABEL), None, None) .filter_map(Result::ok) @@ -112,7 +113,7 @@ impl OntologyBuilder { properties, }); } - } + }*/ let mut indexed_by = HashMap::new(); for quad in store.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None) @@ -127,80 +128,21 @@ impl OntologyBuilder { Ok(Ontology { store, prefixes, - fields, entities, indexed_by, }) } +} - fn fields(store: &Store) -> error::Result> { - let query = SparqlEvaluator::new() - .with_prefix("rdfs", "http://www.w3.org/2000/01/rdf-schema#")? - .with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")? - .parse_query( - r#"SELECT DISTINCT ?subject ?name ?label { - GRAPH ?graph { - ?subject a gl:IndexDocumentField ; - gl:fieldName ?name ; - gl:fieldLabel ?label . - FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label)) - } -}"#)?; - - let mut results = HashMap::new(); - if let QueryResults::Solutions(solutions) = query.on_store(store).execute()? - { - for solution in solutions.filter_map(Result::ok) { - let subject = solution.get("subject").and_then(conversion::term_to_named_node); - 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(subject) = subject && let Some(name) = name { - let field = IndexField { - name: name.to_string(), - label: label.map(|l| l.to_string()), - }; - results.insert(subject.to_owned(), field); - } - } - } - Ok(results) - } - - /*fn subclass_of(dataset: &Dataset, class: NamedNodeRef<'_>) -> impl Iterator { - let query = SparqlEvaluator::new() - .parse_query( - format!( - "PREFIX rdfs: -SELECT ?class {{ - ?class (rdfs:subClassOf|^rdfs:subClassOf)* {class} . -}}" - ) - .as_str(), - ) - .expect("Unable to parse subclass_of query"); - - if let QueryResults::Solutions(solutions) = - query.on_queryable_dataset(dataset).execute().unwrap() - { - solutions.filter_map(Result::ok).filter_map(|solution| { - if let Some(Term::NamedNode(class)) = solution.get("class") { - Some(class.clone()) - } else { - None - } - }) - } else { - unreachable!() - } - }*/ +pub struct IndexEntry { + pub catalog_id: u64, + pub fields: HashMap, } #[derive(Clone, Debug)] pub struct LabeledIri { pub iri: NamedNode, - pub label: Option, - pub comment: Option, + pub label: String, } impl PartialEq for LabeledIri { @@ -211,7 +153,7 @@ impl PartialEq for LabeledIri { impl Display for LabeledIri { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - write!(f, "{}", self.label.clone().unwrap_or_default()) + write!(f, "{}", self.label) } } @@ -226,19 +168,13 @@ pub struct Entity { #[derive(Clone, Debug)] pub struct IndexField { pub name: String, - pub label: Option, + pub label: String, } pub struct Ontology { store: Store, prefixes: HashMap, - // Resource (Property or Class) -> Rust Type - //iri_info: HashMap, - - // NamedIndividual of class IndexField -> Rust Type - fields: HashMap, - // NamedIndividual of class Entity -> Rust Type entities: HashMap, @@ -257,6 +193,81 @@ impl Ontology { &*PREFIXES } + pub fn index(&self, language: &LanguageCondition) -> error::Result> { + let language_filter = language.to_filter_expression("fieldValue"); + let query = format!(r#"SELECT ?individual ?catalogId ?fieldName ?fieldValue +WHERE {{ + ?entity a gl:Entity ; + gl:catalogId ?catalogId ; + gl:associatedProperty ?property . + + ?property gl:indexedByField/gl:fieldName ?fieldName . + + ?individual a ?entity ; + ?property ?fieldValue . + + {language_filter} +}}"#); + let mut sparql = SparqlEvaluator::new() + .with_prefix("gl", "https://graphofliberty.org/2026/04/ont/")? + .parse_query(&query)?; + sparql.dataset_mut().set_default_graph_as_union(); + + let mut results = HashMap::new(); + if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? { + 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); + 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()) + .and_modify(|entry: &mut IndexEntry| { + entry.fields.insert(field_name.to_owned(), field_value.to_owned()); + }).or_insert(IndexEntry { + catalog_id, + fields: HashMap::from_iter([(field_name.to_owned(), field_value.to_owned())]), + }); + } + } + } + + Ok(results) + } + + pub fn fields_for_class(&self, class: &NamedNode, language: &LanguageCondition) -> error::Result> { + let language_filter = language.to_filter_expression("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", "https://graphofliberty.org/2026/04/ont/")? + .parse_query(&query)?; + sparql.dataset_mut().set_default_graph_as_union(); + + let mut results = Vec::new(); + if let QueryResults::Solutions(solutions) = sparql.on_store(&self.store).execute()? { + for solution in solutions.filter_map(Result::ok) { + 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 { + results.push(IndexField { + name: name.to_string(), + label: label.to_string(), + }); + } + } + } + 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) { @@ -277,56 +288,24 @@ impl Ontology { .map(|base| NamedNode::new_unchecked(format!("{base}{name}"))) } - pub fn field_for_property(&self, property: &NamedNode) -> Option<&IndexField> { - self.indexed_by.get(property) - .and_then(|node| self.fields.get(node)) - } - - pub fn fields_for_class(&self, class: &NamedNode) -> Vec<&IndexField> { - self.entities.get(class) - .and_then(|entity| { - entity.properties.iter() - .map(|property| self.field_for_property(property)) - .filter(Option::is_some) - .collect() - }).unwrap_or(Vec::new()) - } - pub fn catalog_id(&self, class: &NamedNode) -> Option { self.entities .get(class) .map(|entity| entity.catalog_id) } - pub fn individuals(&self, class: &NamedNode) -> impl Iterator { - 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, 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(conversion::quad_into_term) - .filter(|term| conversion::language_matches(term, language)) + .filter(|term| language.primary_matches_term(term)) .filter_map(conversion::term_into_string) - .next(); - - let comment = self.store.quads_for_pattern(Some(iri.as_ref().into()), Some(rdfs::COMMENT), None, None) - .filter_map(Result::ok) - .map(conversion::quad_into_term) - .filter(|term| conversion::language_matches(term, language)) - .filter_map(conversion::term_into_string) - .next(); + .next() + .unwrap_or_default(); LabeledIri { iri: iri.clone(), label, - comment, } } @@ -334,17 +313,26 @@ impl Ontology { self.entities.get(class) .map(|entity| LabeledIri { iri: class.to_owned(), - label: Some(entity.label.to_owned()), - comment: entity.comment.to_owned(), + label: entity.label.to_owned(), }) } - pub fn labeled_entities(&self) -> impl Iterator { - self.entities.iter() - .map(|(iri, entity)| LabeledIri { - iri: iri.to_owned(), - label: Some(entity.label.to_owned()), - comment: entity.comment.to_owned(), + pub fn labeled_entities(&self, language: &LanguageCondition) -> impl Iterator { + self.store.quads_for_pattern(None, Some(rdf::TYPE), Some(gl::ENTITY.into()), None) + .filter_map(Result::ok) + .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(conversion::quad_into_term) + .filter(|term| language.primary_matches_term(term)) + .filter_map(conversion::term_into_string) + .next(); + if let Some(label) = label && let NamedOrBlankNode::NamedNode(subject) = quad.subject { + Some(LabeledIri { + iri: subject, + label, + }) + } else { None } }) } diff --git a/search/src/index.rs b/search/src/index.rs index b5683c0..918db1c 100644 --- a/search/src/index.rs +++ b/search/src/index.rs @@ -1,5 +1,3 @@ -use std::cmp::Ordering; -use std::collections::{BTreeMap, BTreeSet}; use crate::{error, update}; use crate::schema::Schema; use std::path::PathBuf; @@ -9,7 +7,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::{span, Level}; +use tracing::{debug, span, Level}; #[derive(Default)] pub struct SearchIndexBuilder { diff --git a/search/src/lib.rs b/search/src/lib.rs index 0383146..b8f34ad 100644 --- a/search/src/lib.rs +++ b/search/src/lib.rs @@ -5,7 +5,7 @@ mod update; pub use tantivy::TantivyDocument as SearchDocument; pub use tantivy::doc; -pub use tantivy::schema::OwnedValue; +pub use tantivy::schema::document::Value; pub use error::{Result, SearchError}; pub use index::{SearchIndex, SearchIndexBuilder}; diff --git a/search/src/schema.rs b/search/src/schema.rs index 13567a0..0490a71 100644 --- a/search/src/schema.rs +++ b/search/src/schema.rs @@ -27,8 +27,8 @@ impl Schema { schema_builder.add_u64_field("type", schema::FAST | schema::INDEXED); schema_builder.add_text_field("iri", schema::STORED | schema::STRING); - schema_builder.add_text_field("label", stored_ngram32.clone()); - schema_builder.add_text_field("definition", stored_en_stem.clone()); + 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()); @@ -45,9 +45,9 @@ impl Schema { Self::schema().get_field("iri").unwrap() } - pub fn field(name: &str) -> Field { + pub fn field(name: &str, language: &str) -> Field { Schema::schema() - .get_field(name) + .get_field(&format!("{name}:{language}")) .expect("Field not found in schema") }