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.

fix: disable vector similarity — stop brute-force cosine queries from saturating turso

/similar now returns [] immediately. Removed findSimilar, all similarity
cache code, and similarity_cache sync. The brute-force vector_distance_cos
queries were taking 10-15s each and blocking keyword search (30-57s).

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

zzstoatzz d179d203 0d355b59

+4 -202
+1 -28
backend/src/db/sync.zig
··· 147 147 conn.exec("COMMIT", .{}) catch {}; 148 148 } 149 149 150 - // sync similarity cache 151 - var cache_count: usize = 0; 152 - { 153 - if (turso.query( 154 - "SELECT source_uri, results, doc_count, computed_at FROM similarity_cache", 155 - &.{}, 156 - )) |res_val| { 157 - var res = res_val; 158 - defer res.deinit(); 159 - 160 - local.lock(); 161 - defer local.unlock(); 162 - conn.exec("DELETE FROM similarity_cache", .{}) catch {}; 163 - conn.exec("BEGIN", .{}) catch {}; 164 - for (res.rows) |row| { 165 - conn.exec( 166 - "INSERT OR REPLACE INTO similarity_cache (source_uri, results, doc_count, computed_at) VALUES (?, ?, ?, ?)", 167 - .{ row.text(0), row.text(1), row.text(2), row.text(3) }, 168 - ) catch {}; 169 - cache_count += 1; 170 - } 171 - conn.exec("COMMIT", .{}) catch {}; 172 - } else |err| { 173 - std.debug.print("sync: turso similarity_cache query failed: {}\n", .{err}); 174 - } 175 - } 176 - 177 150 // record sync time (brief lock) 178 151 { 179 152 local.lock(); ··· 196 169 } 197 170 198 171 local.setReady(true); 199 - std.debug.print("sync: full sync complete - {d} docs, {d} pubs, {d} tags, {d} popular, {d} cached\n", .{ doc_count, pub_count, tag_count, popular_count, cache_count }); 172 + std.debug.print("sync: full sync complete - {d} docs, {d} pubs, {d} tags, {d} popular\n", .{ doc_count, pub_count, tag_count, popular_count }); 200 173 } 201 174 202 175 /// Incremental sync: fetch documents created since last sync
-152
backend/src/search.zig
··· 4 4 const zql = @import("zql"); 5 5 const logfire = @import("logfire"); 6 6 const db = @import("db/mod.zig"); 7 - const stats = @import("metrics.zig").stats; 8 - 9 - // cached embedded doc count (refresh every 5 minutes) 10 - var cached_doc_count: std.atomic.Value(i64) = std.atomic.Value(i64).init(0); 11 - var doc_count_updated_at: std.atomic.Value(i64) = std.atomic.Value(i64).init(0); 12 - const DOC_COUNT_CACHE_SECS = 300; // 5 minutes 13 7 14 8 // JSON output type for search results 15 9 const SearchResultJson = struct { ··· 570 564 return &.{}; 571 565 } 572 566 573 - /// Find documents similar to a given document using vector similarity 574 - /// Uses brute-force cosine distance with caching (cache invalidated when doc count changes) 575 - pub fn findSimilar(alloc: Allocator, uri: []const u8, limit: usize) ![]const u8 { 576 - const c = db.getClient() orelse return error.NotInitialized; 577 - 578 - // get cached doc count (rarely hits Turso - refreshes every 5 min) 579 - const doc_count = getEmbeddedDocCountCached(c) orelse return error.QueryFailed; 580 - 581 - // check LOCAL cache first (instant) 582 - if (db.getLocalDb()) |local| { 583 - if (getCachedSimilarLocal(alloc, local, uri, doc_count)) |cached| { 584 - stats.recordCacheHit(); 585 - return cached; 586 - } 587 - } 588 - 589 - // check Turso cache (slower, but needed if local empty) 590 - if (getCachedSimilar(alloc, c, uri, doc_count)) |cached| { 591 - stats.recordCacheHit(); 592 - // also write to local cache for next time 593 - if (db.getLocalDb()) |local| { 594 - cacheSimilarResultsLocal(local, uri, cached, doc_count); 595 - } 596 - return cached; 597 - } 598 - stats.recordCacheMiss(); 599 - 600 - // cache miss - compute similarity 601 - var output: std.Io.Writer.Allocating = .init(alloc); 602 - errdefer output.deinit(); 603 - 604 - var limit_buf: [8]u8 = undefined; 605 - const limit_str = std.fmt.bufPrint(&limit_buf, "{d}", .{limit}) catch "5"; 606 - 607 - // brute-force cosine similarity search (no vector index needed) 608 - var res = c.query( 609 - \\SELECT d2.uri, d2.did, d2.title, '' as snippet, 610 - \\ d2.created_at, d2.rkey, d2.base_path, d2.has_publication, 611 - \\ d2.platform, COALESCE(d2.path, '') as path 612 - \\FROM documents d1, documents d2 613 - \\WHERE d1.uri = ? 614 - \\ AND d2.uri != d1.uri 615 - \\ AND d1.embedding IS NOT NULL 616 - \\ AND d2.embedding IS NOT NULL 617 - \\ORDER BY vector_distance_cos(d1.embedding, d2.embedding) 618 - \\LIMIT ? 619 - , &.{ uri, limit_str }) catch { 620 - try output.writer.writeAll("[]"); 621 - return try output.toOwnedSlice(); 622 - }; 623 - defer res.deinit(); 624 - 625 - var jw: json.Stringify = .{ .writer = &output.writer }; 626 - try jw.beginArray(); 627 - for (res.rows) |row| try jw.write(Doc.fromRow(row).toJson()); 628 - try jw.endArray(); 629 - 630 - const results = try output.toOwnedSlice(); 631 - 632 - // cache to LOCAL db (instant) 633 - if (db.getLocalDb()) |local| { 634 - cacheSimilarResultsLocal(local, uri, results, doc_count); 635 - } 636 - 637 - // cache to Turso (fire and forget - still useful for durability) 638 - cacheSimilarResults(c, uri, results, doc_count); 639 - 640 - return results; 641 - } 642 - 643 - fn getEmbeddedDocCount(c: *db.Client) ?i64 { 644 - var res = c.query("SELECT COUNT(*) FROM documents WHERE embedding IS NOT NULL", &.{}) catch return null; 645 - defer res.deinit(); 646 - if (res.rows.len == 0) return null; 647 - return res.rows[0].int(0); 648 - } 649 - 650 - fn getEmbeddedDocCountCached(c: *db.Client) ?i64 { 651 - const now = std.time.timestamp(); 652 - const last_update = doc_count_updated_at.load(.acquire); 653 - 654 - // use cached value if fresh enough 655 - if (now - last_update < DOC_COUNT_CACHE_SECS) { 656 - const cached = cached_doc_count.load(.acquire); 657 - if (cached > 0) return cached; 658 - } 659 - 660 - // refresh from Turso 661 - const count = getEmbeddedDocCount(c) orelse return null; 662 - cached_doc_count.store(count, .release); 663 - doc_count_updated_at.store(now, .release); 664 - return count; 665 - } 666 - 667 - fn getCachedSimilar(alloc: Allocator, c: *db.Client, uri: []const u8, current_doc_count: i64) ?[]const u8 { 668 - var count_buf: [20]u8 = undefined; 669 - const count_str = std.fmt.bufPrint(&count_buf, "{d}", .{current_doc_count}) catch return null; 670 - 671 - var res = c.query( 672 - "SELECT results FROM similarity_cache WHERE source_uri = ? AND doc_count = ?", 673 - &.{ uri, count_str }, 674 - ) catch return null; 675 - defer res.deinit(); 676 - 677 - if (res.rows.len == 0) return null; 678 - return alloc.dupe(u8, res.rows[0].text(0)) catch null; 679 - } 680 - 681 - fn cacheSimilarResults(c: *db.Client, uri: []const u8, results: []const u8, doc_count: i64) void { 682 - var count_buf: [20]u8 = undefined; 683 - const count_str = std.fmt.bufPrint(&count_buf, "{d}", .{doc_count}) catch return; 684 - 685 - var ts_buf: [20]u8 = undefined; 686 - const ts_str = std.fmt.bufPrint(&ts_buf, "{d}", .{std.time.timestamp()}) catch return; 687 - 688 - c.exec( 689 - "INSERT OR REPLACE INTO similarity_cache (source_uri, results, doc_count, computed_at) VALUES (?, ?, ?, ?)", 690 - &.{ uri, results, count_str, ts_str }, 691 - ) catch {}; 692 - } 693 - 694 - fn getCachedSimilarLocal(alloc: Allocator, local: *db.LocalDb, uri: []const u8, current_doc_count: i64) ?[]const u8 { 695 - var rows = local.query( 696 - "SELECT results, doc_count FROM similarity_cache WHERE source_uri = ?", 697 - .{uri}, 698 - ) catch return null; 699 - defer rows.deinit(); 700 - 701 - const row = rows.next() orelse return null; 702 - // check doc_count matches for cache validity 703 - if (row.int(1) != current_doc_count) return null; 704 - return alloc.dupe(u8, row.text(0)) catch null; 705 - } 706 - 707 - fn cacheSimilarResultsLocal(local: *db.LocalDb, uri: []const u8, results: []const u8, doc_count: i64) void { 708 - var count_buf: [20]u8 = undefined; 709 - const count_str = std.fmt.bufPrint(&count_buf, "{d}", .{doc_count}) catch return; 710 - 711 - var ts_buf: [20]u8 = undefined; 712 - const ts_str = std.fmt.bufPrint(&ts_buf, "{d}", .{std.time.timestamp()}) catch return; 713 - 714 - local.exec( 715 - "INSERT OR REPLACE INTO similarity_cache (source_uri, results, doc_count, computed_at) VALUES (?, ?, ?, ?)", 716 - .{ uri, results, count_str, ts_str }, 717 - ) catch {}; 718 - } 719 567 720 568 /// Build FTS5 query with OR between terms: "cat dog" -> "cat OR dog*" 721 569 /// Uses OR for better recall with BM25 ranking (more matches = higher score)
+3 -22
backend/src/server.zig
··· 388 388 } 389 389 390 390 fn handleSimilar(request: *http.Server.Request, target: []const u8) !void { 391 - const start_time = std.time.microTimestamp(); 392 - defer metrics.timing.record(.similar, start_time); 393 - 394 - var arena = std.heap.ArenaAllocator.init(std.heap.page_allocator); 395 - defer arena.deinit(); 396 - const alloc = arena.allocator(); 397 - 398 - const uri = parseQueryParam(alloc, target, "uri") catch { 399 - try sendJson(request, "{\"error\":\"missing uri parameter\"}"); 400 - return; 401 - }; 402 - 403 - // span attributes are copied internally, safe to use arena strings 404 - const span = logfire.span("http.similar", .{ .uri = uri }); 405 - defer span.end(); 406 - 407 - const results = search.findSimilar(alloc, uri, 5) catch { 408 - try sendJson(request, "[]"); 409 - return; 410 - }; 411 - 412 - try sendJson(request, results); 391 + _ = target; 392 + // disabled: vector similarity search was saturating Turso with brute-force cosine queries 393 + try sendJson(request, "[]"); 413 394 } 414 395 415 396 fn handleActivity(request: *http.Server.Request) !void {