(
&self,
collection: &str,
field: &str,
query: &str,
top_k: usize,
params: TextSearchParams,
)
| 75 | } |
| 76 | |
| 77 | pub(super) async fn text_search_impl( |
| 78 | &self, |
| 79 | collection: &str, |
| 80 | field: &str, |
| 81 | query: &str, |
| 82 | top_k: usize, |
| 83 | params: TextSearchParams, |
| 84 | ) -> NodeDbResult<Vec<SearchResult>> { |
| 85 | // Server-side FTS query SQL: `text_match(<field>, '<query>')` in |
| 86 | // a WHERE clause selects matching ids; `bm25_score(<field>, |
| 87 | // '<query>')` in the SELECT list exposes the score so callers |
| 88 | // can order/rank. The planner pattern-matches this shape and |
| 89 | // dispatches `SqlPlan::TextSearch`. |
| 90 | // |
| 91 | // `params` (mode, fuzzy, prefix, etc.) is intentionally ignored |
| 92 | // for now — every supported option is also expressible in the |
| 93 | // SQL form, but threading them through the DSL string is its |
| 94 | // own widening. The defaults (Plain query with fuzzy=true) cover |
| 95 | // the common case the trait's spec calls out. |
| 96 | let _ = params; |
| 97 | let coll = quote_identifier(collection); |
| 98 | let field_quoted = quote_identifier(field); |
| 99 | let q_lit = quote_string_literal(query); |
| 100 | let sql = format!( |
| 101 | "SELECT id, bm25_score({field_quoted}, {q_lit}) AS score \ |
| 102 | FROM {coll} \ |
| 103 | WHERE text_match({field_quoted}, {q_lit}) \ |
| 104 | LIMIT {top_k}" |
| 105 | ); |
| 106 | |
| 107 | let (columns, rows) = self.simple_query_raw(&sql).await?; |
| 108 | let id_idx = columns.iter().position(|c| c == "id").unwrap_or(0); |
| 109 | let score_idx = columns.iter().position(|c| c == "score").unwrap_or(1); |
| 110 | |
| 111 | let mut results = Vec::with_capacity(rows.len()); |
| 112 | for row in &rows { |
| 113 | let id = row |
| 114 | .get(id_idx) |
| 115 | .and_then(|v| v.as_str()) |
| 116 | .unwrap_or("") |
| 117 | .to_string(); |
| 118 | // simple_query returns text — score arrives as a stringified |
| 119 | // float. Parse defensively so a missing/malformed score does |
| 120 | // not torpedo the whole result set; callers prefer ordered |
| 121 | // ids with score 0.0 over an Err. |
| 122 | let score = row |
| 123 | .get(score_idx) |
| 124 | .and_then(|v| v.as_str()) |
| 125 | .and_then(|s| s.parse::<f32>().ok()) |
| 126 | .or_else(|| { |
| 127 | row.get(score_idx) |
| 128 | .and_then(|v| v.as_f64()) |
| 129 | .map(|f| f as f32) |
| 130 | }) |
| 131 | .unwrap_or(0.0); |
| 132 | |
| 133 | results.push(SearchResult { |
| 134 | id, |
no test coverage detected