@gscdump/analysis 0.38.2 → 0.40.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.
- package/README.md +9 -5
- package/dist/analyzer/index.d.mts +10 -9
- package/dist/analyzer/index.mjs +154 -92
- package/dist/default-registry.mjs +154 -92
- package/dist/index.d.mts +55 -13
- package/dist/index.mjs +186 -101
- package/package.json +4 -5
package/README.md
CHANGED
|
@@ -89,7 +89,9 @@ const registry = createAnalyzerRegistry({ rows: ROW_ANALYZERS, sql: SQL_ANALYZER
|
|
|
89
89
|
const result = await runAnalyzerFromSource(source, { type: 'striking-distance', minImpressions: 100 }, registry)
|
|
90
90
|
```
|
|
91
91
|
|
|
92
|
-
`attachParquetIndex` and `attachSnapshotIndex` from `@gscdump/engine
|
|
92
|
+
`attachParquetIndex` and `attachSnapshotIndex` from `@gscdump/engine/node`
|
|
93
|
+
wire parquet files (per-day, per-month, or pre-baked `.duckdb` snapshots) into
|
|
94
|
+
a Node DuckDB session.
|
|
93
95
|
|
|
94
96
|
## Browser (DuckDB-WASM)
|
|
95
97
|
|
|
@@ -98,7 +100,6 @@ import { analyzeInBrowser } from '@gscdump/analysis'
|
|
|
98
100
|
// Compose your own narrow registry instead of pulling the kitchen-sink default
|
|
99
101
|
// (which statically imports every SQL analyzer). For demo only:
|
|
100
102
|
import { defaultAnalyzerRegistry } from '@gscdump/analysis/registry'
|
|
101
|
-
import { createEngine } from '@gscdump/engine-duckdb-wasm'
|
|
102
103
|
|
|
103
104
|
const result = await analyzeInBrowser(
|
|
104
105
|
runner,
|
|
@@ -110,7 +111,10 @@ const result = await analyzeInBrowser(
|
|
|
110
111
|
|
|
111
112
|
`analyzeInBrowser` wraps any runner with `query(sql, params, signal?)` in an `AnalysisQuerySource` with the `attachedTables` capability and dispatches via `runAnalyzerFromSource`.
|
|
112
113
|
|
|
113
|
-
|
|
114
|
+
`@gscdump/engine-duckdb-wasm` exports `bootDuckDBWasm`,
|
|
115
|
+
`attachParquetUrlTables`, `createBrowserAnalysisRuntime`, and `resolveWindow`
|
|
116
|
+
(re-exported from `@gscdump/engine/period`). Browser analysis uses attached
|
|
117
|
+
tables rather than a canonical-schema `createEngine`; see ADR-0001.
|
|
114
118
|
|
|
115
119
|
## SQLite (D1 / Cloudflare Workers)
|
|
116
120
|
|
|
@@ -148,7 +152,7 @@ Available source factories:
|
|
|
148
152
|
- `createCompositeSource({ engine, gsc })` — `@gscdump/analysis/source`; engine first, GSC fallback
|
|
149
153
|
- `createInMemoryQuerySource({ queryRows })` — `@gscdump/analysis/source`
|
|
150
154
|
- `createEngineQuerySource({ engine, ctx })` — `@gscdump/engine/source`
|
|
151
|
-
- `createEngine({ ... })` — `@gscdump/engine-
|
|
155
|
+
- `createEngine({ ... })` — `@gscdump/engine-sqlite`
|
|
152
156
|
|
|
153
157
|
Portable analyzers currently cover the row-based tools:
|
|
154
158
|
`striking-distance`, `opportunity`, `brand`, `clustering`, `concentration`,
|
|
@@ -192,7 +196,7 @@ Presets: `last-7d`, `last-28d`, `last-30d`, `last-90d`, `last-180d`, `last-365d`
|
|
|
192
196
|
|
|
193
197
|
- [`gscdump`](../gscdump) — REST client + query builder (edge-safe).
|
|
194
198
|
- [`@gscdump/engine`](../engine) — Parquet/DuckDB storage engine + analyzer/source/period contracts.
|
|
195
|
-
- [`@gscdump/engine
|
|
199
|
+
- [`@gscdump/engine/node`](../engine) — Node DuckDB handle + parquet/snapshot attach helpers.
|
|
196
200
|
- [`@gscdump/engine-duckdb-wasm`](../engine-duckdb-wasm) — DuckDB-WASM browser runtime + drizzle adapter.
|
|
197
201
|
- [`@gscdump/engine-sqlite`](../engine-sqlite) — SQLite / D1 dialect adapter.
|
|
198
202
|
- [`@gscdump/engine-gsc-api`](../engine-gsc-api) — GSC live-API engine adapter.
|
|
@@ -81,6 +81,13 @@ interface BrandSegmentationOptions {
|
|
|
81
81
|
/** Minimum impressions for a keyword to be included. Default: 10 */
|
|
82
82
|
minImpressions?: number;
|
|
83
83
|
}
|
|
84
|
+
interface BrandSegmentationRow {
|
|
85
|
+
query?: string;
|
|
86
|
+
keyword?: string;
|
|
87
|
+
variants?: readonly string[];
|
|
88
|
+
clicks?: number | null;
|
|
89
|
+
impressions?: number | null;
|
|
90
|
+
}
|
|
84
91
|
interface BrandSummary {
|
|
85
92
|
brandClicks: number;
|
|
86
93
|
nonBrandClicks: number;
|
|
@@ -88,9 +95,9 @@ interface BrandSummary {
|
|
|
88
95
|
brandImpressions: number;
|
|
89
96
|
nonBrandImpressions: number;
|
|
90
97
|
}
|
|
91
|
-
interface BrandSegmentationResult {
|
|
92
|
-
brand:
|
|
93
|
-
nonBrand:
|
|
98
|
+
interface BrandSegmentationResult<T extends BrandSegmentationRow = QueriesRow> {
|
|
99
|
+
brand: T[];
|
|
100
|
+
nonBrand: T[];
|
|
94
101
|
summary: BrandSummary;
|
|
95
102
|
}
|
|
96
103
|
interface BrandResultRow {
|
|
@@ -698,12 +705,6 @@ interface TrendsResult {
|
|
|
698
705
|
series: TrendSeriesPoint[];
|
|
699
706
|
}
|
|
700
707
|
declare const trendsAnalyzer: import("@gscdump/engine/analyzer").DefinedAnalyzer;
|
|
701
|
-
/**
|
|
702
|
-
* `zero-click` — high impressions, low CTR, good ranking. SQL groups by
|
|
703
|
-
* (query, url) and applies HAVING/WHERE pushdown; row reducer dedupes to the
|
|
704
|
-
* best-positioned page per query (different semantics, kept for parity with
|
|
705
|
-
* the existing live-GSC behavior).
|
|
706
|
-
*/
|
|
707
708
|
interface ZeroClickResult {
|
|
708
709
|
query: string;
|
|
709
710
|
page: string;
|
package/dist/analyzer/index.mjs
CHANGED
|
@@ -6,7 +6,7 @@ import { num } from "@gscdump/engine/analysis-types";
|
|
|
6
6
|
import { err, ok, unwrapResult } from "gscdump/result";
|
|
7
7
|
import { between, date, extractDateRange, gsc, page, query } from "gscdump/query";
|
|
8
8
|
import { MS_PER_DAY, daysAgoUtc, toIsoDate } from "gscdump/dates";
|
|
9
|
-
import { buildExtrasQueries, buildTotalsSql, mergeExtras, resolveComparisonSQL,
|
|
9
|
+
import { buildExtrasQueries, buildTotalsSql, mergeExtras, resolveComparisonSQL, resolveToSQLOptimized } from "@gscdump/engine/resolver";
|
|
10
10
|
function rowNumber(value) {
|
|
11
11
|
if (typeof value === "number") return Number.isFinite(value) ? value : 0;
|
|
12
12
|
if (typeof value === "bigint") return Number(value);
|
|
@@ -522,8 +522,7 @@ function analyzeBrandSegmentation(keywords, options) {
|
|
|
522
522
|
const impressions = num(row.impressions);
|
|
523
523
|
if (impressions < minImpressions) continue;
|
|
524
524
|
const clicks = num(row.clicks);
|
|
525
|
-
|
|
526
|
-
if (lowerBrandTerms.some((term) => query.includes(term))) {
|
|
525
|
+
if ((row.variants?.length ? row.variants : [row.query ?? row.keyword ?? ""]).some((query) => lowerBrandTerms.some((term) => query.toLowerCase().includes(term)))) {
|
|
527
526
|
brand.push(row);
|
|
528
527
|
brandClicks += clicks;
|
|
529
528
|
brandImpressions += impressions;
|
|
@@ -665,7 +664,7 @@ function createMetricSorter(defaultMetric, orderByMetric) {
|
|
|
665
664
|
return [...items].sort((a, b) => (a[sortBy] - b[sortBy]) * mult);
|
|
666
665
|
};
|
|
667
666
|
}
|
|
668
|
-
const sortRowResults
|
|
667
|
+
const sortRowResults = createSorter((item, metric) => {
|
|
669
668
|
switch (metric) {
|
|
670
669
|
case "clicks": return item.totalClicks;
|
|
671
670
|
case "impressions": return item.totalImpressions;
|
|
@@ -712,7 +711,7 @@ function analyzeCannibalization(rows, options = {}) {
|
|
|
712
711
|
positionSpread
|
|
713
712
|
});
|
|
714
713
|
}
|
|
715
|
-
return sortRowResults
|
|
714
|
+
return sortRowResults(results, sortBy, sortOrder);
|
|
716
715
|
}
|
|
717
716
|
const cannibalizationAnalyzer = defineAnalyzer({
|
|
718
717
|
id: "cannibalization",
|
|
@@ -2250,14 +2249,9 @@ function shapeDataQueryRows(rows, params, extras) {
|
|
|
2250
2249
|
function buildDataDetailPlan(params, options) {
|
|
2251
2250
|
const state = requireBuilderState(params.q, "data-detail");
|
|
2252
2251
|
if (!state.dimensions.includes("date")) throw new Error("data-detail: `date` dimension is required");
|
|
2253
|
-
const main =
|
|
2254
|
-
const totals = buildTotalsSql(state, options);
|
|
2252
|
+
const main = resolveToSQLOptimized(state, options);
|
|
2255
2253
|
const prev = optionalBuilderState(params.qc, "data-detail", "qc");
|
|
2256
|
-
const extraQueries = [
|
|
2257
|
-
name: "totals",
|
|
2258
|
-
sql: totals.sql,
|
|
2259
|
-
params: totals.params
|
|
2260
|
-
}];
|
|
2254
|
+
const extraQueries = [];
|
|
2261
2255
|
if (prev) {
|
|
2262
2256
|
const previousTotals = buildTotalsSql(prev, options);
|
|
2263
2257
|
extraQueries.push({
|
|
@@ -2275,18 +2269,22 @@ function buildDataDetailPlan(params, options) {
|
|
|
2275
2269
|
}
|
|
2276
2270
|
function shapeDataDetailRows(rows, params, extras) {
|
|
2277
2271
|
const { startDate: rangeStart, endDate: rangeEnd } = extractDateRange(requireBuilderState(params.q, "data-detail").filter);
|
|
2278
|
-
const
|
|
2272
|
+
const first = rows[0];
|
|
2273
|
+
const totals = {
|
|
2274
|
+
clicks: Number(first?.totalClicks ?? 0),
|
|
2275
|
+
impressions: Number(first?.totalImpressions ?? 0),
|
|
2276
|
+
ctr: Number(first?.totalCtr ?? 0),
|
|
2277
|
+
position: Number(first?.totalPosition ?? 0)
|
|
2278
|
+
};
|
|
2279
|
+
const coerced = rows.map((raw) => {
|
|
2280
|
+
const { totalCount: _tc, totalClicks: _tclk, totalImpressions: _timp, totalCtr: _tctr, totalPosition: _tpos, sum_position: _sp, ...rest } = raw;
|
|
2281
|
+
return coerceNumericCols(rest);
|
|
2282
|
+
});
|
|
2279
2283
|
const daily = rangeStart && rangeEnd ? padTimeseries(coerced, {
|
|
2280
2284
|
startDate: rangeStart,
|
|
2281
2285
|
endDate: rangeEnd
|
|
2282
2286
|
}) : coerced;
|
|
2283
|
-
const
|
|
2284
|
-
const meta = { totals: {
|
|
2285
|
-
clicks: Number(totalsRow.clicks ?? 0),
|
|
2286
|
-
impressions: Number(totalsRow.impressions ?? 0),
|
|
2287
|
-
ctr: Number(totalsRow.ctr ?? 0),
|
|
2288
|
-
position: Number(totalsRow.position ?? 0)
|
|
2289
|
-
} };
|
|
2287
|
+
const meta = { totals };
|
|
2290
2288
|
if (extras?.prevTotals) {
|
|
2291
2289
|
const previousTotalsRow = extras.prevTotals[0] ?? {};
|
|
2292
2290
|
meta.previousTotals = {
|
|
@@ -2403,13 +2401,75 @@ function paginateInMemory(rows, input) {
|
|
|
2403
2401
|
const o = clampOffset(input.offset);
|
|
2404
2402
|
return rows.slice(o, o + l);
|
|
2405
2403
|
}
|
|
2404
|
+
function paginateSortedInMemory(rows, input, compare) {
|
|
2405
|
+
const limit = clampLimit(input.limit, rows.length);
|
|
2406
|
+
const offset = clampOffset(input.offset);
|
|
2407
|
+
const selectedCount = Math.min(rows.length, offset + limit);
|
|
2408
|
+
if (limit === 0 || offset >= rows.length || selectedCount === 0) return [];
|
|
2409
|
+
const fullSort = () => [...rows].sort(compare).slice(offset, offset + limit);
|
|
2410
|
+
if (selectedCount >= rows.length / 2) return fullSort();
|
|
2411
|
+
let invalidComparison = false;
|
|
2412
|
+
const compareStable = (left, right) => {
|
|
2413
|
+
const order = compare(left.value, right.value);
|
|
2414
|
+
if (Number.isNaN(order)) {
|
|
2415
|
+
invalidComparison = true;
|
|
2416
|
+
return left.index - right.index;
|
|
2417
|
+
}
|
|
2418
|
+
return order || left.index - right.index;
|
|
2419
|
+
};
|
|
2420
|
+
const isWorse = (left, right) => compareStable(left, right) > 0;
|
|
2421
|
+
const heap = [];
|
|
2422
|
+
const siftUp = (start) => {
|
|
2423
|
+
let index = start;
|
|
2424
|
+
while (index > 0) {
|
|
2425
|
+
const parent = index - 1 >>> 1;
|
|
2426
|
+
if (!isWorse(heap[index], heap[parent])) break;
|
|
2427
|
+
const swap = heap[parent];
|
|
2428
|
+
heap[parent] = heap[index];
|
|
2429
|
+
heap[index] = swap;
|
|
2430
|
+
index = parent;
|
|
2431
|
+
}
|
|
2432
|
+
};
|
|
2433
|
+
const siftDown = () => {
|
|
2434
|
+
let index = 0;
|
|
2435
|
+
for (;;) {
|
|
2436
|
+
const left = index * 2 + 1;
|
|
2437
|
+
if (left >= heap.length) return;
|
|
2438
|
+
const right = left + 1;
|
|
2439
|
+
let worse = left;
|
|
2440
|
+
if (right < heap.length && isWorse(heap[right], heap[left])) worse = right;
|
|
2441
|
+
if (!isWorse(heap[worse], heap[index])) return;
|
|
2442
|
+
const swap = heap[index];
|
|
2443
|
+
heap[index] = heap[worse];
|
|
2444
|
+
heap[worse] = swap;
|
|
2445
|
+
index = worse;
|
|
2446
|
+
}
|
|
2447
|
+
};
|
|
2448
|
+
for (let index = 0; index < rows.length; index++) {
|
|
2449
|
+
const candidate = {
|
|
2450
|
+
value: rows[index],
|
|
2451
|
+
index
|
|
2452
|
+
};
|
|
2453
|
+
if (heap.length < selectedCount) {
|
|
2454
|
+
heap.push(candidate);
|
|
2455
|
+
siftUp(heap.length - 1);
|
|
2456
|
+
} else if (compareStable(candidate, heap[0]) < 0) {
|
|
2457
|
+
heap[0] = candidate;
|
|
2458
|
+
siftDown();
|
|
2459
|
+
}
|
|
2460
|
+
}
|
|
2461
|
+
if (invalidComparison) return fullSort();
|
|
2462
|
+
heap.sort(compareStable);
|
|
2463
|
+
if (invalidComparison) return fullSort();
|
|
2464
|
+
return heap.slice(offset, offset + limit).map((entry) => entry.value);
|
|
2465
|
+
}
|
|
2406
2466
|
function resolveSort(input, allowed, defaults) {
|
|
2407
2467
|
return {
|
|
2408
2468
|
sortBy: input.sortBy && allowed.includes(input.sortBy) ? input.sortBy : defaults.sortBy,
|
|
2409
2469
|
sortDir: input.sortDir === "asc" || input.sortDir === "desc" ? input.sortDir : defaults.sortDir
|
|
2410
2470
|
};
|
|
2411
2471
|
}
|
|
2412
|
-
const sortResults
|
|
2472
|
+
const sortResults = createMetricSorter("lostClicks", {
|
|
2413
2473
|
lostClicks: "desc",
|
|
2414
2474
|
declinePercent: "desc",
|
|
2415
2475
|
currentClicks: "asc"
|
|
@@ -2442,7 +2502,7 @@ function analyzeDecay(input, options = {}) {
|
|
|
2442
2502
|
positionDrop: currentPosition != null ? currentPosition - prev.position : null
|
|
2443
2503
|
});
|
|
2444
2504
|
}
|
|
2445
|
-
return sortResults
|
|
2505
|
+
return sortResults(results, sortBy);
|
|
2446
2506
|
}
|
|
2447
2507
|
const decayAnalyzer = defineAnalyzer({
|
|
2448
2508
|
id: "decay",
|
|
@@ -3441,12 +3501,52 @@ function calculateCtrGapScore(actualCtr, position) {
|
|
|
3441
3501
|
const gap = expectedCtr - actualCtr;
|
|
3442
3502
|
return Math.min(gap / expectedCtr, 1);
|
|
3443
3503
|
}
|
|
3444
|
-
const
|
|
3504
|
+
const SORT_DIR = {
|
|
3445
3505
|
opportunityScore: "desc",
|
|
3446
3506
|
potentialClicks: "desc",
|
|
3447
3507
|
impressions: "desc",
|
|
3448
3508
|
position: "asc"
|
|
3449
|
-
}
|
|
3509
|
+
};
|
|
3510
|
+
function analyzeOpportunity(keywords, options = {}) {
|
|
3511
|
+
const minImpressions = options.minImpressions ?? 100;
|
|
3512
|
+
const positionWeight = options.weights?.position ?? 1;
|
|
3513
|
+
const impressionsWeight = options.weights?.impressions ?? 1;
|
|
3514
|
+
const ctrGapWeight = options.weights?.ctrGap ?? 1;
|
|
3515
|
+
const totalWeight = positionWeight + impressionsWeight + ctrGapWeight;
|
|
3516
|
+
const sortBy = options.sortBy ?? "opportunityScore";
|
|
3517
|
+
const results = [];
|
|
3518
|
+
for (const row of keywords) {
|
|
3519
|
+
const impressions = num(row.impressions);
|
|
3520
|
+
if (impressions < minImpressions) continue;
|
|
3521
|
+
const position = num(row.position);
|
|
3522
|
+
const ctr = num(row.ctr);
|
|
3523
|
+
const clicks = num(row.clicks);
|
|
3524
|
+
const positionScore = calculatePositionScore(position);
|
|
3525
|
+
const impressionScore = calculateImpressionScore(impressions);
|
|
3526
|
+
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
3527
|
+
const weightedProduct = positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight;
|
|
3528
|
+
const opportunityScore = Math.round(weightedProduct ** (1 / totalWeight) * 100);
|
|
3529
|
+
const potentialClicks = Math.round(impressions * getExpectedCtr(Math.min(3, position)));
|
|
3530
|
+
results.push({
|
|
3531
|
+
keyword: row.query,
|
|
3532
|
+
page: row.page ?? null,
|
|
3533
|
+
clicks,
|
|
3534
|
+
impressions,
|
|
3535
|
+
ctr,
|
|
3536
|
+
position,
|
|
3537
|
+
opportunityScore,
|
|
3538
|
+
potentialClicks,
|
|
3539
|
+
factors: {
|
|
3540
|
+
positionScore,
|
|
3541
|
+
impressionScore,
|
|
3542
|
+
ctrGapScore
|
|
3543
|
+
}
|
|
3544
|
+
});
|
|
3545
|
+
}
|
|
3546
|
+
const direction = SORT_DIR[sortBy];
|
|
3547
|
+
results.sort((left, right) => direction === "asc" ? left[sortBy] - right[sortBy] : right[sortBy] - left[sortBy]);
|
|
3548
|
+
return options.limit ? results.slice(0, options.limit) : results;
|
|
3549
|
+
}
|
|
3450
3550
|
const opportunityAnalyzer = defineAnalyzer({
|
|
3451
3551
|
id: "opportunity",
|
|
3452
3552
|
buildSql(params) {
|
|
@@ -3560,51 +3660,15 @@ const opportunityAnalyzer = defineAnalyzer({
|
|
|
3560
3660
|
return { queries: queriesQueryState(periodOf(params), params.limit) };
|
|
3561
3661
|
},
|
|
3562
3662
|
reduceRows(rows, params) {
|
|
3563
|
-
const
|
|
3564
|
-
const
|
|
3565
|
-
const positionWeight = 1;
|
|
3566
|
-
const impressionsWeight = 1;
|
|
3567
|
-
const ctrGapWeight = 1;
|
|
3568
|
-
const sortBy = "opportunityScore";
|
|
3569
|
-
const results = [];
|
|
3570
|
-
for (const row of keywords) {
|
|
3571
|
-
const impressions = num(row.impressions);
|
|
3572
|
-
const position = num(row.position);
|
|
3573
|
-
const ctr = num(row.ctr);
|
|
3574
|
-
const clicks = num(row.clicks);
|
|
3575
|
-
if (impressions < minImpressions) continue;
|
|
3576
|
-
const positionScore = calculatePositionScore(position);
|
|
3577
|
-
const impressionScore = calculateImpressionScore(impressions);
|
|
3578
|
-
const ctrGapScore = calculateCtrGapScore(ctr, position);
|
|
3579
|
-
const geometricMean = (positionScore ** positionWeight * impressionScore ** impressionsWeight * ctrGapScore ** ctrGapWeight) ** (1 / 3);
|
|
3580
|
-
const opportunityScore = Math.round(geometricMean * 100);
|
|
3581
|
-
const targetCtr = getExpectedCtr(Math.min(3, position));
|
|
3582
|
-
const potentialClicks = Math.round(impressions * targetCtr);
|
|
3583
|
-
results.push({
|
|
3584
|
-
keyword: row.query,
|
|
3585
|
-
page: row.page ?? null,
|
|
3586
|
-
clicks,
|
|
3587
|
-
impressions,
|
|
3588
|
-
ctr,
|
|
3589
|
-
position,
|
|
3590
|
-
opportunityScore,
|
|
3591
|
-
potentialClicks,
|
|
3592
|
-
factors: {
|
|
3593
|
-
positionScore,
|
|
3594
|
-
impressionScore,
|
|
3595
|
-
ctrGapScore
|
|
3596
|
-
}
|
|
3597
|
-
});
|
|
3598
|
-
}
|
|
3599
|
-
const sorted = sortResults(results, sortBy);
|
|
3600
|
-
const paged = paginateInMemory(sorted, {
|
|
3663
|
+
const results = analyzeOpportunity((Array.isArray(rows) ? rows : []) ?? [], { minImpressions: params.minImpressions });
|
|
3664
|
+
const paged = paginateSortedInMemory(results, {
|
|
3601
3665
|
limit: params.limit,
|
|
3602
3666
|
offset: params.offset
|
|
3603
|
-
});
|
|
3667
|
+
}, (left, right) => right.opportunityScore - left.opportunityScore);
|
|
3604
3668
|
return {
|
|
3605
3669
|
results: paged,
|
|
3606
3670
|
meta: {
|
|
3607
|
-
total:
|
|
3671
|
+
total: results.length,
|
|
3608
3672
|
returned: paged.length
|
|
3609
3673
|
}
|
|
3610
3674
|
};
|
|
@@ -4276,6 +4340,12 @@ const stlDecomposeAnalyzer = defineAnalyzer({
|
|
|
4276
4340
|
}
|
|
4277
4341
|
});
|
|
4278
4342
|
const DEFAULT_ROW_LIMIT$1 = 25e3;
|
|
4343
|
+
function paginateStrikingDistance(results, options) {
|
|
4344
|
+
return paginateSortedInMemory(results, {
|
|
4345
|
+
limit: options.limit ?? 1e3,
|
|
4346
|
+
offset: options.offset
|
|
4347
|
+
}, (left, right) => right.potentialClicks - left.potentialClicks);
|
|
4348
|
+
}
|
|
4279
4349
|
function filterStrikingDistance(rows, options = {}) {
|
|
4280
4350
|
const minPosition = options.minPosition ?? 4;
|
|
4281
4351
|
const maxPosition = options.maxPosition ?? 20;
|
|
@@ -4306,11 +4376,7 @@ const strikingDistanceAnalyzer = defineAnalyzer({
|
|
|
4306
4376
|
id: "striking-distance",
|
|
4307
4377
|
reduce(rows, params) {
|
|
4308
4378
|
const results = filterStrikingDistance(Array.isArray(rows) ? rows : [], params);
|
|
4309
|
-
|
|
4310
|
-
const paged = paginateInMemory(results, {
|
|
4311
|
-
limit: params.limit ?? 1e3,
|
|
4312
|
-
offset: params.offset
|
|
4313
|
-
});
|
|
4379
|
+
const paged = paginateStrikingDistance(results, params);
|
|
4314
4380
|
return {
|
|
4315
4381
|
results: paged,
|
|
4316
4382
|
meta: {
|
|
@@ -4686,7 +4752,18 @@ const trendsAnalyzer = defineAnalyzer({
|
|
|
4686
4752
|
}
|
|
4687
4753
|
});
|
|
4688
4754
|
const DEFAULT_ROW_LIMIT = 25e3;
|
|
4689
|
-
|
|
4755
|
+
function analyzeZeroClick(rows, options = {}) {
|
|
4756
|
+
const minImpressions = options.minImpressions ?? 1e3;
|
|
4757
|
+
const maxCtr = options.maxCtr ?? .03;
|
|
4758
|
+
const maxPosition = options.maxPosition ?? 10;
|
|
4759
|
+
const queryMap = /* @__PURE__ */ new Map();
|
|
4760
|
+
for (const row of rows) {
|
|
4761
|
+
if (row.impressions < minImpressions || row.position > maxPosition || row.ctr > maxCtr) continue;
|
|
4762
|
+
const existing = queryMap.get(row.query);
|
|
4763
|
+
if (!existing || row.position < existing.position) queryMap.set(row.query, { ...row });
|
|
4764
|
+
}
|
|
4765
|
+
return [...queryMap.values()].sort((left, right) => right.impressions - left.impressions);
|
|
4766
|
+
}
|
|
4690
4767
|
const zeroClickAnalyzer = defineAnalyzer({
|
|
4691
4768
|
id: "zero-click",
|
|
4692
4769
|
buildSql(params) {
|
|
@@ -4770,30 +4847,15 @@ const zeroClickAnalyzer = defineAnalyzer({
|
|
|
4770
4847
|
return { rows: gsc.select(query, page).where(between(date, period.startDate, period.endDate)).limit(limit).getState() };
|
|
4771
4848
|
},
|
|
4772
4849
|
reduceRows(rows, params) {
|
|
4773
|
-
const
|
|
4774
|
-
|
|
4775
|
-
|
|
4776
|
-
|
|
4777
|
-
|
|
4778
|
-
|
|
4779
|
-
if (row.impressions < minImpressions) continue;
|
|
4780
|
-
if (row.position > maxPosition) continue;
|
|
4781
|
-
if (row.ctr > maxCtr) continue;
|
|
4782
|
-
const existing = queryMap.get(row.query);
|
|
4783
|
-
if (!existing || row.position < existing.position) queryMap.set(row.query, {
|
|
4784
|
-
query: row.query,
|
|
4785
|
-
page: row.page,
|
|
4786
|
-
clicks: row.clicks,
|
|
4787
|
-
impressions: row.impressions,
|
|
4788
|
-
ctr: row.ctr,
|
|
4789
|
-
position: row.position
|
|
4790
|
-
});
|
|
4791
|
-
}
|
|
4792
|
-
const results = sortRowResults(Array.from(queryMap.values()), "impressions", "desc");
|
|
4793
|
-
const paged = paginateInMemory(results, {
|
|
4850
|
+
const results = analyzeZeroClick(Array.isArray(rows) ? rows : [], {
|
|
4851
|
+
minImpressions: params.minImpressions ?? 1e3,
|
|
4852
|
+
maxCtr: params.maxCtr ?? .03,
|
|
4853
|
+
maxPosition: params.maxPosition ?? 10
|
|
4854
|
+
});
|
|
4855
|
+
const paged = paginateSortedInMemory(results, {
|
|
4794
4856
|
limit: params.limit,
|
|
4795
4857
|
offset: params.offset
|
|
4796
|
-
});
|
|
4858
|
+
}, (left, right) => right.impressions - left.impressions);
|
|
4797
4859
|
return {
|
|
4798
4860
|
results: paged,
|
|
4799
4861
|
meta: {
|