@mantlejs/embeddings
Auto-embed-on-write hook for Mantle JS. Calls a pluggable embedding provider and upserts the result into a VectorRepository<T> after a create/update/patch — “one line instead of a hand-written trigger.”
Installation
Section titled “Installation”npm install @mantlejs/embeddingsConcepts
Section titled “Concepts”Why this package exists
Section titled “Why this package exists”@mantlejs/pinecone, @mantlejs/qdrant, @mantlejs/mongodb’s MongoVectorRepository, and @mantlejs/knex’s KnexVectorRepository are all deliberately model-agnostic: the caller must always supply an already-computed number[] vector — none of them generates an embedding from source text. embed() is the missing “text in, vector attached” step, as a hook rather than something every service hand-rolls in its own create/update/patch methods.
Bring your own provider
Section titled “Bring your own provider”@mantlejs/embeddings never bundles a specific embedding model — no OpenAI/Cohere/etc. SDK dependency. EmbeddingProvider is a one-method interface:
interface EmbeddingProvider { dimensions?: number; embed(text: string): Promise<number[]>;}Wrap whichever provider you use behind it — an HTTP call to an embedding API, a local model, or (for demos) a deterministic hash-based stand-in.
The cross-adapter write-consistency pattern, made concrete
Section titled “The cross-adapter write-consistency pattern, made concrete”See the root README’s “Services with multiple repositories” section for the general pattern; embed() is its reference implementation:
- Idempotent, keyed on the source record’s id.
upsertVector(id, vector, data)is an upsert on every adapter — re-runningembed()for the same id (a retry, or a second edit) replaces the vector at that id rather than duplicating it. This comes from the adapter contract, not from anythingembed()does itself. - Safe to retry. Because it’s an idempotent upsert, calling it again after a partial failure is always safe.
- Non-fatal on failure. A provider error or a vector-store error is caught inside the hook and never rethrown — the primary write already succeeded (or already failed for its own reason), and a degraded embedding provider must never turn that into a failed request for the caller.
Attach after create/update/patch — never after find/get/remove
Section titled “Attach after create/update/patch — never after find/get/remove”embed() reads the just-written record off ctx.result. Attach it to after.create/after.update/after.patch, where a freshly-written single record is guaranteed to be there. It’s a no-op (skips) if ctx.result is an array or a paginated page — i.e. it’s safe to not worry about accidentally attaching it to after.find, but there’s still nothing useful for it to do there.
Quick start
Section titled “Quick start”import { mantle } from "@mantlejs/mantle";import { http } from "@mantlejs/http";import { knex, KnexVectorRepository } from "@mantlejs/knex";import { embed } from "@mantlejs/embeddings";import type { EmbeddingProvider } from "@mantlejs/embeddings";
const app = mantle() .configure(http()) .configure(knex({ client: "pg", connection: process.env.DATABASE_URL }));
class ArticleVectorRepository extends KnexVectorRepository<Article> { readonly tableName = "articles"; readonly vectorColumn = "embedding";}
const provider: EmbeddingProvider = { dimensions: 1536, async embed(text) { const res = await fetch("https://api.openai.com/v1/embeddings", { method: "POST", headers: { Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, "Content-Type": "application/json" }, body: JSON.stringify({ model: "text-embedding-3-small", input: text }), }); const { data } = await res.json(); return data[0].embedding; },};
const articleVectors = new ArticleVectorRepository(app);const embedArticle = embed({ vectors: articleVectors, provider, field: ["title", "body"] });
app.use("articles", new ArticleService(new ArticleRepository(app)));app.service("articles").hooks({ after: { create: [embedArticle], update: [embedArticle], patch: [embedArticle], },});embed(options)
Section titled “embed(options)”Returns a HookFunction<T>.
| Field | Type | Default | Description |
|---|---|---|---|
vectors |
VectorRepository<T> |
— | Where the computed embedding is upserted (required) |
provider |
EmbeddingProvider |
— | Called with the extracted text to produce the vector (required) |
field |
keyof T | Array<keyof T> | ((record: T) => string) |
— | Source text: one field, several fields joined with "\n", or a function (required) |
onError |
(error, record, ctx) => void |
logs via app.get<Logger>("logger") |
Called when extraction, the provider, or the upsert throws. Never fails the primary write. |
EmbeddingProvider
Section titled “EmbeddingProvider”interface EmbeddingProvider { dimensions?: number; embed(text: string): Promise<number[]>;}import type { EmbedOptions, EmbeddingProvider } from "@mantlejs/embeddings";| Type | Description |
|---|---|
EmbedOptions<T> |
Options passed to embed() |
EmbeddingProvider |
The pluggable text-to-vector contract |
Development
Section titled “Development”npx nx build embeddings # compilenpx nx test embeddings # run testsnpx nx lint embeddings # lintPublishing
Section titled “Publishing”Build before publishing:
npx nx build embeddingsFirst publish (scoped packages require --access public):
cd packages/embeddingsnpm publish --access publicSubsequent releases — bump version in packages/embeddings/package.json, then:
cd packages/embeddingsnpm publishTesting locally with Verdaccio
Section titled “Testing locally with Verdaccio”# Terminal 1 — start the local registrynpx nx run @mantle/source:local-registry
# Terminal 2 — publish to itcd packages/embeddingsnpm publish --registry http://localhost:4873