From 993bc9851d1988ae9766f6303ac6df36bdd888f3 Mon Sep 17 00:00:00 2001 From: amikumar91 Date: Wed, 29 Jul 2026 22:27:52 +0530 Subject: [PATCH 1/5] Add RibbonFilter core: banding, back-substitution, and query Implements the standard (non-homogeneous, w=64) Ribbon filter construction and membership query described in issue #33, following Xor8's structure (hash-derive, build-with-retries, query-by-recombine). --- .../org/fastfilter/ribbon/RibbonFilter.java | 144 ++++++++++++++++++ .../fastfilter/ribbon/RibbonFilterTest.java | 52 +++++++ 2 files changed, 196 insertions(+) create mode 100644 fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java create mode 100644 fastfilter/src/test/java/org/fastfilter/ribbon/RibbonFilterTest.java diff --git a/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java new file mode 100644 index 0000000..138fe3d --- /dev/null +++ b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java @@ -0,0 +1,144 @@ +package org.fastfilter.ribbon; + +import org.fastfilter.Filter; +import org.fastfilter.utils.Hash; + +/** + * A (non-homogeneous) Ribbon filter with a fixed ribbon width w = 64. + * + * A Ribbon filter is a retrieval data structure: for every key x in the + * set, solving a banded linear system over GF(2) yields a small function + * f(x) that returns an r-bit fingerprint. Construction can fail (linear + * dependence among the equations) and is retried with a new seed, and - + * if all seed attempts at a given slot count fail - with more slots. + * + */ +public class RibbonFilter implements Filter { + + private static final int DEFAULT_RESULT_BITS = 8; + private static final int COEFF_BITS = 64; + private static final int MAX_ATTEMPTS_PER_SIZE = 256; + private static final double INITIAL_OVERHEAD_FACTOR = 0.10; + private static final double OVERHEAD_GROWTH_FACTOR = 1.5; + + private final int size; + private final int resultBits; + private final int numSlots; + private final int numStarts; + private final long seed; + private final byte[] solution; + private final int bitCount; + + private RibbonFilter(int size, int resultBits, int numSlots, int numStarts, long seed, byte[] solution) { + this.size = size; + this.resultBits = resultBits; + this.numSlots = numSlots; + this.numStarts = numStarts; + this.seed = seed; + this.solution = solution; + this.bitCount = numSlots * resultBits; + } + + public static RibbonFilter construct(long[] keys) { + return construct(keys, DEFAULT_RESULT_BITS); + } + + public static RibbonFilter construct(long[] keys, int resultBits) { + if (resultBits < 1 || resultBits > 8) { + throw new IllegalArgumentException("resultBits must be between 1 and 8"); + } + int size = keys.length; + double overheadFactor = INITIAL_OVERHEAD_FACTOR; + while (true) { + int numStarts = (int) Math.ceil(size * (1 + overheadFactor)); + if (numStarts < 1) { + numStarts = 1; + } + int numSlots = numStarts + COEFF_BITS - 1; + for (int attempt = 0; attempt < MAX_ATTEMPTS_PER_SIZE; attempt++) { + long seed = Hash.randomSeed(); + long[] coeff = new long[numSlots]; + byte[] result = new byte[numSlots]; + if (band(keys, seed, numStarts, resultBits, coeff, result)) { + byte[] solution = backSubstitute(coeff, result, numSlots, resultBits); + return new RibbonFilter(size, resultBits, numSlots, numStarts, seed, solution); + } + } + overheadFactor *= OVERHEAD_GROWTH_FACTOR; + } + } + + private static boolean band(long[] keys, long seed, int numStarts, int resultBits, long[] coeff, byte[] result) { + int mask = (1 << resultBits) - 1; + for (long key : keys) { + long hash = Hash.hash64(key, seed); + int s = Hash.reduce((int) hash, numStarts); + long c = Long.rotateLeft(hash, 21) | 1L; + int r = (int) (Long.rotateLeft(hash, 42) & mask); + while (true) { + int i = Long.numberOfTrailingZeros(c); + int p = s + i; + c >>>= i; + if (coeff[p] == 0) { + coeff[p] = c; + result[p] = (byte) r; + break; + } + c ^= coeff[p]; + r ^= result[p]; + s = p; + if (c == 0) { + return false; + } + } + } + return true; + } + + private static byte[] backSubstitute(long[] coeff, byte[] result, int numSlots, int resultBits) { + byte[] solution = new byte[numSlots]; + int mask = (1 << resultBits) - 1; + for (int i = numSlots - 1; i >= 0; i--) { + long c = coeff[i]; + if (c == 0) { + continue; + } + int contribution = 0; + long rest = c & ~1L; + while (rest != 0) { + int k = Long.numberOfTrailingZeros(rest); + rest &= rest - 1; + contribution ^= solution[i + k]; + } + solution[i] = (byte) ((result[i] ^ contribution) & mask); + } + return solution; + } + + @Override + public boolean mayContain(long key) { + long hash = Hash.hash64(key, seed); + int s = Hash.reduce((int) hash, numStarts); + long c = Long.rotateLeft(hash, 21) | 1L; + int mask = (1 << resultBits) - 1; + int expected = (int) (Long.rotateLeft(hash, 42) & mask); + + int actual = 0; + for (int j = 0; j < resultBits; j++) { + long columnBits = 0; + for (int k = 0; k < COEFF_BITS; k++) { + if (((solution[s + k] >>> j) & 1) != 0) { + columnBits |= (1L << k); + } + } + int bit = Long.bitCount(columnBits & c) & 1; + actual |= bit << j; + } + return actual == expected; + } + + @Override + public long getBitCount() { + return bitCount; + } +} diff --git a/fastfilter/src/test/java/org/fastfilter/ribbon/RibbonFilterTest.java b/fastfilter/src/test/java/org/fastfilter/ribbon/RibbonFilterTest.java new file mode 100644 index 0000000..a150aba --- /dev/null +++ b/fastfilter/src/test/java/org/fastfilter/ribbon/RibbonFilterTest.java @@ -0,0 +1,52 @@ +package org.fastfilter.ribbon; + +import static org.junit.Assert.assertTrue; + +import org.fastfilter.utils.RandomGenerator; +import org.junit.Test; + +public class RibbonFilterTest { + + @Test + public void noFalseNegativesSmall() { + assertNoFalseNegatives(10); + assertNoFalseNegatives(1_000); + } + + @Test + public void noFalseNegativesLarge() { + assertNoFalseNegatives(1_000_000); + } + + @Test + public void falsePositiveRateNearExpected() { + int len = 1_000_000; + long[] list = new long[len * 2]; + RandomGenerator.createRandomUniqueListFast(list, 0); + long[] keys = new long[len]; + long[] nonKeys = new long[len]; + for (int i = 0; i < len; i++) { + keys[i] = list[i]; + nonKeys[i] = list[i + len]; + } + RibbonFilter filter = RibbonFilter.construct(keys); + int falsePositives = 0; + for (long nonKey : nonKeys) { + if (filter.mayContain(nonKey)) { + falsePositives++; + } + } + double fpp = (double) falsePositives / len; + // expected ~= 1/256 ~= 0.0039; generous margin to avoid flakiness + assertTrue("fpp too high: " + fpp, fpp < 0.01); + } + + private void assertNoFalseNegatives(int len) { + long[] keys = new long[len]; + RandomGenerator.createRandomUniqueListFast(keys, 0); + RibbonFilter filter = RibbonFilter.construct(keys); + for (long key : keys) { + assertTrue("key " + key + " should be present", filter.mayContain(key)); + } + } +} From f774f0f161a111e3fc960f35f77bb1beeba419dc Mon Sep 17 00:00:00 2001 From: amikumar91 Date: Wed, 29 Jul 2026 22:33:00 +0530 Subject: [PATCH 2/5] Add RibbonFilter serialization and round-trip test coverage Mirrors Xor8's ByteBuffer/OutputStream serialize + static deserialize convention. Unlike Xor8, numSlots/numStarts must be stored explicitly (not re-derived from size) since the construction retry loop can grow the slot count beyond the simple sizing formula. --- .../org/fastfilter/ribbon/RibbonFilter.java | 69 +++++++++++++++++++ .../org/fastfilter/xor/SerializationTest.java | 6 +- 2 files changed, 74 insertions(+), 1 deletion(-) diff --git a/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java index 138fe3d..5ec9a8f 100644 --- a/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java +++ b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java @@ -1,5 +1,12 @@ package org.fastfilter.ribbon; +import java.io.DataInputStream; +import java.io.DataOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.ByteBuffer; + import org.fastfilter.Filter; import org.fastfilter.utils.Hash; @@ -141,4 +148,66 @@ public boolean mayContain(long key) { public long getBitCount() { return bitCount; } + + @Override + public int getSerializedSize() { + return Integer.BYTES * 5 + Long.BYTES + solution.length * Byte.BYTES; + } + + @Override + public void serialize(ByteBuffer buffer) { + if (buffer.remaining() < getSerializedSize()) { + throw new IllegalArgumentException("Buffer too small"); + } + buffer.putInt(size); + buffer.putInt(resultBits); + buffer.putInt(numSlots); + buffer.putInt(numStarts); + buffer.putLong(seed); + buffer.putInt(solution.length); + buffer.put(solution); + } + + public static RibbonFilter deserialize(ByteBuffer buffer) { + if (buffer.remaining() < Integer.BYTES * 5 + Long.BYTES) { + throw new IllegalArgumentException("Buffer too small"); + } + final int size = buffer.getInt(); + final int resultBits = buffer.getInt(); + final int numSlots = buffer.getInt(); + final int numStarts = buffer.getInt(); + final long seed = buffer.getLong(); + final int len = buffer.getInt(); + if (buffer.remaining() < len * Byte.BYTES) { + throw new IllegalArgumentException("Buffer too small"); + } + final byte[] solution = new byte[len]; + buffer.get(solution); + return new RibbonFilter(size, resultBits, numSlots, numStarts, seed, solution); + } + + @Override + public void serialize(OutputStream out) throws IOException { + DataOutputStream dout = new DataOutputStream(out); + dout.writeInt(size); + dout.writeInt(resultBits); + dout.writeInt(numSlots); + dout.writeInt(numStarts); + dout.writeLong(seed); + dout.writeInt(solution.length); + dout.write(solution); + } + + public static RibbonFilter deserialize(InputStream in) throws IOException { + DataInputStream din = new DataInputStream(in); + final int size = din.readInt(); + final int resultBits = din.readInt(); + final int numSlots = din.readInt(); + final int numStarts = din.readInt(); + final long seed = din.readLong(); + final int len = din.readInt(); + final byte[] solution = new byte[len]; + din.readFully(solution); + return new RibbonFilter(size, resultBits, numSlots, numStarts, seed, solution); + } } diff --git a/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java b/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java index 023475d..b2435ec 100644 --- a/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java +++ b/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.function.Function; import org.fastfilter.Filter; +import org.fastfilter.ribbon.RibbonFilter; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -53,7 +54,10 @@ public static List filters() { (StreamDeserializer) XorBinaryFuse16::deserialize}, new Object[] {"XorBinaryFuse32", (Function) XorBinaryFuse32::construct, (Function) XorBinaryFuse32::deserialize, - (StreamDeserializer) XorBinaryFuse32::deserialize} + (StreamDeserializer) XorBinaryFuse32::deserialize}, + new Object[] {"RibbonFilter", (Function) RibbonFilter::construct, + (Function) RibbonFilter::deserialize, + (StreamDeserializer) RibbonFilter::deserialize} ); } From e13c35d3647a01fdcfb56cb28d699e346897e1cb Mon Sep 17 00:00:00 2001 From: amikumar91 Date: Wed, 29 Jul 2026 23:22:11 +0530 Subject: [PATCH 3/5] Register RibbonFilter as FilterType.RIBBON / TestFilterType.RIBBON Setting is ignored the same way XOR_8/XOR_16/XOR_PLUS_8 ignore it here: TestAllFilters.test() hardcodes setting=10 for every TestFilterType, which would exceed RibbonFilter's 8-bit (byte-storage) ceiling if passed through. --- fastfilter/src/main/java/org/fastfilter/FilterType.java | 8 ++++++++ .../src/test/java/org/fastfilter/TestFilterType.java | 7 +++++++ 2 files changed, 15 insertions(+) diff --git a/fastfilter/src/main/java/org/fastfilter/FilterType.java b/fastfilter/src/main/java/org/fastfilter/FilterType.java index ac69e1a..ca837f7 100644 --- a/fastfilter/src/main/java/org/fastfilter/FilterType.java +++ b/fastfilter/src/main/java/org/fastfilter/FilterType.java @@ -87,6 +87,14 @@ public Filter construct(long[] keys, int setting) { return XorPlus8.construct(keys); } }, + RIBBON { + @Override + public Filter construct(long[] keys, int setting) { + // Fixed-width filter (like XOR_8/XOR_16/XOR_PLUS_8 above): setting + // is ignored, matching the r=8 default used for the Xor8 comparison. + return org.fastfilter.ribbon.RibbonFilter.construct(keys); + } + }, CUCKOO_8 { @Override public Filter construct(long[] keys, int setting) { diff --git a/fastfilter/src/test/java/org/fastfilter/TestFilterType.java b/fastfilter/src/test/java/org/fastfilter/TestFilterType.java index bf3b172..125842b 100644 --- a/fastfilter/src/test/java/org/fastfilter/TestFilterType.java +++ b/fastfilter/src/test/java/org/fastfilter/TestFilterType.java @@ -11,6 +11,7 @@ import org.fastfilter.gcs.GolombCompressedSet; import org.fastfilter.gcs.GolombCompressedSet2; import org.fastfilter.mphf.MPHFilter; +import org.fastfilter.ribbon.RibbonFilter; import org.fastfilter.xor.*; import org.fastfilter.xorplus.XorPlus8; @@ -90,6 +91,12 @@ public Filter construct(long[] keys, int setting) { return XorBinaryFuse32.construct(keys); } }, + RIBBON { + @Override + public Filter construct(long[] keys, int setting) { + return RibbonFilter.construct(keys); + } + }, CUCKOO_8 { @Override public Filter construct(long[] keys, int setting) { From 48c063ecee8035f316a8fb0af9d4accfa5e3ce31 Mon Sep 17 00:00:00 2001 From: amikumar91 Date: Thu, 30 Jul 2026 23:30:18 +0530 Subject: [PATCH 4/5] Rename RibbonFilter to RibbonNaiveFilter, RIBBON to RIBBON_NAIVE Reserves the RibbonFilter/RIBBON name for a future ICML-backed implementation with fast queries; this one keeps the naive O(w*r) per-column query, which is 70-100x slower than the Xor filters (see PR #54 benchmark). --- .../main/java/org/fastfilter/FilterType.java | 7 +++---- ...ibbonFilter.java => RibbonNaiveFilter.java} | 18 +++++++++--------- .../java/org/fastfilter/TestFilterType.java | 6 +++--- ...terTest.java => RibbonNaiveFilterTest.java} | 6 +++--- .../org/fastfilter/xor/SerializationTest.java | 8 ++++---- 5 files changed, 22 insertions(+), 23 deletions(-) rename fastfilter/src/main/java/org/fastfilter/ribbon/{RibbonFilter.java => RibbonNaiveFilter.java} (90%) rename fastfilter/src/test/java/org/fastfilter/ribbon/{RibbonFilterTest.java => RibbonNaiveFilterTest.java} (88%) diff --git a/fastfilter/src/main/java/org/fastfilter/FilterType.java b/fastfilter/src/main/java/org/fastfilter/FilterType.java index ca837f7..22ac83b 100644 --- a/fastfilter/src/main/java/org/fastfilter/FilterType.java +++ b/fastfilter/src/main/java/org/fastfilter/FilterType.java @@ -8,6 +8,7 @@ import org.fastfilter.cuckoo.CuckooPlus16; import org.fastfilter.cuckoo.CuckooPlus8; import org.fastfilter.gcs.GolombCompressedSet; +import org.fastfilter.ribbon.RibbonNaiveFilter; import org.fastfilter.xor.*; import org.fastfilter.xorplus.XorPlus8; @@ -87,12 +88,10 @@ public Filter construct(long[] keys, int setting) { return XorPlus8.construct(keys); } }, - RIBBON { + RIBBON_NAIVE { @Override public Filter construct(long[] keys, int setting) { - // Fixed-width filter (like XOR_8/XOR_16/XOR_PLUS_8 above): setting - // is ignored, matching the r=8 default used for the Xor8 comparison. - return org.fastfilter.ribbon.RibbonFilter.construct(keys); + return RibbonNaiveFilter.construct(keys); } }, CUCKOO_8 { diff --git a/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonNaiveFilter.java similarity index 90% rename from fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java rename to fastfilter/src/main/java/org/fastfilter/ribbon/RibbonNaiveFilter.java index 5ec9a8f..deac226 100644 --- a/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonFilter.java +++ b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonNaiveFilter.java @@ -20,7 +20,7 @@ * if all seed attempts at a given slot count fail - with more slots. * */ -public class RibbonFilter implements Filter { +public class RibbonNaiveFilter implements Filter { private static final int DEFAULT_RESULT_BITS = 8; private static final int COEFF_BITS = 64; @@ -36,7 +36,7 @@ public class RibbonFilter implements Filter { private final byte[] solution; private final int bitCount; - private RibbonFilter(int size, int resultBits, int numSlots, int numStarts, long seed, byte[] solution) { + private RibbonNaiveFilter(int size, int resultBits, int numSlots, int numStarts, long seed, byte[] solution) { this.size = size; this.resultBits = resultBits; this.numSlots = numSlots; @@ -46,11 +46,11 @@ private RibbonFilter(int size, int resultBits, int numSlots, int numStarts, long this.bitCount = numSlots * resultBits; } - public static RibbonFilter construct(long[] keys) { + public static RibbonNaiveFilter construct(long[] keys) { return construct(keys, DEFAULT_RESULT_BITS); } - public static RibbonFilter construct(long[] keys, int resultBits) { + public static RibbonNaiveFilter construct(long[] keys, int resultBits) { if (resultBits < 1 || resultBits > 8) { throw new IllegalArgumentException("resultBits must be between 1 and 8"); } @@ -68,7 +68,7 @@ public static RibbonFilter construct(long[] keys, int resultBits) { byte[] result = new byte[numSlots]; if (band(keys, seed, numStarts, resultBits, coeff, result)) { byte[] solution = backSubstitute(coeff, result, numSlots, resultBits); - return new RibbonFilter(size, resultBits, numSlots, numStarts, seed, solution); + return new RibbonNaiveFilter(size, resultBits, numSlots, numStarts, seed, solution); } } overheadFactor *= OVERHEAD_GROWTH_FACTOR; @@ -168,7 +168,7 @@ public void serialize(ByteBuffer buffer) { buffer.put(solution); } - public static RibbonFilter deserialize(ByteBuffer buffer) { + public static RibbonNaiveFilter deserialize(ByteBuffer buffer) { if (buffer.remaining() < Integer.BYTES * 5 + Long.BYTES) { throw new IllegalArgumentException("Buffer too small"); } @@ -183,7 +183,7 @@ public static RibbonFilter deserialize(ByteBuffer buffer) { } final byte[] solution = new byte[len]; buffer.get(solution); - return new RibbonFilter(size, resultBits, numSlots, numStarts, seed, solution); + return new RibbonNaiveFilter(size, resultBits, numSlots, numStarts, seed, solution); } @Override @@ -198,7 +198,7 @@ public void serialize(OutputStream out) throws IOException { dout.write(solution); } - public static RibbonFilter deserialize(InputStream in) throws IOException { + public static RibbonNaiveFilter deserialize(InputStream in) throws IOException { DataInputStream din = new DataInputStream(in); final int size = din.readInt(); final int resultBits = din.readInt(); @@ -208,6 +208,6 @@ public static RibbonFilter deserialize(InputStream in) throws IOException { final int len = din.readInt(); final byte[] solution = new byte[len]; din.readFully(solution); - return new RibbonFilter(size, resultBits, numSlots, numStarts, seed, solution); + return new RibbonNaiveFilter(size, resultBits, numSlots, numStarts, seed, solution); } } diff --git a/fastfilter/src/test/java/org/fastfilter/TestFilterType.java b/fastfilter/src/test/java/org/fastfilter/TestFilterType.java index 125842b..5a2fb6b 100644 --- a/fastfilter/src/test/java/org/fastfilter/TestFilterType.java +++ b/fastfilter/src/test/java/org/fastfilter/TestFilterType.java @@ -11,7 +11,7 @@ import org.fastfilter.gcs.GolombCompressedSet; import org.fastfilter.gcs.GolombCompressedSet2; import org.fastfilter.mphf.MPHFilter; -import org.fastfilter.ribbon.RibbonFilter; +import org.fastfilter.ribbon.RibbonNaiveFilter; import org.fastfilter.xor.*; import org.fastfilter.xorplus.XorPlus8; @@ -91,10 +91,10 @@ public Filter construct(long[] keys, int setting) { return XorBinaryFuse32.construct(keys); } }, - RIBBON { + RIBBON_NAIVE { @Override public Filter construct(long[] keys, int setting) { - return RibbonFilter.construct(keys); + return RibbonNaiveFilter.construct(keys); } }, CUCKOO_8 { diff --git a/fastfilter/src/test/java/org/fastfilter/ribbon/RibbonFilterTest.java b/fastfilter/src/test/java/org/fastfilter/ribbon/RibbonNaiveFilterTest.java similarity index 88% rename from fastfilter/src/test/java/org/fastfilter/ribbon/RibbonFilterTest.java rename to fastfilter/src/test/java/org/fastfilter/ribbon/RibbonNaiveFilterTest.java index a150aba..6b1b9ad 100644 --- a/fastfilter/src/test/java/org/fastfilter/ribbon/RibbonFilterTest.java +++ b/fastfilter/src/test/java/org/fastfilter/ribbon/RibbonNaiveFilterTest.java @@ -5,7 +5,7 @@ import org.fastfilter.utils.RandomGenerator; import org.junit.Test; -public class RibbonFilterTest { +public class RibbonNaiveFilterTest { @Test public void noFalseNegativesSmall() { @@ -29,7 +29,7 @@ public void falsePositiveRateNearExpected() { keys[i] = list[i]; nonKeys[i] = list[i + len]; } - RibbonFilter filter = RibbonFilter.construct(keys); + RibbonNaiveFilter filter = RibbonNaiveFilter.construct(keys); int falsePositives = 0; for (long nonKey : nonKeys) { if (filter.mayContain(nonKey)) { @@ -44,7 +44,7 @@ public void falsePositiveRateNearExpected() { private void assertNoFalseNegatives(int len) { long[] keys = new long[len]; RandomGenerator.createRandomUniqueListFast(keys, 0); - RibbonFilter filter = RibbonFilter.construct(keys); + RibbonNaiveFilter filter = RibbonNaiveFilter.construct(keys); for (long key : keys) { assertTrue("key " + key + " should be present", filter.mayContain(key)); } diff --git a/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java b/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java index b2435ec..2ce1d4e 100644 --- a/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java +++ b/fastfilter/src/test/java/org/fastfilter/xor/SerializationTest.java @@ -13,7 +13,7 @@ import java.util.List; import java.util.function.Function; import org.fastfilter.Filter; -import org.fastfilter.ribbon.RibbonFilter; +import org.fastfilter.ribbon.RibbonNaiveFilter; import org.junit.Test; import org.junit.runner.RunWith; import org.junit.runners.Parameterized; @@ -55,9 +55,9 @@ public static List filters() { new Object[] {"XorBinaryFuse32", (Function) XorBinaryFuse32::construct, (Function) XorBinaryFuse32::deserialize, (StreamDeserializer) XorBinaryFuse32::deserialize}, - new Object[] {"RibbonFilter", (Function) RibbonFilter::construct, - (Function) RibbonFilter::deserialize, - (StreamDeserializer) RibbonFilter::deserialize} + new Object[] {"RibbonNaiveFilter", (Function) RibbonNaiveFilter::construct, + (Function) RibbonNaiveFilter::deserialize, + (StreamDeserializer) RibbonNaiveFilter::deserialize} ); } From 26baaf9d799acfe358d38a8c45c9d60798cbc19d Mon Sep 17 00:00:00 2001 From: amikumar91 Date: Thu, 30 Jul 2026 23:30:36 +0530 Subject: [PATCH 5/5] Add class-level Javadoc citing the Ribbon filter papers --- .../org/fastfilter/ribbon/RibbonNaiveFilter.java | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonNaiveFilter.java b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonNaiveFilter.java index deac226..c290a81 100644 --- a/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonNaiveFilter.java +++ b/fastfilter/src/main/java/org/fastfilter/ribbon/RibbonNaiveFilter.java @@ -19,6 +19,20 @@ * dependence among the equations) and is retried with a new seed, and - * if all seed attempts at a given slot count fail - with more slots. * + * Martin Dietzfelbinger, Stefan Walzer, [Efficient Gauss Elimination for + * Near-Quadratic Matrices with One Short Random Block per + * Row](https://arxiv.org/abs/1907.04750), ESA 2019. + * + * Peter C. Dillinger, Stefan Walzer, [Ribbon filter: practically smaller + * than Bloom and Xor](https://arxiv.org/abs/2103.02515), arXiv:2103.02515, + * 2021. + * + * This is the "naive" query implementation: each lookup does an O(w * r) + * per-column dot product against the solved band, rather than the + * Interleaved Column-Major Layout (ICML) the papers above use for fast + * queries. It is kept under this name so a future ICML-backed + * implementation can use the plain RibbonFilter / RIBBON name once it + * exists. */ public class RibbonNaiveFilter implements Filter {