DocumentationReference

HazelJS ML Package

npm downloads

@hazeljs/​ml provides machine learning model management for HazelJS with a model registry, decorator-based training/prediction APIs, batteries-included classical algorithms, batch inference, metrics tracking, feature store, experiment tracking, and drift detection.

Quick Reference

  • Purpose: @hazeljs/​ml provides ML model lifecycle management: registration, training, prediction, batch inference, metrics, feature store, experiment tracking, and drift detection — plus pure-TypeScript classifiers, recommenders, forecasting, and clustering.
  • When to use: Use @hazeljs/​ml for managing ML models (training, serving, versioning). Use @hazeljs/​ai for LLM integration (OpenAI, Anthropic). Use @hazeljs/​data for ETL / quality before ML (prepareTrainingData).
  • Key concepts: Built-in models, model registry, @Train / @Predict, batch inference, metrics, feature store, experiments, drift monitoring.
  • Dependencies: @hazeljs/​core. Optional peer: @hazeljs/​data.
  • Common patterns: Register built-in or custom model → train → predict → evaluate → monitor drift.
  • Common mistakes: Not versioning models; expecting TensorFlow.js to be bundled (bring your own); using PipelineService as a full ETL replacement (it is preprocess-only — use @hazeljs/​data for source→sink).

Purpose

Building ML-powered applications requires model registration, training pipelines, inference services, and evaluation metrics. The @hazeljs/​ml package simplifies this by providing:

  • Built-in classical ML – TF-IDF, Naive Bayes, Logistic Regression, Isolation Forest, Cosine k-NN, Item-Item CF, Jaro-Winkler entity resolution, Holt-Winters, k-means, CART decision trees (pure TypeScript, zero ML deps)
  • Built-in @Model wrappers – Ready for MLModule.forRoot({ models: [...] })
  • Model Registry – Register/discover models by name@version; optional JSON artifact persistence
  • Decorator-Based API@Model, @Train, @Predict, @Experiment
  • Feature Store – Online/offline storage, point-in-time retrieval; materialize() can stream from a @hazeljs/​data DataSource
  • Experiment Tracking – MLflow-style runs; auto-log when @Experiment + @Train are combined
  • Drift Detection – PSI, KS, JSD, Chi-square, Wasserstein, concept-shift helper; MonitorService with real prediction windows
  • Training PipelinePipelineService for preprocess-only steps before train (not a replacement for @hazeljs/​data ETL)
  • Inference – PredictorService + BatchService (ordered results)
  • Metrics – accuracy/P/R/F1, confusion matrix, MAE/MSE/RMSE/R², ROC-AUC, Brier, nDCG, MAP
  • Framework-Agnostic – Bring your own TensorFlow.js / ONNX / Transformers.js class; the package does not bundle those runtimes

Architecture

The package uses a registry-based architecture with decorator-driven model registration:

graph TD
  A["MLModule.forRoot()<br/>(Built-in + Custom Models)"] --> B["MLModelBootstrap<br/>(Discovers @Train, @Predict)"]
  B --> C["ModelRegistry<br/>(Name/Version Lookup)"]
  
  D["@Model Decorator<br/>(Metadata)"] --> E["@Train / @Predict / @Experiment"]
  E --> B
  
  C --> F["TrainerService<br/>(Training)"]
  C --> G["PredictorService<br/>(Inference)"]
  C --> H["BatchService<br/>(Batch Predictions)"]
  C --> I["MetricsService<br/>(Evaluation)"]
  C --> FS["FeatureStore / Drift / Monitor"]
  
  G --> J["Single / Batch Prediction"]
  F --> K["Preprocess + Train"]
  
  style A fill:#3b82f6,stroke:#60a5fa,stroke-width:2px,color:#fff
  style B fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff
  style C fill:#10b981,stroke:#34d399,stroke-width:2px,color:#fff
  style D fill:#8b5cf6,stroke:#a78bfa,stroke-width:2px,color:#fff

