use crate::point::SearchResult;
use crate::velesql::{SelectColumns, SimilarityScoreExpr};
#[must_use]
pub fn project_results(
results: &[SearchResult],
select_exprs: &SelectColumns,
) -> Vec<serde_json::Value> {
results
.iter()
.map(|r| project_single(r, select_exprs))
.collect()
}
fn project_single(result: &SearchResult, select_exprs: &SelectColumns) -> serde_json::Value {
match select_exprs {
SelectColumns::All | SelectColumns::QualifiedWildcard(_) => project_wildcard(result),
SelectColumns::Columns(cols) => project_columns(result, cols),
SelectColumns::SimilarityScore(expr) => project_similarity_only(result, expr),
SelectColumns::Aggregations(_) => {
serde_json::Value::Object(serde_json::Map::new())
}
SelectColumns::Mixed {
columns,
aggregations: _,
similarity_scores,
qualified_wildcards,
window_functions,
} => project_mixed(
result,
columns,
similarity_scores,
qualified_wildcards,
window_functions,
),
}
}
fn project_wildcard(result: &SearchResult) -> serde_json::Value {
let mut map = serde_json::Map::new();
map.insert("id".to_string(), serde_json::Value::from(result.point.id));
if let Some(serde_json::Value::Object(payload_map)) = result.point.payload.as_ref() {
for (k, v) in payload_map {
if k != "id" {
map.insert(k.clone(), v.clone());
}
}
}
serde_json::Value::Object(map)
}
fn project_columns(result: &SearchResult, columns: &[crate::velesql::Column]) -> serde_json::Value {
let mut map = serde_json::Map::new();
for col in columns {
let output_key = col.alias.as_deref().unwrap_or(&col.name);
let value = extract_field_value(result, &col.name);
map.insert(output_key.to_string(), value);
}
serde_json::Value::Object(map)
}
fn project_similarity_only(result: &SearchResult, expr: &SimilarityScoreExpr) -> serde_json::Value {
let mut map = serde_json::Map::new();
let key = expr.alias.as_deref().unwrap_or("similarity");
map.insert(
key.to_string(),
serde_json::Value::from(f64::from(result.score)),
);
serde_json::Value::Object(map)
}
fn project_mixed(
result: &SearchResult,
columns: &[crate::velesql::Column],
similarity_scores: &[SimilarityScoreExpr],
qualified_wildcards: &[String],
window_functions: &[crate::velesql::WindowFunction],
) -> serde_json::Value {
let mut map = serde_json::Map::new();
let window_aliases: rustc_hash::FxHashSet<&str> = window_functions
.iter()
.map(|wf| {
wf.alias
.as_deref()
.unwrap_or(wf.function_type.default_alias())
})
.collect();
if !qualified_wildcards.is_empty() {
insert_qualified_wildcards(&mut map, result, &window_aliases);
}
insert_named_columns(&mut map, result, columns);
insert_similarity_scores(&mut map, result, similarity_scores);
insert_window_values(&mut map, result, window_functions);
serde_json::Value::Object(map)
}
fn insert_qualified_wildcards(
map: &mut serde_json::Map<String, serde_json::Value>,
result: &SearchResult,
window_aliases: &rustc_hash::FxHashSet<&str>,
) {
map.insert("id".to_string(), serde_json::Value::from(result.point.id));
if let Some(serde_json::Value::Object(payload_map)) = result.point.payload.as_ref() {
for (k, v) in payload_map {
if k != "id" && !window_aliases.contains(k.as_str()) {
map.insert(k.clone(), v.clone());
}
}
}
}
fn insert_named_columns(
map: &mut serde_json::Map<String, serde_json::Value>,
result: &SearchResult,
columns: &[crate::velesql::Column],
) {
for col in columns {
let output_key = col.alias.as_deref().unwrap_or(&col.name);
let value = extract_field_value(result, &col.name);
map.insert(output_key.to_string(), value);
}
}
fn insert_similarity_scores(
map: &mut serde_json::Map<String, serde_json::Value>,
result: &SearchResult,
similarity_scores: &[SimilarityScoreExpr],
) {
for expr in similarity_scores {
let key = expr.alias.as_deref().unwrap_or("similarity");
map.insert(
key.to_string(),
serde_json::Value::from(f64::from(result.score)),
);
}
}
fn insert_window_values(
map: &mut serde_json::Map<String, serde_json::Value>,
result: &SearchResult,
window_functions: &[crate::velesql::WindowFunction],
) {
for wf in window_functions {
let alias = wf
.alias
.as_deref()
.unwrap_or(wf.function_type.default_alias());
let value = result
.point
.payload
.as_ref()
.and_then(|p| p.get(alias))
.cloned()
.unwrap_or(serde_json::Value::Null);
map.insert(alias.to_string(), value);
}
}
fn extract_field_value(result: &SearchResult, field_path: &str) -> serde_json::Value {
if field_path == "id" {
return serde_json::Value::from(result.point.id);
}
let Some(payload) = result.point.payload.as_ref() else {
return serde_json::Value::Null;
};
if field_path.contains('.') {
let mut current = payload;
for segment in field_path.split('.') {
match current.get(segment) {
Some(next) => current = next,
None => return serde_json::Value::Null,
}
}
current.clone()
} else {
payload
.get(field_path)
.cloned()
.unwrap_or(serde_json::Value::Null)
}
}
#[cfg(test)]
#[path = "projection_tests.rs"]
mod tests;