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

revert: remove all semantic search features, restore keyword-only

backend reverted to 6689407 state:
- server.zig: no mode param, just calls search.search()
- search.zig: remove searchSemantic, searchHybrid, embedQuery
- embedder.zig: back to voyage-3-lite, batch 20, 512 dims

frontend: keyword-only search, no semantic phase, no mode selector

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

+29 -382
+13 -17
backend/src/ingest/embedder.zig
··· 12 12 const logfire = @import("logfire"); 13 13 const db = @import("../db/mod.zig"); 14 14 15 - // voyage-3 limits 16 - const MAX_BATCH_SIZE = 100; // voyage-3 supports up to 128 per request 15 + // voyage-3-lite limits 16 + const MAX_BATCH_SIZE = 20; // conservative batch size for reliability 17 17 const MAX_CONTENT_CHARS = 8000; // ~2000 tokens, well under 32K limit 18 - const EMBEDDING_DIM = 1024; 18 + const EMBEDDING_DIM = 512; 19 19 const POLL_INTERVAL_SECS: u64 = 60; // check for new docs every minute 20 20 const ERROR_BACKOFF_SECS: u64 = 300; // 5 min backoff on errors 21 21 22 - const NUM_WORKERS = 1; 23 - 24 - /// Start the embedder background workers 22 + /// Start the embedder background worker 25 23 pub fn start(allocator: Allocator) void { 26 24 const api_key = posix.getenv("VOYAGE_API_KEY") orelse { 27 25 logfire.info("embedder: VOYAGE_API_KEY not set, embeddings disabled", .{}); 28 26 return; 29 27 }; 30 28 31 - for (0..NUM_WORKERS) |i| { 32 - const thread = std.Thread.spawn(.{}, worker, .{ allocator, api_key }) catch |err| { 33 - logfire.err("embedder: failed to start worker {d}: {}", .{ i, err }); 34 - continue; 35 - }; 36 - thread.detach(); 37 - } 38 - logfire.info("embedder: {d} background workers started", .{NUM_WORKERS}); 29 + const thread = std.Thread.spawn(.{}, worker, .{ allocator, api_key }) catch |err| { 30 + logfire.err("embedder: failed to start thread: {}", .{err}); 31 + return; 32 + }; 33 + thread.detach(); 34 + logfire.info("embedder: background worker started", .{}); 39 35 } 40 36 41 37 fn worker(allocator: Allocator, api_key: []const u8) void { ··· 77 73 78 74 const client = db.getClient() orelse return error.NoClient; 79 75 80 - // query for documents needing embeddings (RANDOM order for parallel workers) 76 + // query for documents needing embeddings 81 77 var result = try client.query( 82 - "SELECT uri, title, content FROM documents WHERE embedding IS NULL ORDER BY RANDOM() LIMIT ?", 78 + "SELECT uri, title, content FROM documents WHERE embedding IS NULL LIMIT ?", 83 79 &.{std.fmt.comptimePrint("{}", .{MAX_BATCH_SIZE})}, 84 80 ); 85 81 defer result.deinit(); ··· 220 216 try jw.beginObject(); 221 217 222 218 try jw.objectField("model"); 223 - try jw.write("voyage-3"); 219 + try jw.write("voyage-3-lite"); 224 220 225 221 try jw.objectField("input_type"); 226 222 try jw.write("document");
-228
backend/src/search.zig
··· 1 1 const std = @import("std"); 2 - const http = std.http; 3 2 const json = std.json; 4 - const posix = std.posix; 5 3 const Allocator = std.mem.Allocator; 6 4 const zql = @import("zql"); 7 - const logfire = @import("logfire"); 8 5 const db = @import("db/mod.zig"); 9 6 const stats = @import("metrics.zig").stats; 10 7 ··· 798 795 } 799 796 buf[pos] = '*'; 800 797 return buf; 801 - } 802 - 803 - // --- semantic search --- 804 - 805 - const EMBEDDING_DIM = 1024; 806 - 807 - /// Embed a query string via Voyage AI, returns JSON array string for vector32() 808 - fn embedQuery(alloc: Allocator, query: []const u8) ![]const u8 { 809 - const api_key = posix.getenv("VOYAGE_API_KEY") orelse return error.NoApiKey; 810 - 811 - var http_client: http.Client = .{ .allocator = alloc }; 812 - defer http_client.deinit(); 813 - 814 - // build request: single query, input_type "query" 815 - var body: std.Io.Writer.Allocating = .init(alloc); 816 - var jw: json.Stringify = .{ .writer = &body.writer, .options = .{} }; 817 - try jw.beginObject(); 818 - try jw.objectField("model"); 819 - try jw.write("voyage-3"); 820 - try jw.objectField("input_type"); 821 - try jw.write("query"); 822 - try jw.objectField("input"); 823 - try jw.beginArray(); 824 - try jw.write(query); 825 - try jw.endArray(); 826 - try jw.endObject(); 827 - const request_body = try body.toOwnedSlice(); 828 - defer alloc.free(request_body); 829 - 830 - var auth_buf: [256]u8 = undefined; 831 - const auth = std.fmt.bufPrint(&auth_buf, "Bearer {s}", .{api_key}) catch 832 - return error.AuthTooLong; 833 - 834 - var response_body: std.Io.Writer.Allocating = .init(alloc); 835 - const res = http_client.fetch(.{ 836 - .location = .{ .url = "https://api.voyageai.com/v1/embeddings" }, 837 - .method = .POST, 838 - .headers = .{ 839 - .content_type = .{ .override = "application/json" }, 840 - .authorization = .{ .override = auth }, 841 - }, 842 - .payload = request_body, 843 - .response_writer = &response_body.writer, 844 - }) catch return error.VoyageRequestFailed; 845 - 846 - if (res.status != .ok) { 847 - response_body.deinit(); 848 - return error.VoyageApiError; 849 - } 850 - 851 - const response_text = try response_body.toOwnedSlice(); 852 - defer alloc.free(response_text); 853 - 854 - // parse response, extract first embedding, format as JSON array for vector32() 855 - const parsed = json.parseFromSlice(json.Value, alloc, response_text, .{}) catch 856 - return error.ParseError; 857 - defer parsed.deinit(); 858 - 859 - const data = parsed.value.object.get("data") orelse return error.MissingData; 860 - if (data != .array or data.array.items.len == 0) return error.InvalidData; 861 - 862 - const embedding_obj = data.array.items[0]; 863 - if (embedding_obj != .object) return error.InvalidData; 864 - const embedding_val = embedding_obj.object.get("embedding") orelse return error.MissingEmbedding; 865 - if (embedding_val != .array or embedding_val.array.items.len != EMBEDDING_DIM) return error.DimensionMismatch; 866 - 867 - // format as "[0.123456,0.789012,...]" 868 - var embedding_buf: std.ArrayList(u8) = .empty; 869 - errdefer embedding_buf.deinit(alloc); 870 - 871 - try embedding_buf.append(alloc, '['); 872 - for (embedding_val.array.items, 0..) |val, j| { 873 - if (j > 0) try embedding_buf.append(alloc, ','); 874 - const f: f64 = switch (val) { 875 - .float => val.float, 876 - .integer => @floatFromInt(val.integer), 877 - else => return error.InvalidValue, 878 - }; 879 - var fmt_buf: [32]u8 = undefined; 880 - const str = std.fmt.bufPrint(&fmt_buf, "{d:.6}", .{f}) catch continue; 881 - try embedding_buf.appendSlice(alloc, str); 882 - } 883 - try embedding_buf.append(alloc, ']'); 884 - 885 - return try alloc.dupe(u8, embedding_buf.items); 886 - } 887 - 888 - /// Semantic search: embed query via Voyage AI, find similar docs by vector distance 889 - pub fn searchSemantic(alloc: Allocator, query: []const u8, platform_filter: ?[]const u8) ![]const u8 { 890 - const span = logfire.span("search.semantic", .{ .query = query }); 891 - defer span.end(); 892 - 893 - const embedding_json = try embedQuery(alloc, query); 894 - defer alloc.free(embedding_json); 895 - 896 - const c = db.getClient() orelse return error.NotInitialized; 897 - 898 - var output: std.Io.Writer.Allocating = .init(alloc); 899 - errdefer output.deinit(); 900 - var jw: json.Stringify = .{ .writer = &output.writer }; 901 - try jw.beginArray(); 902 - 903 - if (platform_filter) |platform| { 904 - var res = c.query( 905 - \\SELECT d.uri, d.did, d.title, '' as snippet, 906 - \\ d.created_at, d.rkey, d.base_path, d.has_publication, 907 - \\ d.platform, COALESCE(d.path, '') as path 908 - \\FROM documents d 909 - \\WHERE d.embedding IS NOT NULL AND d.platform = ? 910 - \\ORDER BY vector_distance_cos(vector32(?), d.embedding) 911 - \\LIMIT 20 912 - , &.{ platform, embedding_json }) catch { 913 - try jw.endArray(); 914 - return try output.toOwnedSlice(); 915 - }; 916 - defer res.deinit(); 917 - for (res.rows) |row| try jw.write(Doc.fromRow(row).toJson()); 918 - } else { 919 - var res = c.query( 920 - \\SELECT d.uri, d.did, d.title, '' as snippet, 921 - \\ d.created_at, d.rkey, d.base_path, d.has_publication, 922 - \\ d.platform, COALESCE(d.path, '') as path 923 - \\FROM documents d 924 - \\WHERE d.embedding IS NOT NULL 925 - \\ORDER BY vector_distance_cos(vector32(?), d.embedding) 926 - \\LIMIT 20 927 - , &.{embedding_json}) catch { 928 - try jw.endArray(); 929 - return try output.toOwnedSlice(); 930 - }; 931 - defer res.deinit(); 932 - for (res.rows) |row| try jw.write(Doc.fromRow(row).toJson()); 933 - } 934 - 935 - try jw.endArray(); 936 - return try output.toOwnedSlice(); 937 - } 938 - 939 - /// Hybrid search: keyword results + semantic results, deduplicated 940 - pub fn searchHybrid(alloc: Allocator, query: []const u8, tag_filter: ?[]const u8, platform_filter: ?[]const u8, since_filter: ?[]const u8) ![]const u8 { 941 - // embed query (slow: ~100-200ms Voyage API call) 942 - const embedding_json = embedQuery(alloc, query) catch { 943 - // fall back to keyword-only if embedding fails 944 - return search(alloc, query, tag_filter, platform_filter, since_filter); 945 - }; 946 - defer alloc.free(embedding_json); 947 - 948 - // keyword search (fast: local SQLite) 949 - const keyword_json = try search(alloc, query, tag_filter, platform_filter, since_filter); 950 - 951 - // vector search (Turso) 952 - const c = db.getClient() orelse return keyword_json; 953 - var vector_res = blk: { 954 - if (platform_filter) |platform| { 955 - break :blk c.query( 956 - \\SELECT d.uri, d.did, d.title, '' as snippet, 957 - \\ d.created_at, d.rkey, d.base_path, d.has_publication, 958 - \\ d.platform, COALESCE(d.path, '') as path 959 - \\FROM documents d 960 - \\WHERE d.embedding IS NOT NULL AND d.platform = ? 961 - \\ORDER BY vector_distance_cos(vector32(?), d.embedding) 962 - \\LIMIT 10 963 - , &.{ platform, embedding_json }) catch return keyword_json; 964 - } else { 965 - break :blk c.query( 966 - \\SELECT d.uri, d.did, d.title, '' as snippet, 967 - \\ d.created_at, d.rkey, d.base_path, d.has_publication, 968 - \\ d.platform, COALESCE(d.path, '') as path 969 - \\FROM documents d 970 - \\WHERE d.embedding IS NOT NULL 971 - \\ORDER BY vector_distance_cos(vector32(?), d.embedding) 972 - \\LIMIT 10 973 - , &.{embedding_json}) catch return keyword_json; 974 - } 975 - }; 976 - defer vector_res.deinit(); 977 - 978 - // extract URIs from keyword results for dedup 979 - var seen = std.StringHashMap(void).init(alloc); 980 - defer seen.deinit(); 981 - 982 - const kw = json.parseFromSlice(json.Value, alloc, keyword_json, .{}) catch return keyword_json; 983 - defer kw.deinit(); 984 - if (kw.value != .array) return keyword_json; 985 - 986 - for (kw.value.array.items) |item| { 987 - if (item == .object) { 988 - if (item.object.get("uri")) |u| { 989 - if (u == .string) seen.put(u.string, {}) catch {}; 990 - } 991 - } 992 - } 993 - 994 - // check if there are any new semantic results 995 - var has_new = false; 996 - for (vector_res.rows) |row| { 997 - if (!seen.contains(row.text(0))) { 998 - has_new = true; 999 - break; 1000 - } 1001 - } 1002 - if (!has_new) return keyword_json; 1003 - 1004 - // merge: keyword results + new semantic results 1005 - var output: std.Io.Writer.Allocating = .init(alloc); 1006 - errdefer output.deinit(); 1007 - var merge_jw: json.Stringify = .{ .writer = &output.writer }; 1008 - try merge_jw.beginArray(); 1009 - 1010 - // write keyword results as raw JSON (avoids re-serialization) 1011 - if (kw.value.array.items.len > 0) { 1012 - try merge_jw.beginWriteRaw(); 1013 - try merge_jw.writer.writeAll(keyword_json[1 .. keyword_json.len - 1]); 1014 - merge_jw.endWriteRaw(); 1015 - } 1016 - 1017 - // append new semantic results 1018 - for (vector_res.rows) |row| { 1019 - if (!seen.contains(row.text(0))) { 1020 - try merge_jw.write(Doc.fromRow(row).toJson()); 1021 - } 1022 - } 1023 - 1024 - try merge_jw.endArray(); 1025 - return try output.toOwnedSlice(); 1026 798 } 1027 799 1028 800 // --- tests ---
+6 -35
backend/src/server.zig
··· 85 85 const tag_filter = parseQueryParam(alloc, target, "tag") catch null; 86 86 const platform_filter = parseQueryParam(alloc, target, "platform") catch null; 87 87 const since_filter = parseQueryParam(alloc, target, "since") catch null; 88 - const mode = parseQueryParam(alloc, target, "mode") catch "keyword"; 89 88 89 + // span attributes are now copied internally, safe to use arena strings 90 90 const span = logfire.span("http.search", .{ 91 91 .query = query, 92 92 .tag = tag_filter, 93 93 .platform = platform_filter, 94 - .mode = mode, 95 94 }); 96 95 defer span.end(); 97 96 ··· 100 99 return; 101 100 } 102 101 103 - const results = blk: { 104 - if (mem.eql(u8, mode, "semantic")) { 105 - if (query.len == 0) { 106 - try sendJson(request, "{\"error\":\"semantic search requires a query\"}"); 107 - return; 108 - } 109 - break :blk search.searchSemantic(alloc, query, platform_filter) catch |err| { 110 - logfire.err("semantic search failed: {}", .{err}); 111 - metrics.stats.recordError(); 112 - return err; 113 - }; 114 - } else if (mem.eql(u8, mode, "hybrid")) { 115 - if (query.len == 0) { 116 - // hybrid without query falls back to keyword (tag/platform browsing) 117 - break :blk search.search(alloc, query, tag_filter, platform_filter, since_filter) catch |err| { 118 - logfire.err("search failed: {}", .{err}); 119 - metrics.stats.recordError(); 120 - return err; 121 - }; 122 - } 123 - break :blk search.searchHybrid(alloc, query, tag_filter, platform_filter, since_filter) catch |err| { 124 - logfire.err("hybrid search failed: {}", .{err}); 125 - metrics.stats.recordError(); 126 - return err; 127 - }; 128 - } else { 129 - break :blk search.search(alloc, query, tag_filter, platform_filter, since_filter) catch |err| { 130 - logfire.err("search failed: {}", .{err}); 131 - metrics.stats.recordError(); 132 - return err; 133 - }; 134 - } 102 + // perform FTS search - arena handles cleanup 103 + const results = search.search(alloc, query, tag_filter, platform_filter, since_filter) catch |err| { 104 + logfire.err("search failed: {}", .{err}); 105 + metrics.stats.recordError(); 106 + return err; 135 107 }; 136 - 137 108 metrics.stats.recordSearch(query); 138 109 logfire.counter("search.requests", 1); 139 110 try sendJson(request, results);
+10 -102
site/index.html
··· 371 371 color: #555; 372 372 } 373 373 374 - .mode-selector { 375 - margin-bottom: 1rem; 376 - } 377 - 378 - .mode-selector-label { 379 - font-size: 11px; 380 - color: #444; 381 - margin-bottom: 0.5rem; 382 - } 383 - 384 - .mode-list { 385 - display: flex; 386 - gap: 0.5rem; 387 - } 388 - 389 - .mode-option { 390 - font-size: 11px; 391 - padding: 3px 8px; 392 - background: #151515; 393 - border: 1px solid #252525; 394 - border-radius: 3px; 395 - cursor: pointer; 396 - color: #777; 397 - } 398 - 399 - .mode-option:hover { 400 - background: #1a1a1a; 401 - border-color: #333; 402 - color: #aaa; 403 - } 404 - 405 - .mode-option.active { 406 - background: rgba(100, 100, 200, 0.2); 407 - border-color: #6a6ad4; 408 - color: #8a8ae4; 409 - } 410 - 411 - .mode-option .mode-desc { 412 - font-size: 9px; 413 - color: #555; 414 - margin-left: 4px; 415 - } 416 - 417 - .mode-option.active .mode-desc { 418 - color: #6a6ad4; 419 - } 420 - 421 374 .platform-filter { 422 375 margin-bottom: 1rem; 423 376 } ··· 489 442 } 490 443 491 444 /* ensure minimum 44px touch targets */ 492 - .tag, .platform-option, .mode-option, .suggestion, input.tag-input { 445 + .tag, .platform-option, .suggestion, input.tag-input { 493 446 min-height: 44px; 494 447 display: inline-flex; 495 448 align-items: center; ··· 567 520 568 521 /* ensure touch targets on tablets too */ 569 522 @media (hover: none) and (pointer: coarse) { 570 - .tag, .platform-option, .mode-option, .suggestion, .related-item, input.tag-input { 523 + .tag, .platform-option, .suggestion, .related-item, input.tag-input { 571 524 min-height: 44px; 572 525 display: inline-flex; 573 526 align-items: center; ··· 583 536 <input type="text" id="query" placeholder="search content..." autofocus> 584 537 <button id="search-btn">search</button> 585 538 </div> 586 - 587 - <div id="mode-selector" class="mode-selector"></div> 588 539 589 540 <div id="suggestions"></div> 590 541 ··· 617 568 const activeFilterDiv = document.getElementById('active-filter'); 618 569 const suggestionsDiv = document.getElementById('suggestions'); 619 570 const platformFilterDiv = document.getElementById('platform-filter'); 620 - const modeSelectorDiv = document.getElementById('mode-selector'); 621 571 622 572 let currentTag = null; 623 573 let currentPlatform = null; 624 - let currentMode = 'hybrid'; 625 574 let allTags = []; 626 575 let popularSearches = []; 627 576 const authorCache = new Map(); 628 - 629 577 async function search(query, tag = null, platform = null) { 630 578 if (!query.trim() && !tag && !platform) return; 631 579 632 580 searchBtn.disabled = true; 581 + resultsDiv.innerHTML = `<div class="status">searching...</div>`; 582 + 633 583 let searchUrl = `${API_URL}/search?q=${encodeURIComponent(query || '')}`; 634 584 if (tag) searchUrl += `&tag=${encodeURIComponent(tag)}`; 635 585 if (platform) searchUrl += `&platform=${encodeURIComponent(platform)}`; 636 - if (currentMode !== 'hybrid') searchUrl += `&mode=${currentMode}`; 637 - 638 - const modeLabel = currentMode === 'semantic' ? 'finding similar...' 639 - : currentMode === 'hybrid' ? 'searching + embedding...' 640 - : 'searching...'; 641 - resultsDiv.innerHTML = `<div class="status">${modeLabel}</div>`; 642 586 643 587 try { 644 588 const res = await fetch(searchUrl); ··· 661 605 resultsDiv.innerHTML = ` 662 606 <div class="empty-state"> 663 607 <p>no results${query ? ` for ${formatQueryForDisplay(query)}` : ''}${tag ? ` in #${escapeHtml(tag)}` : ''}${platform ? ` on ${escapeHtml(platform)}` : ''}</p> 664 - <p>try different keywords</p> 665 608 </div> 666 609 `; 667 610 statsDiv.textContent = ''; ··· 669 612 } 670 613 671 614 let html = ''; 672 - 673 615 for (const doc of results) { 674 616 const entityType = doc.type || 'article'; 675 - const platform = doc.platform || 'leaflet'; 676 - 677 - // build URL based on entity type and platform 678 - const docUrl = buildDocUrl(doc, entityType, platform); 679 - // only show platform badge for actual platforms, not for lexicon-only records 680 - const platformConfig = PLATFORM_CONFIG[platform]; 617 + const plat = doc.platform || 'leaflet'; 618 + const docUrl = buildDocUrl(doc, entityType, plat); 619 + const platformConfig = PLATFORM_CONFIG[plat]; 681 620 const platformBadge = platformConfig 682 621 ? `<span class="platform-badge">${escapeHtml(platformConfig.label)}</span>` 683 622 : ''; 684 623 const date = doc.createdAt ? new Date(doc.createdAt).toLocaleDateString() : ''; 685 - 686 - // platform home URL for meta link 687 - const platformHome = getPlatformHome(platform, doc.basePath); 624 + const platformHome = getPlatformHome(plat, doc.basePath); 688 625 689 626 html += ` 690 627 <div class="result"> ··· 706 643 707 644 resultsDiv.innerHTML = html; 708 645 statsDiv.textContent = `${results.length} result${results.length === 1 ? '' : 's'}`; 709 - 710 - // resolve author display names (non-blocking) 711 646 resolveAuthors(results); 712 647 713 - // load related documents based on top result (skip for semantic/hybrid - already using embeddings) 714 - if (currentMode === 'keyword' && results.length > 0 && results[0].uri) { 648 + if (results.length > 0 && results[0].uri) { 715 649 loadRelated(results[0]); 716 650 } 717 651 718 652 } catch (err) { 719 - resultsDiv.innerHTML = `<div class="status error">error: ${err.message}<br><code style="font-size:0.7rem">${searchUrl}</code></div>`; 653 + resultsDiv.innerHTML = `<div class="status error">error: ${err.message}</div>`; 720 654 } finally { 721 655 searchBtn.disabled = false; 722 656 } ··· 900 834 if (q) params.set('q', q); 901 835 if (currentTag) params.set('tag', currentTag); 902 836 if (currentPlatform) params.set('platform', currentPlatform); 903 - if (currentMode !== 'hybrid') params.set('mode', currentMode); 904 837 const url = params.toString() ? `?${params}` : '/'; 905 838 history.pushState(null, '', url); 906 839 } ··· 956 889 } 957 890 } 958 891 959 - function setMode(mode) { 960 - currentMode = mode; 961 - renderModeSelector(); 962 - if (queryInput.value.trim() || currentTag || currentPlatform) { 963 - doSearch(); 964 - } 965 - } 966 - 967 - function renderModeSelector() { 968 - const modes = [ 969 - { id: 'keyword', label: 'keyword', desc: 'BM25' }, 970 - { id: 'semantic', label: 'semantic', desc: 'embeddings' }, 971 - { id: 'hybrid', label: 'hybrid', desc: 'both' }, 972 - ]; 973 - const html = modes.map(m => ` 974 - <span class="mode-option${currentMode === m.id ? ' active' : ''}" onclick="setMode('${m.id}')">${m.label}<span class="mode-desc">${m.desc}</span></span> 975 - `).join(''); 976 - modeSelectorDiv.innerHTML = `<div class="mode-selector-label">search mode:</div><div class="mode-list">${html}</div>`; 977 - } 978 - 979 892 function renderPlatformFilter() { 980 893 const platforms = [ 981 894 { id: 'leaflet', label: 'leaflet' }, ··· 1096 1009 queryInput.value = params.get('q') || ''; 1097 1010 currentTag = params.get('tag') || null; 1098 1011 currentPlatform = params.get('platform') || null; 1099 - currentMode = params.get('mode') || 'hybrid'; 1100 1012 renderActiveFilter(); 1101 1013 renderTags(); 1102 1014 renderPlatformFilter(); 1103 - renderModeSelector(); 1104 1015 if (queryInput.value || currentTag || currentPlatform) search(queryInput.value, currentTag, currentPlatform); 1105 1016 }); 1106 1017 ··· 1109 1020 const initialQuery = initialParams.get('q'); 1110 1021 const initialTag = initialParams.get('tag'); 1111 1022 const initialPlatform = initialParams.get('platform'); 1112 - const initialMode = initialParams.get('mode'); 1113 1023 if (initialQuery) queryInput.value = initialQuery; 1114 1024 if (initialTag) currentTag = initialTag; 1115 1025 if (initialPlatform) currentPlatform = initialPlatform; 1116 - if (initialMode) currentMode = initialMode; 1117 1026 renderActiveFilter(); 1118 1027 renderPlatformFilter(); 1119 - renderModeSelector(); 1120 1028 1121 1029 if (initialQuery || initialTag || initialPlatform) { 1122 1030 search(initialQuery || '', initialTag, initialPlatform);