1use linkrs_core::types::expr::contextual::ContextualExpression;
7use linkrs_core::types::span::Span;
8use linkrs_core::types::FulltextEngineType;
9use linkrs_core::Value;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12
13use super::stmt::OrderByClause;
14use super::types::{LimitClause, SkipClause};
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct CreateFulltextIndex {
23 pub span: Span,
24 pub index_name: String,
25 pub schema_name: String,
26 pub fields: Vec<IndexFieldDef>,
27 pub engine_type: FulltextEngineType,
28 pub options: IndexOptions,
29 pub if_not_exists: bool,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct IndexFieldDef {
35 pub field_name: String,
36 pub analyzer: Option<String>,
37 pub boost: Option<f32>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct IndexOptions {
43 pub bm25_config: Option<BM25Options>,
44 pub common_options: HashMap<String, Value>,
45}
46
47#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct BM25Options {
50 pub k1: Option<f32>,
51 pub b: Option<f32>,
52 pub field_weights: HashMap<String, f32>,
53 pub analyzer: Option<String>,
54 pub store_original: Option<bool>,
55}
56
57#[derive(Debug, Clone, Serialize, Deserialize)]
59pub struct DropFulltextIndex {
60 pub span: Span,
61 pub index_name: String,
62 pub if_exists: bool,
63}
64
65#[derive(Debug, Clone, Serialize, Deserialize)]
67pub struct AlterFulltextIndex {
68 pub span: Span,
69 pub index_name: String,
70 pub actions: Vec<AlterIndexAction>,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub enum AlterIndexAction {
76 AddField(IndexFieldDef),
77 DropField(String),
78 SetOption(String, Value),
79 Rebuild,
80 Optimize,
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ShowFulltextIndex {
86 pub span: Span,
87 pub pattern: Option<String>,
88 pub from_schema: Option<String>,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DescribeFulltextIndex {
94 pub span: Span,
95 pub index_name: String,
96}
97
98#[derive(Debug, Clone)]
104pub struct SearchStatement {
105 pub span: Span,
106 pub index_name: String,
107 pub query: FulltextQueryExpr,
108 pub yield_clause: Option<FulltextYieldClause>,
109 pub where_clause: Option<ContextualExpression>,
110 pub order_clause: Option<OrderByClause>,
111 pub limit: Option<LimitClause>,
112 pub skip: Option<SkipClause>,
113}
114
115#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum FulltextQueryExpr {
118 Simple(String),
120 Field(String, String),
122 MultiField(Vec<(String, String)>),
124 Boolean {
126 must: Vec<FulltextQueryExpr>,
127 should: Vec<FulltextQueryExpr>,
128 must_not: Vec<FulltextQueryExpr>,
129 },
130 Phrase(String),
132 Prefix(String),
134 Fuzzy(String, Option<u8>),
136 Range {
138 field: String,
139 lower: Option<String>,
140 upper: Option<String>,
141 include_lower: bool,
142 include_upper: bool,
143 },
144 Wildcard(String),
146}
147
148#[derive(Debug, Clone)]
150pub struct FulltextYieldClause {
151 pub items: Vec<FulltextYieldItem>,
152}
153
154#[derive(Debug, Clone)]
156pub struct FulltextYieldItem {
157 pub expr: ContextualExpression,
158 pub alias: Option<String>,
159}
160
161#[derive(Debug, Clone)]
167pub struct MatchFulltext {
168 pub span: Span,
169 pub pattern: String,
170 pub fulltext_condition: FulltextMatchCondition,
171 pub yield_clause: Option<FulltextYieldClause>,
172}
173
174#[derive(Debug, Clone, Serialize, Deserialize)]
176pub struct FulltextMatchCondition {
177 pub field: String,
178 pub query: String,
179 pub index_name: Option<String>,
180}
181
182#[derive(Debug, Clone)]
184pub struct LookupFulltext {
185 pub span: Span,
186 pub schema_name: String,
187 pub index_name: String,
188 pub query: String,
189 pub yield_clause: Option<FulltextYieldClause>,
190 pub limit: Option<usize>,
191}
192
193impl CreateFulltextIndex {
198 pub fn new(
199 span: Span,
200 index_name: String,
201 schema_name: String,
202 fields: Vec<IndexFieldDef>,
203 engine_type: FulltextEngineType,
204 ) -> Self {
205 Self {
206 span,
207 index_name,
208 schema_name,
209 fields,
210 engine_type,
211 options: IndexOptions {
212 bm25_config: None,
213 common_options: HashMap::new(),
214 },
215 if_not_exists: false,
216 }
217 }
218
219 pub fn with_if_not_exists(mut self, if_not_exists: bool) -> Self {
220 self.if_not_exists = if_not_exists;
221 self
222 }
223
224 pub fn with_bm25_options(mut self, options: BM25Options) -> Self {
225 self.options.bm25_config = Some(options);
226 self
227 }
228}
229
230impl SearchStatement {
231 pub fn new(index_name: String, query: FulltextQueryExpr) -> Self {
232 Self {
233 span: Span::default(),
234 index_name,
235 query,
236 yield_clause: None,
237 where_clause: None,
238 order_clause: None,
239 limit: None,
240 skip: None,
241 }
242 }
243
244 pub fn with_yield(mut self, yield_clause: FulltextYieldClause) -> Self {
245 self.yield_clause = Some(yield_clause);
246 self
247 }
248
249 pub fn with_where(mut self, where_clause: ContextualExpression) -> Self {
250 self.where_clause = Some(where_clause);
251 self
252 }
253
254 pub fn with_order(mut self, order_clause: OrderByClause) -> Self {
255 self.order_clause = Some(order_clause);
256 self
257 }
258
259 pub fn with_limit(mut self, limit: LimitClause) -> Self {
260 self.limit = Some(limit);
261 self
262 }
263
264 pub fn with_skip(mut self, skip: SkipClause) -> Self {
265 self.skip = Some(skip);
266 self
267 }
268}
269
270impl FulltextYieldClause {
271 pub fn new(items: Vec<FulltextYieldItem>) -> Self {
272 Self { items }
273 }
274
275 pub fn single(expr: ContextualExpression) -> Self {
276 Self {
277 items: vec![FulltextYieldItem { expr, alias: None }],
278 }
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285
286 #[test]
287 fn test_create_fulltext_index() {
288 let fields = vec![IndexFieldDef {
289 field_name: "content".to_string(),
290 analyzer: None,
291 boost: None,
292 }];
293
294 let create = CreateFulltextIndex::new(
295 Span::default(),
296 "idx_article_content".to_string(),
297 "article".to_string(),
298 fields,
299 FulltextEngineType::Bm25,
300 );
301
302 assert_eq!(create.index_name, "idx_article_content");
303 assert_eq!(create.schema_name, "article");
304 assert_eq!(create.engine_type, FulltextEngineType::Bm25);
305 }
306
307 #[test]
308 fn test_search_statement() {
309 let query = FulltextQueryExpr::Simple("database".to_string());
310 let search = SearchStatement::new("idx_article".into(), query);
311
312 assert!(matches!(search.query, FulltextQueryExpr::Simple(_)));
313 }
314
315 #[test]
316 fn test_yield_clause() {
317 use linkrs_core::types::expr::expression_context::ExpressionAnalysisContext;
318 use linkrs_core::types::expr::{ContextualExpression, Expression, ExpressionMeta};
319 use std::sync::Arc;
320
321 let expr_ctx = Arc::new(ExpressionAnalysisContext::new());
322 let expr = Expression::Function {
323 name: "score".to_string(),
324 args: vec![],
325 };
326 let meta = ExpressionMeta::new(expr);
327 let id = expr_ctx.register_expression(meta);
328 let yield_clause = FulltextYieldClause::single(ContextualExpression::new(id, expr_ctx));
329 assert_eq!(yield_clause.items.len(), 1);
330 }
331
332 #[test]
333 fn test_boolean_query() {
334 let must = vec![FulltextQueryExpr::Simple("database".to_string())];
335 let should = vec![];
336 let must_not = vec![];
337
338 let query = FulltextQueryExpr::Boolean {
339 must,
340 should,
341 must_not,
342 };
343
344 assert!(matches!(query, FulltextQueryExpr::Boolean { .. }));
345 }
346}