@gscdump/analysis 0.33.0 → 0.33.4
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/analyzer/index.mjs +153 -384
- package/dist/default-registry.mjs +153 -384
- package/dist/index.mjs +267 -609
- package/dist/report/index.mjs +114 -225
- package/dist/semantic/index.d.mts +8 -8
- package/dist/semantic/index.mjs +77 -77
- package/package.json +4 -4
package/dist/semantic/index.mjs
CHANGED
|
@@ -1,54 +1,8 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
super(`content-gap requires a source with executeSql (got '${kind}'); use createBrowserQuerySource or createSqliteQuerySource`);
|
|
4
|
-
this.name = "ContentGapSourceUnsupportedError";
|
|
5
|
-
}
|
|
6
|
-
};
|
|
7
|
-
const ORIGIN_RE = /^https?:\/\/[^/]+/;
|
|
8
|
-
const HTML_EXT_RE = /\.(html?|php|aspx?)$/i;
|
|
9
|
-
const SEP_RE = /[-_]+/g;
|
|
10
|
-
const DIGITS_ONLY_RE = /^\d+$/;
|
|
11
|
-
const WWW_RE = /^www\./;
|
|
12
|
-
const DOT_RE = /\./g;
|
|
13
|
-
const HASH_RE = /#.*$/;
|
|
14
|
-
const QUERY_RE = /\?.*$/;
|
|
15
|
-
const TRAIL_SLASH_RE = /(?<=.)\/$/;
|
|
16
|
-
const MODEL_ID = "Xenova/bge-base-en-v1.5";
|
|
17
|
-
const QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
|
|
1
|
+
const CONTENT_GAP_MODEL_ID = "Xenova/bge-base-en-v1.5";
|
|
2
|
+
const CONTENT_GAP_QUERY_PREFIX = "Represent this sentence for searching relevant passages: ";
|
|
18
3
|
const DB_NAME = "content-gap-embeddings";
|
|
19
4
|
const STORE = "vectors";
|
|
20
5
|
let dbPromise = null;
|
|
21
|
-
function normalizeUrl(u) {
|
|
22
|
-
try {
|
|
23
|
-
const url = new URL(u);
|
|
24
|
-
url.hash = "";
|
|
25
|
-
url.search = "";
|
|
26
|
-
let path = url.pathname;
|
|
27
|
-
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
28
|
-
return `${url.origin}${path}`;
|
|
29
|
-
} catch {
|
|
30
|
-
return u.replace(HASH_RE, "").replace(QUERY_RE, "").replace(TRAIL_SLASH_RE, "");
|
|
31
|
-
}
|
|
32
|
-
}
|
|
33
|
-
function deriveUrlText(url) {
|
|
34
|
-
try {
|
|
35
|
-
const u = new URL(url);
|
|
36
|
-
const text = u.pathname.split("/").filter(Boolean).map((s) => decodeURIComponent(s).toLowerCase()).map((s) => s.replace(HTML_EXT_RE, "")).map((s) => s.replace(SEP_RE, " ")).filter((s) => s.length > 0 && !DIGITS_ONLY_RE.test(s)).join(" ");
|
|
37
|
-
if (text.length > 0) return text;
|
|
38
|
-
return u.hostname.replace(WWW_RE, "").replace(DOT_RE, " ");
|
|
39
|
-
} catch {
|
|
40
|
-
return url.replace(ORIGIN_RE, "").replace(SEP_RE, " ");
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
function cosineNormalized(a, b) {
|
|
44
|
-
let dot = 0;
|
|
45
|
-
const n = a.length;
|
|
46
|
-
for (let i = 0; i < n; i++) dot += a[i] * b[i];
|
|
47
|
-
return dot;
|
|
48
|
-
}
|
|
49
|
-
function notify(onProgress, progress) {
|
|
50
|
-
onProgress?.(progress);
|
|
51
|
-
}
|
|
52
6
|
function openDb() {
|
|
53
7
|
if (dbPromise != null) return dbPromise;
|
|
54
8
|
dbPromise = new Promise((resolve, reject) => {
|
|
@@ -63,7 +17,7 @@ function openDb() {
|
|
|
63
17
|
return dbPromise;
|
|
64
18
|
}
|
|
65
19
|
function cacheKey(role, text) {
|
|
66
|
-
return `${
|
|
20
|
+
return `${CONTENT_GAP_MODEL_ID}|${role}|${text}`;
|
|
67
21
|
}
|
|
68
22
|
async function cacheGetMany(role, texts) {
|
|
69
23
|
const db = await openDb().catch(() => null);
|
|
@@ -119,7 +73,25 @@ async function embedRawBatch(extractor, texts, onProgress, batchSize = 32) {
|
|
|
119
73
|
}
|
|
120
74
|
return result;
|
|
121
75
|
}
|
|
122
|
-
async function
|
|
76
|
+
async function selectContentGapDevice(requested) {
|
|
77
|
+
let chosenDevice = "wasm";
|
|
78
|
+
if (requested === "webgpu" || requested == null) {
|
|
79
|
+
const gpu = globalThis.navigator?.gpu;
|
|
80
|
+
if (gpu != null) {
|
|
81
|
+
if (await gpu.requestAdapter().catch(() => null) != null) chosenDevice = "webgpu";
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return chosenDevice;
|
|
85
|
+
}
|
|
86
|
+
async function loadContentGapExtractor(device) {
|
|
87
|
+
const { pipeline, env } = await import("@huggingface/transformers");
|
|
88
|
+
env.useBrowserCache = true;
|
|
89
|
+
return await pipeline("feature-extraction", CONTENT_GAP_MODEL_ID, {
|
|
90
|
+
device,
|
|
91
|
+
dtype: "fp32"
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
async function embedContentGapTexts(extractor, role, texts, transform, onProgress) {
|
|
123
95
|
const cached = await cacheGetMany(role, texts);
|
|
124
96
|
const vectors = Array.from({ length: texts.length });
|
|
125
97
|
const missIdx = [];
|
|
@@ -153,25 +125,7 @@ async function embedCached(extractor, role, texts, transform, onProgress) {
|
|
|
153
125
|
misses
|
|
154
126
|
};
|
|
155
127
|
}
|
|
156
|
-
async function
|
|
157
|
-
let chosenDevice = "wasm";
|
|
158
|
-
if (requested === "webgpu" || requested == null) {
|
|
159
|
-
const gpu = globalThis.navigator?.gpu;
|
|
160
|
-
if (gpu != null) {
|
|
161
|
-
if (await gpu.requestAdapter().catch(() => null) != null) chosenDevice = "webgpu";
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
return chosenDevice;
|
|
165
|
-
}
|
|
166
|
-
async function loadExtractor(device) {
|
|
167
|
-
const { pipeline, env } = await import("@huggingface/transformers");
|
|
168
|
-
env.useBrowserCache = true;
|
|
169
|
-
return await pipeline("feature-extraction", MODEL_ID, {
|
|
170
|
-
device,
|
|
171
|
-
dtype: "fp32"
|
|
172
|
-
});
|
|
173
|
-
}
|
|
174
|
-
async function fetchContentGapInputs(executeSql, options) {
|
|
128
|
+
async function fetchContentGapInputs(executeSql, options, normalizeUrl) {
|
|
175
129
|
const t1 = performance.now();
|
|
176
130
|
const queryRows = await executeSql(`
|
|
177
131
|
WITH query_totals AS (
|
|
@@ -229,6 +183,52 @@ async function fetchContentGapInputs(executeSql, options) {
|
|
|
229
183
|
sqlMs
|
|
230
184
|
};
|
|
231
185
|
}
|
|
186
|
+
var ContentGapSourceUnsupportedError = class extends Error {
|
|
187
|
+
constructor(kind) {
|
|
188
|
+
super(`content-gap requires a source with executeSql (got '${kind}'); use createBrowserQuerySource or createSqliteQuerySource`);
|
|
189
|
+
this.name = "ContentGapSourceUnsupportedError";
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
const ORIGIN_RE = /^https?:\/\/[^/]+/;
|
|
193
|
+
const HTML_EXT_RE = /\.(html?|php|aspx?)$/i;
|
|
194
|
+
const SEP_RE = /[-_]+/g;
|
|
195
|
+
const DIGITS_ONLY_RE = /^\d+$/;
|
|
196
|
+
const WWW_RE = /^www\./;
|
|
197
|
+
const DOT_RE = /\./g;
|
|
198
|
+
const HASH_RE = /#.*$/;
|
|
199
|
+
const QUERY_RE = /\?.*$/;
|
|
200
|
+
const TRAIL_SLASH_RE = /(?<=.)\/$/;
|
|
201
|
+
function normalizeUrl(u) {
|
|
202
|
+
try {
|
|
203
|
+
const url = new URL(u);
|
|
204
|
+
url.hash = "";
|
|
205
|
+
url.search = "";
|
|
206
|
+
let path = url.pathname;
|
|
207
|
+
if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1);
|
|
208
|
+
return `${url.origin}${path}`;
|
|
209
|
+
} catch {
|
|
210
|
+
return u.replace(HASH_RE, "").replace(QUERY_RE, "").replace(TRAIL_SLASH_RE, "");
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
function deriveUrlText(url) {
|
|
214
|
+
try {
|
|
215
|
+
const u = new URL(url);
|
|
216
|
+
const text = u.pathname.split("/").filter(Boolean).map((s) => decodeURIComponent(s).toLowerCase()).map((s) => s.replace(HTML_EXT_RE, "")).map((s) => s.replace(SEP_RE, " ")).filter((s) => s.length > 0 && !DIGITS_ONLY_RE.test(s)).join(" ");
|
|
217
|
+
if (text.length > 0) return text;
|
|
218
|
+
return u.hostname.replace(WWW_RE, "").replace(DOT_RE, " ");
|
|
219
|
+
} catch {
|
|
220
|
+
return url.replace(ORIGIN_RE, "").replace(SEP_RE, " ");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
function cosineNormalized(a, b) {
|
|
224
|
+
let dot = 0;
|
|
225
|
+
const n = a.length;
|
|
226
|
+
for (let i = 0; i < n; i++) dot += a[i] * b[i];
|
|
227
|
+
return dot;
|
|
228
|
+
}
|
|
229
|
+
function notify(onProgress, progress) {
|
|
230
|
+
onProgress?.(progress);
|
|
231
|
+
}
|
|
232
232
|
function rankContentGaps(queries, urls, queryEmbeddings, urlEmbeddings, minDivergence) {
|
|
233
233
|
const urlIndex = /* @__PURE__ */ new Map();
|
|
234
234
|
for (let i = 0; i < urls.length; i++) urlIndex.set(urls[i], i);
|
|
@@ -275,12 +275,12 @@ async function analyzeContentGap(source, opts = {}) {
|
|
|
275
275
|
message: "Checking device..."
|
|
276
276
|
});
|
|
277
277
|
const t0 = performance.now();
|
|
278
|
-
const chosenDevice = await
|
|
278
|
+
const chosenDevice = await selectContentGapDevice(device);
|
|
279
279
|
notify(onProgress, {
|
|
280
280
|
phase: "loading-model",
|
|
281
|
-
message: `Loading ${
|
|
281
|
+
message: `Loading ${CONTENT_GAP_MODEL_ID} on ${chosenDevice} (~110MB, cached after first run)...`
|
|
282
282
|
});
|
|
283
|
-
const extractor = await
|
|
283
|
+
const extractor = await loadContentGapExtractor(chosenDevice);
|
|
284
284
|
const modelMs = performance.now() - t0;
|
|
285
285
|
notify(onProgress, {
|
|
286
286
|
phase: "fetching-data",
|
|
@@ -291,7 +291,7 @@ async function analyzeContentGap(source, opts = {}) {
|
|
|
291
291
|
maxQueries,
|
|
292
292
|
maxUrls,
|
|
293
293
|
minImpressions
|
|
294
|
-
});
|
|
294
|
+
}, normalizeUrl);
|
|
295
295
|
if (queries.length === 0 || urls.length === 0) {
|
|
296
296
|
notify(onProgress, {
|
|
297
297
|
phase: "done",
|
|
@@ -309,7 +309,7 @@ async function analyzeContentGap(source, opts = {}) {
|
|
|
309
309
|
cacheHits: 0,
|
|
310
310
|
totalInputs: 0,
|
|
311
311
|
device: chosenDevice,
|
|
312
|
-
modelId:
|
|
312
|
+
modelId: CONTENT_GAP_MODEL_ID
|
|
313
313
|
}
|
|
314
314
|
};
|
|
315
315
|
}
|
|
@@ -324,7 +324,7 @@ async function analyzeContentGap(source, opts = {}) {
|
|
|
324
324
|
sqlMs
|
|
325
325
|
});
|
|
326
326
|
const t2 = performance.now();
|
|
327
|
-
const queryEmbed = await
|
|
327
|
+
const queryEmbed = await embedContentGapTexts(extractor, "query", queryTexts, (t) => CONTENT_GAP_QUERY_PREFIX + t, (done, total) => {
|
|
328
328
|
notify(onProgress, {
|
|
329
329
|
phase: "embedding-queries",
|
|
330
330
|
message: `Embedding ${queryTexts.length} queries on ${chosenDevice}...`,
|
|
@@ -342,7 +342,7 @@ async function analyzeContentGap(source, opts = {}) {
|
|
|
342
342
|
modelMs,
|
|
343
343
|
sqlMs
|
|
344
344
|
});
|
|
345
|
-
const urlEmbed = await
|
|
345
|
+
const urlEmbed = await embedContentGapTexts(extractor, "passage", urlTexts, (t) => t, (done, total) => {
|
|
346
346
|
notify(onProgress, {
|
|
347
347
|
phase: "embedding-urls",
|
|
348
348
|
message: `Embedding ${urls.length} URLs...`,
|
|
@@ -384,7 +384,7 @@ async function analyzeContentGap(source, opts = {}) {
|
|
|
384
384
|
cacheHits: totalHits,
|
|
385
385
|
totalInputs,
|
|
386
386
|
device: chosenDevice,
|
|
387
|
-
modelId:
|
|
387
|
+
modelId: CONTENT_GAP_MODEL_ID
|
|
388
388
|
}
|
|
389
389
|
};
|
|
390
390
|
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gscdump/analysis",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.33.
|
|
4
|
+
"version": "0.33.4",
|
|
5
5
|
"description": "GSC analyzers — striking-distance, opportunity, movers, decay, brand, clustering, concentration, seasonality. Pure row-based + DuckDB-native.",
|
|
6
6
|
"author": {
|
|
7
7
|
"name": "Harlan Wilton",
|
|
@@ -76,9 +76,9 @@
|
|
|
76
76
|
"dependencies": {
|
|
77
77
|
"drizzle-orm": "1.0.0-rc.3",
|
|
78
78
|
"pluralize": "^8.0.0",
|
|
79
|
-
"@gscdump/engine": "0.33.
|
|
80
|
-
"@gscdump/engine-gsc-api": "0.33.
|
|
81
|
-
"gscdump": "0.33.
|
|
79
|
+
"@gscdump/engine": "0.33.4",
|
|
80
|
+
"@gscdump/engine-gsc-api": "0.33.4",
|
|
81
|
+
"gscdump": "0.33.4"
|
|
82
82
|
},
|
|
83
83
|
"devDependencies": {
|
|
84
84
|
"vitest": "^4.1.9"
|