Skip to main content

reddb_server/application/
query.rs

1use crate::runtime::{
2    ContextSearchResult, RedDBRuntime, RuntimeFilter, RuntimeGraphPattern, RuntimeIvfSearchResult,
3    RuntimeQueryExplain, RuntimeQueryResult, RuntimeQueryWeights, ScanCursor, ScanPage,
4};
5use crate::storage::unified::devx::SimilarResult;
6use crate::storage::unified::dsl::QueryResult as DslQueryResult;
7use crate::RedDBResult;
8
9#[derive(Debug, Clone)]
10pub struct ExecuteQueryInput {
11    pub query: String,
12}
13
14#[derive(Debug, Clone)]
15pub struct ExplainQueryInput {
16    pub query: String,
17}
18
19#[derive(Debug, Clone)]
20pub struct ScanCollectionInput {
21    pub collection: String,
22    pub offset: usize,
23    pub limit: usize,
24}
25
26#[derive(Debug, Clone)]
27pub struct SearchSimilarInput {
28    pub collection: String,
29    pub vector: Vec<f32>,
30    pub k: usize,
31    pub min_score: f32,
32    /// Optional text for semantic search (generates embedding on-the-fly)
33    pub text: Option<String>,
34    /// AI provider for semantic search (default: "openai")
35    pub provider: Option<String>,
36}
37
38#[derive(Debug, Clone)]
39pub struct SearchIvfInput {
40    pub collection: String,
41    pub vector: Vec<f32>,
42    pub k: usize,
43    pub n_lists: usize,
44    pub n_probes: Option<usize>,
45}
46
47#[derive(Debug, Clone)]
48pub struct SearchTextInput {
49    pub query: String,
50    pub collections: Option<Vec<String>>,
51    pub entity_types: Option<Vec<String>>,
52    pub capabilities: Option<Vec<String>>,
53    pub fields: Option<Vec<String>>,
54    pub limit: Option<usize>,
55    pub fuzzy: bool,
56}
57
58#[derive(Debug, Clone)]
59pub struct SearchMultimodalInput {
60    pub query: String,
61    pub collections: Option<Vec<String>>,
62    pub entity_types: Option<Vec<String>>,
63    pub capabilities: Option<Vec<String>>,
64    pub limit: Option<usize>,
65}
66
67#[derive(Debug, Clone)]
68pub struct SearchIndexInput {
69    pub index: String,
70    pub value: String,
71    pub exact: bool,
72    pub collections: Option<Vec<String>>,
73    pub entity_types: Option<Vec<String>>,
74    pub capabilities: Option<Vec<String>>,
75    pub limit: Option<usize>,
76}
77
78#[derive(Debug, Clone)]
79pub struct SearchHybridInput {
80    pub vector: Option<Vec<f32>>,
81    pub query: Option<String>,
82    pub k: Option<usize>,
83    pub collections: Option<Vec<String>>,
84    pub entity_types: Option<Vec<String>>,
85    pub capabilities: Option<Vec<String>>,
86    pub graph_pattern: Option<RuntimeGraphPattern>,
87    pub filters: Vec<RuntimeFilter>,
88    pub weights: Option<RuntimeQueryWeights>,
89    pub min_score: Option<f32>,
90    pub limit: Option<usize>,
91}
92
93#[derive(Debug, Clone)]
94pub struct SearchContextInput {
95    pub query: String,
96    pub field: Option<String>,
97    pub vector: Option<Vec<f32>>,
98    pub collections: Option<Vec<String>>,
99    pub graph_depth: Option<usize>,
100    pub graph_max_edges: Option<usize>,
101    pub max_cross_refs: Option<usize>,
102    pub follow_cross_refs: Option<bool>,
103    pub expand_graph: Option<bool>,
104    pub global_scan: Option<bool>,
105    pub reindex: Option<bool>,
106    pub limit: Option<usize>,
107    pub min_score: Option<f32>,
108}
109
110impl RedDBRuntime {
111    pub fn execute(&self, input: ExecuteQueryInput) -> RedDBResult<RuntimeQueryResult> {
112        self.execute_query(&input.query)
113    }
114
115    pub fn explain(&self, input: ExplainQueryInput) -> RedDBResult<RuntimeQueryExplain> {
116        self.explain_query(&input.query)
117    }
118
119    pub fn scan(&self, input: ScanCollectionInput) -> RedDBResult<ScanPage> {
120        self.scan_collection(
121            &input.collection,
122            Some(ScanCursor {
123                offset: input.offset,
124            }),
125            input.limit,
126        )
127    }
128
129    pub fn search_similar_input(
130        &self,
131        mut input: SearchSimilarInput,
132    ) -> RedDBResult<Vec<SimilarResult>> {
133        // Semantic search: if text provided, generate embedding on-the-fly
134        if let Some(text) = input.text.take() {
135            if input.vector.is_empty() {
136                let provider = match input.provider.as_deref() {
137                    Some(p) => crate::ai::parse_provider(p)?,
138                    None => {
139                        let name = std::env::var("REDDB_AI_PROVIDER")
140                            .ok()
141                            .unwrap_or_else(|| "openai".to_string());
142                        crate::ai::parse_provider(&name)?
143                    }
144                };
145                // S3 / #711: planner-level provider gate runs before the
146                // compatibility check + key resolver so neither emits
147                // side-effects for a policy-denied query.
148                crate::runtime::ai::provider_gate::enforce(self, &provider)?;
149                // Gate non-OpenAI-compatible providers before we spend
150                // cycles resolving a key — Anthropic has no embeddings
151                // endpoint, HuggingFace uses a different wire shape,
152                // Local needs the `local-models` feature flag.
153                if matches!(provider, crate::ai::AiProvider::Local) {
154                    return Err(crate::ai::local_embeddings_unavailable_error());
155                }
156                if !provider.is_openai_compatible() {
157                    return Err(crate::RedDBError::Query(format!(
158                        "SEARCH SIMILAR: embeddings are not yet available for provider '{}'. \
159                         Use an OpenAI-compatible provider (openai, groq, ollama, openrouter, \
160                         together, venice, deepseek, or a custom base URL).",
161                        provider.token()
162                    )));
163                }
164                let api_key = crate::ai::resolve_api_key_from_runtime(&provider, None, self)?;
165                let model = std::env::var(format!(
166                    "REDDB_{}_EMBEDDING_MODEL",
167                    provider.token().to_ascii_uppercase()
168                ))
169                .ok()
170                .or_else(|| std::env::var("REDDB_OPENAI_EMBEDDING_MODEL").ok())
171                .filter(|v| !v.trim().is_empty())
172                .unwrap_or_else(|| provider.default_embedding_model().to_string());
173                let transport = crate::runtime::ai::transport::AiTransport::new(
174                    crate::runtime::ai::transport::AiTransportConfig::default(),
175                );
176                let request = crate::ai::OpenAiEmbeddingRequest {
177                    api_key,
178                    model,
179                    inputs: vec![text],
180                    dimensions: None,
181                    api_base: provider.resolve_api_base(),
182                };
183                let response = crate::runtime::ai::block_on_ai(async move {
184                    crate::ai::openai_embeddings_async(&transport, request).await
185                })
186                .and_then(|result| result)?;
187                input.vector = response.embeddings.into_iter().next().ok_or_else(|| {
188                    crate::RedDBError::Query("embedding API returned no vectors".to_string())
189                })?;
190            }
191        }
192        self.search_similar(&input.collection, &input.vector, input.k, input.min_score)
193    }
194
195    pub fn search_ivf_input(&self, input: SearchIvfInput) -> RedDBResult<RuntimeIvfSearchResult> {
196        RedDBRuntime::search_ivf(
197            self,
198            &input.collection,
199            &input.vector,
200            input.k,
201            input.n_lists,
202            input.n_probes,
203        )
204    }
205
206    pub fn search_text_input(&self, input: SearchTextInput) -> RedDBResult<DslQueryResult> {
207        RedDBRuntime::search_text(
208            self,
209            input.query,
210            input.collections,
211            input.entity_types,
212            input.capabilities,
213            input.fields,
214            input.limit,
215            input.fuzzy,
216        )
217    }
218
219    pub fn search_multimodal_input(
220        &self,
221        input: SearchMultimodalInput,
222    ) -> RedDBResult<DslQueryResult> {
223        RedDBRuntime::search_multimodal(
224            self,
225            input.query,
226            input.collections,
227            input.entity_types,
228            input.capabilities,
229            input.limit,
230        )
231    }
232
233    pub fn search_index_input(&self, input: SearchIndexInput) -> RedDBResult<DslQueryResult> {
234        RedDBRuntime::search_index(
235            self,
236            input.index,
237            input.value,
238            input.exact,
239            input.collections,
240            input.entity_types,
241            input.capabilities,
242            input.limit,
243        )
244    }
245
246    pub fn search_hybrid_input(&self, input: SearchHybridInput) -> RedDBResult<DslQueryResult> {
247        RedDBRuntime::search_hybrid(
248            self,
249            input.vector,
250            input.query,
251            input.k,
252            input.collections,
253            input.entity_types,
254            input.capabilities,
255            input.graph_pattern,
256            input.filters,
257            input.weights,
258            input.min_score,
259            input.limit,
260        )
261    }
262
263    pub fn search_context_input(
264        &self,
265        input: SearchContextInput,
266    ) -> RedDBResult<ContextSearchResult> {
267        RedDBRuntime::search_context(self, input)
268    }
269}