1use crate::error::{RillError, checked_finite_add, ensure_finite};
20use crate::sparse::{FeatureId, SparseFeatures};
21use std::collections::hash_map::DefaultHasher;
22use std::hash::{Hash, Hasher};
23
24#[derive(Debug, Clone)]
26#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
27pub struct FeatureHasherConfig {
28 pub dimension: usize,
30 pub seed: u64,
32 pub signed: bool,
34}
35
36impl Default for FeatureHasherConfig {
37 fn default() -> Self {
38 Self {
39 dimension: 1024,
40 seed: 0,
41 signed: true,
42 }
43 }
44}
45
46#[derive(Debug, Clone)]
55#[cfg_attr(feature = "serde", derive(serde::Serialize))]
56pub struct FeatureHasher {
57 config: FeatureHasherConfig,
58}
59
60impl FeatureHasher {
61 pub fn new(dimension: usize, seed: u64) -> Result<Self, RillError> {
65 Self::with_config(FeatureHasherConfig {
66 dimension,
67 seed,
68 signed: true,
69 })
70 }
71
72 pub fn with_config(config: FeatureHasherConfig) -> Result<Self, RillError> {
74 if config.dimension == 0 {
75 return Err(RillError::InvalidHashDimension(config.dimension));
76 }
77 Ok(Self { config })
78 }
79
80 pub const fn dimension(&self) -> usize {
82 self.config.dimension
83 }
84
85 pub const fn seed(&self) -> u64 {
87 self.config.seed
88 }
89
90 pub const fn signed(&self) -> bool {
92 self.config.signed
93 }
94
95 fn hash_id(&self, id: FeatureId) -> (usize, f64) {
97 let bucket = self.hash_bucket(id);
98 let sign = if self.config.signed {
99 self.hash_sign(id)
100 } else {
101 1.0
102 };
103 (bucket, sign)
104 }
105
106 fn hash_bucket(&self, id: FeatureId) -> usize {
108 let mut hasher = DefaultHasher::new();
109 self.config.seed.hash(&mut hasher);
110 id.hash(&mut hasher);
111 (hasher.finish() as usize) % self.config.dimension
112 }
113
114 fn hash_sign(&self, id: FeatureId) -> f64 {
116 let mut hasher = DefaultHasher::new();
117 (self.config.seed.wrapping_mul(0x517cc1b727220a95)).hash(&mut hasher);
118 id.hash(&mut hasher);
119 if hasher.finish() & 1 == 1 { -1.0 } else { 1.0 }
120 }
121
122 pub fn hash_string(&self, name: &str) -> FeatureId {
124 let mut hasher = DefaultHasher::new();
125 self.config.seed.hash(&mut hasher);
126 name.hash(&mut hasher);
127 hasher.finish()
128 }
129
130 pub fn hash_strings(&self, pairs: &[(&str, f64)]) -> Result<SparseFeatures, RillError> {
135 let mut ids: Vec<(FeatureId, f64)> = Vec::with_capacity(pairs.len());
136 for (name, value) in pairs {
137 ensure_finite("hash_value", *value)?;
138 ids.push((self.hash_string(name), *value));
139 }
140 SparseFeatures::from_unsorted(ids)
141 }
142
143 pub fn transform(&self, features: &SparseFeatures) -> Result<Vec<f64>, RillError> {
149 if self.config.dimension == 0 {
150 return Err(RillError::InvalidHashDimension(0));
151 }
152 features.validate()?;
153 let mut output = vec![0.0; self.config.dimension];
154 for &(id, value) in features.values() {
155 ensure_finite("sparse_value", value)?;
156 let (bucket, sign) = self.hash_id(id);
157 output[bucket] = checked_finite_add(output[bucket], sign * value, "hashed feature")?;
158 }
159 Ok(output)
160 }
161}
162
163#[cfg(feature = "serde")]
164impl<'de> serde::Deserialize<'de> for FeatureHasher {
165 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
166 where
167 D: serde::Deserializer<'de>,
168 {
169 #[derive(serde::Deserialize)]
170 struct FeatureHasherState {
171 config: FeatureHasherConfig,
172 }
173
174 let state = FeatureHasherState::deserialize(deserializer)?;
175 Self::with_config(state.config).map_err(serde::de::Error::custom)
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
184 fn reproducible_output() {
185 let h = FeatureHasher::new(16, 42).unwrap();
186 let sf = SparseFeatures::from_sorted(vec![(1, 3.0), (5, -2.0), (10, 1.0)]).unwrap();
187 let out1 = h.transform(&sf).unwrap();
188 let out2 = h.transform(&sf).unwrap();
189 assert_eq!(out1, out2);
190 }
191
192 #[test]
193 fn collision_overflow_is_rejected() {
194 let hasher = FeatureHasher::with_config(FeatureHasherConfig {
195 dimension: 1,
196 seed: 42,
197 signed: false,
198 })
199 .unwrap();
200 let features = SparseFeatures::from_sorted(vec![(1, f64::MAX), (2, f64::MAX)]).unwrap();
201 assert!(hasher.transform(&features).is_err());
202 }
203
204 #[cfg(feature = "serde")]
205 #[test]
206 fn serde_rejects_zero_dimension() {
207 let malformed = r#"{"config":{"dimension":0,"seed":0,"signed":true}}"#;
208 assert!(serde_json::from_str::<FeatureHasher>(malformed).is_err());
209 }
210
211 #[test]
212 fn different_seeds_produce_different_output() {
213 let h1 = FeatureHasher::new(16, 1).unwrap();
214 let h2 = FeatureHasher::new(16, 2).unwrap();
215 let sf = SparseFeatures::from_sorted(vec![(1, 1.0), (2, 2.0)]).unwrap();
216 let out1 = h1.transform(&sf).unwrap();
217 let out2 = h2.transform(&sf).unwrap();
218 assert_ne!(out1, out2);
219 }
220
221 #[test]
222 fn signed_hashing_produces_negatives() {
223 let h = FeatureHasher::with_config(FeatureHasherConfig {
224 dimension: 256,
225 seed: 42,
226 signed: true,
227 })
228 .unwrap();
229 let sf =
230 SparseFeatures::from_sorted((0..100).map(|i| (i, 1.0)).collect::<Vec<_>>()).unwrap();
231 let out = h.transform(&sf).unwrap();
232 assert!(out.iter().any(|&v| v < 0.0));
234 }
235
236 #[test]
237 fn unsigned_hashing_all_positive() {
238 let h = FeatureHasher::with_config(FeatureHasherConfig {
239 dimension: 256,
240 seed: 42,
241 signed: false,
242 })
243 .unwrap();
244 let sf =
245 SparseFeatures::from_sorted((0..100).map(|i| (i, 1.0)).collect::<Vec<_>>()).unwrap();
246 let out = h.transform(&sf).unwrap();
247 assert!(out.iter().all(|&v| v >= 0.0));
248 }
249
250 #[test]
251 fn dimension_one_all_same_bucket() {
252 let h = FeatureHasher::new(1, 42).unwrap();
253 let sf = SparseFeatures::from_sorted(vec![(1, 3.0), (2, 5.0)]).unwrap();
254 let out = h.transform(&sf).unwrap();
255 assert_eq!(out.len(), 1);
256 assert!(out[0].abs() > 0.0);
258 }
259
260 #[test]
261 fn empty_features_returns_zeros() {
262 let h = FeatureHasher::new(8, 42).unwrap();
263 let sf = SparseFeatures::new();
264 let out = h.transform(&sf).unwrap();
265 assert_eq!(out, vec![0.0; 8]);
266 }
267
268 #[test]
269 fn string_hash_reproducible() {
270 let h = FeatureHasher::new(8, 42).unwrap();
271 let id1 = h.hash_string("user_id");
272 let id2 = h.hash_string("user_id");
273 assert_eq!(id1, id2);
274 }
275
276 #[test]
277 fn different_strings_different_ids() {
278 let h = FeatureHasher::new(8, 42).unwrap();
279 let id1 = h.hash_string("user_id");
280 let id2 = h.hash_string("device_id");
281 assert_ne!(id1, id2);
282 }
283
284 #[test]
285 fn hash_strings_creates_sorted_features() {
286 let h = FeatureHasher::new(8, 42).unwrap();
287 let sf = h
288 .hash_strings(&[("alpha", 1.0), ("beta", 2.0), ("gamma", 3.0)])
289 .unwrap();
290 assert!(sf.validate().is_ok());
292 assert_eq!(sf.len(), 3);
293 }
294
295 #[test]
296 fn invalid_dimension_rejected() {
297 assert!(matches!(
298 FeatureHasher::new(0, 42),
299 Err(RillError::InvalidHashDimension(0))
300 ));
301 }
302
303 #[test]
304 #[cfg(feature = "serde")]
305 fn serde_roundtrip() {
306 let h = FeatureHasher::new(16, 42).unwrap();
307 let json = serde_json::to_string(&h).unwrap();
308 let restored: FeatureHasher = serde_json::from_str(&json).unwrap();
309 assert_eq!(restored.dimension(), 16);
310 assert_eq!(restored.seed(), 42);
311 assert!(restored.signed());
312 }
313}