From f5ca8dd9ae90d0aab03e660619a0fe17189c5d5f287480211b088a949a40614c Mon Sep 17 00:00:00 2001 From: Alex Wied <543423+centromere@users.noreply.github.com> Date: Wed, 17 Jun 2026 20:50:26 -0400 Subject: [PATCH] . --- publish/src/app.rs | 133 ++++++++++----- publish/src/rdf/ontologies/ontology.ttl | 95 ++++++++--- publish/src/rdf/ontology.rs | 213 +++++++++++++++++------- publish/src/rdf/vocab.rs | 9 +- search/src/index.rs | 34 ++-- search/src/lib.rs | 1 + search/src/schema.rs | 27 ++- 7 files changed, 352 insertions(+), 160 deletions(-) diff --git a/publish/src/app.rs b/publish/src/app.rs index da2187d..ca25a5b 100644 --- a/publish/src/app.rs +++ b/publish/src/app.rs @@ -1,17 +1,16 @@ -use crate::rdf::ontology::Ontology; +use std::collections::HashSet; +use crate::rdf::ontology::{LabeledIri, Ontology}; use crate::rdf::term_helper::{TermHelper, TermHelperMut}; use crate::rdf::vocab::{gl, rda}; -use gl_search::{Schema, SearchDocument, SearchIndex, doc}; +use gl_search::{Schema, SearchDocument, SearchIndex, doc, NamedFieldDocument, OwnedValue}; use http::StatusCode; use iced::alignment::Horizontal; use iced::widget::button::Style; use iced::widget::grid::Sizing; -use iced::widget::{ - button, center, column, combo_box, container, grid, mouse_area, opaque, row, scrollable, space, - stack, table, text, text_input, toggler, -}; +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}; @@ -20,7 +19,7 @@ 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 oxigraph::model::{BaseDirection, Dataset, NamedNode, NamedNodeRef, Quad, Term, TermRef}; use tracing::{debug, error, info}; #[derive(Debug, Clone)] @@ -44,6 +43,7 @@ pub(crate) enum Message { HoverRow(QuadKey), UnhoverRow(QuadKey), QueryUpdated(String), + QueryTypeUpdated(NamedNode), SearchResultClicked(NamedNode), DatatypeUpdated(QuadKey, Option), LanguageUpdated(QuadKey, Option), @@ -74,7 +74,8 @@ struct SearchState { window_id: window::Id, action: SearchResultClickAction, query: String, - results: Vec, + type_: LabeledIri, + results: Vec, } pub(crate) struct Publisher { @@ -113,12 +114,14 @@ impl Publisher { Schema::iri_field() => iri.as_str(), ); - if let Some(label) = &info.label { - document.add_text(Schema::field("label"), label.to_lowercase()); + if let Some(label) = &info.label && + let Some(field) = ontology.field_for_property(&rdfs::LABEL.into_owned()) { + document.add_text(Schema::field(&field.name), label); } - if let Some(comment) = &info.comment { - document.add_text(Schema::field("comment"), comment.to_lowercase()); + if let Some(comment) = &info.comment && + let Some(field) = ontology.field_for_property(&rdfs::COMMENT.into_owned()) { + document.add_text(Schema::field(&field.name), comment); } index.add(document).expect("Unable to add annotated IRI to search index"); @@ -192,9 +195,9 @@ impl Publisher { && let TermRef::Literal(literal) = quad.object { let field = Schema::schema() - .get_field(&field.key) + .get_field(field.name.as_str()) .expect("Field not found in schema"); - document.add_text(field, literal.value().to_lowercase()); + document.add_text(field, literal.value()); } } @@ -306,19 +309,29 @@ impl Publisher { if let Some(search_state) = &mut self.search_state { search_state.action = action; } else { - let (id, window_task) = window::open(Settings::default()); - self.search_state = Some(SearchState { - window_id: id, - action, - query: String::new(), - results: Vec::new(), - }); - task = window_task.map(|_| Message::None) + if let Some(type_) = self.ontology.labeled_entities().next() { + let (id, window_task) = window::open(Settings::default()); + self.search_state = Some(SearchState { + window_id: id, + action, + query: String::new(), + type_, + results: Vec::new(), + }); + task = window_task.map(|_| Message::None) + }; + } + } + Message::QueryTypeUpdated(type_) => { + if let Some(search_state) = &mut self.search_state { + if let Some(type_) = self.ontology.labeled_entity(&type_) { + search_state.type_ = type_; + } } } Message::QueryUpdated(new_query) => { if let Some(search_state) = &mut self.search_state { - let type_ = match search_state.action { + let catalog_id = match search_state.action { SearchResultClickAction::Predicate(_) => self.ontology.catalog_id(&rdf::PROPERTY.into_owned()), SearchResultClickAction::Object(key) => { self.document @@ -334,11 +347,8 @@ impl Publisher { search_state.results = self .index - .query(type_, new_query.as_str(), Schema::all_fields()) - .expect("Error encountered while querying index") - .iter() - .map(NamedNode::new_unchecked) - .collect(); + .query(catalog_id, new_query.as_str(), Schema::all_fields()) + .expect("Error encountered while querying index"); search_state.query = new_query; }; } @@ -670,7 +680,41 @@ impl Publisher { let search_input = text_input("Query", &search_state.query).on_input(Message::QueryUpdated); - let label_column = table::column("Label", |result: &NamedNode| { + let mut entities = self.ontology + .labeled_entities() + .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: &NamedFieldDocument| { + let cell_value = document.0 + .get(&field.name) + .and_then(|values| values.first()) + .map(|value| match value { + OwnedValue::Str(string) => string, + _ => "???", + }).unwrap_or(""); + + let iri = document.0 + .get("iri") + .and_then(|values| values.first()) + .map(|value| match value { + OwnedValue::Str(string) => string, + _ => "???", + }).unwrap_or(""); + + button(text(cell_value).wrapping(Wrapping::Word)) + .on_press(Message::SearchResultClicked(NamedNode::new_unchecked(iri))) + .style(button::text) + }).width(Length::Fixed(256.0))); + } + + /*let label_column = table::column("Label", |result: &NamedNode| { let header_text = self .ontology .info(result.as_ref()) @@ -695,7 +739,7 @@ impl Publisher { .style(button::text) }); - /*let description_column = table::column("Description", |result: &NamedNode| { + let description_column = table::column("Description", |result: &NamedNode| { let header_text = self.ontology.info(result.as_ref()) .and_then(|info| info.comment.clone()) .unwrap_or("Unknown".to_string()); @@ -703,25 +747,24 @@ impl Publisher { button(text(header_text)) .on_press(Message::SearchResultClicked(result.clone())) .style(button::text) - });*/ + }); let iri_column = table::column("IRI", |result: &NamedNode| { button(text(result.as_str())) .on_press(Message::SearchResultClicked(result.clone())) .style(button::text) - }); + });*/ - let results_table = scrollable(table( - [ - label_column, - //description_column, - type_column, - iri_column, - ], - &search_state.results, - )); + let results_table = if columns.is_empty() { + None + } else { + Some(scrollable(table(columns, &search_state.results))) + }; - return column![search_input, results_table].into(); + return column![ + row![search_input, type_selector], + results_table + ].into(); } let add_row_button = button("Add row").on_press(Message::AddRow(None)); @@ -742,8 +785,8 @@ impl Publisher { let body: Element = if self.show_new_document_buttons { column![ - self.view_new_entity_buttons(self.ontology.subclass_of(gl::ENTITY)), - self.view_new_entity_buttons(self.ontology.subclass_of(rda::ENTITY)), + //self.view_new_entity_buttons(self.ontology.subclass_of(gl::ENTITY)), + //self.view_new_entity_buttons(self.ontology.subclass_of(rda::ENTITY)), ] .into() } else { @@ -818,4 +861,4 @@ where ) ] .into() -} +} \ No newline at end of file diff --git a/publish/src/rdf/ontologies/ontology.ttl b/publish/src/rdf/ontologies/ontology.ttl index d71a3b6..c18f556 100644 --- a/publish/src/rdf/ontologies/ontology.ttl +++ b/publish/src/rdf/ontologies/ontology.ttl @@ -99,6 +99,11 @@ rdam:P30154 rdf:type owl:ObjectProperty . rdam:uniformResourceLocator.en rdf:type owl:ObjectProperty . +### https://graphofliberty.org/2026/04/ont/associatedProperty +:associatedProperty rdf:type owl:ObjectProperty ; + rdfs:label "associated property"@en . + + ### https://graphofliberty.org/2026/04/ont/indexedByField :indexedByField rdf:type owl:ObjectProperty ; rdfs:domain owl:DatatypeProperty ; @@ -163,6 +168,18 @@ ldp:contains rdf:type owl:DatatypeProperty ; rdfs:label "catalog id" . +### https://graphofliberty.org/2026/04/ont/fieldLabel +:fieldLabel rdf:type owl:DatatypeProperty ; + rdfs:comment "The label of a field, which shall be displayed to the user."@en ; + rdfs:label "field label"@en . + + +### https://graphofliberty.org/2026/04/ont/fieldName +:fieldName rdf:type owl:DatatypeProperty ; + rdfs:comment "The name of the field, as defined in the full-text search document schema."@en ; + rdfs:label "field name"@en . + + ### https://graphofliberty.org/2026/04/ont/readOnly :readOnly rdf:type owl:DatatypeProperty ; rdfs:comment "Indicates that the property or class is read only (server managed) and should not be made editable in user-facing applications."@en ; @@ -195,8 +212,7 @@ rdac:C10002 rdf:type owl:Class ; ### http://rdaregistry.info/Elements/c/C10004 rdac:C10004 rdf:type owl:Class ; - rdfs:subClassOf rdac:C10002 ; - rdfs:label "Person"@en . + rdfs:subClassOf rdac:C10002 . ### http://rdaregistry.info/Elements/c/C10007 @@ -282,11 +298,6 @@ ldp:Resource rdf:type owl:Class . rdfs:subClassOf :Entity . -### https://graphofliberty.org/2026/04/ont/Person -:Person rdf:type owl:Class ; - rdfs:subClassOf :Entity . - - ### https://graphofliberty.org/2026/04/ont/Podcast :Podcast rdf:type owl:Class ; rdfs:subClassOf :Entity . @@ -366,6 +377,13 @@ rdaa:identifierForPerson.en rdf:type owl:NamedIndividual . rdaa:surname.en rdf:type owl:NamedIndividual . +### http://rdaregistry.info/Elements/c/C10004 +rdac:C10004 rdf:type owl:NamedIndividual ; + :associatedProperty rdaa:P50291 , + rdaa:P50292 ; + :catalogId "8"^^xsd:nonNegativeInteger . + + ### http://rdaregistry.info/Elements/c/C10007 rdac:C10007 rdf:type owl:NamedIndividual ; :template [ rdf:type rdac:C10007 @@ -385,14 +403,28 @@ rdam:uniformResourceLocator.en rdf:type owl:NamedIndividual . ### http://www.w3.org/1999/02/22-rdf-syntax-ns#Property rdf:Property rdf:type owl:NamedIndividual ; + :associatedProperty rdfs:comment , + rdfs:label ; :catalogId "0"^^xsd:nonNegativeInteger . ### http://www.w3.org/2000/01/rdf-schema#Class rdfs:Class rdf:type owl:NamedIndividual ; + :associatedProperty rdfs:comment , + rdfs:label ; :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 ldp:BasicContainer rdf:type owl:NamedIndividual ; :readOnly "true"^^xsd:boolean . @@ -448,11 +480,6 @@ ldp:contains rdf:type owl:NamedIndividual ; :catalogId "7"^^xsd:nonNegativeInteger . -### https://graphofliberty.org/2026/04/ont/Person -:Person rdf:type owl:NamedIndividual ; - :catalogId "8"^^xsd:nonNegativeInteger . - - ### https://graphofliberty.org/2026/04/ont/Podcast :Podcast rdf:type owl:NamedIndividual ; :catalogId "9"^^xsd:nonNegativeInteger . @@ -463,18 +490,36 @@ ldp:contains rdf:type owl:NamedIndividual ; :catalogId "10"^^xsd:nonNegativeInteger . +### https://graphofliberty.org/2026/04/ont/comment +:comment rdf:type owl:NamedIndividual , + :IndexField ; + :fieldLabel "Comment"@en ; + :fieldName "comment" ; + rdfs:label "Comment Field"@en . + + ### https://graphofliberty.org/2026/04/ont/givenName :givenName rdf:type owl:NamedIndividual , :IndexField ; - rdfs:label "Given Name"@en ; - rdfs:value "given name" . + :fieldLabel "Given Name"@en ; + :fieldName "givenName" ; + rdfs:label "Given Name Field"@en . + + +### https://graphofliberty.org/2026/04/ont/label +:label rdf:type owl:NamedIndividual , + :IndexField ; + :fieldLabel "Label"@en ; + :fieldName "label" ; + rdfs:label "Label Field"@en . ### https://graphofliberty.org/2026/04/ont/surname :surname rdf:type owl:NamedIndividual , :IndexField ; - rdfs:label "Surname"@en ; - rdfs:value "surname" . + :fieldLabel "Surname"@en ; + :fieldName "surname" ; + rdfs:label "Surname Field"@en . ################################################################# @@ -490,12 +535,27 @@ rdaa:P50291 rdfs:label "has surname"@en . rdaa:P50292 rdfs:label "has given name"@en . +rdac:C10004 rdfs:label "Person"@en . + + rdac:C10007 rdfs:label "Manifestation"@en . rdam:P30154 rdfs:label "has uniform resource locator"@en . +rdf:Property rdfs:label "Property"@en . + + +rdfs:Class rdfs:label "Class"@en . + + +rdfs:comment rdfs:label "Comment Property"@en . + + +rdfs:label rdfs:label "Label Property"@en . + + :AudioBook rdfs:label "Audio Book"@en . @@ -514,9 +574,6 @@ rdam:P30154 rdfs:label "has uniform resource locator"@en . :Music rdfs:label "Music"@en . -:Person rdfs:label "Person"@en . - - :Podcast rdfs:label "Podcast"@en . diff --git a/publish/src/rdf/ontology.rs b/publish/src/rdf/ontology.rs index 58eb587..db4c863 100644 --- a/publish/src/rdf/ontology.rs +++ b/publish/src/rdf/ontology.rs @@ -1,5 +1,5 @@ use crate::error; -use crate::rdf::vocab::{gl, owl}; +use crate::rdf::vocab::{gl, owl, rda}; use oxigraph::io::{RdfFormat, RdfParser}; use oxigraph::model::vocab::{rdf, rdfs, xsd}; use oxigraph::model::{ @@ -7,9 +7,10 @@ use oxigraph::model::{ TermRef, Triple, TripleRef, }; use oxigraph::sparql::{QueryResults, SparqlEvaluator}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; +use std::fmt::Display; use iced::widget::sensor::Key; -use tracing::debug; +use tracing::{debug, info}; const PREFIXES: &[(&str, &str)] = &[ ("rdf", "http://www.w3.org/1999/02/22-rdf-syntax-ns#"), @@ -155,11 +156,11 @@ SELECT DISTINCT ?class ?subject ?label ?comment ?read_only { ?subject a ?class . OPTIONAL { ?subject rdfs:label ?label - FILTER (LANG(?label) = 'en' || LANG(?label) = '') + FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label)) } OPTIONAL { ?subject rdfs:comment ?comment - FILTER (LANG(?comment) = 'en' || LANG(?comment) = '') + FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?comment)) } OPTIONAL { ?subject gl:readOnly ?read_only } }"#, @@ -200,11 +201,11 @@ SELECT DISTINCT ?class ?subject ?label ?comment ?read_only { r#"PREFIX rdfs: PREFIX gl: -SELECT DISTINCT ?subject ?key ?label { +SELECT DISTINCT ?subject ?name ?label { ?subject a gl:IndexField ; - rdfs:value ?key ; - rdfs:label ?label . - FILTER(langMATCHES(LANG(?label), "en") || !hasLANG(?label)) + gl:fieldName ?name ; + gl:fieldLabel ?label . + FILTER (langMATCHES(LANG(?label), "en") || !hasLANG(?label)) }"#, ) .expect("Unable to parse field query"); @@ -215,13 +216,13 @@ SELECT DISTINCT ?subject ?key ?label { { for solution in solutions.filter_map(Result::ok) { let subject = solution.get("subject").and_then(term_to_named_node); - let key = solution.get("key").and_then(term_to_string); + let name = solution.get("name").and_then(term_to_string); let label = solution.get("label").and_then(term_to_string); - if let Some(subject) = subject && let Some(key) = key { + if let Some(subject) = subject && let Some(name) = name { let field = IndexField { - key: key.to_owned(), - label: label.map(String::from), + name: name.to_string(), + label: label.map(|l| l.to_string()), }; results.insert(subject.to_owned(), field); } @@ -230,6 +231,34 @@ SELECT DISTINCT ?subject ?key ?label { 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 fn build(&mut self) -> error::Result { let prefixes = PREFIXES .iter() @@ -251,6 +280,41 @@ SELECT DISTINCT ?subject ?key ?label { // Full-text search index field names let fields = Self::fields(&dataset); + let mut entities: HashMap = HashMap::new(); + let entity_iris = Self::subclass_of(&dataset, gl::ENTITY) + .chain(Self::subclass_of(&dataset, rda::ENTITY)) + .chain([ + rdf::PROPERTY.into_owned(), + rdfs::CLASS.into_owned(), + ]); + + for iri in entity_iris { + let subject = iri.as_ref().into(); + + let label = dataset.quads_for_pattern(Some(subject), Some(rdfs::LABEL), None, None) + .filter_map(|quad| term_to_string(&quad.object.into_owned()).map(String::from)) + .next(); + + let catalog_id = dataset.quads_for_pattern(Some(subject), Some(gl::CATALOG_ID), None, None) + .filter_map(|quad| term_to_u64(&quad.object.into_owned())) + .next(); + + if let Some(catalog_id) = catalog_id { + let mut properties = HashSet::new(); + for quad in dataset.quads_for_pattern(Some(subject), Some(gl::ASSOCIATED_PROPERTY), None, None) { + if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject + && let TermRef::NamedNode(property) = quad.object { + properties.insert(property.into_owned()); + } + } + entities.insert(iri, Entity { + label: label.unwrap_or(catalog_id.to_string()), + catalog_id, + properties, + }); + } + } + let mut indexed_by = HashMap::new(); for quad in dataset.quads_for_pattern(None, Some(gl::INDEXED_BY_FIELD), None, None) { if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject @@ -260,30 +324,35 @@ SELECT DISTINCT ?subject ?key ?label { } } - // Catalog IDs (used to quickly filter full-text search results) - let mut catalog_ids = HashMap::new(); - for quad in dataset.quads_for_pattern(None, Some(gl::CATALOG_ID), None, None) { - if let NamedOrBlankNodeRef::NamedNode(subject) = quad.subject - && let TermRef::Literal(literal) = quad.object - { - if literal.datatype() == xsd::NON_NEGATIVE_INTEGER { - let value: u64 = literal.value().parse().expect("Failed to parse catalog ID from ontology. It ought to be a non-negative integer."); - catalog_ids.insert(subject.into_owned(), value); - } - } - } - Ok(Ontology { dataset, prefixes, iri_info, fields, + entities, indexed_by, - catalog_ids, }) } } +#[derive(Clone, Debug)] +pub struct LabeledIri { + pub iri: NamedNode, + pub label: String, +} + +impl PartialEq for LabeledIri { + fn eq(&self, other: &Self) -> bool { + self.iri == other.iri + } +} + +impl Display for LabeledIri { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.label.clone()) + } +} + #[derive(Clone, Debug)] pub struct IriInformation { pub type_: NamedNode, @@ -292,19 +361,34 @@ pub struct IriInformation { pub read_only: bool, } +#[derive(Clone, Debug)] +pub struct Entity { + pub label: String, + pub catalog_id: u64, + pub properties: HashSet, +} + #[derive(Clone, Debug)] pub struct IndexField { - pub key: String, + pub name: String, pub label: Option, } pub struct Ontology { dataset: Dataset, 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, + + // Property -> NamedIndividual of class IndexField indexed_by: HashMap, - catalog_ids: HashMap, } fn term_to_named_node(term: &Term) -> Option<&NamedNode> { @@ -340,6 +424,14 @@ fn term_to_boolean(term: &Term) -> Option { } } +fn term_to_u64(term: &Term) -> Option { + if let Term::Literal(literal) = term && + literal.datatype() == xsd::NON_NEGATIVE_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 } +} + impl Ontology { pub fn builder<'a>() -> OntologyBuilder<'a> { OntologyBuilder { @@ -377,12 +469,39 @@ impl Ontology { .and_then(|node| self.fields.get(node)) } - pub fn catalog_id(&self, class: &NamedNode) -> Option { - self.catalog_ids.get(class).copied() + 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 iri_info(&self) -> &HashMap - { + pub fn catalog_id(&self, class: &NamedNode) -> Option { + self.entities + .get(class) + .map(|entity| entity.catalog_id) + } + + pub fn labeled_entity(&self, class: &NamedNode) -> Option { + self.entities.get(class) + .map(|entity| LabeledIri { + iri: class.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: entity.label.to_owned(), + }) + } + + pub fn iri_info(&self) -> &HashMap { &self.iri_info } @@ -423,34 +542,6 @@ impl Ontology { Self::is_read_only_impl(&self.iri_info, triple) } - pub fn subclass_of(&self, 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(&self.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 fn template_triples<'a>( &'a self, class: NamedNodeRef<'a>, diff --git a/publish/src/rdf/vocab.rs b/publish/src/rdf/vocab.rs index 45ab243..275c74e 100644 --- a/publish/src/rdf/vocab.rs +++ b/publish/src/rdf/vocab.rs @@ -3,16 +3,21 @@ pub mod gl { pub const TEMPLATE: NamedNodeRef = NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/template"); + pub const INDEXED_BY_FIELD: NamedNodeRef = NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/indexedByField"); + pub const CATALOG_ID: NamedNodeRef = NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/catalogId"); pub const ENTITY: NamedNodeRef = NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/Entity"); - pub const INDEX_FIELD: NamedNodeRef = - NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/IndexField"); + pub const ASSOCIATED_PROPERTY: NamedNodeRef = + NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/associatedProperty"); + + /*pub const INDEX_FIELD: NamedNodeRef = + NamedNodeRef::new_unchecked("https://graphofliberty.org/2026/04/ont/IndexField");*/ } pub mod owl { diff --git a/search/src/index.rs b/search/src/index.rs index 5305b77..2e9f2ec 100644 --- a/search/src/index.rs +++ b/search/src/index.rs @@ -1,13 +1,13 @@ -use crate::error; +use crate::{error, SearchDocument}; use crate::error::SearchError; use crate::schema::Schema; use std::path::PathBuf; use tantivy::collector::TopDocs; use tantivy::directory::{ManagedDirectory, MmapDirectory}; use tantivy::query::{BooleanQuery, Occur, QueryParser, TermQuery}; -use tantivy::schema::{Field, IndexRecordOption, Value}; -use tantivy::tokenizer::{NgramTokenizer, TokenizerManager}; -use tantivy::{Index, IndexReader, IndexWriter, ReloadPolicy, TantivyDocument, Term}; +use tantivy::schema::{Field, IndexRecordOption, NamedFieldDocument, Value}; +use tantivy::tokenizer::{LowerCaser, NgramTokenizer, TextAnalyzer, TokenizerManager}; +use tantivy::{Document, Index, IndexReader, IndexWriter, ReloadPolicy, Score, TantivyDocument, Term}; #[derive(Default)] pub struct SearchIndexBuilder { @@ -23,8 +23,12 @@ impl SearchIndexBuilder { pub fn build(self) -> error::Result { if let Some(path) = self.path { let ngram_32 = NgramTokenizer::new(1, 32, false)?; + let ngram_32_lowercase = TextAnalyzer::builder(ngram_32) + .filter(LowerCaser) + .build(); + let tokenizer_manager = TokenizerManager::default(); - tokenizer_manager.register("ngram_32", ngram_32); + tokenizer_manager.register("ngram_32", ngram_32_lowercase); let mmap_directory = MmapDirectory::open(path)?; let managed_directory = ManagedDirectory::wrap(Box::new(mmap_directory))?; @@ -82,7 +86,7 @@ impl SearchIndex { type_: Option, user_query: &str, default_fields: Vec, - ) -> error::Result> { + ) -> error::Result> { let parser = QueryParser::for_index(&self.index, default_fields); let (user_query, _) = parser.parse_query_lenient(user_query); @@ -98,16 +102,12 @@ impl SearchIndex { let query = BooleanQuery::new(subqueries); let searcher = self.reader.searcher(); - let results = searcher.search(&query, &TopDocs::with_limit(10000).order_by_score())?; - let mut iris = vec![]; - for (_score, address) in results.iter() { - let doc: TantivyDocument = searcher.doc(*address)?; - if let Some(doc_iri) = doc.get_first(Schema::iri_field()) { - let doc_iri_string = doc_iri.as_str().unwrap_or("???").to_string(); - iris.push(doc_iri_string); - } - } - - Ok(iris) + let results = searcher.search(&query, &TopDocs::with_limit(10000).order_by_score())? + .iter() + .map(|(_, address)| searcher.doc(*address)) + .filter_map(Result::ok) + .map(|doc: SearchDocument| doc.to_named_doc(Schema::schema())) + .collect(); + Ok(results) } } diff --git a/search/src/lib.rs b/search/src/lib.rs index 2cc46b8..f94c46b 100644 --- a/search/src/lib.rs +++ b/search/src/lib.rs @@ -4,6 +4,7 @@ mod schema; pub use tantivy::TantivyDocument as SearchDocument; pub use tantivy::doc; +pub use tantivy::schema::{NamedFieldDocument, OwnedValue}; pub use error::{Result, SearchError}; pub use index::{SearchIndex, SearchIndexBuilder}; diff --git a/search/src/schema.rs b/search/src/schema.rs index fa423a0..ff9b0de 100644 --- a/search/src/schema.rs +++ b/search/src/schema.rs @@ -11,36 +11,31 @@ pub struct Schema; impl Schema { pub fn schema() -> &'static TantivySchema { SCHEMA.get_or_init(|| { - let ngram_32 = 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(); - let 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(); let mut schema_builder = TantivySchema::builder(); 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", ngram_32.clone()); - schema_builder.add_text_field("comment", en_stem.clone()); - schema_builder.add_text_field("given name", ngram_32.clone()); - schema_builder.add_text_field("surname", ngram_32); + schema_builder.add_text_field("label", stored_ngram32.clone()); + schema_builder.add_text_field("comment", stored_en_stem.clone()); - schema_builder.add_text_field("title", en_stem.clone()); + schema_builder.add_text_field("givenName", stored_ngram32.clone()); + schema_builder.add_text_field("surname", stored_ngram32); + + /*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_u64_field("page", schema::STORED); - schema_builder.add_u64_field("book", schema::STORED); - schema_builder.add_u64_field("chapter", schema::STORED); - schema_builder.add_u64_field("verse", schema::STORED); - + schema_builder.add_text_field("content", en_stem);*/ schema_builder.build() }) }