@gscdump/engine 1.4.11 → 1.5.0
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.
- package/dist/entities/empty-types.d.mts +22 -0
- package/dist/entities/empty-types.mjs +58 -0
- package/dist/entities/indexing-metadata.d.mts +26 -0
- package/dist/entities/indexing-metadata.mjs +31 -0
- package/dist/entities/inspection.d.mts +240 -0
- package/dist/entities/inspection.mjs +443 -0
- package/dist/entities/io.mjs +16 -0
- package/dist/{query-dim.d.mts → entities/query-dim.d.mts} +3 -3
- package/dist/{query-dim.mjs → entities/query-dim.mjs} +4 -4
- package/dist/{sitemap-projection.mjs → entities/sitemap-projection.mjs} +1 -1
- package/dist/entities/sitemap-shared.d.mts +199 -0
- package/dist/entities/sitemap-shared.mjs +243 -0
- package/dist/entities/sitemap-write.d.mts +3 -0
- package/dist/entities/sitemap-write.mjs +528 -0
- package/dist/entities/sitemap.d.mts +3 -0
- package/dist/entities/sitemap.mjs +91 -0
- package/dist/entities.d.mts +9 -481
- package/dist/entities.mjs +7 -1380
- package/dist/rollups/canonical.d.mts +71 -0
- package/dist/rollups/canonical.mjs +335 -0
- package/dist/rollups/core.d.mts +201 -0
- package/dist/rollups/core.mjs +116 -0
- package/dist/rollups/dates.mjs +11 -0
- package/dist/rollups/defaults.d.mts +11 -0
- package/dist/rollups/defaults.mjs +17 -0
- package/dist/rollups/hourly.d.mts +38 -0
- package/dist/rollups/hourly.mjs +38 -0
- package/dist/rollups/indexing.d.mts +46 -0
- package/dist/rollups/indexing.mjs +357 -0
- package/dist/rollups/traffic.d.mts +38 -0
- package/dist/rollups/traffic.mjs +289 -0
- package/dist/rollups/windows.d.mts +90 -0
- package/dist/rollups/windows.mjs +176 -0
- package/dist/rollups.d.mts +8 -471
- package/dist/rollups.mjs +7 -1316
- package/package.json +4 -4
- /package/dist/{sitemap-projection.d.mts → entities/sitemap-projection.d.mts} +0 -0
package/dist/rollups.mjs
CHANGED
|
@@ -1,1317 +1,8 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import "./
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
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 };
|