search for standard sites pub-search.waow.tech
search zig blog atproto
11
fork

Configure Feed

Select the types of activity you want to include in your feed.

add prefix matching to FTS search

"cat" now matches "catch", "category", etc. by appending * to each search term.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

zzstoatzz a3b9e039 32fb63d4

+59 -8
+59 -8
backend/src/db.zig
··· 190 190 191 191 const temp_alloc = gpa.allocator(); 192 192 193 - // normalize query to match FTS5 tokenization (dots become spaces) 194 - const normalized_query = try alloc.dupe(u8, query); 195 - for (normalized_query) |*c| { 196 - if (c.* == '.') c.* = ' '; 197 - } 193 + // normalize query: dots become spaces, add prefix matching with * 194 + const fts_query = try buildFtsQuery(alloc, query); 198 195 199 196 var jw: json.Stringify = .{ .writer = &output.writer }; 200 197 try jw.beginArray(); ··· 224 221 \\JOIN document_tags dt ON d.uri = dt.document_uri 225 222 \\WHERE documents_fts MATCH ? AND dt.tag = ? 226 223 \\ORDER BY rank LIMIT 40 227 - , &.{ normalized_query, tag }) catch null 224 + , &.{ fts_query, tag }) catch null 228 225 else 229 226 execSql( 230 227 \\SELECT f.uri, d.did, d.title, ··· 236 233 \\LEFT JOIN publications p ON d.publication_uri = p.uri 237 234 \\WHERE documents_fts MATCH ? 238 235 \\ORDER BY rank LIMIT 40 239 - , &.{normalized_query}) catch null; 236 + , &.{fts_query}) catch null; 240 237 241 238 if (doc_result) |result| { 242 239 defer temp_alloc.free(result); ··· 284 281 \\JOIN publications p ON f.uri = p.uri 285 282 \\WHERE publications_fts MATCH ? 286 283 \\ORDER BY rank LIMIT 10 287 - , &.{normalized_query}) catch null; 284 + , &.{fts_query}) catch null; 288 285 289 286 if (pub_result) |result| { 290 287 defer temp_alloc.free(result); ··· 361 358 }, 362 359 else => 0, 363 360 }; 361 + } 362 + 363 + // build FTS5 query with prefix matching: "cat dog" -> "cat* dog*" 364 + fn buildFtsQuery(alloc: Allocator, query: []const u8) ![]const u8 { 365 + if (query.len == 0) return ""; 366 + 367 + // normalize dots to spaces 368 + const normalized = try alloc.dupe(u8, query); 369 + for (normalized) |*c| { 370 + if (c.* == '.') c.* = ' '; 371 + } 372 + 373 + // count words to calculate output size 374 + var word_count: usize = 0; 375 + var in_word = false; 376 + for (normalized) |c| { 377 + if (c == ' ') { 378 + in_word = false; 379 + } else if (!in_word) { 380 + word_count += 1; 381 + in_word = true; 382 + } 383 + } 384 + 385 + if (word_count == 0) return ""; 386 + 387 + // allocate: original length + one '*' per word + spaces 388 + const result = try alloc.alloc(u8, normalized.len + word_count); 389 + var pos: usize = 0; 390 + in_word = false; 391 + 392 + for (normalized) |c| { 393 + if (c == ' ') { 394 + if (in_word) { 395 + result[pos] = '*'; 396 + pos += 1; 397 + in_word = false; 398 + } 399 + result[pos] = ' '; 400 + pos += 1; 401 + } else { 402 + result[pos] = c; 403 + pos += 1; 404 + in_word = true; 405 + } 406 + } 407 + 408 + // add final * if ended in a word 409 + if (in_word) { 410 + result[pos] = '*'; 411 + pos += 1; 412 + } 413 + 414 + return result[0..pos]; 364 415 } 365 416 366 417 fn execSql(sql: []const u8, args: []const []const u8) ![]const u8 {