@podge/sdk-node
v0.2.0
Published
SDK for the Podge API
Downloads
349
Readme
@podge/sdk-node
Find the needle, keep the haystack. Podge takes data of every shape and size and turns it into shippable, performant search for your application — no infrastructure to run, no query tuning, no schemas to perfect. Just ingest and search.
Highlighting, faceting, and the rest of the polish come standard. Easy for humans and AI agents alike.
This SDK is the fastest way to wire Podge into a Node.js or TypeScript application.
Core concepts
Podge organizes everything into four primitives:
- Workspace — the top-level container, similar to a project. Everything else lives inside it.
- Environment — an isolated context within a workspace. A
devenvironment is created for you by default; add more to match each environment where you keep data (e.g.staging,production). Collections and their data are scoped to an environment, so you can work in one without affecting another. - Collection — a group of items that can be searched together. If your application is
a directory of burritos, you'd create a
burritoscollection. - Item — an individual searchable record within a collection (a single burrito).
Keeping data in sync
Podge is a search index that mirrors your source of truth, so the goal is to keep it in step with your application's data. Hook into the changes in your app and push them to Podge as they happen:
- Item added →
podge.createItem() - Item modified →
podge.updateItem() - Item deleted →
podge.deleteItem()
For data that already exists, use createImportJob() to backfill an entire collection
in one go via a presigned S3 upload — no need to loop through records one at a time.
Note: Podge is currently in closed beta and isn't recommended for production use yet. Create an account to get access.
Installation
pnpm add @podge/sdk-nodeGetting Started
import { PodgeSDK } from "@podge/sdk-node";
interface Product {
title: string;
category: string;
price: number;
}
const podge = new PodgeSDK({
apiKey: "your-api-key",
workspace: "my-workspace",
environment: "production",
});
// Insert a typed item
await podge.createItem<Product>("products", {
title: "Ergonomic Keyboard",
category: "Electronics",
price: 149.99,
});
// Search — results are typed as Product
const { results } = await podge.search<Product>("products", {
search: "keyboard",
});
console.log(results[0].document.title);Usage
Inserting Items
// Insert a single item
await podge.createItem<Product>("products", {
title: "Ergonomic Keyboard",
category: "Electronics",
price: 149.99,
});
// Dry run — validate without persisting
await podge.createItem("products", { title: "Test" }, { dryRun: true });Items are ingested asynchronously — after insertion, the document is queued for tokenization and indexing. Use getDocument() to check ingestion status. Typical ingestion latency is a few seconds for single items.
Search
// Simple search
const { results } = await podge.search<Product>("products", {
search: "keyboard",
});
// With filters, facets, sorting, and highlighting
const { results, facets } = await podge.search<Product>("products", {
search: "ergonomic",
filters: {
category: "Electronics",
price: { gte: 50, lte: 500 },
},
options: {
requireAllWords: true,
limit: 20,
offset: 0,
rank: true,
highlight: {
fields: ["title", "description"],
pre_tag: "<b>",
post_tag: "</b>",
},
},
facets: ["category", "brand"],
sort: [{ field: "price", order: "asc" }],
});Search behavior
- Default mode: Token matching with term count scoring. Each word in the query is matched independently. Results are ranked by how many query terms appear in the document.
rank: true: Enables BM25 relevance scoring (k1=2.5, b=0.75). Documents are ranked by statistical relevance rather than simple term counts.requireAllWords: true: All query terms must appear in the document (AND logic). Default is OR logic.- Quoted phrases: Use
"exact phrase"in the search string for contiguous word matching. - Exclusions: Prefix a term with
-to exclude documents containing that word (e.g.,keyboard -wireless). - Field-scoped search: Use
fieldoption to restrict which fields are searched instead of searching all indexed fields.
Pagination
limit: 1–100 (default 25)offset: 0-based offset for pagination- Response includes
total(total matching documents) andcount(results in this page)
Facets
- Request facets by passing field names in the
facetsarray - Returns up to 100 values per faceted field, sorted by count
- Facet counts reflect all matching documents, not just the current page
Sorting
- Sort by any indexed field with
sort: [{ field: "price", order: "asc" }] - Use
_scoreas a special field name to sort by relevance score
Filters
Filters support direct values for exact match, or operator objects for advanced filtering:
// Exact match
filters: { category: "Electronics" }
// Comparison operators
filters: { price: { gte: 50, lte: 500 } }
// Not equal
filters: { status: { ne: "discontinued" } }
// Match any value in a list
filters: { category: { in: ["Electronics", "Accessories"] } }
// Exclude values
filters: { category: { nin: ["Furniture"] } }
// Field existence
filters: { description: { exists: true } }
// Nested fields use dot notation
filters: { "metadata.region": "us-east" }Available operators: eq, ne, gt, gte, lt, lte, in, nin, exists.
Operator–type compatibility:
| Operator | string | number | date | boolean |
|----------|--------|--------|------|---------|
| eq, ne | yes | yes | yes | yes |
| gt, gte, lt, lte | no | yes | yes | no |
| in, nin | yes | yes | yes | yes |
| exists | yes | yes | yes | yes |
Field Types and Schema
Documents are schema-on-write — field types are automatically detected when documents are ingested:
| Detected type | Examples | Storage |
|---|---|---|
| string | Any text value | string map (searchable) |
| number | Integer values | number map (filterable/sortable) |
| date | ISO 8601, MM/DD/YYYY, DD/MM/YYYY, RFC 2822, Unix timestamps | datetime map (filterable/sortable) |
| boolean | true, false | string map |
Nested objects are flattened with dot notation (e.g., address.city).
All fields default to search status (indexed and searchable). Use updateCollectionSchema to set fields to ignore if they should be excluded from search results:
// View discovered fields and their types
const schema = await podge.getCollectionSchema("products");
// schema.fields → [{ field: "title", fieldType: "string", status: "search", ... }]
// Exclude a field from search indexing
await podge.updateCollectionSchema("products", {
fields: [{ field: "internal_id", status: "ignore" }],
});Collections
const collection = await podge.createCollection({ name: "Products" });
const collections = await podge.listCollections();
const schema = await podge.getCollectionSchema("products");Batch Insert
await podge.createItemsBatch<Product>("products", [
{ title: "Standing Desk", category: "Furniture", price: 599.99 },
{ title: "Monitor Arm", category: "Accessories", price: 89.99 },
]);Bulk Import
For large datasets, use import jobs with presigned S3 upload URLs.
Supported formats: JSON arrays ([{...}, {...}]) or single JSON objects ({...}).
Limits: 200 MB max per file, 10 MB JSON buffer limit.
// Create an import job with presigned upload URLs
const job = await podge.createImportJob("products", {
fileCount: 2,
fileNames: ["batch-1.json", "batch-2.json"],
});
// Upload files to the presigned URLs (expire after 1 hour)
for (const { url } of job.uploadUrls) {
await fetch(url, {
method: "PUT",
headers: { "Content-Type": "application/json" },
body: JSON.stringify([{ title: "Product", price: 9.99 }]),
});
}
// Poll job status
const status = await podge.getImportJob(job.jobId);
console.log(status.status);
// Job progresses: "created" → "streaming" → "ingesting" → "completed" | "failed"
// Track progress: status.ingestedItems, status.failedItems, status.totalItemsEnvironments
const environments = await podge.listEnvironments();
const env = await podge.createEnvironment({ name: "staging" });Document Retrieval
const doc = await podge.getDocument<Product>("products", "doc-abc123");
console.log(doc.status); // "INGESTED" | "PENDING"
console.log(doc.document?.title);Error Handling
The SDK uses Axios under the hood. API errors are thrown as AxiosError instances:
import { AxiosError } from "axios";
try {
await podge.search("products", { search: "test" });
} catch (err) {
if (err instanceof AxiosError) {
console.error(err.response?.status); // 401, 400, 404, etc.
console.error(err.response?.data); // Error details from API
}
}Common error status codes:
| Status | Meaning | |--------|---------| | 400 | Validation error — check request body/parameters | | 401 | Invalid or missing API key | | 403 | API key does not have access to this workspace/environment | | 404 | Workspace, environment, or collection not found |
Development
# Install dependencies
pnpm install
# Build
pnpm run build
# Run tests
pnpm test
# Watch mode
pnpm run dev
# Lint
pnpm run lintExamples
See the examples/ directory for runnable scripts:
basic-usage.ts— End-to-end walkthrough: create environments, collections, insert items, and searchbulk-import.ts— Import workflow with presigned uploads and job pollingsearch-with-facets.ts— Advanced search with filters, facets, sorting, and highlighting
Run any example with:
PODGE_API_KEY=your-key npx tsx examples/basic-usage.ts