Key Components

  1. MLModule – Registers ModelRegistry, TrainerService, PipelineService, PredictorService, BatchService, MetricsService, FeatureStoreService, ExperimentService, DriftService, MonitorService
  2. ModelRegistry – Name/version lookup; optional configurePersistence(dir) + saveArtifact / loadArtifact
  3. TrainerService – Invokes @Train; runs named PipelineService when @Train({ pipeline }) is set; auto-logs @Experiment runs
  4. PredictorService / BatchService – Single and ordered batch inference
  5. PipelineService – Preprocess-only training chains (use @hazeljs/​data for production ETL)
  6. MetricsService – Classification, regression, ranking, and calibration metrics
  7. Decorators@Model, @Train, @Predict, @Experiment

ML Decorators

Three decorators define an ML model and how it is trained and used. The registry and services discover them via reflection—no manual wiring.

@Model (class)

Attaches registry metadata so the model can be registered and looked up by name and version.

PropertyTypeRequiredDescription
namestringYesUnique model id (e.g. sentiment-classifier)
versionstringYesSemver (e.g. 1.0.0)
frameworkstringYestensorflow | onnx | custom
descriptionstringNoHuman-readable description
tagsstring[]NoTags for filtering (default: [])

Use one @Model per class and add @Injectable() so the app can construct the model.

@Train (method)

Marks the single method that trains the model. TrainerService.train(modelName, data) invokes it.

OptionTypeDefaultDescription
pipelinestringdefaultName of a registered PipelineService pipeline to run before training
batchSizenumber32Hint for batching (optional)
epochsnumber10Hint for epochs (optional)

Exactly one @Train() method per model; it receives training data and can return TrainingResult (e.g. accuracy, loss).

@Predict (method)

Marks the single method that runs inference. PredictorService.predict(modelName, input) invokes it.

OptionTypeDefaultDescription
batchbooleanfalseHint that the method supports batch input
endpointstring/​predictHint for route naming

Exactly one @Predict() method per model; it receives one input and returns a prediction object (e.g. { sentiment, confidence }).

Rules

  • One model class = one @Model, one @Train method, one @Predict method.
  • Order: Apply @Model on the class, then @Train and @Predict on the methods. Use @Injectable() from @hazeljs/​core.
  • Discovery: When you pass model classes to MLModule.forRoot({ models: [...] }), the bootstrap finds the decorated methods and registers the model.

Advantages

1. Declarative ML

Define models with decorators—training and prediction methods are discovered automatically.

2. Model Versioning

Register multiple versions of a model; the registry supports lookup by name and version.

3. Framework Flexibility

Use TensorFlow.js, ONNX, Transformers.js, or custom implementations—the package is backend-agnostic.

4. Batch Inference

BatchService for efficient batch predictions with configurable batch size. Results preserve input order.

5. Evaluation Built-In

MetricsService with evaluate() for accuracy/F1/precision/recall, plus regression (MAE/MSE/RMSE/R²), confusion matrix, ROC-AUC, Brier, nDCG, and MAP helpers.

Installation

npm install @hazeljs/​ml @hazeljs/​core
# optional: validate/​profile training data
npm install @hazeljs/​data

Optional Peer Dependencies

# Only if you bring your own neural backends (not required for built-in models)
npm install @tensorflow/​tfjs-node
npm install onnxruntime-node
npm install @huggingface/​transformers

Quick Start

1. Import MLModule with a built-in model

import { HazelApp } from '@hazeljs/​core';
import { MLModule, TextNaiveBayesModel } from '@hazeljs/​ml';

const app = new HazelApp({
  imports: [
    MLModule.forRoot({
      models: [TextNaiveBayesModel],
      artifactDir: '.​/models',
      experiments: { storage: 'memory' },
    }),
  ],
});

app.listen(3000);

2. Train and predict

import { TrainerService, PredictorService } from '@hazeljs/​ml';

await trainer.train('text-naive-bayes', {
  samples: [
    { text: 'great product', label: 'positive' },
    { text: 'terrible quality', label: 'negative' },
  ],
});

