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, ollama, openrouter, \
160                         together, venice, red-router, or a custom base URL).",
161                        provider.token()
162                    )));
163                }
164                let crate::ai::AiConnection { api_key, api_base } =
165                    crate::ai::resolve_connection_from_runtime(&provider, None, self)?;
166                let model = std::env::var(format!(
167                    "REDDB_{}_EMBEDDING_MODEL",
168                    provider.token().replace('-', "_").to_ascii_uppercase()
169                ))
170                .ok()
171                .or_else(|| std::env::var("REDDB_OPENAI_EMBEDDING_MODEL").ok())
172                .filter(|v| !v.trim().is_empty())
173                .unwrap_or_else(|| provider.default_embedding_model().to_string());
174                let transport = crate::runtime::ai::transport::AiTransport::new(
175                    crate::runtime::ai::transport::AiTransportConfig::default(),
176                );
177                let request = crate::ai::OpenAiEmbeddingRequest {
178                    api_key,
179                    model,
180                    inputs: vec![text],
181                    dimensions: None,
182                    api_base,
183                };
184                let response = crate::runtime::ai::block_on_ai(async move {
185                    crate::ai::openai_embeddings_async(&transport, request).await
186                })
187                .and_then(|result| result)?;
188                input.vector = response.embeddings.into_iter().next().ok_or_else(|| {
189                    crate::RedDBError::Query("embedding API returned no vectors".to_string())
190                })?;
191            }
192        }
193        self.search_similar(&input.collection, &input.vector, input.k, input.min_score)
194    }
195
196    pub fn search_ivf_input(&self, input: SearchIvfInput) -> RedDBResult<RuntimeIvfSearchResult> {
197        RedDBRuntime::search_ivf(
198            self,
199            &input.collection,
200            &input.vector,
201            input.k,
202            input.n_lists,
203            input.n_probes,
204        )
205    }
206
207    pub fn search_text_input(&self, input: SearchTextInput) -> RedDBResult<DslQueryResult> {
208        RedDBRuntime::search_text(
209            self,
210            input.query,
211            input.collections,
212            input.entity_types,
213            input.capabilities,
214            input.fields,
215            input.limit,
216            input.fuzzy,
217        )
218    }
219
220    pub fn search_multimodal_input(
221        &self,
222        input: SearchMultimodalInput,
223    ) -> RedDBResult<DslQueryResult> {
224        RedDBRuntime::search_multimodal(
225            self,
226            input.query,
227            input.collections,
228            input.entity_types,
229            input.capabilities,
230            input.limit,
231        )
232    }
233
234    pub fn search_index_input(&self, input: SearchIndexInput) -> RedDBResult<DslQueryResult> {
235        RedDBRuntime::search_index(
236            self,
237            input.index,
238            input.value,
239            input.exact,
240            input.collections,
241            input.entity_types,
242            input.capabilities,
243            input.limit,
244        )
245    }
246
247    pub fn search_hybrid_input(&self, input: SearchHybridInput) -> RedDBResult<DslQueryResult> {
248        RedDBRuntime::search_hybrid(
249            self,
250            input.vector,
251            input.query,
252            input.k,
253            input.collections,
254            input.entity_types,
255            input.capabilities,
256            input.graph_pattern,
257            input.filters,
258            input.weights,
259            input.min_score,
260            input.limit,
261        )
262    }
263
264    pub fn search_context_input(
265        &self,
266        input: SearchContextInput,
267    ) -> RedDBResult<ContextSearchResult> {
268        RedDBRuntime::search_context(self, input)
269    }
270}