@searchcraft/client
v0.2.1
Published
TypeScript client library for the Searchcraft API
Readme
Features
- Full TypeScript support with comprehensive type definitions.
- Works with JavaScript projects as well.
- Functional, immutable API design.
- Composable query builder.
- Support for all Searchcraft query modes (fuzzy, exact, dynamic) and operations.
- Full index, federation, synonyms, and stopwords management.
- Complete query language support.
- Works in both Node.js and browser environments.
- Available via NPM and CDN.
- Zero runtime dependencies.
Installation
NPM
npm install @searchcraft/clientYarn
yarn add @searchcraft/clientPNPM
pnpm add @searchcraft/clientCDN (UMD)
<script src="https://unpkg.com/@searchcraft/client/dist/index.umd.js"></script>
<script>
const { createClient, createApiKey, createIndexName } = SearchcraftClient;
</script>Quick Start
import { createClient, createApiKey, createIndexName, fuzzy } from '@searchcraft/client';
// Initialize the client
const client = createClient({
endpointUrl: 'https://your-searchcraft-instance.com',
readKey: createApiKey('your-read-key'),
});
// Perform a search
const indexName = createIndexName('your-index');
const request = fuzzy().term('search query').limit(10).buildRequest();
const response = await client.search.searchIndex(indexName, request);
console.log(`Found ${response.data.count} results`);
console.log(response.data.hits);Usage
Client Initialization
import { createClient, createApiKey } from '@searchcraft/client';
const client = createClient({
endpointUrl: 'https://your-instance.com',
readKey: createApiKey('your-read-key'),
ingestKey: createApiKey('your-ingest-key'), // Optional, for document operations
adminKey: createApiKey('your-admin-key'), // Optional, only needed for self-hosted clusters
timeout: 30000, // Optional, default 30 seconds
headers: {
// Optional Searchcraft headers for analytics and tracking:
'X-Sc-User-Id': 'user-123', // Unique identifier for the current user
'X-Sc-Session-Id': 'session-abc', // Session identifier for tracking user sessions
'X-Sc-User-Type': 'authenticated', // User type: 'authenticated' or 'anonymous'
},
});Note: The
adminKeyis only required for self-hosted Searchcraft clusters. If you're using Searchcraft Cloud, you do not need to provide an admin key.
Runtime notes: Streaming endpoints (e.g.
searchSummary) require a runtime with WHATWGReadableStreamsupport — Node.js 18+, Deno, Bun, or any modern browser. When running on Node.js the client automatically sets aUser-Agent: searchcraft-client-js/<version>header; in browsers the header is suppressed becauseUser-Agentis forbidden by the fetch specification.
Basic Search
import { fuzzy, exact, dynamic } from '@searchcraft/client';
// Fuzzy search (typo-tolerant, uses weight ranking, synonyms and stopwords)
const fuzzyQuery = fuzzy().term('search term').limit(20).buildRequest();
// Exact search
const exactQuery = exact().term('exact match').buildRequest();
// Dynamic search (adapts based on word count)
const dynamicQuery = dynamic().term('adaptive search').buildRequest();
const response = await client.search.searchIndex(indexName, fuzzyQuery);Query Builder
The query builder provides a fluent, immutable API for constructing complex queries.
📚 Full Query Syntax Documentation: For complete details on the Searchcraft query language, operators, and advanced features, see the official documentation.
import { exact } from '@searchcraft/client';
const query = exact()
.field('category', 'electronics')
.and(exact().range('price', 10, 100))
.not('discontinued')
.orderBy('rating', 'desc')
.limit(25)
.buildRequest();
const response = await client.search.searchIndex(indexName, query);Field Queries
// Simple field match
exact().field('title', 'laptop').buildRequest();
// IN query
exact().fieldIn('tags', ['tech', 'gadgets', 'mobile']).buildRequest();
// Range query
exact().range('price', 10, 100).buildRequest(); // Inclusive
exact().range('price', 10, 100, false).buildRequest(); // Exclusive
// Comparison queries
exact().compare('rating', '>', 4.5).buildRequest();
exact().compare('stock', '<=', 10).buildRequest();Date Queries
const from = new Date('2024-01-01');
const to = new Date('2024-12-31');
exact().range('created_at', from, to).buildRequest();
exact().compare('updated_at', '>=', new Date('2024-06-01')).buildRequest();Boolean Operators
// AND
exact().field('active', true).and(exact().compare('price', '<', 100)).buildRequest();
// OR
exact().field('brand', 'Apple').or(exact().field('brand', 'Samsung')).buildRequest();
// NOT (exclusion)
fuzzy().term('laptop').not('refurbished').buildRequest();
// Grouping
const subQuery = exact().field('category', 'tech').or('category:science');
exact().group(subQuery).and(exact().compare('rating', '>', 4)).buildRequest();Pagination and Sorting
fuzzy()
.term('query')
.limit(20)
.offset(40)
.orderBy('created_at', 'desc')
.buildRequest();Federated Search
Search across multiple indices:
import { createFederationName } from '@searchcraft/client';
const federationName = createFederationName('my-federation');
const request = fuzzy().term('search term').buildRequest();
const response = await client.search.searchFederation(federationName, request);AI-Powered Search Summaries
When an index has AI enabled and a search_summary configuration, you can
stream an LLM-generated summary of the top hits for a query. The engine
responds with Server-Sent Events which the client surfaces as an async
iterable of tagged events.
import { fuzzy } from '@searchcraft/client';
const request = fuzzy().term('wireless headphones').limit(5).buildRequest();
for await (const event of client.search.searchSummary(indexName, request)) {
switch (event.type) {
case 'metadata':
console.log('Result count:', event.data.results_count, 'cached:', event.data.cached);
break;
case 'delta':
process.stdout.write(event.data.content);
break;
case 'done':
console.log('\nStream complete.');
break;
case 'error':
console.error('Summary error:', event.data.message);
break;
}
}Event shapes:
| Event type | Data shape |
|------------|----------------------------------------------|
| metadata | { results_count: number; cached: boolean } |
| delta | { content: string } |
| done | { results_count: number } |
| error | { message: string } |
Requires that the index has ai_enabled: true, that an llm_provider and
search_summary.model are configured, and that the authentication key has
the LLM_RAG_SUMMARIES permission.
Advanced: Raw Query Objects
For complex queries or when you need maximum control, you can construct request objects directly instead of using the query builder:
const request = {
query: [
{ occur: 'must', exact: { ctx: 'active:true' } },
{ occur: 'must', exact: { ctx: 'category:/electronics' } },
{ fuzzy: { ctx: 'wireless headphones' } },
],
limit: 20,
};
const response = await client.search.searchIndex(indexName, request);Note: The query builder (e.g., fuzzy().term()...) is recommended for most use cases. Use raw objects when you need to dynamically construct complex queries or have specific requirements the builder doesn't cover.
Document Management
import { createIndexName } from '@searchcraft/client';
const indexName = createIndexName('my-index');
// Insert a document
await client.documents.insert(indexName, {
id: '123',
title: 'Product Title',
description: 'Product description',
price: 99.99,
});
// Delete a document by its id
await client.documents.delete(indexName, '123');
// Batch insert documents
await client.documents.batchInsert(indexName, [
{ id: '1', title: 'Product 1' },
{ id: '2', title: 'Product 2' },
]);
// Batch delete documents by their ids
await client.documents.batchDelete(indexName, ['1', '2']);
// Delete all documents from an index
await client.documents.deleteAll(indexName);Index Management
import { createIndexName } from '@searchcraft/client';
const indexName = createIndexName('my-index');
// List all index names
const { index_names } = await client.indices.list();
// Get index configuration
const config = await client.indices.get(indexName);
// Create a new index
await client.indices.create(indexName, {
search_fields: ['title', 'body'],
fields: {
title: { type: 'text', indexed: true, stored: true },
body: { type: 'text', indexed: true, stored: true },
},
});
// Update index configuration (partial)
await client.indices.update(indexName, {
weight_multipliers: { title: 2.0, body: 0.7 },
});
// Inspect AI capability flags for an index
const { ai } = await client.indices.getCapabilities(indexName);
console.log(ai.enabled, ai.searchSummaryConfigured);
// Delete an index
await client.indices.delete(indexName);Enabling AI on an Index
Indices created or updated against Searchcraft engine 0.10.0 or newer can
carry an ai configuration block plus a top-level ai_enabled flag. Toggling
ai_enabled requires an admin-level key; editing other ai fields only
requires the ingest key.
await client.indices.create(indexName, {
search_fields: ['title', 'body'],
fields: {
title: { type: 'text', indexed: true, stored: true },
body: { type: 'text', indexed: true, stored: true },
},
ai_enabled: true,
ai: {
llm_provider: 'anthropic',
llm_api_key: process.env.ANTHROPIC_API_KEY,
search_summary: {
model: 'claude-sonnet-4-6',
temperature: 0.2,
character_limit: 500,
},
},
});
// Partial updates send a flat body (no `{ index: ... }` wrapper).
await client.indices.update(indexName, { ai_enabled: false });Supported llm_provider values: anthropic, bedrock, google, llamacpp,
mistral, ollama, openai, xai.
Federation Management
import { createFederationName } from '@searchcraft/client';
const federationName = createFederationName('my-federation');
// List all federations
const federations = await client.federations.list();
// Get a specific federation
const federation = await client.federations.get(federationName);
// Delete a federation
await client.federations.delete(federationName);Synonyms
// Get all synonyms for an index
const synonyms = await client.synonyms.get(indexName);
// Add synonyms — format: "synonym:original-term" or "many,synonyms:original,terms"
await client.synonyms.add(indexName, [
'nyc:new york city',
'usa:united states',
]);
// Delete specific synonyms by key
await client.synonyms.delete(indexName, ['nyc', 'usa']);
// Delete all synonyms
await client.synonyms.deleteAll(indexName);Stopwords
// Get all stopwords for an index
const stopwords = await client.stopwords.get(indexName);
// Add custom stopwords
await client.stopwords.add(indexName, ['foo', 'bar']);
// Delete specific stopwords
await client.stopwords.delete(indexName, ['foo', 'bar']);
// Delete all custom stopwords
await client.stopwords.deleteAll(indexName);Authentication Management (self-hosted only)
The auth endpoints are only available on self-hosted Searchcraft clusters and
require an adminKey in the client config.
// List every key on the cluster
const allKeys = await client.auth.listKeys();
// Scope lookups by owner
const appKeys = await client.auth.listApplicationKeys(42);
const orgKeys = await client.auth.listOrganizationKeys(7);
const fedKeys = await client.auth.listFederationKeys(createFederationName('my-federation'));
// New in engine 0.10.0: list every key that can access a given index
const indexKeys = await client.auth.listIndexKeys(createIndexName('products'));
// Create, update, and delete keys
const created = await client.auth.createKey({
name: 'my-read-key',
permissions: 1, // 1 = read, 15 = ingest, 63 = admin
allowed_indexes: ['products'],
status: 'active',
});
await client.auth.updateKey(created.key, { status: 'inactive' });
await client.auth.deleteKey(created.key);Health Check
// check() throws on error, so if it returns the service is healthy
const health = await client.health.check();
console.log('Status:', health.status); // 200
console.log('Message:', health.data); // "Searchcraft is healthy."API Reference
Client
createClient(config: SearchcraftConfig): SearchcraftClient
Search API
searchIndex<T>(indexName: IndexName, request: SearchRequest): Promise<SearchResponse<T>>searchFederation<T>(federationName: FederationName, request: SearchRequest): Promise<SearchResponse<T>>searchSummary(indexName: IndexName, request: SearchRequest): AsyncIterable<SummaryStreamEvent>— Streams an AI-generated summary as taggedmetadata/delta/done/errorevents (engine 0.10.0+)
Document API
insert(indexName: IndexName, document: DocumentWithId): Promise<DocumentOperationResponse>- Insert a documentdelete(indexName: IndexName, documentId: string | number): Promise<DocumentDeleteResponse>- Delete a document by idbatchInsert(indexName: IndexName, documents: DocumentWithId[]): Promise<DocumentOperationResponse>- Batch insert documentsbatchDelete(indexName: IndexName, documentIds: (string | number)[]): Promise<DocumentDeleteResponse>- Batch delete documents by idsdeleteAll(indexName: IndexName): Promise<DocumentOperationResponse>- Delete all documents from an indexget<T>(indexName: IndexName, internalId: string): Promise<SearchHit<T>>- Get a document by its internal Searchcraft ID (_id)
Index API
list(): Promise<IndexListResponse>- List all index namesget(indexName: IndexName): Promise<IndexConfig>- Get the configuration for a specific indexcreate(indexName: IndexName, indexConfig: IndexConfig): Promise<IndexOperationResponse>- Create a new indexupdate(indexName: IndexName, indexConfig: Partial<IndexConfig>): Promise<IndexOperationResponse>- Update an existing index (partial)delete(indexName: IndexName): Promise<IndexOperationResponse>- Delete an indexgetStats(): Promise<AllIndexStatsResponse>- Get document counts for every index on the clustergetIndexStats(indexName: IndexName): Promise<IndexStats>- Get the document count for a single indexgetCapabilities(indexName: IndexName): Promise<IndexCapabilities>- Inspect AI capability flags for an index (engine 0.10.0+)
Federation API
list(): Promise<Federation[]>- List all federationsget(federationName: FederationName): Promise<Federation>- Get a specific federationdelete(federationName: FederationName): Promise<FederationOperationResponse>- Delete a federation
Synonyms API
get(indexName: IndexName): Promise<SynonymsMap>- Get all synonyms for an indexadd(indexName: IndexName, synonyms: string[]): Promise<SynonymOperationResponse>- Add synonyms in"synonym:original-term"formatdelete(indexName: IndexName, synonyms: string[]): Promise<SynonymOperationResponse>- Delete specific synonyms by keydeleteAll(indexName: IndexName): Promise<SynonymOperationResponse>- Delete all synonyms from an index
Stopwords API
get(indexName: IndexName): Promise<string[]>- Get all stopwords for an indexadd(indexName: IndexName, stopwords: string[]): Promise<StopwordOperationResponse>- Add custom stopwordsdelete(indexName: IndexName, stopwords: string[]): Promise<StopwordOperationResponse>- Delete specific stopwordsdeleteAll(indexName: IndexName): Promise<StopwordOperationResponse>- Delete all stopwords from an index
Auth API (self-hosted only)
Requires adminKey in the client config.
listKeys(): Promise<AuthKeyListResponse>- List every authentication key on the clustergetKey(key: string): Promise<AuthKey>- Look up a single key by valuecreateKey(request: CreateAuthKeyRequest): Promise<AuthKey>- Create a new keyupdateKey(key: string, request: UpdateAuthKeyRequest): Promise<AuthKey>- Partially update a keydeleteKey(key: string): Promise<string>- Delete a keydeleteAllKeys(): Promise<string>- Delete every key on the clusterlistApplicationKeys(applicationId: number): Promise<AuthKeyListResponse>- List keys for an applicationlistOrganizationKeys(organizationId: number): Promise<AuthKeyListResponse>- List keys for an organizationlistFederationKeys(federationName: FederationName): Promise<AuthKeyListResponse>- List keys associated with a federationlistIndexKeys(indexName: IndexName): Promise<AuthKeyListResponse>- List keys that can access an index (engine 0.10.0+)
Health API
check(): Promise<HealthCheckResponse>
Query Builder
fuzzy(): QueryBuilder- Create a fuzzy query builderexact(): QueryBuilder- Create an exact query builderdynamic(): QueryBuilder- Create a dynamic query builder
QueryBuilder Methods
All methods return a new QueryBuilder instance (immutable):
term(term: string): QueryBuilder- Add a search termfield(field: string, value: string | number | boolean): QueryBuilder- Add field:value queryfieldIn(field: string, values: (string | number)[]): QueryBuilder- Add field IN queryrange(field: string, from: string | number | Date, to: string | number | Date, inclusive?: boolean): QueryBuilder- Add range querycompare(field: string, operator: '>' | '<' | '>=' | '<=', value: number | Date): QueryBuilder- Add comparison queryand(query: string | QueryBuilder): QueryBuilder- Add AND operatoror(query: string | QueryBuilder): QueryBuilder- Add OR operatornot(term: string): QueryBuilder- Add NOT operatorgroup(query: string | QueryBuilder): QueryBuilder- Group query with parentheseslimit(limit: number): QueryBuilder- Set result limitoffset(offset: number): QueryBuilder- Set result offsetorderBy(field: string, sort?: 'asc' | 'desc'): QueryBuilder- Set orderingoccur(occur: 'should' | 'must'): QueryBuilder- Set occur modebuild(): SearchQuery- Build the query objectbuildRequest(): SearchRequest- Build the complete request object
Type Safety
The library uses branded types for enhanced compile-time type safety. Branded types prevent you from accidentally mixing up different string-based identifiers.
Benefits of Branded Types
- ✅ Compile-time safety - TypeScript catches type mismatches before runtime
- ✅ Prevents mixing types - Can't accidentally use an
IndexNamewhere aFederationNameis expected - ✅ IDE support - Better autocomplete and inline error detection
- ✅ Zero runtime overhead - Types are erased during compilation
- ✅ Works in JavaScript - Functions work normally, just without compile-time checks
Usage
import {
createApiKey,
createIndexName,
createFederationName,
createDocumentId
} from '@searchcraft/client';
const apiKey = createApiKey('key'); // ApiKey type
const indexName = createIndexName('my-index'); // IndexName type
const federationName = createFederationName('my-fed'); // FederationName type
const docId = createDocumentId('123'); // DocumentId type
// ✅ TypeScript ensures you use the correct type
await client.search.searchIndex(indexName, request);
await client.search.searchFederation(federationName, request);
// ❌ TypeScript error - prevents mistakes at compile time
await client.search.searchFederation(indexName, request); // Error!Error Handling
import {
SearchcraftError,
AuthenticationError,
NotFoundError,
ValidationError,
NetworkError,
ApiError,
} from '@searchcraft/client';
try {
const response = await client.search.searchIndex(indexName, request);
} catch (error) {
if (error instanceof AuthenticationError) {
console.error('Authentication failed');
} else if (error instanceof ValidationError) {
console.error('Validation error:', error.field);
} else if (error instanceof NetworkError) {
console.error('Network error');
}
}Documentation
For comprehensive documentation on Searchcraft, including:
- Query Language Reference - Complete syntax and operators
- API Reference - All endpoints and parameters
- Search Guides - Best practices and advanced techniques
- Integration Examples - Real-world use cases
Visit the official documentation at docs.searchcraft.io
Development
# Install dependencies
npm install
# Run tests
npm test
# Run tests with coverage
npm run test:coverage
# Build
npm run build
# Lint
npm run lint
# Format
npm run formatIssues
Please file issues in the Searchcraft Issues repository.