const result = await predictor.predict('text-naive-bayes', { text: 'I love this' });
/​/​ { label, confidence, scores }

Built-in Models

Model nameClassUse case
text-naive-bayesTextNaiveBayesModelTicket/chat routing, spam, intent
text-logistic-regressionTextLogisticRegressionModelBinary/multi-class text
isolation-forestIsolationForestModelFraud / outlier detection
cosine-knnCosineKnnModelSimilar tickets / k-NN
item-item-cfItemItemCFModelRecommendations
entity-resolverEntityResolverModelDuplicate customers / fuzzy match
holt-wintersHoltWintersModelDemand / wait-time forecast
kmeansKMeansModelSegmentation / clustering
decision-treeDecisionTreeModelInterpretable tabular classify

Algorithms are also exported directly (TfidfVectorizer, NaiveBayesClassifier, jaroWinkler, HoltWinters, KMeans, DecisionTreeClassifier, …) for use without decorators.

Custom Models (decorators)

You can still define your own model class:

import { Service } from '@hazeljs/​core';
import { Model, Train, Predict, ModelRegistry } from '@hazeljs/​ml';

@Model({ name: 'sentiment-classifier', version: '1.0.0', framework: 'custom' })
@Service()
export class SentimentClassifier {
  private labels = ['positive', 'negative', 'neutral'];
  private weights: Record<string, number[]> = {};

  constructor(private registry: ModelRegistry) {}

  @Train()
  async train(data: { text: string; label: string }[]): Promise<void> {
    /​/​ Your training logic – e.g. bag-of-words, embeddings
    const vocab = this.buildVocabulary(data);
    this.weights = this.computeWeights(data, vocab);
  }

  @Predict()
  async predict(input: { text: string }): Promise<{ sentiment: string; confidence: number }> {
    const scores = this.score(input.text);
    const idx = scores.indexOf(Math.max(...scores));
    return {
      sentiment: this.labels[idx],
      confidence: scores[idx],
    };
  }
}

Predict from a Controller

import { Controller, Post, Body } from '@hazeljs/​core';
import { PredictorService } from '@hazeljs/​ml';

@Controller('ml')
export class MLController {
  constructor(private predictor: PredictorService) {}

  @Post('predict')
  async predict(@Body() body: { text: string; model?: string }) {
    const result = await this.predictor.predict(
      body.model ?? 'text-naive-bayes',
      body
    );
    return { result };
  }
}

Training Pipeline

PipelineService is preprocess-only (normalize/filter samples before TrainerService.train). For production ETL (connectors, quality, sinks), use @hazeljs/​data PipelineRunner / PipelineBuilder, then pass cleaned samples via prepareTrainingData().

When @Train({ pipeline: 'name' }) is set and that pipeline is registered, TrainerService runs it automatically before calling your train method.

import { PipelineService } from '@hazeljs/​ml';

const pipeline = new PipelineService();

/​/​ Inline steps (no registration required)
const steps = [
  { name: 'normalize', transform: (d: unknown) => ({ ...(d as object), text: (d as { text: string }).text?.toLowerCase() }) },
  { name: 'filter', transform: (d: unknown) => (d as { text: string }).text?.length ? d : null },
];
const processed = await pipeline.run(data, steps);

/​/​ Or register a named pipeline for @Train({ pipeline: 'default' })
pipeline.registerPipeline('default', steps);

Preparing training data with @hazeljs/​data

import { Schema, QualityService } from '@hazeljs/​data';
import { prepareTrainingData, TrainerService } from '@hazeljs/​ml';

const SampleSchema = Schema.object({
  text: Schema.string().min(1),
  label: Schema.string().oneOf(['positive', 'negative']),
});

const prepared = await prepareTrainingData(
  { samples },
  { schema: SampleSchema, qualityService: new QualityService(), failOnQuality: true }
);
await trainer.train('text-naive-bayes', prepared.data);

