@ai4data/search 0.1.2 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -42,12 +42,12 @@ When you load the package from a CDN (e.g. via an import map) and your page is o
42
42
  import { SearchClient } from '@ai4data/search'
43
43
 
44
44
  const client = await SearchClient.fromCDN(manifestUrl, {
45
- workerUrl: 'https://esm.sh/@ai4data/search@0.1.0/worker',
45
+ workerUrl: 'https://esm.sh/@ai4data/search@0.1.0/worker', // or unpkg.com/.../dist/worker.mjs
46
46
  })
47
47
  client.on('results', ({ data }) => console.log(data))
48
48
  ```
49
49
 
50
- This works from any static host (GitHub Pages, localhost, etc.) with no build step. If you see a worker error in the console, check the detailed message (filename and line). If the worker fails to start when using `fromCDN`, try serving the demo from the package root so the worker loads from `/dist/worker.mjs` (same-origin) instead.
50
+ If you pass an esm.sh worker URL, the client automatically fetches the **raw** worker bundle from unpkg (esm.sh returns a wrapper that fails when run from a blob). You can also pass `https://unpkg.com/@ai4data/search@VERSION/dist/worker.mjs` directly. This works from any static host with no build step.
51
51
 
52
52
  ### Custom worker path (bundler)
53
53
 
