1use crate::physical::types::{OperatorResult, PhysicalOperatorExec};
3use akar_common::types::{PhysicalTypeID, Value};
4use akar_common::vector::{DataChunk, ValueVector};
5use akar_storage::table::TableCatalog;
6use std::sync::Arc;
7
8pub struct PhysicalCountRelTable {
13 pub table_name: String,
14 pub table_id: u64,
15 pub table_catalog: Option<Arc<TableCatalog>>,
16}
17
18impl PhysicalOperatorExec for PhysicalCountRelTable {
19 fn operator_type(&self) -> &str {
20 "count_rel_table"
21 }
22
23 fn execute(&self, _input: Vec<DataChunk>) -> OperatorResult {
24 let tc = self
25 .table_catalog
26 .as_ref()
27 .ok_or_else(|| "No table catalog for CountRelTable".to_string())?;
28
29 let count = if let Some(table) = tc.get_rel_table(self.table_id) {
30 table.num_rows as i64
31 } else {
32 0
33 };
34
35 let mut v = ValueVector::new(PhysicalTypeID::Int64, 1);
36 v.resize(1);
37 v.set_i64(0, count);
38 let arr = akar_common::arrow_vector::ArrowVector::from_legacy(&v).array;
39 Ok(vec![DataChunk::new(vec![arr], vec![PhysicalTypeID::Int64])])
40 }
41}
42
43pub struct PhysicalCreateFtsIndex {
48 pub index_name: String,
49 pub table_name: String,
50 pub column_name: String,
51 pub docs_table: String,
52 pub terms_table: String,
53 pub posting_table: String,
54 pub table_catalog: Arc<TableCatalog>,
55}
56
57impl PhysicalOperatorExec for PhysicalCreateFtsIndex {
58 fn operator_type(&self) -> &str {
59 "create_fts_index"
60 }
61
62 fn execute(&self, _input: Vec<DataChunk>) -> OperatorResult {
63 let (col_idx, num_rows, source_data) = {
71 let source_table = match self.table_catalog.get_node_table_by_name(&self.table_name) {
72 Some(t) => t,
73 None => return Err(format!("Table '{}' not found", self.table_name).into()),
74 };
75 let col_idx = source_table
76 .columns
77 .iter()
78 .position(|c| c.name == self.column_name)
79 .ok_or_else(|| format!("Column '{}' not found in '{}'", self.column_name, self.table_name))?;
80 (
81 col_idx,
82 source_table.num_rows as usize,
83 source_table.to_column_major_data(),
84 )
85 };
86
87 if self.table_catalog.get_node_table_by_name(&self.docs_table).is_none() {
89 let docs_cols = vec![
90 akar_storage::table::ColumnDefinition {
91 name: "doc_id".into(),
92 logical_type: akar_common::types::LogicalTypeID::Int64,
93 is_primary_key: true,
94 compression: akar_common::enums::CompressionType::Uncompressed,
95 },
96 akar_storage::table::ColumnDefinition {
97 name: "text".into(),
98 logical_type: akar_common::types::LogicalTypeID::String,
99 is_primary_key: false,
100 compression: akar_common::enums::CompressionType::Uncompressed,
101 },
102 ];
103 self.table_catalog.create_node_table(self.docs_table.clone(), docs_cols);
104 }
105 if self.table_catalog.get_node_table_by_name(&self.terms_table).is_none() {
106 let terms_cols = vec![
107 akar_storage::table::ColumnDefinition {
108 name: "term_id".into(),
109 logical_type: akar_common::types::LogicalTypeID::Int64,
110 is_primary_key: true,
111 compression: akar_common::enums::CompressionType::Uncompressed,
112 },
113 akar_storage::table::ColumnDefinition {
114 name: "term".into(),
115 logical_type: akar_common::types::LogicalTypeID::String,
116 is_primary_key: false,
117 compression: akar_common::enums::CompressionType::Uncompressed,
118 },
119 akar_storage::table::ColumnDefinition {
120 name: "doc_freq".into(),
121 logical_type: akar_common::types::LogicalTypeID::Int64,
122 is_primary_key: false,
123 compression: akar_common::enums::CompressionType::Uncompressed,
124 },
125 ];
126 self.table_catalog
127 .create_node_table(self.terms_table.clone(), terms_cols);
128 }
129
130 let mut term_map: std::collections::HashMap<String, (i64, i64)> = std::collections::HashMap::new();
132 let mut doc_rows: Vec<Vec<Value>> = Vec::new();
134 let mut postings: Vec<(i64, i64, i64)> = Vec::new();
136
137 for row_idx in 0..num_rows {
138 let text = if let Some(col_data) = source_data.get(col_idx) {
139 if let Some(Value::String(s)) = col_data.get(row_idx) {
140 s.clone()
141 } else {
142 String::new()
143 }
144 } else {
145 String::new()
146 };
147
148 let doc_id = row_idx as i64;
149 doc_rows.push(vec![Value::Int64(doc_id), Value::String(text.clone())]);
150
151 let tokens = akar_fts::tokenize(&text);
153 let mut freq_map: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
154 for token in tokens {
155 let stemmed = akar_fts::stem_word(&token);
156 if !akar_fts::STOP_WORDS.contains(&stemmed.as_str()) {
157 *freq_map.entry(stemmed).or_insert(0) += 1;
158 }
159 }
160
161 for (term, freq) in freq_map {
162 let next_id = term_map.len() as i64;
163 let (term_id, doc_freq) = term_map.entry(term).or_insert((next_id, 0));
164 *doc_freq += 1;
165 postings.push((*term_id, doc_id, freq));
166 }
167 }
168
169 {
171 let mut docs_table = self.table_catalog.get_node_table_by_name_mut(&self.docs_table).unwrap();
172 for row in doc_rows {
173 docs_table.insert_row(row)?;
174 }
175 }
176
177 if self.table_catalog.get_node_table_by_name(&self.terms_table).is_some() {
179 let mut terms_table = self
180 .table_catalog
181 .get_node_table_by_name_mut(&self.terms_table)
182 .unwrap();
183 let mut term_list: Vec<(String, i64, i64)> =
184 term_map.into_iter().map(|(t, (id, df))| (t, id, df)).collect();
185 term_list.sort_by_key(|(_, id, _)| *id);
186 for (term, term_id, doc_freq) in term_list {
187 terms_table.insert_row(vec![Value::Int64(term_id), Value::String(term), Value::Int64(doc_freq)])?;
188 }
189 }
190
191 let docs_table_id = self
193 .table_catalog
194 .get_node_table_by_name(&self.docs_table)
195 .unwrap()
196 .table_id;
197 let terms_table_id = self
198 .table_catalog
199 .get_node_table_by_name(&self.terms_table)
200 .unwrap()
201 .table_id;
202
203 if self.table_catalog.get_rel_table_by_name(&self.posting_table).is_none() {
204 let posting_cols = vec![akar_storage::table::ColumnDefinition {
205 name: "term_freq".into(),
206 logical_type: akar_common::types::LogicalTypeID::Int64,
207 is_primary_key: false,
208 compression: akar_common::enums::CompressionType::Uncompressed,
209 }];
210 self.table_catalog.create_rel_table(
212 self.posting_table.clone(),
213 terms_table_id,
214 docs_table_id,
215 posting_cols,
216 );
217 }
218
219 {
220 let mut posting_table = self
221 .table_catalog
222 .get_rel_table_by_name_mut(&self.posting_table)
223 .unwrap();
224 for (term_id, doc_id, freq) in postings {
225 posting_table.insert_rel(term_id as u64, doc_id as u64, vec![Value::Int64(freq)])?;
226 }
227 }
228
229 let mut result_vec = akar_common::vector::ValueVector::new(akar_common::types::PhysicalTypeID::String, 1);
230 result_vec.resize(1);
231 result_vec
232 .set_value(
233 0,
234 &Value::String(format!("FTS index '{}' built successfully.", self.index_name)),
235 )
236 .unwrap();
237 let arr = akar_common::arrow_vector::ArrowVector::from_legacy(&result_vec).array;
238 let mut result = DataChunk::new(vec![arr], vec![akar_common::types::PhysicalTypeID::String]);
239 result.size = 1;
240 result.field_names = vec!["result".to_string()];
241 Ok(vec![result])
242 }
243}
244
245#[derive(Debug, Clone)]
248pub struct PhysicalFtsScan {
249 pub index_name: String,
250 pub query_string: String,
251 pub docs_table: String,
252 pub terms_table: String,
253 pub posting_table: String,
254 pub table_name: String,
257 pub column_name: String,
258 pub table_catalog: Arc<TableCatalog>,
259}
260
261impl PhysicalOperatorExec for PhysicalFtsScan {
262 fn operator_type(&self) -> &str {
263 "fts_scan"
264 }
265
266 fn execute(&self, _input: Vec<DataChunk>) -> OperatorResult {
267 self.sync_index_with_source()?;
270
271 let query_tokens: Vec<String> = akar_fts::tokenize(&self.query_string)
273 .into_iter()
274 .map(|t| akar_fts::stem_word(&t))
275 .filter(|t| !akar_fts::STOP_WORDS.contains(&t.as_str()))
276 .collect();
277
278 let terms_table = match self.table_catalog.get_node_table_by_name(&self.terms_table) {
280 Some(t) => t,
281 None => {
282 return Err(format!(
283 "FTS terms table '{}' not found. Has the index been created?",
284 self.terms_table
285 )
286 .into());
287 }
288 };
289
290 let num_docs = self
292 .table_catalog
293 .get_node_table_by_name(&self.docs_table)
294 .map(|t| t.num_rows as f64)
295 .unwrap_or(1.0);
296
297 let terms_data = terms_table.to_column_major_data();
301 let num_terms = terms_table.num_rows as usize;
302 let mut term_index: std::collections::HashMap<String, (i64, i64)> = std::collections::HashMap::new();
303 for row_idx in 0..num_terms {
304 let term_id = match terms_data.first().and_then(|d| d.get(row_idx)) {
305 Some(Value::Int64(id)) => *id,
306 _ => continue,
307 };
308 let term_str = match terms_data.get(1).and_then(|d| d.get(row_idx)) {
309 Some(Value::String(s)) => s.clone(),
310 _ => continue,
311 };
312 let doc_freq = match terms_data.get(2).and_then(|d| d.get(row_idx)) {
313 Some(Value::Int64(df)) => *df,
314 _ => 0,
315 };
316 term_index.insert(term_str, (term_id, doc_freq));
317 }
318 drop(terms_data);
319 drop(terms_table);
320
321 let mut matching_terms: Vec<(i64, i64)> = Vec::new(); for token in &query_tokens {
323 if let Some(&(term_id, doc_freq)) = term_index.get(token.as_str()) {
324 matching_terms.push((term_id, doc_freq));
325 }
326 }
327
328 let source_table = self.table_catalog.get_node_table_by_name(&self.table_name);
332 let source_col = source_table
333 .as_ref()
334 .and_then(|t| t.columns.iter().position(|c| c.name == self.column_name));
335 let doc_valid = |doc_id: i64| -> bool {
336 let Ok(r) = usize::try_from(doc_id) else {
337 return false;
338 };
339 match (&source_table, source_col) {
340 (Some(t), Some(ci)) => r < t.num_rows as usize && matches!(t.get_value(r, ci), Some(Value::String(_))),
341 _ => true, }
343 };
344
345 let mut doc_scores: std::collections::HashMap<i64, f64> = std::collections::HashMap::new();
347
348 if let Some(posting_table) = self.table_catalog.get_rel_table_by_name(&self.posting_table) {
349 for &(term_id, doc_freq) in &matching_terms {
350 let idf = ((num_docs - doc_freq as f64 + 0.5) / (doc_freq as f64 + 0.5) + 1.0).ln();
351 let posting_rels = posting_table.get_outgoing_edges(term_id as u64);
353 for (doc_id, rel_vals) in posting_rels {
354 if !doc_valid(doc_id as i64) {
355 continue;
356 }
357 let tf = if let Some(Value::Int64(freq)) = rel_vals.first() {
358 *freq as f64
359 } else {
360 1.0
361 };
362 let k1 = 1.5_f64;
364 let score = idf * (tf * (k1 + 1.0)) / (tf + k1);
365 *doc_scores.entry(doc_id as i64).or_insert(0.0) += score;
366 }
367 }
368 }
369
370 let mut ranked: Vec<(i64, f64)> = doc_scores.into_iter().collect();
372 ranked.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
373
374 let n = ranked.len();
376 let mut id_vec = akar_common::vector::ValueVector::new(akar_common::types::PhysicalTypeID::Int64, n);
377 let mut score_vec = akar_common::vector::ValueVector::new(akar_common::types::PhysicalTypeID::Double, n);
378 id_vec.resize(n);
379 score_vec.resize(n);
380 for (i, (doc_id, score)) in ranked.into_iter().enumerate() {
381 id_vec.set_i64(i, doc_id);
382 score_vec.set_double(i, score);
383 }
384 let arr1 = akar_common::arrow_vector::ArrowVector::from_legacy(&id_vec).array;
385 let arr2 = akar_common::arrow_vector::ArrowVector::from_legacy(&score_vec).array;
386 let mut chunk = DataChunk::new(
387 vec![arr1, arr2],
388 vec![
389 akar_common::types::PhysicalTypeID::Int64,
390 akar_common::types::PhysicalTypeID::Double,
391 ],
392 );
393 chunk.size = n;
394 chunk.field_names = vec!["doc_id".to_string(), "score".to_string()];
395 Ok(vec![chunk])
396 }
397}
398
399impl PhysicalFtsScan {
400 fn sync_index_with_source(&self) -> Result<(), String> {
408 let Some(source_table) = self.table_catalog.get_node_table_by_name(&self.table_name) else {
409 return Ok(());
410 };
411 let Some(col_idx) = source_table.columns.iter().position(|c| c.name == self.column_name) else {
412 return Ok(());
413 };
414 let source_count = source_table.num_rows as usize;
415
416 let already_indexed = match self.table_catalog.get_node_table_by_name(&self.docs_table) {
417 Some(docs) => docs.num_rows as usize,
418 None => return Ok(()),
419 };
420
421 if already_indexed >= source_count {
422 return Ok(());
423 }
424
425 let mut new_docs: Vec<(usize, String)> = Vec::new();
427 for row_id in already_indexed..source_count {
428 if let Some(Value::String(s)) = source_table.get_value(row_id, col_idx) {
429 new_docs.push((row_id, s.clone()));
430 }
431 }
432 drop(source_table);
433
434 if new_docs.is_empty() {
435 return Ok(());
436 }
437
438 let mut term_info: std::collections::HashMap<String, (i64, i64, usize)> = std::collections::HashMap::new();
440 let mut max_term_id: i64 = -1;
441 {
442 let terms = self
443 .table_catalog
444 .get_node_table_by_name(&self.terms_table)
445 .ok_or_else(|| format!("Terms table '{}' not found", self.terms_table))?;
446 let data = terms.to_column_major_data();
447 for row_idx in 0..terms.num_rows as usize {
448 let term_id = match data.first().and_then(|d| d.get(row_idx)) {
449 Some(Value::Int64(id)) => *id,
450 _ => continue,
451 };
452 let term = match data.get(1).and_then(|d| d.get(row_idx)) {
453 Some(Value::String(s)) => s.clone(),
454 _ => continue,
455 };
456 let df = match data.get(2).and_then(|d| d.get(row_idx)) {
457 Some(Value::Int64(v)) => *v,
458 _ => 0,
459 };
460 max_term_id = max_term_id.max(term_id);
461 term_info.insert(term, (term_id, df, row_idx));
462 }
463 }
464
465 let mut next_term_id = max_term_id + 1;
466 let mut new_postings: Vec<(i64, usize, i64)> = Vec::new();
467 let mut new_terms: Vec<(i64, String)> = Vec::new();
468 let mut df_updates: Vec<(usize, i64)> = Vec::new(); for (doc_id, text) in &new_docs {
471 let mut freq: std::collections::HashMap<String, i64> = std::collections::HashMap::new();
472 for token in akar_fts::tokenize(text) {
473 let stemmed = akar_fts::stem_word(&token);
474 if !akar_fts::STOP_WORDS.contains(&stemmed.as_str()) {
475 *freq.entry(stemmed).or_insert(0) += 1;
476 }
477 }
478 for (term, f) in freq {
479 if let Some((term_id, df, row_idx)) = term_info.get_mut(&term) {
480 *df += 1;
481 df_updates.push((*row_idx, *df));
482 new_postings.push((*term_id, *doc_id, f));
483 } else {
484 let tid = next_term_id;
485 next_term_id += 1;
486 term_info.insert(term.clone(), (tid, 1, usize::MAX));
487 new_terms.push((tid, term));
488 new_postings.push((tid, *doc_id, f));
489 }
490 }
491 }
492
493 {
495 let mut docs = self
496 .table_catalog
497 .get_node_table_by_name_mut(&self.docs_table)
498 .ok_or_else(|| format!("Docs table '{}' not found", self.docs_table))?;
499 for (doc_id, text) in &new_docs {
500 docs.insert_row(vec![Value::Int64(*doc_id as i64), Value::String(text.clone())])?;
501 }
502 }
503 {
504 let mut terms = self
505 .table_catalog
506 .get_node_table_by_name_mut(&self.terms_table)
507 .ok_or_else(|| format!("Terms table '{}' not found", self.terms_table))?;
508 for (term_id, term) in &new_terms {
509 terms.insert_row(vec![
510 Value::Int64(*term_id),
511 Value::String(term.clone()),
512 Value::Int64(1),
513 ])?;
514 }
515 for (row_idx, df) in df_updates {
516 terms.update_cell(row_idx as u64, 2, Value::Int64(df))?;
517 }
518 }
519 {
520 let mut posting = self
521 .table_catalog
522 .get_rel_table_by_name_mut(&self.posting_table)
523 .ok_or_else(|| format!("Posting table '{}' not found", self.posting_table))?;
524 for (term_id, doc_id, f) in &new_postings {
525 posting.insert_rel(*term_id as u64, *doc_id as u64, vec![Value::Int64(*f)])?;
526 }
527 }
528 Ok(())
529 }
530}