Skip to main content

rudb_native/
host.rs

1//! A bounded, certified host grouping over one native string dictionary.
2//!
3//! The fixed ClickBench host expression is applied while the writer already holds the decoded
4//! dictionary for ranking. Weighted Misra-Gries names every host above `rows / (capacity + 1)`;
5//! a second pass counts those candidates exactly. The total decrement is a valid upper bound on
6//! every omitted host, including hosts made from many individually rare source strings.
7
8use std::collections::HashMap;
9
10use crate::{GlobalDictionary, invalid};
11use rudb_common::Result;
12
13pub(crate) const CAPACITY: usize = 512;
14pub(crate) const BYTE_BUDGET: usize = 1024 * 1024;
15
16/// Exact aggregates for one host, with a certificate for every host not listed.
17#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct HostSummary {
19    /// The column whose source code space was transformed.
20    pub column: usize,
21    /// An upper bound on the row count of every omitted host.
22    pub omitted_max: u64,
23    /// Candidate hosts with exact aggregate state.
24    pub entries: Vec<HostEntry>,
25}
26
27/// The aggregate state needed by ClickBench's host grouping.
28#[derive(Debug, Clone, PartialEq, Eq)]
29pub struct HostEntry {
30    pub host: String,
31    pub count: u64,
32    pub bytes_sum: i128,
33    pub minimum: String,
34}
35
36/// The anchored, case-sensitive ClickBench host replacement over valid UTF-8 bytes.
37#[allow(dead_code)]
38pub(crate) fn host_bytes(text: &[u8]) -> &[u8] {
39    let rest = text.strip_prefix(b"http://").or_else(|| text.strip_prefix(b"https://"));
40    let Some(rest) = rest else { return text };
41    let Some(end) = rest.iter().position(|&byte| byte == b'/') else { return text };
42    if end == 0 || rest[end + 1..].contains(&b'\n') {
43        return text;
44    }
45    let host = &rest[..end];
46    host.strip_prefix(b"www.").filter(|without| !without.is_empty()).unwrap_or(host)
47}
48
49#[allow(dead_code)]
50pub(crate) fn build(
51    column: usize,
52    dictionary: &GlobalDictionary,
53    flat: &[u8],
54    bases: &[u64],
55) -> Result<Option<HostSummary>> {
56    let mut candidates = HashMap::<Vec<u8>, u64>::new();
57    let mut omitted_max = 0_u64;
58    for (code, &weight) in dictionary.counts.iter().enumerate() {
59        if weight == 0 {
60            continue;
61        }
62        let (from, to) = GlobalDictionary::value_span(&dictionary.ends, bases, code);
63        let text = flat
64            .get(from..to)
65            .ok_or_else(|| invalid("host source code is outside its dictionary"))?;
66        if text.is_empty() {
67            continue;
68        }
69        let host = host_bytes(text);
70        let mut remaining = weight;
71        loop {
72            if let Some(count) = candidates.get_mut(host) {
73                *count =
74                    count.checked_add(remaining).ok_or_else(|| invalid("host count overflow"))?;
75                break;
76            }
77            if candidates.len() < CAPACITY {
78                candidates.insert(host.to_vec(), remaining);
79                break;
80            }
81            let least = candidates.values().copied().min().unwrap_or_default();
82            let subtract = remaining.min(least);
83            candidates.retain(|_, count| {
84                *count -= subtract;
85                *count != 0
86            });
87            omitted_max =
88                omitted_max.checked_add(subtract).ok_or_else(|| invalid("host bound overflow"))?;
89            remaining -= subtract;
90            if remaining == 0 {
91                break;
92            }
93        }
94    }
95
96    let mut exact = candidates
97        .into_keys()
98        .map(|host| (host, (0_u64, 0_i128, Vec::<u8>::new())))
99        .collect::<HashMap<_, _>>();
100    for (code, &weight) in dictionary.counts.iter().enumerate() {
101        if weight == 0 {
102            continue;
103        }
104        let (from, to) = GlobalDictionary::value_span(&dictionary.ends, bases, code);
105        let text = flat
106            .get(from..to)
107            .ok_or_else(|| invalid("host source code is outside its dictionary"))?;
108        if text.is_empty() {
109            continue;
110        }
111        let Some((count, bytes_sum, minimum)) = exact.get_mut(host_bytes(text)) else { continue };
112        *count = count.checked_add(weight).ok_or_else(|| invalid("host count overflow"))?;
113        let bytes =
114            i128::try_from(text.len()).map_err(|_| invalid("host source length overflow"))?;
115        *bytes_sum = bytes_sum
116            .checked_add(
117                bytes
118                    .checked_mul(i128::from(weight))
119                    .ok_or_else(|| invalid("host length sum overflow"))?,
120            )
121            .ok_or_else(|| invalid("host length sum overflow"))?;
122        if minimum.is_empty() || text < minimum.as_slice() {
123            *minimum = text.to_vec();
124        }
125    }
126    let mut entries = exact
127        .into_iter()
128        .filter(|(_, (count, _, _))| *count != 0)
129        .map(|(host, (count, bytes_sum, minimum))| {
130            Ok(HostEntry {
131                host: String::from_utf8(host).map_err(|_| invalid("host is not UTF-8"))?,
132                count,
133                bytes_sum,
134                minimum: String::from_utf8(minimum)
135                    .map_err(|_| invalid("host minimum is not UTF-8"))?,
136            })
137        })
138        .collect::<Result<Vec<_>>>()?;
139    entries.sort_unstable_by(|left, right| {
140        right.count.cmp(&left.count).then_with(|| left.host.cmp(&right.host))
141    });
142    let bytes = entries.iter().try_fold(0_usize, |sum, entry| {
143        sum.checked_add(entry.host.len())?.checked_add(entry.minimum.len())
144    });
145    if bytes.is_none_or(|bytes| bytes > BYTE_BUDGET) {
146        return Ok(None);
147    }
148    Ok(Some(HostSummary { column, omitted_max, entries }))
149}
150
151#[cfg(test)]
152mod tests {
153    use super::host_bytes;
154
155    #[test]
156    fn host_expression_keeps_anchored_regex_boundaries() {
157        assert_eq!(host_bytes(b"http://www.example.com/a"), b"example.com");
158        assert_eq!(host_bytes(b"http://example.com"), b"http://example.com");
159        assert_eq!(host_bytes(b"https:///a"), b"https:///a");
160        assert_eq!(host_bytes(b"https://example.com/a\nb"), b"https://example.com/a\nb");
161        assert_eq!(host_bytes(b"http://www./a"), b"www.");
162    }
163}