package/dist/index.d.mts CHANGED
@@ -359,14 +359,6 @@ interface SearchClientOptions {
359
359
  type MessageHandler<T extends WorkerOutboundMessage['type']> = (msg: Extract<WorkerOutboundMessage, {
360
360
  type: T;
361
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
362
  declare function createWorkerFromUrl(url: string): Promise<Worker>;
371
363
  declare class SearchClient {
372
364
  /** True once the index + BM25 corpus are loaded. Lexical search available. */
package/dist/index.mjs CHANGED
@@ -1,2 +1,2 @@
1
- function P(i){let e=i.includes("esm.sh")&&!i.includes("?")?`${i}?bundle`:i;return fetch(e,{mode:"cors"}).then(t=>{if(!t.ok)throw new Error(`Failed to fetch worker: ${t.status} ${t.statusText}`);return t.text()}).then(t=>{let r=new Blob([t],{type:"application/javascript"}),o=URL.createObjectURL(r);return new Worker(o,{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=>{let n=s.message||s.message||"Worker error",a=s.filename||"",c=s.lineno??"";console.error("[SearchClient] Worker error:",n,a?`at ${a}`:"",c?`:${c}`:""),this.loadingMessage=`Search worker error: ${n}`};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 P(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 F(i,e){let t=new Float32Array(i.length);for(let r=0;r<i.length;r++)t[r]=i[r]*e;return t}function L(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 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 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 C(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 E=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 v=(g.score-b)/W;p.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,W=y-b||1;for(let g of m){let v=(g.score-b)/W,N=String(g.id);if(p.has(N))p.get(N).lexicalScore=v;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:v,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,E as HybridSearch,R as SearchClient,P as createWorkerFromUrl,F as dequantize,w as dotProductMixed,S as fetchJson,L as l2NormalizeInPlace,x as toInt8Array};
1
+ function P(a){let e=a.match(/esm\.sh\/@ai4data\/search@([^/]+)\/worker/);return e?`https://unpkg.com/@ai4data/search@${e[1].split("?")[0]}/dist/worker.mjs`:(a.match(/cdn\.jsdelivr\.net\/npm\/@ai4data\/search@([^/]+)\//),a)}function O(a){let e=P(a);return fetch(e,{mode:"cors"}).then(t=>{if(!t.ok)throw new Error(`Failed to fetch worker: ${t.status} ${t.statusText}`);return t.text()}).then(t=>{let r=t.trim();if(r.startsWith("<!")||r.startsWith("<html"))throw new Error("Worker URL returned HTML (likely 404 or error page). Check the worker URL and that the package is published.");if(r.length<1e3)throw new Error(`Worker script too short (${r.length} chars). Expected a bundled script. Check the worker URL.`);if(/^\s*import\s+/.test(r))throw new Error("Worker URL returned a wrapper with import statements; it cannot run from a blob. Use a CDN that serves the raw bundle (e.g. unpkg.com/@ai4data/search@VERSION/dist/worker.mjs).");let n=new Blob([t],{type:"application/javascript"}),s=URL.createObjectURL(n);try{return new Worker(s,{type:"module"})}catch(o){throw URL.revokeObjectURL(s),o instanceof Error?o:new Error(String(o))}})}var _=class a{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=>{let o=s,i=o.error?.message??o.message??o.message??"Worker error (no details; check DevTools Console for the worker context or Network tab for failed requests)",c=o.filename||"",l=o.lineno??"";console.error("[SearchClient] Worker error:",i,c?`at ${c}`:"",l?`:${l}`:"",o.error?o.error.stack:""),this.loadingMessage=`Search worker error: ${i}`};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())}static fromCDN(e,t){let{workerUrl:r,...n}=t;return O(r).then(s=>new a(e,{...n,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 b(a,e,t){let r=0,n=a.length;for(let s=0;s<n;s++)r+=a[s]*(e[s]*t);return r}function F(a,e){let t=new Float32Array(a.length);for(let r=0;r<a.length;r++)t[r]=a[r]*e;return t}function U(a){let e=0;for(let t=0;t<a.length;t++)e+=a[t]*a[t];if(e=Math.sqrt(e),e<1e-9)return a;for(let t=0;t<a.length;t++)a[t]/=e;return a}function k(a){return new Int8Array(a)}async function S(a,e){let t=e?.cacheName??null,r=a.endsWith(".gz");if(t&&typeof caches<"u")try{let i=await(await caches.open(t)).match(a);if(i)return i.json()}catch{}let n=await fetch(a);if(!n.ok)throw new Error(`fetchJson: HTTP ${n.status}: ${a}`);let s;if(r){let o=n.headers.get("Content-Encoding");if(o==="gzip"||o==="x-gzip")s=await n.json();else{let i=n.body.pipeThrough(new DecompressionStream("gzip"));s=await new Response(i).json()}}else s=await n.json();if(t&&typeof caches<"u")try{(await caches.open(t)).put(a,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:k(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]=b(e,d.qv,d.scale)}let i=[];for(let l=0;l<o.length;l++)o[l]>=n&&i.push(l);i.sort((l,d)=>o[d]-o[l]);let c=i.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 x(a,e){let t=e[0],r=0,n=a.length;for(;r<n;){let s=r+n>>>1;a[s][0]<t?r=s+1:n=s}a.splice(r,0,e)}var L=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??{},i=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(i,{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:k(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(),i=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-i+(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,i=[];for(let[,c]of t){if(o.has(c))continue;o.add(c);let l=this._scoreUpperNode(e,c);x(i,[l,c]),i.length>n&&i.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);x(i,[h,u]),i.length>n&&i.shift()}}return i}_scoreUpperNode(e,t){let r=this.nodeCache.get(t);return r?b(e,r.qv,r.scale):-1/0}async _beamSearchLayer0(e,t,r){let n=new Set,s=[],o=[];for(let[,i]of t){if(n.has(i))continue;n.add(i);let c=await this._getLayer0Node(i);if(!c)continue;let l=b(e,c.qv,c.scale);x(s,[l,i]),x(o,[l,i])}for(s.length>r&&(s=s.slice(-r)),o.length>r&&(o=o.slice(-r));o.length>0;){let[i,c]=o.pop(),l=s.length>=r?s[0][0]:-1/0;if(i<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=b(e,m.qv,m.scale),M=s.length>=r?s[0][0]:-1/0;(f>M||s.length<r)&&(x(o,[f,h]),x(s,[f,h]),s.length>r&&s.shift())}}return s.sort((i,c)=>c[0]-i[0]).map(([i,c])=>({id:c,score:i,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),i={...o??{},id:s.id,scale:s.scale,qv:o?.qv??k(s.qv),neighbors:s.neighbors,_l0loaded:!0};this.nodeCache.set(s.id,i)}return this.nodeCache.get(e)??null}};var E=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,i=r?.ef??50,c=r?.mode??"hybrid",l=n*3,d={topK:l,ef:i},[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,w=h[h.length-1].score||0,W=y-w||1;for(let g of h){let v=(g.score-w)/W;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,w=m[m.length-1].score||0,W=y-w||1;for(let g of m){let v=(g.score-w)/W,N=String(g.id);if(f.has(N))f.get(N).lexicalScore=v;else{let R=this.idToMeta?this.idToMeta(g.id):{};f.set(N,{...g,...R,id:g.id,title:R.title??g.title??"",text:R.text??g.text??"",semanticScore:0,lexicalScore:v,rawSemanticScore:0})}}}let M=[...f.values()].map(y=>({...y,score:s*y.semanticScore+o*y.lexicalScore}));return M.sort((y,w)=>w.score-y.score),M.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,L as HNSWEngine,E as HybridSearch,_ as SearchClient,O as createWorkerFromUrl,F as dequantize,b as dotProductMixed,S as fetchJson,U as l2NormalizeInPlace,k 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 * 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 // Request with ?bundle so esm.sh returns a single file (no sub-imports that would fail from blob)\n const fetchUrl = url.includes('esm.sh') && !url.includes('?') ? `${url}?bundle` : url\n return fetch(fetchUrl, { 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: ErrorEvent) => {\n const msg =\n (err as ErrorEvent).message ||\n (err as unknown as { message?: string }).message ||\n 'Worker error'\n const filename = (err as ErrorEvent).filename || ''\n const lineno = (err as ErrorEvent).lineno ?? ''\n console.error('[SearchClient] Worker error:', msg, filename ? `at ${filename}` : '', lineno ? `:${lineno}` : '')\n this.loadingMessage = `Search worker error: ${msg}`\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","fetchUrl","r","code","blob","blobUrl","SearchClient","_SearchClient","manifestUrl","opts","e","err","msg","filename","lineno","initMsg","type","handler","bucket","h","text","limit","resolve","off","workerUrl","rest","worker","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 */\n/**\n * Resolve a CDN worker URL to one that returns a single script (no import statements).\n * ESM.sh returns a wrapper with imports that fail when run from a blob: URL; unpkg/jsDelivr\n * serve the raw dist/worker.mjs which is self-contained.\n */\nfunction getBundledWorkerUrl(url: string): string {\n const esmMatch = url.match(/esm\\.sh\\/@ai4data\\/search@([^/]+)\\/worker/)\n if (esmMatch) {\n const version = esmMatch[1].split('?')[0]\n return `https://unpkg.com/@ai4data/search@${version}/dist/worker.mjs`\n }\n const jdelivrMatch = url.match(/cdn\\.jsdelivr\\.net\\/npm\\/@ai4data\\/search@([^/]+)\\//)\n if (jdelivrMatch) return url\n return url\n}\n\nexport function createWorkerFromUrl(url: string): Promise<Worker> {\n const fetchUrl = getBundledWorkerUrl(url)\n return fetch(fetchUrl, { 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 trimmed = code.trim()\n if (trimmed.startsWith('<!') || trimmed.startsWith('<html'))\n throw new Error('Worker URL returned HTML (likely 404 or error page). Check the worker URL and that the package is published.')\n if (trimmed.length < 1000)\n throw new Error(`Worker script too short (${trimmed.length} chars). Expected a bundled script. Check the worker URL.`)\n // Reject wrapper scripts that would fail from blob (imports like /node/... don't resolve from blob:)\n if (/^\\s*import\\s+/.test(trimmed))\n throw new Error(\n 'Worker URL returned a wrapper with import statements; it cannot run from a blob. Use a CDN that serves the raw bundle (e.g. unpkg.com/@ai4data/search@VERSION/dist/worker.mjs).'\n )\n const blob = new Blob([code], { type: 'application/javascript' })\n const blobUrl = URL.createObjectURL(blob)\n try {\n return new Worker(blobUrl, { type: 'module' })\n } catch (e) {\n URL.revokeObjectURL(blobUrl)\n throw e instanceof Error ? e : new Error(String(e))\n }\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: ErrorEvent) => {\n const e = err as ErrorEvent & { error?: Error }\n const msg =\n e.error?.message ??\n e.message ??\n (e as unknown as { message?: string }).message ??\n 'Worker error (no details; check DevTools Console for the worker context or Network tab for failed requests)'\n const filename = e.filename || ''\n const lineno = e.lineno ?? ''\n console.error('[SearchClient] Worker error:', msg, filename ? `at ${filename}` : '', lineno ? `:${lineno}` : '', e.error ? e.error.stack : '')\n this.loadingMessage = `Search worker error: ${msg}`\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":["getBundledWorkerUrl","url","esmMatch","createWorkerFromUrl","fetchUrl","r","code","trimmed","blob","blobUrl","e","SearchClient","_SearchClient","manifestUrl","opts","err","msg","filename","lineno","initMsg","type","handler","bucket","h","text","limit","resolve","off","workerUrl","rest","worker","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","a","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","b","docIdx","score","e","results","source"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ai4data/search",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",