npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

typensearch

v2.0.6

Published

Awesome OpenSearch model using TypeScript classes.

Readme

TypenSearch

TypenSearch is a simple and powerful Object Document Mapper (ODM) for OpenSearch, designed to help developers easily interact with OpenSearch indices using TypeScript. Inspired by Typegoose, it brings the power of TypeScript decorators to OpenSearch, making it more intuitive and type-safe.

Features

  • 🎯 Intuitive schema definition with TypeScript decorators
  • 🚀 Automatic index management and mapping
  • ⚡ Type-safe CRUD operations
  • 🛠 Custom field options support
  • 🔍 Powerful search capabilities

Installation

npm install --save typensearch

Quick Start

1. OpenSearch Connection Setup

import { initialize } from "typensearch";

await initialize(
  {
    node: "http://localhost:9200",
    // Additional OpenSearch client options
    auth: {
      username: "admin",
      password: "admin",
    },
    ssl: {
      rejectUnauthorized: false,
    },
  },
  {
    createIndexesIfNotExists: [User.prototype],
  }
);

2. Model Definition

import { OpenSearchIndex, Field, Model } from "typensearch";

@OpenSearchIndex({
  name: "users", // Index name (optional, defaults to lowercase class name)
  numberOfShards: 2,
  numberOfReplicas: 1,
  settings: {
    // Additional index settings
    "index.mapping.total_fields.limit": 2000,
  },
})
class User extends Model {
  @Field({
    type: "text",
    required: true,
    fields: {
      keyword: { type: "keyword" }, // Multi-fields configuration
    },
  })
  username: string;

  @Field({
    type: "keyword",
    required: true,
    validate: (value: string) => {
      return /^[^@]+@[^@]+\.[^@]+$/.test(value);
    },
  })
  email: string;

  @Field({
    type: "object",
    properties: {
      street: { type: "text" },
      city: { type: "keyword" },
      country: { type: "keyword" },
    },
  })
  address?: {
    street: string;
    city: string;
    country: string;
  };
}

3. CRUD Operations

// Create a document
const user = await User.index({
  username: "john_doe",
  email: "[email protected]",
  address: {
    street: "123 Main St",
    city: "New York",
    country: "USA",
  },
});

// Create/Update multiple documents using bulk operation
const bulkResponse = await User.bulkIndex(
  [
    {
      username: "john_doe",
      email: "[email protected]",
      address: {
        street: "123 Main St",
        city: "New York",
        country: "USA",
      },
    },
    {
      _id: "existing_user", // Update if ID exists
      username: "jane_doe",
      email: "[email protected]",
    },
  ],
  { refresh: true }
);

// Delete multiple documents using bulk operation
await User.bulkDelete(["user1", "user2", "user3"], { refresh: true });

// Get document by ID
const foundUser = await User.get("user_id");

// Update document
foundUser.username = "jane_doe";
await foundUser.save();

// Update multiple documents
await User.updateMany(
  { city: "New York" }, // search condition
  { country: "US" } // fields to update
);

// Search (using query builder)
const users = await User.query<User>()
  .match("username", "john", { operator: "AND" })
  .bool((q) =>
    q
      .must("address.city", "New York")
      .should("tags", ["developer", "typescript"])
  )
  .sort("username", "desc")
  .from(0)
  .size(10)
  .execute();

// Delete document
await foundUser.delete();

// Delete multiple documents
await User.deleteMany({
  "address.country": "USA",
});

// Count documents
const count = await User.count({
  query: {
    term: { "address.city": "New York" },
  },
});

4. Schema Migration

TypenSearch provides powerful schema migration capabilities to help you manage changes to your index mappings safely and efficiently.

// Basic Migration Example
@OpenSearchIndex({
  name: "users",
  settings: {
    "index.mapping.total_fields.limit": 2000,
  },
})
class User extends Model {
  @Field({ type: "keyword" })
  name: string;

  @Field({ type: "integer" })
  age: number;
}

// Adding new fields
@OpenSearchIndex({
  name: "users",
  settings: {
    "index.mapping.total_fields.limit": 2000,
  },
})
class UpdatedUser extends Model {
  @Field({ type: "keyword" })
  name: string;

  @Field({ type: "integer" })
  age: number;

  @Field({ type: "text" })
  description: string;
}

// Check migration plan
const plan = await UpdatedUser.planMigration();
console.log("Migration Plan:", {
  addedFields: plan.addedFields,
  modifiedFields: plan.modifiedFields,
  deletedFields: plan.deletedFields,
  requiresReindex: plan.requiresReindex,
  estimatedDuration: plan.estimatedDuration,
});

