Preserve negation of a single operand in a VC (fixes #178)
Change-Id: I894618d6fa610804949394580905c51fde8535fc
diff --git a/Changes b/Changes
index a522f7a..7ad8452 100644
--- a/Changes
+++ b/Changes
@@ -1,4 +1,4 @@
-0.64.7 2026-04-28
+0.64.7 2026-04-29
- [bugfix] Keep highlights that extend beyond a cut match
(diewald; fixes #177; diewald; AI-assisted Claude Opus 4.6)
- [bugfix] Correctly handle foundry and layer in attribute groups
@@ -10,6 +10,8 @@
(fixes #121; diewald; AI-assisted Claude Opus 4.6)
- [bugfix] Fix regex alternation in non-enclosed groups
(fixes #243; diewald; AI-assisted Claude Opus 4.6)
+ - [bugfix] Preserve negation of a single operand inside a
+ collection group (fixes #178; diewald; AI-assisted Claude Opus 4.6))
0.64.6 2026-03-09
- [performance] Add leaf cache. (diewald)
diff --git a/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java b/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java
index 4464302..527557e 100644
--- a/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java
+++ b/src/main/java/de/ids_mannheim/korap/collection/CollectionBuilder.java
@@ -382,7 +382,11 @@
if (this.operands == null || this.operands.isEmpty())
return null;
- if (this.operands.size() == 1)
+ // Shortcut for a single positive operand. A negative
+ // operand must go through the BooleanGroupFilter so that
+ // its negation is preserved.
+ if (this.operands.size() == 1
+ && !this.operands.get(0).isNegative())
return this.operands.get(0).toFilter();
// BooleanFilter bool = new BooleanFilter();
diff --git a/src/test/java/de/ids_mannheim/korap/collection/TestCollectionBuilder.java b/src/test/java/de/ids_mannheim/korap/collection/TestCollectionBuilder.java
index c9d47ea..6e00612 100644
--- a/src/test/java/de/ids_mannheim/korap/collection/TestCollectionBuilder.java
+++ b/src/test/java/de/ids_mannheim/korap/collection/TestCollectionBuilder.java
@@ -220,14 +220,18 @@
assertEquals("author:tree", kbi.toString());
assertTrue(kbi.isNegative());
+ // A negative single operand inside a group must keep its
+ // negation. The outer .not() then negates
+ // again, semantically yielding a positive `author:tree`,
+ // expressed as a negated group around a negative operand.
kbi = kc.andGroup().with(kc.term("author", "tree").not());
kbi.not();
- assertEquals("author:tree", kbi.toString());
+ assertEquals("AndGroup(-author:tree)", kbi.toString());
assertTrue(kbi.isNegative());
kbi = kc.orGroup().with(kc.term("author", "tree").not());
kbi.not();
- assertEquals("author:tree", kbi.toString());
+ assertEquals("OrGroup(-author:tree)", kbi.toString());
assertTrue(kbi.isNegative());
};
diff --git a/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java b/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java
index 5877547..8b5fcef 100644
--- a/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java
+++ b/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionIndex.java
@@ -552,6 +552,80 @@
@Test
+ public void testIndexWithNegativeYearDateRangeIssue178 () throws IOException {
+ ki = new KrillIndex();
+ ki.addDoc(createDoc1());
+ ki.addDoc(createDoc2());
+ ki.addDoc(createDoc3());
+ ki.commit();
+ CollectionBuilder cb = new CollectionBuilder();
+ KrillCollection kcn = new KrillCollection(ki);
+
+ // All three docs are in 2005, so positive match returns 3
+ kcn.fromBuilder(cb.date("pubDate", "2005"));
+ assertEquals(3, kcn.docCount());
+
+ // Negation must exclude all docs in 2005
+ kcn.fromBuilder(cb.date("pubDate", "2005").not());
+ assertEquals(0, kcn.docCount());
+
+ // Negation of a year that does not contain any doc keeps all docs
+ kcn.fromBuilder(cb.date("pubDate", "2006").not());
+ assertEquals(3, kcn.docCount());
+
+ // Month check
+ kcn.fromBuilder(cb.date("pubDate", "2005-12"));
+ assertEquals(3, kcn.docCount());
+
+ // Negation of a month
+ kcn.fromBuilder(cb.date("pubDate", "2005-12").not());
+ assertEquals(0, kcn.docCount());
+
+ // Day check
+ kcn.fromBuilder(cb.date("pubDate", "2005-12-10"));
+ assertEquals(1, kcn.docCount());
+
+ // Negation of a day
+ kcn.fromBuilder(cb.date("pubDate", "2005-12-10").not());
+ assertEquals(2, kcn.docCount());
+
+
+ // Negation via the JSON KoralQuery path
+ String json = "{\"collection\":{\"@type\":\"koral:doc\","
+ + "\"key\":\"pubDate\",\"type\":\"type:date\","
+ + "\"value\":\"2005\",\"match\":\"match:ne\"}}";
+ KrillCollection kc = new KrillCollection(json);
+ kc.setIndex(ki);
+ assertEquals(0, kc.docCount());
+
+ // Negation as a single-operand docGroup (this is the actual
+ // problematic case described in issue #178).
+ String groupJson = "{\"collection\":{\"@type\":\"koral:docGroup\","
+ + "\"operation\":\"operation:and\",\"operands\":[{"
+ + "\"@type\":\"koral:doc\",\"key\":\"pubDate\","
+ + "\"type\":\"type:date\",\"value\":\"2005\","
+ + "\"match\":\"match:ne\"}]}}";
+ KrillCollection kcGroup = new KrillCollection(groupJson);
+ kcGroup.setIndex(ki);
+ assertEquals(0, kcGroup.docCount());
+
+ // Negation combined with another (positive) operand in an
+ // AndGroup must still exclude matching dates.
+ String mixJson = "{\"collection\":{\"@type\":\"koral:docGroup\","
+ + "\"operation\":\"operation:and\",\"operands\":[{"
+ + "\"@type\":\"koral:doc\",\"key\":\"author\","
+ + "\"type\":\"type:string\",\"value\":\"Frank\","
+ + "\"match\":\"match:eq\"},{"
+ + "\"@type\":\"koral:doc\",\"key\":\"pubDate\","
+ + "\"type\":\"type:date\",\"value\":\"2005\","
+ + "\"match\":\"match:ne\"}]}}";
+ KrillCollection kcMix = new KrillCollection(mixJson);
+ kcMix.setIndex(ki);
+ assertEquals(0, kcMix.docCount());
+ };
+
+
+ @Test
public void testIndexWithRegexes () throws IOException {
ki = new KrillIndex();
diff --git a/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionJSON.java b/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionJSON.java
index 9804ac5..4846a43 100644
--- a/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionJSON.java
+++ b/src/test/java/de/ids_mannheim/korap/collection/TestKrillCollectionJSON.java
@@ -101,6 +101,17 @@
};
@Test
+ public void collectionWithNegativeDateYear () {
+ // pubDate ne 2017 must serialize to a Lucene query that excludes
+ // documents whose pubDate is in 2017.
+ String json = "{\"collection\":{\"@type\":\"koral:doc\","
+ + "\"key\":\"pubDate\",\"type\":\"type:date\","
+ + "\"value\":\"2017\",\"match\":\"match:ne\"}}";
+ KrillCollection kc = new KrillCollection(json);
+ assertEquals("-pubDate:[20170000 TO 20179999]", kc.toString());
+ };
+
+ @Test
public void collectionWithMultipleNe () {
String metaQuery = _getJSONString("collection_multine.jsonld");
KrillCollection kc = new KrillCollection(metaQuery);