Skip to main content

linkrs_query/parser/ast/
fulltext.rs

1//! Full-Text Search AST Definitions
2//!
3//! This module defines the Abstract Syntax Tree (AST) nodes for full-text search queries,
4//! including CREATE FULLTEXT INDEX, SEARCH, and related statements.
5
6use 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// ============================================================================
17// Full-Text Index DDL Statements
18// ============================================================================
19
20/// CREATE FULLTEXT INDEX statement
21#[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/// Index field definition
33#[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/// Index options
41#[derive(Debug, Clone, Serialize, Deserialize)]
42pub struct IndexOptions {
43    pub bm25_config: Option<BM25Options>,
44    pub common_options: HashMap<String, Value>,
45}
46
47/// BM25 specific options
48#[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/// DROP FULLTEXT INDEX statement
58#[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/// ALTER FULLTEXT INDEX statement
66#[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/// Alter index action
74#[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/// SHOW FULLTEXT INDEX statement
84#[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/// DESCRIBE FULLTEXT INDEX statement
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct DescribeFulltextIndex {
94    pub span: Span,
95    pub index_name: String,
96}
97
98// ============================================================================
99// Full-Text Search DML Statements
100// ============================================================================
101
102/// SEARCH statement
103#[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/// Full-text query expression
116#[derive(Debug, Clone, Serialize, Deserialize)]
117pub enum FulltextQueryExpr {
118    /// Simple text query: MATCH 'database'
119    Simple(String),
120    /// Field-specific query: title:'database'
121    Field(String, String),
122    /// Multi-field query: title:'database' OR content:'database'
123    MultiField(Vec<(String, String)>),
124    /// Boolean query: title:'database' AND content:'optimization'
125    Boolean {
126        must: Vec<FulltextQueryExpr>,
127        should: Vec<FulltextQueryExpr>,
128        must_not: Vec<FulltextQueryExpr>,
129    },
130    /// Phrase query: "database optimization"
131    Phrase(String),
132    /// Prefix query: data*
133    Prefix(String),
134    /// Fuzzy query: database~
135    Fuzzy(String, Option<u8>),
136    /// Range query: [2020 TO 2023]
137    Range {
138        field: String,
139        lower: Option<String>,
140        upper: Option<String>,
141        include_lower: bool,
142        include_upper: bool,
143    },
144    /// Wildcard query: data*ase
145    Wildcard(String),
146}
147
148/// YIELD clause for full-text search
149#[derive(Debug, Clone)]
150pub struct FulltextYieldClause {
151    pub items: Vec<FulltextYieldItem>,
152}
153
154/// Yield item for full-text search
155#[derive(Debug, Clone)]
156pub struct FulltextYieldItem {
157    pub expr: ContextualExpression,
158    pub alias: Option<String>,
159}
160
161// ============================================================================
162// MATCH clause extensions for full-text search
163// ============================================================================
164
165/// MATCH with full-text search
166#[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/// Full-text match condition in WHERE clause
175#[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/// LOOKUP with full-text search
183#[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
193// ============================================================================
194// Helper Functions
195// ============================================================================
196
197impl 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}