// Execute migration
const result = await UpdatedUser.migrate();

// Safe Migration with Backup and Rollback
const result = await UpdatedUser.migrate({
  backup: true,
  waitForCompletion: true,
});

if (!result.success) {
  const rollback = await UpdatedUser.rollback(result.migrationId);
}

// Large Dataset Migration
const result = await UpdatedUser.migrate({
  backup: true,
  waitForCompletion: false,
  timeout: "1h",
});

// Check Migration History
const history = await UpdatedUser.getMigrationHistory();

Migration Options

interface MigrationOptions {
  dryRun?: boolean; // Test migration without applying changes
  backup?: boolean; // Create backup before migration
  waitForCompletion?: boolean; // Wait for migration to complete
  timeout?: string; // Migration timeout
  batchSize?: number; // Number of documents to process in each batch
}

API Reference

Decorators

@OpenSearchIndex(options: IndexOptions)

Defines index settings.

interface IndexOptions {
  name?: string; // Index name
  numberOfShards?: number; // Number of shards
  numberOfReplicas?: number; // Number of replicas
  settings?: Record<string, unknown>; // Additional index settings
}

@Field(options: FieldOptions)

Defines field type and properties.

interface FieldOptions<T> {
  type: string;
  required?: boolean;
  default?: T;
  boost?: number;
  fields?: Record<string, unknown>;
  properties?: Record<string, FieldOptions<unknown>>;
  validate?: (value: T) => boolean;
}

Model Methods

All methods return Promises.

Static Methods

  • Model.index<T>(doc: Partial<T>, refresh?: boolean): Create a new document
  • Model.get<T>(id: string): Get document by ID
  • Model.updateMany<T>(query: any, updates: Partial<T>, options?: UpdateOptions): Update multiple documents
  • Model.deleteMany(query: any): Delete multiple documents
  • Model.search(body: any, size?: number): Search documents with raw query
  • Model.count(body: any): Count documents
  • Model.bulkIndex<T>(docs: Partial<T>[], options?: BulkOptions): Create or update multiple documents in one operation
  • Model.bulkDelete(ids: string[], options?: BulkOptions): Delete multiple documents by their IDs
  • Model.planMigration(): Generate schema change plan
  • Model.migrate(options?: MigrationOptions): Execute schema changes
  • Model.rollback(migrationId: string): Rollback a migration
  • Model.getMigrationHistory(): Get migration history
  • Model.getMapping(): Get current index mapping with all field options
  • Model.query<T>(): Get a new query builder instance

Instance Methods

  • save(refresh?: boolean): Save current document
  • delete(refresh?: boolean): Delete current document
  • validate(): Validate document against schema rules

Query Builder

Provides a type-safe query builder for writing OpenSearch queries.

Basic Queries

// Match query
const results = await User.query<User>()
  .match("username", "john", {
    operator: "AND",
    fuzziness: "AUTO",
  })
  .execute();

// Term query
const results = await User.query<User>()
  .term("age", 25, {
    boost: 2.0,
  })
  .execute();

// Range query
const results = await User.query<User>()
  .range("age", {
    gte: 20,
    lte: 30,
  })
  .execute();

Boolean Queries

const results = await User.query<User>()
  .bool((q) =>
    q
      .must("role", "admin")
      .mustNot("status", "inactive")
      .should("tags", ["developer", "typescript"])
      .filter("age", { gte: 20, lte: 30 })
  )
  .execute();

Search Options

const results = await User.query<User>()
  .match("username", "john")
  // Sorting
  .sort("createdAt", "desc")
  .sort("username", { order: "asc", missing: "_last" })
  // Pagination
  .from(0)
  .size(10)
  // Field filtering
  .source({
    includes: ["username", "email", "age"],
    excludes: ["password"],
  })
  // Additional options
  .timeout("5s")
  .trackTotalHits(true)
  .execute();

Query Options

MatchQueryOptions
{
  operator?: "OR" | "AND";
  minimum_should_match?: number | string;
  fuzziness?: number | "AUTO";
  prefix_length?: number;
  max_expansions?: number;
  fuzzy_transpositions?: boolean;
  lenient?: boolean;
  zero_terms_query?: "none" | "all";
  analyzer?: string;
}
TermQueryOptions
{
  boost?: number;
  case_insensitive?: boolean;
}
RangeQueryOptions
{
  gt?: number | string | Date;
  gte?: number | string | Date;
  lt?: number | string | Date;
  lte?: number | string | Date;
  format?: string;
  relation?: "INTERSECTS" | "CONTAINS" | "WITHIN";
  time_zone?: string;
}
SortOptions
{
  order?: "asc" | "desc";
  mode?: "min" | "max" | "sum" | "avg" | "median";
  missing?: "_last" | "_first" | any;
}

