search for standard sites
pub-search.waow.tech
search
zig
blog
atproto
1const std = @import("std");
2const json = std.json;
3const Allocator = std.mem.Allocator;
4const zql = @import("zql");
5const db = @import("db/mod.zig");
6const activity = @import("activity.zig");
7
8const TagJson = struct { tag: []const u8, count: i64 };
9const PopularJson = struct { query: []const u8, count: i64 };
10
11const TagsQuery = zql.Query(
12 \\SELECT tag, COUNT(*) as count
13 \\FROM document_tags
14 \\GROUP BY tag
15 \\ORDER BY count DESC
16 \\LIMIT 100
17);
18
19pub fn getTags(alloc: Allocator) ![]const u8 {
20 const c = db.getClient() orelse return error.NotInitialized;
21
22 var output: std.Io.Writer.Allocating = .init(alloc);
23 errdefer output.deinit();
24
25 var res = c.query(TagsQuery.positional, &.{}) catch {
26 try output.writer.writeAll("{\"error\":\"failed to fetch tags\"}");
27 return try output.toOwnedSlice();
28 };
29 defer res.deinit();
30
31 var jw: json.Stringify = .{ .writer = &output.writer };
32 try jw.beginArray();
33 for (res.rows) |row| try jw.write(TagJson{ .tag = row.text(0), .count = row.int(1) });
34 try jw.endArray();
35 return try output.toOwnedSlice();
36}
37
38pub const Stats = struct {
39 documents: i64,
40 publications: i64,
41 searches: i64,
42 errors: i64,
43 started_at: i64,
44 cache_hits: i64,
45 cache_misses: i64,
46};
47
48pub fn getStats() Stats {
49 const c = db.getClient() orelse return .{ .documents = 0, .publications = 0, .searches = 0, .errors = 0, .started_at = 0, .cache_hits = 0, .cache_misses = 0 };
50
51 var res = c.query(
52 \\SELECT
53 \\ (SELECT COUNT(*) FROM documents) as docs,
54 \\ (SELECT COUNT(*) FROM publications) as pubs,
55 \\ (SELECT total_searches FROM stats WHERE id = 1) as searches,
56 \\ (SELECT total_errors FROM stats WHERE id = 1) as errors,
57 \\ (SELECT service_started_at FROM stats WHERE id = 1) as started_at,
58 \\ (SELECT COALESCE(cache_hits, 0) FROM stats WHERE id = 1) as cache_hits,
59 \\ (SELECT COALESCE(cache_misses, 0) FROM stats WHERE id = 1) as cache_misses
60 , &.{}) catch return .{ .documents = 0, .publications = 0, .searches = 0, .errors = 0, .started_at = 0, .cache_hits = 0, .cache_misses = 0 };
61 defer res.deinit();
62
63 const row = res.first() orelse return .{ .documents = 0, .publications = 0, .searches = 0, .errors = 0, .started_at = 0, .cache_hits = 0, .cache_misses = 0 };
64 return .{
65 .documents = row.int(0),
66 .publications = row.int(1),
67 .searches = row.int(2),
68 .errors = row.int(3),
69 .started_at = row.int(4),
70 .cache_hits = row.int(5),
71 .cache_misses = row.int(6),
72 };
73}
74
75pub fn recordSearch(query: []const u8) void {
76 const c = db.getClient() orelse return;
77
78 activity.record();
79 c.exec("UPDATE stats SET total_searches = total_searches + 1 WHERE id = 1", &.{}) catch {};
80
81 // track popular searches (skip empty/very short queries)
82 if (query.len >= 2) {
83 c.exec(
84 "INSERT INTO popular_searches (query, count) VALUES (?, 1) ON CONFLICT(query) DO UPDATE SET count = count + 1",
85 &.{query},
86 ) catch {};
87 }
88}
89
90pub fn recordError() void {
91 const c = db.getClient() orelse return;
92 c.exec("UPDATE stats SET total_errors = total_errors + 1 WHERE id = 1", &.{}) catch {};
93}
94
95pub fn recordCacheHit() void {
96 const c = db.getClient() orelse return;
97 c.exec("UPDATE stats SET cache_hits = COALESCE(cache_hits, 0) + 1 WHERE id = 1", &.{}) catch {};
98}
99
100pub fn recordCacheMiss() void {
101 const c = db.getClient() orelse return;
102 c.exec("UPDATE stats SET cache_misses = COALESCE(cache_misses, 0) + 1 WHERE id = 1", &.{}) catch {};
103}
104
105pub fn getPopular(alloc: Allocator, limit: usize) ![]const u8 {
106 const c = db.getClient() orelse return error.NotInitialized;
107
108 var output: std.Io.Writer.Allocating = .init(alloc);
109 errdefer output.deinit();
110
111 var buf: [8]u8 = undefined;
112 const limit_str = std.fmt.bufPrint(&buf, "{d}", .{limit}) catch "3";
113
114 var res = c.query(
115 "SELECT query, count FROM popular_searches ORDER BY count DESC LIMIT ?",
116 &.{limit_str},
117 ) catch {
118 try output.writer.writeAll("[]");
119 return try output.toOwnedSlice();
120 };
121 defer res.deinit();
122
123 var jw: json.Stringify = .{ .writer = &output.writer };
124 try jw.beginArray();
125 for (res.rows) |row| try jw.write(PopularJson{ .query = row.text(0), .count = row.int(1) });
126 try jw.endArray();
127 return try output.toOwnedSlice();
128}