Skip to main content

paimon_datafusion/
vector_search.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use 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}