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

feat: add publicationName to search results

Join publications table in all search queries to return
the human-readable publication name alongside basePath.

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

+79 -23
+78 -23
backend/src/server/search.zig
··· 33 33 path: []const u8 = "", // URL path from record (e.g., "/001") 34 34 source: []const u8 = "", 35 35 coverImage: []const u8 = "", 36 + publicationName: []const u8 = "", 36 37 }; 37 38 38 39 /// Document search result (internal) ··· 48 49 platform: []const u8, 49 50 path: []const u8, 50 51 coverImage: []const u8, 52 + publicationName: []const u8, 51 53 52 54 fn fromRow(row: db.Row) Doc { 53 55 return .{ ··· 62 64 .platform = row.text(8), 63 65 .path = row.text(9), 64 66 .coverImage = row.text(10), 67 + .publicationName = row.text(11), 65 68 }; 66 69 } 67 70 ··· 78 81 .platform = row.text(8), 79 82 .path = row.text(9), 80 83 .coverImage = row.text(10), 84 + .publicationName = row.text(11), 81 85 }; 82 86 } 83 87 ··· 94 98 .platform = self.platform, 95 99 .path = self.path, 96 100 .coverImage = self.coverImage, 101 + .publicationName = self.publicationName, 97 102 }; 98 103 } 99 104 }; ··· 102 107 \\SELECT d.uri, d.did, d.title, '' as snippet, 103 108 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 104 109 \\ d.platform, COALESCE(d.path, '') as path, 105 - \\ COALESCE(d.cover_image, '') as cover_image 110 + \\ COALESCE(d.cover_image, '') as cover_image, 111 + \\ COALESCE(p.name, '') as publication_name 106 112 \\FROM documents d 113 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 107 114 \\JOIN document_tags dt ON d.uri = dt.document_uri 108 115 \\WHERE dt.tag = :tag 109 116 \\ORDER BY d.created_at DESC LIMIT 40 ··· 114 121 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 115 122 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 116 123 \\ d.platform, COALESCE(d.path, '') as path, 117 - \\ COALESCE(d.cover_image, '') as cover_image 124 + \\ COALESCE(d.cover_image, '') as cover_image, 125 + \\ COALESCE(p.name, '') as publication_name 118 126 \\FROM documents_fts f 119 127 \\JOIN documents d ON f.uri = d.uri 128 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 120 129 \\JOIN document_tags dt ON d.uri = dt.document_uri 121 130 \\WHERE documents_fts MATCH :query AND dt.tag = :tag 122 131 \\ORDER BY rank + (julianday('now') - julianday(d.created_at)) / 30.0 LIMIT 40 ··· 127 136 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 128 137 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 129 138 \\ d.platform, COALESCE(d.path, '') as path, 130 - \\ COALESCE(d.cover_image, '') as cover_image 139 + \\ COALESCE(d.cover_image, '') as cover_image, 140 + \\ COALESCE(p.name, '') as publication_name 131 141 \\FROM documents_fts f 132 142 \\JOIN documents d ON f.uri = d.uri 143 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 133 144 \\WHERE documents_fts MATCH :query 134 145 \\ORDER BY rank + (julianday('now') - julianday(d.created_at)) / 30.0 LIMIT 40 135 146 ); ··· 139 150 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 140 151 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 141 152 \\ d.platform, COALESCE(d.path, '') as path, 142 - \\ COALESCE(d.cover_image, '') as cover_image 153 + \\ COALESCE(d.cover_image, '') as cover_image, 154 + \\ COALESCE(p.name, '') as publication_name 143 155 \\FROM documents_fts f 144 156 \\JOIN documents d ON f.uri = d.uri 157 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 145 158 \\WHERE documents_fts MATCH :query AND d.created_at >= :since 146 159 \\ORDER BY rank + (julianday('now') - julianday(d.created_at)) / 30.0 LIMIT 40 147 160 ); ··· 151 164 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 152 165 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 153 166 \\ d.platform, COALESCE(d.path, '') as path, 154 - \\ COALESCE(d.cover_image, '') as cover_image 167 + \\ COALESCE(d.cover_image, '') as cover_image, 168 + \\ COALESCE(p.name, '') as publication_name 155 169 \\FROM documents_fts f 156 170 \\JOIN documents d ON f.uri = d.uri 171 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 157 172 \\WHERE documents_fts MATCH :query AND d.platform = :platform 158 173 \\ORDER BY rank + (julianday('now') - julianday(d.created_at)) / 30.0 LIMIT 40 159 174 ); ··· 163 178 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 164 179 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 165 180 \\ d.platform, COALESCE(d.path, '') as path, 166 - \\ COALESCE(d.cover_image, '') as cover_image 181 + \\ COALESCE(d.cover_image, '') as cover_image, 182 + \\ COALESCE(p.name, '') as publication_name 167 183 \\FROM documents_fts f 168 184 \\JOIN documents d ON f.uri = d.uri 185 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 169 186 \\WHERE documents_fts MATCH :query AND d.platform = :platform AND d.created_at >= :since 170 187 \\ORDER BY rank + (julianday('now') - julianday(d.created_at)) / 30.0 LIMIT 40 171 188 ); ··· 174 191 \\SELECT d.uri, d.did, d.title, '' as snippet, 175 192 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 176 193 \\ d.platform, COALESCE(d.path, '') as path, 177 - \\ COALESCE(d.cover_image, '') as cover_image 194 + \\ COALESCE(d.cover_image, '') as cover_image, 195 + \\ COALESCE(p.name, '') as publication_name 178 196 \\FROM documents d 197 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 179 198 \\JOIN document_tags dt ON d.uri = dt.document_uri 180 199 \\WHERE dt.tag = :tag AND d.platform = :platform 181 200 \\ORDER BY d.created_at DESC LIMIT 40 ··· 186 205 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 187 206 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 188 207 \\ d.platform, COALESCE(d.path, '') as path, 189 - \\ COALESCE(d.cover_image, '') as cover_image 208 + \\ COALESCE(d.cover_image, '') as cover_image, 209 + \\ COALESCE(p.name, '') as publication_name 190 210 \\FROM documents_fts f 191 211 \\JOIN documents d ON f.uri = d.uri 212 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 192 213 \\JOIN document_tags dt ON d.uri = dt.document_uri 193 214 \\WHERE documents_fts MATCH :query AND dt.tag = :tag AND d.platform = :platform 194 215 \\ORDER BY rank + (julianday('now') - julianday(d.created_at)) / 30.0 LIMIT 40 ··· 198 219 \\SELECT d.uri, d.did, d.title, '' as snippet, 199 220 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 200 221 \\ d.platform, COALESCE(d.path, '') as path, 201 - \\ COALESCE(d.cover_image, '') as cover_image 222 + \\ COALESCE(d.cover_image, '') as cover_image, 223 + \\ COALESCE(p.name, '') as publication_name 202 224 \\FROM documents d 225 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 203 226 \\WHERE d.platform = :platform 204 227 \\ORDER BY d.created_at DESC LIMIT 40 205 228 ); ··· 208 231 \\SELECT d.uri, d.did, d.title, '' as snippet, 209 232 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 210 233 \\ d.platform, COALESCE(d.path, '') as path, 211 - \\ COALESCE(d.cover_image, '') as cover_image 234 + \\ COALESCE(d.cover_image, '') as cover_image, 235 + \\ COALESCE(p.name, '') as publication_name 212 236 \\FROM documents d 237 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 213 238 \\WHERE d.did = :author 214 239 \\ORDER BY d.created_at DESC LIMIT 40 215 240 ); ··· 218 243 \\SELECT d.uri, d.did, d.title, '' as snippet, 219 244 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 220 245 \\ d.platform, COALESCE(d.path, '') as path, 221 - \\ COALESCE(d.cover_image, '') as cover_image 246 + \\ COALESCE(d.cover_image, '') as cover_image, 247 + \\ COALESCE(p.name, '') as publication_name 222 248 \\FROM documents d 249 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 223 250 \\WHERE d.did = :author AND d.platform = :platform 224 251 \\ORDER BY d.created_at DESC LIMIT 40 225 252 ); ··· 233 260 \\ p.base_path, 234 261 \\ 1 as has_publication, 235 262 \\ d.platform, COALESCE(d.path, '') as path, 236 - \\ COALESCE(d.cover_image, '') as cover_image 263 + \\ COALESCE(d.cover_image, '') as cover_image, 264 + \\ COALESCE(p.name, '') as publication_name 237 265 \\FROM documents d 238 266 \\JOIN publications p ON d.publication_uri = p.uri 239 267 \\JOIN publications_fts pf ON p.uri = pf.uri ··· 247 275 \\ p.base_path, 248 276 \\ 1 as has_publication, 249 277 \\ d.platform, COALESCE(d.path, '') as path, 250 - \\ COALESCE(d.cover_image, '') as cover_image 278 + \\ COALESCE(d.cover_image, '') as cover_image, 279 + \\ COALESCE(p.name, '') as publication_name 251 280 \\FROM documents d 252 281 \\JOIN publications p ON d.publication_uri = p.uri 253 282 \\JOIN publications_fts pf ON p.uri = pf.uri ··· 261 290 \\ p.base_path, 262 291 \\ 1 as has_publication, 263 292 \\ d.platform, COALESCE(d.path, '') as path, 264 - \\ COALESCE(d.cover_image, '') as cover_image 293 + \\ COALESCE(d.cover_image, '') as cover_image, 294 + \\ COALESCE(p.name, '') as publication_name 265 295 \\FROM documents d 266 296 \\JOIN publications p ON d.publication_uri = p.uri 267 297 \\JOIN publications_fts pf ON p.uri = pf.uri ··· 275 305 \\ p.base_path, 276 306 \\ 1 as has_publication, 277 307 \\ d.platform, COALESCE(d.path, '') as path, 278 - \\ COALESCE(d.cover_image, '') as cover_image 308 + \\ COALESCE(d.cover_image, '') as cover_image, 309 + \\ COALESCE(p.name, '') as publication_name 279 310 \\FROM documents d 280 311 \\JOIN publications p ON d.publication_uri = p.uri 281 312 \\JOIN publications_fts pf ON p.uri = pf.uri ··· 570 601 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 571 602 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 572 603 \\ d.platform, COALESCE(d.path, '') as path, 573 - \\ COALESCE(d.cover_image, '') as cover_image 604 + \\ COALESCE(d.cover_image, '') as cover_image, 605 + \\ COALESCE(p.name, '') as publication_name 574 606 \\FROM documents_fts f 575 607 \\JOIN documents d ON f.uri = d.uri 608 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 576 609 \\WHERE documents_fts MATCH ? AND d.platform = ? AND (? = '' OR d.did = ?) 577 610 \\ORDER BY rank LIMIT 40 578 611 , .{ fts_query, platform, author_val, author_val }); ··· 597 630 \\SELECT d.uri, d.did, d.title, '' as snippet, 598 631 \\ d.created_at, d.rkey, p.base_path, 599 632 \\ 1 as has_publication, d.platform, COALESCE(d.path, '') as path, 600 - \\ COALESCE(d.cover_image, '') as cover_image 633 + \\ COALESCE(d.cover_image, '') as cover_image, 634 + \\ COALESCE(p.name, '') as publication_name 601 635 \\FROM documents d 602 636 \\JOIN publications p ON d.publication_uri = p.uri 603 637 \\JOIN publications_fts pf ON p.uri = pf.uri ··· 625 659 \\ snippet(documents_fts, 2, '', '', '...', 32) as snippet, 626 660 \\ d.created_at, d.rkey, d.base_path, d.has_publication, 627 661 \\ d.platform, COALESCE(d.path, '') as path, 628 - \\ COALESCE(d.cover_image, '') as cover_image 662 + \\ COALESCE(d.cover_image, '') as cover_image, 663 + \\ COALESCE(p.name, '') as publication_name 629 664 \\FROM documents_fts f 630 665 \\JOIN documents d ON f.uri = d.uri 666 + \\LEFT JOIN publications p ON d.publication_uri = p.uri 631 667 \\WHERE documents_fts MATCH ? AND (? = '' OR d.did = ?) 632 668 \\ORDER BY rank LIMIT 40 633 669 , .{ fts_query, author_val, author_val }); ··· 659 695 \\SELECT d.uri, d.did, d.title, '' as snippet, 660 696 \\ d.created_at, d.rkey, p.base_path, 661 697 \\ 1 as has_publication, d.platform, COALESCE(d.path, '') as path, 662 - \\ COALESCE(d.cover_image, '') as cover_image 698 + \\ COALESCE(d.cover_image, '') as cover_image, 699 + \\ COALESCE(p.name, '') as publication_name 663 700 \\FROM documents d 664 701 \\JOIN publications p ON d.publication_uri = p.uri 665 702 \\JOIN publications_fts pf ON p.uri = pf.uri ··· 867 904 .platform = r.platform, 868 905 .path = r.path, 869 906 .coverImage = extras.cover_images.get(r.uri) orelse "", 907 + .publicationName = extras.pub_names.get(r.uri) orelse "", 870 908 }); 871 909 count += 1; 872 910 } ··· 1083 1121 }; 1084 1122 try jw.objectField("coverImage"); 1085 1123 try jw.write(cover); 1124 + // for semantic-only results, pub name may need local DB fallback 1125 + const pub_name = blk: { 1126 + const existing = jsonStr(obj, "publicationName"); 1127 + if (existing.len > 0) break :blk existing; 1128 + if (bits & 0b10 != 0) break :blk hybrid_extras.pub_names.get(entry.uri) orelse ""; 1129 + break :blk existing; 1130 + }; 1131 + try jw.objectField("publicationName"); 1132 + try jw.write(pub_name); 1086 1133 try jw.objectField("createdAt"); 1087 1134 try jw.write(jsonStr(obj, "createdAt")); 1088 1135 try jw.objectField("source"); ··· 1193 1240 .platform = r.platform, 1194 1241 .path = r.path, 1195 1242 .coverImage = extras.cover_images.get(r.uri) orelse "", 1243 + .publicationName = extras.pub_names.get(r.uri) orelse "", 1196 1244 }); 1197 1245 } 1198 1246 try jw.endArray(); ··· 1206 1254 const LocalExtras = struct { 1207 1255 snippets: std.StringHashMap([]const u8), 1208 1256 cover_images: std.StringHashMap([]const u8), 1257 + pub_names: std.StringHashMap([]const u8), 1209 1258 }; 1210 1259 1211 - /// Fetch content previews and cover images from local SQLite for a list of URIs. 1260 + /// Fetch content previews, cover images, and publication names from local SQLite for a list of URIs. 1212 1261 /// Gracefully returns empty maps if local db is unavailable. 1213 1262 fn fetchLocalExtras(alloc: Allocator, uris: []const []const u8) LocalExtras { 1214 1263 var snippets = std.StringHashMap([]const u8).init(alloc); 1215 1264 var cover_images = std.StringHashMap([]const u8).init(alloc); 1216 - const local = db.getLocalDb() orelse return .{ .snippets = snippets, .cover_images = cover_images }; 1265 + var pub_names = std.StringHashMap([]const u8).init(alloc); 1266 + const local = db.getLocalDb() orelse return .{ .snippets = snippets, .cover_images = cover_images, .pub_names = pub_names }; 1217 1267 for (uris) |uri| { 1218 1268 var rows = local.query( 1219 - "SELECT substr(content, 1, 200), COALESCE(cover_image, '') FROM documents WHERE uri = ?", 1269 + "SELECT substr(content, 1, 200), COALESCE(cover_image, ''), COALESCE((SELECT name FROM publications WHERE uri = documents.publication_uri), '') FROM documents WHERE uri = ?", 1220 1270 .{uri}, 1221 1271 ) catch continue; 1222 1272 defer rows.deinit(); ··· 1231 1281 const duped = alloc.dupe(u8, cover) catch continue; 1232 1282 cover_images.put(uri, duped) catch continue; 1233 1283 } 1284 + const pub_name = row.text(2); 1285 + if (pub_name.len > 0) { 1286 + const duped = alloc.dupe(u8, pub_name) catch continue; 1287 + pub_names.put(uri, duped) catch continue; 1288 + } 1234 1289 } 1235 1290 } 1236 - return .{ .snippets = snippets, .cover_images = cover_images }; 1291 + return .{ .snippets = snippets, .cover_images = cover_images, .pub_names = pub_names }; 1237 1292 } 1238 1293 1239 1294 // --- JSON helpers (for hybrid search parsing) ---
+1
mcp/src/pub_search/_types.py
··· 20 20 path: str = "" 21 21 source: str = "" 22 22 score: float = 0.0 23 + publicationName: str = "" 23 24 24 25 @computed_field 25 26 @property