probabilistic_machines 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +58 -0
- data/Cargo.lock +294 -0
- data/Cargo.toml +24 -0
- data/LICENSE.txt +21 -0
- data/README.md +203 -0
- data/ext/probabilistic_machines_native/core/Cargo.toml +24 -0
- data/ext/probabilistic_machines_native/core/src/countmin.rs +240 -0
- data/ext/probabilistic_machines_native/core/src/cuckoo.rs +283 -0
- data/ext/probabilistic_machines_native/core/src/hash.rs +88 -0
- data/ext/probabilistic_machines_native/core/src/hyperloglog.rs +254 -0
- data/ext/probabilistic_machines_native/core/src/lib.rs +65 -0
- data/ext/probabilistic_machines_native/core/src/tdigest.rs +445 -0
- data/ext/probabilistic_machines_native/core/src/topk.rs +325 -0
- data/ext/probabilistic_machines_native/extconf.rb +3 -0
- data/ext/probabilistic_machines_native/ffi/Cargo.toml +20 -0
- data/ext/probabilistic_machines_native/ffi/extconf.rb +21 -0
- data/ext/probabilistic_machines_native/ffi/src/countmin.rs +188 -0
- data/ext/probabilistic_machines_native/ffi/src/cuckoo.rs +149 -0
- data/ext/probabilistic_machines_native/ffi/src/data.rs +69 -0
- data/ext/probabilistic_machines_native/ffi/src/hyperloglog.rs +155 -0
- data/ext/probabilistic_machines_native/ffi/src/lib.rs +140 -0
- data/ext/probabilistic_machines_native/ffi/src/tdigest.rs +166 -0
- data/ext/probabilistic_machines_native/ffi/src/topk.rs +299 -0
- data/lib/probabilistic_machines/version.rb +5 -0
- data/lib/probabilistic_machines.rb +7 -0
- data/sig/probabilistic_machines.rbs +182 -0
- metadata +90 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
//! `ProbabilisticMachines::HeavyHitters`: which keys dominate a stream.
|
|
2
|
+
|
|
3
|
+
use std::cell::RefCell;
|
|
4
|
+
use std::collections::{HashMap, HashSet};
|
|
5
|
+
|
|
6
|
+
use bytes::Bytes;
|
|
7
|
+
use magnus::scan_args::scan_args;
|
|
8
|
+
use magnus::typed_data::Obj;
|
|
9
|
+
use magnus::{Error, RArray, RHash, RModule, RString, Ruby, Value, function, method, prelude::*};
|
|
10
|
+
use probabilistic_machines::{CodecError, HeavyHitters, hash64};
|
|
11
|
+
|
|
12
|
+
use crate::data::hitter;
|
|
13
|
+
use crate::{
|
|
14
|
+
Whole, codec_error, mismatch, optional_kwarg, required_kwarg, unsupported, unsupported_blob,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/// Counter counts [`Table`] is monomorphised for.
|
|
18
|
+
const COUNTERS: [usize; 3] = [16, 64, 256];
|
|
19
|
+
|
|
20
|
+
#[derive(Clone)]
|
|
21
|
+
enum Table {
|
|
22
|
+
K16(Box<HeavyHitters<u64, 16>>),
|
|
23
|
+
K64(Box<HeavyHitters<u64, 64>>),
|
|
24
|
+
K256(Box<HeavyHitters<u64, 256>>),
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/// Runs `body` against whichever variant is held.
|
|
28
|
+
macro_rules! table {
|
|
29
|
+
($table:expr, |$inner:ident| $body:expr) => {
|
|
30
|
+
match $table {
|
|
31
|
+
Table::K16($inner) => $body,
|
|
32
|
+
Table::K64($inner) => $body,
|
|
33
|
+
Table::K256($inner) => $body,
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/// Space-Saving needs `Copy + Eq` keys, so the table counts 64-bit key hashes
|
|
39
|
+
/// and `names` holds the string each tracked hash came from. A key the table
|
|
40
|
+
/// evicted leaves its name behind, so `names` is pruned back to the tracked
|
|
41
|
+
/// hashes once it holds more than twice the counters.
|
|
42
|
+
#[magnus::wrap(class = "ProbabilisticMachines::HeavyHitters", free_immediately, size)]
|
|
43
|
+
struct Wrapper {
|
|
44
|
+
table: RefCell<Table>,
|
|
45
|
+
names: RefCell<HashMap<u64, String>>,
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const fn counters_of<const K: usize>(_: &HeavyHitters<u64, K>) -> usize {
|
|
49
|
+
K
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const fn frame_len_of<const K: usize>(_: &HeavyHitters<u64, K>) -> usize {
|
|
53
|
+
HeavyHitters::<u64, K>::ENCODED_LEN
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/// Each shape is constructed behind its own call frame. Inlined, a debug build
|
|
57
|
+
/// sizes one frame for every arm of the dispatch match at once.
|
|
58
|
+
#[inline(never)]
|
|
59
|
+
fn boxed_new<const K: usize>() -> Box<HeavyHitters<u64, K>> {
|
|
60
|
+
Box::new(HeavyHitters::new())
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
#[inline(never)]
|
|
64
|
+
fn boxed_decode<const K: usize>(
|
|
65
|
+
ruby: &Ruby,
|
|
66
|
+
frame: &[u8],
|
|
67
|
+
) -> Result<Box<HeavyHitters<u64, K>>, Error> {
|
|
68
|
+
let mut table = boxed_new::<K>();
|
|
69
|
+
table
|
|
70
|
+
.decode_into(frame)
|
|
71
|
+
.map_err(|error| codec_error(ruby, error))?;
|
|
72
|
+
Ok(table)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/// A `usize` as the key table's 64-bit little-endian length field.
|
|
76
|
+
fn length_field(length: usize) -> [u8; 8] {
|
|
77
|
+
let mut field = [0_u8; 8];
|
|
78
|
+
for (slot, byte) in field.iter_mut().zip(length.to_le_bytes()) {
|
|
79
|
+
*slot = byte;
|
|
80
|
+
}
|
|
81
|
+
field
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
fn bad_key_table(ruby: &Ruby, reason: &str) -> Error {
|
|
85
|
+
Error::new(
|
|
86
|
+
ruby.exception_arg_error(),
|
|
87
|
+
format!("the blob's key table {reason}"),
|
|
88
|
+
)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/// Reads the name records that follow the core frame back into the
|
|
92
|
+
/// hash-to-name map.
|
|
93
|
+
fn key_table(ruby: &Ruby, mut records: &[u8]) -> Result<HashMap<u64, String>, Error> {
|
|
94
|
+
let mut names = HashMap::new();
|
|
95
|
+
while !records.is_empty() {
|
|
96
|
+
let Some((field, tail)) = records.split_first_chunk::<8>() else {
|
|
97
|
+
return Err(bad_key_table(ruby, "ends inside a record header"));
|
|
98
|
+
};
|
|
99
|
+
let Ok(length) = usize::try_from(u64::from_le_bytes(*field)) else {
|
|
100
|
+
return Err(bad_key_table(ruby, "names a key this platform cannot hold"));
|
|
101
|
+
};
|
|
102
|
+
let Some((name, tail)) = tail.split_at_checked(length) else {
|
|
103
|
+
return Err(bad_key_table(ruby, "ends inside a key"));
|
|
104
|
+
};
|
|
105
|
+
let Ok(name) = str::from_utf8(name) else {
|
|
106
|
+
return Err(bad_key_table(ruby, "holds a key that is not UTF-8"));
|
|
107
|
+
};
|
|
108
|
+
names.insert(hash64(name.as_bytes()), name.to_owned());
|
|
109
|
+
records = tail;
|
|
110
|
+
}
|
|
111
|
+
Ok(names)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
impl Wrapper {
|
|
115
|
+
fn new(ruby: &Ruby, args: &[Value]) -> Result<Self, Error> {
|
|
116
|
+
let args = scan_args::<(), (), (), (), RHash, ()>(args)?;
|
|
117
|
+
let Whole(counters): Whole<usize> = required_kwarg(args.keywords, "k")?;
|
|
118
|
+
let table = match counters {
|
|
119
|
+
16 => Table::K16(boxed_new::<16>()),
|
|
120
|
+
64 => Table::K64(boxed_new::<64>()),
|
|
121
|
+
256 => Table::K256(boxed_new::<256>()),
|
|
122
|
+
_ => return Err(unsupported(ruby, "k", counters, &COUNTERS)),
|
|
123
|
+
};
|
|
124
|
+
Ok(Self {
|
|
125
|
+
table: RefCell::new(table),
|
|
126
|
+
names: RefCell::new(HashMap::new()),
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/// Reads a blob written by [`dump`](Self::dump).
|
|
131
|
+
fn load(ruby: &Ruby, blob: Bytes) -> Result<Self, Error> {
|
|
132
|
+
let Some(shape) = blob.get(1..3) else {
|
|
133
|
+
return Err(codec_error(ruby, CodecError::Truncated));
|
|
134
|
+
};
|
|
135
|
+
let counters = u16::from_le_bytes([shape[0], shape[1]]);
|
|
136
|
+
let table = match counters {
|
|
137
|
+
16 => Table::K16(boxed_decode::<16>(ruby, &blob)?),
|
|
138
|
+
64 => Table::K64(boxed_decode::<64>(ruby, &blob)?),
|
|
139
|
+
256 => Table::K256(boxed_decode::<256>(ruby, &blob)?),
|
|
140
|
+
_ => return Err(unsupported_blob(ruby, "counter count", &COUNTERS)),
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
let frame_len = table!(&table, |inner| frame_len_of(&**inner));
|
|
144
|
+
let names = key_table(ruby, &blob[frame_len..])?;
|
|
145
|
+
let unnamed = table!(&table, |inner| inner
|
|
146
|
+
.hitters()
|
|
147
|
+
.any(|entry| !names.contains_key(&entry.key)));
|
|
148
|
+
if unnamed {
|
|
149
|
+
return Err(bad_key_table(ruby, "does not name every tracked key"));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
Ok(Self {
|
|
153
|
+
table: RefCell::new(table),
|
|
154
|
+
names: RefCell::new(names),
|
|
155
|
+
})
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
fn counters(&self) -> usize {
|
|
159
|
+
table!(&*self.table.borrow(), |table| counters_of(&**table))
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
fn add(rb_self: &Self, args: &[Value]) -> Result<(), Error> {
|
|
163
|
+
let args = scan_args::<(String,), (), (), (), RHash, ()>(args)?;
|
|
164
|
+
let (key,) = args.required;
|
|
165
|
+
let count =
|
|
166
|
+
optional_kwarg::<Whole<u64>>(args.keywords, "count")?.map_or(1, |Whole(count)| count);
|
|
167
|
+
let hash = hash64(key.as_bytes());
|
|
168
|
+
table!(&mut *rb_self.table.borrow_mut(), |table| table
|
|
169
|
+
.add_count(hash, count));
|
|
170
|
+
rb_self.names.borrow_mut().insert(hash, key);
|
|
171
|
+
rb_self.prune();
|
|
172
|
+
Ok(())
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
fn count(&self, key: String) -> u64 {
|
|
176
|
+
let hash = hash64(key.as_bytes());
|
|
177
|
+
table!(&*self.table.borrow(), |table| table.count(&hash))
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
fn observed(&self) -> u64 {
|
|
181
|
+
table!(&*self.table.borrow(), |table| table.observed())
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
fn size(&self) -> usize {
|
|
185
|
+
table!(&*self.table.borrow(), |table| table.len())
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
fn is_empty(&self) -> bool {
|
|
189
|
+
table!(&*self.table.borrow(), |table| table.is_empty())
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
fn ranked(ruby: &Ruby, rb_self: &Self) -> Result<RArray, Error> {
|
|
193
|
+
rb_self.take(ruby, usize::MAX)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
fn top(ruby: &Ruby, rb_self: &Self, Whole(count): Whole<usize>) -> Result<RArray, Error> {
|
|
197
|
+
rb_self.take(ruby, count)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
fn merge(ruby: &Ruby, rb_self: Obj<Self>, other: &Self) -> Result<Obj<Self>, Error> {
|
|
201
|
+
let source = other.table.borrow().clone();
|
|
202
|
+
let names = other.names.borrow().clone();
|
|
203
|
+
match (&mut *rb_self.table.borrow_mut(), &source) {
|
|
204
|
+
(Table::K16(target), Table::K16(source)) => target.merge(source),
|
|
205
|
+
(Table::K64(target), Table::K64(source)) => target.merge(source),
|
|
206
|
+
(Table::K256(target), Table::K256(source)) => target.merge(source),
|
|
207
|
+
_ => return Err(mismatch(ruby, "table")),
|
|
208
|
+
}
|
|
209
|
+
rb_self.names.borrow_mut().extend(names);
|
|
210
|
+
rb_self.prune();
|
|
211
|
+
Ok(rb_self)
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/// The core frame followed by the names of the keys it tracks.
|
|
215
|
+
///
|
|
216
|
+
/// ```text
|
|
217
|
+
/// core frame HeavyHitters::<u64, k>::ENCODED_LEN bytes, tag 0xB1
|
|
218
|
+
/// name record u64 little-endian byte length, then that many UTF-8 bytes
|
|
219
|
+
/// ```
|
|
220
|
+
///
|
|
221
|
+
/// One record per tracked key, in the core frame's order, filling the rest
|
|
222
|
+
/// of the blob. The table counts key hashes, so [`load`](Self::load)
|
|
223
|
+
/// rehashes each name to rebuild the hash-to-name map `top` and `ranked`
|
|
224
|
+
/// read.
|
|
225
|
+
fn dump(ruby: &Ruby, rb_self: &Self) -> RString {
|
|
226
|
+
let (frame, keys) = table!(&*rb_self.table.borrow(), |table| (
|
|
227
|
+
table.encoded(),
|
|
228
|
+
table.hitters().map(|entry| entry.key).collect::<Vec<u64>>()
|
|
229
|
+
));
|
|
230
|
+
let names = rb_self.names.borrow();
|
|
231
|
+
let mut blob = Vec::from(frame);
|
|
232
|
+
for name in keys.iter().filter_map(|key| names.get(key)) {
|
|
233
|
+
blob.extend_from_slice(&length_field(name.len()));
|
|
234
|
+
blob.extend_from_slice(name.as_bytes());
|
|
235
|
+
}
|
|
236
|
+
ruby.str_from_slice(&blob)
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
fn inspect(&self) -> String {
|
|
240
|
+
format!(
|
|
241
|
+
"#<ProbabilisticMachines::HeavyHitters k={} tracked={} observed={}>",
|
|
242
|
+
self.counters(),
|
|
243
|
+
self.size(),
|
|
244
|
+
self.observed()
|
|
245
|
+
)
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/// The `count` most frequent tracked keys, most frequent first.
|
|
249
|
+
fn take(&self, ruby: &Ruby, count: usize) -> Result<RArray, Error> {
|
|
250
|
+
let ranked = table!(&*self.table.borrow(), |table| table.ranked());
|
|
251
|
+
let names = self.names.borrow();
|
|
252
|
+
let hitters = ruby.ary_new_capa(ranked.len().min(count));
|
|
253
|
+
for entry in ranked.into_iter().take(count) {
|
|
254
|
+
let key = names.get(&entry.key).ok_or_else(|| {
|
|
255
|
+
Error::new(
|
|
256
|
+
ruby.exception_runtime_error(),
|
|
257
|
+
"a tracked key lost its name",
|
|
258
|
+
)
|
|
259
|
+
})?;
|
|
260
|
+
hitters.push(hitter(
|
|
261
|
+
ruby,
|
|
262
|
+
ruby.str_new(key).as_value(),
|
|
263
|
+
entry.count,
|
|
264
|
+
entry.overestimate,
|
|
265
|
+
)?)?;
|
|
266
|
+
}
|
|
267
|
+
Ok(hitters)
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
fn prune(&self) {
|
|
271
|
+
let mut names = self.names.borrow_mut();
|
|
272
|
+
if names.len() <= 2 * self.counters() {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
let tracked: HashSet<u64> = table!(&*self.table.borrow(), |table| table
|
|
276
|
+
.hitters()
|
|
277
|
+
.map(|entry| entry.key)
|
|
278
|
+
.collect());
|
|
279
|
+
names.retain(|hash, _| tracked.contains(hash));
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
pub fn define(ruby: &Ruby, module: RModule) -> Result<(), Error> {
|
|
284
|
+
let class = module.define_class("HeavyHitters", ruby.class_object())?;
|
|
285
|
+
class.define_singleton_method("new", function!(Wrapper::new, -1))?;
|
|
286
|
+
class.define_singleton_method("load", function!(Wrapper::load, 1))?;
|
|
287
|
+
class.define_method("k", method!(Wrapper::counters, 0))?;
|
|
288
|
+
class.define_method("add", method!(Wrapper::add, -1))?;
|
|
289
|
+
class.define_method("count", method!(Wrapper::count, 1))?;
|
|
290
|
+
class.define_method("observed", method!(Wrapper::observed, 0))?;
|
|
291
|
+
class.define_method("size", method!(Wrapper::size, 0))?;
|
|
292
|
+
class.define_method("empty?", method!(Wrapper::is_empty, 0))?;
|
|
293
|
+
class.define_method("top", method!(Wrapper::top, 1))?;
|
|
294
|
+
class.define_method("ranked", method!(Wrapper::ranked, 0))?;
|
|
295
|
+
class.define_method("merge", method!(Wrapper::merge, 1))?;
|
|
296
|
+
class.define_method("dump", method!(Wrapper::dump, 0))?;
|
|
297
|
+
class.define_method("inspect", method!(Wrapper::inspect, 0))?;
|
|
298
|
+
Ok(())
|
|
299
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'probabilistic_machines/version'
|
|
4
|
+
|
|
5
|
+
# Every class, the two `Data` result objects and both error classes are defined
|
|
6
|
+
# by the native extension; there is no pure-Ruby implementation to fall back to.
|
|
7
|
+
require 'probabilistic_machines_native/probabilistic_machines_native'
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
# Sketches that answer a counting question in memory fixed at construction
|
|
2
|
+
# time. Every class here is defined by the native extension.
|
|
3
|
+
module ProbabilisticMachines
|
|
4
|
+
VERSION: String
|
|
5
|
+
|
|
6
|
+
class Error < StandardError
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
# Raised when a cuckoo filter has nowhere left to put a fingerprint.
|
|
10
|
+
class Full < Error
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
# A tracked key and its counter. The true count lies in
|
|
14
|
+
# `count - overestimate` through `count`.
|
|
15
|
+
class Hitter < ::Data
|
|
16
|
+
attr_reader key: String
|
|
17
|
+
attr_reader count: Integer
|
|
18
|
+
attr_reader overestimate: Integer
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# One cluster of a digest: the mean of the values that collapsed into it.
|
|
22
|
+
class Centroid < ::Data
|
|
23
|
+
attr_reader mean: Float
|
|
24
|
+
attr_reader weight: Float
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Distinct counts in a fixed register array.
|
|
28
|
+
class HyperLogLog
|
|
29
|
+
# `registers` is 1024, 4096, 16384 or 65536.
|
|
30
|
+
def self.new: (registers: Integer) -> instance
|
|
31
|
+
|
|
32
|
+
def self.load: (String blob) -> instance
|
|
33
|
+
|
|
34
|
+
def registers: () -> Integer
|
|
35
|
+
def add: (String item) -> nil
|
|
36
|
+
|
|
37
|
+
# For streams that ship 64-bit hashes instead of keys.
|
|
38
|
+
def add_hash: (Integer hash) -> nil
|
|
39
|
+
|
|
40
|
+
def estimate: () -> Float
|
|
41
|
+
|
|
42
|
+
# Relative standard error of the estimate, `1.04 / sqrt(registers)`.
|
|
43
|
+
def standard_error: () -> Float
|
|
44
|
+
|
|
45
|
+
def empty?: () -> bool
|
|
46
|
+
|
|
47
|
+
# Unions `other` in. Raises `ArgumentError` on a different register count.
|
|
48
|
+
def merge: (HyperLogLog other) -> self
|
|
49
|
+
|
|
50
|
+
def dump: () -> String
|
|
51
|
+
def inspect: () -> String
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# How often each item was seen, in `width * depth` counters.
|
|
55
|
+
class CountMinSketch
|
|
56
|
+
# `width` is 1024 or 4096; `depth` is 4 or 8.
|
|
57
|
+
def self.new: (width: Integer, depth: Integer) -> instance
|
|
58
|
+
|
|
59
|
+
def self.load: (String blob) -> instance
|
|
60
|
+
|
|
61
|
+
def width: () -> Integer
|
|
62
|
+
def depth: () -> Integer
|
|
63
|
+
def add: (String item, ?count: Integer) -> nil
|
|
64
|
+
|
|
65
|
+
# Never below the true count, and over it by at most
|
|
66
|
+
# `e * total / width` with probability `1 - e ** -depth`.
|
|
67
|
+
def count: (String item) -> Integer
|
|
68
|
+
|
|
69
|
+
# Sum of every count added.
|
|
70
|
+
def total: () -> Integer
|
|
71
|
+
def saturated?: () -> bool
|
|
72
|
+
|
|
73
|
+
# Sums `other` in. Raises `ArgumentError` on a different shape.
|
|
74
|
+
def merge: (CountMinSketch other) -> self
|
|
75
|
+
|
|
76
|
+
def dump: () -> String
|
|
77
|
+
def inspect: () -> String
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Quantiles of a stream, in clusters instead of samples.
|
|
81
|
+
class TDigest
|
|
82
|
+
def self.new: (compression: Integer) -> instance
|
|
83
|
+
|
|
84
|
+
def self.load: (String blob) -> instance
|
|
85
|
+
|
|
86
|
+
def compression: () -> Integer
|
|
87
|
+
def add: (Float value, ?weight: Float) -> nil
|
|
88
|
+
|
|
89
|
+
# Value at quantile `q`, `Float::NAN` when empty.
|
|
90
|
+
def quantile: (Float q) -> Float
|
|
91
|
+
|
|
92
|
+
# Fraction of the stream at or below `value`, `Float::NAN` when empty.
|
|
93
|
+
def cdf: (Float value) -> Float
|
|
94
|
+
|
|
95
|
+
# Total weight added.
|
|
96
|
+
def count: () -> Float
|
|
97
|
+
|
|
98
|
+
# Smallest value added, `Float::INFINITY` when empty.
|
|
99
|
+
def minimum: () -> Float
|
|
100
|
+
|
|
101
|
+
# Largest value added, `-Float::INFINITY` when empty.
|
|
102
|
+
def maximum: () -> Float
|
|
103
|
+
|
|
104
|
+
# Clusters currently held.
|
|
105
|
+
def size: () -> Integer
|
|
106
|
+
|
|
107
|
+
def empty?: () -> bool
|
|
108
|
+
def centroids: () -> Array[Centroid]
|
|
109
|
+
def merge: (TDigest other) -> self
|
|
110
|
+
def dump: () -> String
|
|
111
|
+
def inspect: () -> String
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Membership that can forget, in `buckets * 4` fingerprints.
|
|
115
|
+
class CuckooFilter
|
|
116
|
+
# `buckets` is 1024, 4096, 16384 or 65536.
|
|
117
|
+
def self.new: (buckets: Integer) -> instance
|
|
118
|
+
|
|
119
|
+
def self.load: (String blob) -> instance
|
|
120
|
+
|
|
121
|
+
def buckets: () -> Integer
|
|
122
|
+
|
|
123
|
+
# Fingerprint slots in the table, `buckets * 4`.
|
|
124
|
+
def capacity: () -> Integer
|
|
125
|
+
|
|
126
|
+
# Raises `Full` when the item has nowhere to go.
|
|
127
|
+
def insert: (String item) -> bool
|
|
128
|
+
|
|
129
|
+
# Never false for an item that was inserted; true for one that was not
|
|
130
|
+
# with probability about 3%.
|
|
131
|
+
def include?: (String item) -> bool
|
|
132
|
+
|
|
133
|
+
alias contains? include?
|
|
134
|
+
|
|
135
|
+
# Only for items that were inserted: removing an item that merely collides
|
|
136
|
+
# with one takes the other item's fingerprint with it.
|
|
137
|
+
def remove: (String item) -> bool
|
|
138
|
+
|
|
139
|
+
def size: () -> Integer
|
|
140
|
+
def empty?: () -> bool
|
|
141
|
+
def dump: () -> String
|
|
142
|
+
def inspect: () -> String
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Which keys dominate a stream, in `k` counters instead of one per key.
|
|
146
|
+
class HeavyHitters
|
|
147
|
+
# `k` is 16, 64 or 256.
|
|
148
|
+
def self.new: (k: Integer) -> instance
|
|
149
|
+
|
|
150
|
+
def self.load: (String blob) -> instance
|
|
151
|
+
|
|
152
|
+
def k: () -> Integer
|
|
153
|
+
|
|
154
|
+
# Keys are UTF-8 strings, so `top` and `ranked` can hand them back.
|
|
155
|
+
def add: (String key, ?count: Integer) -> nil
|
|
156
|
+
|
|
157
|
+
# Upper bound on how often `key` occurred, 0 when it is not tracked.
|
|
158
|
+
def count: (String key) -> Integer
|
|
159
|
+
|
|
160
|
+
# Observations fed in.
|
|
161
|
+
def observed: () -> Integer
|
|
162
|
+
|
|
163
|
+
# Keys currently tracked, at most `k`.
|
|
164
|
+
def size: () -> Integer
|
|
165
|
+
|
|
166
|
+
def empty?: () -> bool
|
|
167
|
+
|
|
168
|
+
# The `n` most frequent tracked keys, most frequent first.
|
|
169
|
+
def top: (Integer n) -> Array[Hitter]
|
|
170
|
+
|
|
171
|
+
# Every tracked key, most frequent first.
|
|
172
|
+
def ranked: () -> Array[Hitter]
|
|
173
|
+
|
|
174
|
+
# Folds `other` in. Raises `ArgumentError` on a different `k`.
|
|
175
|
+
def merge: (HeavyHitters other) -> self
|
|
176
|
+
|
|
177
|
+
# The crate's frame followed by the key table that carries the strings.
|
|
178
|
+
def dump: () -> String
|
|
179
|
+
|
|
180
|
+
def inspect: () -> String
|
|
181
|
+
end
|
|
182
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: probabilistic_machines
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Abdelkader Boudih
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: rb_sys
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - "~>"
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0.9'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - "~>"
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0.9'
|
|
26
|
+
description: HyperLogLog, Count-Min Sketch, t-digest, cuckoo filter and Space-Saving
|
|
27
|
+
top-k, bound to Ruby from the no_std probabilistic-machines crate. Each sketch answers
|
|
28
|
+
a counting question in memory fixed at construction time, and serialises to a byte
|
|
29
|
+
string other languages reading the same crate can decode.
|
|
30
|
+
email:
|
|
31
|
+
- oss@seuros.com
|
|
32
|
+
executables: []
|
|
33
|
+
extensions:
|
|
34
|
+
- ext/probabilistic_machines_native/extconf.rb
|
|
35
|
+
extra_rdoc_files: []
|
|
36
|
+
files:
|
|
37
|
+
- CHANGELOG.md
|
|
38
|
+
- Cargo.lock
|
|
39
|
+
- Cargo.toml
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- ext/probabilistic_machines_native/core/Cargo.toml
|
|
43
|
+
- ext/probabilistic_machines_native/core/src/countmin.rs
|
|
44
|
+
- ext/probabilistic_machines_native/core/src/cuckoo.rs
|
|
45
|
+
- ext/probabilistic_machines_native/core/src/hash.rs
|
|
46
|
+
- ext/probabilistic_machines_native/core/src/hyperloglog.rs
|
|
47
|
+
- ext/probabilistic_machines_native/core/src/lib.rs
|
|
48
|
+
- ext/probabilistic_machines_native/core/src/tdigest.rs
|
|
49
|
+
- ext/probabilistic_machines_native/core/src/topk.rs
|
|
50
|
+
- ext/probabilistic_machines_native/extconf.rb
|
|
51
|
+
- ext/probabilistic_machines_native/ffi/Cargo.toml
|
|
52
|
+
- ext/probabilistic_machines_native/ffi/extconf.rb
|
|
53
|
+
- ext/probabilistic_machines_native/ffi/src/countmin.rs
|
|
54
|
+
- ext/probabilistic_machines_native/ffi/src/cuckoo.rs
|
|
55
|
+
- ext/probabilistic_machines_native/ffi/src/data.rs
|
|
56
|
+
- ext/probabilistic_machines_native/ffi/src/hyperloglog.rs
|
|
57
|
+
- ext/probabilistic_machines_native/ffi/src/lib.rs
|
|
58
|
+
- ext/probabilistic_machines_native/ffi/src/tdigest.rs
|
|
59
|
+
- ext/probabilistic_machines_native/ffi/src/topk.rs
|
|
60
|
+
- lib/probabilistic_machines.rb
|
|
61
|
+
- lib/probabilistic_machines/version.rb
|
|
62
|
+
- sig/probabilistic_machines.rbs
|
|
63
|
+
homepage: https://github.com/seuros/probabilistic_machines
|
|
64
|
+
licenses:
|
|
65
|
+
- MIT
|
|
66
|
+
metadata:
|
|
67
|
+
source_code_uri: https://github.com/seuros/probabilistic_machines
|
|
68
|
+
changelog_uri: https://github.com/seuros/probabilistic_machines/blob/main/CHANGELOG.md
|
|
69
|
+
bug_tracker_uri: https://github.com/seuros/probabilistic_machines/issues
|
|
70
|
+
rubygems_mfa_required: 'true'
|
|
71
|
+
cargo_crate_name: probabilistic_machines_native
|
|
72
|
+
cargo_manifest_path: ext/probabilistic_machines_native/ffi/Cargo.toml
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: 4.0.0
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 4.0.20
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: Distinct counts, quantiles and frequencies in bounded memory
|
|
90
|
+
test_files: []
|