Geo Queries

// Geo Distance Query
const results = await User.query<User>()
  .geoDistance("location", {
    distance: "200km",
    point: {
      lat: 40.73,
      lon: -73.93,
    },
  })
  .execute();

// Geo Bounding Box Query
const results = await User.query<User>()
  .geoBoundingBox("location", {
    topLeft: {
      lat: 40.73,
      lon: -74.1,
    },
    bottomRight: {
      lat: 40.01,
      lon: -73.86,
    },
  })
  .execute();

Aggregations

TypenSearch provides powerful aggregation capabilities for data analysis.

// Metric Aggregations
const results = await User.query<User>()
  .match("role", "developer")
  .aggs(
    "age_stats",
    (a) => a.stats("age") // Calculate statistics (min, max, avg, sum)
  )
  .aggs(
    "avg_salary",
    (a) => a.avg("salary") // Calculate average
  )
  .execute();

// Bucket Aggregations
const results = await User.query<User>()
  .terms("job_categories", { field: "job_title" }) // Group by job title
  .aggs("avg_age", (a) => a.avg("age")) // Add sub-aggregation
  .execute();

// Date Histogram Aggregation
const results = await User.query<User>()
  .dateHistogram("signups_over_time", {
    field: "createdAt",
    calendar_interval: "1d", // Daily intervals
    format: "yyyy-MM-dd",
  })
  .execute();

// Range Aggregation
const results = await User.query<User>()
  .rangeAggregation("salary_ranges", {
    field: "salary",
    ranges: [{ to: 50000 }, { from: 50000, to: 100000 }, { from: 100000 }],
  })
  .execute();

// Nested Aggregations
const results = await User.query<User>()
  .terms("job_categories", { field: "job_title" })
  .aggs("experience_stats", (a) =>
    a
      .stats("years_of_experience")
      .subAggs("salary_stats", (ssa) => ssa.stats("salary"))
  )
  .execute();

Aggregation Options

MetricAggregationOptions
interface MetricAggregationOptions {
  field: string;
  script?: string;
  missing?: unknown;
}
BucketAggregationOptions
interface BucketAggregationOptions {
  field: string;
  size?: number;
  minDocCount?: number;
  order?: {
    [key: string]: "asc" | "desc";
  };
  missing?: unknown;
}
DateHistogramAggregationOptions
interface DateHistogramAggregationOptions {
  field: string;
  interval?: string;
  format?: string;
  timeZone?: string;
  minDocCount?: number;
  missing?: unknown;
}
RangeAggregationOptions
interface RangeAggregationOptions {
  field: string;
  ranges: Array<{
    key?: string;
    from?: number;
    to?: number;
  }>;
  keyed?: boolean;
}

Error Handling

TypenSearch may throw the following errors:

try {
  await user.save();
} catch (error) {
  if (error instanceof ValidationError) {
    // Validation failed
    console.error("Validation failed:", error.message);
  } else if (error instanceof ConnectionError) {
    // OpenSearch connection failed
    console.error("Connection failed:", error.message);
  } else {
    // Other errors
    console.error("Unknown error:", error);
  }
}

Best Practices

Index Settings Optimization

@OpenSearchIndex({
  name: 'products',
  settings: {
    'index.mapping.total_fields.limit': 2000,
    'index.number_of_shards': 3,
    'index.number_of_replicas': 1,
    'index.refresh_interval': '5s',
    analysis: {
      analyzer: {
        my_analyzer: {
          type: 'custom',
          tokenizer: 'standard',
          filter: ['lowercase', 'stop', 'snowball']
        }
      }
    }
  }
})

Efficient Searching

const results = await Product.search({
  _source: ["name", "price"], // Only fetch needed fields
  query: {
    bool: {
      must: [{ match: { name: "phone" } }],
      filter: [{ range: { price: { gte: 100, lte: 200 } } }],
    },
  },
  sort: [{ price: "asc" }],
  from: 0,
  size: 20,
});

Migration Best Practices

  1. Always test with dryRun first
  2. Use backup: true option for important changes
  3. Set waitForCompletion: false for large datasets and run in background
  4. Monitor migration progress using getMigrationHistory()

Contributing

  1. Fork the repository
  2. Create your feature branch: git checkout -b feature/something-new
  3. Commit your changes: git commit -am 'Add some feature'
  4. Push to the branch: git push origin feature/something-new
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

  • Report bugs and request features through issues
  • Contribute code through pull requests
  • Suggest documentation improvements
  • Share use cases