Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
GitHub - SiluPanda/embed-cache: Content-addressable embedding cache with deduplication and TTL · GitHub
Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/embed-cache: Content-addressable embedding cache with deduplication and TTL · GitHub
Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/embed-cache: Content-addressable embedding cache with deduplication and TTL · GitHub
Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' GitHub - SiluPanda/embed-cache: Content-addressable embedding cache with deduplication and TTL · GitHub
Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/embed-cache: Content-addressable embedding cache with deduplication and TTL · GitHub
Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' GitHub - SiluPanda/embed-cache: Content-addressable embedding cache with deduplication and TTL · GitHub
Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); GitHub - SiluPanda/embed-cache: Content-addressable embedding cache with deduplication and TTL · GitHub
Skip to content

Repository files navigation

embed-cache

Content-addressable embedding cache with deduplication, LRU eviction, TTL support, and batch optimization. Zero external runtime dependencies -- caller supplies the embedder function.

npm versionnpm downloadslicensenodeTypeScript


Description

Embedding API calls are the dominant ongoing cost in most RAG (Retrieval-Augmented Generation) pipelines. The same text is routinely embedded multiple times: documents are re-indexed on restart, chunked text reappears across overlapping documents, periodic re-indexing jobs sweep all content even when most of it has not changed, and parallel ingestion workers independently embed the same source files.

embed-cache wraps any embedding function with a transparent, content-addressable cache. Cache keys are derived from the text content itself (SHA-256 of normalized text + model ID), so identical text always hits the cache regardless of what called it or when. When text has not changed, the API is never called. When it has changed, only the changed text is re-embedded.

Key properties:

  • Content-addressable keys -- same text + same model always produces the same cache key.
  • Batch optimization -- embedBatch() collects all cache misses and makes a single embedder call.
  • Change detection -- track documents by ID and detect when content has changed before re-embedding.
  • Cost tracking -- hit rate, estimated tokens saved, and estimated dollar cost avoided.
  • LRU eviction -- configurable maximum cache size with least-recently-used eviction.
  • TTL expiry -- entries expire after a configurable time-to-live, per-entry or globally.
  • Zero runtime dependencies -- only uses Node.js built-in node:crypto. You bring your own embedder.

Installation

npm install embed-cache

Requires Node.js 18 or later.


Quick Start

