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.
- 🎯 Intuitive schema definition with TypeScript decorators
- 🚀 Automatic index management and mapping
- ⚡ Type-safe CRUD operations
- đź› Custom field options support
- 🔍 Powerful search capabilities
npm install --save typensearchimport { initialize } from "typensearch";
await initialize(
{
node: "http://localhost:9200",
// Additional OpenSearch client options
auth: {
username: "admin",
password: "admin",
},
ssl: {
rejectUnauthorized: false,
},
},
{
createIndexesIfNotExists: [User.prototype],
}
);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;
};
}// Create a document
const user = await User.index({
username: "john_doe",
email: "john.doe@example.com",
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: "john.doe@example.com",
address: {
street: "123 Main St",
city: "New York",
country: "USA",
},
},
{
_id: "existing_user", // Update if ID exists
username: "jane_doe",
email: "jane.doe@example.com",
},
],
{ 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" },
},
});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();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
}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
}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;
}All methods return Promises.
Model.index<T>(doc: Partial<T>, refresh?: boolean): Create a new documentModel.get<T>(id: string): Get document by IDModel.updateMany<T>(query: any, updates: Partial<T>, options?: UpdateOptions): Update multiple documentsModel.deleteMany(query: any): Delete multiple documentsModel.search(body: any, size?: number): Search documents with raw queryModel.count(body: any): Count documentsModel.bulkIndex<T>(docs: Partial<T>[], options?: BulkOptions): Create or update multiple documents in one operationModel.bulkDelete(ids: string[], options?: BulkOptions): Delete multiple documents by their IDsModel.planMigration(): Generate schema change planModel.migrate(options?: MigrationOptions): Execute schema changesModel.rollback(migrationId: string): Rollback a migrationModel.getMigrationHistory(): Get migration historyModel.getMapping(): Get current index mapping with all field optionsModel.query<T>(): Get a new query builder instance
save(refresh?: boolean): Save current documentdelete(refresh?: boolean): Delete current documentvalidate(): Validate document against schema rules
Provides a type-safe query builder for writing OpenSearch 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();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();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();{
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;
}{
boost?: number;
case_insensitive?: boolean;
}{
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;
}{
order?: "asc" | "desc";
mode?: "min" | "max" | "sum" | "avg" | "median";
missing?: "_last" | "_first" | any;
}// 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();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();interface MetricAggregationOptions {
field: string;
script?: string;
missing?: unknown;
}interface BucketAggregationOptions {
field: string;
size?: number;
minDocCount?: number;
order?: {
[key: string]: "asc" | "desc";
};
missing?: unknown;
}interface DateHistogramAggregationOptions {
field: string;
interval?: string;
format?: string;
timeZone?: string;
minDocCount?: number;
missing?: unknown;
}interface RangeAggregationOptions {
field: string;
ranges: Array<{
key?: string;
from?: number;
to?: number;
}>;
keyed?: boolean;
}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);
}
}@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']
}
}
}
}
})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,
});- Always test with
dryRunfirst - Use
backup: trueoption for important changes - Set
waitForCompletion: falsefor large datasets and run in background - Monitor migration progress using
getMigrationHistory()
- Fork the repository
- Create your feature branch:
git checkout -b feature/something-new - Commit your changes:
git commit -am 'Add some feature' - Push to the branch:
git push origin feature/something-new - Submit a pull request
This project is licensed under the MIT License - see the LICENSE file for details.
- Report bugs and request features through issues
- Contribute code through pull requests
- Suggest documentation improvements
- Share use cases