Skip to main content

reddb_server/application/
query.rs

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