endee
v1.1.0
Published
TypeScript client for encrypted vector database with maximum security and speed
Maintainers
Readme
Endee - TypeScript Vector Database Client
Endee is a TypeScript client for a local vector database designed for maximum speed and efficiency. This package provides full type safety, modern ES module support, and optimized code for rapid Approximate Nearest Neighbor (ANN) searches on vector data.
Key Features
- TypeScript First: Full type safety and IntelliSense support
- Fast ANN Searches: Efficient similarity searches on vector data
- Multiple Distance Metrics: Support for cosine, L2, and inner product distance metrics
- Hybrid Indexes: Support for dense vectors, sparse vectors, and hybrid (dense + sparse) searches
- Metadata Support: Attach and search with metadata and filters
- High Performance: Optimized for speed and efficiency
- Modern ES Modules: Native ES module support with proper tree-shaking
Requirements
- Node.js >= 18.0.0
- Endee Local server running (see Quick Start)
Installation
npm install endeeQuick Start
Initialize the Client
The Endee client connects to your local server (defaults to http://127.0.0.1:8080/api/v1):
import { Endee, Precision } from 'endee';
// Connect to local Endee server (defaults to localhost:8080)
const client = new Endee();Using Authentication? If your server has NDD_AUTH_TOKEN set, pass the same token when initializing:
const client = new Endee('your-auth-token');Setting a Custom Base URL
If your server runs on a different port, use setBaseUrl():
const client = new Endee();
// Set custom base URL for non-default port
client.setBaseUrl('http://0.0.0.0:8081/api/v1');Create a Dense Index
import { Precision } from 'endee';
await client.createIndex({
name: 'my_vectors',
dimension: 384,
spaceType: 'cosine',
precision: Precision.INT8D,
});Dense Index Parameters:
| Parameter | Description |
|-----------|-------------|
| name | Unique name for your index |
| dimension | Vector dimensionality (must match your embedding model's output) |
| spaceType | Distance metric - "cosine", "l2", or "ip" (inner product) |
| M | Graph connectivity - higher values increase recall but use more memory (default: 16) |
| efCon | Construction-time parameter - higher values improve index quality (default: 128) |
| precision | Quantization precision (default: Precision.INT8D) |
Create a Hybrid Index
Hybrid indexes combine dense vector search with sparse vector search. Add the sparseDimension parameter:
await client.createIndex({
name: 'hybrid_index',
dimension: 384, // Dense vector dimension
sparseDimension: 30000, // Sparse vector dimension (vocabulary size)
spaceType: 'cosine',
precision: Precision.INT8D,
});List and Access Indexes
// List all indexes
const indexes = await client.listIndexes();
// Get reference to an existing index
const index = await client.getIndex('my_vectors');
// Delete an index
await client.deleteIndex('my_vectors');Upserting Vectors
The index.upsert() method adds or updates vectors in an existing index.
const index = await client.getIndex('my_index');
await index.upsert([
{
id: 'vec1',
vector: [0.1, 0.2, 0.3 /* ... */],
meta: { title: 'First document' },
filter: { category: 'tech' },
},
{
id: 'vec2',
vector: [0.3, 0.4, 0.5 /* ... */],
meta: { title: 'Second document' },
filter: { category: 'science' },
},
]);Vector Object Fields:
| Field | Required | Description |
|-------|----------|-------------|
| id | Yes | Unique identifier for the vector |
| vector | Yes | Array of floats representing the embedding |
| meta | No | Arbitrary metadata object |
| filter | No | Key-value pairs for filtering during queries |
Querying the Index
The index.query() method performs a similarity search.
const results = await index.query({
vector: [0.15, 0.25 /* ... */],
topK: 5,
ef: 128,
includeVectors: true,
});
for (const item of results) {
console.log(`ID: ${item.id}, Similarity: ${item.similarity}`);
}Query Parameters:
| Parameter | Description |
|-----------|-------------|
| vector | Query vector (must match index dimension) |
| topK | Number of results to return (default: 10, max: 512) |
| ef | Search quality parameter (default: 128, max: 1024) |
| includeVectors | Include vector data in results (default: false) |
| filter | Optional filter criteria (array of filter objects) |
| sparseIndices | Sparse vector indices for hybrid search (default: null) |
| sparseValues | Sparse vector values for hybrid search (default: null) |
| prefilterCardinalityThreshold | Controls when search switches from HNSW filtered search to brute-force prefiltering on the matched subset (default: 10,000, range: 1,000–1,000,000). See Filter Tuning for details. |
| filterBoostPercentage | Expands the internal HNSW candidate pool by this percentage when a filter is active, compensating for filtered-out results (default: 0, range: 0–100). See Filter Tuning for details. |
Filtered Querying
Use the filter parameter to restrict results. All filters are combined with logical AND.
const results = await index.query({
vector: [0.15, 0.25 /* ... */],
topK: 5,
filter: [
{ category: { $eq: 'tech' } },
{ score: { $range: [80, 100] } },
],
});Filtering Operators
| Operator | Description | Example |
|----------|-------------|---------|
| $eq | Exact match | { status: { $eq: 'published' } } |
| $in | Match any in list | { tags: { $in: ['ai', 'ml'] } } |
| $range | Numeric range (inclusive) | { score: { $range: [70, 95] } } |
Note: The
$rangeoperator supports values within [0 - 999]. Normalize larger values before upserting.
Filter Tuning
When using filtered queries, two optional parameters let you tune the trade-off between search speed and recall:
prefilterCardinalityThreshold
Controls when the search strategy switches from HNSW filtered search (fast, graph-based) to brute-force prefiltering (exhaustive scan on the matched subset).
| Value | Behavior |
|-------|----------|
| 1_000 | Prefilter only for very selective filters — minimum value |
| 10_000 | Prefilter only when the filter matches ≤10,000 vectors (default) |
| 1_000_000 | Prefilter for almost all filtered searches — maximum value |
The intuition: when very few vectors match your filter, HNSW may struggle to find enough valid candidates through graph traversal. In that case, scanning the filtered subset directly (prefiltering) is faster and more accurate. Raising the threshold means prefiltering kicks in more often; lowering it favors HNSW graph search.
// Only prefilter when filter matches ≤5,000 vectors
const results = await index.query({
vector: [/* ... */],
topK: 10,
filter: [{ category: { $eq: 'rare' } }],
prefilterCardinalityThreshold: 5_000,
});filterBoostPercentage
When using HNSW filtered search, some candidates explored during graph traversal are discarded by the filter, which can leave you with fewer results than topK. filterBoostPercentage compensates by expanding the internal candidate pool before filtering is applied.
0→ no boost, standard candidate pool size (default)20→ fetch 20% more candidates internally before applying the filter- Maximum:
100(doubles the candidate pool)
// Fetch 30% more candidates to compensate for aggressive filtering
const results = await index.query({
vector: [/* ... */],
topK: 10,
filter: [{ visibility: { $eq: 'public' } }],
filterBoostPercentage: 30,
});Using Both Together
const results = await index.query({
vector: [/* ... */],
topK: 10,
filter: [{ category: { $eq: 'rare' } }],
prefilterCardinalityThreshold: 5_000, // switch to brute-force for small match sets
filterBoostPercentage: 25, // boost candidates for HNSW filtered search
});Tip: Start with the defaults (
prefilterCardinalityThreshold: 10_000,filterBoostPercentage: 0). If filtered queries return fewer results than expected, try increasingfilterBoostPercentage. If filtered queries are slow on selective filters, try loweringprefilterCardinalityThreshold. Valid range for the threshold is1,000–1,000,000.
Hybrid Search
Upserting Hybrid Vectors
Provide both dense vectors and sparse representations:
const index = await client.getIndex('hybrid_index');
await index.upsert([
{
id: 'doc1',
vector: [0.1, 0.2 /* ... */], // Dense vector
sparseIndices: [10, 50, 200], // Non-zero term positions
sparseValues: [0.8, 0.5, 0.3], // Weights for each position
meta: { title: 'Document 1' },
},
{
id: 'doc2',
vector: [0.3, 0.4 /* ... */],
sparseIndices: [15, 100, 500],
sparseValues: [0.9, 0.4, 0.6],
meta: { title: 'Document 2' },
},
]);Hybrid Vector Fields:
| Field | Required | Description |
|-------|----------|-------------|
| id | Yes | Unique identifier |
| vector | Yes | Dense embedding vector |
| sparseIndices | Yes (hybrid) | Non-zero term positions in sparse vector |
| sparseValues | Yes (hybrid) | Weights for each sparse index |
| meta | No | Metadata dictionary |
| filter | No | Filter fields |
Important:
sparseIndicesandsparseValuesmust have the same length. Values insparseIndicesmust be within[0, sparseDimension).
Querying Hybrid Index
Provide both dense and sparse query vectors:
const results = await index.query({
vector: [0.15, 0.25 /* ... */], // Dense query
sparseIndices: [10, 100, 300], // Sparse query positions
sparseValues: [0.7, 0.5, 0.4], // Sparse query weights
topK: 5,
});
for (const item of results) {
console.log(`ID: ${item.id}, Similarity: ${item.similarity}`);
}Hybrid Query Parameters:
| Parameter | Description |
|-----------|-------------|
| vector | Dense query vector |
| sparseIndices | Sparse query positions |
| sparseValues | Sparse query weights |
| topK | Number of results to return |
| ef | Search quality parameter (max 1024, default: 128) |
| includeVectors | Include vector data in results (default: false) |
| filter | Optional filter criteria |
| prefilterCardinalityThreshold | Controls when search switches from HNSW filtered search to brute-force prefiltering on the matched subset (default: 10,000, range: 1,000–1,000,000). See Filter Tuning for details. |
| filterBoostPercentage | Expands the internal HNSW candidate pool by this percentage when a filter is active, compensating for filtered-out results (default: 0, range: 0–100). See Filter Tuning for details. |
You can also query with:
- Dense only: Provide only
vector - Sparse only: Provide only
sparseIndicesandsparseValues - Hybrid: Provide all three for combined results
Deletion Methods
Delete by ID
Delete vector with a specifc vector id.
await index.deleteVector('vec1');Delete by Filter
Delete all vectors matching specific filters.
await index.deleteWithFilter([{'category': {'$eq' : 'tech'}}]);Delete Index
Delete an entire Index.
await client.deleteIndex('my_index');Warning: Deletion operations are irreversible.
Additional Operations
Get Vector by ID
const vector = await index.getVector('vec1');Describe Index
const info = index.describe();
console.log(info);
// { name, spaceType, dimension, sparseDimension, isHybrid, count, precision, M }Precision Options
Endee supports different quantization precision levels:
import { Precision } from 'endee';
Precision.BINARY; // Binary quantization (1-bit) - smallest storage, fastest search
Precision.INT8D; // 8-bit integer quantization (default) - balanced performance
Precision.INT16D; // 16-bit integer quantization - higher precision
Precision.FLOAT16; // 16-bit floating point - good balance
Precision.FLOAT32; // 32-bit floating point - highest precisionChoosing Precision:
BINARY: Best for very large datasets where speed and storage are criticalINT8D(default): Recommended for most use cases - good balance of accuracy and performanceINT16D: When you need better accuracy than INT8D but less storage than FLOAT32FLOAT16: Good compromise between precision and storage for embeddingsFLOAT32: When you need maximum precision and storage is not a concern
Complete Example
import { Endee, Precision } from 'endee';
// Initialize client
const client = new Endee();
// Create a dense index
await client.createIndex({
name: 'documents',
dimension: 384,
spaceType: 'cosine',
precision: Precision.INT8D,
});
// Get the index
const index = await client.getIndex('documents');
// Add vectors
await index.upsert([
{
id: 'doc1',
vector: [0.1, 0.2 /* ... 384 dimensions */],
meta: { title: 'First Document' },
filter: { category: 'tech' },
},
{
id: 'doc2',
vector: [0.3, 0.4 /* ... 384 dimensions */],
meta: { title: 'Second Document' },
filter: { category: 'science' },
},
]);
// Query the index
const results = await index.query({
vector: [0.15, 0.25 /* ... */],
topK: 5,
});
for (const item of results) {
console.log(`ID: ${item.id}, Similarity: ${item.similarity}`);
}API Reference
Endee Class
| Method | Description |
|--------|-------------|
| createIndex(options) | Create a new index (add sparseDimension for hybrid) |
| listIndexes() | List all indexes |
| deleteIndex(name) | Delete an index |
| getIndex(name) | Get reference to an index |
| setBaseUrl(url) | Set a custom base URL |
Index Class
| Method | Description |
|--------|-------------|
| upsert(vectors) | Insert or update vectors |
| query(options) | Search for similar vectors (supports vector, topK, filter, ef, includeVectors, sparseIndices, sparseValues, prefilterCardinalityThreshold, filterBoostPercentage) |
| deleteVector(id) | Delete a vector by ID |
| deleteWithFilter(filter) | Delete vectors by Filter |
| getVector(id) | Get a vector by ID |
| describe() | Get index info |
TypeScript Types
The package includes comprehensive TypeScript types:
import type {
VectorItem,
QueryOptions,
QueryResult,
CreateIndexOptions,
IndexDescription,
SpaceType,
Precision,
} from 'endee';License
MIT
Author
Pankaj Singh
