@ai4data/search 0.1.0 β†’ 0.1.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 CHANGED
@@ -34,7 +34,22 @@ client.on('results', ({ data, stats }) => {
34
34
  client.destroy()
35
35
  ```
36
36
 
37
- ### Custom worker path
37
+ ### Loading from a CDN (any origin)
38
+
39
+ When you load the package from a CDN (e.g. via an import map) and your page is on another origin, the browser blocks creating a worker from the CDN URL. Use **`SearchClient.fromCDN()`** so the worker is fetched and run from a blob URL (same-origin):
40
+
41
+ ```ts
42
+ import { SearchClient } from '@ai4data/search'
43
+
44
+ const client = await SearchClient.fromCDN(manifestUrl, {
45
+ workerUrl: 'https://esm.sh/@ai4data/search@0.1.0/worker',
46
+ })
47
+ client.on('results', ({ data }) => console.log(data))
48
+ ```
49
+
50
+ This works from any static host (GitHub Pages, localhost, etc.) with no build step.
51
+
52
+ ### Custom worker path (bundler)
38
53
 
39
54
  If your bundler does not resolve the default worker URL, pass a factory:
40
55
 
@@ -61,6 +76,15 @@ const rankWorker = new Worker(
61
76
 
62
77
  Vue and React adapters are planned (future: `@ai4data/search/vue`, `@ai4data/search/react`).
63
78
 
79
+ ## Demo
80
+
81
+ Two demos are included:
82
+
83
+ 1. **Local build** (uses the built package from `dist/`): run `npm run demo`, then open **http://localhost:5173/demo/**.
84
+ 2. **Standalone HTML** (loads the package from npm via [esm.sh](https://esm.sh)): open **demo/standalone.html** in a browser. Serve the file over HTTP (e.g. from the package directory run `npx serve .` and open http://localhost:3000/demo/standalone.html). No build step; you must use **workerFactory** so the worker is loaded from the CDN (see the file for the pattern).
85
+
86
+ Both demos need a **manifest URL** that points to a `manifest.json` for a search collection.
87
+
64
88
  ## Development
65
89
 
66
90
  From the repo root (with workspaces):
@@ -78,6 +102,39 @@ npm install
78
102
  npm run build
79
103
  ```
80
104
 
105
+ ## Publishing (maintainers)
106
+
107
+ The package is published under the [@ai4data](https://www.npmjs.com/org/ai4data) npm organization. Only maintainers with publish access to the org can release.
108
+
109
+ **Prerequisites**
110
+
111
+ - npm account that is a member of the **ai4data** org with permission to publish.
112
+ - Logged in locally: `npm login` (use your npm credentials or a machine account token).
113
+
114
+ **Steps**
115
+
116
+ 1. Bump the version in `package.json` (or use `npm version patch|minor|major` from this directory).
117
+ 2. From the package directory, publish with public access (required for scoped packages):
118
+
119
+ ```bash
120
+ cd packages/ai4data/search
121
+ npm publish --access public
122
+ ```
123
+
124
+ The `prepublishOnly` script runs `npm run build` automatically before packing, so the published tarball always includes an up-to-date `dist/`.
125
+
126
+ 3. Optionally tag the release in git and push:
127
+
128
+ ```bash
129
+ git tag @ai4data/search@1.0.0
130
+ git push origin @ai4data/search@1.0.0
131
+ ```
132
+
133
+ **What gets published**
134
+
135
+ - Only the `dist/` directory (built ESM and types) and `README.md` are included (see `files` in `package.json`).
136
+ - Consumers install with: `npm install @ai4data/search`.
137
+
81
138
  ## Documentation
82
139
 
83
140
  - [Main project docs](https://worldbank.github.io/ai4data)
package/dist/index.d.mts CHANGED
@@ -343,10 +343,31 @@ interface SearchClientOptions {
343
343
  * ```
344
344
  */
345
345
  workerFactory?: () => Worker;
346
+ /**
347
+ * URL to the worker script (e.g. from a CDN). Use this when loading the package from a CDN
348
+ * so the worker is created via fetch + blob and works cross-origin. Ignored if workerFactory is set.
349
+ *
350
+ * @example
351
+ * ```ts
352
+ * const client = await SearchClient.fromCDN(manifestUrl, {
353
+ * workerUrl: 'https://esm.sh/@ai4data/search@0.1.0/worker'
354
+ * })
355
+ * ```
356
+ */
357
+ workerUrl?: string;
346
358
  }
347
359
  type MessageHandler<T extends WorkerOutboundMessage['type']> = (msg: Extract<WorkerOutboundMessage, {
348
360
  type: T;
349
361
  }>) => void;
362
+ /**
363
+ * Create a Worker from a cross-origin URL by fetching the script and instantiating
364
+ * from a blob URL. Use this when loading the package from a CDN so the worker is
365
+ * same-origin and browsers allow it.
366
+ *
367
+ * @param url - Full URL to the worker script (e.g. https://esm.sh/@ai4data/search@0.1.0/worker)
368
+ * @returns Promise that resolves with the Worker instance
369
+ */
370
+ declare function createWorkerFromUrl(url: string): Promise<Worker>;
350
371
  declare class SearchClient {
351
372
  /** True once the index + BM25 corpus are loaded. Lexical search available. */
352
373
  isIndexReady: boolean;
@@ -401,6 +422,25 @@ declare class SearchClient {
401
422
  * The client is unusable after this call.
402
423
  */
403
424
  destroy(): void;
425
+ /**
426
+ * Create a SearchClient when loading the package from a CDN. Fetches the worker
427
+ * script and creates the worker from a blob URL so it works cross-origin.
428
+ *
429
+ * @param manifestUrl - URL to the collection manifest.json
430
+ * @param opts - Options; must include workerUrl (e.g. https://esm.sh/@ai4data/search@0.1.0/worker)
431
+ * @returns Promise that resolves with the SearchClient
432
+ *
433
+ * @example
434
+ * ```ts
435
+ * const client = await SearchClient.fromCDN('https://example.com/data/manifest.json', {
436
+ * workerUrl: 'https://esm.sh/@ai4data/search@0.1.0/worker'
437
+ * })
438
+ * client.on('results', ({ data }) => console.log(data))
439
+ * ```
440
+ */
441
+ static fromCDN(manifestUrl: string, opts: SearchClientOptions & {
442
+ workerUrl: string;
443
+ }): Promise<SearchClient>;
404
444
  private _handleMessage;
405
445
  }
406
446
 
@@ -690,4 +730,4 @@ declare function l2NormalizeInPlace(vec: Float32Array): Float32Array;
690
730
  */
691
731
  declare function toInt8Array(arr: number[] | Int8Array): Int8Array;
692
732
 
693
- export { type BM25CorpusEntry, type BM25Engine, type CollectionManifest, FlatEngine, type FlatIndexConfig, type FlatItem, type GeographicCoverage, type HNSWConfig, HNSWEngine, type HNSWIndexConfig, HybridSearch, SearchClient, type SearchClientOptions, type SearchEngine, type SearchMode, type SearchOptions, type SearchResult, type SearchStats, type Shard, type ShardNode, type WorkerErrorMessage, type WorkerInboundMessage, type WorkerIndexReadyMessage, type WorkerInitMessage, type WorkerOutboundMessage, type WorkerProgressMessage, type WorkerReadyMessage, type WorkerResultsMessage, type WorkerSearchMessage, dequantize, dotProductMixed, fetchJson, l2NormalizeInPlace, toInt8Array };
733
+ export { type BM25CorpusEntry, type BM25Engine, type CollectionManifest, FlatEngine, type FlatIndexConfig, type FlatItem, type GeographicCoverage, type HNSWConfig, HNSWEngine, type HNSWIndexConfig, HybridSearch, SearchClient, type SearchClientOptions, type SearchEngine, type SearchMode, type SearchOptions, type SearchResult, type SearchStats, type Shard, type ShardNode, type WorkerErrorMessage, type WorkerInboundMessage, type WorkerIndexReadyMessage, type WorkerInitMessage, type WorkerOutboundMessage, type WorkerProgressMessage, type WorkerReadyMessage, type WorkerResultsMessage, type WorkerSearchMessage, createWorkerFromUrl, dequantize, dotProductMixed, fetchJson, l2NormalizeInPlace, toInt8Array };
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- var R=class{constructor(e,t={}){this.isIndexReady=!1;this.isModelReady=!1;this.loadingMessage="Initializing\u2026";this.activeFallback=!1;this.manifest=null;this.handlers=new Map;this.destroyed=!1;this.worker=t.workerFactory?t.workerFactory():new Worker(new URL("./worker.mjs",import.meta.url),{type:"module"}),this.worker.onmessage=s=>{this._handleMessage(s.data)},this.worker.onerror=s=>{console.error("[SearchClient] Worker error:",s),this.loadingMessage="Search worker error"};let n={type:"init",manifestUrl:new URL(e,globalThis.location?.href??"http://localhost/").href,modelId:t.modelId,skipModelLoad:t.skipModelLoad,modelLoadDelaySeconds:t.modelLoadDelaySeconds};this.worker.postMessage(n)}on(e,t){this.handlers.has(e)||this.handlers.set(e,new Set);let r=this.handlers.get(e),n=t;return r.add(n),()=>r.delete(n)}search(e,t={}){this.isIndexReady&&this.worker.postMessage({type:"search",text:e,topK:t.topK??20,ef:t.ef??50,ef_upper:t.ef_upper??2,threshold:t.threshold??0,mode:t.mode??"hybrid"})}getRecent(e=10){this.worker.postMessage({type:"getRecent",limit:e})}ping(){return new Promise(e=>{let t=this.on("pong",()=>{t(),e()});this.worker.postMessage({type:"ping"})})}destroy(){this.destroyed||(this.destroyed=!0,this.worker.terminate(),this.handlers.clear())}_handleMessage(e){switch(e.type){case"progress":this.loadingMessage=e.message;break;case"index_ready":this.isIndexReady=!0;break;case"ready":this.isModelReady=e.modelLoaded!==!1,this.manifest=e.config;break;case"results":this.activeFallback=e.fallback??!1;break;case"error":this.isIndexReady=!0,this.loadingMessage=`Error: ${e.message}`,console.error("[SearchClient] Worker error message:",e.message);break}let t=this.handlers.get(e.type);if(t)for(let r of t)r(e)}};function x(i,e,t){let r=0,n=i.length;for(let s=0;s<n;s++)r+=i[s]*(e[s]*t);return r}function L(i,e){let t=new Float32Array(i.length);for(let r=0;r<i.length;r++)t[r]=i[r]*e;return t}function F(i){let e=0;for(let t=0;t<i.length;t++)e+=i[t]*i[t];if(e=Math.sqrt(e),e<1e-9)return i;for(let t=0;t<i.length;t++)i[t]/=e;return i}function M(i){return new Int8Array(i)}async function S(i,e){let t=e?.cacheName??null,r=i.endsWith(".gz");if(t&&typeof caches<"u")try{let a=await(await caches.open(t)).match(i);if(a)return a.json()}catch{}let n=await fetch(i);if(!n.ok)throw new Error(`fetchJson: HTTP ${n.status}: ${i}`);let s;if(r){let o=n.headers.get("Content-Encoding");if(o==="gzip"||o==="x-gzip")s=await n.json();else{let a=n.body.pipeThrough(new DecompressionStream("gzip"));s=await new Response(a).json()}}else s=await n.json();if(t&&typeof caches<"u")try{(await caches.open(t)).put(i,new Response(JSON.stringify(s),{headers:{"Content-Type":"application/json"}}))}catch{}return s}var I=class{constructor(){this.ready=!1;this.items=[],this.lastStats=null}async load(e){let t=await S(e);return this.items=t.items.map(r=>({...r,qv:M(r.qv)})),this.ready=!0,t.items}search(e,t){if(!this.ready)throw new Error("FlatEngine: not loaded yet");let r=t?.topK??20,n=t?.threshold??0,s=Date.now(),o=new Float32Array(this.items.length);for(let l=0;l<this.items.length;l++){let d=this.items[l];o[l]=x(e,d.qv,d.scale)}let a=[];for(let l=0;l<o.length;l++)o[l]>=n&&a.push(l);a.sort((l,d)=>o[d]-o[l]);let c=a.slice(0,r).map(l=>{let d=this.items[l],p={};for(let[u,h]of Object.entries(d))["id","scale","qv","title","text"].includes(u)||(p[u]=h);return{id:d.id,score:o[l],title:d.title,text:d.text,...p}});return this.lastStats={latencyMs:Date.now()-s,shardsLoaded:0,totalCachedShards:0},c}};var C=class{constructor(e,t="hnsw-shards-v1",r=".json"){this.baseUrl=e.endsWith("/")?e:e+"/",this.cacheName=t,this.shardSuffix=r,this.memoryCache=new Map,this.inflight=new Map,this._insertOrder=[]}async load(e){if(this.memoryCache.has(e))return this.memoryCache.get(e);if(this.inflight.has(e))return this.inflight.get(e);let t=this._fetchShard(e);this.inflight.set(e,t);try{let r=await t;return this.memoryCache.set(e,r),this._insertOrder.push(e),r}finally{this.inflight.delete(e)}}prefetch(e){for(let t of e)!this.memoryCache.has(t)&&!this.inflight.has(t)&&this.load(t)}evict(e=200){for(;this._insertOrder.length>e;){let t=this._insertOrder.shift();this.memoryCache.delete(t)}}_shardUrl(e){return this.baseUrl+`shard_${String(e).padStart(3,"0")}${this.shardSuffix}`}_fetchShard(e){return S(this._shardUrl(e),{cacheName:this.cacheName})}};function w(i,e){let t=e[0],r=0,n=i.length;for(;r<n;){let s=r+n>>>1;i[s][0]<t?r=s+1:n=s}i.splice(r,0,e)}var O=class{constructor(){this.ready=!1;this.config=null,this.upperLayers=null,this.nodeToShard=null,this.loader=null,this.nodeCache=new Map,this.lastStats=null}async init(e,t){let r=t?.cacheName??"hnsw-shards-v1",n=t?.manifest??null,s=e.endsWith("/")?e:e+"/",o=n?.index??{},a=s+(o.config??"index/config.json"),c=s+(o.upper_layers??"index/upper_layers.json"),l=s+(o.node_to_shard??"index/node_to_shard.json"),d=n?.compressed?".json.gz":".json",[p,u,h]=await Promise.all([S(a,{cacheName:r}),S(c,{cacheName:r}),S(l,{cacheName:r})]);this.config=p,this.upperLayers=u,this.nodeToShard=h,this.loader=new C(s+"index/layer0/",r,d);for(let[m,f]of Object.entries(u.nodes))this.nodeCache.set(parseInt(m,10),{id:parseInt(m,10),scale:f.scale,qv:M(f.qv),neighbors:[],layers:f.layers,max_layer:f.max_layer});this.ready=!0}async search(e,t){if(!this.ready||!this.config||!this.upperLayers||!this.loader)throw new Error("HNSWEngine: not initialized. Call init() first.");let r=t?.ef??50,n=t?.ef_upper??2,s=t?.topK??10,o=Date.now(),a=this.loader.memoryCache.size,c=[[this._scoreUpperNode(e,this.upperLayers.entry_node_id),this.upperLayers.entry_node_id]];for(let p=this.config.n_layers-1;p>=1;p--)c=this._beamDescentLayer(e,c,p,n);let l=await this._beamSearchLayer0(e,c,r),d=this.loader.memoryCache.size-a+(this.loader.inflight.size>0?1:0);return this.lastStats={latencyMs:Date.now()-o,shardsLoaded:Math.max(0,d),totalCachedShards:this.loader.memoryCache.size},this.loader.evict(300),l.slice(0,s)}_beamDescentLayer(e,t,r,n){let s=String(r),o=new Set,a=[];for(let[,c]of t){if(o.has(c))continue;o.add(c);let l=this._scoreUpperNode(e,c);w(a,[l,c]),a.length>n&&a.shift();let d=this.nodeCache.get(c);if(!d)continue;let p=d.layers?.[s]??[];for(let u of p){if(o.has(u))continue;o.add(u);let h=this._scoreUpperNode(e,u);w(a,[h,u]),a.length>n&&a.shift()}}return a}_scoreUpperNode(e,t){let r=this.nodeCache.get(t);return r?x(e,r.qv,r.scale):-1/0}async _beamSearchLayer0(e,t,r){let n=new Set,s=[],o=[];for(let[,a]of t){if(n.has(a))continue;n.add(a);let c=await this._getLayer0Node(a);if(!c)continue;let l=x(e,c.qv,c.scale);w(s,[l,a]),w(o,[l,a])}for(s.length>r&&(s=s.slice(-r)),o.length>r&&(o=o.slice(-r));o.length>0;){let[a,c]=o.pop(),l=s.length>=r?s[0][0]:-1/0;if(a<l)break;let d=await this._getLayer0Node(c);if(!d)continue;let p=d.neighbors.filter(h=>!n.has(h)),u=new Set;for(let h of p){let m=this.nodeToShard[String(h)];m!=null&&!this.loader.memoryCache.has(m)&&u.add(m)}u.size>0&&await Promise.all([...u].map(h=>this.loader.load(h)));for(let h of p){n.add(h);let m=await this._getLayer0Node(h);if(!m)continue;let f=x(e,m.qv,m.scale),k=s.length>=r?s[0][0]:-1/0;(f>k||s.length<r)&&(w(o,[f,h]),w(s,[f,h]),s.length>r&&s.shift())}}return s.sort((a,c)=>c[0]-a[0]).map(([a,c])=>({id:c,score:a,title:""}))}async _getLayer0Node(e){let t=this.nodeCache.get(e);if(t?._l0loaded)return t;let r=this.nodeToShard[String(e)];if(r==null)return this.nodeCache.get(e)??null;let n=await this.loader.load(r);for(let s of n.nodes){let o=this.nodeCache.get(s.id),a={...o??{},id:s.id,scale:s.scale,qv:o?.qv??M(s.qv),neighbors:s.neighbors,_l0loaded:!0};this.nodeCache.set(s.id,a)}return this.nodeCache.get(e)??null}};var P=class{constructor(e,t=null,r=null){this.semantic=e,this.bm25=t,this.idToMeta=r}async search(e,t,r){let n=r?.topK??20,s=r?.semanticWeight??.7,o=r?.lexicalWeight??.3,a=r?.ef??50,c=r?.mode??"hybrid",l=n*3,d={topK:l,ef:a},[p,u]=await Promise.all([c!=="lexical"&&e&&this.semantic?Promise.resolve(this.semantic.search(e,d)):Promise.resolve([]),c!=="semantic"&&this.bm25&&t?Promise.resolve(this._runBM25(t,l)):Promise.resolve([])]),h=await p,m=await u;if(c==="semantic")return this._formatResults(h,n,"semantic");if(c==="lexical")return this._formatResults(m,n,"lexical");let f=new Map;if(h.length>0){let y=h[0].score||1,b=h[h.length-1].score||0,_=y-b||1;for(let g of h){let v=(g.score-b)/_;f.set(String(g.id),{...g,title:g.title,semanticScore:v,lexicalScore:0,rawSemanticScore:g.score})}}if(m.length>0){let y=m[0].score||1,b=m[m.length-1].score||0,_=y-b||1;for(let g of m){let v=(g.score-b)/_,W=String(g.id);if(f.has(W))f.get(W).lexicalScore=v;else{let N=this.idToMeta?this.idToMeta(g.id):{};f.set(W,{...g,...N,id:g.id,title:N.title??g.title??"",text:N.text??g.text??"",semanticScore:0,lexicalScore:v,rawSemanticScore:0})}}}let k=[...f.values()].map(y=>({...y,score:s*y.semanticScore+o*y.lexicalScore}));return k.sort((y,b)=>b.score-y.score),k.slice(0,n)}_runBM25(e,t){if(!this.bm25)return[];try{return this.bm25.search(e,t).map(([n,s])=>{let o=this.idToMeta?this.idToMeta(n):{};return{...o,id:n,score:s,title:o.title??"",text:o.text??""}})}catch(r){return console.warn("BM25 search error:",r),[]}}_formatResults(e,t,r){return e.slice(0,t).map(n=>({...n,semanticScore:r==="semantic"?n.score??0:0,lexicalScore:r==="lexical"?n.score??0:0}))}};export{I as FlatEngine,O as HNSWEngine,P as HybridSearch,R as SearchClient,L as dequantize,x as dotProductMixed,S as fetchJson,F as l2NormalizeInPlace,M as toInt8Array};
1
+ function F(i){return fetch(i,{mode:"cors"}).then(e=>{if(!e.ok)throw new Error(`Failed to fetch worker: ${e.status} ${e.statusText}`);return e.text()}).then(e=>{let t=new Blob([e],{type:"application/javascript"}),r=URL.createObjectURL(t);return new Worker(r,{type:"module"})})}var R=class i{constructor(e,t={}){this.isIndexReady=!1;this.isModelReady=!1;this.loadingMessage="Initializing\u2026";this.activeFallback=!1;this.manifest=null;this.handlers=new Map;this.destroyed=!1;if(t.workerFactory)this.worker=t.workerFactory();else{if(t.workerUrl)throw new Error("SearchClient: workerUrl is only supported with SearchClient.fromCDN(). Use fromCDN(manifestUrl, { workerUrl }) or pass workerFactory.");this.worker=new Worker(new URL("./worker.mjs",import.meta.url),{type:"module"})}this.worker.onmessage=s=>{this._handleMessage(s.data)},this.worker.onerror=s=>{console.error("[SearchClient] Worker error:",s),this.loadingMessage="Search worker error"};let o={type:"init",manifestUrl:new URL(e,globalThis.location?.href??"http://localhost/").href,modelId:t.modelId,skipModelLoad:t.skipModelLoad,modelLoadDelaySeconds:t.modelLoadDelaySeconds};this.worker.postMessage(o)}on(e,t){this.handlers.has(e)||this.handlers.set(e,new Set);let r=this.handlers.get(e),o=t;return r.add(o),()=>r.delete(o)}search(e,t={}){this.isIndexReady&&this.worker.postMessage({type:"search",text:e,topK:t.topK??20,ef:t.ef??50,ef_upper:t.ef_upper??2,threshold:t.threshold??0,mode:t.mode??"hybrid"})}getRecent(e=10){this.worker.postMessage({type:"getRecent",limit:e})}ping(){return new Promise(e=>{let t=this.on("pong",()=>{t(),e()});this.worker.postMessage({type:"ping"})})}destroy(){this.destroyed||(this.destroyed=!0,this.worker.terminate(),this.handlers.clear())}static fromCDN(e,t){let{workerUrl:r,...o}=t;return F(r).then(s=>new i(e,{...o,workerFactory:()=>s}))}_handleMessage(e){switch(e.type){case"progress":this.loadingMessage=e.message;break;case"index_ready":this.isIndexReady=!0;break;case"ready":this.isModelReady=e.modelLoaded!==!1,this.manifest=e.config;break;case"results":this.activeFallback=e.fallback??!1;break;case"error":this.isIndexReady=!0,this.loadingMessage=`Error: ${e.message}`,console.error("[SearchClient] Worker error message:",e.message);break}let t=this.handlers.get(e.type);if(t)for(let r of t)r(e)}};function w(i,e,t){let r=0,o=i.length;for(let s=0;s<o;s++)r+=i[s]*(e[s]*t);return r}function L(i,e){let t=new Float32Array(i.length);for(let r=0;r<i.length;r++)t[r]=i[r]*e;return t}function E(i){let e=0;for(let t=0;t<i.length;t++)e+=i[t]*i[t];if(e=Math.sqrt(e),e<1e-9)return i;for(let t=0;t<i.length;t++)i[t]/=e;return i}function x(i){return new Int8Array(i)}async function S(i,e){let t=e?.cacheName??null,r=i.endsWith(".gz");if(t&&typeof caches<"u")try{let a=await(await caches.open(t)).match(i);if(a)return a.json()}catch{}let o=await fetch(i);if(!o.ok)throw new Error(`fetchJson: HTTP ${o.status}: ${i}`);let s;if(r){let n=o.headers.get("Content-Encoding");if(n==="gzip"||n==="x-gzip")s=await o.json();else{let a=o.body.pipeThrough(new DecompressionStream("gzip"));s=await new Response(a).json()}}else s=await o.json();if(t&&typeof caches<"u")try{(await caches.open(t)).put(i,new Response(JSON.stringify(s),{headers:{"Content-Type":"application/json"}}))}catch{}return s}var I=class{constructor(){this.ready=!1;this.items=[],this.lastStats=null}async load(e){let t=await S(e);return this.items=t.items.map(r=>({...r,qv:x(r.qv)})),this.ready=!0,t.items}search(e,t){if(!this.ready)throw new Error("FlatEngine: not loaded yet");let r=t?.topK??20,o=t?.threshold??0,s=Date.now(),n=new Float32Array(this.items.length);for(let l=0;l<this.items.length;l++){let d=this.items[l];n[l]=w(e,d.qv,d.scale)}let a=[];for(let l=0;l<n.length;l++)n[l]>=o&&a.push(l);a.sort((l,d)=>n[d]-n[l]);let c=a.slice(0,r).map(l=>{let d=this.items[l],f={};for(let[u,h]of Object.entries(d))["id","scale","qv","title","text"].includes(u)||(f[u]=h);return{id:d.id,score:n[l],title:d.title,text:d.text,...f}});return this.lastStats={latencyMs:Date.now()-s,shardsLoaded:0,totalCachedShards:0},c}};var v=class{constructor(e,t="hnsw-shards-v1",r=".json"){this.baseUrl=e.endsWith("/")?e:e+"/",this.cacheName=t,this.shardSuffix=r,this.memoryCache=new Map,this.inflight=new Map,this._insertOrder=[]}async load(e){if(this.memoryCache.has(e))return this.memoryCache.get(e);if(this.inflight.has(e))return this.inflight.get(e);let t=this._fetchShard(e);this.inflight.set(e,t);try{let r=await t;return this.memoryCache.set(e,r),this._insertOrder.push(e),r}finally{this.inflight.delete(e)}}prefetch(e){for(let t of e)!this.memoryCache.has(t)&&!this.inflight.has(t)&&this.load(t)}evict(e=200){for(;this._insertOrder.length>e;){let t=this._insertOrder.shift();this.memoryCache.delete(t)}}_shardUrl(e){return this.baseUrl+`shard_${String(e).padStart(3,"0")}${this.shardSuffix}`}_fetchShard(e){return S(this._shardUrl(e),{cacheName:this.cacheName})}};function M(i,e){let t=e[0],r=0,o=i.length;for(;r<o;){let s=r+o>>>1;i[s][0]<t?r=s+1:o=s}i.splice(r,0,e)}var O=class{constructor(){this.ready=!1;this.config=null,this.upperLayers=null,this.nodeToShard=null,this.loader=null,this.nodeCache=new Map,this.lastStats=null}async init(e,t){let r=t?.cacheName??"hnsw-shards-v1",o=t?.manifest??null,s=e.endsWith("/")?e:e+"/",n=o?.index??{},a=s+(n.config??"index/config.json"),c=s+(n.upper_layers??"index/upper_layers.json"),l=s+(n.node_to_shard??"index/node_to_shard.json"),d=o?.compressed?".json.gz":".json",[f,u,h]=await Promise.all([S(a,{cacheName:r}),S(c,{cacheName:r}),S(l,{cacheName:r})]);this.config=f,this.upperLayers=u,this.nodeToShard=h,this.loader=new v(s+"index/layer0/",r,d);for(let[m,p]of Object.entries(u.nodes))this.nodeCache.set(parseInt(m,10),{id:parseInt(m,10),scale:p.scale,qv:x(p.qv),neighbors:[],layers:p.layers,max_layer:p.max_layer});this.ready=!0}async search(e,t){if(!this.ready||!this.config||!this.upperLayers||!this.loader)throw new Error("HNSWEngine: not initialized. Call init() first.");let r=t?.ef??50,o=t?.ef_upper??2,s=t?.topK??10,n=Date.now(),a=this.loader.memoryCache.size,c=[[this._scoreUpperNode(e,this.upperLayers.entry_node_id),this.upperLayers.entry_node_id]];for(let f=this.config.n_layers-1;f>=1;f--)c=this._beamDescentLayer(e,c,f,o);let l=await this._beamSearchLayer0(e,c,r),d=this.loader.memoryCache.size-a+(this.loader.inflight.size>0?1:0);return this.lastStats={latencyMs:Date.now()-n,shardsLoaded:Math.max(0,d),totalCachedShards:this.loader.memoryCache.size},this.loader.evict(300),l.slice(0,s)}_beamDescentLayer(e,t,r,o){let s=String(r),n=new Set,a=[];for(let[,c]of t){if(n.has(c))continue;n.add(c);let l=this._scoreUpperNode(e,c);M(a,[l,c]),a.length>o&&a.shift();let d=this.nodeCache.get(c);if(!d)continue;let f=d.layers?.[s]??[];for(let u of f){if(n.has(u))continue;n.add(u);let h=this._scoreUpperNode(e,u);M(a,[h,u]),a.length>o&&a.shift()}}return a}_scoreUpperNode(e,t){let r=this.nodeCache.get(t);return r?w(e,r.qv,r.scale):-1/0}async _beamSearchLayer0(e,t,r){let o=new Set,s=[],n=[];for(let[,a]of t){if(o.has(a))continue;o.add(a);let c=await this._getLayer0Node(a);if(!c)continue;let l=w(e,c.qv,c.scale);M(s,[l,a]),M(n,[l,a])}for(s.length>r&&(s=s.slice(-r)),n.length>r&&(n=n.slice(-r));n.length>0;){let[a,c]=n.pop(),l=s.length>=r?s[0][0]:-1/0;if(a<l)break;let d=await this._getLayer0Node(c);if(!d)continue;let f=d.neighbors.filter(h=>!o.has(h)),u=new Set;for(let h of f){let m=this.nodeToShard[String(h)];m!=null&&!this.loader.memoryCache.has(m)&&u.add(m)}u.size>0&&await Promise.all([...u].map(h=>this.loader.load(h)));for(let h of f){o.add(h);let m=await this._getLayer0Node(h);if(!m)continue;let p=w(e,m.qv,m.scale),k=s.length>=r?s[0][0]:-1/0;(p>k||s.length<r)&&(M(n,[p,h]),M(s,[p,h]),s.length>r&&s.shift())}}return s.sort((a,c)=>c[0]-a[0]).map(([a,c])=>({id:c,score:a,title:""}))}async _getLayer0Node(e){let t=this.nodeCache.get(e);if(t?._l0loaded)return t;let r=this.nodeToShard[String(e)];if(r==null)return this.nodeCache.get(e)??null;let o=await this.loader.load(r);for(let s of o.nodes){let n=this.nodeCache.get(s.id),a={...n??{},id:s.id,scale:s.scale,qv:n?.qv??x(s.qv),neighbors:s.neighbors,_l0loaded:!0};this.nodeCache.set(s.id,a)}return this.nodeCache.get(e)??null}};var P=class{constructor(e,t=null,r=null){this.semantic=e,this.bm25=t,this.idToMeta=r}async search(e,t,r){let o=r?.topK??20,s=r?.semanticWeight??.7,n=r?.lexicalWeight??.3,a=r?.ef??50,c=r?.mode??"hybrid",l=o*3,d={topK:l,ef:a},[f,u]=await Promise.all([c!=="lexical"&&e&&this.semantic?Promise.resolve(this.semantic.search(e,d)):Promise.resolve([]),c!=="semantic"&&this.bm25&&t?Promise.resolve(this._runBM25(t,l)):Promise.resolve([])]),h=await f,m=await u;if(c==="semantic")return this._formatResults(h,o,"semantic");if(c==="lexical")return this._formatResults(m,o,"lexical");let p=new Map;if(h.length>0){let y=h[0].score||1,b=h[h.length-1].score||0,W=y-b||1;for(let g of h){let C=(g.score-b)/W;p.set(String(g.id),{...g,title:g.title,semanticScore:C,lexicalScore:0,rawSemanticScore:g.score})}}if(m.length>0){let y=m[0].score||1,b=m[m.length-1].score||0,W=y-b||1;for(let g of m){let C=(g.score-b)/W,N=String(g.id);if(p.has(N))p.get(N).lexicalScore=C;else{let _=this.idToMeta?this.idToMeta(g.id):{};p.set(N,{...g,..._,id:g.id,title:_.title??g.title??"",text:_.text??g.text??"",semanticScore:0,lexicalScore:C,rawSemanticScore:0})}}}let k=[...p.values()].map(y=>({...y,score:s*y.semanticScore+n*y.lexicalScore}));return k.sort((y,b)=>b.score-y.score),k.slice(0,o)}_runBM25(e,t){if(!this.bm25)return[];try{return this.bm25.search(e,t).map(([o,s])=>{let n=this.idToMeta?this.idToMeta(o):{};return{...n,id:o,score:s,title:n.title??"",text:n.text??""}})}catch(r){return console.warn("BM25 search error:",r),[]}}_formatResults(e,t,r){return e.slice(0,t).map(o=>({...o,semanticScore:r==="semantic"?o.score??0:0,lexicalScore:r==="lexical"?o.score??0:0}))}};export{I as FlatEngine,O as HNSWEngine,P as HybridSearch,R as SearchClient,F as createWorkerFromUrl,L as dequantize,w as dotProductMixed,S as fetchJson,E as l2NormalizeInPlace,x as toInt8Array};
2
2
  //# sourceMappingURL=index.mjs.map
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/client/SearchClient.ts","../src/engine/int8-codec.ts","../src/engine/fetch-json.ts","../src/engine/flat-engine.ts","../src/engine/shard-loader.ts","../src/engine/hnsw-engine.ts","../src/engine/hybrid-search.ts"],"sourcesContent":["/**\n * SearchClient β€” framework-agnostic wrapper around the search Web Worker.\n *\n * Works in any JavaScript environment that supports Web Workers.\n * Extend or wrap with framework adapters (see adapters/vue.ts, adapters/react.ts).\n *\n * @example\n * ```ts\n * const client = new SearchClient('https://example.com/data/prwp/manifest.json')\n *\n * client.on('index_ready', () => {\n * client.search('climate finance', { topK: 10, mode: 'hybrid' })\n * })\n *\n * client.on('results', ({ data, stats }) => {\n * console.log(data) // SearchResult[]\n * console.log(stats) // SearchStats | null\n * })\n *\n * // Clean up when done\n * client.destroy()\n * ```\n */\n\nimport type { WorkerOutboundMessage, WorkerInboundMessage } from '../types/worker'\nimport type { CollectionManifest } from '../types/manifest'\nimport type { SearchOptions } from '../types/search'\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type SearchMode = 'semantic' | 'lexical' | 'hybrid'\n\nexport interface SearchClientOptions {\n /** HuggingFace model ID to use for embeddings (default: avsolatorio/GIST-small-Embedding-v0) */\n modelId?: string\n /** If true, skip loading the embedding model (for testing BM25 fallback). */\n skipModelLoad?: boolean\n /** Delay (seconds) before loading the embedding model; index + BM25 load first (for testing). */\n modelLoadDelaySeconds?: number\n /**\n * Factory function that creates the Web Worker.\n * Defaults to the bundled search worker created via `new URL()`.\n * Override when you need a custom worker path (e.g. CDN, service worker proxy).\n *\n * @example\n * ```ts\n * // Vite / webpack 5 (recommended β€” bundler resolves the path)\n * new SearchClient(url, {\n * workerFactory: () => new Worker(new URL('@ai4data/search/worker', import.meta.url), { type: 'module' })\n * })\n * ```\n */\n workerFactory?: () => Worker\n}\n\ntype MessageHandler<T extends WorkerOutboundMessage['type']> = (\n msg: Extract<WorkerOutboundMessage, { type: T }>,\n) => void\n\n// ── SearchClient ──────────────────────────────────────────────────────────────\n\nexport class SearchClient {\n // ── Public state (plain properties β€” no reactivity) ──\n\n /** True once the index + BM25 corpus are loaded. Lexical search available. */\n isIndexReady = false\n\n /** True once the ONNX embedding model is ready. Semantic + hybrid search available. */\n isModelReady = false\n\n /** Latest progress/status message from the worker. */\n loadingMessage = 'Initializing…'\n\n /** True when the last search fell back to BM25 because the model wasn't ready. */\n activeFallback = false\n\n /** Parsed collection manifest, available after `index_ready`. */\n manifest: CollectionManifest | null = null\n\n // ── Private ──\n\n private readonly worker: Worker\n private readonly handlers = new Map<string, Set<(msg: WorkerOutboundMessage) => void>>()\n private destroyed = false\n\n // ── Constructor ──────────────────────────────────────────────────────────────\n\n /**\n * @param manifestUrl - Absolute or relative URL to `manifest.json`.\n * Relative URLs are resolved against `location.href`.\n * @param opts - Optional configuration.\n */\n constructor(manifestUrl: string, opts: SearchClientOptions = {}) {\n this.worker = opts.workerFactory\n ? opts.workerFactory()\n : new Worker(new URL('./worker.mjs', import.meta.url), { type: 'module' })\n\n this.worker.onmessage = (e: MessageEvent<WorkerOutboundMessage>) => {\n this._handleMessage(e.data)\n }\n\n this.worker.onerror = (err) => {\n console.error('[SearchClient] Worker error:', err)\n this.loadingMessage = 'Search worker error'\n }\n\n // Resolve relative manifest URLs against current page origin\n const resolvedUrl = new URL(\n manifestUrl,\n globalThis.location?.href ?? 'http://localhost/',\n ).href\n\n const initMsg: WorkerInboundMessage = {\n type: 'init',\n manifestUrl: resolvedUrl,\n modelId: opts.modelId,\n skipModelLoad: opts.skipModelLoad,\n modelLoadDelaySeconds: opts.modelLoadDelaySeconds,\n }\n this.worker.postMessage(initMsg)\n }\n\n // ── Event bus ─────────────────────────────────────────────────────────────────\n\n /**\n * Subscribe to a specific worker message type.\n * Returns an unsubscribe function β€” call it to remove the handler.\n *\n * @example\n * ```ts\n * const off = client.on('results', ({ data }) => setResults(data))\n * // later…\n * off()\n * ```\n */\n on<T extends WorkerOutboundMessage['type']>(\n type: T,\n handler: MessageHandler<T>,\n ): () => void {\n if (!this.handlers.has(type)) this.handlers.set(type, new Set())\n const bucket = this.handlers.get(type)!\n const h = handler as (msg: WorkerOutboundMessage) => void\n bucket.add(h)\n return () => bucket.delete(h)\n }\n\n // ── Actions ───────────────────────────────────────────────────────────────────\n\n /**\n * Submit a search query. No-op if the index is not yet ready.\n *\n * @param text - Natural-language query\n * @param opts - Optional topK, ef, mode ('semantic' | 'lexical' | 'hybrid')\n */\n search(text: string, opts: SearchOptions & { mode?: SearchMode } = {}): void {\n if (!this.isIndexReady) return\n this.worker.postMessage({\n type: 'search',\n text,\n topK: opts.topK ?? 20,\n ef: opts.ef ?? 50,\n ef_upper: opts.ef_upper ?? 2,\n threshold: opts.threshold ?? 0.0,\n mode: opts.mode ?? 'hybrid',\n } satisfies WorkerInboundMessage)\n }\n\n /**\n * Fetch the most-recent items from the index (useful for pre-search state).\n */\n getRecent(limit = 10): void {\n this.worker.postMessage({ type: 'getRecent', limit } satisfies WorkerInboundMessage)\n }\n\n /**\n * Ping the worker. Resolves when the worker responds with 'pong'.\n */\n ping(): Promise<void> {\n return new Promise((resolve) => {\n const off = this.on('pong', () => { off(); resolve() })\n this.worker.postMessage({ type: 'ping' } satisfies WorkerInboundMessage)\n })\n }\n\n /**\n * Terminate the worker and clean up all event listeners.\n * The client is unusable after this call.\n */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.worker.terminate()\n this.handlers.clear()\n }\n\n // ── Internal ──────────────────────────────────────────────────────────────────\n\n private _handleMessage(msg: WorkerOutboundMessage): void {\n // Update plain-property state\n switch (msg.type) {\n case 'progress':\n this.loadingMessage = msg.message\n break\n case 'index_ready':\n this.isIndexReady = true\n break\n case 'ready':\n this.isModelReady = msg.modelLoaded !== false\n this.manifest = msg.config\n break\n case 'results':\n this.activeFallback = msg.fallback ?? false\n break\n case 'error':\n this.isIndexReady = true // exit loading state on error\n this.loadingMessage = `Error: ${msg.message}`\n console.error('[SearchClient] Worker error message:', msg.message)\n break\n }\n\n // Dispatch to registered handlers\n const bucket = this.handlers.get(msg.type)\n if (bucket) {\n for (const h of bucket) h(msg)\n }\n }\n}\n","/**\n * int8-codec.ts\n *\n * Quantization scheme: vectors are stored as Int8 values in [-127, 127].\n * Each vector is accompanied by a scalar `scale` such that the original\n * float value β‰ˆ int8_value * scale. Dot products are computed in mixed\n * precision (Float32 query Γ— dequantized Int8 stored vector) to keep\n * both accuracy and memory efficiency.\n */\n\n/**\n * Compute the dot product between a Float32 query vector and a stored\n * Int8-quantized vector, dequantizing on the fly.\n *\n * @param queryF32 - L2-normalised query vector (Float32Array)\n * @param storedQV - Int8-quantized stored vector\n * @param storedScale - Per-vector dequantization scale factor\n * @returns Approximate cosine similarity score\n */\nexport function dotProductMixed(\n queryF32: Float32Array,\n storedQV: Int8Array,\n storedScale: number,\n): number {\n let dot = 0.0;\n const len = queryF32.length;\n for (let i = 0; i < len; i++) {\n dot += queryF32[i] * (storedQV[i] * storedScale);\n }\n return dot;\n}\n\n/**\n * Dequantize an Int8 vector back to Float32 using the stored scale.\n *\n * @param qv - Int8-quantized vector\n * @param scale - Per-vector dequantization scale factor\n * @returns Reconstructed Float32 vector\n */\nexport function dequantize(qv: Int8Array, scale: number): Float32Array {\n const out = new Float32Array(qv.length);\n for (let i = 0; i < qv.length; i++) {\n out[i] = qv[i] * scale;\n }\n return out;\n}\n\n/**\n * L2-normalise a Float32 vector in place.\n * Vectors whose norm is below 1e-9 are left unchanged to avoid division by zero.\n *\n * @param vec - Vector to normalise (mutated in place)\n * @returns The same (now normalised) vector\n */\nexport function l2NormalizeInPlace(vec: Float32Array): Float32Array {\n let norm = 0.0;\n for (let i = 0; i < vec.length; i++) norm += vec[i] * vec[i];\n norm = Math.sqrt(norm);\n if (norm < 1e-9) return vec;\n for (let i = 0; i < vec.length; i++) vec[i] /= norm;\n return vec;\n}\n\n/**\n * Convert a plain number array (or an existing Int8Array) to an Int8Array.\n * Values outside [-128, 127] are silently truncated by the typed-array constructor.\n *\n * @param arr - Source values\n * @returns An Int8Array view / copy of the input\n */\nexport function toInt8Array(arr: number[] | Int8Array): Int8Array {\n return new Int8Array(arr);\n}\n","/**\n * fetch-json.ts\n *\n * Utility for fetching JSON (plain or gzip-compressed) with optional\n * Cache Storage read/write so repeat cold-starts skip the network.\n */\n\nexport interface FetchJsonOptions {\n /**\n * When provided, the response is read from (and written to) a named\n * Cache Storage bucket. Pass `null` to disable caching entirely.\n */\n cacheName?: string | null\n}\n\n/**\n * Fetch a JSON resource, transparently handling gzip-compressed responses.\n *\n * Caching behaviour:\n * 1. If `cacheName` is set and the Cache API is available, attempt a cache hit.\n * 2. On a miss, fetch from the network.\n * 3. Decompress if the URL ends with `.gz` and the server did not already\n * decompress it (i.e. `Content-Encoding` is absent or non-gzip).\n * 4. Write the parsed object back to Cache Storage for future requests.\n *\n * @param url - Absolute or relative URL to fetch\n * @param opts - Optional caching configuration\n * @returns Parsed JSON payload cast to `T`\n * @throws {Error} On non-2xx HTTP responses\n */\nexport async function fetchJson<T = unknown>(\n url: string,\n opts?: FetchJsonOptions,\n): Promise<T> {\n const cacheName = opts?.cacheName ?? null;\n const isGz = url.endsWith('.gz');\n\n // --- cache read ---\n if (cacheName && typeof caches !== 'undefined') {\n try {\n const cache = await caches.open(cacheName);\n const cached = await cache.match(url);\n if (cached) {\n return cached.json() as Promise<T>;\n }\n } catch (_) {\n // Cache API unavailable or denied β€” fall through to network fetch\n }\n }\n\n // --- network fetch ---\n const resp = await fetch(url);\n if (!resp.ok) {\n throw new Error(`fetchJson: HTTP ${resp.status}: ${url}`);\n }\n\n let data: T;\n\n if (isGz) {\n const encoding = resp.headers.get('Content-Encoding');\n if (encoding === 'gzip' || encoding === 'x-gzip') {\n // The server already decompressed it for us\n data = (await resp.json()) as T;\n } else {\n // Decompress manually in the browser via DecompressionStream\n const stream = resp.body!.pipeThrough(new DecompressionStream('gzip'));\n data = (await new Response(stream).json()) as T;\n }\n } else {\n data = (await resp.json()) as T;\n }\n\n // --- cache write ---\n if (cacheName && typeof caches !== 'undefined') {\n try {\n const cache = await caches.open(cacheName);\n cache.put(\n url,\n new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json' },\n }),\n );\n } catch (_) {\n // Best-effort β€” ignore write failures\n }\n }\n\n return data;\n}\n","/**\n * flat-engine.ts\n *\n * Brute-force (flat) search engine backed by an Int8-quantized index.\n * Suitable for collections up to ~50 k documents where exact nearest-neighbour\n * search is fast enough without an ANN index structure.\n */\n\nimport type { FlatItem, SearchEngine, SearchOptions, SearchResult, SearchStats } from '../types/search'\nimport { dotProductMixed, toInt8Array } from './int8-codec'\nimport { fetchJson } from './fetch-json'\n\n/** Shape of the JSON file loaded by `FlatEngine.load()` */\ninterface FlatIndexFile {\n dim: number\n items: FlatItem[]\n}\n\n/** Internal representation β€” `qv` is always an `Int8Array` after loading. Defined\n * explicitly (not via `Omit<FlatItem, 'qv'>`) to avoid `[key: string]: unknown`\n * index-signature narrowing that makes named properties return `unknown`. */\ninterface LoadedFlatItem {\n id: string | number\n idno?: string\n title: string\n text: string\n scale: number\n qv: Int8Array\n type?: string\n [key: string]: unknown\n}\n\n/**\n * Brute-force semantic search engine.\n *\n * Usage:\n * ```ts\n * const engine = new FlatEngine()\n * await engine.load('/data/flat/embeddings.int8.json')\n * const results = engine.search(queryVec, { topK: 10 })\n * ```\n */\nexport class FlatEngine implements SearchEngine {\n /** Internal item list with Int8-converted vectors */\n private items: LoadedFlatItem[]\n\n /** True once `load()` has completed successfully */\n readonly ready: boolean = false\n\n /** Statistics from the most recent `search()` call, or `null` before first search */\n lastStats: SearchStats | null\n\n constructor() {\n this.items = []\n this.lastStats = null\n }\n\n /**\n * Fetch and parse the flat index file, converting all `qv` arrays to `Int8Array`.\n *\n * @param url - URL of the `embeddings.int8.json` index file\n * @returns The raw item list from the JSON (before Int8 conversion)\n */\n async load(url: string): Promise<FlatItem[]> {\n const data = await fetchJson<FlatIndexFile>(url)\n this.items = data.items.map(item => ({\n ...item,\n qv: toInt8Array(item.qv as number[]),\n }))\n ;(this as { ready: boolean }).ready = true\n return data.items\n }\n\n /**\n * Run a brute-force cosine-similarity search over all loaded items.\n *\n * @param queryVec - L2-normalised query embedding (Float32Array)\n * @param opts - Optional search parameters\n * @returns Top-K results sorted by descending score\n * @throws {Error} If called before `load()` has completed\n */\n search(queryVec: Float32Array, opts?: SearchOptions): SearchResult[] {\n if (!this.ready) throw new Error('FlatEngine: not loaded yet')\n\n const topK = opts?.topK ?? 20\n const threshold = opts?.threshold ?? 0.0\n\n const t0 = Date.now()\n const scores = new Float32Array(this.items.length)\n\n for (let i = 0; i < this.items.length; i++) {\n const item = this.items[i]\n scores[i] = dotProductMixed(queryVec, item.qv, item.scale)\n }\n\n // Collect candidate indices above threshold\n const candidates: number[] = []\n for (let i = 0; i < scores.length; i++) {\n if (scores[i] >= threshold) candidates.push(i)\n }\n\n candidates.sort((a, b) => scores[b] - scores[a])\n\n const results = candidates.slice(0, topK).map(i => {\n const item = this.items[i]\n // Include all preview fields except internal-only ones\n const extra: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(item)) {\n if (!['id', 'scale', 'qv', 'title', 'text'].includes(k)) {\n extra[k] = v\n }\n }\n return {\n id: item.id,\n score: scores[i],\n title: item.title,\n text: item.text,\n ...extra,\n } as SearchResult\n })\n\n this.lastStats = {\n latencyMs: Date.now() - t0,\n shardsLoaded: 0,\n totalCachedShards: 0,\n }\n\n return results\n }\n}\n","/**\n * shard-loader.ts\n *\n * Loads layer-0 shard files on demand, deduplicates in-flight requests,\n * and maintains a bounded in-memory cache to limit worker heap growth.\n */\n\nimport type { Shard } from '../types/search'\nimport { fetchJson } from './fetch-json'\n\n/**\n * Loads, deduplicates, and caches HNSW layer-0 shard files.\n *\n * Shards are stored in files named `shard_NNN<suffix>` (e.g. `shard_007.json`)\n * under a common base URL. The loader combines three levels of caching:\n *\n * 1. In-memory `Map` β€” fastest; survives across queries within the same worker.\n * 2. In-flight deduplication β€” a second caller for the same shard awaits the\n * already-running fetch rather than issuing a duplicate request.\n * 3. Cache Storage (via `fetchJson`) β€” survives page reloads.\n */\nexport class ShardLoader {\n /** Base URL for shard files (always ends with `/`) */\n readonly baseUrl: string\n /** Cache Storage bucket name passed through to `fetchJson` */\n readonly cacheName: string\n /** File extension appended to each shard filename (e.g. `.json` or `.json.gz`) */\n readonly shardSuffix: string\n\n /** In-memory shard cache keyed by numeric shard ID */\n memoryCache: Map<number, Shard>\n /** Promises for shards currently being fetched, keyed by numeric shard ID */\n inflight: Map<number, Promise<Shard>>\n /** Insertion-order log used by `evict()` to expire the oldest entries first */\n _insertOrder: number[]\n\n constructor(\n baseUrl: string,\n cacheName = 'hnsw-shards-v1',\n shardSuffix = '.json',\n ) {\n this.baseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'\n this.cacheName = cacheName\n this.shardSuffix = shardSuffix\n this.memoryCache = new Map()\n this.inflight = new Map()\n this._insertOrder = []\n }\n\n /**\n * Load a shard by ID, returning the in-memory copy if already cached,\n * joining an in-flight fetch if one exists, or issuing a new network request.\n *\n * @param shardId - Numeric shard identifier\n * @returns Parsed shard data\n */\n async load(shardId: number): Promise<Shard> {\n if (this.memoryCache.has(shardId)) {\n return this.memoryCache.get(shardId)!\n }\n if (this.inflight.has(shardId)) {\n return this.inflight.get(shardId)!\n }\n\n const promise = this._fetchShard(shardId)\n this.inflight.set(shardId, promise)\n\n try {\n const data = await promise\n this.memoryCache.set(shardId, data)\n this._insertOrder.push(shardId)\n return data\n } finally {\n this.inflight.delete(shardId)\n }\n }\n\n /**\n * Kick off background loads for a set of shard IDs without awaiting them.\n * Useful for prefetching neighbours that will likely be needed soon.\n *\n * @param shardIds - Shard IDs to prefetch\n */\n prefetch(shardIds: number[]): void {\n for (const sid of shardIds) {\n if (!this.memoryCache.has(sid) && !this.inflight.has(sid)) {\n this.load(sid)\n }\n }\n }\n\n /**\n * Evict the oldest in-memory shard entries to keep heap usage bounded.\n *\n * @param maxEntries - Maximum number of shards to keep (default: 200)\n */\n evict(maxEntries = 200): void {\n while (this._insertOrder.length > maxEntries) {\n const oldest = this._insertOrder.shift()!\n this.memoryCache.delete(oldest)\n }\n }\n\n /**\n * Build the URL for a given shard ID.\n *\n * @param shardId - Numeric shard identifier\n * @returns Full URL string\n */\n private _shardUrl(shardId: number): string {\n return this.baseUrl + `shard_${String(shardId).padStart(3, '0')}${this.shardSuffix}`\n }\n\n /**\n * Fetch and parse a shard file from the network (or Cache Storage).\n *\n * @param shardId - Numeric shard identifier\n * @returns Parsed shard data\n */\n private _fetchShard(shardId: number): Promise<Shard> {\n return fetchJson<Shard>(this._shardUrl(shardId), { cacheName: this.cacheName })\n }\n}\n","/**\n * hnsw-engine.ts\n *\n * Approximate nearest-neighbour search using a pre-built HNSW index.\n * Upper layers (layers β‰₯ 1) are held entirely in memory; layer-0 is\n * loaded on demand from shard files via `ShardLoader`.\n */\n\nimport type { HNSWConfig, HNSWIndexConfig, UpperLayersData, CollectionManifest } from '../types/manifest'\nimport type {\n NodeCacheEntry,\n ScoredNode,\n SearchEngine,\n SearchResult,\n SearchStats,\n SearchOptions,\n} from '../types/search'\nimport { dotProductMixed, toInt8Array } from './int8-codec'\nimport { ShardLoader } from './shard-loader'\nimport { fetchJson } from './fetch-json'\n\n/** Options accepted by `HNSWEngine.init()` */\ninterface HNSWInitOptions {\n /** Cache Storage bucket name forwarded to `ShardLoader` and `fetchJson` */\n cacheName?: string\n /** Parsed manifest; used to resolve index file paths and compressed flag */\n manifest?: CollectionManifest | null\n}\n\n/**\n * Insert `item` into `arr` in ascending score order (binary search).\n * `arr[0]` is always the lowest-scoring element after insertion.\n *\n * @param arr - Sorted array to insert into (mutated in place)\n * @param item - `[score, nodeId]` tuple to insert\n */\nfunction _sortedInsert(arr: ScoredNode[], item: ScoredNode): void {\n const score = item[0]\n let lo = 0\n let hi = arr.length\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (arr[mid][0] < score) lo = mid + 1\n else hi = mid\n }\n arr.splice(lo, 0, item)\n}\n\n/**\n * HNSW approximate nearest-neighbour search engine.\n *\n * Typical usage:\n * ```ts\n * const engine = new HNSWEngine()\n * await engine.init('/data/prwp/')\n * const results = await engine.search(queryVec, { topK: 10, ef: 50 })\n * ```\n */\nexport class HNSWEngine implements SearchEngine {\n /** Parsed `index/config.json` */\n private config: HNSWConfig | null\n /** Parsed `index/upper_layers.json` */\n private upperLayers: UpperLayersData | null\n /** Maps string node ID β†’ shard ID */\n private nodeToShard: Record<string, number> | null\n /** Shard loader for layer-0 data */\n private loader: ShardLoader | null\n /** In-memory node cache (Int8 vectors, neighbours) */\n private nodeCache: Map<number, NodeCacheEntry>\n\n /** True once `init()` has completed successfully */\n readonly ready: boolean = false\n\n /** Statistics from the most recent `search()` call, or `null` before first search */\n lastStats: SearchStats | null\n\n constructor() {\n this.config = null\n this.upperLayers = null\n this.nodeToShard = null\n this.loader = null\n this.nodeCache = new Map()\n this.lastStats = null\n }\n\n /**\n * Load all index metadata and populate the upper-layer node cache.\n * This must be called (and awaited) before any call to `search()`.\n *\n * @param baseUrl - Base URL of the collection directory (e.g. `/data/prwp/`)\n * @param opts - Optional cache name and manifest\n */\n async init(baseUrl: string, opts?: HNSWInitOptions): Promise<void> {\n const cacheName = opts?.cacheName ?? 'hnsw-shards-v1'\n const manifest = opts?.manifest ?? null\n\n const base = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'\n const idx: Partial<HNSWIndexConfig> = manifest?.index ?? {}\n const configPath = base + (idx.config ?? 'index/config.json')\n const upperPath = base + (idx.upper_layers ?? 'index/upper_layers.json')\n const nodePath = base + (idx.node_to_shard ?? 'index/node_to_shard.json')\n const shardSuffix = manifest?.compressed ? '.json.gz' : '.json'\n\n const [config, upperLayers, nodeToShard] = await Promise.all([\n fetchJson<HNSWConfig>(configPath, { cacheName }),\n fetchJson<UpperLayersData>(upperPath, { cacheName }),\n fetchJson<Record<string, number>>(nodePath, { cacheName }),\n ])\n\n this.config = config\n this.upperLayers = upperLayers\n this.nodeToShard = nodeToShard\n this.loader = new ShardLoader(base + 'index/layer0/', cacheName, shardSuffix)\n\n // Pre-populate the node cache with upper-layer nodes\n for (const [idStr, node] of Object.entries(upperLayers.nodes)) {\n this.nodeCache.set(parseInt(idStr, 10), {\n id: parseInt(idStr, 10),\n scale: node.scale,\n qv: toInt8Array(node.qv),\n neighbors: [],\n layers: node.layers,\n max_layer: node.max_layer,\n })\n }\n\n ;(this as { ready: boolean }).ready = true // bypass readonly for post-init assignment\n }\n\n /**\n * Search the HNSW index for the nearest neighbours of `queryVec`.\n *\n * @param queryVec - L2-normalised query embedding (Float32Array)\n * @param opts - Optional search parameters (`topK`, `ef`, `ef_upper`)\n * @returns Top-K results sorted by descending score\n * @throws {Error} If called before `init()` has completed\n */\n async search(queryVec: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n if (!this.ready || !this.config || !this.upperLayers || !this.loader) {\n throw new Error('HNSWEngine: not initialized. Call init() first.')\n }\n\n const ef = opts?.ef ?? 50\n const ef_upper = opts?.ef_upper ?? 2\n const topK = opts?.topK ?? 10\n\n const t0 = Date.now()\n const prevCacheSize = this.loader.memoryCache.size\n\n // Greedy descent through upper layers to find good entry points for layer 0\n let entryPoints: ScoredNode[] = [\n [this._scoreUpperNode(queryVec, this.upperLayers.entry_node_id), this.upperLayers.entry_node_id],\n ]\n\n for (let layer = this.config.n_layers - 1; layer >= 1; layer--) {\n entryPoints = this._beamDescentLayer(queryVec, entryPoints, layer, ef_upper)\n }\n\n const results = await this._beamSearchLayer0(queryVec, entryPoints, ef)\n\n const shardsLoaded =\n this.loader.memoryCache.size - prevCacheSize + (this.loader.inflight.size > 0 ? 1 : 0)\n\n this.lastStats = {\n latencyMs: Date.now() - t0,\n shardsLoaded: Math.max(0, shardsLoaded),\n totalCachedShards: this.loader.memoryCache.size,\n }\n\n this.loader.evict(300)\n return results.slice(0, topK)\n }\n\n /**\n * Single-layer greedy beam descent for layers β‰₯ 1 (upper layers).\n * All nodes at these layers are already in `nodeCache`.\n *\n * @param queryVec - L2-normalised query vector\n * @param entryPoints - Current best candidates as `[score, nodeId]` tuples\n * @param layer - Layer index to traverse\n * @param ef_upper - Beam width (number of candidates to keep)\n * @returns Updated candidate list for the next layer\n */\n private _beamDescentLayer(\n queryVec: Float32Array,\n entryPoints: ScoredNode[],\n layer: number,\n ef_upper: number,\n ): ScoredNode[] {\n const layerStr = String(layer)\n const seen = new Set<number>()\n const W: ScoredNode[] = []\n\n for (const [, nodeId] of entryPoints) {\n if (seen.has(nodeId)) continue\n seen.add(nodeId)\n const score = this._scoreUpperNode(queryVec, nodeId)\n _sortedInsert(W, [score, nodeId])\n if (W.length > ef_upper) W.shift()\n\n const node = this.nodeCache.get(nodeId)\n if (!node) continue\n\n const neighbors = node.layers?.[layerStr] ?? []\n for (const nid of neighbors) {\n if (seen.has(nid)) continue\n seen.add(nid)\n const s = this._scoreUpperNode(queryVec, nid)\n _sortedInsert(W, [s, nid])\n if (W.length > ef_upper) W.shift()\n }\n }\n\n return W\n }\n\n /**\n * Score a node that is present in `nodeCache` (upper-layer or already loaded layer-0).\n *\n * @param queryVec - L2-normalised query vector\n * @param nodeId - Node to score\n * @returns Approximate dot-product similarity, or `-Infinity` if node is absent\n */\n private _scoreUpperNode(queryVec: Float32Array, nodeId: number): number {\n const node = this.nodeCache.get(nodeId)\n if (!node) return -Infinity\n return dotProductMixed(queryVec, node.qv, node.scale)\n }\n\n /**\n * Layer-0 beam search. Loads shard files on demand as the search frontier expands.\n *\n * @param queryVec - L2-normalised query vector\n * @param entryPoints - Entry candidates from upper-layer descent\n * @param ef - Beam width (number of candidates to maintain in `W`)\n * @returns All candidates in `W` sorted by descending score as `SearchResult` objects\n */\n private async _beamSearchLayer0(\n queryVec: Float32Array,\n entryPoints: ScoredNode[],\n ef: number,\n ): Promise<SearchResult[]> {\n const visited = new Set<number>()\n let W: ScoredNode[] = []\n let C: ScoredNode[] = []\n\n for (const [, nodeId] of entryPoints) {\n if (visited.has(nodeId)) continue\n visited.add(nodeId)\n const node = await this._getLayer0Node(nodeId)\n if (!node) continue\n const s = dotProductMixed(queryVec, node.qv, node.scale)\n _sortedInsert(W, [s, nodeId])\n _sortedInsert(C, [s, nodeId])\n }\n\n if (W.length > ef) W = W.slice(-ef)\n if (C.length > ef) C = C.slice(-ef)\n\n while (C.length > 0) {\n const [cScore, cId] = C.pop()!\n const worstInW = W.length >= ef ? W[0][0] : -Infinity\n if (cScore < worstInW) break\n\n const cNode = await this._getLayer0Node(cId)\n if (!cNode) continue\n\n const unvisited = cNode.neighbors.filter(n => !visited.has(n))\n\n // Batch-prefetch all shards needed for unvisited neighbours\n const neededShards = new Set<number>()\n for (const nid of unvisited) {\n const sId = this.nodeToShard![String(nid)]\n if (sId != null && !this.loader!.memoryCache.has(sId)) {\n neededShards.add(sId)\n }\n }\n if (neededShards.size > 0) {\n await Promise.all([...neededShards].map(s => this.loader!.load(s)))\n }\n\n for (const nid of unvisited) {\n visited.add(nid)\n const nNode = await this._getLayer0Node(nid)\n if (!nNode) continue\n const score = dotProductMixed(queryVec, nNode.qv, nNode.scale)\n const currentWorst = W.length >= ef ? W[0][0] : -Infinity\n if (score > currentWorst || W.length < ef) {\n _sortedInsert(C, [score, nid])\n _sortedInsert(W, [score, nid])\n if (W.length > ef) W.shift()\n }\n }\n }\n\n return W\n .sort((a, b) => b[0] - a[0])\n .map(([score, id]) => ({ id, score, title: '' }))\n }\n\n /**\n * Retrieve a layer-0 node from cache, loading its shard file if necessary.\n * Once loaded, the node entry in `nodeCache` is augmented with `neighbors`\n * and `_l0loaded = true`.\n *\n * @param nodeId - Node to retrieve\n * @returns Fully populated cache entry, or `null` if the node cannot be found\n */\n private async _getLayer0Node(nodeId: number): Promise<NodeCacheEntry | null> {\n const cached = this.nodeCache.get(nodeId)\n if (cached?._l0loaded) return cached\n\n const shardId = this.nodeToShard![String(nodeId)]\n if (shardId == null) return this.nodeCache.get(nodeId) ?? null\n\n const shard = await this.loader!.load(shardId)\n\n for (const n of shard.nodes) {\n const existing = this.nodeCache.get(n.id)\n const entry: NodeCacheEntry = {\n ...(existing ?? {}),\n id: n.id,\n scale: n.scale,\n qv: existing?.qv ?? toInt8Array(n.qv),\n neighbors: n.neighbors,\n _l0loaded: true,\n }\n this.nodeCache.set(n.id, entry)\n }\n\n return this.nodeCache.get(nodeId) ?? null\n }\n}\n","/**\n * hybrid-search.ts\n *\n * Combines semantic (HNSW / flat) and lexical (BM25) search results using\n * min-max normalisation and a configurable linear blend.\n */\n\nimport type { SearchEngine, BM25Engine, SearchResult, SearchOptions } from '../types/search'\n\n/** Options accepted by `HybridSearch.search()` */\nexport interface HybridSearchOptions {\n /** Number of results to return (default: 20) */\n topK?: number\n /** Weight applied to normalised semantic scores (default: 0.7) */\n semanticWeight?: number\n /** Weight applied to normalised BM25 scores (default: 0.3) */\n lexicalWeight?: number\n /** HNSW beam width forwarded to the semantic engine (default: 50) */\n ef?: number\n /** Search mode: `'semantic'`, `'lexical'`, or `'hybrid'` (default: `'hybrid'`) */\n mode?: 'semantic' | 'lexical' | 'hybrid'\n}\n\n/** Extended result type used internally during score merging */\ninterface MergedResult extends SearchResult {\n semanticScore: number\n lexicalScore: number\n rawSemanticScore: number\n}\n\n/**\n * Hybrid search combining a semantic vector engine and an optional BM25 engine.\n *\n * In `'hybrid'` mode both engines are queried in parallel; scores are\n * min-max normalised independently and then linearly blended.\n *\n * Example:\n * ```ts\n * const hybrid = new HybridSearch(hnswEngine, bm25Engine, id => titlesMap[id])\n * const results = await hybrid.search(queryVec, 'development finance', { topK: 10 })\n * ```\n */\nexport class HybridSearch {\n private readonly semantic: SearchEngine\n private readonly bm25: BM25Engine | null\n private readonly idToMeta: ((id: number | string) => Partial<SearchResult>) | null\n\n /**\n * @param semanticEngine - Initialised `SearchEngine` (FlatEngine or HNSWEngine)\n * @param bm25Engine - Optional BM25 engine; pass `null` to disable lexical search\n * @param idToMeta - Optional callback to look up display metadata by document ID\n */\n constructor(\n semanticEngine: SearchEngine,\n bm25Engine: BM25Engine | null = null,\n idToMeta: ((id: number | string) => Partial<SearchResult>) | null = null,\n ) {\n this.semantic = semanticEngine\n this.bm25 = bm25Engine\n this.idToMeta = idToMeta\n }\n\n /**\n * Run a hybrid (or single-mode) search query.\n *\n * @param queryVec - L2-normalised query embedding, or `null` for lexical-only mode\n * @param queryText - Raw query string for BM25, or empty string for semantic-only mode\n * @param opts - Search options\n * @returns Top-K results sorted by descending combined score\n */\n async search(\n queryVec: Float32Array | null,\n queryText: string,\n opts?: HybridSearchOptions,\n ): Promise<SearchResult[]> {\n const topK = opts?.topK ?? 20\n const semanticWeight = opts?.semanticWeight ?? 0.7\n const lexicalWeight = opts?.lexicalWeight ?? 0.3\n const ef = opts?.ef ?? 50\n const mode = opts?.mode ?? 'hybrid'\n\n const candidateK = topK * 3\n\n const searchOpts: SearchOptions = { topK: candidateK, ef }\n\n const [semanticResults, lexicalResults] = await Promise.all([\n mode !== 'lexical' && queryVec && this.semantic\n ? Promise.resolve(this.semantic.search(queryVec, searchOpts))\n : Promise.resolve([]),\n mode !== 'semantic' && this.bm25 && queryText\n ? Promise.resolve(this._runBM25(queryText, candidateK))\n : Promise.resolve([]),\n ])\n\n // Unwrap potential Promise from synchronous search implementations\n const semResults = await semanticResults\n const lexResults = await lexicalResults\n\n if (mode === 'semantic') return this._formatResults(semResults, topK, 'semantic')\n if (mode === 'lexical') return this._formatResults(lexResults, topK, 'lexical')\n\n // --- Merge & blend ---\n const scoreMap = new Map<string, MergedResult>()\n\n if (semResults.length > 0) {\n const maxSem = semResults[0].score || 1\n const minSem = semResults[semResults.length - 1].score || 0\n const rangeSem = maxSem - minSem || 1\n for (const r of semResults) {\n const normScore = (r.score - minSem) / rangeSem\n scoreMap.set(String(r.id), {\n ...r,\n title: r.title,\n semanticScore: normScore,\n lexicalScore: 0,\n rawSemanticScore: r.score,\n })\n }\n }\n\n if (lexResults.length > 0) {\n const maxBm25 = lexResults[0].score || 1\n const minBm25 = lexResults[lexResults.length - 1].score || 0\n const rangeBm25 = maxBm25 - minBm25 || 1\n for (const r of lexResults) {\n const normScore = (r.score - minBm25) / rangeBm25\n const idStr = String(r.id)\n if (scoreMap.has(idStr)) {\n scoreMap.get(idStr)!.lexicalScore = normScore\n } else {\n const meta = this.idToMeta ? this.idToMeta(r.id) : {}\n scoreMap.set(idStr, {\n ...r,\n ...meta,\n id: r.id,\n title: meta.title ?? r.title ?? '',\n text: meta.text ?? r.text ?? '',\n semanticScore: 0,\n lexicalScore: normScore,\n rawSemanticScore: 0,\n })\n }\n }\n }\n\n const merged = [...scoreMap.values()].map(r => ({\n ...r,\n score: semanticWeight * r.semanticScore + lexicalWeight * r.lexicalScore,\n }))\n\n merged.sort((a, b) => b.score - a.score)\n return merged.slice(0, topK)\n }\n\n /**\n * Run the BM25 engine and map raw `[docIdx, score]` tuples to `SearchResult` objects.\n *\n * @param queryText - Raw query string\n * @param topK - Maximum number of results to return\n * @returns BM25 results as `SearchResult` objects (score order: descending)\n */\n private _runBM25(queryText: string, topK: number): SearchResult[] {\n if (!this.bm25) return []\n try {\n const raw = this.bm25.search(queryText, topK)\n return raw.map(([docIdx, score]) => {\n const meta = this.idToMeta ? this.idToMeta(docIdx) : {}\n return {\n ...meta,\n id: docIdx,\n score,\n title: meta.title ?? '',\n text: meta.text ?? '',\n } as SearchResult\n })\n } catch (e) {\n console.warn('BM25 search error:', e)\n return []\n }\n }\n\n /**\n * Format single-mode results, adding the appropriate `semanticScore` /\n * `lexicalScore` fields expected by callers.\n *\n * @param results - Raw results from one engine\n * @param topK - Slice limit\n * @param source - Which engine produced the results\n * @returns Results annotated with zeroed-out score fields for the unused engine\n */\n private _formatResults(\n results: SearchResult[],\n topK: number,\n source: 'semantic' | 'lexical',\n ): SearchResult[] {\n return results.slice(0, topK).map(r => ({\n ...r,\n semanticScore: source === 'semantic' ? (r.score ?? 0) : 0,\n lexicalScore: source === 'lexical' ? (r.score ?? 0) : 0,\n }))\n }\n}\n"],"mappings":"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","names":["SearchClient","manifestUrl","opts","e","err","initMsg","type","handler","bucket","h","text","limit","resolve","off","msg","dotProductMixed","queryF32","storedQV","storedScale","dot","len","i","dequantize","qv","scale","out","l2NormalizeInPlace","vec","norm","toInt8Array","arr","fetchJson","url","opts","cacheName","isGz","cached","resp","data","encoding","stream","FlatEngine","url","data","fetchJson","item","toInt8Array","queryVec","opts","topK","threshold","t0","scores","i","dotProductMixed","candidates","a","b","results","extra","k","v","ShardLoader","baseUrl","cacheName","shardSuffix","shardId","promise","data","shardIds","sid","maxEntries","oldest","fetchJson","_sortedInsert","arr","item","score","lo","hi","mid","HNSWEngine","baseUrl","opts","cacheName","manifest","base","idx","configPath","upperPath","nodePath","shardSuffix","config","upperLayers","nodeToShard","fetchJson","ShardLoader","idStr","node","toInt8Array","queryVec","ef","ef_upper","topK","t0","prevCacheSize","entryPoints","layer","results","shardsLoaded","layerStr","seen","W","nodeId","neighbors","nid","s","dotProductMixed","visited","C","cScore","cId","worstInW","cNode","unvisited","n","neededShards","sId","nNode","currentWorst","b","id","cached","shardId","shard","existing","entry","HybridSearch","semanticEngine","bm25Engine","idToMeta","queryVec","queryText","opts","topK","semanticWeight","lexicalWeight","ef","mode","candidateK","searchOpts","semanticResults","lexicalResults","semResults","lexResults","scoreMap","maxSem","minSem","rangeSem","r","normScore","maxBm25","minBm25","rangeBm25","idStr","meta","merged","a","docIdx","score","e","results","source"]}
1
+ {"version":3,"sources":["../src/client/SearchClient.ts","../src/engine/int8-codec.ts","../src/engine/fetch-json.ts","../src/engine/flat-engine.ts","../src/engine/shard-loader.ts","../src/engine/hnsw-engine.ts","../src/engine/hybrid-search.ts"],"sourcesContent":["/**\n * SearchClient β€” framework-agnostic wrapper around the search Web Worker.\n *\n * Works in any JavaScript environment that supports Web Workers.\n * Extend or wrap with framework adapters (see adapters/vue.ts, adapters/react.ts).\n *\n * @example\n * ```ts\n * const client = new SearchClient('https://example.com/data/prwp/manifest.json')\n *\n * client.on('index_ready', () => {\n * client.search('climate finance', { topK: 10, mode: 'hybrid' })\n * })\n *\n * client.on('results', ({ data, stats }) => {\n * console.log(data) // SearchResult[]\n * console.log(stats) // SearchStats | null\n * })\n *\n * // Clean up when done\n * client.destroy()\n * ```\n */\n\nimport type { WorkerOutboundMessage, WorkerInboundMessage } from '../types/worker'\nimport type { CollectionManifest } from '../types/manifest'\nimport type { SearchOptions } from '../types/search'\n\n// ── Types ─────────────────────────────────────────────────────────────────────\n\nexport type SearchMode = 'semantic' | 'lexical' | 'hybrid'\n\nexport interface SearchClientOptions {\n /** HuggingFace model ID to use for embeddings (default: avsolatorio/GIST-small-Embedding-v0) */\n modelId?: string\n /** If true, skip loading the embedding model (for testing BM25 fallback). */\n skipModelLoad?: boolean\n /** Delay (seconds) before loading the embedding model; index + BM25 load first (for testing). */\n modelLoadDelaySeconds?: number\n /**\n * Factory function that creates the Web Worker.\n * Defaults to the bundled search worker created via `new URL()`.\n * Override when you need a custom worker path (e.g. CDN, service worker proxy).\n *\n * @example\n * ```ts\n * // Vite / webpack 5 (recommended β€” bundler resolves the path)\n * new SearchClient(url, {\n * workerFactory: () => new Worker(new URL('@ai4data/search/worker', import.meta.url), { type: 'module' })\n * })\n * ```\n */\n workerFactory?: () => Worker\n /**\n * URL to the worker script (e.g. from a CDN). Use this when loading the package from a CDN\n * so the worker is created via fetch + blob and works cross-origin. Ignored if workerFactory is set.\n *\n * @example\n * ```ts\n * const client = await SearchClient.fromCDN(manifestUrl, {\n * workerUrl: 'https://esm.sh/@ai4data/search@0.1.0/worker'\n * })\n * ```\n */\n workerUrl?: string\n}\n\ntype MessageHandler<T extends WorkerOutboundMessage['type']> = (\n msg: Extract<WorkerOutboundMessage, { type: T }>,\n) => void\n\n/**\n * Create a Worker from a cross-origin URL by fetching the script and instantiating\n * from a blob URL. Use this when loading the package from a CDN so the worker is\n * same-origin and browsers allow it.\n *\n * @param url - Full URL to the worker script (e.g. https://esm.sh/@ai4data/search@0.1.0/worker)\n * @returns Promise that resolves with the Worker instance\n */\nexport function createWorkerFromUrl(url: string): Promise<Worker> {\n return fetch(url, { mode: 'cors' })\n .then((r) => {\n if (!r.ok) throw new Error(`Failed to fetch worker: ${r.status} ${r.statusText}`)\n return r.text()\n })\n .then((code) => {\n const blob = new Blob([code], { type: 'application/javascript' })\n const blobUrl = URL.createObjectURL(blob)\n return new Worker(blobUrl, { type: 'module' })\n })\n}\n\n// ── SearchClient ──────────────────────────────────────────────────────────────\n\nexport class SearchClient {\n // ── Public state (plain properties β€” no reactivity) ──\n\n /** True once the index + BM25 corpus are loaded. Lexical search available. */\n isIndexReady = false\n\n /** True once the ONNX embedding model is ready. Semantic + hybrid search available. */\n isModelReady = false\n\n /** Latest progress/status message from the worker. */\n loadingMessage = 'Initializing…'\n\n /** True when the last search fell back to BM25 because the model wasn't ready. */\n activeFallback = false\n\n /** Parsed collection manifest, available after `index_ready`. */\n manifest: CollectionManifest | null = null\n\n // ── Private ──\n\n private readonly worker: Worker\n private readonly handlers = new Map<string, Set<(msg: WorkerOutboundMessage) => void>>()\n private destroyed = false\n\n // ── Constructor ──────────────────────────────────────────────────────────────\n\n /**\n * @param manifestUrl - Absolute or relative URL to `manifest.json`.\n * Relative URLs are resolved against `location.href`.\n * @param opts - Optional configuration.\n */\n constructor(manifestUrl: string, opts: SearchClientOptions = {}) {\n if (opts.workerFactory) {\n this.worker = opts.workerFactory()\n } else if (opts.workerUrl) {\n throw new Error(\n 'SearchClient: workerUrl is only supported with SearchClient.fromCDN(). Use fromCDN(manifestUrl, { workerUrl }) or pass workerFactory.'\n )\n } else {\n this.worker = new Worker(new URL('./worker.mjs', import.meta.url), { type: 'module' })\n }\n\n this.worker.onmessage = (e: MessageEvent<WorkerOutboundMessage>) => {\n this._handleMessage(e.data)\n }\n\n this.worker.onerror = (err) => {\n console.error('[SearchClient] Worker error:', err)\n this.loadingMessage = 'Search worker error'\n }\n\n // Resolve relative manifest URLs against current page origin\n const resolvedUrl = new URL(\n manifestUrl,\n globalThis.location?.href ?? 'http://localhost/',\n ).href\n\n const initMsg: WorkerInboundMessage = {\n type: 'init',\n manifestUrl: resolvedUrl,\n modelId: opts.modelId,\n skipModelLoad: opts.skipModelLoad,\n modelLoadDelaySeconds: opts.modelLoadDelaySeconds,\n }\n this.worker.postMessage(initMsg)\n }\n\n // ── Event bus ─────────────────────────────────────────────────────────────────\n\n /**\n * Subscribe to a specific worker message type.\n * Returns an unsubscribe function β€” call it to remove the handler.\n *\n * @example\n * ```ts\n * const off = client.on('results', ({ data }) => setResults(data))\n * // later…\n * off()\n * ```\n */\n on<T extends WorkerOutboundMessage['type']>(\n type: T,\n handler: MessageHandler<T>,\n ): () => void {\n if (!this.handlers.has(type)) this.handlers.set(type, new Set())\n const bucket = this.handlers.get(type)!\n const h = handler as (msg: WorkerOutboundMessage) => void\n bucket.add(h)\n return () => bucket.delete(h)\n }\n\n // ── Actions ───────────────────────────────────────────────────────────────────\n\n /**\n * Submit a search query. No-op if the index is not yet ready.\n *\n * @param text - Natural-language query\n * @param opts - Optional topK, ef, mode ('semantic' | 'lexical' | 'hybrid')\n */\n search(text: string, opts: SearchOptions & { mode?: SearchMode } = {}): void {\n if (!this.isIndexReady) return\n this.worker.postMessage({\n type: 'search',\n text,\n topK: opts.topK ?? 20,\n ef: opts.ef ?? 50,\n ef_upper: opts.ef_upper ?? 2,\n threshold: opts.threshold ?? 0.0,\n mode: opts.mode ?? 'hybrid',\n } satisfies WorkerInboundMessage)\n }\n\n /**\n * Fetch the most-recent items from the index (useful for pre-search state).\n */\n getRecent(limit = 10): void {\n this.worker.postMessage({ type: 'getRecent', limit } satisfies WorkerInboundMessage)\n }\n\n /**\n * Ping the worker. Resolves when the worker responds with 'pong'.\n */\n ping(): Promise<void> {\n return new Promise((resolve) => {\n const off = this.on('pong', () => { off(); resolve() })\n this.worker.postMessage({ type: 'ping' } satisfies WorkerInboundMessage)\n })\n }\n\n /**\n * Terminate the worker and clean up all event listeners.\n * The client is unusable after this call.\n */\n destroy(): void {\n if (this.destroyed) return\n this.destroyed = true\n this.worker.terminate()\n this.handlers.clear()\n }\n\n /**\n * Create a SearchClient when loading the package from a CDN. Fetches the worker\n * script and creates the worker from a blob URL so it works cross-origin.\n *\n * @param manifestUrl - URL to the collection manifest.json\n * @param opts - Options; must include workerUrl (e.g. https://esm.sh/@ai4data/search@0.1.0/worker)\n * @returns Promise that resolves with the SearchClient\n *\n * @example\n * ```ts\n * const client = await SearchClient.fromCDN('https://example.com/data/manifest.json', {\n * workerUrl: 'https://esm.sh/@ai4data/search@0.1.0/worker'\n * })\n * client.on('results', ({ data }) => console.log(data))\n * ```\n */\n static fromCDN(\n manifestUrl: string,\n opts: SearchClientOptions & { workerUrl: string },\n ): Promise<SearchClient> {\n const { workerUrl, ...rest } = opts\n return createWorkerFromUrl(workerUrl).then((worker) => {\n return new SearchClient(manifestUrl, { ...rest, workerFactory: () => worker })\n })\n }\n\n // ── Internal ──────────────────────────────────────────────────────────────────\n\n private _handleMessage(msg: WorkerOutboundMessage): void {\n // Update plain-property state\n switch (msg.type) {\n case 'progress':\n this.loadingMessage = msg.message\n break\n case 'index_ready':\n this.isIndexReady = true\n break\n case 'ready':\n this.isModelReady = msg.modelLoaded !== false\n this.manifest = msg.config\n break\n case 'results':\n this.activeFallback = msg.fallback ?? false\n break\n case 'error':\n this.isIndexReady = true // exit loading state on error\n this.loadingMessage = `Error: ${msg.message}`\n console.error('[SearchClient] Worker error message:', msg.message)\n break\n }\n\n // Dispatch to registered handlers\n const bucket = this.handlers.get(msg.type)\n if (bucket) {\n for (const h of bucket) h(msg)\n }\n }\n}\n","/**\n * int8-codec.ts\n *\n * Quantization scheme: vectors are stored as Int8 values in [-127, 127].\n * Each vector is accompanied by a scalar `scale` such that the original\n * float value β‰ˆ int8_value * scale. Dot products are computed in mixed\n * precision (Float32 query Γ— dequantized Int8 stored vector) to keep\n * both accuracy and memory efficiency.\n */\n\n/**\n * Compute the dot product between a Float32 query vector and a stored\n * Int8-quantized vector, dequantizing on the fly.\n *\n * @param queryF32 - L2-normalised query vector (Float32Array)\n * @param storedQV - Int8-quantized stored vector\n * @param storedScale - Per-vector dequantization scale factor\n * @returns Approximate cosine similarity score\n */\nexport function dotProductMixed(\n queryF32: Float32Array,\n storedQV: Int8Array,\n storedScale: number,\n): number {\n let dot = 0.0;\n const len = queryF32.length;\n for (let i = 0; i < len; i++) {\n dot += queryF32[i] * (storedQV[i] * storedScale);\n }\n return dot;\n}\n\n/**\n * Dequantize an Int8 vector back to Float32 using the stored scale.\n *\n * @param qv - Int8-quantized vector\n * @param scale - Per-vector dequantization scale factor\n * @returns Reconstructed Float32 vector\n */\nexport function dequantize(qv: Int8Array, scale: number): Float32Array {\n const out = new Float32Array(qv.length);\n for (let i = 0; i < qv.length; i++) {\n out[i] = qv[i] * scale;\n }\n return out;\n}\n\n/**\n * L2-normalise a Float32 vector in place.\n * Vectors whose norm is below 1e-9 are left unchanged to avoid division by zero.\n *\n * @param vec - Vector to normalise (mutated in place)\n * @returns The same (now normalised) vector\n */\nexport function l2NormalizeInPlace(vec: Float32Array): Float32Array {\n let norm = 0.0;\n for (let i = 0; i < vec.length; i++) norm += vec[i] * vec[i];\n norm = Math.sqrt(norm);\n if (norm < 1e-9) return vec;\n for (let i = 0; i < vec.length; i++) vec[i] /= norm;\n return vec;\n}\n\n/**\n * Convert a plain number array (or an existing Int8Array) to an Int8Array.\n * Values outside [-128, 127] are silently truncated by the typed-array constructor.\n *\n * @param arr - Source values\n * @returns An Int8Array view / copy of the input\n */\nexport function toInt8Array(arr: number[] | Int8Array): Int8Array {\n return new Int8Array(arr);\n}\n","/**\n * fetch-json.ts\n *\n * Utility for fetching JSON (plain or gzip-compressed) with optional\n * Cache Storage read/write so repeat cold-starts skip the network.\n */\n\nexport interface FetchJsonOptions {\n /**\n * When provided, the response is read from (and written to) a named\n * Cache Storage bucket. Pass `null` to disable caching entirely.\n */\n cacheName?: string | null\n}\n\n/**\n * Fetch a JSON resource, transparently handling gzip-compressed responses.\n *\n * Caching behaviour:\n * 1. If `cacheName` is set and the Cache API is available, attempt a cache hit.\n * 2. On a miss, fetch from the network.\n * 3. Decompress if the URL ends with `.gz` and the server did not already\n * decompress it (i.e. `Content-Encoding` is absent or non-gzip).\n * 4. Write the parsed object back to Cache Storage for future requests.\n *\n * @param url - Absolute or relative URL to fetch\n * @param opts - Optional caching configuration\n * @returns Parsed JSON payload cast to `T`\n * @throws {Error} On non-2xx HTTP responses\n */\nexport async function fetchJson<T = unknown>(\n url: string,\n opts?: FetchJsonOptions,\n): Promise<T> {\n const cacheName = opts?.cacheName ?? null;\n const isGz = url.endsWith('.gz');\n\n // --- cache read ---\n if (cacheName && typeof caches !== 'undefined') {\n try {\n const cache = await caches.open(cacheName);\n const cached = await cache.match(url);\n if (cached) {\n return cached.json() as Promise<T>;\n }\n } catch (_) {\n // Cache API unavailable or denied β€” fall through to network fetch\n }\n }\n\n // --- network fetch ---\n const resp = await fetch(url);\n if (!resp.ok) {\n throw new Error(`fetchJson: HTTP ${resp.status}: ${url}`);\n }\n\n let data: T;\n\n if (isGz) {\n const encoding = resp.headers.get('Content-Encoding');\n if (encoding === 'gzip' || encoding === 'x-gzip') {\n // The server already decompressed it for us\n data = (await resp.json()) as T;\n } else {\n // Decompress manually in the browser via DecompressionStream\n const stream = resp.body!.pipeThrough(new DecompressionStream('gzip'));\n data = (await new Response(stream).json()) as T;\n }\n } else {\n data = (await resp.json()) as T;\n }\n\n // --- cache write ---\n if (cacheName && typeof caches !== 'undefined') {\n try {\n const cache = await caches.open(cacheName);\n cache.put(\n url,\n new Response(JSON.stringify(data), {\n headers: { 'Content-Type': 'application/json' },\n }),\n );\n } catch (_) {\n // Best-effort β€” ignore write failures\n }\n }\n\n return data;\n}\n","/**\n * flat-engine.ts\n *\n * Brute-force (flat) search engine backed by an Int8-quantized index.\n * Suitable for collections up to ~50 k documents where exact nearest-neighbour\n * search is fast enough without an ANN index structure.\n */\n\nimport type { FlatItem, SearchEngine, SearchOptions, SearchResult, SearchStats } from '../types/search'\nimport { dotProductMixed, toInt8Array } from './int8-codec'\nimport { fetchJson } from './fetch-json'\n\n/** Shape of the JSON file loaded by `FlatEngine.load()` */\ninterface FlatIndexFile {\n dim: number\n items: FlatItem[]\n}\n\n/** Internal representation β€” `qv` is always an `Int8Array` after loading. Defined\n * explicitly (not via `Omit<FlatItem, 'qv'>`) to avoid `[key: string]: unknown`\n * index-signature narrowing that makes named properties return `unknown`. */\ninterface LoadedFlatItem {\n id: string | number\n idno?: string\n title: string\n text: string\n scale: number\n qv: Int8Array\n type?: string\n [key: string]: unknown\n}\n\n/**\n * Brute-force semantic search engine.\n *\n * Usage:\n * ```ts\n * const engine = new FlatEngine()\n * await engine.load('/data/flat/embeddings.int8.json')\n * const results = engine.search(queryVec, { topK: 10 })\n * ```\n */\nexport class FlatEngine implements SearchEngine {\n /** Internal item list with Int8-converted vectors */\n private items: LoadedFlatItem[]\n\n /** True once `load()` has completed successfully */\n readonly ready: boolean = false\n\n /** Statistics from the most recent `search()` call, or `null` before first search */\n lastStats: SearchStats | null\n\n constructor() {\n this.items = []\n this.lastStats = null\n }\n\n /**\n * Fetch and parse the flat index file, converting all `qv` arrays to `Int8Array`.\n *\n * @param url - URL of the `embeddings.int8.json` index file\n * @returns The raw item list from the JSON (before Int8 conversion)\n */\n async load(url: string): Promise<FlatItem[]> {\n const data = await fetchJson<FlatIndexFile>(url)\n this.items = data.items.map(item => ({\n ...item,\n qv: toInt8Array(item.qv as number[]),\n }))\n ;(this as { ready: boolean }).ready = true\n return data.items\n }\n\n /**\n * Run a brute-force cosine-similarity search over all loaded items.\n *\n * @param queryVec - L2-normalised query embedding (Float32Array)\n * @param opts - Optional search parameters\n * @returns Top-K results sorted by descending score\n * @throws {Error} If called before `load()` has completed\n */\n search(queryVec: Float32Array, opts?: SearchOptions): SearchResult[] {\n if (!this.ready) throw new Error('FlatEngine: not loaded yet')\n\n const topK = opts?.topK ?? 20\n const threshold = opts?.threshold ?? 0.0\n\n const t0 = Date.now()\n const scores = new Float32Array(this.items.length)\n\n for (let i = 0; i < this.items.length; i++) {\n const item = this.items[i]\n scores[i] = dotProductMixed(queryVec, item.qv, item.scale)\n }\n\n // Collect candidate indices above threshold\n const candidates: number[] = []\n for (let i = 0; i < scores.length; i++) {\n if (scores[i] >= threshold) candidates.push(i)\n }\n\n candidates.sort((a, b) => scores[b] - scores[a])\n\n const results = candidates.slice(0, topK).map(i => {\n const item = this.items[i]\n // Include all preview fields except internal-only ones\n const extra: Record<string, unknown> = {}\n for (const [k, v] of Object.entries(item)) {\n if (!['id', 'scale', 'qv', 'title', 'text'].includes(k)) {\n extra[k] = v\n }\n }\n return {\n id: item.id,\n score: scores[i],\n title: item.title,\n text: item.text,\n ...extra,\n } as SearchResult\n })\n\n this.lastStats = {\n latencyMs: Date.now() - t0,\n shardsLoaded: 0,\n totalCachedShards: 0,\n }\n\n return results\n }\n}\n","/**\n * shard-loader.ts\n *\n * Loads layer-0 shard files on demand, deduplicates in-flight requests,\n * and maintains a bounded in-memory cache to limit worker heap growth.\n */\n\nimport type { Shard } from '../types/search'\nimport { fetchJson } from './fetch-json'\n\n/**\n * Loads, deduplicates, and caches HNSW layer-0 shard files.\n *\n * Shards are stored in files named `shard_NNN<suffix>` (e.g. `shard_007.json`)\n * under a common base URL. The loader combines three levels of caching:\n *\n * 1. In-memory `Map` β€” fastest; survives across queries within the same worker.\n * 2. In-flight deduplication β€” a second caller for the same shard awaits the\n * already-running fetch rather than issuing a duplicate request.\n * 3. Cache Storage (via `fetchJson`) β€” survives page reloads.\n */\nexport class ShardLoader {\n /** Base URL for shard files (always ends with `/`) */\n readonly baseUrl: string\n /** Cache Storage bucket name passed through to `fetchJson` */\n readonly cacheName: string\n /** File extension appended to each shard filename (e.g. `.json` or `.json.gz`) */\n readonly shardSuffix: string\n\n /** In-memory shard cache keyed by numeric shard ID */\n memoryCache: Map<number, Shard>\n /** Promises for shards currently being fetched, keyed by numeric shard ID */\n inflight: Map<number, Promise<Shard>>\n /** Insertion-order log used by `evict()` to expire the oldest entries first */\n _insertOrder: number[]\n\n constructor(\n baseUrl: string,\n cacheName = 'hnsw-shards-v1',\n shardSuffix = '.json',\n ) {\n this.baseUrl = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'\n this.cacheName = cacheName\n this.shardSuffix = shardSuffix\n this.memoryCache = new Map()\n this.inflight = new Map()\n this._insertOrder = []\n }\n\n /**\n * Load a shard by ID, returning the in-memory copy if already cached,\n * joining an in-flight fetch if one exists, or issuing a new network request.\n *\n * @param shardId - Numeric shard identifier\n * @returns Parsed shard data\n */\n async load(shardId: number): Promise<Shard> {\n if (this.memoryCache.has(shardId)) {\n return this.memoryCache.get(shardId)!\n }\n if (this.inflight.has(shardId)) {\n return this.inflight.get(shardId)!\n }\n\n const promise = this._fetchShard(shardId)\n this.inflight.set(shardId, promise)\n\n try {\n const data = await promise\n this.memoryCache.set(shardId, data)\n this._insertOrder.push(shardId)\n return data\n } finally {\n this.inflight.delete(shardId)\n }\n }\n\n /**\n * Kick off background loads for a set of shard IDs without awaiting them.\n * Useful for prefetching neighbours that will likely be needed soon.\n *\n * @param shardIds - Shard IDs to prefetch\n */\n prefetch(shardIds: number[]): void {\n for (const sid of shardIds) {\n if (!this.memoryCache.has(sid) && !this.inflight.has(sid)) {\n this.load(sid)\n }\n }\n }\n\n /**\n * Evict the oldest in-memory shard entries to keep heap usage bounded.\n *\n * @param maxEntries - Maximum number of shards to keep (default: 200)\n */\n evict(maxEntries = 200): void {\n while (this._insertOrder.length > maxEntries) {\n const oldest = this._insertOrder.shift()!\n this.memoryCache.delete(oldest)\n }\n }\n\n /**\n * Build the URL for a given shard ID.\n *\n * @param shardId - Numeric shard identifier\n * @returns Full URL string\n */\n private _shardUrl(shardId: number): string {\n return this.baseUrl + `shard_${String(shardId).padStart(3, '0')}${this.shardSuffix}`\n }\n\n /**\n * Fetch and parse a shard file from the network (or Cache Storage).\n *\n * @param shardId - Numeric shard identifier\n * @returns Parsed shard data\n */\n private _fetchShard(shardId: number): Promise<Shard> {\n return fetchJson<Shard>(this._shardUrl(shardId), { cacheName: this.cacheName })\n }\n}\n","/**\n * hnsw-engine.ts\n *\n * Approximate nearest-neighbour search using a pre-built HNSW index.\n * Upper layers (layers β‰₯ 1) are held entirely in memory; layer-0 is\n * loaded on demand from shard files via `ShardLoader`.\n */\n\nimport type { HNSWConfig, HNSWIndexConfig, UpperLayersData, CollectionManifest } from '../types/manifest'\nimport type {\n NodeCacheEntry,\n ScoredNode,\n SearchEngine,\n SearchResult,\n SearchStats,\n SearchOptions,\n} from '../types/search'\nimport { dotProductMixed, toInt8Array } from './int8-codec'\nimport { ShardLoader } from './shard-loader'\nimport { fetchJson } from './fetch-json'\n\n/** Options accepted by `HNSWEngine.init()` */\ninterface HNSWInitOptions {\n /** Cache Storage bucket name forwarded to `ShardLoader` and `fetchJson` */\n cacheName?: string\n /** Parsed manifest; used to resolve index file paths and compressed flag */\n manifest?: CollectionManifest | null\n}\n\n/**\n * Insert `item` into `arr` in ascending score order (binary search).\n * `arr[0]` is always the lowest-scoring element after insertion.\n *\n * @param arr - Sorted array to insert into (mutated in place)\n * @param item - `[score, nodeId]` tuple to insert\n */\nfunction _sortedInsert(arr: ScoredNode[], item: ScoredNode): void {\n const score = item[0]\n let lo = 0\n let hi = arr.length\n while (lo < hi) {\n const mid = (lo + hi) >>> 1\n if (arr[mid][0] < score) lo = mid + 1\n else hi = mid\n }\n arr.splice(lo, 0, item)\n}\n\n/**\n * HNSW approximate nearest-neighbour search engine.\n *\n * Typical usage:\n * ```ts\n * const engine = new HNSWEngine()\n * await engine.init('/data/prwp/')\n * const results = await engine.search(queryVec, { topK: 10, ef: 50 })\n * ```\n */\nexport class HNSWEngine implements SearchEngine {\n /** Parsed `index/config.json` */\n private config: HNSWConfig | null\n /** Parsed `index/upper_layers.json` */\n private upperLayers: UpperLayersData | null\n /** Maps string node ID β†’ shard ID */\n private nodeToShard: Record<string, number> | null\n /** Shard loader for layer-0 data */\n private loader: ShardLoader | null\n /** In-memory node cache (Int8 vectors, neighbours) */\n private nodeCache: Map<number, NodeCacheEntry>\n\n /** True once `init()` has completed successfully */\n readonly ready: boolean = false\n\n /** Statistics from the most recent `search()` call, or `null` before first search */\n lastStats: SearchStats | null\n\n constructor() {\n this.config = null\n this.upperLayers = null\n this.nodeToShard = null\n this.loader = null\n this.nodeCache = new Map()\n this.lastStats = null\n }\n\n /**\n * Load all index metadata and populate the upper-layer node cache.\n * This must be called (and awaited) before any call to `search()`.\n *\n * @param baseUrl - Base URL of the collection directory (e.g. `/data/prwp/`)\n * @param opts - Optional cache name and manifest\n */\n async init(baseUrl: string, opts?: HNSWInitOptions): Promise<void> {\n const cacheName = opts?.cacheName ?? 'hnsw-shards-v1'\n const manifest = opts?.manifest ?? null\n\n const base = baseUrl.endsWith('/') ? baseUrl : baseUrl + '/'\n const idx: Partial<HNSWIndexConfig> = manifest?.index ?? {}\n const configPath = base + (idx.config ?? 'index/config.json')\n const upperPath = base + (idx.upper_layers ?? 'index/upper_layers.json')\n const nodePath = base + (idx.node_to_shard ?? 'index/node_to_shard.json')\n const shardSuffix = manifest?.compressed ? '.json.gz' : '.json'\n\n const [config, upperLayers, nodeToShard] = await Promise.all([\n fetchJson<HNSWConfig>(configPath, { cacheName }),\n fetchJson<UpperLayersData>(upperPath, { cacheName }),\n fetchJson<Record<string, number>>(nodePath, { cacheName }),\n ])\n\n this.config = config\n this.upperLayers = upperLayers\n this.nodeToShard = nodeToShard\n this.loader = new ShardLoader(base + 'index/layer0/', cacheName, shardSuffix)\n\n // Pre-populate the node cache with upper-layer nodes\n for (const [idStr, node] of Object.entries(upperLayers.nodes)) {\n this.nodeCache.set(parseInt(idStr, 10), {\n id: parseInt(idStr, 10),\n scale: node.scale,\n qv: toInt8Array(node.qv),\n neighbors: [],\n layers: node.layers,\n max_layer: node.max_layer,\n })\n }\n\n ;(this as { ready: boolean }).ready = true // bypass readonly for post-init assignment\n }\n\n /**\n * Search the HNSW index for the nearest neighbours of `queryVec`.\n *\n * @param queryVec - L2-normalised query embedding (Float32Array)\n * @param opts - Optional search parameters (`topK`, `ef`, `ef_upper`)\n * @returns Top-K results sorted by descending score\n * @throws {Error} If called before `init()` has completed\n */\n async search(queryVec: Float32Array, opts?: SearchOptions): Promise<SearchResult[]> {\n if (!this.ready || !this.config || !this.upperLayers || !this.loader) {\n throw new Error('HNSWEngine: not initialized. Call init() first.')\n }\n\n const ef = opts?.ef ?? 50\n const ef_upper = opts?.ef_upper ?? 2\n const topK = opts?.topK ?? 10\n\n const t0 = Date.now()\n const prevCacheSize = this.loader.memoryCache.size\n\n // Greedy descent through upper layers to find good entry points for layer 0\n let entryPoints: ScoredNode[] = [\n [this._scoreUpperNode(queryVec, this.upperLayers.entry_node_id), this.upperLayers.entry_node_id],\n ]\n\n for (let layer = this.config.n_layers - 1; layer >= 1; layer--) {\n entryPoints = this._beamDescentLayer(queryVec, entryPoints, layer, ef_upper)\n }\n\n const results = await this._beamSearchLayer0(queryVec, entryPoints, ef)\n\n const shardsLoaded =\n this.loader.memoryCache.size - prevCacheSize + (this.loader.inflight.size > 0 ? 1 : 0)\n\n this.lastStats = {\n latencyMs: Date.now() - t0,\n shardsLoaded: Math.max(0, shardsLoaded),\n totalCachedShards: this.loader.memoryCache.size,\n }\n\n this.loader.evict(300)\n return results.slice(0, topK)\n }\n\n /**\n * Single-layer greedy beam descent for layers β‰₯ 1 (upper layers).\n * All nodes at these layers are already in `nodeCache`.\n *\n * @param queryVec - L2-normalised query vector\n * @param entryPoints - Current best candidates as `[score, nodeId]` tuples\n * @param layer - Layer index to traverse\n * @param ef_upper - Beam width (number of candidates to keep)\n * @returns Updated candidate list for the next layer\n */\n private _beamDescentLayer(\n queryVec: Float32Array,\n entryPoints: ScoredNode[],\n layer: number,\n ef_upper: number,\n ): ScoredNode[] {\n const layerStr = String(layer)\n const seen = new Set<number>()\n const W: ScoredNode[] = []\n\n for (const [, nodeId] of entryPoints) {\n if (seen.has(nodeId)) continue\n seen.add(nodeId)\n const score = this._scoreUpperNode(queryVec, nodeId)\n _sortedInsert(W, [score, nodeId])\n if (W.length > ef_upper) W.shift()\n\n const node = this.nodeCache.get(nodeId)\n if (!node) continue\n\n const neighbors = node.layers?.[layerStr] ?? []\n for (const nid of neighbors) {\n if (seen.has(nid)) continue\n seen.add(nid)\n const s = this._scoreUpperNode(queryVec, nid)\n _sortedInsert(W, [s, nid])\n if (W.length > ef_upper) W.shift()\n }\n }\n\n return W\n }\n\n /**\n * Score a node that is present in `nodeCache` (upper-layer or already loaded layer-0).\n *\n * @param queryVec - L2-normalised query vector\n * @param nodeId - Node to score\n * @returns Approximate dot-product similarity, or `-Infinity` if node is absent\n */\n private _scoreUpperNode(queryVec: Float32Array, nodeId: number): number {\n const node = this.nodeCache.get(nodeId)\n if (!node) return -Infinity\n return dotProductMixed(queryVec, node.qv, node.scale)\n }\n\n /**\n * Layer-0 beam search. Loads shard files on demand as the search frontier expands.\n *\n * @param queryVec - L2-normalised query vector\n * @param entryPoints - Entry candidates from upper-layer descent\n * @param ef - Beam width (number of candidates to maintain in `W`)\n * @returns All candidates in `W` sorted by descending score as `SearchResult` objects\n */\n private async _beamSearchLayer0(\n queryVec: Float32Array,\n entryPoints: ScoredNode[],\n ef: number,\n ): Promise<SearchResult[]> {\n const visited = new Set<number>()\n let W: ScoredNode[] = []\n let C: ScoredNode[] = []\n\n for (const [, nodeId] of entryPoints) {\n if (visited.has(nodeId)) continue\n visited.add(nodeId)\n const node = await this._getLayer0Node(nodeId)\n if (!node) continue\n const s = dotProductMixed(queryVec, node.qv, node.scale)\n _sortedInsert(W, [s, nodeId])\n _sortedInsert(C, [s, nodeId])\n }\n\n if (W.length > ef) W = W.slice(-ef)\n if (C.length > ef) C = C.slice(-ef)\n\n while (C.length > 0) {\n const [cScore, cId] = C.pop()!\n const worstInW = W.length >= ef ? W[0][0] : -Infinity\n if (cScore < worstInW) break\n\n const cNode = await this._getLayer0Node(cId)\n if (!cNode) continue\n\n const unvisited = cNode.neighbors.filter(n => !visited.has(n))\n\n // Batch-prefetch all shards needed for unvisited neighbours\n const neededShards = new Set<number>()\n for (const nid of unvisited) {\n const sId = this.nodeToShard![String(nid)]\n if (sId != null && !this.loader!.memoryCache.has(sId)) {\n neededShards.add(sId)\n }\n }\n if (neededShards.size > 0) {\n await Promise.all([...neededShards].map(s => this.loader!.load(s)))\n }\n\n for (const nid of unvisited) {\n visited.add(nid)\n const nNode = await this._getLayer0Node(nid)\n if (!nNode) continue\n const score = dotProductMixed(queryVec, nNode.qv, nNode.scale)\n const currentWorst = W.length >= ef ? W[0][0] : -Infinity\n if (score > currentWorst || W.length < ef) {\n _sortedInsert(C, [score, nid])\n _sortedInsert(W, [score, nid])\n if (W.length > ef) W.shift()\n }\n }\n }\n\n return W\n .sort((a, b) => b[0] - a[0])\n .map(([score, id]) => ({ id, score, title: '' }))\n }\n\n /**\n * Retrieve a layer-0 node from cache, loading its shard file if necessary.\n * Once loaded, the node entry in `nodeCache` is augmented with `neighbors`\n * and `_l0loaded = true`.\n *\n * @param nodeId - Node to retrieve\n * @returns Fully populated cache entry, or `null` if the node cannot be found\n */\n private async _getLayer0Node(nodeId: number): Promise<NodeCacheEntry | null> {\n const cached = this.nodeCache.get(nodeId)\n if (cached?._l0loaded) return cached\n\n const shardId = this.nodeToShard![String(nodeId)]\n if (shardId == null) return this.nodeCache.get(nodeId) ?? null\n\n const shard = await this.loader!.load(shardId)\n\n for (const n of shard.nodes) {\n const existing = this.nodeCache.get(n.id)\n const entry: NodeCacheEntry = {\n ...(existing ?? {}),\n id: n.id,\n scale: n.scale,\n qv: existing?.qv ?? toInt8Array(n.qv),\n neighbors: n.neighbors,\n _l0loaded: true,\n }\n this.nodeCache.set(n.id, entry)\n }\n\n return this.nodeCache.get(nodeId) ?? null\n }\n}\n","/**\n * hybrid-search.ts\n *\n * Combines semantic (HNSW / flat) and lexical (BM25) search results using\n * min-max normalisation and a configurable linear blend.\n */\n\nimport type { SearchEngine, BM25Engine, SearchResult, SearchOptions } from '../types/search'\n\n/** Options accepted by `HybridSearch.search()` */\nexport interface HybridSearchOptions {\n /** Number of results to return (default: 20) */\n topK?: number\n /** Weight applied to normalised semantic scores (default: 0.7) */\n semanticWeight?: number\n /** Weight applied to normalised BM25 scores (default: 0.3) */\n lexicalWeight?: number\n /** HNSW beam width forwarded to the semantic engine (default: 50) */\n ef?: number\n /** Search mode: `'semantic'`, `'lexical'`, or `'hybrid'` (default: `'hybrid'`) */\n mode?: 'semantic' | 'lexical' | 'hybrid'\n}\n\n/** Extended result type used internally during score merging */\ninterface MergedResult extends SearchResult {\n semanticScore: number\n lexicalScore: number\n rawSemanticScore: number\n}\n\n/**\n * Hybrid search combining a semantic vector engine and an optional BM25 engine.\n *\n * In `'hybrid'` mode both engines are queried in parallel; scores are\n * min-max normalised independently and then linearly blended.\n *\n * Example:\n * ```ts\n * const hybrid = new HybridSearch(hnswEngine, bm25Engine, id => titlesMap[id])\n * const results = await hybrid.search(queryVec, 'development finance', { topK: 10 })\n * ```\n */\nexport class HybridSearch {\n private readonly semantic: SearchEngine\n private readonly bm25: BM25Engine | null\n private readonly idToMeta: ((id: number | string) => Partial<SearchResult>) | null\n\n /**\n * @param semanticEngine - Initialised `SearchEngine` (FlatEngine or HNSWEngine)\n * @param bm25Engine - Optional BM25 engine; pass `null` to disable lexical search\n * @param idToMeta - Optional callback to look up display metadata by document ID\n */\n constructor(\n semanticEngine: SearchEngine,\n bm25Engine: BM25Engine | null = null,\n idToMeta: ((id: number | string) => Partial<SearchResult>) | null = null,\n ) {\n this.semantic = semanticEngine\n this.bm25 = bm25Engine\n this.idToMeta = idToMeta\n }\n\n /**\n * Run a hybrid (or single-mode) search query.\n *\n * @param queryVec - L2-normalised query embedding, or `null` for lexical-only mode\n * @param queryText - Raw query string for BM25, or empty string for semantic-only mode\n * @param opts - Search options\n * @returns Top-K results sorted by descending combined score\n */\n async search(\n queryVec: Float32Array | null,\n queryText: string,\n opts?: HybridSearchOptions,\n ): Promise<SearchResult[]> {\n const topK = opts?.topK ?? 20\n const semanticWeight = opts?.semanticWeight ?? 0.7\n const lexicalWeight = opts?.lexicalWeight ?? 0.3\n const ef = opts?.ef ?? 50\n const mode = opts?.mode ?? 'hybrid'\n\n const candidateK = topK * 3\n\n const searchOpts: SearchOptions = { topK: candidateK, ef }\n\n const [semanticResults, lexicalResults] = await Promise.all([\n mode !== 'lexical' && queryVec && this.semantic\n ? Promise.resolve(this.semantic.search(queryVec, searchOpts))\n : Promise.resolve([]),\n mode !== 'semantic' && this.bm25 && queryText\n ? Promise.resolve(this._runBM25(queryText, candidateK))\n : Promise.resolve([]),\n ])\n\n // Unwrap potential Promise from synchronous search implementations\n const semResults = await semanticResults\n const lexResults = await lexicalResults\n\n if (mode === 'semantic') return this._formatResults(semResults, topK, 'semantic')\n if (mode === 'lexical') return this._formatResults(lexResults, topK, 'lexical')\n\n // --- Merge & blend ---\n const scoreMap = new Map<string, MergedResult>()\n\n if (semResults.length > 0) {\n const maxSem = semResults[0].score || 1\n const minSem = semResults[semResults.length - 1].score || 0\n const rangeSem = maxSem - minSem || 1\n for (const r of semResults) {\n const normScore = (r.score - minSem) / rangeSem\n scoreMap.set(String(r.id), {\n ...r,\n title: r.title,\n semanticScore: normScore,\n lexicalScore: 0,\n rawSemanticScore: r.score,\n })\n }\n }\n\n if (lexResults.length > 0) {\n const maxBm25 = lexResults[0].score || 1\n const minBm25 = lexResults[lexResults.length - 1].score || 0\n const rangeBm25 = maxBm25 - minBm25 || 1\n for (const r of lexResults) {\n const normScore = (r.score - minBm25) / rangeBm25\n const idStr = String(r.id)\n if (scoreMap.has(idStr)) {\n scoreMap.get(idStr)!.lexicalScore = normScore\n } else {\n const meta = this.idToMeta ? this.idToMeta(r.id) : {}\n scoreMap.set(idStr, {\n ...r,\n ...meta,\n id: r.id,\n title: meta.title ?? r.title ?? '',\n text: meta.text ?? r.text ?? '',\n semanticScore: 0,\n lexicalScore: normScore,\n rawSemanticScore: 0,\n })\n }\n }\n }\n\n const merged = [...scoreMap.values()].map(r => ({\n ...r,\n score: semanticWeight * r.semanticScore + lexicalWeight * r.lexicalScore,\n }))\n\n merged.sort((a, b) => b.score - a.score)\n return merged.slice(0, topK)\n }\n\n /**\n * Run the BM25 engine and map raw `[docIdx, score]` tuples to `SearchResult` objects.\n *\n * @param queryText - Raw query string\n * @param topK - Maximum number of results to return\n * @returns BM25 results as `SearchResult` objects (score order: descending)\n */\n private _runBM25(queryText: string, topK: number): SearchResult[] {\n if (!this.bm25) return []\n try {\n const raw = this.bm25.search(queryText, topK)\n return raw.map(([docIdx, score]) => {\n const meta = this.idToMeta ? this.idToMeta(docIdx) : {}\n return {\n ...meta,\n id: docIdx,\n score,\n title: meta.title ?? '',\n text: meta.text ?? '',\n } as SearchResult\n })\n } catch (e) {\n console.warn('BM25 search error:', e)\n return []\n }\n }\n\n /**\n * Format single-mode results, adding the appropriate `semanticScore` /\n * `lexicalScore` fields expected by callers.\n *\n * @param results - Raw results from one engine\n * @param topK - Slice limit\n * @param source - Which engine produced the results\n * @returns Results annotated with zeroed-out score fields for the unused engine\n */\n private _formatResults(\n results: SearchResult[],\n topK: number,\n source: 'semantic' | 'lexical',\n ): SearchResult[] {\n return results.slice(0, topK).map(r => ({\n ...r,\n semanticScore: source === 'semantic' ? (r.score ?? 0) : 0,\n lexicalScore: source === 'lexical' ? (r.score ?? 0) : 0,\n }))\n }\n}\n"],"mappings":"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","names":["createWorkerFromUrl","url","r","code","blob","blobUrl","SearchClient","_SearchClient","manifestUrl","opts","e","err","initMsg","type","handler","bucket","h","text","limit","resolve","off","workerUrl","rest","worker","msg","dotProductMixed","queryF32","storedQV","storedScale","dot","len","i","dequantize","qv","scale","out","l2NormalizeInPlace","vec","norm","toInt8Array","arr","fetchJson","url","opts","cacheName","isGz","cached","resp","data","encoding","stream","FlatEngine","url","data","fetchJson","item","toInt8Array","queryVec","opts","topK","threshold","t0","scores","i","dotProductMixed","candidates","a","b","results","extra","k","v","ShardLoader","baseUrl","cacheName","shardSuffix","shardId","promise","data","shardIds","sid","maxEntries","oldest","fetchJson","_sortedInsert","arr","item","score","lo","hi","mid","HNSWEngine","baseUrl","opts","cacheName","manifest","base","idx","configPath","upperPath","nodePath","shardSuffix","config","upperLayers","nodeToShard","fetchJson","ShardLoader","idStr","node","toInt8Array","queryVec","ef","ef_upper","topK","t0","prevCacheSize","entryPoints","layer","results","shardsLoaded","layerStr","seen","W","nodeId","neighbors","nid","s","dotProductMixed","visited","C","cScore","cId","worstInW","cNode","unvisited","n","neededShards","sId","nNode","currentWorst","b","id","cached","shardId","shard","existing","entry","HybridSearch","semanticEngine","bm25Engine","idToMeta","queryVec","queryText","opts","topK","semanticWeight","lexicalWeight","ef","mode","candidateK","searchOpts","semanticResults","lexicalResults","semResults","lexResults","scoreMap","maxSem","minSem","rangeSem","r","normScore","maxBm25","minBm25","rangeBm25","idStr","meta","merged","a","docIdx","score","e","results","source"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai4data/search",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "description": "Framework-agnostic semantic search client: HNSW, BM25, hybrid ranking in a Web Worker",
5
5
  "main": "dist/index.mjs",
6
6
  "module": "dist/index.mjs",
@@ -25,6 +25,7 @@
25
25
  ],
26
26
  "scripts": {
27
27
  "build": "tsup",
28
+ "demo": "npm run build && npx serve . -p 5173",
28
29
  "prepublishOnly": "npm run build",
29
30
  "test": "echo 'Add smoke test if desired'",
30
31
  "lint": "echo 'Add ESLint if desired'"