Batch Predictions

BatchService processes inputs in batches with configurable concurrency. Results are returned in the same order as inputs.

import { BatchService } from '@hazeljs/​ml';

const batchService = new BatchService(predictorService);
const results = await batchService.predictBatch('sentiment-classifier', items, {
  batchSize: 32,
  concurrency: 4,
});
/​/​ results[i] corresponds to items[i]

Metrics and Evaluation

Inject MetricsService via MLModule. Use evaluate() for classification metrics, or helpers for regression/ranking:

import { Injectable } from '@hazeljs/​core';
import { MetricsService } from '@hazeljs/​ml';

@Injectable()
class EvaluationService {
  constructor(private metricsService: MetricsService) {}

  async runEvaluation() {
    const testData = [
      { text: 'great product', label: 'positive' },
      { text: 'terrible', label: 'negative' },
    ];
    const evaluation = await this.metricsService.evaluate('text-naive-bayes', testData, {
      metrics: ['accuracy', 'f1', 'precision', 'recall', 'confusion'],
      labelKey: 'label',
      predictionKey: 'label',
    });
    /​/​ evaluation.metrics + evaluation.confusionMatrix

    /​/​ Regression helpers
    const { mae, mse, rmse, r2 } = this.metricsService.computeRegressionMetrics(actual, predicted);

    /​/​ Ranking helpers
    const ndcg = this.metricsService.computeNDCG([3, 2, 1]);
    const map = this.metricsService.computeMAP([[1, 1, 0], [0, 1, 1]]);
  }
}

Manual Model Registration

When not using MLModule.forRoot({ models: [...] }):

import { registerMLModel, ModelRegistry, TrainerService, PredictorService } from '@hazeljs/​ml';

registerMLModel(
  sentimentInstance,
  modelRegistry,
  trainerService,
  predictorService
);

Feature Store

TypeScript-native feature store for managing ML features with online and offline storage:

import {
  FeatureStoreService,
  Feature,
  FeatureView,
  MemoryOnlineStore,
  RedisOnlineStore,
  FileOfflineStore,
  PostgresOfflineStore,
} from '@hazeljs/​ml';

/​/​ Define features with decorators
@FeatureView({
  name: 'user-behavior',
  entities: ['user'],
  description: 'Features derived from user behavior',
  online: true,
  offline: true,
})
class UserBehaviorFeatures {
  @Feature({ valueType: 'number', description: 'Total login count' })
  loginCount: number;

  @Feature({ valueType: 'number', description: 'Average session duration in seconds' })
  avgSessionDuration: number;

  @Feature({ valueType: 'string', tags: ['demographic'] })
  userSegment: string;
}

/​/​ Configure feature store
const featureStore = new FeatureStoreService();
featureStore.configure({
  online: {
    type: 'redis',
    redis: { host: 'localhost', port: 6379 },
  },
  offline: {
    type: 'postgres',
    postgres: {
      host: 'localhost',
      port: 5432,
      database: 'features',
      user: 'user',
      password: 'pass',
    },
  },
  enablePointInTime: true, /​/​ Prevents data leakage in training
});

/​/​ Get features for online inference (low-latency)
const onlineFeatures = await featureStore.getOnlineFeatures(
  ['user123', 'user456'],
  ['loginCount', 'avgSessionDuration']
);

/​/​ Get historical features for training (point-in-time correct)
const trainingFeatures = await featureStore.getOfflineFeatures(
  ['user123'],
  ['loginCount', 'avgSessionDuration'],
  new Date('2024-01-01') /​/​ Features as they were on this date
);

/​/​ Push features to online store
await featureStore.pushOnlineFeatures('user123', {
  loginCount: 42,
  avgSessionDuration: 320,
});

