@tna5/tantex
v0.1.1
Published
Client for the tantex full-text search server
Readme
tantex
JavaScript/TypeScript client for the tantex full-text search server.
Installation
npm install tantex
# or
bun add tantexQuick start
import { Tantex } from 'tantex'
const client = new Tantex({ url: 'http://localhost:3000' })
// 1. Create an index
await client.createIndex('articles', {
fields: [
{ name: 'title', type: 'text' },
{ name: 'body', type: 'text' },
{ name: 'date', type: 'date', fast: true },
{ name: 'views', type: 'u64', fast: true },
],
compression: 'zstd:3',
})
// 2. Ingest documents
await client.ingest('articles', [
{ title: 'Hello world', body: 'My first post.', date: '2026-01-01', views: 120 },
{ title: 'Tantex is fast', body: 'Benchmarks ...', date: '2026-06-17', views: 4800 },
])
// 3. Commit (make docs visible to searches)
await client.commit('articles')
// 4. Search
const results = await client.search('articles', 'fast search')
console.log(results.hits)new Tantex(options?)
| Option | Type | Default | Description |
|---|---|---|---|
| url | string | "http://127.0.0.1:3000" | HTTP URL of the tantex server |
| apiKey | string | — | API key sent as X-Api-Key. Only required when the server has keys configured. |
| timeout | number | 30000 | Request timeout in milliseconds |
// Local server, no auth
const client = new Tantex()
// Remote server with API key
const client = new Tantex({
url: 'https://search.example.com',
apiKey: 'tant2_abc123...',
})Index management
createIndex(name, schema)
Create a new index. The schema is permanent — changing field types requires deleting and recreating the index.
await client.createIndex('products', {
fields: [
{ name: 'id', type: 'u64', stored: true, indexed: true, fast: true },
{ name: 'name', type: 'text', stored: true, indexed: true },
{ name: 'category', type: 'text', stored: true, indexed: true, tokenizer: 'raw' },
{ name: 'price', type: 'f64', stored: true, indexed: false, fast: true },
{ name: 'in_stock', type: 'bool', stored: true, indexed: true },
{ name: 'tags', type: 'json', stored: true, indexed: true },
],
compression: 'zstd:3',
block_size: 16384,
})Field types
| Type | Description |
|---|---|
| text | Full-text field. Tokenized by default ("default" tokenizer). |
| u64 | Unsigned 64-bit integer |
| i64 | Signed 64-bit integer |
| f64 | 64-bit float |
| date | ISO 8601 date/datetime string |
| bool | Boolean |
| bytes | Raw byte array |
| json | Arbitrary JSON object (nested fields searchable) |
Field options
| Option | Default | Description |
|---|---|---|
| stored | true | Store the raw value so it can be retrieved in search hits |
| indexed | true | Make the field searchable / filterable |
| fast | false | Enable columnar access for sorting, aggregations, and range filters |
| tokenizer | "default" | Tokenizer for text fields: "default", "raw", "en_stem" |
Schema options
| Option | Default | Description |
|---|---|---|
| compression | "lz4" | Docstore compression: "none", "lz4", "zstd", "zstd:3", "brotli", "snappy" |
| block_size | 16384 | Docstore block size in bytes. Larger = better compression, slower point reads. |
listIndexes()
const indexes = await client.listIndexes()
// IndexInfo[]
// { name, doc_count, num_segments, pending_docs, search_count, total_docs_ingested, ... }getIndex(name)
const info = await client.getIndex('articles')
// IndexDetails — includes schema, doc_count, num_segments, pending_docsdeleteIndex(name)
Deletes the index and all its data from disk. Irreversible.
await client.deleteIndex('articles')getSegments(name)
const segments = await client.getSegments('articles')
// SegmentInfo[] — { segment_id, num_docs, num_deleted_docs, size_bytes }Ingesting documents
ingest(index, documents, options?)
Ingest an array of plain objects. Documents are split into batches and sent automatically.
const result = await client.ingest('products', docs, {
batchSize: 2000,
onProgress: (indexed, total) => {
console.log(`${indexed} / ${total} indexed`)
},
})
// result: { indexed: number, errors: string[] }Ingest options
| Option | Default | Description |
|---|---|---|
| batchSize | 1000 | Documents per HTTP request |
| onProgress | — | Called after each batch: (indexed: number, total: number) => void |
ingestStream(index, source, options?)
Ingest from an async iterable — suitable for large files or streaming pipelines. Memory usage stays bounded regardless of total document count.
import { createReadStream } from 'node:fs'
import { createInterface } from 'node:readline'
async function* readNdjson(path) {
const rl = createInterface({ input: createReadStream(path) })
for await (const line of rl) {
if (line.trim()) yield JSON.parse(line)
}
}
const result = await client.ingestStream('articles', readNdjson('./dump.ndjson'), {
batchSize: 5000,
onProgress: (n) => process.stdout.write(`\r${n.toLocaleString()} docs`),
})commit(index)
Flush pending documents and make them visible to searches. The server also auto-commits on a configurable timer and document-count threshold.
await client.commit('articles')Searching
search(index, query, options?)
const results = await client.search('articles', 'rust programming', {
limit: 20,
offset: 0,
})
for (const hit of results.hits) {
console.log(hit.score, hit.doc)
}
// results.total_hits — total number of matching documents
// results.elapsed_us — server-side query latency in microsecondsSearch options
| Option | Default | Description |
|---|---|---|
| limit | 10 | Maximum number of hits to return |
| offset | 0 | Number of hits to skip (for pagination) |
Query syntax
tantex uses tantivy query syntax.
# Full-text (searches all indexed text fields)
rust programming
# Field-scoped
title:tantivy body:search
# Phrase
"full text search"
# Boolean
rust AND NOT javascript
title:tantivy OR title:search
# Range (numeric / date)
views:[100 TO 5000]
date:[2026-01-01 TO 2026-12-31]
# Wildcard (prefix only)
prog*
# Boost
title:rust^3 body:rustMetrics
getMetrics()
const metrics = await client.getMetrics()
// {
// totalDocs: number,
// totalIndexes: number,
// totalSegments: number,
// totalPendingDocs: number,
// indexes: IndexInfo[],
// }Configuration
getConfig()
const config = await client.getConfig()setConfig(patch)
Update tunable server parameters at runtime. All fields are optional.
await client.setConfig({
auto_commit_doc_count: 500_000,
auto_commit_interval_secs: 10,
num_indexing_threads: 8,
merge_target_docs: 20_000_000,
})| Field | Description |
|---|---|
| shm_buffer_size | Shared-memory buffer size per session (bytes) |
| writer_heap_size | Tantivy writer heap in bytes |
| auto_commit_doc_count | Soft commit threshold (docs buffered) |
| auto_commit_interval_secs | Idle timeout before auto-commit |
| hard_commit_multiplier | Hard commit at N × auto_commit_doc_count docs |
| merge_target_docs | Target segment size for the merge policy |
| max_merge_factor | Max segments merged in one pass |
| min_num_segments | Minimum segments before a merge is scheduled |
| num_indexing_threads | Total writer thread budget |
| index_threads_pct | % of threads given to tantivy vs the parse pool |
API keys
API keys protect the HTTP API for external clients. The dashboard (localhost) always has full access.
authStatus()
const { auth_required } = await client.authStatus()listApiKeys()
const keys = await client.listApiKeys()
// ApiKeyInfo[] — { id, name, created_at, last_used_at }createApiKey(name)
The raw key is returned exactly once — store it immediately.
const { id, name, key } = await client.createApiKey('production-app')
console.log(key) // tant2_abc123... ← save this nowrevokeApiKey(id)
await client.revokeApiKey(id)Error handling
All methods throw TantexError on HTTP errors.
import { Tantex, TantexError } from 'tantex'
try {
await client.search('missing-index', 'query')
} catch (err) {
if (err instanceof TantexError) {
console.error(err.status, err.message) // 404, "index not found"
}
}High-throughput local ingestion
For maximum ingestion speed on a local machine, use TantexSocketClient which speaks the binary Unix socket protocol directly (MessagePack over a Unix domain socket, same transport as the internal benchmark tool).
import { TantexSocketClient } from 'tantex'
const client = new TantexSocketClient('/tmp/tantex.sock')
await client.connect()
await client.createIndex('bench', { fields: [...] })
const result = await client.ingest('bench', docs, {
batchSize: 5000,
onProgress: (n) => console.log(`${n} indexed`),
})
await client.commit('bench')
client.close()TantexSocketClient exposes the same methods as Tantex plus getSegments() and getConfig() / setConfig(). It works in Node.js and Bun. Not available in browsers.
Tip: Open multiple
TantexSocketClientconnections and run them in parallel to saturate all indexing threads — the server handles concurrent writers per connection.
TypeScript
The package ships full type definitions. Key types:
import type {
TantexOptions,
SchemaDefinition,
FieldDefinition,
FieldType,
IndexInfo,
IndexDetails,
SegmentInfo,
SearchResult,
SearchHit,
SearchOptions,
IngestResult,
IngestOptions,
ServerConfig,
ConfigPatch,
ApiKeyInfo,
CreatedApiKey,
} from 'tantex'