@@ -120,6 +120,55 @@ impl<'a, C: GeoConfig<Diff>> GeoDiffCount<'a, C> {
120120 result
121121 }
122122
123+ /// Constructs a [`GeoDiffCount`] directly from an iterator of item hashes.
124+ ///
125+ /// Inserting hashes one by one via [`Count::push_hash`] repeatedly toggles individual bits
126+ /// and rebalances the sparse/dense split, which is wasteful when the hashes are known up
127+ /// front. Instead, this function uses the (exact) number of hashes to estimate the split
128+ /// point between the sparsely stored most-significant buckets (the "numbers") and the densely
129+ /// stored least-significant buckets (the "bits"):
130+ ///
131+ /// * Buckets at or above the estimated split point are collected, sorted, and reduced
132+ /// modulo two so that buckets occurring an even number of times cancel out. This matches
133+ /// the symmetric-difference semantics of [`Diff`] and only sorts the comparatively few
134+ /// most-significant buckets.
135+ /// * Buckets below the split point are folded directly into a dense bit vector, where
136+ /// repeated buckets cancel out as well.
137+ ///
138+ /// Both pieces are then combined into a single descending stream of one bits and handed to
139+ /// [`Self::from_bit_chunks`], which re-establishes the filter invariants. The resulting filter
140+ /// is identical to the one produced by pushing the same hashes one by one.
141+ pub fn from_hashes ( config : C , hashes : impl ExactSizeIterator < Item = u64 > ) -> Self {
142+ let split = estimate_split_bucket ( & config, hashes. len ( ) ) ;
143+
144+ // Buckets >= split are sparse and collected for sorting; buckets < split are dense and
145+ // folded directly into the bit vector, where repeated buckets cancel via xor. The toggler
146+ // resolves the bit vector's owned storage once, keeping the per-bit work out of the loop.
147+ let mut numbers = Vec :: new ( ) ;
148+ let mut bits = BitVec :: default ( ) ;
149+ bits. resize ( split) ;
150+ {
151+ let mut toggler = bits. toggler ( ) ;
152+ for hash in hashes {
153+ let bucket = config. hash_to_bucket ( hash) . into_usize ( ) ;
154+ if bucket >= split {
155+ numbers. push ( bucket) ;
156+ } else {
157+ toggler. toggle ( bucket) ;
158+ }
159+ }
160+ }
161+
162+ // Sort the most-significant buckets and drop those occurring an even number of times.
163+ numbers. sort_unstable_by ( |a, b| b. cmp ( a) ) ;
164+ erase_even_occurrences ( & mut numbers) ;
165+
166+ Self :: from_bit_chunks (
167+ config,
168+ iter_bit_chunks ( numbers. into_iter ( ) , bits. bit_chunks ( ) ) ,
169+ )
170+ }
171+
123172 /// Compare two geometric filters after applying the specified mask.
124173 ///
125174 /// To reduce the number of operations, the implementation first xors the bit chunks together,
@@ -429,6 +478,47 @@ pub(crate) fn xor<C: GeoConfig<Diff>>(
429478 )
430479}
431480
481+ /// Estimates the bucket id that separates the sparse most-significant buckets ("numbers") from
482+ /// the dense least-significant buckets ("bits") for a filter built from `n` hashes.
483+ ///
484+ /// The expected number of hashes falling into buckets `>= s` is `n * phi^s`. We pick `s` such
485+ /// that this is a small multiple of the most-significant bucket budget, so that after parity
486+ /// reduction the most-significant buckets are almost always supplied by the collected numbers.
487+ /// Choosing a slightly lower split only makes the collected set a bit larger; correctness does
488+ /// not depend on the estimate, since [`GeoDiffCount::from_bit_chunks`] re-splits the combined
489+ /// stream regardless.
490+ fn estimate_split_bucket < C : GeoConfig < Diff > > ( config : & C , n : usize ) -> usize {
491+ let target = 2 * config. max_msb_len ( ) ;
492+ if n <= target {
493+ return 0 ;
494+ }
495+ let ratio = target as f64 / n as f64 ;
496+ let split = ( ratio. ln ( ) / config. phi_f64 ( ) . ln ( ) ) . floor ( ) as usize ;
497+ // No bucket can ever exceed this bound, so never allocate a larger bit vector.
498+ split. min ( 64 * config. bits_per_level ( ) )
499+ }
500+
501+ /// Given a slice sorted in descending order, keeps a single copy of every value occurring an odd
502+ /// number of times and drops every value occurring an even number of times. The retained values
503+ /// stay sorted in descending order.
504+ fn erase_even_occurrences ( values : & mut Vec < usize > ) {
505+ let mut write = 0 ;
506+ let mut read = 0 ;
507+ while read < values. len ( ) {
508+ let value = values[ read] ;
509+ let mut count = 0 ;
510+ while read < values. len ( ) && values[ read] == value {
511+ read += 1 ;
512+ count += 1 ;
513+ }
514+ if count % 2 == 1 {
515+ values[ write] = value;
516+ write += 1 ;
517+ }
518+ }
519+ values. truncate ( write) ;
520+ }
521+
432522impl < C : GeoConfig < Diff > > Count < Diff > for GeoDiffCount < ' _ , C > {
433523 fn push_hash ( & mut self , hash : u64 ) {
434524 self . xor_bit ( self . config . hash_to_bucket ( hash) ) ;
@@ -542,6 +632,40 @@ mod tests {
542632 assert_eq ! ( m. iter_ones( ) . count( ) , 101 ) ;
543633 }
544634
635+ /// Building a filter from an iterator of hashes must produce exactly the same filter as
636+ /// pushing those hashes one by one.
637+ #[ test]
638+ fn test_from_hashes ( ) {
639+ fn assert_from_hashes_matches < C : GeoConfig < Diff > + Default > ( hashes : & [ u64 ] , n : usize ) {
640+ let mut expected: GeoDiffCount < ' _ , C > = GeoDiffCount :: new ( C :: default ( ) ) ;
641+ for & hash in hashes {
642+ expected. push_hash ( hash) ;
643+ }
644+ let actual: GeoDiffCount < ' _ , C > =
645+ GeoDiffCount :: from_hashes ( C :: default ( ) , hashes. iter ( ) . copied ( ) ) ;
646+ assert_eq ! ( expected, actual, "filter mismatch for n = {n}" ) ;
647+ assert_eq ! (
648+ expected. iter_ones( ) . collect_vec( ) ,
649+ actual. iter_ones( ) . collect_vec( ) ,
650+ "ones mismatch for n = {n}" ,
651+ ) ;
652+ }
653+
654+ prng_test_harness ( 4 , |rnd| {
655+ for n in [ 0usize , 1 , 5 , 50 , 500 , 5000 , 50000 ] {
656+ // Draw from a smaller pool so that buckets are hit multiple times, exercising the
657+ // even-occurrence cancellation on both the dense bits and the sparse numbers path.
658+ let pool: Vec < u64 > = ( 0 ..n. div_ceil ( 2 ) . max ( 1 ) ) . map ( |_| rnd. next_u64 ( ) ) . collect ( ) ;
659+ let hashes: Vec < u64 > = ( 0 ..n)
660+ . map ( |_| * pool. iter ( ) . choose ( rnd) . expect ( "pool is non-empty" ) )
661+ . collect ( ) ;
662+
663+ assert_from_hashes_matches :: < GeoDiffConfig7 > ( & hashes, n) ;
664+ assert_from_hashes_matches :: < GeoDiffConfig13 > ( & hashes, n) ;
665+ }
666+ } ) ;
667+ }
668+
545669 #[ test]
546670 fn test_estimate_fast ( ) {
547671 prng_test_harness ( 1 , |rnd| {
0 commit comments