diff --git a/CHANGELOG.md b/CHANGELOG.md index 2af566285..c2b6262cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- `AnnotationComponentType::apply_update_graph_index` makes sure all edges + from named dominance components are also contained in the default + dominance component `Dominance/annis`. + ## [4.2.0] - 2026-08-11 ### Added diff --git a/graphannis/benches/graphannis.rs b/graphannis/benches/graphannis.rs index 8603e52a1..ac912eedb 100644 --- a/graphannis/benches/graphannis.rs +++ b/graphannis/benches/graphannis.rs @@ -13,8 +13,10 @@ use fake::locales::*; use graphannis::CorpusStorage; use graphannis::corpusstorage::ResultOrder; use graphannis::corpusstorage::{QueryLanguage, SearchQuery}; +use graphannis::model::AnnotationComponentType; use graphannis::update::{GraphUpdate, UpdateEvent}; use std::collections::HashSet; +use std::io::BufReader; use std::path::Path; use std::path::PathBuf; @@ -429,6 +431,39 @@ fn apply_update_ondisk(bench: &mut Criterion) { }); } +/// Benchmark a more complex graph with dominance and pointing relations. +fn apply_update_complex_graph(bench: &mut Criterion) { + if CORPUS_STORAGE.is_none() { + return; + } + + let cs = CORPUS_STORAGE.as_ref().unwrap(); + + bench.bench_function("apply_update_complex_graph", move |b| { + cs.create_empty_corpus("apply_update_test_corpus", false) + .unwrap(); + + let mut input = + BufReader::new(&include_bytes!("../../core/tests/single_sentence.graphml")[..]); + let mut node_updates = GraphUpdate::new(); + let mut edge_updates = GraphUpdate::new(); + let _config_str = graphannis_core::graph::serialization::graphml::read_graphml::< + AnnotationComponentType, + _, + _, + >(&mut input, &mut node_updates, &mut edge_updates, &|_| {}) + .unwrap(); + + b.iter(|| { + cs.apply_update("apply_update_test_corpus", &mut node_updates) + .unwrap(); + cs.apply_update("apply_update_test_corpus", &mut edge_updates) + .unwrap(); + }); + cs.delete("apply_update_test_corpus").unwrap(); + }); +} + criterion_group!(name=default; config= Criterion::default().sample_size(50); targets = deserialize_gum, find_first_ten_token_gum, @@ -441,5 +476,6 @@ criterion_group!(name=default; config= Criterion::default().sample_size(50); tar criterion_group!(name=apply_update; config= Criterion::default().sample_size(20); targets = apply_update_inmemory, apply_update_ondisk, + apply_update_complex_graph, ); criterion_main!(default, apply_update); diff --git a/graphannis/src/annis/db/aql/model.rs b/graphannis/src/annis/db/aql/model.rs index 4ad880787..a695deade 100644 --- a/graphannis/src/annis/db/aql/model.rs +++ b/graphannis/src/annis/db/aql/model.rs @@ -7,13 +7,13 @@ use facet::Facet; use graphannis_core::{ annostorage::ValueSearch, dfs::CycleSafeDFS, - errors::ComponentTypeError, + errors::{ComponentTypeError, Result}, graph::{ANNIS_NS, NODE_TYPE_KEY, storage::union::UnionEdgeContainer}, types::ComponentType, util::disk_collections::{DEFAULT_BLOCK_CACHE_CAPACITY, DiskMap, EvictionStrategy}, }; use std::{ - collections::{BTreeMap, HashMap}, + collections::{BTreeMap, HashMap, HashSet}, fmt, }; use transient_btree_index::BtreeConfig; @@ -393,6 +393,60 @@ impl AQLUpdateGraphIndex { Ok(None) } } + + /// Make sure all edges in named dominance components are in an unnamed dominance component. + fn add_empty_dominance_edges( + &mut self, + graph: &mut AnnotationGraph, + ) -> std::result::Result<(), ComponentTypeError> { + let default_dominance_component = AnnotationComponent::new( + AnnotationComponentType::Dominance, + ANNIS_NS.into(), + "".into(), + ); + + let existing_dominance_components: Vec<_> = + graph.get_all_components(Some(AnnotationComponentType::Dominance), None); + + let (unnamed_dominance_components, named_dominance_componts): (Vec<_>, Vec<_>) = + existing_dominance_components + .into_iter() + .filter(|c| c != &default_dominance_component) + .partition(|c| c.name.is_empty()); + + let named_dominance_gs: Vec<_> = named_dominance_componts + .into_iter() + .filter_map(|c| graph.get_graphstorage(&c)) + .collect(); + + let unnamed_dominance_gs: Vec<_> = unnamed_dominance_components + .into_iter() + .filter_map(|c| graph.get_graphstorage(&c)) + .collect(); + + let default_dominance_gs = graph.get_or_create_writable(&default_dominance_component)?; + for other_dominance_gs in named_dominance_gs { + for source_node in other_dominance_gs.source_nodes() { + let source = source_node?; + // The ">" operator will search all components that have an empty name. + // We don't need to add an edge if its already part of such a component. + let existing_unnamed_outgoing: Result> = unnamed_dominance_gs + .iter() + .flat_map(|gs| gs.get_outgoing_edges(source)) + .collect(); + let existing_unnamed_outgoing = + HashSet::::from_iter(existing_unnamed_outgoing?); + for target in other_dominance_gs.get_outgoing_edges(source) { + let target = target?; + if !existing_unnamed_outgoing.contains(&target) { + default_dominance_gs.add_edge(Edge { source, target })?; + } + } + } + } + + Ok(()) + } } impl ComponentType for AnnotationComponentType { @@ -578,6 +632,8 @@ impl ComponentType for AnnotationComponentType { } index.invalid_nodes.compact()?; + index.add_empty_dominance_edges(graph)?; + // Re-index the inherited coverage component. // To make this operation fast, we need to optimize the order component first let order_component = AnnotationComponent::new( diff --git a/graphannis/src/annis/db/aql/model/snapshots/graphannis__annis__db__aql__model__tests__add_dominance_edges_with_empty_name.snap b/graphannis/src/annis/db/aql/model/snapshots/graphannis__annis__db__aql__model__tests__add_dominance_edges_with_empty_name.snap new file mode 100644 index 000000000..561a64afb --- /dev/null +++ b/graphannis/src/annis/db/aql/model/snapshots/graphannis__annis__db__aql__model__tests__add_dominance_edges_with_empty_name.snap @@ -0,0 +1,198 @@ +--- +source: graphannis/src/annis/db/aql/model/tests.rs +expression: "String::from_utf8_lossy(&output)" +--- + + + + + + + + + + + + corpus + + + zossen + corpus + + + datasource + + + ROOT + syntax + node + + + S + syntax + node + + + NP + syntax + node + + + default_layer + node + ART + Die + + + default_layer + node + NN + Jugendlichen + + + + PP + syntax + node + + + default_layer + node + APPR + in + + + + default_layer + node + NE + Zossen + + + + default_layer + node + VMFIN + wollen + + + + NP + syntax + node + + + default_layer + node + ART + ein + + + + default_layer + node + NN + Musikcafé + + + + default_layer + node + $. + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/graphannis/src/annis/db/aql/model/tests.rs b/graphannis/src/annis/db/aql/model/tests.rs index 5373757f8..b84ba2b51 100644 --- a/graphannis/src/annis/db/aql/model/tests.rs +++ b/graphannis/src/annis/db/aql/model/tests.rs @@ -28,11 +28,11 @@ fn global_stats_token_count() { assert_eq!(true, graph.global_statistics.is_some()); let global_stats = graph.global_statistics.unwrap(); assert_eq!(true, global_stats.all_token_in_order_component); - assert_matches!(global_stats.corpus_size, + assert_matches!(global_stats.corpus_size, CorpusSize::Token { base_token_count, segmentation_count } if base_token_count == 16 && segmentation_count.len() == 2 - && *segmentation_count.get("diplomatic").unwrap() == 11 + && *segmentation_count.get("diplomatic").unwrap() == 11 && *segmentation_count.get("norm").unwrap() == 13); } @@ -345,3 +345,24 @@ fn add_token_to_single_sentence() { .unwrap(); assert_snapshot!(String::from_utf8_lossy(&output)); } + +#[test] +fn add_dominance_edges_with_empty_name() { + let content = + &include_bytes!("../../../../../tests/single_sentence_only_named_edges.graphml")[..]; + let (graph, _config) = + graphml::import::(content, false, |_| {}).unwrap(); + + let mut output = Vec::::default(); + graphannis_core::graph::serialization::graphml::export_stable_order( + &graph, + None, + &mut output, + |_| {}, + ) + .unwrap(); + + // The output has changed, because new edges have been added. + assert_ne!(content, output); + assert_snapshot!(String::from_utf8_lossy(&output)); +} diff --git a/graphannis/src/annis/db/aql/operators/arity.rs b/graphannis/src/annis/db/aql/operators/arity.rs index f935712ae..0e7f5b3b2 100644 --- a/graphannis/src/annis/db/aql/operators/arity.rs +++ b/graphannis/src/annis/db/aql/operators/arity.rs @@ -36,12 +36,16 @@ impl UnaryOperatorSpec for AritySpec { let mut graphstorages = Vec::default(); for component in db.get_all_components(Some(AnnotationComponentType::Dominance), None) { - if let Some(gs) = db.get_graphstorage(&component) { + if let Some(gs) = db.get_graphstorage(&component) + && gs.get_statistics().is_none_or(|stats| stats.nodes > 0) + { graphstorages.push(gs); } } for component in db.get_all_components(Some(AnnotationComponentType::Pointing), None) { - if let Some(gs) = db.get_graphstorage(&component) { + if let Some(gs) = db.get_graphstorage(&component) + && gs.get_statistics().is_none_or(|stats| stats.nodes > 0) + { graphstorages.push(gs); } } diff --git a/graphannis/src/annis/db/aql/operators/edge_op.rs b/graphannis/src/annis/db/aql/operators/edge_op.rs index 11a0969c4..f04c35765 100644 --- a/graphannis/src/annis/db/aql/operators/edge_op.rs +++ b/graphannis/src/annis/db/aql/operators/edge_op.rs @@ -43,7 +43,12 @@ impl BaseEdgeOp { GraphAnnisError::ImpossibleSearch(format!("Component {} does not exist", c)) })?; - gs.push(gs_for_component); + if gs_for_component + .get_statistics() + .is_none_or(|stats| stats.nodes > 0) + { + gs.push(gs_for_component); + } } let max_nodes_estimate = calculate_max_node_estimate(db, &spec, &gs, false)?; diff --git a/graphannis/tests/single_sentence_only_named_edges.graphml b/graphannis/tests/single_sentence_only_named_edges.graphml new file mode 100644 index 000000000..2eda62789 --- /dev/null +++ b/graphannis/tests/single_sentence_only_named_edges.graphml @@ -0,0 +1,205 @@ + + + + + + + + + + + + corpus + + + zossen + corpus + + + datasource + + + ROOT + syntax + node + + + S + syntax + node + + + NP + syntax + node + + + default_layer + node + ART + Die + + + default_layer + node + NN + Jugendlichen + + + + PP + syntax + node + + + default_layer + node + APPR + in + + + + default_layer + node + NE + Zossen + + + + default_layer + node + VMFIN + wollen + + + + NP + syntax + node + + + default_layer + node + ART + ein + + + + default_layer + node + NN + Musikcafé + + + + default_layer + node + $. + . + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +