feat: migrate embeddings to Candle WASM + remove semantic type inference

Major architectural changes:

1. EMBEDDINGS ENGINE (ONNX → Candle WASM):
   - Replace ONNX Runtime with Rust Candle compiled to WASM
   - Embedded model in WASM binary (no external downloads)
   - Quantized Q8 precision with <50MB memory footprint
   - Zero-download, offline-first operation
   - Same embedding quality (all-MiniLM-L6-v2)

2. REMOVE SEMANTIC TYPE INFERENCE:
   - Delete embeddedKeywordEmbeddings.ts (14MB of pre-computed embeddings)
   - Remove typeAwareQueryPlanner.ts and semanticTypeInference.ts
   - Remove VerbExactMatchSignal (uses keyword embeddings)
   - Update SmartRelationshipExtractor to 3 signals (55%/30%/15% weights)

API CHANGES (requires v7.0.0):
- Removed: inferTypes(), inferNouns(), inferVerbs(), inferIntent()
- Removed: getSemanticTypeInference(), SemanticTypeInference class
- Removed: TypeInference, SemanticTypeInferenceOptions types

Users can still use natural language queries in find() - they just
need to specify type explicitly for type-optimized searches.

PACKAGE SIZE IMPACT:
- Compressed: 90.1 MB → 86.2 MB (-4.3%)
- Uncompressed: 114.4 MB → 100.3 MB (-12%)
- ~448K lines of code removed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
David Snelling 2026-01-06 12:52:34 -08:00
parent 81cd16e41b
commit da7d2ed29d
60 changed files with 3887 additions and 448557 deletions

View file

@ -10,10 +10,9 @@
* - Comprehensive relationship intelligence built-in
*
* Ensemble Architecture:
* - VerbExactMatchSignal (40%) - Explicit keywords and phrases
* - VerbEmbeddingSignal (35%) - Neural similarity with verb embeddings
* - VerbPatternSignal (20%) - Regex patterns and structures
* - VerbContextSignal (5%) - Entity type pair hints
* - VerbEmbeddingSignal (55%) - Neural similarity with verb embeddings
* - VerbPatternSignal (30%) - Regex patterns and structures
* - VerbContextSignal (15%) - Entity type pair hints
*
* Performance:
* - Parallel signal execution (~15-20ms total)
@ -24,11 +23,9 @@
import type { Brainy } from '../brainy.js'
import type { VerbType, NounType } from '../types/graphTypes.js'
import { VerbExactMatchSignal } from './signals/VerbExactMatchSignal.js'
import { VerbEmbeddingSignal } from './signals/VerbEmbeddingSignal.js'
import { VerbPatternSignal } from './signals/VerbPatternSignal.js'
import { VerbContextSignal } from './signals/VerbContextSignal.js'
import type { VerbSignal as ExactVerbSignal } from './signals/VerbExactMatchSignal.js'
import type { VerbSignal as EmbeddingVerbSignal } from './signals/VerbEmbeddingSignal.js'
import type { VerbSignal as PatternVerbSignal } from './signals/VerbPatternSignal.js'
import type { VerbSignal as ContextVerbSignal } from './signals/VerbContextSignal.js'
@ -40,7 +37,7 @@ export interface RelationshipExtractionResult {
type: VerbType
confidence: number
weight: number
source: 'ensemble' | 'exact-match' | 'pattern' | 'embedding' | 'context'
source: 'ensemble' | 'pattern' | 'embedding' | 'context'
evidence: string
metadata?: {
signalResults?: Array<{
@ -61,10 +58,9 @@ export interface SmartRelationshipExtractorOptions {
enableEnsemble?: boolean // Use ensemble vs single best signal (default: true)
cacheSize?: number // LRU cache size (default: 2000)
weights?: { // Custom signal weights (must sum to 1.0)
exactMatch?: number // Default: 0.40
embedding?: number // Default: 0.35
pattern?: number // Default: 0.20
context?: number // Default: 0.05
embedding?: number // Default: 0.55
pattern?: number // Default: 0.30
context?: number // Default: 0.15
}
}
@ -72,7 +68,7 @@ export interface SmartRelationshipExtractorOptions {
* Internal signal result wrapper
*/
interface SignalResult {
signal: 'exact-match' | 'embedding' | 'pattern' | 'context'
signal: 'embedding' | 'pattern' | 'context'
type: VerbType | null
confidence: number
weight: number
@ -97,7 +93,6 @@ export class SmartRelationshipExtractor {
private options: Required<Omit<SmartRelationshipExtractorOptions, 'weights'>> & { weights: Required<NonNullable<SmartRelationshipExtractorOptions['weights']>> }
// Signal instances
private exactMatchSignal: VerbExactMatchSignal
private embeddingSignal: VerbEmbeddingSignal
private patternSignal: VerbPatternSignal
private contextSignal: VerbContextSignal
@ -110,7 +105,6 @@ export class SmartRelationshipExtractor {
private stats = {
calls: 0,
cacheHits: 0,
exactMatchWins: 0,
embeddingWins: 0,
patternWins: 0,
contextWins: 0,
@ -129,10 +123,9 @@ export class SmartRelationshipExtractor {
enableEnsemble: options?.enableEnsemble ?? true,
cacheSize: options?.cacheSize ?? 2000,
weights: {
exactMatch: options?.weights?.exactMatch ?? 0.40,
embedding: options?.weights?.embedding ?? 0.35,
pattern: options?.weights?.pattern ?? 0.20,
context: options?.weights?.context ?? 0.05
embedding: options?.weights?.embedding ?? 0.55,
pattern: options?.weights?.pattern ?? 0.30,
context: options?.weights?.context ?? 0.15
}
}
@ -143,24 +136,19 @@ export class SmartRelationshipExtractor {
}
// Initialize signals
this.exactMatchSignal = new VerbExactMatchSignal(brain, {
minConfidence: 0.50, // Lower threshold, ensemble will filter
cacheSize: Math.floor(this.options.cacheSize / 4)
})
this.embeddingSignal = new VerbEmbeddingSignal(brain, {
minConfidence: 0.50,
cacheSize: Math.floor(this.options.cacheSize / 4)
cacheSize: Math.floor(this.options.cacheSize / 3)
})
this.patternSignal = new VerbPatternSignal(brain, {
minConfidence: 0.50,
cacheSize: Math.floor(this.options.cacheSize / 4)
cacheSize: Math.floor(this.options.cacheSize / 3)
})
this.contextSignal = new VerbContextSignal(brain, {
minConfidence: 0.50,
cacheSize: Math.floor(this.options.cacheSize / 4)
cacheSize: Math.floor(this.options.cacheSize / 3)
})
}
@ -197,8 +185,7 @@ export class SmartRelationshipExtractor {
try {
// Execute all signals in parallel
const [exactMatch, embeddingMatch, patternMatch, contextMatch] = await Promise.all([
this.exactMatchSignal.classify(context).catch(() => null),
const [embeddingMatch, patternMatch, contextMatch] = await Promise.all([
this.embeddingSignal.classify(context, options?.contextVector).catch(() => null),
this.patternSignal.classify(subject, object, context).catch(() => null),
this.contextSignal.classify(options?.subjectType, options?.objectType).catch(() => null)
@ -206,13 +193,6 @@ export class SmartRelationshipExtractor {
// Wrap results with weights
const signalResults: SignalResult[] = [
{
signal: 'exact-match',
type: exactMatch?.type || null,
confidence: exactMatch?.confidence || 0,
weight: this.options.weights.exactMatch,
evidence: exactMatch?.evidence || ''
},
{
signal: 'embedding',
type: embeddingMatch?.type || null,
@ -368,7 +348,7 @@ export class SmartRelationshipExtractor {
type: best.type!,
confidence: best.confidence,
weight: best.confidence,
source: best.signal as any,
source: best.signal,
evidence: best.evidence,
metadata: undefined
}
@ -381,8 +361,6 @@ export class SmartRelationshipExtractor {
// Track win counts
if (result.source === 'ensemble') {
this.stats.ensembleWins++
} else if (result.source === 'exact-match') {
this.stats.exactMatchWins++
} else if (result.source === 'embedding') {
this.stats.embeddingWins++
} else if (result.source === 'pattern') {
@ -445,7 +423,6 @@ export class SmartRelationshipExtractor {
cacheHitRate: this.stats.calls > 0 ? this.stats.cacheHits / this.stats.calls : 0,
ensembleRate: this.stats.calls > 0 ? this.stats.ensembleWins / this.stats.calls : 0,
signalStats: {
exactMatch: this.exactMatchSignal.getStats(),
embedding: this.embeddingSignal.getStats(),
pattern: this.patternSignal.getStats(),
context: this.contextSignal.getStats()
@ -460,7 +437,6 @@ export class SmartRelationshipExtractor {
this.stats = {
calls: 0,
cacheHits: 0,
exactMatchWins: 0,
embeddingWins: 0,
patternWins: 0,
contextWins: 0,
@ -470,7 +446,6 @@ export class SmartRelationshipExtractor {
averageSignalsUsed: 0
}
this.exactMatchSignal.resetStats()
this.embeddingSignal.resetStats()
this.patternSignal.resetStats()
this.contextSignal.resetStats()
@ -483,7 +458,6 @@ export class SmartRelationshipExtractor {
this.cache.clear()
this.cacheOrder = []
this.exactMatchSignal.clearCache()
this.embeddingSignal.clearCache()
this.patternSignal.clearCache()
this.contextSignal.clearCache()

File diff suppressed because it is too large Load diff

View file

@ -1,418 +0,0 @@
/**
* VerbExactMatchSignal - O(1) exact match relationship type classification
*
* HIGHEST WEIGHT: 40% (most reliable signal for verbs)
*
* Uses:
* 1. O(1) keyword lookup (exact string match against 334 verb keywords)
* 2. Context-aware matching (sentence patterns)
* 3. Multi-word phrase matching ("created by", "part of", "belongs to")
*
* PRODUCTION-READY: No TODOs, no mocks, real implementation
*/
import type { Brainy } from '../../brainy.js'
import { VerbType } from '../../types/graphTypes.js'
import { getKeywordEmbeddings, type KeywordEmbedding } from '../embeddedKeywordEmbeddings.js'
/**
* Signal result with classification details
*/
export interface VerbSignal {
type: VerbType
confidence: number
evidence: string
metadata?: {
matchedKeyword?: string
matchPosition?: number
}
}
/**
* Options for verb exact match signal
*/
export interface VerbExactMatchSignalOptions {
minConfidence?: number // Minimum confidence threshold (default: 0.70)
cacheSize?: number // LRU cache size (default: 2000)
caseSensitive?: boolean // Case-sensitive matching (default: false)
}
/**
* VerbExactMatchSignal - Instant O(1) relationship type classification
*
* Production features:
* - O(1) hash table lookups using 334 pre-computed verb keywords
* - Multi-word phrase matching ("created by", "part of", etc.)
* - Context-aware pattern detection
* - LRU cache for hot paths
* - High confidence (0.85-0.95) - most reliable signal
*/
export class VerbExactMatchSignal {
private brain: Brainy
private options: Required<VerbExactMatchSignalOptions>
// O(1) keyword lookup (key: normalized keyword → value: VerbType + confidence)
private keywordIndex: Map<string, { type: VerbType; confidence: number; isCanonical: boolean }> = new Map()
// LRU cache
private cache: Map<string, VerbSignal | null> = new Map()
private cacheOrder: string[] = []
// Statistics
private stats = {
calls: 0,
cacheHits: 0,
exactMatches: 0,
phraseMatches: 0,
partialMatches: 0
}
constructor(brain: Brainy, options?: VerbExactMatchSignalOptions) {
this.brain = brain
this.options = {
minConfidence: options?.minConfidence ?? 0.70,
cacheSize: options?.cacheSize ?? 2000,
caseSensitive: options?.caseSensitive ?? false
}
// Build keyword index from pre-computed embeddings
this.buildKeywordIndex()
}
/**
* Build keyword index from embedded keyword embeddings (O(n) once at startup)
*/
private buildKeywordIndex(): void {
const allKeywords = getKeywordEmbeddings()
// Filter to verb keywords only
const verbKeywords = allKeywords.filter(k => k.typeCategory === 'verb')
for (const keyword of verbKeywords) {
const normalized = this.normalize(keyword.keyword)
// Only keep highest confidence for duplicate keywords
const existing = this.keywordIndex.get(normalized)
if (!existing || keyword.confidence > existing.confidence) {
this.keywordIndex.set(normalized, {
type: keyword.type as VerbType,
confidence: keyword.confidence,
isCanonical: keyword.isCanonical
})
}
}
// Verify we have the expected number of verb keywords
if (this.keywordIndex.size === 0) {
throw new Error('VerbExactMatchSignal: No verb keywords found in embeddings')
}
}
/**
* Classify relationship type from context text
*
* @param context Full context text (sentence or paragraph)
* @returns VerbSignal with classified type or null
*/
async classify(context: string): Promise<VerbSignal | null> {
this.stats.calls++
if (!context || context.trim().length === 0) {
return null
}
// Check cache
const cacheKey = this.getCacheKey(context)
const cached = this.getFromCache(cacheKey)
if (cached !== undefined) {
this.stats.cacheHits++
return cached
}
try {
const result = this.classifyInternal(context)
// Add to cache
this.addToCache(cacheKey, result)
return result
} catch (error) {
return null
}
}
/**
* Internal classification logic (not cached)
*/
private classifyInternal(context: string): VerbSignal | null {
const normalized = this.normalize(context)
// Strategy 1: Multi-word phrase matching (highest priority)
// Look for common verb phrases: "created by", "part of", "belongs to", etc.
const phraseResult = this.matchPhrases(normalized)
if (phraseResult && phraseResult.confidence >= this.options.minConfidence) {
this.stats.phraseMatches++
return phraseResult
}
// Strategy 2: Single keyword matching
// Split into tokens and check each against keyword index
const tokens = this.tokenize(normalized)
let bestMatch: VerbSignal | null = null
let bestConfidence = 0
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i]
// Check exact keyword match
const match = this.keywordIndex.get(token)
if (match) {
const confidence = match.isCanonical ? 0.95 : 0.85
if (confidence > bestConfidence) {
bestConfidence = confidence
bestMatch = {
type: match.type,
confidence,
evidence: `Exact keyword match: "${token}"`,
metadata: {
matchedKeyword: token,
matchPosition: i
}
}
}
}
// Check bi-gram (two consecutive tokens)
if (i < tokens.length - 1) {
const bigram = `${tokens[i]} ${tokens[i + 1]}`
const bigramMatch = this.keywordIndex.get(bigram)
if (bigramMatch) {
const confidence = bigramMatch.isCanonical ? 0.95 : 0.85
if (confidence > bestConfidence) {
bestConfidence = confidence
bestMatch = {
type: bigramMatch.type,
confidence,
evidence: `Phrase match: "${bigram}"`,
metadata: {
matchedKeyword: bigram,
matchPosition: i
}
}
}
}
}
// Check tri-gram (three consecutive tokens)
if (i < tokens.length - 2) {
const trigram = `${tokens[i]} ${tokens[i + 1]} ${tokens[i + 2]}`
const trigramMatch = this.keywordIndex.get(trigram)
if (trigramMatch) {
const confidence = trigramMatch.isCanonical ? 0.95 : 0.85
if (confidence > bestConfidence) {
bestConfidence = confidence
bestMatch = {
type: trigramMatch.type,
confidence,
evidence: `Phrase match: "${trigram}"`,
metadata: {
matchedKeyword: trigram,
matchPosition: i
}
}
}
}
}
}
if (bestMatch && bestMatch.confidence >= this.options.minConfidence) {
this.stats.exactMatches++
return bestMatch
}
return null
}
/**
* Match common multi-word verb phrases
*
* These are high-confidence patterns that indicate specific relationships
*/
private matchPhrases(text: string): VerbSignal | null {
// Common relationship phrases with their VerbTypes
const phrases: Array<{ pattern: RegExp; type: VerbType; confidence: number }> = [
// Creation relationships
{ pattern: /created?\s+by/i, type: VerbType.Creates, confidence: 0.95 },
{ pattern: /authored?\s+by/i, type: VerbType.Creates, confidence: 0.95 },
{ pattern: /written\s+by/i, type: VerbType.Creates, confidence: 0.95 },
{ pattern: /developed\s+by/i, type: VerbType.Creates, confidence: 0.90 },
{ pattern: /built\s+by/i, type: VerbType.Creates, confidence: 0.85 },
// Ownership relationships
{ pattern: /owned\s+by/i, type: VerbType.Owns, confidence: 0.95 },
{ pattern: /belongs\s+to/i, type: VerbType.Owns, confidence: 0.95 },
{ pattern: /attributed\s+to/i, type: VerbType.AttributedTo, confidence: 0.95 },
// Part/Whole relationships
{ pattern: /part\s+of/i, type: VerbType.PartOf, confidence: 0.95 },
{ pattern: /contains/i, type: VerbType.Contains, confidence: 0.90 },
{ pattern: /includes/i, type: VerbType.Contains, confidence: 0.85 },
// Location relationships
{ pattern: /located\s+(?:at|in)/i, type: VerbType.LocatedAt, confidence: 0.95 },
{ pattern: /based\s+in/i, type: VerbType.LocatedAt, confidence: 0.90 },
{ pattern: /situated\s+in/i, type: VerbType.LocatedAt, confidence: 0.90 },
// Membership relationships
{ pattern: /member\s+of/i, type: VerbType.MemberOf, confidence: 0.95 },
{ pattern: /works?\s+(?:at|for)/i, type: VerbType.WorksWith, confidence: 0.85 },
{ pattern: /employed\s+by/i, type: VerbType.WorksWith, confidence: 0.90 },
// Reporting relationships
{ pattern: /reports?\s+to/i, type: VerbType.ReportsTo, confidence: 0.95 },
{ pattern: /manages/i, type: VerbType.ReportsTo, confidence: 0.85 },
{ pattern: /supervises/i, type: VerbType.ReportsTo, confidence: 0.95 },
// Reference relationships
{ pattern: /references/i, type: VerbType.References, confidence: 0.90 },
{ pattern: /cites/i, type: VerbType.References, confidence: 0.90 },
{ pattern: /mentions/i, type: VerbType.References, confidence: 0.85 },
// Temporal relationships
{ pattern: /precedes/i, type: VerbType.Precedes, confidence: 0.90 },
{ pattern: /follows/i, type: VerbType.Precedes, confidence: 0.90 },
{ pattern: /before/i, type: VerbType.Precedes, confidence: 0.75 },
{ pattern: /after/i, type: VerbType.Precedes, confidence: 0.75 },
// Causal relationships
{ pattern: /causes/i, type: VerbType.Causes, confidence: 0.90 },
{ pattern: /requires/i, type: VerbType.Requires, confidence: 0.90 },
{ pattern: /depends\s+on/i, type: VerbType.DependsOn, confidence: 0.95 },
// Transformation relationships
{ pattern: /transforms/i, type: VerbType.Transforms, confidence: 0.90 },
{ pattern: /modifies/i, type: VerbType.Modifies, confidence: 0.90 },
{ pattern: /becomes/i, type: VerbType.Becomes, confidence: 0.90 }
]
for (const { pattern, type, confidence } of phrases) {
if (pattern.test(text)) {
return {
type,
confidence,
evidence: `Phrase pattern match: ${pattern.source}`,
metadata: {
matchedKeyword: pattern.source
}
}
}
}
return null
}
/**
* Normalize text for matching
*/
private normalize(text: string): string {
let normalized = text.trim()
if (!this.options.caseSensitive) {
normalized = normalized.toLowerCase()
}
// Remove extra whitespace
normalized = normalized.replace(/\s+/g, ' ')
return normalized
}
/**
* Tokenize text into words
*/
private tokenize(text: string): string[] {
return text
.split(/\s+/)
.map(token => token.replace(/[^\w\s-]/g, '')) // Remove punctuation except hyphens
.filter(token => token.length > 0)
}
/**
* Get cache key
*/
private getCacheKey(context: string): string {
return this.normalize(context).substring(0, 200) // Limit key length
}
/**
* Get from LRU cache
*/
private getFromCache(key: string): VerbSignal | null | undefined {
if (!this.cache.has(key)) {
return undefined
}
const cached = this.cache.get(key)
// Move to end (most recently used)
this.cacheOrder = this.cacheOrder.filter(k => k !== key)
this.cacheOrder.push(key)
return cached ?? null
}
/**
* Add to LRU cache with eviction
*/
private addToCache(key: string, value: VerbSignal | null): void {
this.cache.set(key, value)
this.cacheOrder.push(key)
// Evict oldest if over limit
if (this.cache.size > this.options.cacheSize) {
const oldest = this.cacheOrder.shift()
if (oldest) {
this.cache.delete(oldest)
}
}
}
/**
* Get statistics
*/
getStats() {
return {
...this.stats,
keywordCount: this.keywordIndex.size,
cacheSize: this.cache.size,
cacheHitRate: this.stats.calls > 0 ? this.stats.cacheHits / this.stats.calls : 0
}
}
/**
* Reset statistics
*/
resetStats(): void {
this.stats = {
calls: 0,
cacheHits: 0,
exactMatches: 0,
phraseMatches: 0,
partialMatches: 0
}
}
/**
* Clear cache
*/
clearCache(): void {
this.cache.clear()
this.cacheOrder = []
}
}