Public mirror for @nebutra/search from Nebutra/Nebutra-Sailor.
This repository is generated from the Nebutra Sailor monorepo. Package releases are cut from the monorepo and mirrored here for discovery, standalone cloning, and contribution intake.
- Canonical source:
packages/integrations/searchinNebutra/Nebutra-Sailor - Package registry: npm and GitHub Packages
- Contributions: open issues or PRs here; maintainers port accepted changes back into the monorepo source package
Status: Foundation — Type definitions, factory pattern, and provider stubs are complete. Provider implementations require external service credentials to activate. See inline TODOs for integration points.
Provider-agnostic full-text search package for Nebutra-Sailor. Supports Meilisearch, Typesense, and Algolia backends with the same API.
pnpm add @nebutra/searchimport { getSearch } from "@nebutra/search";
// Auto-detects provider from environment
const search = await getSearch();
// Index a document
await search.indexDocument("products", {
id: "prod_123",
name: "Widget Pro",
description: "Professional-grade widget",
price: 99.99,
tenantId: "org_456", // Optional: for multi-tenant filtering
});
// Search
const results = await search.search("products", {
query: "widget",
tenantId: "org_456", // Only search within this tenant
hitsPerPage: 20,
page: 1,
});
// Delete
await search.deleteDocument("products", "prod_123", "org_456");The factory auto-detects the correct provider based on environment variables:
| Priority | Env Variable | Provider |
|---|---|---|
| 1 | SEARCH_PROVIDER |
As specified |
| 2 | MEILISEARCH_URL |
meilisearch |
| 3 | TYPESENSE_URL |
typesense |
| 4 | ALGOLIA_APP_ID |
algolia |
| 5 | — | meilisearch (default) |
# .env
MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_API_KEY=your-api-keyconst search = await createSearch({
provider: "meilisearch",
url: "http://localhost:7700",
apiKey: process.env.MEILISEARCH_API_KEY,
});# .env
TYPESENSE_URL=http://localhost:8108
TYPESENSE_API_KEY=your-api-keyconst search = await createSearch({
provider: "typesense",
url: "http://localhost:8108",
apiKey: process.env.TYPESENSE_API_KEY,
});# .env
ALGOLIA_APP_ID=your-app-id
ALGOLIA_SEARCH_KEY=your-search-key
ALGOLIA_ADMIN_KEY=your-admin-keyconst search = await createSearch({
provider: "algolia",
appId: process.env.ALGOLIA_APP_ID,
searchKey: process.env.ALGOLIA_SEARCH_KEY,
adminKey: process.env.ALGOLIA_ADMIN_KEY,
});Index a single document (upsert semantics).
await search.indexDocument("products", {
id: "prod_123",
name: "Widget",
tenantId: "org_456",
category: "tools",
});Index multiple documents in batch (more efficient).
await search.indexDocuments("products", [
{ id: "prod_1", name: "Widget", tenantId: "org_456" },
{ id: "prod_2", name: "Gadget", tenantId: "org_456" },
]);Search with full-text query and optional filters.
const results = await search.search("products", {
query: "widget pro",
tenantId: "org_456", // Filter to tenant
filters: { category: "tools", inStock: true },
facets: ["category", "brand"],
sort: ["price:desc"],
page: 1,
hitsPerPage: 20,
highlightFields: ["name", "description"],
});Returns:
{
hits: [
{
doc: { id: "prod_123", name: "Widget Pro", ... },
score: 0.95,
highlights: { name: "<mark>Widget</mark> Pro" }
}
],
totalHits: 42,
totalPages: 3,
page: 1,
hitsPerPage: 20,
processingTimeMs: 12,
facetDistribution: { category: { tools: 25, ... } }
}Delete a single document.
await search.deleteDocument("products", "prod_123", "org_456");Delete all documents matching a filter (e.g., for tenant cleanup).
// Delete all products from a tenant
await search.deleteByFilter("products", { tenantId: "org_456" });Create or configure an index with settings.
await search.createIndex("products", {
searchableAttributes: ["name", "description", "category"],
filterableAttributes: ["category", "inStock", "tenantId"],
facetableAttributes: ["category", "brand"],
sortableAttributes: ["price", "createdAt"],
rankingRules: ["words", "typo", "proximity", "attribute"],
});Update index settings (not supported by all providers after creation).
await search.updateSettings("products", {
searchableAttributes: ["name", "description"],
});Gracefully shut down the search provider.
await search.close();All providers support tenant isolation via tenantId:
// Index with tenant
await search.indexDocument("products", {
id: "prod_123",
name: "Widget",
tenantId: "org_456", // Tenant isolation
});
// Search scoped to tenant
const results = await search.search("products", {
query: "widget",
tenantId: "org_456", // Only this tenant's docs
});
// Delete tenant data
await search.deleteByFilter("products", {
tenantId: "org_456", // Clean up on offboarding
});Under the hood:
- Meilisearch / Typesense: Separate indices per tenant (e.g.,
products__org_456) - Algolia: Facet-based filtering within shared indices
For advanced use or testing:
import { MeilisearchProvider } from "@nebutra/search/meilisearch";
import { TypesenseProvider } from "@nebutra/search/typesense";
import { AlgoliaProvider } from "@nebutra/search/algolia";
// Use directly
const search = new MeilisearchProvider();All types are exported from the main package:
import type {
SearchDocument,
SearchQuery,
SearchResult,
IndexSettings,
} from "@nebutra/search";Uses @nebutra/logger for structured logging. All operations log at debug level; errors log at error level.
# Provider selection (optional)
SEARCH_PROVIDER=meilisearch
# Meilisearch
MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_API_KEY=xyz
# Typesense
TYPESENSE_URL=http://localhost:8108
TYPESENSE_API_KEY=xyz
# Algolia
ALGOLIA_APP_ID=xyz
ALGOLIA_SEARCH_KEY=xyz
ALGOLIA_ADMIN_KEY=xyzUse the factory to swap providers in tests:
import { setSearch } from "@nebutra/search";
const mockSearch = {
name: "mock",
async search() { return { hits: [] } },
// ... other methods
};
setSearch(mockSearch);