npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@nebutra/search

v0.1.1

Published

> **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.

Readme

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.

@nebutra/search

Provider-agnostic full-text search package for Nebutra-Sailor. Supports Meilisearch, Typesense, and Algolia backends with the same API.

Quick Start

Installation

pnpm add @nebutra/search

Basic Usage

import { 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");

Provider Auto-Detection

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) |

Meilisearch Setup

# .env
MEILISEARCH_URL=http://localhost:7700
MEILISEARCH_API_KEY=your-api-key
const search = await createSearch({
  provider: "meilisearch",
  url: "http://localhost:7700",
  apiKey: process.env.MEILISEARCH_API_KEY,
});

Typesense Setup

# .env
TYPESENSE_URL=http://localhost:8108
TYPESENSE_API_KEY=your-api-key
const search = await createSearch({
  provider: "typesense",
  url: "http://localhost:8108",
  apiKey: process.env.TYPESENSE_API_KEY,
});

Algolia Setup

# .env
ALGOLIA_APP_ID=your-app-id
ALGOLIA_SEARCH_KEY=your-search-key
ALGOLIA_ADMIN_KEY=your-admin-key
const search = await createSearch({
  provider: "algolia",
  appId: process.env.ALGOLIA_APP_ID,
  searchKey: process.env.ALGOLIA_SEARCH_KEY,
  adminKey: process.env.ALGOLIA_ADMIN_KEY,
});

API Reference

indexDocument(index, doc)

Index a single document (upsert semantics).

await search.indexDocument("products", {
  id: "prod_123",
  name: "Widget",
  tenantId: "org_456",
  category: "tools",
});

indexDocuments(index, docs)

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(index, query)

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, ... } }
}

deleteDocument(index, docId, tenantId?)

Delete a single document.

await search.deleteDocument("products", "prod_123", "org_456");

deleteByFilter(index, filters)

Delete all documents matching a filter (e.g., for tenant cleanup).

// Delete all products from a tenant
await search.deleteByFilter("products", { tenantId: "org_456" });

createIndex(index, settings)

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"],
});

updateSettings(index, settings)

Update index settings (not supported by all providers after creation).

await search.updateSettings("products", {
  searchableAttributes: ["name", "description"],
});

close()

Gracefully shut down the search provider.

await search.close();

Multi-Tenancy

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

Direct Provider Imports

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();

Type Safety

All types are exported from the main package:

import type {
  SearchDocument,
  SearchQuery,
  SearchResult,
  IndexSettings,
} from "@nebutra/search";

Logging

Uses @nebutra/logger for structured logging. All operations log at debug level; errors log at error level.

Environment Variables

# 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=xyz

Testing

Use the factory to swap providers in tests:

import { setSearch } from "@nebutra/search";

const mockSearch = {
  name: "mock",
  async search() { return { hits: [] } },
  // ... other methods
};

setSearch(mockSearch);