/​/​ Materialize from a @hazeljs/​data DataSource (optional)
featureStore.registerView('user-behavior', {
  name: 'user-behavior',
  entities: ['user'],
  features: [
    { name: 'loginCount', valueType: 'number' },
    { name: 'avgSessionDuration', valueType: 'number' },
  ],
  source: {
    type: 'batch',
    config: {
      dataSource: csvOrMemorySource, /​/​ any { read(): AsyncGenerator }
      entityIdField: 'userId',
    },
  },
});
await featureStore.materialize('user-behavior', [], { toOnline: true, toOffline: true });

Feature Store Benefits

  • Point-in-Time Correctness – Prevents data leakage by retrieving features as they existed at training time
  • Dual Storage – Online store (Redis/Memory) for low-latency inference, offline store (Postgres/File) for training
  • Type-Safe – Decorator-driven feature definitions with TypeScript types
  • Zero Python Dependencies – Pure TypeScript implementation, no Feast or Python required

Experiment Tracking

MLflow-style experiment tracking with runs, metrics, parameters, and artifacts:

import { ExperimentService, Experiment } from '@hazeljs/​ml';

/​/​ Configure experiment service
const experimentService = new ExperimentService();
experimentService.configure({
  storage: 'file',
  file: { directory: '.​/experiments' },
});

/​/​ Create an experiment
const experiment = experimentService.createExperiment('sentiment-classifier', {
  description: 'Training sentiment classification models',
  tags: ['nlp', 'classification'],
});

/​/​ Start a training run
const run = experimentService.startRun(experiment.id, {
  name: 'run-v1',
  params: { learningRate: 0.01, epochs: 10, batchSize: 32 },
  tags: ['baseline'],
});

/​/​ Log metrics during training
experimentService.logMetric(run.id, 'accuracy', 0.95);
experimentService.logMetric(run.id, 'loss', 0.05);
experimentService.logMetrics(run.id, {
  precision: 0.94,
  recall: 0.96,
  f1Score: 0.95,
});

/​/​ Log artifacts (models, plots, logs)
experimentService.logArtifact(
  run.id,
  'model',
  'model',
  modelBuffer,
  { framework: 'tensorflow', size: modelBuffer.length }
);

/​/​ End the run
experimentService.endRun(run.id, 'completed');

/​/​ Find best run by metric
const bestRun = experimentService.getBestRun(experiment.id, 'accuracy', 'max');
console.log('Best accuracy:', bestRun.metrics.accuracy);

/​/​ Compare runs
const comparison = experimentService.compareRuns([run1.id, run2.id, run3.id]);
/​/​ [{ runId, params, metrics, durationMs }, ...]

Experiment Tracking with @Experiment Decorator

@Experiment({
  name: 'sentiment-classifier',
  description: 'Training sentiment classification models',
  tags: ['nlp'],
  autoLogParams: true,
  autoLogMetrics: true,
})
@Model({ name: 'sentiment', version: '1.0.0', framework: 'custom' })
@Injectable()
class SentimentClassifier {
  @Train()
  async train(data: TrainingData) {
    /​/​ Training runs are automatically tracked
  }
}

Drift Detection & Monitoring

Production ML monitoring with statistical drift detection:

import { DriftService, MonitorService } from '@hazeljs/​ml';

/​/​ Initialize drift service
const driftService = new DriftService();

/​/​ Set reference distribution from training data
driftService.setReferenceDistribution('age', trainingAges);
driftService.setReferenceDistribution('income', trainingIncomes);

/​/​ Detect drift in production data
const ageResult = driftService.detectDrift('age', productionAges, {
  method: 'ks', /​/​ Kolmogorov-Smirnov test
  threshold: 0.1,
});

if (ageResult.driftDetected) {
  console.warn(`Drift detected: ${ageResult.message}`);
  console.log(`KS statistic: ${ageResult.score}, p-value: ${ageResult.pValue}`);
}

/​/​ Run full drift report on multiple features
const report = driftService.detectDriftReport(
  {
    age: productionAges,
    income: productionIncomes,
    creditScore: productionCreditScores,
  },
  {
    method: 'psi', /​/​ Population Stability Index
    threshold: 0.25,
  }
);

