paimon_datafusion/
vector_search.rs1use std::any::Any;
19use std::fmt::Debug;
20use std::sync::Arc;
21
22use async_trait::async_trait;
23use datafusion::arrow::datatypes::SchemaRef as ArrowSchemaRef;
24use datafusion::catalog::Session;
25use datafusion::catalog::TableFunctionImpl;
26use datafusion::common::project_schema;
27use datafusion::datasource::{TableProvider, TableType};
28use datafusion::error::Result as DFResult;
29use datafusion::logical_expr::{Expr, TableProviderFilterPushDown};
30use datafusion::physical_plan::empty::EmptyExec;
31use datafusion::physical_plan::ExecutionPlan;
32use datafusion::prelude::SessionContext;
33use paimon::catalog::Catalog;
34
35use crate::error::to_datafusion_error;
36use crate::runtime::{await_with_runtime, block_on_with_runtime};
37use crate::table::{PaimonScanBuilder, PaimonTableProvider};
38use crate::table_function_args::{
39 extract_int_literal, extract_string_literal, parse_table_identifier,
40};
41
42const FUNCTION_NAME: &str = "vector_search";
43
44pub fn register_vector_search(
45 ctx: &SessionContext,
46 catalog: Arc<dyn Catalog>,
47 default_database: &str,
48) {
49 ctx.register_udtf(
50 "vector_search",
51 Arc::new(VectorSearchFunction::new(catalog, default_database)),
52 );
53}
54
55pub struct VectorSearchFunction {
56 catalog: Arc<dyn Catalog>,
57 default_database: String,
58}
59
60impl Debug for VectorSearchFunction {
61 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62 f.debug_struct("VectorSearchFunction")
63 .field("default_database", &self.default_database)
64 .finish()
65 }
66}
67
68impl VectorSearchFunction {
69 pub fn new(catalog: Arc<dyn Catalog>, default_database: &str) -> Self {
70 Self {
71 catalog,
72 default_database: default_database.to_string(),
73 }
74 }
75}
76
77impl TableFunctionImpl for VectorSearchFunction {
78 fn call(&self, args: &[Expr]) -> DFResult<Arc<dyn TableProvider>> {
79 if args.len() != 4 {
80 return Err(datafusion::error::DataFusionError::Plan(
81 "vector_search requires 4 arguments: (table_name, column_name, query_vector_json, limit)".to_string(),
82 ));
83 }
84
85 let table_name = extract_string_literal(FUNCTION_NAME, &args[0], "table_name")?;
86 let column_name = extract_string_literal(FUNCTION_NAME, &args[1], "column_name")?;
87 let query_vector_json =
88 extract_string_literal(FUNCTION_NAME, &args[2], "query_vector_json")?;
89 let limit = extract_int_literal(FUNCTION_NAME, &args[3], "limit")?;
90
91 if limit <= 0 {
92 return Err(datafusion::error::DataFusionError::Plan(
93 "vector_search: limit must be positive".to_string(),
94 ));
95 }
96
97 let query_vector: Vec<f32> = serde_json::from_str(&query_vector_json).map_err(|e| {
98 datafusion::error::DataFusionError::Plan(format!(
99 "vector_search: query_vector_json must be a JSON array of floats, got '{}': {}",
100 query_vector_json, e
101 ))
102 })?;
103
104 if query_vector.is_empty() {
105 return Err(datafusion::error::DataFusionError::Plan(
106 "vector_search: query vector cannot be empty".to_string(),
107 ));
108 }
109
110 let identifier =
111 parse_table_identifier(FUNCTION_NAME, &table_name, &self.default_database)?;
112
113 let catalog = Arc::clone(&self.catalog);
114 let table = block_on_with_runtime(
115 async move { catalog.get_table(&identifier).await },
116 "vector_search: catalog access thread panicked",
117 )
118 .map_err(to_datafusion_error)?;
119
120 let inner = PaimonTableProvider::try_new(table)?;
121
122 Ok(Arc::new(VectorSearchTableProvider {
123 inner,
124 column_name,
125 query_vector,
126 limit: limit as usize,
127 }))
128 }
129}
130
131#[derive(Debug)]
132struct VectorSearchTableProvider {
133 inner: PaimonTableProvider,
134 column_name: String,
135 query_vector: Vec<f32>,
136 limit: usize,
137}
138
139#[async_trait]
140impl TableProvider for VectorSearchTableProvider {
141 fn as_any(&self) -> &dyn Any {
142 self
143 }
144
145 fn schema(&self) -> ArrowSchemaRef {
146 self.inner.schema()
147 }
148
149 fn table_type(&self) -> TableType {
150 TableType::Base
151 }
152
153 async fn scan(
154 &self,
155 state: &dyn Session,
156 projection: Option<&Vec<usize>>,
157 _filters: &[Expr],
158 limit: Option<usize>,
159 ) -> DFResult<Arc<dyn ExecutionPlan>> {
160 let table = self.inner.table();
161
162 let row_ranges = await_with_runtime(async {
163 let mut builder = table.new_vector_search_builder();
164 builder
165 .with_vector_column(&self.column_name)
166 .with_query_vector(self.query_vector.clone())
167 .with_limit(self.limit);
168 builder.execute().await.map_err(to_datafusion_error)
169 })
170 .await?;
171
172 if row_ranges.is_empty() {
173 let schema = project_schema(&self.schema(), projection)?;
174 return Ok(Arc::new(EmptyExec::new(schema)));
175 }
176
177 let mut read_builder = table.new_read_builder();
178 if let Some(limit) = limit {
179 read_builder.with_limit(limit);
180 }
181 let scan = read_builder.new_scan().with_row_ranges(row_ranges);
182 let plan = await_with_runtime(scan.plan())
183 .await
184 .map_err(to_datafusion_error)?;
185
186 let target = state.config_options().execution.target_partitions;
187 PaimonScanBuilder {
188 table,
189 schema: &self.schema(),
190 plan: &plan,
191 projection,
192 pushed_predicate: None,
193 limit,
194 target_partitions: target,
195 filter_exact: false,
196 }
197 .build()
198 }
199
200 fn supports_filters_pushdown(
201 &self,
202 filters: &[&Expr],
203 ) -> DFResult<Vec<TableProviderFilterPushDown>> {
204 Ok(vec![
205 TableProviderFilterPushDown::Unsupported;
206 filters.len()
207 ])
208 }
209}