import{createCache}from'embed-cache';constcache=createCache({embedder: async(texts)=>{// Call OpenAI, Cohere, or any embedding APIconstresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);},model: 'text-embedding-3-small',maxSize: 50_000,ttl: 60*60*1000,// 1 hour});// Single embed -- repeated calls never invoke the embedder twice for the same textconstvec=awaitcache.embed('Hello world');// Batch embed -- collects all cache misses and makes ONE embedder callconstvecs=awaitcache.embedBatch(['Hello','World','Hello']);// Only calls embedder with ['World'] if 'Hello' is already cached// Check statsconsts=cache.stats();console.log(s.hitRate);// 0-1console.log(s.tokensEstimatedSaved);// estimated tokens saved via cache hitsconsole.log(s.costEstimatedSaved);// estimated USD saved

Features

Batch Optimization

embedBatch() separates hits from misses before calling the embedder:

  1. Compute a content-addressable key for every text in the batch.
  2. Look up all keys in the cache. Cached vectors are returned immediately.
  3. Collect all misses into a single array.
  4. Call embedder(missedTexts) once.
  5. Store the new vectors and return all results in the original input order.

This minimizes API calls when a batch contains repeated or previously seen texts.

Change Detection

Track documents by ID so you can skip re-embedding when content has not changed:

awaitcache.trackDocument('doc-42',content);// Later, check if the document has changedif(awaitcache.hasChanged('doc-42',newContent)){// Content changed -- re-embedawaitcache.trackDocument('doc-42',newContent);constvecs=awaitcache.embedBatch(chunks);}

hasChanged() computes a SHA-256 hash of the content and compares it to the stored hash. For untracked documents, it returns true.

Text Normalization

Before computing cache keys, text is normalized to collapse cosmetic variations that produce identical embeddings:

  1. Unicode NFC normalization
  2. Trim leading and trailing whitespace
  3. Collapse runs of internal whitespace to a single space

The normalized form is used only for key computation. The original text is passed to the embedder unchanged.

Normalization is enabled by default. Set normalizeText: false to disable it.

Model-Aware Keys

The model identifier is included in every cache key. Vectors from different models are never mixed. Changing the model option automatically separates the key namespace -- no explicit cache bust is required.

Known model aliases are canonicalized automatically:

InputCanonical form
text-embedding-3-smallopenai/text-embedding-3-small
text-embedding-3-largeopenai/text-embedding-3-large
text-embedding-ada-002openai/text-embedding-ada-002
embed-english-v3.0cohere/embed-english-v3.0
embed-multilingual-v3.0cohere/embed-multilingual-v3.0

Unknown model strings are lowercased and used as-is.

LRU Eviction

When the cache reaches maxSize, the least recently used entry is evicted to make room. Every cache hit promotes the accessed entry to the front of the LRU list. Eviction is O(1) via a doubly-linked list.

TTL Expiry

Entries expire lazily on access. When a cached entry is read after its TTL has elapsed, it is deleted and treated as a cache miss. TTL can be set globally via the ttl option or overridden per-call via EmbedOptions.

Cost Tracking

The cache estimates tokens saved on each hit using a character-to-token approximation (Math.ceil(text.length / 4)) and computes dollar cost avoided using the configured modelPricePerMillion.

Serialization

Export the entire cache state as a JSON string for persistence or transfer:

constdata=cache.serialize();// data is a JSON string: { entries: [...], model: "...", version: 1 }

API Reference

createCache(options: EmbedCacheOptions): EmbedCache

Factory function. Creates and returns a new EmbedCache instance.

import{createCache}from'embed-cache';constcache=createCache({embedder: myEmbedderFn,model: 'text-embedding-3-small',});

Parameters:

ParameterTypeRequiredDefaultDescription
options.embedderEmbedderFnYes--Function that accepts an array of texts and returns an array of embedding vectors.
options.modelstringYes--Model identifier. Included in cache keys to namespace entries by model.
options.ttlnumberNoundefinedDefault time-to-live in milliseconds for all cache entries.
options.maxSizenumberNo10000Maximum number of cached entries. LRU eviction kicks in when this limit is reached.
options.modelPricePerMillionnumberNo0.1Price in USD per 1 million tokens. Used for cost savings estimation.
options.algorithm'sha256' | 'sha1' | 'md5'No'sha256'Hash algorithm for cache key derivation.
options.normalizeTextbooleanNotrueWhether to apply NFC normalization, trim, and whitespace collapsing before hashing.

Returns:EmbedCache


EmbedCache.embed(text: string, options?: EmbedOptions): Promise<number[]>

Embed a single text string. Returns the embedding vector from the cache if available, otherwise calls the embedder, caches the result, and returns it.

constvector=awaitcache.embed('Hello world');

Parameters:

ParameterTypeRequiredDefaultDescription
textstringYes--The text to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for this specific entry.
options.bypassCachebooleanNofalseWhen true, skip the cache lookup and always call the embedder. The result is not stored in the cache.

Returns:Promise<number[]> -- the embedding vector.


EmbedCache.embedBatch(texts: string[], options?: EmbedOptions): Promise<number[][]>

Embed multiple texts in a single call. Looks up all texts in the cache, collects misses, calls the embedder once for all misses, caches the results, and returns all vectors in the original input order.

constvectors=awaitcache.embedBatch(['Hello','World','Hello']);// vectors[0] and vectors[2] are the same (both from 'Hello')// The embedder was only called with the uncached texts

Parameters:

ParameterTypeRequiredDefaultDescription
textsstring[]Yes--Array of texts to embed.
options.ttlnumberNoglobal ttlOverride the default TTL for entries created by this call.
options.bypassCachebooleanNofalseWhen true, skip all cache lookups and call the embedder with all texts.

Returns:Promise<number[][]> -- array of embedding vectors in the same order as the input texts.


EmbedCache.hasChanged(docId: string, content: string): Promise<boolean>

Check whether a tracked document's content has changed since it was last tracked.

constchanged=awaitcache.hasChanged('doc-42',newContent);// true if content differs from last trackDocument call, or if docId is untracked

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesCurrent content to compare against the stored hash.

Returns:Promise<boolean> -- true if the content has changed or the document is untracked, false if the content matches.


EmbedCache.trackDocument(docId: string, content: string): Promise<void>

Record a document's content hash for future change detection via hasChanged().

awaitcache.trackDocument('doc-42',content);

Parameters:

ParameterTypeRequiredDescription
docIdstringYesUnique identifier for the document.
contentstringYesDocument content to hash and store.

Returns:Promise<void>


EmbedCache.stats(): CacheStats

Return current cache statistics including hit rate, token savings, and cost savings.

consts=cache.stats();console.log(s.hitRate);// 0.75console.log(s.tokensEstimatedSaved);// 12500console.log(s.costEstimatedSaved);// 0.0025

Returns:CacheStats object with the following fields:

FieldTypeDescription
totalRequestsnumberTotal number of embed/embedBatch lookups performed.
hitsnumberNumber of cache hits.
missesnumberNumber of cache misses.
hitRatenumberRatio of hits to total requests (0 to 1). Returns 0 when no requests have been made.
sizenumberCurrent number of entries in the cache.
tokensEstimatedSavednumberEstimated total tokens saved via cache hits.
costEstimatedSavednumberEstimated USD saved, computed as tokensEstimatedSaved / 1_000_000 * modelPricePerMillion.
modelstringThe model identifier this cache was created with.
createdAtstringISO 8601 timestamp of when the cache was created.

EmbedCache.serialize(): string

Serialize the entire cache state to a JSON string. The output includes all cached entries, the model identifier, and a version field.

constjson=cache.serialize();// Store to disk, transfer to another environment, etc.

Returns:string -- JSON string with the structure:

{
"entries": [
{ "key": "abc123...", "vector": [0.1, 0.2, ...] }
],
"model": "text-embedding-3-small",
"version": 1
}

EmbedCache.clear(): void

Remove all cached entries and reset all statistics.

cache.clear();console.log(cache.size);// 0

EmbedCache.size: number (read-only)

The current number of entries in the cache.

console.log(cache.size);// 42

Types

EmbedderFn

typeEmbedderFn=(texts: string[])=>Promise<number[][]>;

A function that accepts an array of text strings and returns a promise resolving to an array of embedding vectors. Each vector is a number[]. The returned array must have the same length as the input array, with vectors in corresponding order.

EmbedCacheOptions

interfaceEmbedCacheOptions{embedder: EmbedderFn;model: string;ttl?: number;maxSize?: number;modelPricePerMillion?: number;algorithm?: 'sha256'|'sha1'|'md5';normalizeText?: boolean;}

EmbedOptions

interfaceEmbedOptions{ttl?: number;bypassCache?: boolean;}

CacheStats

interfaceCacheStats{totalRequests: number;hits: number;misses: number;hitRate: number;size: number;tokensEstimatedSaved: number;costEstimatedSaved: number;model: string;createdAt: string;}

EmbedCache

interfaceEmbedCache{embed(text: string,options?: EmbedOptions): Promise<number[]>;embedBatch(texts: string[],options?: EmbedOptions): Promise<number[][]>;hasChanged(docId: string,content: string): Promise<boolean>;trackDocument(docId: string,content: string): Promise<void>;stats(): CacheStats;serialize(): string;clear(): void;readonlysize: number;}

Configuration

Hash Algorithms

The algorithm option controls which hash function is used for cache key derivation:

AlgorithmKey lengthSpeedCollision resistance
sha256 (default)64 hex charsFastExcellent -- no known collisions
sha140 hex charsFasterWeak -- not recommended for adversarial inputs
md532 hex charsFastestBroken -- use only when speed matters more than security

For virtually all use cases, the default sha256 is recommended. Hash computation for a 2 KB text chunk takes under 0.05ms.

Model Price Defaults

When modelPricePerMillion is not provided, it defaults to 0.1 USD per million tokens. For accurate cost tracking, provide the actual price for your model. Reference prices for common models:

ModelPrice per 1M tokens (USD)
text-embedding-3-small$0.02
text-embedding-3-large$0.13
text-embedding-ada-002$0.10
embed-english-v3.0$0.10
embed-multilingual-v3.0$0.10

LRU and TTL Interaction

When both maxSize and ttl are configured, both mechanisms are active independently. An entry can be evicted by LRU pressure (cache is full and the entry is the least recently used) or by TTL expiry (entry is older than its TTL). TTL expiry is lazy -- expired entries are only removed when accessed.


Error Handling

  • Embedder errors propagate. If the embedder function throws during embed() or embedBatch(), the error is propagated to the caller. Nothing is written to the cache for the failed call.
  • TTL expiry is transparent. Expired entries are silently removed on access and treated as cache misses. The embedder is called to produce a fresh vector.
  • LRU eviction is silent. When the cache is full, the least recently used entry is evicted without notification.

Advanced Usage

Bypass Cache for Specific Calls

Force a fresh embedding even when the text is cached:

constfresh=awaitcache.embed('Hello',{bypassCache: true});

Per-Entry TTL Override

Set a custom TTL for a specific embed call, overriding the global default:

// This entry expires in 5 seconds, regardless of the global TTLconstvec=awaitcache.embed('time-sensitive query',{ttl: 5000});

Document Re-Indexing Pipeline

Combine change detection with batch embedding for efficient document re-indexing:

constcache=createCache({embedder: myEmbedder,model: 'text-embedding-3-small',modelPricePerMillion: 0.02,});for(constdocofdocuments){if(awaitcache.hasChanged(doc.id,doc.content)){constchunks=chunkDocument(doc.content);constvectors=awaitcache.embedBatch(chunks);awaitvectorStore.upsert(doc.id,chunks,vectors);awaitcache.trackDocument(doc.id,doc.content);}}console.log(cache.stats().costEstimatedSaved);// USD saved

Export and Restore Cache State

Serialize the cache for persistence or transfer between environments:

import{writeFileSync,readFileSync}from'fs';// Exportconstdata=cache.serialize();writeFileSync('embedding-cache.json',data);// The serialized format is a JSON string containing all entries,// the model identifier, and a version field for forward compatibility.

Custom Embedder Functions

Any function matching the EmbedderFn signature works as an embedder:

import{createCache,typeEmbedderFn}from'embed-cache';// OpenAIconstopenaiEmbedder: EmbedderFn=async(texts)=>{constresp=awaitopenai.embeddings.create({model: 'text-embedding-3-small',input: texts,});returnresp.data.map((d)=>d.embedding);};// CohereconstcohereEmbedder: EmbedderFn=async(texts)=>{constresp=awaitcohere.embed({model: 'embed-english-v3.0',
texts,inputType: 'search_document',});returnresp.embeddings;};// Local model (e.g., via HTTP)constlocalEmbedder: EmbedderFn=async(texts)=>{constresp=awaitfetch('http://localhost:8080/embed',{method: 'POST',body: JSON.stringify({ texts }),headers: {'Content-Type': 'application/json'},});constjson=awaitresp.json();returnjson.embeddings;};

TypeScript

embed-cache is written in TypeScript with strict mode enabled. All public types are exported from the package entry point:

import{createCache,typeEmbedderFn,typeEmbedCacheOptions,typeEmbedOptions,typeCacheStats,typeEmbedCache,}from'embed-cache';

Type declarations are included in the published package (dist/index.d.ts).


License

MIT

About

Content-addressable embedding cache with deduplication and TTL

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages