2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Unified Embedding Manager
|
2025-12-17 17:42:37 -08:00
|
|
|
*
|
2025-09-02 10:00:52 -07:00
|
|
|
* THE single source of truth for all embedding operations in Brainy.
|
2026-01-06 12:52:34 -08:00
|
|
|
* Uses Candle WASM inference for universal compatibility.
|
2025-12-17 17:42:37 -08:00
|
|
|
*
|
2025-09-02 10:00:52 -07:00
|
|
|
* Features:
|
|
|
|
|
* - Singleton pattern ensures ONE model instance
|
2026-01-06 12:52:34 -08:00
|
|
|
* - Candle WASM (no transformers.js or ONNX Runtime dependency)
|
2025-12-17 17:42:37 -08:00
|
|
|
* - Bundled model (no runtime downloads)
|
|
|
|
|
* - Works everywhere: Node.js, Bun, Bun --compile, browsers
|
2025-09-02 10:00:52 -07:00
|
|
|
* - Memory monitoring
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { Vector, EmbeddingFunction } from '../coreTypes.js'
|
2025-12-17 17:42:37 -08:00
|
|
|
import { WASMEmbeddingEngine } from './wasm/index.js'
|
test(8.0): Tier-1 integration via deterministic embedder (suite runnable again)
The integration suite loaded the real WASM model for every test, so a full run
took ~hours and reliably aborted (vitest reporter timeout) — which is why it was
never gated and silently rotted. Brainy already separates the embedder from all
other logic, so running integration with a deterministic embedder keeps every
code path real (HNSW build+search, graph traversal, metadata filtering,
transactions, storage/sharding, VFS, aggregation, import) while making the suite
complete in ~2 min on ANY machine — no GPU, no 16 GB requirement.
- Add isDeterministicEmbedMode() — a single source of truth for the test-embedder
switch (BRAINY_DETERMINISTIC_EMBEDDINGS, plus the legacy BRAINY_UNIT_TEST alias),
used by EmbeddingManager (init/embed/embedBatch) and brainy's eager-init guard.
- setup-integration.ts opts into it: Tier 1 = functional end-to-end. Real-model
semantic quality + the real embedding pipeline move to a Tier-2 suite.
Full integration now runs 482 passing / 583 in ~2 min (was un-completable). The
remaining failures are pre-existing test rot, fixed next. Unit 1414 green.
2026-06-16 12:44:14 -07:00
|
|
|
import { isDeterministicEmbedMode } from './deterministicEmbedMode.js'
|
2025-09-02 10:00:52 -07:00
|
|
|
|
2026-06-11 14:51:00 -07:00
|
|
|
declare global {
|
|
|
|
|
/**
|
|
|
|
|
* Unit-test escape hatch set by the test harness (tests/setup-unit.ts):
|
|
|
|
|
* when truthy, EmbeddingManager serves deterministic mock embeddings
|
|
|
|
|
* instead of loading the real model. Guarded against production use in
|
|
|
|
|
* init()/embed()/embedBatch(). Ambient `var` is required here — `let`/
|
|
|
|
|
* `const` in `declare global` do not attach to `globalThis`.
|
|
|
|
|
*/
|
|
|
|
|
var __BRAINY_UNIT_TEST__: boolean | undefined
|
|
|
|
|
}
|
|
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Types
|
|
|
|
|
export type ModelPrecision = 'q8' | 'fp32'
|
|
|
|
|
|
|
|
|
|
interface EmbeddingStats {
|
|
|
|
|
initialized: boolean
|
|
|
|
|
precision: ModelPrecision
|
|
|
|
|
modelName: string
|
|
|
|
|
embedCount: number
|
|
|
|
|
initTime: number | null
|
|
|
|
|
memoryMB: number | null
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Global state for true singleton across entire process
|
|
|
|
|
let globalInstance: EmbeddingManager | null = null
|
|
|
|
|
let globalInitPromise: Promise<void> | null = null
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Unified Embedding Manager - Clean, simple, reliable
|
2025-12-17 17:42:37 -08:00
|
|
|
*
|
2026-01-06 12:52:34 -08:00
|
|
|
* Now powered by Candle WASM for universal compatibility.
|
2025-09-02 10:00:52 -07:00
|
|
|
*/
|
|
|
|
|
export class EmbeddingManager {
|
2025-12-17 17:42:37 -08:00
|
|
|
private engine: WASMEmbeddingEngine
|
|
|
|
|
private precision: ModelPrecision = 'q8'
|
|
|
|
|
private modelName = 'all-MiniLM-L6-v2'
|
2025-09-02 10:00:52 -07:00
|
|
|
private initialized = false
|
|
|
|
|
private initTime: number | null = null
|
|
|
|
|
private embedCount = 0
|
|
|
|
|
private locked = false
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
private constructor() {
|
2025-12-17 17:42:37 -08:00
|
|
|
this.engine = WASMEmbeddingEngine.getInstance()
|
2026-03-22 14:53:48 -07:00
|
|
|
// Log deferred to init() — at construction time we don't know if a plugin
|
|
|
|
|
// (like Cortex) will replace the WASM embedder with a native one.
|
2025-09-02 10:00:52 -07:00
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Get the singleton instance
|
|
|
|
|
*/
|
|
|
|
|
static getInstance(): EmbeddingManager {
|
|
|
|
|
if (!globalInstance) {
|
|
|
|
|
globalInstance = new EmbeddingManager()
|
|
|
|
|
}
|
|
|
|
|
return globalInstance
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Initialize the model (happens once)
|
|
|
|
|
*/
|
|
|
|
|
async init(): Promise<void> {
|
|
|
|
|
// In unit test mode, skip real model initialization
|
test(8.0): Tier-1 integration via deterministic embedder (suite runnable again)
The integration suite loaded the real WASM model for every test, so a full run
took ~hours and reliably aborted (vitest reporter timeout) — which is why it was
never gated and silently rotted. Brainy already separates the embedder from all
other logic, so running integration with a deterministic embedder keeps every
code path real (HNSW build+search, graph traversal, metadata filtering,
transactions, storage/sharding, VFS, aggregation, import) while making the suite
complete in ~2 min on ANY machine — no GPU, no 16 GB requirement.
- Add isDeterministicEmbedMode() — a single source of truth for the test-embedder
switch (BRAINY_DETERMINISTIC_EMBEDDINGS, plus the legacy BRAINY_UNIT_TEST alias),
used by EmbeddingManager (init/embed/embedBatch) and brainy's eager-init guard.
- setup-integration.ts opts into it: Tier 1 = functional end-to-end. Real-model
semantic quality + the real embedding pipeline move to a Tier-2 suite.
Full integration now runs 482 passing / 583 in ~2 min (was un-completable). The
remaining failures are pre-existing test rot, fixed next. Unit 1414 green.
2026-06-16 12:44:14 -07:00
|
|
|
const isTestMode = isDeterministicEmbedMode()
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-11 16:23:32 -07:00
|
|
|
if (isTestMode) {
|
2025-12-17 17:42:37 -08:00
|
|
|
// Production safeguard
|
2025-09-11 16:23:32 -07:00
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
|
|
|
throw new Error(
|
|
|
|
|
'CRITICAL: Mock embeddings detected in production environment! ' +
|
|
|
|
|
'BRAINY_UNIT_TEST or __BRAINY_UNIT_TEST__ is set while NODE_ENV=production. ' +
|
|
|
|
|
'This is a security risk. Remove test flags before deploying to production.'
|
|
|
|
|
)
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
if (!this.initialized) {
|
|
|
|
|
this.initialized = true
|
|
|
|
|
this.initTime = 1 // Mock init time
|
|
|
|
|
console.log('🧪 EmbeddingManager: Using mocked embeddings for unit tests')
|
|
|
|
|
}
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Already initialized
|
2025-12-17 17:42:37 -08:00
|
|
|
if (this.initialized && this.engine.isInitialized()) {
|
2025-09-02 10:00:52 -07:00
|
|
|
return
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Initialization in progress
|
|
|
|
|
if (globalInitPromise) {
|
|
|
|
|
await globalInitPromise
|
|
|
|
|
return
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Start initialization
|
|
|
|
|
globalInitPromise = this.performInit()
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
try {
|
|
|
|
|
await globalInitPromise
|
|
|
|
|
} finally {
|
|
|
|
|
globalInitPromise = null
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Perform actual initialization
|
|
|
|
|
*/
|
|
|
|
|
private async performInit(): Promise<void> {
|
|
|
|
|
const startTime = Date.now()
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
try {
|
2025-12-17 17:42:37 -08:00
|
|
|
await this.engine.initialize()
|
|
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Lock precision after successful initialization
|
|
|
|
|
this.locked = true
|
|
|
|
|
this.initialized = true
|
|
|
|
|
this.initTime = Date.now() - startTime
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Log success
|
|
|
|
|
const memoryMB = this.getMemoryUsage()
|
2025-09-11 16:23:32 -07:00
|
|
|
console.log(`📊 Precision: Q8 | Memory: ${memoryMB}MB`)
|
2025-12-17 17:42:37 -08:00
|
|
|
console.log('🔒 Configuration locked')
|
2025-09-02 10:00:52 -07:00
|
|
|
} catch (error) {
|
|
|
|
|
this.initialized = false
|
2025-12-17 17:42:37 -08:00
|
|
|
throw new Error(
|
|
|
|
|
`Failed to initialize embedding model: ${error instanceof Error ? error.message : String(error)}`
|
|
|
|
|
)
|
2025-09-02 10:00:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Generate embeddings
|
|
|
|
|
*/
|
2025-10-17 12:29:27 -07:00
|
|
|
async embed(text: string | string[] | Record<string, unknown>): Promise<Vector> {
|
2025-12-17 17:42:37 -08:00
|
|
|
// Check for unit test environment
|
test(8.0): Tier-1 integration via deterministic embedder (suite runnable again)
The integration suite loaded the real WASM model for every test, so a full run
took ~hours and reliably aborted (vitest reporter timeout) — which is why it was
never gated and silently rotted. Brainy already separates the embedder from all
other logic, so running integration with a deterministic embedder keeps every
code path real (HNSW build+search, graph traversal, metadata filtering,
transactions, storage/sharding, VFS, aggregation, import) while making the suite
complete in ~2 min on ANY machine — no GPU, no 16 GB requirement.
- Add isDeterministicEmbedMode() — a single source of truth for the test-embedder
switch (BRAINY_DETERMINISTIC_EMBEDDINGS, plus the legacy BRAINY_UNIT_TEST alias),
used by EmbeddingManager (init/embed/embedBatch) and brainy's eager-init guard.
- setup-integration.ts opts into it: Tier 1 = functional end-to-end. Real-model
semantic quality + the real embedding pipeline move to a Tier-2 suite.
Full integration now runs 482 passing / 583 in ~2 min (was un-completable). The
remaining failures are pre-existing test rot, fixed next. Unit 1414 green.
2026-06-16 12:44:14 -07:00
|
|
|
const isTestMode = isDeterministicEmbedMode()
|
2025-09-25 10:47:44 -07:00
|
|
|
|
2025-09-11 16:23:32 -07:00
|
|
|
if (isTestMode) {
|
|
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
|
|
|
throw new Error('CRITICAL: Mock embeddings in production!')
|
|
|
|
|
}
|
2025-09-02 10:00:52 -07:00
|
|
|
return this.getMockEmbedding(text)
|
|
|
|
|
}
|
2025-09-25 10:47:44 -07:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Ensure initialized
|
|
|
|
|
await this.init()
|
2025-09-25 10:47:44 -07:00
|
|
|
|
2025-12-17 17:42:37 -08:00
|
|
|
// Normalize input to string
|
2025-09-25 10:47:44 -07:00
|
|
|
let input: string
|
|
|
|
|
if (Array.isArray(text)) {
|
2025-12-17 17:42:37 -08:00
|
|
|
input = text.map((t) => (typeof t === 'string' ? t : String(t))).join(' ')
|
2025-09-25 10:47:44 -07:00
|
|
|
} else if (typeof text === 'string') {
|
|
|
|
|
input = text
|
2025-10-17 12:29:27 -07:00
|
|
|
} else if (typeof text === 'object') {
|
|
|
|
|
input = JSON.stringify(text)
|
2025-09-25 10:47:44 -07:00
|
|
|
} else {
|
2025-10-17 12:29:27 -07:00
|
|
|
console.warn('EmbeddingManager.embed received unexpected input type:', typeof text)
|
2025-09-25 10:47:44 -07:00
|
|
|
input = String(text)
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
|
|
|
|
// Generate embedding using WASM engine
|
|
|
|
|
const embedding = await this.engine.embed(input)
|
|
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Validate dimensions
|
|
|
|
|
if (embedding.length !== 384) {
|
|
|
|
|
console.warn(`Unexpected embedding dimension: ${embedding.length}`)
|
|
|
|
|
if (embedding.length < 384) {
|
|
|
|
|
return [...embedding, ...new Array(384 - embedding.length).fill(0)]
|
|
|
|
|
} else {
|
|
|
|
|
return embedding.slice(0, 384)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
this.embedCount++
|
|
|
|
|
return embedding
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Generate mock embeddings for unit tests
|
|
|
|
|
*/
|
2025-10-17 12:29:27 -07:00
|
|
|
private getMockEmbedding(text: string | string[] | Record<string, unknown>): Vector {
|
2025-09-02 10:00:52 -07:00
|
|
|
const input = Array.isArray(text) ? text.join(' ') : text
|
|
|
|
|
const str = typeof input === 'string' ? input : JSON.stringify(input)
|
|
|
|
|
const vector = new Array(384).fill(0)
|
2025-10-17 12:29:27 -07:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Create semi-realistic embeddings based on text content
|
|
|
|
|
for (let i = 0; i < Math.min(str.length, 384); i++) {
|
|
|
|
|
vector[i] = (str.charCodeAt(i % str.length) % 256) / 256
|
|
|
|
|
}
|
2025-10-17 12:29:27 -07:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
// Add position-based variation
|
|
|
|
|
for (let i = 0; i < 384; i++) {
|
|
|
|
|
vector[i] += Math.sin(i * 0.1 + str.length) * 0.1
|
|
|
|
|
}
|
2025-10-17 12:29:27 -07:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
this.embedCount++
|
|
|
|
|
return vector
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
/**
|
|
|
|
|
* Batch embed multiple texts using native WASM batch API
|
|
|
|
|
*
|
|
|
|
|
* Uses the engine's embedBatch() for a single WASM forward pass
|
|
|
|
|
* instead of N individual embed() calls.
|
|
|
|
|
*
|
2026-01-27 16:49:26 -08:00
|
|
|
* Large batches (>MICRO_BATCH_SIZE) are split into micro-batches
|
|
|
|
|
* with event loop yielding between each, preventing the synchronous
|
|
|
|
|
* WASM call from blocking the server for hundreds of milliseconds.
|
|
|
|
|
*
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
* @param texts Array of strings to embed
|
|
|
|
|
* @returns Array of embedding vectors (384 dimensions each)
|
|
|
|
|
*/
|
2026-01-27 18:26:37 -08:00
|
|
|
async embedBatch(texts: string[], options?: { signal?: AbortSignal }): Promise<number[][]> {
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
if (texts.length === 0) return []
|
|
|
|
|
|
test(8.0): Tier-1 integration via deterministic embedder (suite runnable again)
The integration suite loaded the real WASM model for every test, so a full run
took ~hours and reliably aborted (vitest reporter timeout) — which is why it was
never gated and silently rotted. Brainy already separates the embedder from all
other logic, so running integration with a deterministic embedder keeps every
code path real (HNSW build+search, graph traversal, metadata filtering,
transactions, storage/sharding, VFS, aggregation, import) while making the suite
complete in ~2 min on ANY machine — no GPU, no 16 GB requirement.
- Add isDeterministicEmbedMode() — a single source of truth for the test-embedder
switch (BRAINY_DETERMINISTIC_EMBEDDINGS, plus the legacy BRAINY_UNIT_TEST alias),
used by EmbeddingManager (init/embed/embedBatch) and brainy's eager-init guard.
- setup-integration.ts opts into it: Tier 1 = functional end-to-end. Real-model
semantic quality + the real embedding pipeline move to a Tier-2 suite.
Full integration now runs 482 passing / 583 in ~2 min (was un-completable). The
remaining failures are pre-existing test rot, fixed next. Unit 1414 green.
2026-06-16 12:44:14 -07:00
|
|
|
const isTestMode = isDeterministicEmbedMode()
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
|
|
|
|
|
if (isTestMode) {
|
|
|
|
|
if (process.env.NODE_ENV === 'production') {
|
|
|
|
|
throw new Error('CRITICAL: Mock embeddings in production!')
|
|
|
|
|
}
|
|
|
|
|
return texts.map(t => this.getMockEmbedding(t))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await this.init()
|
2026-01-27 16:49:26 -08:00
|
|
|
|
|
|
|
|
// Small batches: single WASM call (no overhead)
|
|
|
|
|
const MICRO_BATCH_SIZE = 20
|
|
|
|
|
if (texts.length <= MICRO_BATCH_SIZE) {
|
|
|
|
|
const results = await this.engine.embedBatch(texts)
|
|
|
|
|
this.embedCount += texts.length
|
|
|
|
|
return results
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Large batches: micro-batch with event loop yielding
|
|
|
|
|
// Each micro-batch of ~20 texts blocks ~10-30ms, then yields
|
|
|
|
|
// so other requests (HTTP, timers, I/O) can proceed
|
|
|
|
|
const allResults: number[][] = []
|
|
|
|
|
for (let i = 0; i < texts.length; i += MICRO_BATCH_SIZE) {
|
2026-01-27 18:26:37 -08:00
|
|
|
if (options?.signal?.aborted) {
|
|
|
|
|
return allResults
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-27 16:49:26 -08:00
|
|
|
const batch = texts.slice(i, i + MICRO_BATCH_SIZE)
|
|
|
|
|
const batchResults = await this.engine.embedBatch(batch)
|
|
|
|
|
allResults.push(...batchResults)
|
|
|
|
|
this.embedCount += batch.length
|
|
|
|
|
|
|
|
|
|
// Yield to event loop between micro-batches
|
|
|
|
|
if (i + MICRO_BATCH_SIZE < texts.length) {
|
|
|
|
|
await new Promise<void>(resolve => setTimeout(resolve, 0))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return allResults
|
feat: add structured content extraction and batch embedding optimization to highlight()
Fix highlight() hanging on structured text input by addressing 3 root causes:
1. embedBatch() now uses native WASM batch API (single forward pass instead
of N individual embed() calls via Promise.all)
2. highlight() auto-detects content type (plain text, rich-text JSON, HTML,
Markdown) and extracts meaningful text segments. Supports TipTap, Slate.js,
Lexical, Draft.js, and Quill Delta formats. New contentType hint and
contentExtractor callback for custom parsers.
3. Semantic matching phase has 10s timeout - falls back to text-only matches
instead of hanging indefinitely.
Also fixes extractTextContent() array check: uses type-based detection
(typeof data[0] === 'number') instead of length-based (data.length > 10)
so arrays of objects are properly indexed for text search.
New types: ContentType, ContentCategory, ExtractedSegment
New fields: HighlightParams.contentType, HighlightParams.contentExtractor,
Highlight.contentCategory
2026-01-27 10:27:22 -08:00
|
|
|
}
|
|
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Get embedding function for compatibility
|
|
|
|
|
*/
|
|
|
|
|
getEmbeddingFunction(): EmbeddingFunction {
|
2025-10-17 12:29:27 -07:00
|
|
|
return async (data: string | string[] | Record<string, unknown>): Promise<Vector> => {
|
2025-09-02 10:00:52 -07:00
|
|
|
return await this.embed(data)
|
|
|
|
|
}
|
|
|
|
|
}
|
2025-09-17 14:53:54 -07:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Get memory usage in MB
|
|
|
|
|
*/
|
|
|
|
|
private getMemoryUsage(): number | null {
|
|
|
|
|
if (typeof process !== 'undefined' && process.memoryUsage) {
|
|
|
|
|
const usage = process.memoryUsage()
|
|
|
|
|
return Math.round(usage.heapUsed / 1024 / 1024)
|
|
|
|
|
}
|
|
|
|
|
return null
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Get current statistics
|
|
|
|
|
*/
|
|
|
|
|
getStats(): EmbeddingStats {
|
2025-12-17 17:42:37 -08:00
|
|
|
const engineStats = this.engine.getStats()
|
2025-09-02 10:00:52 -07:00
|
|
|
return {
|
|
|
|
|
initialized: this.initialized,
|
|
|
|
|
precision: this.precision,
|
|
|
|
|
modelName: this.modelName,
|
2025-12-17 17:42:37 -08:00
|
|
|
embedCount: this.embedCount + engineStats.embedCount,
|
2025-09-02 10:00:52 -07:00
|
|
|
initTime: this.initTime,
|
2025-12-17 17:42:37 -08:00
|
|
|
memoryMB: this.getMemoryUsage(),
|
2025-09-02 10:00:52 -07:00
|
|
|
}
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Check if initialized
|
|
|
|
|
*/
|
|
|
|
|
isInitialized(): boolean {
|
|
|
|
|
return this.initialized
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Get current precision
|
|
|
|
|
*/
|
|
|
|
|
getPrecision(): ModelPrecision {
|
|
|
|
|
return this.precision
|
|
|
|
|
}
|
2025-12-17 17:42:37 -08:00
|
|
|
|
2025-09-02 10:00:52 -07:00
|
|
|
/**
|
|
|
|
|
* Validate precision matches expected
|
|
|
|
|
*/
|
|
|
|
|
validatePrecision(expected: ModelPrecision): void {
|
|
|
|
|
if (this.locked && expected !== this.precision) {
|
|
|
|
|
throw new Error(
|
|
|
|
|
`Precision mismatch! System using ${this.precision.toUpperCase()} ` +
|
|
|
|
|
`but ${expected.toUpperCase()} was requested. Cannot mix precisions.`
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Export singleton instance and convenience functions
|
|
|
|
|
export const embeddingManager = EmbeddingManager.getInstance()
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Direct embed function
|
|
|
|
|
*/
|
2025-12-17 17:42:37 -08:00
|
|
|
export async function embed(
|
|
|
|
|
text: string | string[] | Record<string, unknown>
|
|
|
|
|
): Promise<Vector> {
|
2025-09-02 10:00:52 -07:00
|
|
|
return await embeddingManager.embed(text)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get embedding function for compatibility
|
|
|
|
|
*/
|
|
|
|
|
export function getEmbeddingFunction(): EmbeddingFunction {
|
|
|
|
|
return embeddingManager.getEmbeddingFunction()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Get statistics
|
|
|
|
|
*/
|
|
|
|
|
export function getEmbeddingStats(): EmbeddingStats {
|
|
|
|
|
return embeddingManager.getStats()
|
2025-12-17 17:42:37 -08:00
|
|
|
}
|