use std::fmt::Write as _;
use crate::point::SearchResult;
use crate::velesql::SelectColumns;
use rustc_hash::FxHashSet;
pub fn apply_distinct(results: Vec<SearchResult>, columns: &SelectColumns) -> Vec<SearchResult> {
let (column_names, include_score) = match columns {
SelectColumns::Columns(cols) => (cols.iter().map(|c| c.name.clone()).collect(), false),
SelectColumns::Mixed {
columns: cols,
similarity_scores,
qualified_wildcards,
..
} => {
let cols_for_dedup = if qualified_wildcards.is_empty() {
cols.iter().map(|c| c.name.clone()).collect()
} else {
Vec::new()
};
(cols_for_dedup, !similarity_scores.is_empty())
}
SelectColumns::SimilarityScore(_) => (Vec::new(), true),
SelectColumns::All
| SelectColumns::Aggregations(_)
| SelectColumns::QualifiedWildcard(_) => (Vec::new(), false),
};
let mut seen: FxHashSet<String> = FxHashSet::default();
results
.into_iter()
.filter(|r| {
let key = compute_distinct_key(r, &column_names, include_score);
seen.insert(key)
})
.collect()
}
pub fn compute_distinct_key(
result: &SearchResult,
columns: &[String],
include_score: bool,
) -> String {
let payload = result.point.payload.as_ref();
let mut key = if columns.is_empty() {
payload.map_or_else(|| "null".to_string(), canonical_json_string)
} else {
columns
.iter()
.map(|col| {
payload
.and_then(|p| p.get(col))
.map_or_else(|| "null".to_string(), canonical_json_string)
})
.collect::<Vec<_>>()
.join("\x1F") };
if include_score {
let _ = write!(key, "\x1F{}", result.score);
}
key
}
fn canonical_json_string(value: &serde_json::Value) -> String {
match value {
serde_json::Value::Object(map) => {
let mut keys: Vec<_> = map.keys().collect();
keys.sort_unstable();
let pairs: Vec<String> = keys
.iter()
.map(|k| format!("{}:{}", k, canonical_json_string(&map[*k])))
.collect();
format!("{{{}}}", pairs.join(","))
}
serde_json::Value::Array(arr) => {
let items: Vec<String> = arr.iter().map(canonical_json_string).collect();
format!("[{}]", items.join(","))
}
_ => value.to_string(),
}
}
#[cfg(test)]
#[path = "distinct_unit_tests.rs"]
mod tests;