use super::{
ExternalReadOperator, ExternalReadResourceContract, ExternalReadResultBudget,
TextSeedExecutionOutput, TextSeedExecutionRequest, VectorIndexExecutionOutput,
VectorIndexExecutionRequest, VectorSeedExecutionOutput, VectorSeedExecutionRequest,
};
use crate::binding::Binding;
use crate::kernel::collect_bounded_operator_bindings_with_account;
use crate::memory::external_read_memory_budget;
use crate::observer::{ExecutionObserver, QueryExecutionObserver};
use crate::pipeline::{emit_owned_binding_batches, BatchControl, BindingBatch};
use crate::{ExecutionLimit, ExecutionMemoryConfig, QueryMemoryClass, QueryMemoryLedger};
use hawdb_core::{HawDBError, Result, RuntimeTaskContext, Value};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::num::NonZeroUsize;
#[derive(Clone, Copy)]
pub struct SeedReadContext<'a> {
pub parameters: &'a BTreeMap<String, Value>,
pub external: &'a dyn BatchExternalRead,
pub memory: &'a ExecutionMemoryConfig,
pub memory_ledger: &'a QueryMemoryLedger,
pub task_context: Option<&'a RuntimeTaskContext>,
pub observer: &'a QueryExecutionObserver,
}
pub type VectorSeedContext<'a> = SeedReadContext<'a>;
pub trait BatchExternalRead {
fn vector_index_definition(
&self,
_index: &str,
) -> Result<(
hawdb_core::VectorIndexDescriptor,
hawdb_core::VectorIndexReadIdentity,
)> {
Err(HawDBError::Execution(
"vector eligibility requires a bound index definition".into(),
))
}
fn execute_vector_index(
&self,
request: VectorIndexExecutionRequest<'_>,
) -> Result<Box<dyn VectorIndexExecutionOutput>> {
request.resources.checkpoint()?;
Err(HawDBError::Execution(
"vector index search requires a bound canonical/index read domain".into(),
))
}
fn execute_complete_text_seed(
&self,
request: TextSeedExecutionRequest<'_>,
) -> Result<TextSeedExecutionOutput> {
request.resources.checkpoint()?;
Err(HawDBError::Execution(
"complete text search requires a provider with source-exhaustion support".into(),
))
}
fn execute_text_seed(
&self,
request: TextSeedExecutionRequest<'_>,
) -> Result<TextSeedExecutionOutput> {
request.resources.checkpoint()?;
Err(HawDBError::Execution(
"text search capability is unavailable without a text projection".into(),
))
}
fn execute_vector_seed(
&self,
request: VectorSeedExecutionRequest<'_>,
) -> Result<VectorSeedExecutionOutput>;
}
pub struct BatchExternalReadAdapter<'a> {
external: RefCell<&'a mut dyn ExternalReadOperator>,
}
impl<'a> BatchExternalReadAdapter<'a> {
pub fn new(external: &'a mut dyn ExternalReadOperator) -> Self {
Self {
external: RefCell::new(external),
}
}
}
impl BatchExternalRead for BatchExternalReadAdapter<'_> {
fn vector_index_definition(
&self,
index: &str,
) -> Result<(
hawdb_core::VectorIndexDescriptor,
hawdb_core::VectorIndexReadIdentity,
)> {
self.external.borrow_mut().vector_index_definition(index)
}
fn execute_vector_index(
&self,
request: VectorIndexExecutionRequest<'_>,
) -> Result<Box<dyn VectorIndexExecutionOutput>> {
self.external.borrow_mut().execute_vector_index(request)
}
fn execute_complete_text_seed(
&self,
request: TextSeedExecutionRequest<'_>,
) -> Result<TextSeedExecutionOutput> {
self.external
.borrow_mut()
.execute_complete_text_seed(request)
}
fn execute_text_seed(
&self,
request: TextSeedExecutionRequest<'_>,
) -> Result<TextSeedExecutionOutput> {
self.external.borrow_mut().execute_text_seed(request)
}
fn execute_vector_seed(
&self,
request: VectorSeedExecutionRequest<'_>,
) -> Result<VectorSeedExecutionOutput> {
self.external.borrow_mut().execute_vector_seed(request)
}
}
fn vector_embedding_parameter(
parameters: &BTreeMap<String, Value>,
name: &str,
vector_plan: &hawdb_plan_cypher::VectorPhysicalPlan,
) -> Result<Vec<f32>> {
if let Some(Value::Vector(vector)) = parameters.get(name) {
if vector.dimension().get() as usize != vector_plan_embedding_dimension(vector_plan) {
return Err(HawDBError::Semantic(format!(
"vector search parameter '${name}' dimension changed after planning"
)));
}
return Ok(vector.as_ref().coordinates().collect());
}
let Some(Value::List(values)) = parameters.get(name) else {
return Err(HawDBError::Semantic(format!(
"vector search parameter '${name}' must be a numeric list"
)));
};
let embedding = values
.iter()
.map(|value| match value {
Value::Float(value) if value.is_finite() => {
let value = *value as f32;
if value.is_finite() {
Ok(value)
} else {
Err(HawDBError::Semantic(format!(
"vector search parameter '${name}' exceeds f32 range"
)))
}
}
Value::Int(value) => Ok(*value as f32),
_ => Err(HawDBError::Semantic(format!(
"vector search parameter '${name}' must contain finite numbers"
))),
})
.collect::<Result<Vec<_>>>()?;
let expected_dimension = vector_plan_embedding_dimension(vector_plan);
if embedding.len() != expected_dimension {
return Err(HawDBError::Semantic(format!(
"vector search parameter '${name}' dimension changed after planning"
)));
}
Ok(embedding)
}
fn vector_plan_embedding_dimension(plan: &hawdb_plan_cypher::VectorPhysicalPlan) -> usize {
match plan {
hawdb_plan_cypher::VectorPhysicalPlan::VectorCandidateScan {
embedding_dimension,
..
}
| hawdb_plan_cypher::VectorPhysicalPlan::RawVectorRerank {
embedding_dimension,
..
} => *embedding_dimension,
hawdb_plan_cypher::VectorPhysicalPlan::ResidualFilter { input, .. }
| hawdb_plan_cypher::VectorPhysicalPlan::TopK { input, .. } => {
vector_plan_embedding_dimension(input)
}
hawdb_plan_cypher::VectorPhysicalPlan::Filter { .. } => 0,
}
}
fn vector_plan_top_k(plan: &hawdb_plan_cypher::VectorPhysicalPlan) -> Option<usize> {
match plan {
hawdb_plan_cypher::VectorPhysicalPlan::TopK { limit, .. } => Some(*limit),
hawdb_plan_cypher::VectorPhysicalPlan::VectorCandidateScan { input, .. }
| hawdb_plan_cypher::VectorPhysicalPlan::RawVectorRerank { input, .. }
| hawdb_plan_cypher::VectorPhysicalPlan::ResidualFilter { input, .. } => {
vector_plan_top_k(input)
}
hawdb_plan_cypher::VectorPhysicalPlan::Filter { .. } => None,
}
}
pub struct VectorSeedScanSpec<'a> {
pub embedding_parameter: &'a str,
pub output_external_id: &'a bool,
pub metadata_filters: &'a BTreeMap<String, String>,
pub resource_profile: &'a hawdb_plan_cypher::VectorExecutionResourceProfile,
pub vector_plan: &'a hawdb_plan_cypher::VectorPhysicalPlan,
}
impl VectorSeedScanSpec<'_> {
pub fn stream(
self,
context: VectorSeedContext<'_>,
execution_limit: ExecutionLimit,
emit: &mut dyn FnMut(BindingBatch) -> Result<BatchControl>,
) -> Result<BatchControl> {
let Self {
embedding_parameter,
output_external_id,
metadata_filters,
resource_profile,
vector_plan,
} = self;
let max_rows = vector_plan_top_k(vector_plan)
.ok_or_else(|| {
HawDBError::Execution("vector seed physical plan is missing TopK".to_string())
})?
.min(execution_limit.output_rows.unwrap_or(usize::MAX));
if max_rows == 0 {
return emit_owned_binding_batches(Vec::new(), context.memory.batch_rows.get(), emit);
}
let embedding =
vector_embedding_parameter(context.parameters, embedding_parameter, vector_plan)?;
let external_memory = external_read_memory_budget(*resource_profile, context.memory);
let admitted_parallelism = context
.task_context
.map_or(1, |task_context| task_context.admitted_parallelism().get());
let resources = ExternalReadResourceContract {
priority: resource_profile.priority,
max_parallelism: NonZeroUsize::new(
resource_profile
.max_parallelism
.max(1)
.min(admitted_parallelism),
)
.expect("resolved external read parallelism is non-zero"),
max_working_memory_bytes: external_memory.max_working_bytes,
result: ExternalReadResultBudget {
max_rows,
max_memory_bytes: external_memory.max_result_bytes,
},
task_context: context.task_context,
};
let external_account = context.memory_ledger.account(
QueryMemoryClass::ExternalRead,
"VectorSeedScan external read",
NonZeroUsize::new(resources.reserved_memory_bytes())
.expect("external read reservation is non-zero"),
);
let _external_lease = external_account.reserve(resources.reserved_memory_bytes())?;
resources.checkpoint()?;
let output = context
.external
.execute_vector_seed(VectorSeedExecutionRequest {
embedding: &embedding,
metadata_filters,
vector_plan,
resources,
})?;
resources.checkpoint()?;
output.validate_result_budget(resources.result)?;
context.observer.record_vector_execution(output.report);
let mut bindings = collect_bounded_operator_bindings_with_account(
"VectorSeedScan",
output.rows.into_iter().map(|row| {
let mut values = BTreeMap::from([
("id".to_string(), Value::String(row.id)),
(
hawdb_plan_cypher::VECTOR_SEED_SCORE_COLUMN.to_string(),
Value::Float(row.score),
),
]);
if context.observer.seed_graph_scoring_input().is_some() {
crate::scoring::annotate_seed(&mut values, row.score);
}
if *output_external_id && let Some(external_id) = row.external_id {
values.insert("external_id".to_string(), Value::String(external_id));
}
Binding {
values,
nodes: BTreeMap::new(),
relationships: BTreeMap::new(),
}
}),
context.memory.blocking_operator_bytes,
context.memory_ledger.account(
QueryMemoryClass::BlockingState,
"VectorSeedScan",
context.memory.blocking_operator_bytes,
),
)?;
bindings.truncate(execution_limit.output_rows.unwrap_or(usize::MAX));
emit_owned_binding_batches(bindings, context.memory.batch_rows.get(), emit)
}
}
mod text;
pub use text::TextSeedScanSpec;
#[cfg(test)]
mod tests;