@gscdump/analysis 0.33.0 → 0.33.3

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.
@@ -1,54 +1,8 @@
1
- var ContentGapSourceUnsupportedError = class extends Error {
2
- constructor(kind) {
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 `${MODEL_ID}|${role}|${text}`;
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 embedCached(extractor, role, texts, transform, onProgress) {
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 selectDevice(requested) {
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 selectDevice(device);
278
+ const chosenDevice = await selectContentGapDevice(device);
279
279
  notify(onProgress, {
280
280
  phase: "loading-model",
281
- message: `Loading ${MODEL_ID} on ${chosenDevice} (~110MB, cached after first run)...`
281
+ message: `Loading ${CONTENT_GAP_MODEL_ID} on ${chosenDevice} (~110MB, cached after first run)...`
282
282
  });
283
- const extractor = await loadExtractor(chosenDevice);
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: MODEL_ID
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 embedCached(extractor, "query", queryTexts, (t) => QUERY_PREFIX + t, (done, total) => {
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 embedCached(extractor, "passage", urlTexts, (t) => t, (done, total) => {
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: MODEL_ID
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.0",
4
+ "version": "0.33.3",
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.0",
80
- "@gscdump/engine-gsc-api": "0.33.0",
81
- "gscdump": "0.33.0"
79
+ "@gscdump/engine": "0.33.3",
80
+ "@gscdump/engine-gsc-api": "0.33.3",
81
+ "gscdump": "0.33.3"
82
82
  },
83
83
  "devDependencies": {
84
84
  "vitest": "^4.1.9"