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",
auth: {
username: "admin",
password: "admin",
},
ssl: {
rejectUnauthorized: false,
},
},
{
createIndexesIfNotExists: [User.prototype],
}
);
2. Model Definition
import { OpenSearchIndex, Field, Model } from "typensearch";
@OpenSearchIndex({
name: "users",
numberOfShards: 2,
numberOfReplicas: 1,
settings: {
"index.mapping.total_fields.limit": 2000,
},
})
class User extends Model {
@Field({
type: "text",
required: true,
fields: {
keyword: { type: "keyword" },
},
})
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
const user = await User.index({
username: "john_doe",
email: "john.doe@example.com",
address: {
street: "123 Main St",
city: "New York",
country: "USA",
},
});
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",
username: "jane_doe",
email: "jane.doe@example.com",
},
],
{ refresh: true }
);
await User.bulkDelete(["user1", "user2", "user3"], { refresh: true });
const foundUser = await User.get("user_id");
foundUser.username = "jane_doe";
await foundUser.save();
await User.updateMany(
{ city: "New York" },
{ country: "US" }
);
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();
await foundUser.delete();
await User.deleteMany({
"address.country": "USA",
});
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.
@OpenSearchIndex({
name: "users",
settings: {
"index.mapping.total_fields.limit": 2000,
},
})
class User extends Model {
@Field({ type: "keyword" })
name: string;
@Field({ type: "integer" })
age: number;
}
@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;
}
const plan = await UpdatedUser.planMigration();
console.log("Migration Plan:", {
addedFields: plan.addedFields,
modifiedFields: plan.modifiedFields,
deletedFields: plan.deletedFields,
requiresReindex: plan.requiresReindex,
estimatedDuration: plan.estimatedDuration,
});
const result = await UpdatedUser.migrate();
const result = await UpdatedUser.migrate({
backup: true,
waitForCompletion: true,
});
if (!result.success) {
const rollback = await UpdatedUser.rollback(result.migrationId);
}
const result = await UpdatedUser.migrate({
backup: true,
waitForCompletion: false,
timeout: "1h",
});
const history = await UpdatedUser.getMigrationHistory();
Migration Options
interface MigrationOptions {
dryRun?: boolean;
backup?: boolean;
waitForCompletion?: boolean;
timeout?: string;
batchSize?: number;
}
API Reference
Decorators
@OpenSearchIndex(options: IndexOptions)
Defines index settings.
interface IndexOptions {
name?: string;
numberOfShards?: number;
numberOfReplicas?: number;
settings?: Record<string, unknown>;
}
@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
const results = await User.query<User>()
.match("username", "john", {
operator: "AND",
fuzziness: "AUTO",
})
.execute();
const results = await User.query<User>()
.term("age", 25, {
boost: 2.0,
})
.execute();
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")
.sort("createdAt", "desc")
.sort("username", { order: "asc", missing: "_last" })
.from(0)
.size(10)
.source({
includes: ["username", "email", "age"],
excludes: ["password"],
})
.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
const results = await User.query<User>()
.geoDistance("location", {
distance: "200km",
point: {
lat: 40.73,
lon: -73.93,
},
})
.execute();
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.
const results = await User.query<User>()
.match("role", "developer")
.aggs(
"age_stats",
(a) => a.stats("age")
)
.aggs(
"avg_salary",
(a) => a.avg("salary")
)
.execute();
const results = await User.query<User>()
.terms("job_categories", { field: "job_title" })
.aggs("avg_age", (a) => a.avg("age"))
.execute();
const results = await User.query<User>()
.dateHistogram("signups_over_time", {
field: "createdAt",
calendar_interval: "1d",
format: "yyyy-MM-dd",
})
.execute();
const results = await User.query<User>()
.rangeAggregation("salary_ranges", {
field: "salary",
ranges: [{ to: 50000 }, { from: 50000, to: 100000 }, { from: 100000 }],
})
.execute();
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) {
console.error("Validation failed:", error.message);
} else if (error instanceof ConnectionError) {
console.error("Connection failed:", error.message);
} else {
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"],
query: {
bool: {
must: [{ match: { name: "phone" } }],
filter: [{ range: { price: { gte: 100, lte: 200 } } }],
},
},
sort: [{ price: "asc" }],
from: 0,
size: 20,
});
Migration Best Practices
- Always test with
dryRun first
- Use
backup: true option for important changes
- Set
waitForCompletion: false for large datasets and run in background
- Monitor migration progress using
getMigrationHistory()
Contributing
- 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
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