@gscdump/engine 1.4.11 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/analysis-range.d.mts +7 -0
  2. package/dist/analysis-range.mjs +33 -0
  3. package/dist/entities/empty-types.d.mts +22 -0
  4. package/dist/entities/empty-types.mjs +58 -0
  5. package/dist/entities/indexing-metadata.d.mts +26 -0
  6. package/dist/entities/indexing-metadata.mjs +31 -0
  7. package/dist/entities/inspection.d.mts +240 -0
  8. package/dist/entities/inspection.mjs +443 -0
  9. package/dist/entities/io.mjs +16 -0
  10. package/dist/{query-dim.d.mts → entities/query-dim.d.mts} +3 -3
  11. package/dist/{query-dim.mjs → entities/query-dim.mjs} +4 -4
  12. package/dist/{sitemap-projection.mjs → entities/sitemap-projection.mjs} +1 -1
  13. package/dist/entities/sitemap-shared.d.mts +199 -0
  14. package/dist/entities/sitemap-shared.mjs +243 -0
  15. package/dist/entities/sitemap-write.d.mts +3 -0
  16. package/dist/entities/sitemap-write.mjs +528 -0
  17. package/dist/entities/sitemap.d.mts +3 -0
  18. package/dist/entities/sitemap.mjs +91 -0
  19. package/dist/entities.d.mts +9 -481
  20. package/dist/entities.mjs +7 -1380
  21. package/dist/rollups/canonical.d.mts +71 -0
  22. package/dist/rollups/canonical.mjs +335 -0
  23. package/dist/rollups/core.d.mts +201 -0
  24. package/dist/rollups/core.mjs +116 -0
  25. package/dist/rollups/dates.mjs +11 -0
  26. package/dist/rollups/defaults.d.mts +11 -0
  27. package/dist/rollups/defaults.mjs +17 -0
  28. package/dist/rollups/hourly.d.mts +38 -0
  29. package/dist/rollups/hourly.mjs +38 -0
  30. package/dist/rollups/indexing.d.mts +46 -0
  31. package/dist/rollups/indexing.mjs +357 -0
  32. package/dist/rollups/traffic.d.mts +38 -0
  33. package/dist/rollups/traffic.mjs +289 -0
  34. package/dist/rollups/windows.d.mts +90 -0
  35. package/dist/rollups/windows.mjs +176 -0
  36. package/dist/rollups.d.mts +8 -471
  37. package/dist/rollups.mjs +7 -1316
  38. package/package.json +9 -4
  39. /package/dist/{sitemap-projection.d.mts → entities/sitemap-projection.d.mts} +0 -0