console.log(`Drift detected in ${report.driftedFeatures}/​${report.totalFeatures} features`);
console.log(`Overall drift: ${report.overallDrift}`);

/​/​ Detect prediction drift
const predDrift = driftService.detectPredictionDrift(
  trainingPredictions,
  productionPredictions
);

/​/​ Set up continuous monitoring
const monitorService = new MonitorService(driftService);

monitorService.registerModel({
  modelName: 'credit-risk-model',
  modelVersion: '1.0.0',
  featureDrift: {
    method: 'ks',
    threshold: 0.1,
  },
  accuracyMonitor: {
    threshold: 0.85,
    windowSize: 100,
  },
  checkIntervalMinutes: 60,
});

/​/​ Set up alert handler
monitorService.onAlert(async (alert) => {
  console.error(`[${alert.severity}] ${alert.alertType}: ${alert.message}`);
  /​/​ Send to Slack, PagerDuty, etc.
});

/​/​ Record accuracy for monitoring
monitorService.recordAccuracy('credit-risk-model', 0.92);

Drift Detection Methods

MethodUse CaseRange
PSI (Population Stability Index)Overall distribution shift0–∞ (>0.25 = significant)
KS (Kolmogorov-Smirnov)Continuous features0–1 (D statistic + p-value)
JSD (Jensen-Shannon Divergence)Symmetric distribution comparison0–0.693
Chi-squareCategorical features / binned numeric (method: 'chi2')Chi² statistic + p-value
WassersteinEarth Mover's Distance0–∞ (normalized by std)
ConceptJoint (prediction, label) shift via detectConceptDriftChi² on pair counts

All statistical tests are implemented in pure TypeScript with no Python dependencies. MonitorService.recordPrediction() stores a real window used by checkModel() (not dummy features).

Service Summary

ServicePurpose
ModelRegistryRegister/lookup models; optional artifact persistence
TrainerServiceInvoke @Train (+ pipeline + experiment auto-log)
PredictorServiceInvoke @Predict
PipelineServicePreprocess-only training pipelines
BatchServiceBatch prediction (results in input order)
MetricsServiceClassification / regression / ranking evaluation
FeatureStoreServiceOnline/offline features; materialize from DataSource
ExperimentServiceExperiments, runs, metrics, artifacts
DriftServicePSI, KS, JSD, Chi², Wasserstein, concept drift
MonitorServiceContinuous monitoring with windows + webhooks

Recipes

Recipe: Built-in Text Classifier

import { MLModule, TextNaiveBayesModel, TrainerService, PredictorService } from '@hazeljs/​ml';

MLModule.forRoot({ models: [TextNaiveBayesModel] });

await trainer.train('text-naive-bayes', {
  samples: [
    { text: 'refund please', label: 'billing' },
    { text: 'app crashed', label: 'bug' },
  ],
});
const out = await predictor.predict('text-naive-bayes', { text: 'charge me twice' });

Recipe: Serve ML Predictions via REST

import { Controller, Post, Body } from '@hazeljs/​core';
import { PredictorService } from '@hazeljs/​ml';

@Controller('ml')
export class MLController {
  constructor(private readonly predictor: PredictorService) {}

  @Post('predict')
  async predict(@Body() body: { model?: string; text: string }) {
    const result = await this.predictor.predict(body.model ?? 'text-naive-bayes', {
      text: body.text,
    });
    return { result };
  }
}

Recipe: Feature Store with Online/Offline Access

import { Service } from '@hazeljs/​core';
import { FeatureStoreService } from '@hazeljs/​ml';

@Service()
export class FeatureService {
  constructor(private readonly features: FeatureStoreService) {}

  async storeUserFeatures(userId: string, feats: Record<string, number>) {
    await this.features.pushOnlineFeatures(userId, feats);
    await this.features.writeOfflineFeatures(userId, feats, new Date());
  }

  async getOnline(userId: string) {
    return this.features.getOnlineFeatures([userId], ['loginCount', 'avgSessionDuration']);
  }
}