searchsocket 0.6.0 → 0.6.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/dist/cli.js CHANGED
@@ -12,7 +12,7 @@ import { Command, Option } from "commander";
12
12
  // package.json
13
13
  var package_default = {
14
14
  name: "searchsocket",
15
- version: "0.6.0",
15
+ version: "0.6.1",
16
16
  description: "Semantic site search and MCP retrieval for SvelteKit static sites",
17
17
  license: "MIT",
18
18
  author: "Greg Priday <greg@siteorigin.com>",
@@ -874,7 +874,7 @@ var UpstashSearchStore = class {
874
874
  }
875
875
  async upsertChunks(chunks, scope) {
876
876
  if (chunks.length === 0) return;
877
- const BATCH_SIZE = 100;
877
+ const BATCH_SIZE = 90;
878
878
  for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
879
879
  const batch = chunks.slice(i, i + BATCH_SIZE);
880
880
  await this.chunksNs.upsert(
@@ -1018,7 +1018,7 @@ var UpstashSearchStore = class {
1018
1018
  }
1019
1019
  async deleteByIds(ids, _scope) {
1020
1020
  if (ids.length === 0) return;
1021
- const BATCH_SIZE = 100;
1021
+ const BATCH_SIZE = 90;
1022
1022
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
1023
1023
  const batch = ids.slice(i, i + BATCH_SIZE);
1024
1024
  await this.chunksNs.delete(batch);
@@ -1046,7 +1046,7 @@ var UpstashSearchStore = class {
1046
1046
  } catch {
1047
1047
  }
1048
1048
  if (ids.length > 0) {
1049
- const BATCH_SIZE = 100;
1049
+ const BATCH_SIZE = 90;
1050
1050
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
1051
1051
  const batch = ids.slice(i, i + BATCH_SIZE);
1052
1052
  await ns.delete(batch);
@@ -1085,7 +1085,39 @@ var UpstashSearchStore = class {
1085
1085
  }));
1086
1086
  }
1087
1087
  async getContentHashes(scope) {
1088
+ return this.scanHashes(this.chunksNs, scope);
1089
+ }
1090
+ /**
1091
+ * Fetch content hashes for a specific set of chunk keys using direct fetch()
1092
+ * instead of range(). This avoids potential issues with range() returning
1093
+ * vectors from the wrong namespace on hybrid indexes.
1094
+ */
1095
+ async fetchContentHashesForKeys(keys, scope) {
1088
1096
  const map = /* @__PURE__ */ new Map();
1097
+ if (keys.length === 0) return map;
1098
+ const BATCH_SIZE = 90;
1099
+ for (let i = 0; i < keys.length; i += BATCH_SIZE) {
1100
+ const batch = keys.slice(i, i + BATCH_SIZE);
1101
+ try {
1102
+ const results = await this.chunksNs.fetch(batch, {
1103
+ includeMetadata: true
1104
+ });
1105
+ for (const doc of results) {
1106
+ if (doc && doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
1107
+ map.set(String(doc.id), doc.metadata.contentHash);
1108
+ }
1109
+ }
1110
+ } catch {
1111
+ }
1112
+ }
1113
+ return map;
1114
+ }
1115
+ /**
1116
+ * Scan all IDs in the chunks namespace for this scope.
1117
+ * Used for deletion detection (finding stale chunk keys).
1118
+ */
1119
+ async scanChunkIds(scope) {
1120
+ const ids = /* @__PURE__ */ new Set();
1089
1121
  let cursor = "0";
1090
1122
  try {
1091
1123
  for (; ; ) {
@@ -1094,6 +1126,28 @@ var UpstashSearchStore = class {
1094
1126
  limit: 100,
1095
1127
  includeMetadata: true
1096
1128
  });
1129
+ for (const doc of result.vectors) {
1130
+ if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName) {
1131
+ ids.add(String(doc.id));
1132
+ }
1133
+ }
1134
+ if (!result.nextCursor || result.nextCursor === "0") break;
1135
+ cursor = result.nextCursor;
1136
+ }
1137
+ } catch {
1138
+ }
1139
+ return ids;
1140
+ }
1141
+ async scanHashes(ns, scope) {
1142
+ const map = /* @__PURE__ */ new Map();
1143
+ let cursor = "0";
1144
+ try {
1145
+ for (; ; ) {
1146
+ const result = await ns.range({
1147
+ cursor,
1148
+ limit: 100,
1149
+ includeMetadata: true
1150
+ });
1097
1151
  for (const doc of result.vectors) {
1098
1152
  if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
1099
1153
  map.set(String(doc.id), doc.metadata.contentHash);
@@ -1156,7 +1210,7 @@ var UpstashSearchStore = class {
1156
1210
  }
1157
1211
  async deletePagesByIds(ids, _scope) {
1158
1212
  if (ids.length === 0) return;
1159
- const BATCH_SIZE = 50;
1213
+ const BATCH_SIZE = 90;
1160
1214
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
1161
1215
  const batch = ids.slice(i, i + BATCH_SIZE);
1162
1216
  await this.pagesNs.delete(batch);
@@ -1164,7 +1218,7 @@ var UpstashSearchStore = class {
1164
1218
  }
1165
1219
  async upsertPages(pages, scope) {
1166
1220
  if (pages.length === 0) return;
1167
- const BATCH_SIZE = 50;
1221
+ const BATCH_SIZE = 90;
1168
1222
  for (let i = 0; i < pages.length; i += BATCH_SIZE) {
1169
1223
  const batch = pages.slice(i, i + BATCH_SIZE);
1170
1224
  await this.pagesNs.upsert(
@@ -1308,7 +1362,7 @@ var UpstashSearchStore = class {
1308
1362
  } catch {
1309
1363
  }
1310
1364
  if (ids.length > 0) {
1311
- const BATCH_SIZE = 100;
1365
+ const BATCH_SIZE = 90;
1312
1366
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
1313
1367
  const batch = ids.slice(i, i + BATCH_SIZE);
1314
1368
  await ns.delete(batch);
@@ -3375,10 +3429,9 @@ var IndexPipeline = class _IndexPipeline {
3375
3429
  this.logger.info("Dry run \u2014 no writes will be performed");
3376
3430
  }
3377
3431
  const manifestStart = stageStart();
3378
- const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getContentHashes(scope);
3379
3432
  const existingPageHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getPageHashes(scope);
3380
3433
  stageEnd("manifest", manifestStart);
3381
- this.logger.debug(`Manifest: ${existingHashes.size} existing chunk hashes, ${existingPageHashes.size} existing page hashes loaded`);
3434
+ this.logger.debug(`Manifest: ${existingPageHashes.size} existing page hashes loaded`);
3382
3435
  const sourceStart = stageStart();
3383
3436
  this.logger.info(`Loading pages (source: ${sourceMode})...`);
3384
3437
  let sourcePages;
@@ -3767,6 +3820,11 @@ var IndexPipeline = class _IndexPipeline {
3767
3820
  for (const chunk of chunks) {
3768
3821
  currentChunkMap.set(chunk.chunkKey, chunk);
3769
3822
  }
3823
+ const chunkHashStart = stageStart();
3824
+ const currentChunkKeys = chunks.map((c) => c.chunkKey);
3825
+ const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.fetchContentHashesForKeys(currentChunkKeys, scope);
3826
+ stageEnd("chunk_hashes", chunkHashStart);
3827
+ this.logger.debug(`Fetched ${existingHashes.size} existing chunk hashes for ${currentChunkKeys.length} current keys`);
3770
3828
  let changedChunks = chunks.filter((chunk) => {
3771
3829
  if (options.force) {
3772
3830
  return true;
@@ -3780,7 +3838,8 @@ var IndexPipeline = class _IndexPipeline {
3780
3838
  }
3781
3839
  return existingHash !== chunk.contentHash;
3782
3840
  });
3783
- const deletes = [...existingHashes.keys()].filter((chunkKey) => !currentChunkMap.has(chunkKey));
3841
+ const existingChunkIds = options.force ? /* @__PURE__ */ new Set() : await this.store.scanChunkIds(scope);
3842
+ const deletes = [...existingChunkIds].filter((chunkKey) => !currentChunkMap.has(chunkKey));
3784
3843
  if (this.hooks.beforeIndex) {
3785
3844
  changedChunks = await this.hooks.beforeIndex(changedChunks);
3786
3845
  }
package/dist/index.cjs CHANGED
@@ -17285,7 +17285,7 @@ var UpstashSearchStore = class {
17285
17285
  }
17286
17286
  async upsertChunks(chunks, scope) {
17287
17287
  if (chunks.length === 0) return;
17288
- const BATCH_SIZE = 100;
17288
+ const BATCH_SIZE = 90;
17289
17289
  for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
17290
17290
  const batch = chunks.slice(i, i + BATCH_SIZE);
17291
17291
  await this.chunksNs.upsert(
@@ -17429,7 +17429,7 @@ var UpstashSearchStore = class {
17429
17429
  }
17430
17430
  async deleteByIds(ids, _scope) {
17431
17431
  if (ids.length === 0) return;
17432
- const BATCH_SIZE = 100;
17432
+ const BATCH_SIZE = 90;
17433
17433
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17434
17434
  const batch = ids.slice(i, i + BATCH_SIZE);
17435
17435
  await this.chunksNs.delete(batch);
@@ -17457,7 +17457,7 @@ var UpstashSearchStore = class {
17457
17457
  } catch {
17458
17458
  }
17459
17459
  if (ids.length > 0) {
17460
- const BATCH_SIZE = 100;
17460
+ const BATCH_SIZE = 90;
17461
17461
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17462
17462
  const batch = ids.slice(i, i + BATCH_SIZE);
17463
17463
  await ns.delete(batch);
@@ -17496,7 +17496,39 @@ var UpstashSearchStore = class {
17496
17496
  }));
17497
17497
  }
17498
17498
  async getContentHashes(scope) {
17499
+ return this.scanHashes(this.chunksNs, scope);
17500
+ }
17501
+ /**
17502
+ * Fetch content hashes for a specific set of chunk keys using direct fetch()
17503
+ * instead of range(). This avoids potential issues with range() returning
17504
+ * vectors from the wrong namespace on hybrid indexes.
17505
+ */
17506
+ async fetchContentHashesForKeys(keys, scope) {
17499
17507
  const map = /* @__PURE__ */ new Map();
17508
+ if (keys.length === 0) return map;
17509
+ const BATCH_SIZE = 90;
17510
+ for (let i = 0; i < keys.length; i += BATCH_SIZE) {
17511
+ const batch = keys.slice(i, i + BATCH_SIZE);
17512
+ try {
17513
+ const results = await this.chunksNs.fetch(batch, {
17514
+ includeMetadata: true
17515
+ });
17516
+ for (const doc of results) {
17517
+ if (doc && doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17518
+ map.set(String(doc.id), doc.metadata.contentHash);
17519
+ }
17520
+ }
17521
+ } catch {
17522
+ }
17523
+ }
17524
+ return map;
17525
+ }
17526
+ /**
17527
+ * Scan all IDs in the chunks namespace for this scope.
17528
+ * Used for deletion detection (finding stale chunk keys).
17529
+ */
17530
+ async scanChunkIds(scope) {
17531
+ const ids = /* @__PURE__ */ new Set();
17500
17532
  let cursor = "0";
17501
17533
  try {
17502
17534
  for (; ; ) {
@@ -17505,6 +17537,28 @@ var UpstashSearchStore = class {
17505
17537
  limit: 100,
17506
17538
  includeMetadata: true
17507
17539
  });
17540
+ for (const doc of result.vectors) {
17541
+ if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName) {
17542
+ ids.add(String(doc.id));
17543
+ }
17544
+ }
17545
+ if (!result.nextCursor || result.nextCursor === "0") break;
17546
+ cursor = result.nextCursor;
17547
+ }
17548
+ } catch {
17549
+ }
17550
+ return ids;
17551
+ }
17552
+ async scanHashes(ns, scope) {
17553
+ const map = /* @__PURE__ */ new Map();
17554
+ let cursor = "0";
17555
+ try {
17556
+ for (; ; ) {
17557
+ const result = await ns.range({
17558
+ cursor,
17559
+ limit: 100,
17560
+ includeMetadata: true
17561
+ });
17508
17562
  for (const doc of result.vectors) {
17509
17563
  if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17510
17564
  map.set(String(doc.id), doc.metadata.contentHash);
@@ -17567,7 +17621,7 @@ var UpstashSearchStore = class {
17567
17621
  }
17568
17622
  async deletePagesByIds(ids, _scope) {
17569
17623
  if (ids.length === 0) return;
17570
- const BATCH_SIZE = 50;
17624
+ const BATCH_SIZE = 90;
17571
17625
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17572
17626
  const batch = ids.slice(i, i + BATCH_SIZE);
17573
17627
  await this.pagesNs.delete(batch);
@@ -17575,7 +17629,7 @@ var UpstashSearchStore = class {
17575
17629
  }
17576
17630
  async upsertPages(pages, scope) {
17577
17631
  if (pages.length === 0) return;
17578
- const BATCH_SIZE = 50;
17632
+ const BATCH_SIZE = 90;
17579
17633
  for (let i = 0; i < pages.length; i += BATCH_SIZE) {
17580
17634
  const batch = pages.slice(i, i + BATCH_SIZE);
17581
17635
  await this.pagesNs.upsert(
@@ -17719,7 +17773,7 @@ var UpstashSearchStore = class {
17719
17773
  } catch {
17720
17774
  }
17721
17775
  if (ids.length > 0) {
17722
- const BATCH_SIZE = 100;
17776
+ const BATCH_SIZE = 90;
17723
17777
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17724
17778
  const batch = ids.slice(i, i + BATCH_SIZE);
17725
17779
  await ns.delete(batch);
@@ -20634,10 +20688,9 @@ var IndexPipeline = class _IndexPipeline {
20634
20688
  this.logger.info("Dry run \u2014 no writes will be performed");
20635
20689
  }
20636
20690
  const manifestStart = stageStart();
20637
- const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getContentHashes(scope);
20638
20691
  const existingPageHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getPageHashes(scope);
20639
20692
  stageEnd("manifest", manifestStart);
20640
- this.logger.debug(`Manifest: ${existingHashes.size} existing chunk hashes, ${existingPageHashes.size} existing page hashes loaded`);
20693
+ this.logger.debug(`Manifest: ${existingPageHashes.size} existing page hashes loaded`);
20641
20694
  const sourceStart = stageStart();
20642
20695
  this.logger.info(`Loading pages (source: ${sourceMode})...`);
20643
20696
  let sourcePages;
@@ -21026,6 +21079,11 @@ var IndexPipeline = class _IndexPipeline {
21026
21079
  for (const chunk of chunks) {
21027
21080
  currentChunkMap.set(chunk.chunkKey, chunk);
21028
21081
  }
21082
+ const chunkHashStart = stageStart();
21083
+ const currentChunkKeys = chunks.map((c) => c.chunkKey);
21084
+ const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.fetchContentHashesForKeys(currentChunkKeys, scope);
21085
+ stageEnd("chunk_hashes", chunkHashStart);
21086
+ this.logger.debug(`Fetched ${existingHashes.size} existing chunk hashes for ${currentChunkKeys.length} current keys`);
21029
21087
  let changedChunks = chunks.filter((chunk) => {
21030
21088
  if (options.force) {
21031
21089
  return true;
@@ -21039,7 +21097,8 @@ var IndexPipeline = class _IndexPipeline {
21039
21097
  }
21040
21098
  return existingHash !== chunk.contentHash;
21041
21099
  });
21042
- const deletes = [...existingHashes.keys()].filter((chunkKey) => !currentChunkMap.has(chunkKey));
21100
+ const existingChunkIds = options.force ? /* @__PURE__ */ new Set() : await this.store.scanChunkIds(scope);
21101
+ const deletes = [...existingChunkIds].filter((chunkKey) => !currentChunkMap.has(chunkKey));
21043
21102
  if (this.hooks.beforeIndex) {
21044
21103
  changedChunks = await this.hooks.beforeIndex(changedChunks);
21045
21104
  }
package/dist/index.d.cts CHANGED
@@ -88,6 +88,18 @@ declare class UpstashSearchStore {
88
88
  deleteScope(scope: Scope): Promise<void>;
89
89
  listScopes(projectId: string): Promise<ScopeInfo[]>;
90
90
  getContentHashes(scope: Scope): Promise<Map<string, string>>;
91
+ /**
92
+ * Fetch content hashes for a specific set of chunk keys using direct fetch()
93
+ * instead of range(). This avoids potential issues with range() returning
94
+ * vectors from the wrong namespace on hybrid indexes.
95
+ */
96
+ fetchContentHashesForKeys(keys: string[], scope: Scope): Promise<Map<string, string>>;
97
+ /**
98
+ * Scan all IDs in the chunks namespace for this scope.
99
+ * Used for deletion detection (finding stale chunk keys).
100
+ */
101
+ scanChunkIds(scope: Scope): Promise<Set<string>>;
102
+ private scanHashes;
91
103
  listPages(scope: Scope, opts?: {
92
104
  cursor?: string;
93
105
  limit?: number;
package/dist/index.d.ts CHANGED
@@ -88,6 +88,18 @@ declare class UpstashSearchStore {
88
88
  deleteScope(scope: Scope): Promise<void>;
89
89
  listScopes(projectId: string): Promise<ScopeInfo[]>;
90
90
  getContentHashes(scope: Scope): Promise<Map<string, string>>;
91
+ /**
92
+ * Fetch content hashes for a specific set of chunk keys using direct fetch()
93
+ * instead of range(). This avoids potential issues with range() returning
94
+ * vectors from the wrong namespace on hybrid indexes.
95
+ */
96
+ fetchContentHashesForKeys(keys: string[], scope: Scope): Promise<Map<string, string>>;
97
+ /**
98
+ * Scan all IDs in the chunks namespace for this scope.
99
+ * Used for deletion detection (finding stale chunk keys).
100
+ */
101
+ scanChunkIds(scope: Scope): Promise<Set<string>>;
102
+ private scanHashes;
91
103
  listPages(scope: Scope, opts?: {
92
104
  cursor?: string;
93
105
  limit?: number;
package/dist/index.js CHANGED
@@ -17273,7 +17273,7 @@ var UpstashSearchStore = class {
17273
17273
  }
17274
17274
  async upsertChunks(chunks, scope) {
17275
17275
  if (chunks.length === 0) return;
17276
- const BATCH_SIZE = 100;
17276
+ const BATCH_SIZE = 90;
17277
17277
  for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
17278
17278
  const batch = chunks.slice(i, i + BATCH_SIZE);
17279
17279
  await this.chunksNs.upsert(
@@ -17417,7 +17417,7 @@ var UpstashSearchStore = class {
17417
17417
  }
17418
17418
  async deleteByIds(ids, _scope) {
17419
17419
  if (ids.length === 0) return;
17420
- const BATCH_SIZE = 100;
17420
+ const BATCH_SIZE = 90;
17421
17421
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17422
17422
  const batch = ids.slice(i, i + BATCH_SIZE);
17423
17423
  await this.chunksNs.delete(batch);
@@ -17445,7 +17445,7 @@ var UpstashSearchStore = class {
17445
17445
  } catch {
17446
17446
  }
17447
17447
  if (ids.length > 0) {
17448
- const BATCH_SIZE = 100;
17448
+ const BATCH_SIZE = 90;
17449
17449
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17450
17450
  const batch = ids.slice(i, i + BATCH_SIZE);
17451
17451
  await ns.delete(batch);
@@ -17484,7 +17484,39 @@ var UpstashSearchStore = class {
17484
17484
  }));
17485
17485
  }
17486
17486
  async getContentHashes(scope) {
17487
+ return this.scanHashes(this.chunksNs, scope);
17488
+ }
17489
+ /**
17490
+ * Fetch content hashes for a specific set of chunk keys using direct fetch()
17491
+ * instead of range(). This avoids potential issues with range() returning
17492
+ * vectors from the wrong namespace on hybrid indexes.
17493
+ */
17494
+ async fetchContentHashesForKeys(keys, scope) {
17487
17495
  const map = /* @__PURE__ */ new Map();
17496
+ if (keys.length === 0) return map;
17497
+ const BATCH_SIZE = 90;
17498
+ for (let i = 0; i < keys.length; i += BATCH_SIZE) {
17499
+ const batch = keys.slice(i, i + BATCH_SIZE);
17500
+ try {
17501
+ const results = await this.chunksNs.fetch(batch, {
17502
+ includeMetadata: true
17503
+ });
17504
+ for (const doc of results) {
17505
+ if (doc && doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17506
+ map.set(String(doc.id), doc.metadata.contentHash);
17507
+ }
17508
+ }
17509
+ } catch {
17510
+ }
17511
+ }
17512
+ return map;
17513
+ }
17514
+ /**
17515
+ * Scan all IDs in the chunks namespace for this scope.
17516
+ * Used for deletion detection (finding stale chunk keys).
17517
+ */
17518
+ async scanChunkIds(scope) {
17519
+ const ids = /* @__PURE__ */ new Set();
17488
17520
  let cursor = "0";
17489
17521
  try {
17490
17522
  for (; ; ) {
@@ -17493,6 +17525,28 @@ var UpstashSearchStore = class {
17493
17525
  limit: 100,
17494
17526
  includeMetadata: true
17495
17527
  });
17528
+ for (const doc of result.vectors) {
17529
+ if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName) {
17530
+ ids.add(String(doc.id));
17531
+ }
17532
+ }
17533
+ if (!result.nextCursor || result.nextCursor === "0") break;
17534
+ cursor = result.nextCursor;
17535
+ }
17536
+ } catch {
17537
+ }
17538
+ return ids;
17539
+ }
17540
+ async scanHashes(ns, scope) {
17541
+ const map = /* @__PURE__ */ new Map();
17542
+ let cursor = "0";
17543
+ try {
17544
+ for (; ; ) {
17545
+ const result = await ns.range({
17546
+ cursor,
17547
+ limit: 100,
17548
+ includeMetadata: true
17549
+ });
17496
17550
  for (const doc of result.vectors) {
17497
17551
  if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17498
17552
  map.set(String(doc.id), doc.metadata.contentHash);
@@ -17555,7 +17609,7 @@ var UpstashSearchStore = class {
17555
17609
  }
17556
17610
  async deletePagesByIds(ids, _scope) {
17557
17611
  if (ids.length === 0) return;
17558
- const BATCH_SIZE = 50;
17612
+ const BATCH_SIZE = 90;
17559
17613
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17560
17614
  const batch = ids.slice(i, i + BATCH_SIZE);
17561
17615
  await this.pagesNs.delete(batch);
@@ -17563,7 +17617,7 @@ var UpstashSearchStore = class {
17563
17617
  }
17564
17618
  async upsertPages(pages, scope) {
17565
17619
  if (pages.length === 0) return;
17566
- const BATCH_SIZE = 50;
17620
+ const BATCH_SIZE = 90;
17567
17621
  for (let i = 0; i < pages.length; i += BATCH_SIZE) {
17568
17622
  const batch = pages.slice(i, i + BATCH_SIZE);
17569
17623
  await this.pagesNs.upsert(
@@ -17707,7 +17761,7 @@ var UpstashSearchStore = class {
17707
17761
  } catch {
17708
17762
  }
17709
17763
  if (ids.length > 0) {
17710
- const BATCH_SIZE = 100;
17764
+ const BATCH_SIZE = 90;
17711
17765
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17712
17766
  const batch = ids.slice(i, i + BATCH_SIZE);
17713
17767
  await ns.delete(batch);
@@ -20622,10 +20676,9 @@ var IndexPipeline = class _IndexPipeline {
20622
20676
  this.logger.info("Dry run \u2014 no writes will be performed");
20623
20677
  }
20624
20678
  const manifestStart = stageStart();
20625
- const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getContentHashes(scope);
20626
20679
  const existingPageHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getPageHashes(scope);
20627
20680
  stageEnd("manifest", manifestStart);
20628
- this.logger.debug(`Manifest: ${existingHashes.size} existing chunk hashes, ${existingPageHashes.size} existing page hashes loaded`);
20681
+ this.logger.debug(`Manifest: ${existingPageHashes.size} existing page hashes loaded`);
20629
20682
  const sourceStart = stageStart();
20630
20683
  this.logger.info(`Loading pages (source: ${sourceMode})...`);
20631
20684
  let sourcePages;
@@ -21014,6 +21067,11 @@ var IndexPipeline = class _IndexPipeline {
21014
21067
  for (const chunk of chunks) {
21015
21068
  currentChunkMap.set(chunk.chunkKey, chunk);
21016
21069
  }
21070
+ const chunkHashStart = stageStart();
21071
+ const currentChunkKeys = chunks.map((c) => c.chunkKey);
21072
+ const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.fetchContentHashesForKeys(currentChunkKeys, scope);
21073
+ stageEnd("chunk_hashes", chunkHashStart);
21074
+ this.logger.debug(`Fetched ${existingHashes.size} existing chunk hashes for ${currentChunkKeys.length} current keys`);
21017
21075
  let changedChunks = chunks.filter((chunk) => {
21018
21076
  if (options.force) {
21019
21077
  return true;
@@ -21027,7 +21085,8 @@ var IndexPipeline = class _IndexPipeline {
21027
21085
  }
21028
21086
  return existingHash !== chunk.contentHash;
21029
21087
  });
21030
- const deletes = [...existingHashes.keys()].filter((chunkKey) => !currentChunkMap.has(chunkKey));
21088
+ const existingChunkIds = options.force ? /* @__PURE__ */ new Set() : await this.store.scanChunkIds(scope);
21089
+ const deletes = [...existingChunkIds].filter((chunkKey) => !currentChunkMap.has(chunkKey));
21031
21090
  if (this.hooks.beforeIndex) {
21032
21091
  changedChunks = await this.hooks.beforeIndex(changedChunks);
21033
21092
  }
@@ -17316,7 +17316,7 @@ var UpstashSearchStore = class {
17316
17316
  }
17317
17317
  async upsertChunks(chunks, scope) {
17318
17318
  if (chunks.length === 0) return;
17319
- const BATCH_SIZE = 100;
17319
+ const BATCH_SIZE = 90;
17320
17320
  for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
17321
17321
  const batch = chunks.slice(i, i + BATCH_SIZE);
17322
17322
  await this.chunksNs.upsert(
@@ -17460,7 +17460,7 @@ var UpstashSearchStore = class {
17460
17460
  }
17461
17461
  async deleteByIds(ids, _scope) {
17462
17462
  if (ids.length === 0) return;
17463
- const BATCH_SIZE = 100;
17463
+ const BATCH_SIZE = 90;
17464
17464
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17465
17465
  const batch = ids.slice(i, i + BATCH_SIZE);
17466
17466
  await this.chunksNs.delete(batch);
@@ -17488,7 +17488,7 @@ var UpstashSearchStore = class {
17488
17488
  } catch {
17489
17489
  }
17490
17490
  if (ids.length > 0) {
17491
- const BATCH_SIZE = 100;
17491
+ const BATCH_SIZE = 90;
17492
17492
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17493
17493
  const batch = ids.slice(i, i + BATCH_SIZE);
17494
17494
  await ns.delete(batch);
@@ -17527,7 +17527,39 @@ var UpstashSearchStore = class {
17527
17527
  }));
17528
17528
  }
17529
17529
  async getContentHashes(scope) {
17530
+ return this.scanHashes(this.chunksNs, scope);
17531
+ }
17532
+ /**
17533
+ * Fetch content hashes for a specific set of chunk keys using direct fetch()
17534
+ * instead of range(). This avoids potential issues with range() returning
17535
+ * vectors from the wrong namespace on hybrid indexes.
17536
+ */
17537
+ async fetchContentHashesForKeys(keys, scope) {
17530
17538
  const map = /* @__PURE__ */ new Map();
17539
+ if (keys.length === 0) return map;
17540
+ const BATCH_SIZE = 90;
17541
+ for (let i = 0; i < keys.length; i += BATCH_SIZE) {
17542
+ const batch = keys.slice(i, i + BATCH_SIZE);
17543
+ try {
17544
+ const results = await this.chunksNs.fetch(batch, {
17545
+ includeMetadata: true
17546
+ });
17547
+ for (const doc of results) {
17548
+ if (doc && doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17549
+ map.set(String(doc.id), doc.metadata.contentHash);
17550
+ }
17551
+ }
17552
+ } catch {
17553
+ }
17554
+ }
17555
+ return map;
17556
+ }
17557
+ /**
17558
+ * Scan all IDs in the chunks namespace for this scope.
17559
+ * Used for deletion detection (finding stale chunk keys).
17560
+ */
17561
+ async scanChunkIds(scope) {
17562
+ const ids = /* @__PURE__ */ new Set();
17531
17563
  let cursor = "0";
17532
17564
  try {
17533
17565
  for (; ; ) {
@@ -17536,6 +17568,28 @@ var UpstashSearchStore = class {
17536
17568
  limit: 100,
17537
17569
  includeMetadata: true
17538
17570
  });
17571
+ for (const doc of result.vectors) {
17572
+ if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName) {
17573
+ ids.add(String(doc.id));
17574
+ }
17575
+ }
17576
+ if (!result.nextCursor || result.nextCursor === "0") break;
17577
+ cursor = result.nextCursor;
17578
+ }
17579
+ } catch {
17580
+ }
17581
+ return ids;
17582
+ }
17583
+ async scanHashes(ns, scope) {
17584
+ const map = /* @__PURE__ */ new Map();
17585
+ let cursor = "0";
17586
+ try {
17587
+ for (; ; ) {
17588
+ const result = await ns.range({
17589
+ cursor,
17590
+ limit: 100,
17591
+ includeMetadata: true
17592
+ });
17539
17593
  for (const doc of result.vectors) {
17540
17594
  if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17541
17595
  map.set(String(doc.id), doc.metadata.contentHash);
@@ -17598,7 +17652,7 @@ var UpstashSearchStore = class {
17598
17652
  }
17599
17653
  async deletePagesByIds(ids, _scope) {
17600
17654
  if (ids.length === 0) return;
17601
- const BATCH_SIZE = 50;
17655
+ const BATCH_SIZE = 90;
17602
17656
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17603
17657
  const batch = ids.slice(i, i + BATCH_SIZE);
17604
17658
  await this.pagesNs.delete(batch);
@@ -17606,7 +17660,7 @@ var UpstashSearchStore = class {
17606
17660
  }
17607
17661
  async upsertPages(pages, scope) {
17608
17662
  if (pages.length === 0) return;
17609
- const BATCH_SIZE = 50;
17663
+ const BATCH_SIZE = 90;
17610
17664
  for (let i = 0; i < pages.length; i += BATCH_SIZE) {
17611
17665
  const batch = pages.slice(i, i + BATCH_SIZE);
17612
17666
  await this.pagesNs.upsert(
@@ -17750,7 +17804,7 @@ var UpstashSearchStore = class {
17750
17804
  } catch {
17751
17805
  }
17752
17806
  if (ids.length > 0) {
17753
- const BATCH_SIZE = 100;
17807
+ const BATCH_SIZE = 90;
17754
17808
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17755
17809
  const batch = ids.slice(i, i + BATCH_SIZE);
17756
17810
  await ns.delete(batch);
@@ -21776,10 +21830,9 @@ var IndexPipeline = class _IndexPipeline {
21776
21830
  this.logger.info("Dry run \u2014 no writes will be performed");
21777
21831
  }
21778
21832
  const manifestStart = stageStart();
21779
- const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getContentHashes(scope);
21780
21833
  const existingPageHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getPageHashes(scope);
21781
21834
  stageEnd("manifest", manifestStart);
21782
- this.logger.debug(`Manifest: ${existingHashes.size} existing chunk hashes, ${existingPageHashes.size} existing page hashes loaded`);
21835
+ this.logger.debug(`Manifest: ${existingPageHashes.size} existing page hashes loaded`);
21783
21836
  const sourceStart = stageStart();
21784
21837
  this.logger.info(`Loading pages (source: ${sourceMode})...`);
21785
21838
  let sourcePages;
@@ -22168,6 +22221,11 @@ var IndexPipeline = class _IndexPipeline {
22168
22221
  for (const chunk of chunks) {
22169
22222
  currentChunkMap.set(chunk.chunkKey, chunk);
22170
22223
  }
22224
+ const chunkHashStart = stageStart();
22225
+ const currentChunkKeys = chunks.map((c) => c.chunkKey);
22226
+ const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.fetchContentHashesForKeys(currentChunkKeys, scope);
22227
+ stageEnd("chunk_hashes", chunkHashStart);
22228
+ this.logger.debug(`Fetched ${existingHashes.size} existing chunk hashes for ${currentChunkKeys.length} current keys`);
22171
22229
  let changedChunks = chunks.filter((chunk) => {
22172
22230
  if (options.force) {
22173
22231
  return true;
@@ -22181,7 +22239,8 @@ var IndexPipeline = class _IndexPipeline {
22181
22239
  }
22182
22240
  return existingHash !== chunk.contentHash;
22183
22241
  });
22184
- const deletes = [...existingHashes.keys()].filter((chunkKey) => !currentChunkMap.has(chunkKey));
22242
+ const existingChunkIds = options.force ? /* @__PURE__ */ new Set() : await this.store.scanChunkIds(scope);
22243
+ const deletes = [...existingChunkIds].filter((chunkKey) => !currentChunkMap.has(chunkKey));
22185
22244
  if (this.hooks.beforeIndex) {
22186
22245
  changedChunks = await this.hooks.beforeIndex(changedChunks);
22187
22246
  }
package/dist/sveltekit.js CHANGED
@@ -17304,7 +17304,7 @@ var UpstashSearchStore = class {
17304
17304
  }
17305
17305
  async upsertChunks(chunks, scope) {
17306
17306
  if (chunks.length === 0) return;
17307
- const BATCH_SIZE = 100;
17307
+ const BATCH_SIZE = 90;
17308
17308
  for (let i = 0; i < chunks.length; i += BATCH_SIZE) {
17309
17309
  const batch = chunks.slice(i, i + BATCH_SIZE);
17310
17310
  await this.chunksNs.upsert(
@@ -17448,7 +17448,7 @@ var UpstashSearchStore = class {
17448
17448
  }
17449
17449
  async deleteByIds(ids, _scope) {
17450
17450
  if (ids.length === 0) return;
17451
- const BATCH_SIZE = 100;
17451
+ const BATCH_SIZE = 90;
17452
17452
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17453
17453
  const batch = ids.slice(i, i + BATCH_SIZE);
17454
17454
  await this.chunksNs.delete(batch);
@@ -17476,7 +17476,7 @@ var UpstashSearchStore = class {
17476
17476
  } catch {
17477
17477
  }
17478
17478
  if (ids.length > 0) {
17479
- const BATCH_SIZE = 100;
17479
+ const BATCH_SIZE = 90;
17480
17480
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17481
17481
  const batch = ids.slice(i, i + BATCH_SIZE);
17482
17482
  await ns.delete(batch);
@@ -17515,7 +17515,39 @@ var UpstashSearchStore = class {
17515
17515
  }));
17516
17516
  }
17517
17517
  async getContentHashes(scope) {
17518
+ return this.scanHashes(this.chunksNs, scope);
17519
+ }
17520
+ /**
17521
+ * Fetch content hashes for a specific set of chunk keys using direct fetch()
17522
+ * instead of range(). This avoids potential issues with range() returning
17523
+ * vectors from the wrong namespace on hybrid indexes.
17524
+ */
17525
+ async fetchContentHashesForKeys(keys, scope) {
17518
17526
  const map = /* @__PURE__ */ new Map();
17527
+ if (keys.length === 0) return map;
17528
+ const BATCH_SIZE = 90;
17529
+ for (let i = 0; i < keys.length; i += BATCH_SIZE) {
17530
+ const batch = keys.slice(i, i + BATCH_SIZE);
17531
+ try {
17532
+ const results = await this.chunksNs.fetch(batch, {
17533
+ includeMetadata: true
17534
+ });
17535
+ for (const doc of results) {
17536
+ if (doc && doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17537
+ map.set(String(doc.id), doc.metadata.contentHash);
17538
+ }
17539
+ }
17540
+ } catch {
17541
+ }
17542
+ }
17543
+ return map;
17544
+ }
17545
+ /**
17546
+ * Scan all IDs in the chunks namespace for this scope.
17547
+ * Used for deletion detection (finding stale chunk keys).
17548
+ */
17549
+ async scanChunkIds(scope) {
17550
+ const ids = /* @__PURE__ */ new Set();
17519
17551
  let cursor = "0";
17520
17552
  try {
17521
17553
  for (; ; ) {
@@ -17524,6 +17556,28 @@ var UpstashSearchStore = class {
17524
17556
  limit: 100,
17525
17557
  includeMetadata: true
17526
17558
  });
17559
+ for (const doc of result.vectors) {
17560
+ if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName) {
17561
+ ids.add(String(doc.id));
17562
+ }
17563
+ }
17564
+ if (!result.nextCursor || result.nextCursor === "0") break;
17565
+ cursor = result.nextCursor;
17566
+ }
17567
+ } catch {
17568
+ }
17569
+ return ids;
17570
+ }
17571
+ async scanHashes(ns, scope) {
17572
+ const map = /* @__PURE__ */ new Map();
17573
+ let cursor = "0";
17574
+ try {
17575
+ for (; ; ) {
17576
+ const result = await ns.range({
17577
+ cursor,
17578
+ limit: 100,
17579
+ includeMetadata: true
17580
+ });
17527
17581
  for (const doc of result.vectors) {
17528
17582
  if (doc.metadata?.projectId === scope.projectId && doc.metadata?.scopeName === scope.scopeName && doc.metadata?.contentHash) {
17529
17583
  map.set(String(doc.id), doc.metadata.contentHash);
@@ -17586,7 +17640,7 @@ var UpstashSearchStore = class {
17586
17640
  }
17587
17641
  async deletePagesByIds(ids, _scope) {
17588
17642
  if (ids.length === 0) return;
17589
- const BATCH_SIZE = 50;
17643
+ const BATCH_SIZE = 90;
17590
17644
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17591
17645
  const batch = ids.slice(i, i + BATCH_SIZE);
17592
17646
  await this.pagesNs.delete(batch);
@@ -17594,7 +17648,7 @@ var UpstashSearchStore = class {
17594
17648
  }
17595
17649
  async upsertPages(pages, scope) {
17596
17650
  if (pages.length === 0) return;
17597
- const BATCH_SIZE = 50;
17651
+ const BATCH_SIZE = 90;
17598
17652
  for (let i = 0; i < pages.length; i += BATCH_SIZE) {
17599
17653
  const batch = pages.slice(i, i + BATCH_SIZE);
17600
17654
  await this.pagesNs.upsert(
@@ -17738,7 +17792,7 @@ var UpstashSearchStore = class {
17738
17792
  } catch {
17739
17793
  }
17740
17794
  if (ids.length > 0) {
17741
- const BATCH_SIZE = 100;
17795
+ const BATCH_SIZE = 90;
17742
17796
  for (let i = 0; i < ids.length; i += BATCH_SIZE) {
17743
17797
  const batch = ids.slice(i, i + BATCH_SIZE);
17744
17798
  await ns.delete(batch);
@@ -21764,10 +21818,9 @@ var IndexPipeline = class _IndexPipeline {
21764
21818
  this.logger.info("Dry run \u2014 no writes will be performed");
21765
21819
  }
21766
21820
  const manifestStart = stageStart();
21767
- const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getContentHashes(scope);
21768
21821
  const existingPageHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.getPageHashes(scope);
21769
21822
  stageEnd("manifest", manifestStart);
21770
- this.logger.debug(`Manifest: ${existingHashes.size} existing chunk hashes, ${existingPageHashes.size} existing page hashes loaded`);
21823
+ this.logger.debug(`Manifest: ${existingPageHashes.size} existing page hashes loaded`);
21771
21824
  const sourceStart = stageStart();
21772
21825
  this.logger.info(`Loading pages (source: ${sourceMode})...`);
21773
21826
  let sourcePages;
@@ -22156,6 +22209,11 @@ var IndexPipeline = class _IndexPipeline {
22156
22209
  for (const chunk of chunks) {
22157
22210
  currentChunkMap.set(chunk.chunkKey, chunk);
22158
22211
  }
22212
+ const chunkHashStart = stageStart();
22213
+ const currentChunkKeys = chunks.map((c) => c.chunkKey);
22214
+ const existingHashes = options.force ? /* @__PURE__ */ new Map() : await this.store.fetchContentHashesForKeys(currentChunkKeys, scope);
22215
+ stageEnd("chunk_hashes", chunkHashStart);
22216
+ this.logger.debug(`Fetched ${existingHashes.size} existing chunk hashes for ${currentChunkKeys.length} current keys`);
22159
22217
  let changedChunks = chunks.filter((chunk) => {
22160
22218
  if (options.force) {
22161
22219
  return true;
@@ -22169,7 +22227,8 @@ var IndexPipeline = class _IndexPipeline {
22169
22227
  }
22170
22228
  return existingHash !== chunk.contentHash;
22171
22229
  });
22172
- const deletes = [...existingHashes.keys()].filter((chunkKey) => !currentChunkMap.has(chunkKey));
22230
+ const existingChunkIds = options.force ? /* @__PURE__ */ new Set() : await this.store.scanChunkIds(scope);
22231
+ const deletes = [...existingChunkIds].filter((chunkKey) => !currentChunkMap.has(chunkKey));
22173
22232
  if (this.hooks.beforeIndex) {
22174
22233
  changedChunks = await this.hooks.beforeIndex(changedChunks);
22175
22234
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "searchsocket",
3
- "version": "0.6.0",
3
+ "version": "0.6.1",
4
4
  "description": "Semantic site search and MCP retrieval for SvelteKit static sites",
5
5
  "license": "MIT",
6
6
  "author": "Greg Priday <greg@siteorigin.com>",
@@ -67,18 +67,9 @@
67
67
  "optional": true
68
68
  }
69
69
  },
70
- "scripts": {
71
- "build": "tsup",
72
- "clean": "rm -rf dist",
73
- "typecheck": "tsc --noEmit",
74
- "test": "vitest run",
75
- "test:watch": "vitest",
76
- "test:quality": "SEARCHSOCKET_QUALITY_TESTS=1 vitest run tests/quality.test.ts"
77
- },
78
70
  "engines": {
79
71
  "node": ">=20"
80
72
  },
81
- "packageManager": "pnpm@10.29.2",
82
73
  "dependencies": {
83
74
  "@clack/prompts": "^1.2.0",
84
75
  "@modelcontextprotocol/sdk": "^1.26.0",
@@ -107,5 +98,13 @@
107
98
  "tsup": "^8.5.1",
108
99
  "typescript": "^5.9.3",
109
100
  "vitest": "^4.0.18"
101
+ },
102
+ "scripts": {
103
+ "build": "tsup",
104
+ "clean": "rm -rf dist",
105
+ "typecheck": "tsc --noEmit",
106
+ "test": "vitest run",
107
+ "test:watch": "vitest",
108
+ "test:quality": "SEARCHSOCKET_QUALITY_TESTS=1 vitest run tests/quality.test.ts"
110
109
  }
111
- }
110
+ }