@gscdump/engine 0.6.3 → 0.7.2
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/README.md +1 -1
- package/dist/_chunks/analysis-types.d.mts +47 -0
- package/dist/_chunks/contracts.d.mts +1 -0
- package/dist/_chunks/dispatch.mjs +75 -0
- package/dist/_chunks/registry.d.mts +92 -0
- package/dist/_chunks/resolver.mjs +91 -0
- package/dist/_chunks/source-types.d.mts +31 -0
- package/dist/analysis-types.d.mts +2 -0
- package/dist/analysis-types.mjs +7 -0
- package/dist/analyzer/index.d.mts +59 -0
- package/dist/analyzer/index.mjs +104 -0
- package/dist/contracts.d.mts +1 -1
- package/dist/period/index.d.mts +57 -0
- package/dist/period/index.mjs +150 -0
- package/dist/resolver/index.d.mts +1 -27
- package/dist/resolver/index.mjs +1 -89
- package/dist/scope.d.mts +3 -3
- package/dist/source/index.d.mts +78 -0
- package/dist/source/index.mjs +113 -0
- package/package.json +64 -27
- package/dist/rollups.d.mts +0 -162
- package/dist/rollups.mjs +0 -346
package/dist/rollups.d.mts
DELETED
|
@@ -1,162 +0,0 @@
|
|
|
1
|
-
import { N as TableName$1, a as DataSource, w as Row$1 } from "./_chunks/storage.mjs";
|
|
2
|
-
import { t as ColumnDef } from "./_chunks/schema.mjs";
|
|
3
|
-
import { TenantCtx } from "gscdump/contracts";
|
|
4
|
-
interface RollupCtx extends TenantCtx {
|
|
5
|
-
/** When the rollup was built. Stamped into payload + filename. */
|
|
6
|
-
builtAt: number;
|
|
7
|
-
}
|
|
8
|
-
/**
|
|
9
|
-
* Tenant-scoped engine surface a rollup builder needs. Subset of
|
|
10
|
-
* `StorageEngine.runSQL` so rollups stay testable without a full engine.
|
|
11
|
-
*/
|
|
12
|
-
interface RollupEngine {
|
|
13
|
-
runSQL: (opts: {
|
|
14
|
-
ctx: TenantCtx;
|
|
15
|
-
fileSets: Record<string, {
|
|
16
|
-
table: TableName$1;
|
|
17
|
-
partitions?: string[];
|
|
18
|
-
}>;
|
|
19
|
-
table?: TableName$1;
|
|
20
|
-
sql: string;
|
|
21
|
-
params?: unknown[];
|
|
22
|
-
}) => Promise<{
|
|
23
|
-
rows: Row$1[];
|
|
24
|
-
}>;
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* One rollup definition. Build runs SQL over the tenant's facts and/or reads
|
|
28
|
-
* from entity stores via `dataSource`, returning a JSON-serializable payload
|
|
29
|
-
* that the runner timestamps + writes.
|
|
30
|
-
*/
|
|
31
|
-
interface RollupDef {
|
|
32
|
-
id: string;
|
|
33
|
-
/**
|
|
34
|
-
* Window in days the rollup covers. `null` means full history. Used by
|
|
35
|
-
* the runner to populate `windowDays` in the payload metadata so readers
|
|
36
|
-
* can validate freshness.
|
|
37
|
-
*/
|
|
38
|
-
windowDays: number | null;
|
|
39
|
-
/**
|
|
40
|
-
* Storage format. `'json'` (default) wraps the build payload in a
|
|
41
|
-
* `RollupEnvelope` and writes as a JSON blob. `'parquet'` expects `build`
|
|
42
|
-
* to return rows matching `parquetColumns` and writes a parquet file plus
|
|
43
|
-
* a tiny JSON sidecar envelope that points at it, so metadata
|
|
44
|
-
* (`builtAt` / `windowDays`) stays readable without decoding parquet.
|
|
45
|
-
*/
|
|
46
|
-
format?: 'json' | 'parquet';
|
|
47
|
-
/**
|
|
48
|
-
* Column schema for parquet output. Required when `format === 'parquet'`.
|
|
49
|
-
* Types map the same way as the fact-table encoder: VARCHAR / DATE go
|
|
50
|
-
* through BYTE_ARRAY/UTF8; BIGINT → INT64; INTEGER → INT32; DOUBLE → DOUBLE.
|
|
51
|
-
*/
|
|
52
|
-
parquetColumns?: readonly ColumnDef[];
|
|
53
|
-
/** Sort-key column names for parquet row-group stats. Optional. */
|
|
54
|
-
parquetSortKey?: readonly string[];
|
|
55
|
-
build: (deps: {
|
|
56
|
-
engine: RollupEngine;
|
|
57
|
-
ctx: TenantCtx;
|
|
58
|
-
/**
|
|
59
|
-
* Tenant-scoped object store. Rollups that aggregate over entity
|
|
60
|
-
* snapshots (e.g. indexing metadata) read JSON docs through this.
|
|
61
|
-
* Pure-SQL rollups can ignore it.
|
|
62
|
-
*/
|
|
63
|
-
dataSource: DataSource;
|
|
64
|
-
/**
|
|
65
|
-
* Wall-clock millis when the runner started this rollup. Use for
|
|
66
|
-
* derived window cutoffs (e.g. trailing-28d boundary) so the SQL can
|
|
67
|
-
* inline a date literal and stay portable across DuckDB builds that
|
|
68
|
-
* don't bundle the ICU extension (Workers DuckDB, for one — CURRENT_DATE
|
|
69
|
-
* lives in ICU).
|
|
70
|
-
*/
|
|
71
|
-
builtAt: number;
|
|
72
|
-
}) => Promise<unknown>;
|
|
73
|
-
}
|
|
74
|
-
/**
|
|
75
|
-
* Wire shape persisted to R2/disk. Readers can rely on the `version` + `builtAt`.
|
|
76
|
-
* Parquet rollups write this envelope as a sidecar whose `payload` points at
|
|
77
|
-
* the co-located `.parquet` object via `{ parquetKey, rowCount }`.
|
|
78
|
-
*/
|
|
79
|
-
interface RollupEnvelope<T = unknown> {
|
|
80
|
-
version: 1;
|
|
81
|
-
id: string;
|
|
82
|
-
builtAt: number;
|
|
83
|
-
windowDays: number | null;
|
|
84
|
-
payload: T;
|
|
85
|
-
}
|
|
86
|
-
interface ParquetRollupPointer {
|
|
87
|
-
parquetKey: string;
|
|
88
|
-
rowCount: number;
|
|
89
|
-
}
|
|
90
|
-
declare function rollupKey(ctx: TenantCtx, id: string, builtAt: number): string;
|
|
91
|
-
declare function rollupParquetKey(ctx: TenantCtx, id: string, builtAt: number): string;
|
|
92
|
-
interface RebuildRollupsOptions {
|
|
93
|
-
engine: RollupEngine;
|
|
94
|
-
dataSource: DataSource;
|
|
95
|
-
ctx: TenantCtx;
|
|
96
|
-
defs: readonly RollupDef[];
|
|
97
|
-
now?: () => number;
|
|
98
|
-
}
|
|
99
|
-
interface RebuildRollupResult {
|
|
100
|
-
id: string;
|
|
101
|
-
/** JSON envelope key. For parquet rollups this is the sidecar pointer. */
|
|
102
|
-
objectKey: string;
|
|
103
|
-
/** Parquet payload key. Present only when `format === 'parquet'`. */
|
|
104
|
-
parquetKey?: string;
|
|
105
|
-
/** Envelope byte size; for parquet rollups does NOT include parquet bytes. */
|
|
106
|
-
bytes: number;
|
|
107
|
-
/** Parquet payload byte size when `format === 'parquet'`. */
|
|
108
|
-
parquetBytes?: number;
|
|
109
|
-
builtAt: number;
|
|
110
|
-
}
|
|
111
|
-
declare function rebuildRollups(opts: RebuildRollupsOptions): Promise<RebuildRollupResult[]>;
|
|
112
|
-
/**
|
|
113
|
-
* Daily totals across the full history. One row per (date, table) with
|
|
114
|
-
* clicks + impressions + position. Powers sparklines and headline totals.
|
|
115
|
-
*
|
|
116
|
-
* Includes `anonymizedImpressionsPct` per day computed as
|
|
117
|
-
* 1 - sum(query_grained_impressions) / sum(page_grained_impressions)
|
|
118
|
-
* — surfaces GSC's anonymous-query gap so the dashboard can warn users not
|
|
119
|
-
* to trust query-grained breakdowns as comprehensive.
|
|
120
|
-
*/
|
|
121
|
-
declare const dailyTotalsRollup: RollupDef;
|
|
122
|
-
/** Weekly totals, ISO week aligned. Cheap and stable for trend widgets. */
|
|
123
|
-
declare const weeklyTotalsRollup: RollupDef;
|
|
124
|
-
/**
|
|
125
|
-
* Top 1000 pages by clicks over the trailing 28-day window. JSON for v1;
|
|
126
|
-
* promote to parquet (`top_pages_28d.parquet`) when the dashboard needs
|
|
127
|
-
* server-side WHERE filtering on this rollup.
|
|
128
|
-
*/
|
|
129
|
-
declare const topPages28dRollup: RollupDef;
|
|
130
|
-
/**
|
|
131
|
-
* Top 250 countries by clicks over the trailing 28-day window. Countries
|
|
132
|
-
* cardinality is bounded (~250 ISO codes), so the list fits in a tiny JSON
|
|
133
|
-
* payload regardless of traffic shape. Powers a geo-overview widget without
|
|
134
|
-
* spinning up DuckDB-WASM.
|
|
135
|
-
*/
|
|
136
|
-
declare const topCountries28dRollup: RollupDef;
|
|
137
|
-
/** Top 1000 keywords by clicks over the trailing 28-day window. */
|
|
138
|
-
declare const topKeywords28dRollup: RollupDef;
|
|
139
|
-
/**
|
|
140
|
-
* Parquet-format companion to `topKeywords28dRollup`. Same shape, but persists
|
|
141
|
-
* as a parquet object plus JSON sidecar pointer so widgets that need
|
|
142
|
-
* server-side WHERE (filter by prefix, by clicks threshold, paginate) can scan
|
|
143
|
-
* it directly with DuckDB-WASM instead of loading all 1000 rows into JS.
|
|
144
|
-
*
|
|
145
|
-
* Opt-in: include in the caller's rollup def list alongside (or instead of)
|
|
146
|
-
* the JSON variant; the runner treats the two as independent ids so they can
|
|
147
|
-
* coexist during a migration.
|
|
148
|
-
*/
|
|
149
|
-
declare const topKeywords28dParquetRollup: RollupDef;
|
|
150
|
-
/**
|
|
151
|
-
* Aggregates the per-URL Indexing API metadata entity store (populated by
|
|
152
|
-
* `gscdump entities indexing snapshot`) into daily counts of `URL_UPDATED`
|
|
153
|
-
* and `URL_REMOVED` notifications. Covers the third entity-snapshot shape
|
|
154
|
-
* without needing its own parquet family — publish events are sparse and
|
|
155
|
-
* aggregate cleanly into a small JSON rollup.
|
|
156
|
-
*
|
|
157
|
-
* Safe no-op when the entity store is empty: returns `{ totals: {...}, days: [] }`
|
|
158
|
-
* so downstream readers don't have to special-case first-run sites.
|
|
159
|
-
*/
|
|
160
|
-
declare const indexingMetadataRollup: RollupDef;
|
|
161
|
-
declare const DEFAULT_ROLLUPS: readonly RollupDef[];
|
|
162
|
-
export { DEFAULT_ROLLUPS, ParquetRollupPointer, RebuildRollupResult, RebuildRollupsOptions, RollupCtx, RollupDef, RollupEngine, RollupEnvelope, dailyTotalsRollup, indexingMetadataRollup, rebuildRollups, rollupKey, rollupParquetKey, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
|
package/dist/rollups.mjs
DELETED
|
@@ -1,346 +0,0 @@
|
|
|
1
|
-
import { createIndexingMetadataStore } from "./entities.mjs";
|
|
2
|
-
import { encodeRowsToParquetFlex } from "./adapters/hyparquet.mjs";
|
|
3
|
-
import { MS_PER_DAY } from "gscdump";
|
|
4
|
-
function rollupPrefix(ctx) {
|
|
5
|
-
return ctx.siteId ? `u_${ctx.userId}/${ctx.siteId}/rollups` : `u_${ctx.userId}/rollups`;
|
|
6
|
-
}
|
|
7
|
-
function rollupKey(ctx, id, builtAt) {
|
|
8
|
-
return `${rollupPrefix(ctx)}/${id}__v${builtAt}.json`;
|
|
9
|
-
}
|
|
10
|
-
function rollupParquetKey(ctx, id, builtAt) {
|
|
11
|
-
return `${rollupPrefix(ctx)}/${id}__v${builtAt}.parquet`;
|
|
12
|
-
}
|
|
13
|
-
async function rebuildRollups(opts) {
|
|
14
|
-
const now = opts.now ?? (() => Date.now());
|
|
15
|
-
const results = [];
|
|
16
|
-
for (const def of opts.defs) {
|
|
17
|
-
const builtAt = now();
|
|
18
|
-
const payload = await def.build({
|
|
19
|
-
engine: opts.engine,
|
|
20
|
-
ctx: opts.ctx,
|
|
21
|
-
dataSource: opts.dataSource,
|
|
22
|
-
builtAt
|
|
23
|
-
});
|
|
24
|
-
if (def.format === "parquet") {
|
|
25
|
-
if (!def.parquetColumns || def.parquetColumns.length === 0) throw new Error(`rollup '${def.id}' declared format='parquet' without parquetColumns`);
|
|
26
|
-
const rows = payload ?? [];
|
|
27
|
-
const parquetBytes = encodeRowsToParquetFlex(rows, {
|
|
28
|
-
columns: def.parquetColumns,
|
|
29
|
-
sortKey: def.parquetSortKey
|
|
30
|
-
});
|
|
31
|
-
const parquetKey = rollupParquetKey(opts.ctx, def.id, builtAt);
|
|
32
|
-
await opts.dataSource.write(parquetKey, parquetBytes);
|
|
33
|
-
const pointer = {
|
|
34
|
-
parquetKey,
|
|
35
|
-
rowCount: rows.length
|
|
36
|
-
};
|
|
37
|
-
const envelope = {
|
|
38
|
-
version: 1,
|
|
39
|
-
id: def.id,
|
|
40
|
-
builtAt,
|
|
41
|
-
windowDays: def.windowDays,
|
|
42
|
-
payload: pointer
|
|
43
|
-
};
|
|
44
|
-
const envelopeBytes = new TextEncoder().encode(JSON.stringify(envelope));
|
|
45
|
-
const key = rollupKey(opts.ctx, def.id, builtAt);
|
|
46
|
-
await opts.dataSource.write(key, envelopeBytes);
|
|
47
|
-
results.push({
|
|
48
|
-
id: def.id,
|
|
49
|
-
objectKey: key,
|
|
50
|
-
parquetKey,
|
|
51
|
-
bytes: envelopeBytes.byteLength,
|
|
52
|
-
parquetBytes: parquetBytes.byteLength,
|
|
53
|
-
builtAt
|
|
54
|
-
});
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
const envelope = {
|
|
58
|
-
version: 1,
|
|
59
|
-
id: def.id,
|
|
60
|
-
builtAt,
|
|
61
|
-
windowDays: def.windowDays,
|
|
62
|
-
payload
|
|
63
|
-
};
|
|
64
|
-
const json = JSON.stringify(envelope);
|
|
65
|
-
const bytes = new TextEncoder().encode(json);
|
|
66
|
-
const key = rollupKey(opts.ctx, def.id, builtAt);
|
|
67
|
-
await opts.dataSource.write(key, bytes);
|
|
68
|
-
results.push({
|
|
69
|
-
id: def.id,
|
|
70
|
-
objectKey: key,
|
|
71
|
-
bytes: bytes.byteLength,
|
|
72
|
-
builtAt
|
|
73
|
-
});
|
|
74
|
-
}
|
|
75
|
-
return results;
|
|
76
|
-
}
|
|
77
|
-
function utcDateMinusDays(at, days) {
|
|
78
|
-
const d = new Date(at - days * MS_PER_DAY);
|
|
79
|
-
return `${d.getUTCFullYear()}-${String(d.getUTCMonth() + 1).padStart(2, "0")}-${String(d.getUTCDate()).padStart(2, "0")}`;
|
|
80
|
-
}
|
|
81
|
-
const dailyTotalsRollup = {
|
|
82
|
-
id: "daily_totals",
|
|
83
|
-
windowDays: null,
|
|
84
|
-
async build({ engine, ctx }) {
|
|
85
|
-
const pages = await engine.runSQL({
|
|
86
|
-
ctx,
|
|
87
|
-
table: "pages",
|
|
88
|
-
fileSets: { FILES: { table: "pages" } },
|
|
89
|
-
sql: `
|
|
90
|
-
SELECT
|
|
91
|
-
date,
|
|
92
|
-
SUM(clicks)::BIGINT AS clicks,
|
|
93
|
-
SUM(impressions)::BIGINT AS impressions,
|
|
94
|
-
SUM(sum_position)::DOUBLE AS sum_position
|
|
95
|
-
FROM read_parquet({{FILES}}, union_by_name = true)
|
|
96
|
-
GROUP BY date
|
|
97
|
-
ORDER BY date
|
|
98
|
-
`
|
|
99
|
-
});
|
|
100
|
-
const keywords = await engine.runSQL({
|
|
101
|
-
ctx,
|
|
102
|
-
table: "keywords",
|
|
103
|
-
fileSets: { FILES: { table: "keywords" } },
|
|
104
|
-
sql: `
|
|
105
|
-
SELECT
|
|
106
|
-
date,
|
|
107
|
-
SUM(impressions)::BIGINT AS impressions
|
|
108
|
-
FROM read_parquet({{FILES}}, union_by_name = true)
|
|
109
|
-
GROUP BY date
|
|
110
|
-
`
|
|
111
|
-
});
|
|
112
|
-
const keywordImpressionsByDate = /* @__PURE__ */ new Map();
|
|
113
|
-
for (const r of keywords.rows) keywordImpressionsByDate.set(String(r.date), BigInt(r.impressions));
|
|
114
|
-
return pages.rows.map((r) => {
|
|
115
|
-
const totalImpressions = BigInt(r.impressions);
|
|
116
|
-
const queryImpressions = keywordImpressionsByDate.get(String(r.date)) ?? BigInt(0);
|
|
117
|
-
const anonymized = totalImpressions === BigInt(0) ? 0 : 1 - Number(queryImpressions) / Number(totalImpressions);
|
|
118
|
-
return {
|
|
119
|
-
date: r.date,
|
|
120
|
-
clicks: Number(r.clicks),
|
|
121
|
-
impressions: Number(r.impressions),
|
|
122
|
-
sum_position: Number(r.sum_position),
|
|
123
|
-
anonymizedImpressionsPct: Math.max(0, Math.min(1, anonymized))
|
|
124
|
-
};
|
|
125
|
-
});
|
|
126
|
-
}
|
|
127
|
-
};
|
|
128
|
-
const weeklyTotalsRollup = {
|
|
129
|
-
id: "weekly_totals",
|
|
130
|
-
windowDays: null,
|
|
131
|
-
async build({ engine, ctx }) {
|
|
132
|
-
return (await engine.runSQL({
|
|
133
|
-
ctx,
|
|
134
|
-
table: "pages",
|
|
135
|
-
fileSets: { FILES: { table: "pages" } },
|
|
136
|
-
sql: `
|
|
137
|
-
SELECT
|
|
138
|
-
strftime(date_trunc('week', date::DATE), '%Y-%m-%d') AS week,
|
|
139
|
-
SUM(clicks)::BIGINT AS clicks,
|
|
140
|
-
SUM(impressions)::BIGINT AS impressions,
|
|
141
|
-
SUM(sum_position)::DOUBLE AS sum_position
|
|
142
|
-
FROM read_parquet({{FILES}}, union_by_name = true)
|
|
143
|
-
GROUP BY 1
|
|
144
|
-
ORDER BY 1
|
|
145
|
-
`
|
|
146
|
-
})).rows.map((r) => ({
|
|
147
|
-
week: r.week,
|
|
148
|
-
clicks: Number(r.clicks),
|
|
149
|
-
impressions: Number(r.impressions),
|
|
150
|
-
sum_position: Number(r.sum_position)
|
|
151
|
-
}));
|
|
152
|
-
}
|
|
153
|
-
};
|
|
154
|
-
const topPages28dRollup = {
|
|
155
|
-
id: "top_pages_28d",
|
|
156
|
-
windowDays: 28,
|
|
157
|
-
async build({ engine, ctx, builtAt }) {
|
|
158
|
-
const cutoff = utcDateMinusDays(builtAt, 28);
|
|
159
|
-
return (await engine.runSQL({
|
|
160
|
-
ctx,
|
|
161
|
-
table: "pages",
|
|
162
|
-
fileSets: { FILES: { table: "pages" } },
|
|
163
|
-
sql: `
|
|
164
|
-
SELECT
|
|
165
|
-
url,
|
|
166
|
-
SUM(clicks)::BIGINT AS clicks,
|
|
167
|
-
SUM(impressions)::BIGINT AS impressions,
|
|
168
|
-
SUM(sum_position)::DOUBLE AS sum_position
|
|
169
|
-
FROM read_parquet({{FILES}}, union_by_name = true)
|
|
170
|
-
WHERE date >= '${cutoff}'
|
|
171
|
-
GROUP BY url
|
|
172
|
-
ORDER BY clicks DESC
|
|
173
|
-
LIMIT 1000
|
|
174
|
-
`
|
|
175
|
-
})).rows.map((r) => ({
|
|
176
|
-
url: r.url,
|
|
177
|
-
clicks: Number(r.clicks),
|
|
178
|
-
impressions: Number(r.impressions),
|
|
179
|
-
sum_position: Number(r.sum_position)
|
|
180
|
-
}));
|
|
181
|
-
}
|
|
182
|
-
};
|
|
183
|
-
const topCountries28dRollup = {
|
|
184
|
-
id: "top_countries_28d",
|
|
185
|
-
windowDays: 28,
|
|
186
|
-
async build({ engine, ctx, builtAt }) {
|
|
187
|
-
const cutoff = utcDateMinusDays(builtAt, 28);
|
|
188
|
-
return (await engine.runSQL({
|
|
189
|
-
ctx,
|
|
190
|
-
table: "countries",
|
|
191
|
-
fileSets: { FILES: { table: "countries" } },
|
|
192
|
-
sql: `
|
|
193
|
-
SELECT
|
|
194
|
-
country,
|
|
195
|
-
SUM(clicks)::BIGINT AS clicks,
|
|
196
|
-
SUM(impressions)::BIGINT AS impressions,
|
|
197
|
-
SUM(sum_position)::DOUBLE AS sum_position
|
|
198
|
-
FROM read_parquet({{FILES}}, union_by_name = true)
|
|
199
|
-
WHERE date >= '${cutoff}'
|
|
200
|
-
GROUP BY country
|
|
201
|
-
ORDER BY clicks DESC
|
|
202
|
-
LIMIT 250
|
|
203
|
-
`
|
|
204
|
-
})).rows.map((r) => ({
|
|
205
|
-
country: r.country,
|
|
206
|
-
clicks: Number(r.clicks),
|
|
207
|
-
impressions: Number(r.impressions),
|
|
208
|
-
sum_position: Number(r.sum_position)
|
|
209
|
-
}));
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
const topKeywords28dRollup = {
|
|
213
|
-
id: "top_keywords_28d",
|
|
214
|
-
windowDays: 28,
|
|
215
|
-
async build({ engine, ctx, builtAt }) {
|
|
216
|
-
const cutoff = utcDateMinusDays(builtAt, 28);
|
|
217
|
-
return (await engine.runSQL({
|
|
218
|
-
ctx,
|
|
219
|
-
table: "keywords",
|
|
220
|
-
fileSets: { FILES: { table: "keywords" } },
|
|
221
|
-
sql: `
|
|
222
|
-
SELECT
|
|
223
|
-
query,
|
|
224
|
-
SUM(clicks)::BIGINT AS clicks,
|
|
225
|
-
SUM(impressions)::BIGINT AS impressions,
|
|
226
|
-
SUM(sum_position)::DOUBLE AS sum_position
|
|
227
|
-
FROM read_parquet({{FILES}}, union_by_name = true)
|
|
228
|
-
WHERE date >= '${cutoff}'
|
|
229
|
-
GROUP BY query
|
|
230
|
-
ORDER BY clicks DESC
|
|
231
|
-
LIMIT 1000
|
|
232
|
-
`
|
|
233
|
-
})).rows.map((r) => ({
|
|
234
|
-
query: r.query,
|
|
235
|
-
clicks: Number(r.clicks),
|
|
236
|
-
impressions: Number(r.impressions),
|
|
237
|
-
sum_position: Number(r.sum_position)
|
|
238
|
-
}));
|
|
239
|
-
}
|
|
240
|
-
};
|
|
241
|
-
const topKeywords28dParquetRollup = {
|
|
242
|
-
id: "top_keywords_28d_parquet",
|
|
243
|
-
windowDays: 28,
|
|
244
|
-
format: "parquet",
|
|
245
|
-
parquetColumns: [
|
|
246
|
-
{
|
|
247
|
-
name: "query",
|
|
248
|
-
type: "VARCHAR",
|
|
249
|
-
nullable: false
|
|
250
|
-
},
|
|
251
|
-
{
|
|
252
|
-
name: "clicks",
|
|
253
|
-
type: "BIGINT",
|
|
254
|
-
nullable: false
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
name: "impressions",
|
|
258
|
-
type: "BIGINT",
|
|
259
|
-
nullable: false
|
|
260
|
-
},
|
|
261
|
-
{
|
|
262
|
-
name: "sum_position",
|
|
263
|
-
type: "DOUBLE",
|
|
264
|
-
nullable: false
|
|
265
|
-
}
|
|
266
|
-
],
|
|
267
|
-
parquetSortKey: ["clicks"],
|
|
268
|
-
async build({ engine, ctx, builtAt }) {
|
|
269
|
-
const cutoff = utcDateMinusDays(builtAt, 28);
|
|
270
|
-
return (await engine.runSQL({
|
|
271
|
-
ctx,
|
|
272
|
-
table: "keywords",
|
|
273
|
-
fileSets: { FILES: { table: "keywords" } },
|
|
274
|
-
sql: `
|
|
275
|
-
SELECT
|
|
276
|
-
query,
|
|
277
|
-
SUM(clicks)::BIGINT AS clicks,
|
|
278
|
-
SUM(impressions)::BIGINT AS impressions,
|
|
279
|
-
SUM(sum_position)::DOUBLE AS sum_position
|
|
280
|
-
FROM read_parquet({{FILES}}, union_by_name = true)
|
|
281
|
-
WHERE date >= '${cutoff}'
|
|
282
|
-
GROUP BY query
|
|
283
|
-
ORDER BY clicks DESC
|
|
284
|
-
LIMIT 1000
|
|
285
|
-
`
|
|
286
|
-
})).rows.map((r) => ({
|
|
287
|
-
query: String(r.query),
|
|
288
|
-
clicks: BigInt(r.clicks),
|
|
289
|
-
impressions: BigInt(r.impressions),
|
|
290
|
-
sum_position: Number(r.sum_position)
|
|
291
|
-
}));
|
|
292
|
-
}
|
|
293
|
-
};
|
|
294
|
-
const indexingMetadataRollup = {
|
|
295
|
-
id: "indexing_metadata",
|
|
296
|
-
windowDays: null,
|
|
297
|
-
async build({ dataSource, ctx }) {
|
|
298
|
-
const index = await createIndexingMetadataStore({ dataSource }).loadIndex(ctx);
|
|
299
|
-
const records = Object.values(index.records);
|
|
300
|
-
const updatesByDay = /* @__PURE__ */ new Map();
|
|
301
|
-
const removesByDay = /* @__PURE__ */ new Map();
|
|
302
|
-
let totalUpdates = 0;
|
|
303
|
-
let totalRemoves = 0;
|
|
304
|
-
let latestUpdate;
|
|
305
|
-
let latestRemove;
|
|
306
|
-
for (const r of records) {
|
|
307
|
-
if (r.latestUpdateAt) {
|
|
308
|
-
totalUpdates++;
|
|
309
|
-
const day = r.latestUpdateAt.slice(0, 10);
|
|
310
|
-
updatesByDay.set(day, (updatesByDay.get(day) ?? 0) + 1);
|
|
311
|
-
if (!latestUpdate || r.latestUpdateAt > latestUpdate) latestUpdate = r.latestUpdateAt;
|
|
312
|
-
}
|
|
313
|
-
if (r.latestRemoveAt) {
|
|
314
|
-
totalRemoves++;
|
|
315
|
-
const day = r.latestRemoveAt.slice(0, 10);
|
|
316
|
-
removesByDay.set(day, (removesByDay.get(day) ?? 0) + 1);
|
|
317
|
-
if (!latestRemove || r.latestRemoveAt > latestRemove) latestRemove = r.latestRemoveAt;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
const days = new Set([...updatesByDay.keys(), ...removesByDay.keys()]);
|
|
321
|
-
const perDay = Array.from(days).sort().map((day) => ({
|
|
322
|
-
day,
|
|
323
|
-
updates: updatesByDay.get(day) ?? 0,
|
|
324
|
-
removes: removesByDay.get(day) ?? 0
|
|
325
|
-
}));
|
|
326
|
-
return {
|
|
327
|
-
totals: {
|
|
328
|
-
urls: records.length,
|
|
329
|
-
updates: totalUpdates,
|
|
330
|
-
removes: totalRemoves,
|
|
331
|
-
latestUpdateAt: latestUpdate ?? null,
|
|
332
|
-
latestRemoveAt: latestRemove ?? null
|
|
333
|
-
},
|
|
334
|
-
days: perDay
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
};
|
|
338
|
-
const DEFAULT_ROLLUPS = [
|
|
339
|
-
dailyTotalsRollup,
|
|
340
|
-
weeklyTotalsRollup,
|
|
341
|
-
topPages28dRollup,
|
|
342
|
-
topKeywords28dRollup,
|
|
343
|
-
topCountries28dRollup,
|
|
344
|
-
indexingMetadataRollup
|
|
345
|
-
];
|
|
346
|
-
export { DEFAULT_ROLLUPS, dailyTotalsRollup, indexingMetadataRollup, rebuildRollups, rollupKey, rollupParquetKey, topCountries28dRollup, topKeywords28dParquetRollup, topKeywords28dRollup, topPages28dRollup, weeklyTotalsRollup };
|