package/dist/rollups.mjs CHANGED
@@ -1,1317 +1,8 @@
1
- import { engineErrors } from "./errors.mjs";
2
- import "./layout.mjs";
3
- import { inspectionParquetKey, sitemapUrlsIndexPrefix, sitemapUrlsPrefix, sitemapUrlsProjectionManifestKey } from "./entity-keys.mjs";
4
- import { encodeRowsToParquetFlex } from "./adapters/hyparquet.mjs";
5
- import { readOptional } from "./adapters/read-optional.mjs";
6
- import { decodeSitemapProjectionManifest, selectSitemapProjectionFiles } from "./sitemap-projection.mjs";
7
- import { createQueryDimStore } from "./query-dim.mjs";
8
- import { createIndexingMetadataStore, createSitemapReadStore } from "./entities.mjs";
9
- import { MS_PER_DAY } from "gscdump/dates";
10
- import { encodeJsonBigintSafe } from "@gscdump/lakehouse/bigint";
11
- function rollupPrefix(ctx, searchType) {
12
- const base = ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/rollups` : `u_${ctx.userId}/rollups`;
13
- return searchType !== void 0 && searchType !== "web" ? `${base}/${searchType}` : base;
14
- }
15
- function rollupKey(ctx, id, builtAt, searchType) {
16
- return `${rollupPrefix(ctx, searchType)}/${id}__v${builtAt}.json`;
17
- }
18
- function rollupParquetKey(ctx, id, builtAt, searchType) {
19
- return `${rollupPrefix(ctx, searchType)}/${id}__v${builtAt}.parquet`;
20
- }
21
- const ROLLUP_FILE_RE = /^(?<id>[a-z0-9_]+)__v(?<ts>\d+)\.json$/;
22
- async function readLatestRollup(bucket, ctx, id, searchType) {
23
- const prefix = `${rollupPrefix(ctx, searchType)}/`;
24
- let newest = null;
25
- let cursor;
26
- do {
27
- const listing = await bucket.list({
28
- prefix,
29
- cursor
30
- });
31
- for (const obj of listing.objects) {
32
- const m = ROLLUP_FILE_RE.exec(obj.key.slice(prefix.length));
33
- if (!m?.groups || m.groups.id !== id) continue;
34
- const ts = Number(m.groups.ts);
35
- if (!newest || ts > newest.ts) newest = {
36
- ts,
37
- key: obj.key
38
- };
39
- }
40
- cursor = listing.truncated ? listing.cursor : void 0;
41
- } while (cursor !== void 0);
42
- if (!newest) return null;
43
- const obj = await bucket.get(newest.key);
44
- if (!obj) return null;
45
- return JSON.parse(await obj.text());
46
- }
47
- async function rebuildRollups(opts) {
48
- const now = opts.now ?? (() => Date.now());
49
- const dataEndMs = opts.dataEndDate !== void 0 ? isoDateToUtcMs(opts.dataEndDate) : null;
50
- const results = [];
51
- for (const def of opts.defs) {
52
- const builtAt = now();
53
- const windowAnchorMs = dataEndMs ?? builtAt;
54
- const defSearchType = def.sliceOrthogonal === true ? void 0 : opts.searchType;
55
- try {
56
- const payload = await def.build({
57
- engine: opts.engine,
58
- ctx: opts.ctx,
59
- dataSource: opts.dataSource,
60
- windowAnchorMs,
61
- ...defSearchType !== void 0 ? { searchType: defSearchType } : {}
62
- });
63
- if (def.format === "parquet") {
64
- if (!def.parquetColumns || def.parquetColumns.length === 0) throw new Error(`rollup '${def.id}' declared format='parquet' without parquetColumns`);
65
- const rows = payload ?? [];
66
- const parquetBytes = encodeRowsToParquetFlex(rows, {
67
- columns: def.parquetColumns,
68
- sortKey: def.parquetSortKey
69
- });
70
- const parquetKey = rollupParquetKey(opts.ctx, def.id, builtAt, defSearchType);
71
- await opts.dataSource.write(parquetKey, parquetBytes);
72
- const pointer = {
73
- parquetKey,
74
- rowCount: rows.length
75
- };
76
- const envelopeBytes = encodeJsonBigintSafe({
77
- version: 1,
78
- id: def.id,
79
- builtAt,
80
- windowDays: def.windowDays,
81
- payload: pointer
82
- });
83
- const key = rollupKey(opts.ctx, def.id, builtAt, defSearchType);
84
- await opts.dataSource.write(key, envelopeBytes);
85
- results.push({
86
- id: def.id,
87
- objectKey: key,
88
- parquetKey,
89
- bytes: envelopeBytes.byteLength,
90
- parquetBytes: parquetBytes.byteLength,
91
- builtAt
92
- });
93
- continue;
94
- }
95
- const bytes = encodeJsonBigintSafe({
96
- version: 1,
97
- id: def.id,
98
- builtAt,
99
- windowDays: def.windowDays,
100
- payload
101
- });
102
- const key = rollupKey(opts.ctx, def.id, builtAt, defSearchType);
103
- await opts.dataSource.write(key, bytes);
104
- results.push({
105
- id: def.id,
106
- objectKey: key,
107
- bytes: bytes.byteLength,
108
- builtAt
109
- });
110
- } catch (err) {
111
- results.push({
112
- id: def.id,
113
- objectKey: "",
114
- bytes: 0,
115
- builtAt,
116
- error: engineErrors.rollupBuildFailed(def.id, err)
117
- });
118
- }
119
- }
120
- return results;
121
- }
122
- function isoDateToUtcMs(iso) {
123
- const m = /^(\d{4})-(\d{2})-(\d{2})$/.exec(iso);
124
- if (!m) throw new Error(`dataEndDate must be ISO YYYY-MM-DD, got: ${iso}`);
125
- return Date.UTC(Number(m[1]), Number(m[2]) - 1, Number(m[3]));
126
- }
127
- function utcDateMinusDays(at, days) {
128
- const d = new Date(at - days * MS_PER_DAY);
129
- return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
130
- }
131
- const WINDOW_BYTE_BUDGET = 10 * 1024 * 1024;
132
- const ROLLUP_PAGE_ROWS = 5e4;
133
- const ROLLUP_PAGE_ROWS_WIDE = 2e4;
134
- const ROLLUP_PAGE_ROWS_DAILY = 7e4;
135
- const DAILY_MAX_WINDOW_DAYS = 7;
136
- const DAY_RE = /^daily\/(\d{4})-(\d{2})-(\d{2})$/;
137
- const WEEK_RE = /^weekly\/(\d{4})-(\d{2})-(\d{2})$/;
138
- const MONTH_RE = /^monthly\/(\d{4})-(\d{2})$/;
139
- const QUARTER_RE = /^quarterly\/(\d{4})-Q([1-4])$/;
140
- function isoDate(ms) {
141
- const d = new Date(ms);
142
- return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
143
- }
144
- function partitionDaySpan(partition) {
145
- const day = DAY_RE.exec(partition);
146
- if (day) {
147
- const ms = Date.UTC(Number(day[1]), Number(day[2]) - 1, Number(day[3]));
148
- return {
149
- startMs: ms,
150
- endMs: ms
151
- };
152
- }
153
- const week = WEEK_RE.exec(partition);
154
- if (week) {
155
- const ms = Date.UTC(Number(week[1]), Number(week[2]) - 1, Number(week[3]));
156
- return {
157
- startMs: ms,
158
- endMs: ms + 6 * MS_PER_DAY
159
- };
160
- }
161
- const month = MONTH_RE.exec(partition);
162
- if (month) {
163
- const y = Number(month[1]);
164
- const m = Number(month[2]) - 1;
165
- return {
166
- startMs: Date.UTC(y, m, 1),
167
- endMs: Date.UTC(y, m + 1, 1) - MS_PER_DAY
168
- };
169
- }
170
- const quarter = QUARTER_RE.exec(partition);
171
- if (quarter) {
172
- const y = Number(quarter[1]);
173
- const startMonth = (Number(quarter[2]) - 1) * 3;
174
- return {
175
- startMs: Date.UTC(y, startMonth, 1),
176
- endMs: Date.UTC(y, startMonth + 3, 1) - MS_PER_DAY
177
- };
178
- }
179
- return null;
180
- }
181
- function clamp(n, lo, hi) {
182
- return Math.max(lo, Math.min(hi, n));
183
- }
184
- function planRollupWindows(parts, clampRange, maxWindowDays) {
185
- const clampStartMs = clampRange ? Date.parse(`${clampRange.start}T00:00:00Z`) : void 0;
186
- const clampEndMs = clampRange ? Date.parse(`${clampRange.end}T00:00:00Z`) : void 0;
187
- const spans = [];
188
- for (const p of parts) {
189
- const span = partitionDaySpan(p.partition);
190
- if (!span) continue;
191
- if (clampStartMs !== void 0 && clampEndMs !== void 0) {
192
- if (span.endMs < clampStartMs || span.startMs > clampEndMs) continue;
193
- }
194
- spans.push({
195
- partition: p.partition,
196
- bytes: p.bytes,
197
- startMs: span.startMs,
198
- endMs: span.endMs
199
- });
200
- }
201
- if (spans.length === 0) return [];
202
- let rangeStartMs = Number.POSITIVE_INFINITY;
203
- let rangeEndMs = Number.NEGATIVE_INFINITY;
204
- let totalBytes = 0;
205
- for (const span of spans) {
206
- if (span.startMs < rangeStartMs) rangeStartMs = span.startMs;
207
- if (span.endMs > rangeEndMs) rangeEndMs = span.endMs;
208
- totalBytes += span.bytes;
209
- }
210
- if (clampStartMs !== void 0) rangeStartMs = Math.max(rangeStartMs, clampStartMs);
211
- if (clampEndMs !== void 0) rangeEndMs = Math.min(rangeEndMs, clampEndMs);
212
- const spanDays = Math.floor((rangeEndMs - rangeStartMs) / MS_PER_DAY) + 1;
213
- const bytesPerDay = Math.max(1, totalBytes / spanDays);
214
- const byteWindowDays = clamp(Math.floor(WINDOW_BYTE_BUDGET / bytesPerDay), 7, 400);
215
- const windowDays = maxWindowDays != null ? Math.max(1, Math.min(byteWindowDays, maxWindowDays)) : byteWindowDays;
216
- if (!Number.isFinite(windowDays) || !Number.isFinite(rangeStartMs) || !Number.isFinite(rangeEndMs) || rangeEndMs < rangeStartMs) return [];
217
- const windowWidthMs = windowDays * MS_PER_DAY;
218
- const windowCount = Math.floor((rangeEndMs - rangeStartMs) / windowWidthMs) + 1;
219
- const partitionsByWindow = Array.from({ length: windowCount }, () => []);
220
- for (const span of spans) {
221
- const overlapStartMs = Math.max(span.startMs, rangeStartMs);
222
- const overlapEndMs = Math.min(span.endMs, rangeEndMs);
223
- if (overlapStartMs > overlapEndMs) continue;
224
- const firstWindow = Math.max(0, Math.ceil((overlapStartMs - rangeStartMs - (windowDays - 1) * MS_PER_DAY) / windowWidthMs));
225
- const lastWindow = Math.min(windowCount - 1, Math.floor((overlapEndMs - rangeStartMs) / windowWidthMs));
226
- for (let index = firstWindow; index <= lastWindow; index++) partitionsByWindow[index].push(span.partition);
227
- }
228
- const windows = [];
229
- for (let index = 0; index < windowCount; index++) {
230
- const cursorMs = rangeStartMs + index * windowWidthMs;
231
- const windowEndMs = Math.min(cursorMs + (windowDays - 1) * MS_PER_DAY, rangeEndMs);
232
- const partitions = partitionsByWindow[index];
233
- if (partitions.length > 0) windows.push({
234
- start: isoDate(cursorMs),
235
- end: isoDate(windowEndMs),
236
- partitions
237
- });
238
- }
239
- return windows;
240
- }
241
- function partitionsInRange(parts, start, end) {
242
- const startMs = Date.parse(`${start}T00:00:00Z`);
243
- const endMs = Date.parse(`${end}T00:00:00Z`);
244
- const out = [];
245
- for (const p of parts) {
246
- const span = partitionDaySpan(p.partition);
247
- if (!span) continue;
248
- if (span.endMs >= startMs && span.startMs <= endMs) out.push(p.partition);
249
- }
250
- return out;
251
- }
252
- async function runPagedQuery(opts) {
253
- const out = [];
254
- for (let offset = 0;; offset += opts.pageRows) {
255
- const result = await opts.engine.runSQL({
256
- ctx: opts.ctx,
257
- table: opts.table,
258
- fileSets: opts.fileSets,
259
- sql: `${opts.coreSql}\nORDER BY ${opts.orderBy}\nLIMIT ${opts.pageRows} OFFSET ${offset}`,
260
- ...opts.searchType !== void 0 ? { searchType: opts.searchType } : {}
261
- });
262
- out.push(...result.rows);
263
- if (result.rows.length < opts.pageRows) break;
264
- }
265
- return out;
266
- }
267
- async function runWindowed(opts) {
268
- const windows = planRollupWindows(await opts.engine.listPartitions({
269
- ctx: opts.ctx,
270
- table: opts.table,
271
- ...opts.searchType !== void 0 ? { searchType: opts.searchType } : {}
272
- }), void 0, opts.maxWindowDays);
273
- const rows = [];
274
- for (const w of windows) {
275
- const fileSets = {
276
- FILES: {
277
- table: opts.table,
278
- partitions: w.partitions
279
- },
280
- ...opts.extraFileSets
281
- };
282
- if (opts.paginate) {
283
- rows.push(...await runPagedQuery({
284
- engine: opts.engine,
285
- ctx: opts.ctx,
286
- table: opts.table,
287
- ...opts.searchType !== void 0 ? { searchType: opts.searchType } : {},
288
- fileSets,
289
- coreSql: opts.sqlFor(w),
290
- orderBy: opts.paginate.orderBy,
291
- pageRows: opts.paginate.pageRows
292
- }));
293
- continue;
294
- }
295
- const result = await opts.engine.runSQL({
296
- ctx: opts.ctx,
297
- table: opts.table,
298
- fileSets,
299
- sql: opts.sqlFor(w),
300
- ...opts.searchType !== void 0 ? { searchType: opts.searchType } : {}
301
- });
302
- rows.push(...result.rows);
303
- }
304
- return rows;
305
- }
306
- const dailyTotalsRollup = {
307
- id: "daily_totals",
308
- windowDays: null,
309
- async build({ engine, ctx, searchType }) {
310
- const pageRows = await runWindowed({
311
- engine,
312
- ctx,
313
- table: "pages",
314
- ...searchType !== void 0 ? { searchType } : {},
315
- sqlFor: (w) => `
316
- SELECT
317
- date,
318
- SUM(clicks)::BIGINT AS clicks,
319
- SUM(impressions)::BIGINT AS impressions,
320
- SUM(sum_position)::DOUBLE AS sum_position
321
- FROM read_parquet({{FILES}}, union_by_name = true)
322
- WHERE date >= '${w.start}' AND date <= '${w.end}'
323
- GROUP BY date
324
- ORDER BY date
325
- `
326
- });
327
- const queryRows = await runWindowed({
328
- engine,
329
- ctx,
330
- table: "queries",
331
- ...searchType !== void 0 ? { searchType } : {},
332
- sqlFor: (w) => `
333
- SELECT
334
- date,
335
- SUM(impressions)::BIGINT AS impressions
336
- FROM read_parquet({{FILES}}, union_by_name = true)
337
- WHERE date >= '${w.start}' AND date <= '${w.end}'
338
- GROUP BY date
339
- `
340
- });
341
- const pagesByDate = /* @__PURE__ */ new Map();
342
- for (const r of pageRows) {
343
- const date = String(r.date);
344
- const cur = pagesByDate.get(date) ?? {
345
- date,
346
- clicks: BigInt(0),
347
- impressions: BigInt(0),
348
- sum_position: 0
349
- };
350
- cur.clicks += BigInt(r.clicks);
351
- cur.impressions += BigInt(r.impressions);
352
- cur.sum_position += Number(r.sum_position);
353
- pagesByDate.set(date, cur);
354
- }
355
- const queryImpressionsByDate = /* @__PURE__ */ new Map();
356
- for (const r of queryRows) {
357
- const date = String(r.date);
358
- queryImpressionsByDate.set(date, (queryImpressionsByDate.get(date) ?? BigInt(0)) + BigInt(r.impressions));
359
- }
360
- return Array.from(pagesByDate.values()).sort((a, b) => a.date < b.date ? -1 : 1).map((r) => {
361
- const totalImpressions = BigInt(r.impressions);
362
- const queryImpressions = queryImpressionsByDate.get(String(r.date)) ?? BigInt(0);
363
- const anonymized = totalImpressions === BigInt(0) ? 0 : 1 - Number(queryImpressions) / Number(totalImpressions);
364
- return {
365
- date: r.date,
366
- clicks: Number(r.clicks),
367
- impressions: Number(r.impressions),
368
- sum_position: Number(r.sum_position),
369
- anonymizedImpressionsPct: Math.max(0, Math.min(1, anonymized))
370
- };
371
- });
372
- }
373
- };
374
- const weeklyTotalsRollup = {
375
- id: "weekly_totals",
376
- windowDays: null,
377
- async build({ engine, ctx, searchType }) {
378
- const rows = await runWindowed({
379
- engine,
380
- ctx,
381
- table: "pages",
382
- ...searchType !== void 0 ? { searchType } : {},
383
- sqlFor: (w) => `
384
- SELECT
385
- strftime(date_trunc('week', date::DATE), '%Y-%m-%d') AS week,
386
- SUM(clicks)::BIGINT AS clicks,
387
- SUM(impressions)::BIGINT AS impressions,
388
- SUM(sum_position)::DOUBLE AS sum_position
389
- FROM read_parquet({{FILES}}, union_by_name = true)
390
- WHERE date >= '${w.start}' AND date <= '${w.end}'
391
- GROUP BY 1
392
- ORDER BY 1
393
- `
394
- });
395
- const byWeek = /* @__PURE__ */ new Map();
396
- for (const r of rows) {
397
- const week = String(r.week);
398
- const cur = byWeek.get(week) ?? {
399
- week,
400
- clicks: 0,
401
- impressions: 0,
402
- sum_position: 0
403
- };
404
- cur.clicks += Number(r.clicks);
405
- cur.impressions += Number(r.impressions);
406
- cur.sum_position += Number(r.sum_position);
407
- byWeek.set(week, cur);
408
- }
409
- return Array.from(byWeek.values()).sort((a, b) => a.week < b.week ? -1 : 1);
410
- }
411
- };
412
- const topPages28dRollup = {
413
- id: "top_pages_28d",
414
- windowDays: 28,
415
- async build({ engine, ctx, windowAnchorMs, searchType }) {
416
- const cutoff = utcDateMinusDays(windowAnchorMs, 28);
417
- const partitions = partitionsInRange(await engine.listPartitions({
418
- ctx,
419
- table: "pages",
420
- ...searchType !== void 0 ? { searchType } : {}
421
- }), cutoff, utcDateMinusDays(windowAnchorMs, 0));
422
- if (partitions.length === 0) return [];
423
- return (await engine.runSQL({
424
- ctx,
425
- table: "pages",
426
- fileSets: { FILES: {
427
- table: "pages",
428
- partitions
429
- } },
430
- ...searchType !== void 0 ? { searchType } : {},
431
- sql: `
432
- SELECT
433
- url,
434
- SUM(clicks)::BIGINT AS clicks,
435
- SUM(impressions)::BIGINT AS impressions,
436
- SUM(sum_position)::DOUBLE AS sum_position
437
- FROM read_parquet({{FILES}}, union_by_name = true)
438
- WHERE date >= '${cutoff}'
439
- GROUP BY url
440
- ORDER BY clicks DESC
441
- LIMIT 1000
442
- `
443
- })).rows.map((r) => ({
444
- url: r.url,
445
- clicks: Number(r.clicks),
446
- impressions: Number(r.impressions),
447
- sum_position: Number(r.sum_position)
448
- }));
449
- }
450
- };
451
- const topCountries28dRollup = {
452
- id: "top_countries_28d",
453
- windowDays: 28,
454
- async build({ engine, ctx, windowAnchorMs, searchType }) {
455
- const cutoff = utcDateMinusDays(windowAnchorMs, 28);
456
- const partitions = partitionsInRange(await engine.listPartitions({
457
- ctx,
458
- table: "countries",
459
- ...searchType !== void 0 ? { searchType } : {}
460
- }), cutoff, utcDateMinusDays(windowAnchorMs, 0));
461
- if (partitions.length === 0) return [];
462
- return (await engine.runSQL({
463
- ctx,
464
- table: "countries",
465
- fileSets: { FILES: {
466
- table: "countries",
467
- partitions
468
- } },
469
- ...searchType !== void 0 ? { searchType } : {},
470
- sql: `
471
- SELECT
472
- country,
473
- SUM(clicks)::BIGINT AS clicks,
474
- SUM(impressions)::BIGINT AS impressions,
475
- SUM(sum_position)::DOUBLE AS sum_position
476
- FROM read_parquet({{FILES}}, union_by_name = true)
477
- WHERE date >= '${cutoff}'
478
- GROUP BY country
479
- ORDER BY clicks DESC
480
- LIMIT 250
481
- `
482
- })).rows.map((r) => ({
483
- country: r.country,
484
- clicks: Number(r.clicks),
485
- impressions: Number(r.impressions),
486
- sum_position: Number(r.sum_position)
487
- }));
488
- }
489
- };
490
- const topKeywords28dRollup = {
491
- id: "top_keywords_28d",
492
- windowDays: 28,
493
- async build({ engine, ctx, windowAnchorMs, searchType }) {
494
- const cutoff = utcDateMinusDays(windowAnchorMs, 28);
495
- const partitions = partitionsInRange(await engine.listPartitions({
496
- ctx,
497
- table: "queries",
498
- ...searchType !== void 0 ? { searchType } : {}
499
- }), cutoff, utcDateMinusDays(windowAnchorMs, 0));
500
- if (partitions.length === 0) return [];
501
- return (await engine.runSQL({
502
- ctx,
503
- table: "queries",
504
- fileSets: { FILES: {
505
- table: "queries",
506
- partitions
507
- } },
508
- ...searchType !== void 0 ? { searchType } : {},
509
- sql: `
510
- SELECT
511
- query,
512
- SUM(clicks)::BIGINT AS clicks,
513
- SUM(impressions)::BIGINT AS impressions,
514
- SUM(sum_position)::DOUBLE AS sum_position
515
- FROM read_parquet({{FILES}}, union_by_name = true)
516
- WHERE date >= '${cutoff}'
517
- GROUP BY query
518
- ORDER BY clicks DESC
519
- LIMIT 1000
520
- `
521
- })).rows.map((r) => ({
522
- query: r.query,
523
- clicks: Number(r.clicks),
524
- impressions: Number(r.impressions),
525
- sum_position: Number(r.sum_position)
526
- }));
527
- }
528
- };
529
- const topKeywords28dParquetRollup = {
530
- id: "top_keywords_28d_parquet",
531
- windowDays: 28,
532
- format: "parquet",
533
- parquetColumns: [
534
- {
535
- name: "query",
536
- type: "VARCHAR",
537
- nullable: false
538
- },
539
- {
540
- name: "clicks",
541
- type: "BIGINT",
542
- nullable: false
543
- },
544
- {
545
- name: "impressions",
546
- type: "BIGINT",
547
- nullable: false
548
- },
549
- {
550
- name: "sum_position",
551
- type: "DOUBLE",
552
- nullable: false
553
- }
554
- ],
555
- parquetSortKey: ["clicks"],
556
- async build({ engine, ctx, windowAnchorMs, searchType }) {
557
- const cutoff = utcDateMinusDays(windowAnchorMs, 28);
558
- const partitions = partitionsInRange(await engine.listPartitions({
559
- ctx,
560
- table: "queries",
561
- ...searchType !== void 0 ? { searchType } : {}
562
- }), cutoff, utcDateMinusDays(windowAnchorMs, 0));
563
- if (partitions.length === 0) return [];
564
- return (await engine.runSQL({
565
- ctx,
566
- table: "queries",
567
- fileSets: { FILES: {
568
- table: "queries",
569
- partitions
570
- } },
571
- ...searchType !== void 0 ? { searchType } : {},
572
- sql: `
573
- SELECT
574
- query,
575
- SUM(clicks)::BIGINT AS clicks,
576
- SUM(impressions)::BIGINT AS impressions,
577
- SUM(sum_position)::DOUBLE AS sum_position
578
- FROM read_parquet({{FILES}}, union_by_name = true)
579
- WHERE date >= '${cutoff}'
580
- GROUP BY query
581
- ORDER BY clicks DESC
582
- LIMIT 1000
583
- `
584
- })).rows.map((r) => ({
585
- query: String(r.query),
586
- clicks: BigInt(r.clicks),
587
- impressions: BigInt(r.impressions),
588
- sum_position: Number(r.sum_position)
589
- }));
590
- }
591
- };
592
- const CANONICAL_VARIANT_LIMIT = 10;
593
- function retainCanonicalVariant(bucket, query, clicks, impressions, sumPos) {
594
- bucket.count++;
595
- let insertAt = 0;
596
- while (insertAt < bucket.top.length) {
597
- const existing = bucket.top[insertAt];
598
- if (clicks > existing.clicks || clicks === existing.clicks && query.localeCompare(existing.query) < 0) break;
599
- insertAt++;
600
- }
601
- if (insertAt >= CANONICAL_VARIANT_LIMIT) return;
602
- bucket.top.splice(insertAt, 0, {
603
- query,
604
- clicks,
605
- impressions,
606
- sumPos
607
- });
608
- if (bucket.top.length > CANONICAL_VARIANT_LIMIT) bucket.top.pop();
609
- }
610
- const queryCanonicalVariantsRollup = {
611
- id: "query_canonical_variants",
612
- windowDays: null,
613
- format: "parquet",
614
- parquetColumns: [
615
- {
616
- name: "joinKey",
617
- type: "VARCHAR",
618
- nullable: false
619
- },
620
- {
621
- name: "variantCount",
622
- type: "BIGINT",
623
- nullable: false
624
- },
625
- {
626
- name: "canonicalName",
627
- type: "VARCHAR",
628
- nullable: true
629
- },
630
- {
631
- name: "variants",
632
- type: "VARCHAR",
633
- nullable: true
634
- }
635
- ],
636
- parquetSortKey: ["joinKey"],
637
- async build({ engine, ctx, dataSource, searchType }) {
638
- const parts = await engine.listPartitions({
639
- ctx,
640
- table: "queries",
641
- ...searchType !== void 0 ? { searchType } : {}
642
- });
643
- if (parts.length === 0) return [];
644
- const partitions = parts.map((p) => p.partition);
645
- const byCanonical = /* @__PURE__ */ new Map();
646
- const dimStore = createQueryDimStore({ dataSource });
647
- const useDim = await dimStore.loadMeta(ctx) !== null;
648
- const canonExpr = useDim ? "COALESCE(qd.query_canonical, q.query)" : "q.query";
649
- let cursor = null;
650
- for (;;) {
651
- const after = cursor === null ? "" : `AND q.query > '${cursor.replace(/'/g, "''")}'`;
652
- const fileSets = { FILES: {
653
- table: "queries",
654
- partitions
655
- } };
656
- if (useDim) fileSets.QUERY_DIM = {
657
- table: "queries",
658
- keys: [dimStore.parquetKey(ctx)]
659
- };
660
- const { rows } = await engine.runSQL({
661
- ctx,
662
- table: "queries",
663
- ...searchType !== void 0 ? { searchType } : {},
664
- fileSets,
665
- sql: `
666
- SELECT
667
- ${canonExpr} AS joinKey,
668
- q.query AS query,
669
- SUM(q.clicks) AS clicks,
670
- SUM(q.impressions) AS impressions,
671
- SUM(q.sum_position) AS sum_pos
672
- FROM read_parquet({{FILES}}, union_by_name = true) q
673
- ${useDim ? "LEFT JOIN read_parquet({{QUERY_DIM}}, union_by_name = true) qd ON q.query = qd.query" : ""}
674
- WHERE q.query IS NOT NULL ${after}
675
- GROUP BY ${canonExpr}, q.query
676
- ORDER BY q.query
677
- LIMIT ${ROLLUP_PAGE_ROWS_WIDE}
678
- `
679
- });
680
- for (const r of rows) {
681
- const joinKey = String(r.joinKey);
682
- let bucket = byCanonical.get(joinKey);
683
- if (!bucket) {
684
- bucket = {
685
- count: 0,
686
- top: []
687
- };
688
- byCanonical.set(joinKey, bucket);
689
- }
690
- retainCanonicalVariant(bucket, String(r.query), Number(r.clicks), Number(r.impressions), Number(r.sum_pos));
691
- }
692
- if (rows.length < 2e4) break;
693
- cursor = String(rows[rows.length - 1].query);
694
- }
695
- const out = [];
696
- for (const [joinKey, bucket] of byCanonical) {
697
- const canonicalName = bucket.top[0]?.query ?? null;
698
- const top = bucket.top.filter((v) => v.impressions > 0);
699
- const variantsStr = top.length === 0 ? null : top.map((v) => `${v.query}:::${v.clicks}:::${v.impressions}:::${(v.sumPos / v.impressions + 1).toFixed(1)}`).join("||");
700
- out.push({
701
- joinKey,
702
- variantCount: BigInt(bucket.count),
703
- canonicalName,
704
- variants: variantsStr
705
- });
706
- }
707
- return out;
708
- }
709
- };
710
- const CANONICAL_DAILY_ROLLUP_FINAL_ID = "query_canonical_daily";
711
- const CANONICAL_DAILY_PART_STEM = "query_canonical_daily__part";
712
- const queryCanonicalDailyRollup = {
713
- id: "query_canonical_daily",
714
- windowDays: null,
715
- format: "parquet",
716
- parquetColumns: [
717
- {
718
- name: "query_canonical",
719
- type: "VARCHAR",
720
- nullable: false
721
- },
722
- {
723
- name: "date",
724
- type: "DATE",
725
- nullable: false
726
- },
727
- {
728
- name: "clicks",
729
- type: "BIGINT",
730
- nullable: false
731
- },
732
- {
733
- name: "impressions",
734
- type: "BIGINT",
735
- nullable: false
736
- },
737
- {
738
- name: "sum_position",
739
- type: "DOUBLE",
740
- nullable: false
741
- }
742
- ],
743
- parquetSortKey: ["date", "query_canonical"],
744
- async build({ engine, ctx, dataSource, searchType }) {
745
- const dimStore = createQueryDimStore({ dataSource });
746
- const useDim = await dimStore.loadMeta(ctx) !== null;
747
- const canonExpr = useDim ? `COALESCE(qd.query_canonical, q.query)` : `query`;
748
- return (await runWindowed({
749
- engine,
750
- ctx,
751
- table: "queries",
752
- ...searchType !== void 0 ? { searchType } : {},
753
- ...useDim ? { extraFileSets: { QUERY_DIM: {
754
- table: "queries",
755
- keys: [dimStore.parquetKey(ctx)]
756
- } } } : {},
757
- paginate: {
758
- orderBy: "date, query_canonical",
759
- pageRows: ROLLUP_PAGE_ROWS_DAILY
760
- },
761
- maxWindowDays: DAILY_MAX_WINDOW_DAYS,
762
- sqlFor: dailyWindowSqlFor(useDim, canonExpr)
763
- })).map(mapDailyRow);
764
- }
765
- };
766
- function canonicalDailyShardPredicate(queryExpr, shardIndex, shardCount) {
767
- return shardCount <= 1 ? "" : ` AND (hash(${queryExpr}) % ${shardCount}) = ${shardIndex}`;
768
- }
769
- function dailyWindowSqlFor(useDim, canonExpr) {
770
- return useDim ? (w, shard) => `
771
- SELECT
772
- ${canonExpr} AS query_canonical,
773
- CAST(q.date AS VARCHAR) AS date,
774
- SUM(q.clicks)::BIGINT AS clicks,
775
- SUM(q.impressions)::BIGINT AS impressions,
776
- SUM(q.sum_position)::DOUBLE AS sum_position
777
- FROM read_parquet({{FILES}}, union_by_name = true) q
778
- LEFT JOIN read_parquet({{QUERY_DIM}}, union_by_name = true) qd ON q.query = qd.query
779
- WHERE q.date >= '${w.start}' AND q.date <= '${w.end}'${canonicalDailyShardPredicate("q.query", shard?.index ?? 0, shard?.count ?? 1)}
780
- GROUP BY ${canonExpr}, q.date
781
- ` : (w, shard) => `
782
- SELECT
783
- ${canonExpr} AS query_canonical,
784
- CAST(q.date AS VARCHAR) AS date,
785
- SUM(q.clicks)::BIGINT AS clicks,
786
- SUM(q.impressions)::BIGINT AS impressions,
787
- SUM(q.sum_position)::DOUBLE AS sum_position
788
- FROM read_parquet({{FILES}}, union_by_name = true) q
789
- WHERE q.date >= '${w.start}' AND q.date <= '${w.end}'${canonicalDailyShardPredicate("q.query", shard?.index ?? 0, shard?.count ?? 1)}
790
- GROUP BY ${canonExpr}, q.date
791
- `;
792
- }
793
- function mapDailyRow(r) {
794
- return {
795
- query_canonical: String(r.query_canonical),
796
- date: String(r.date),
797
- clicks: BigInt(r.clicks),
798
- impressions: BigInt(r.impressions),
799
- sum_position: Number(r.sum_position)
800
- };
801
- }
802
- async function rebuildCanonicalDailyResumable(opts) {
803
- const { engine, ctx, dataSource, searchType, builtAt, windowOffset, deadlineMs } = opts;
804
- const sType = searchType !== void 0 ? { searchType } : {};
805
- const startPageOffset = opts.pageOffset ?? 0;
806
- const pageRows = opts.pageRows ?? 7e4;
807
- const maxWindowDays = opts.maxWindowDays ?? DAILY_MAX_WINDOW_DAYS;
808
- const shardCount = Math.max(1, Math.floor(opts.shardCount ?? 1));
809
- const windows = planRollupWindows((await engine.listPartitions({
810
- ctx,
811
- table: "queries",
812
- ...sType
813
- })).map((p) => ({
814
- partition: p.partition,
815
- bytes: p.bytes
816
- })), void 0, maxWindowDays);
817
- const windowsTotal = windows.length;
818
- const dimStore = createQueryDimStore({ dataSource });
819
- const useDim = await dimStore.loadMeta(ctx) !== null;
820
- const canonExpr = useDim ? `COALESCE(qd.query_canonical, q.query)` : `query`;
821
- const extraFileSets = useDim ? { QUERY_DIM: {
822
- table: "queries",
823
- keys: [dimStore.parquetKey(ctx)]
824
- } } : void 0;
825
- const sqlFor = dailyWindowSqlFor(useDim, canonExpr);
826
- const cols = queryCanonicalDailyRollup.parquetColumns;
827
- const sortKey = queryCanonicalDailyRollup.parquetSortKey;
828
- let i = windowOffset;
829
- let page = startPageOffset;
830
- let nextWindowOffset = windowsTotal;
831
- let nextPageOffset = 0;
832
- let rowsWritten = 0;
833
- let pausedMidWindow = false;
834
- const flushPage = async (windowIdx, pageOffset, rows) => {
835
- if (rows.length === 0) return;
836
- const key = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w${windowIdx}_p${pageOffset}`, builtAt, searchType);
837
- await dataSource.write(key, encodeRowsToParquetFlex(rows, {
838
- columns: cols,
839
- sortKey
840
- }));
841
- rowsWritten += rows.length;
842
- };
843
- for (; i < windowsTotal; i++) {
844
- const w = windows[i];
845
- for (; page < shardCount;) {
846
- const coreSql = sqlFor(w, {
847
- index: page,
848
- count: shardCount
849
- });
850
- const result = await engine.runSQL({
851
- ctx,
852
- table: "queries",
853
- ...sType,
854
- fileSets: {
855
- FILES: {
856
- table: "queries",
857
- partitions: w.partitions
858
- },
859
- ...extraFileSets
860
- },
861
- sql: `${coreSql}\nORDER BY date, query_canonical\nLIMIT ${pageRows}`
862
- });
863
- if (result.rows.length >= pageRows) throw new Error(`query_canonical_daily shard overflow: window=${i} shard=${page}/${shardCount} returned >= ${pageRows} rows; increase shardCount or pageRows`);
864
- await flushPage(i, page, result.rows.map(mapDailyRow));
865
- page += 1;
866
- if (Date.now() > deadlineMs) {
867
- nextWindowOffset = i;
868
- nextPageOffset = page;
869
- pausedMidWindow = true;
870
- break;
871
- }
872
- }
873
- if (pausedMidWindow) break;
874
- page = 0;
875
- if (Date.now() > deadlineMs) {
876
- nextWindowOffset = i + 1;
877
- nextPageOffset = 0;
878
- break;
879
- }
880
- }
881
- if (nextWindowOffset < windowsTotal || nextPageOffset > 0) return {
882
- done: false,
883
- nextWindowOffset,
884
- nextPageOffset,
885
- windowsTotal,
886
- windowsBuilt: nextWindowOffset - windowOffset,
887
- rowsWritten
888
- };
889
- const partPrefixDir = rollupParquetKey(ctx, CANONICAL_DAILY_PART_STEM, builtAt, searchType).replace(/[^/]*$/, "");
890
- const partKeys = (await dataSource.list(partPrefixDir)).filter((k) => k.includes(`${CANONICAL_DAILY_PART_STEM}__w`) && k.endsWith(`__v${builtAt}.parquet`)).sort();
891
- if (partKeys.length === 0) {
892
- const emptyKey = rollupParquetKey(ctx, `${CANONICAL_DAILY_PART_STEM}__w0_p0`, builtAt, searchType);
893
- await dataSource.write(emptyKey, encodeRowsToParquetFlex([], {
894
- columns: cols,
895
- sortKey
896
- }));
897
- partKeys.push(emptyKey);
898
- }
899
- const envelope = {
900
- version: 1,
901
- id: CANONICAL_DAILY_ROLLUP_FINAL_ID,
902
- builtAt,
903
- windowDays: queryCanonicalDailyRollup.windowDays,
904
- payload: {
905
- parquetKey: partKeys[0],
906
- parquetKeys: partKeys,
907
- rowCount: 0
908
- }
909
- };
910
- await dataSource.write(rollupKey(ctx, CANONICAL_DAILY_ROLLUP_FINAL_ID, builtAt, searchType), encodeJsonBigintSafe(envelope));
911
- return {
912
- done: true,
913
- nextWindowOffset,
914
- nextPageOffset,
915
- windowsTotal,
916
- windowsBuilt: nextWindowOffset - windowOffset,
917
- rowsWritten
918
- };
919
- }
920
- const indexingMetadataRollup = {
921
- id: "indexing_metadata",
922
- windowDays: null,
923
- async build({ dataSource, ctx }) {
924
- const index = await createIndexingMetadataStore({ dataSource }).loadIndex(ctx);
925
- const records = Object.values(index.records);
926
- const updatesByDay = /* @__PURE__ */ new Map();
927
- const removesByDay = /* @__PURE__ */ new Map();
928
- let totalUpdates = 0;
929
- let totalRemoves = 0;
930
- let latestUpdate;
931
- let latestRemove;
932
- for (const r of records) {
933
- if (r.latestUpdateAt) {
934
- totalUpdates++;
935
- const day = r.latestUpdateAt.slice(0, 10);
936
- updatesByDay.set(day, (updatesByDay.get(day) ?? 0) + 1);
937
- if (!latestUpdate || r.latestUpdateAt > latestUpdate) latestUpdate = r.latestUpdateAt;
938
- }
939
- if (r.latestRemoveAt) {
940
- totalRemoves++;
941
- const day = r.latestRemoveAt.slice(0, 10);
942
- removesByDay.set(day, (removesByDay.get(day) ?? 0) + 1);
943
- if (!latestRemove || r.latestRemoveAt > latestRemove) latestRemove = r.latestRemoveAt;
944
- }
945
- }
946
- const days = /* @__PURE__ */ new Set([...updatesByDay.keys(), ...removesByDay.keys()]);
947
- const perDay = Array.from(days).sort().map((day) => ({
948
- day,
949
- updates: updatesByDay.get(day) ?? 0,
950
- removes: removesByDay.get(day) ?? 0
951
- }));
952
- return {
953
- totals: {
954
- urls: records.length,
955
- updates: totalUpdates,
956
- removes: totalRemoves,
957
- latestUpdateAt: latestUpdate ?? null,
958
- latestRemoveAt: latestRemove ?? null
959
- },
960
- days: perDay
961
- };
962
- }
963
- };
964
- const indexingHealthRollup = {
965
- id: "indexing_health",
966
- windowDays: 90,
967
- sliceOrthogonal: true,
968
- async build({ engine, ctx, dataSource, windowAnchorMs }) {
969
- const key = inspectionParquetKey(ctx);
970
- if (!await dataSource.head?.(key)) return { days: [] };
971
- const sql = `
972
- SELECT
973
- substr(CAST(inspectedAt AS VARCHAR), 1, 10) AS date,
974
- COUNT(*)::BIGINT AS total_urls,
975
- SUM(CASE WHEN CAST(indexStatus AS VARCHAR) = 'PASS' THEN 1 ELSE 0 END)::BIGINT AS indexed_count,
976
- SUM(CASE WHEN CAST(pageFetchState AS VARCHAR) = 'SOFT_404' THEN 1 ELSE 0 END)::BIGINT AS soft_404,
977
- SUM(CASE WHEN CAST(pageFetchState AS VARCHAR) = 'REDIRECT_ERROR' THEN 1 ELSE 0 END)::BIGINT AS redirect,
978
- SUM(CASE WHEN CAST(pageFetchState AS VARCHAR) = 'NOT_FOUND' THEN 1 ELSE 0 END)::BIGINT AS not_found,
979
- SUM(CASE WHEN CAST(mobileUsabilityVerdict AS VARCHAR) = 'PASS' THEN 1 ELSE 0 END)::BIGINT AS mobile_passes,
980
- SUM(CASE WHEN CAST(richResultsVerdict AS VARCHAR) = 'PASS' THEN 1 ELSE 0 END)::BIGINT AS rich_results_passes,
981
- SUM(CASE WHEN canonicalMismatchKind IN ('path', 'cross_domain') THEN 1 ELSE 0 END)::BIGINT AS canonical_mismatches
982
- FROM read_parquet({{INSPECTIONS}}, union_by_name = true)
983
- WHERE substr(CAST(inspectedAt AS VARCHAR), 1, 10) >= '${utcDateMinusDays(windowAnchorMs, 90)}'
984
- GROUP BY 1
985
- ORDER BY 1
986
- `;
987
- return { days: (await engine.runSQL({
988
- ctx,
989
- table: "pages",
990
- fileSets: { INSPECTIONS: {
991
- table: "pages",
992
- keys: [key]
993
- } },
994
- sql
995
- })).rows.map((r) => ({
996
- date: String(r.date),
997
- total_urls: Number(r.total_urls),
998
- indexed_count: Number(r.indexed_count),
999
- soft_404: Number(r.soft_404),
1000
- redirect: Number(r.redirect),
1001
- not_found: Number(r.not_found),
1002
- mobile_passes: Number(r.mobile_passes),
1003
- rich_results_passes: Number(r.rich_results_passes),
1004
- canonical_mismatches: Number(r.canonical_mismatches)
1005
- })) };
1006
- }
1007
- };
1008
- function currentSitemapProjectionRelation(hasIndexes, hasDeltas) {
1009
- const sources = [];
1010
- if (hasIndexes) sources.push(`
1011
- SELECT
1012
- feedpath_hash,
1013
- url_hash,
1014
- loc,
1015
- removed_at,
1016
- greatest(
1017
- coalesce(removed_at, 0),
1018
- coalesce(last_seen_at, 0),
1019
- coalesce(first_seen_at, 0)
1020
- )::BIGINT AS observed_at,
1021
- 0::INTEGER AS source_order
1022
- FROM read_parquet({{URLS_INDEX}}, union_by_name = true)
1023
- `);
1024
- if (hasDeltas) sources.push(`
1025
- SELECT
1026
- feedpath_hash,
1027
- url_hash,
1028
- loc,
1029
- CASE WHEN op = 'removed' THEN "at" ELSE NULL END AS removed_at,
1030
- "at"::BIGINT AS observed_at,
1031
- 1::INTEGER AS source_order
1032
- FROM read_parquet({{URLS_DELTA}}, union_by_name = true)
1033
- WHERE op IN ('added', 'removed')
1034
- `);
1035
- return `(
1036
- WITH membership_events AS (
1037
- ${sources.join("\nUNION ALL\n")}
1038
- )
1039
- SELECT feedpath_hash, url_hash, loc, removed_at
1040
- FROM membership_events
1041
- QUALIFY row_number() OVER (
1042
- PARTITION BY feedpath_hash, url_hash
1043
- ORDER BY observed_at DESC, source_order DESC
1044
- ) = 1
1045
- )`;
1046
- }
1047
- const indexPercentRollup = {
1048
- id: "index_percent",
1049
- windowDays: 90,
1050
- sliceOrthogonal: true,
1051
- async build({ engine, ctx, dataSource, windowAnchorMs, searchType }) {
1052
- const [listedIndexKeys, listedDeltaKeys, manifestBytes] = await Promise.all([
1053
- dataSource.list(sitemapUrlsIndexPrefix(ctx)),
1054
- dataSource.list(`${sitemapUrlsPrefix(ctx)}/deltas/`),
1055
- readOptional(dataSource, sitemapUrlsProjectionManifestKey(ctx))
1056
- ]);
1057
- const projection = selectSitemapProjectionFiles(listedIndexKeys, listedDeltaKeys, manifestBytes ? decodeSitemapProjectionManifest(new TextDecoder().decode(manifestBytes)) : void 0);
1058
- if (projection.indexKeys.length === 0 && projection.deltaKeys.length === 0) return {
1059
- totalSitemapUrls: 0,
1060
- days: []
1061
- };
1062
- const sitemapFileSets = {
1063
- ...projection.indexKeys.length > 0 ? { URLS_INDEX: {
1064
- table: "pages",
1065
- keys: projection.indexKeys
1066
- } } : {},
1067
- ...projection.deltaKeys.length > 0 ? { URLS_DELTA: {
1068
- table: "pages",
1069
- keys: projection.deltaKeys
1070
- } } : {}
1071
- };
1072
- const currentMembership = currentSitemapProjectionRelation(projection.indexKeys.length > 0, projection.deltaKeys.length > 0);
1073
- const cutoff = utcDateMinusDays(windowAnchorMs, 90);
1074
- const factSearchType = searchType ?? "web";
1075
- const pagesPartitions = partitionsInRange(await engine.listPartitions({
1076
- ctx,
1077
- table: "pages",
1078
- searchType: factSearchType
1079
- }), cutoff, utcDateMinusDays(windowAnchorMs, 0));
1080
- const numerator = await engine.runSQL({
1081
- ctx,
1082
- table: "pages",
1083
- fileSets: {
1084
- PAGES: {
1085
- table: "pages",
1086
- partitions: pagesPartitions
1087
- },
1088
- ...sitemapFileSets
1089
- },
1090
- searchType: factSearchType,
1091
- sql: `
1092
- SELECT
1093
- p.date AS date,
1094
- COUNT(DISTINCT p.url)::BIGINT AS clicked_urls
1095
- FROM read_parquet({{PAGES}}, union_by_name = true) p
1096
- INNER JOIN ${currentMembership} s
1097
- ON s.loc = p.url AND s.removed_at IS NULL
1098
- WHERE p.clicks > 0 AND p.date >= '${cutoff}'
1099
- GROUP BY p.date
1100
- ORDER BY p.date
1101
- `
1102
- });
1103
- const denom = await engine.runSQL({
1104
- ctx,
1105
- table: "pages",
1106
- fileSets: sitemapFileSets,
1107
- sql: `
1108
- SELECT COUNT(DISTINCT loc)::BIGINT AS total
1109
- FROM ${currentMembership}
1110
- WHERE removed_at IS NULL
1111
- `
1112
- });
1113
- const total = Number(denom.rows[0]?.total ?? 0);
1114
- return {
1115
- totalSitemapUrls: total,
1116
- days: numerator.rows.map((r) => {
1117
- const clicked = Number(r.clicked_urls);
1118
- return {
1119
- date: String(r.date),
1120
- clicked_urls: clicked,
1121
- total_sitemap_urls: total,
1122
- ratio: total === 0 ? 0 : clicked / total
1123
- };
1124
- })
1125
- };
1126
- }
1127
- };
1128
- const sitemapHealthRollup = {
1129
- id: "sitemap_health",
1130
- windowDays: 90,
1131
- sliceOrthogonal: true,
1132
- async build({ dataSource, ctx, windowAnchorMs }) {
1133
- const index = await createSitemapReadStore({ dataSource }).loadIndex(ctx);
1134
- const records = Object.values(index.records);
1135
- const cutoff = utcDateMinusDays(windowAnchorMs, 90);
1136
- const byDay = /* @__PURE__ */ new Map();
1137
- const feeds = [];
1138
- for (const r of records) {
1139
- const day = (r.capturedAt ?? r.lastDownloaded ?? "").slice(0, 10);
1140
- if (!day || day < cutoff) continue;
1141
- const errors = Number(r.errors ?? 0);
1142
- const warnings = Number(r.warnings ?? 0);
1143
- const urlCount = Number(r.urlCount ?? 0);
1144
- const bucket = byDay.get(day) ?? {
1145
- day,
1146
- feeds: 0,
1147
- total_urls: 0,
1148
- errors: 0,
1149
- warnings: 0
1150
- };
1151
- bucket.feeds += 1;
1152
- bucket.total_urls += urlCount;
1153
- bucket.errors += errors;
1154
- bucket.warnings += warnings;
1155
- byDay.set(day, bucket);
1156
- feeds.push({
1157
- path: r.path,
1158
- urlCount,
1159
- errors,
1160
- warnings,
1161
- contentHash: r.contentHash ?? null,
1162
- lastDownloaded: r.lastDownloaded ?? null,
1163
- capturedAt: r.capturedAt
1164
- });
1165
- }
1166
- return {
1167
- days: Array.from(byDay.values()).sort((a, b) => a.day < b.day ? -1 : 1),
1168
- feeds
1169
- };
1170
- }
1171
- };
1172
- const RECENT_SITEMAP_CHANGE_LIMIT = 200;
1173
- function isLessRecent(a, b) {
1174
- return a.at < b.at || a.at === b.at && a.sequence > b.sequence;
1175
- }
1176
- function retainRecentSitemapChange(heap, change) {
1177
- if (heap.length < RECENT_SITEMAP_CHANGE_LIMIT) {
1178
- heap.push(change);
1179
- let index = heap.length - 1;
1180
- while (index > 0) {
1181
- const parent = index - 1 >> 1;
1182
- if (!isLessRecent(heap[index], heap[parent])) break;
1183
- const parentValue = heap[parent];
1184
- heap[parent] = heap[index];
1185
- heap[index] = parentValue;
1186
- index = parent;
1187
- }
1188
- return;
1189
- }
1190
- if (!isLessRecent(heap[0], change)) return;
1191
- heap[0] = change;
1192
- let index = 0;
1193
- for (;;) {
1194
- const left = index * 2 + 1;
1195
- const right = left + 1;
1196
- let leastRecent = index;
1197
- if (left < heap.length && isLessRecent(heap[left], heap[leastRecent])) leastRecent = left;
1198
- if (right < heap.length && isLessRecent(heap[right], heap[leastRecent])) leastRecent = right;
1199
- if (leastRecent === index) return;
1200
- const childValue = heap[leastRecent];
1201
- heap[leastRecent] = heap[index];
1202
- heap[index] = childValue;
1203
- index = leastRecent;
1204
- }
1205
- }
1206
- const sitemapChanges28dRollup = {
1207
- id: "sitemap_changes_28d",
1208
- windowDays: 28,
1209
- sliceOrthogonal: true,
1210
- async build({ dataSource, ctx, windowAnchorMs }) {
1211
- const store = createSitemapReadStore({ dataSource });
1212
- const from = utcDateMinusDays(windowAnchorMs, 28);
1213
- const to = utcDateMinusDays(windowAnchorMs, 0);
1214
- const counts = /* @__PURE__ */ new Map();
1215
- const addedTop = [];
1216
- const removedTop = [];
1217
- let sequence = 0;
1218
- function key(k) {
1219
- return `${k.day}\x00${k.feedpath}`;
1220
- }
1221
- for await (const d of store.loadEvents(ctx, {
1222
- from,
1223
- to
1224
- })) {
1225
- const day = new Date(d.observedAt).toISOString().slice(0, 10);
1226
- const k = key({
1227
- day,
1228
- feedpath: d.feedpath
1229
- });
1230
- const cur = counts.get(k) ?? {
1231
- day,
1232
- feedpath: d.feedpath,
1233
- added: 0,
1234
- removed: 0
1235
- };
1236
- const change = {
1237
- loc: d.loc,
1238
- feedpath: d.feedpath,
1239
- at: d.observedAt,
1240
- sequence: sequence++
1241
- };
1242
- if (d.op === "added") {
1243
- cur.added += 1;
1244
- retainRecentSitemapChange(addedTop, change);
1245
- } else {
1246
- cur.removed += 1;
1247
- retainRecentSitemapChange(removedTop, change);
1248
- }
1249
- counts.set(k, cur);
1250
- }
1251
- const days = Array.from(counts.values()).sort((a, b) => {
1252
- if (a.day !== b.day) return a.day < b.day ? -1 : 1;
1253
- return a.feedpath < b.feedpath ? -1 : 1;
1254
- });
1255
- const toRecentList = (heap) => heap.sort((a, b) => b.at - a.at || a.sequence - b.sequence).map(({ loc, feedpath, at }) => ({
1256
- loc,
1257
- feedpath,
1258
- at
1259
- }));
1260
- return {
1261
- days,
1262
- topAdded: toRecentList(addedTop),
1263
- topRemoved: toRecentList(removedTop)
1264
- };
1265
- }
1266
- };
1267
- async function rebuildDailyFromHourly(opts) {
1268
- const { engine, ctx, date, searchType } = opts;
1269
- const rows = (await engine.runSQL({
1270
- ctx,
1271
- table: "hourly_pages",
1272
- fileSets: { FILES: {
1273
- table: "hourly_pages",
1274
- partitions: [`hourly/${date}`]
1275
- } },
1276
- searchType,
1277
- sql: `
1278
- SELECT
1279
- url,
1280
- DATE '${date}' AS date,
1281
- SUM(clicks)::BIGINT AS clicks,
1282
- SUM(impressions)::BIGINT AS impressions,
1283
- SUM(sum_position)::DOUBLE AS sum_position
1284
- FROM read_parquet({{FILES}}, union_by_name = true)
1285
- WHERE date = '${date}'
1286
- GROUP BY url
1287
- `
1288
- })).rows.map((r) => ({
1289
- url: r.url,
1290
- date,
1291
- clicks: Number(r.clicks),
1292
- impressions: Number(r.impressions),
1293
- sum_position: Number(r.sum_position)
1294
- }));
1295
- await engine.writeDay({
1296
- userId: ctx.userId,
1297
- siteId: ctx.siteId,
1298
- table: "pages",
1299
- date,
1300
- searchType
1301
- }, rows);
1302
- return { rowsWritten: rows.length };
1303
- }
1304
- const DEFAULT_ROLLUPS = [
1305
- dailyTotalsRollup,
1306
- weeklyTotalsRollup,
1307
- topPages28dRollup,
1308
- topKeywords28dRollup,
1309
- topCountries28dRollup,
1310
- indexingMetadataRollup,
1311
- indexingHealthRollup,
1312
- indexPercentRollup,
1313
- sitemapHealthRollup,
1314
- sitemapChanges28dRollup
1315
- ];
1316
- const CANONICAL_ROLLUPS = [queryCanonicalVariantsRollup, queryCanonicalDailyRollup];
1
+ import { readLatestRollup, rebuildRollups, rollupKey, rollupParquetKey } from "./rollups/core.mjs";
2
+ import { ROLLUP_PAGE_ROWS, ROLLUP_PAGE_ROWS_DAILY, ROLLUP_PAGE_ROWS_WIDE, WINDOW_BYTE_BUDGET, planRollupWindows, runWindowed } from "./rollups/windows.mjs";
3
+ import { queryCanonicalDailyRollup, queryCanonicalVariantsRollup, rebuildCanonicalDailyResumable } from "./rollups/canonical.mjs";
4
+ import { indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, sitemapChanges28dRollup, sitemapHealthRollup } from "./rollups/indexing.mjs";
5
+ import { dailyTotalsRollup, topCountries28dRollup, topKeywords28dParquetRollup, topPages28dRollup, weeklyTotalsRollup } from "./rollups/traffic.mjs";
6
+ import { CANONICAL_ROLLUPS, DEFAULT_ROLLUPS } from "./rollups/defaults.mjs";
7
+ import { rebuildDailyFromHourly } from "./rollups/hourly.mjs";
1317
8
  export { CANONICAL_ROLLUPS, DEFAULT_ROLLUPS, ROLLUP_PAGE_ROWS, ROLLUP_PAGE_ROWS_DAILY, ROLLUP_PAGE_ROWS_WIDE, WINDOW_BYTE_BUDGET, dailyTotalsRollup, indexPercentRollup, indexingHealthRollup, indexingMetadataRollup, planRollupWindows, queryCanonicalDailyRollup, queryCanonicalVariantsRollup, readLatestRollup, rebuildCanonicalDailyResumable, rebuildDailyFromHourly, rebuildRollups, rollupKey, rollupParquetKey, runWindowed, sitemapChanges28dRollup, sitemapHealthRollup, topCountries28dRollup, topKeywords28dParquetRollup, topPages28dRollup, weeklyTotalsRollup };