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

searchcraft

v0.1.1

Published

Full-text search for Next.js. No external service.

Readme

searchcraft

Full-text search for Next.js. No external service.

Sifter is a zero-dependency, in-process full-text search library built for Next.js applications. It uses TF-IDF with BM25 scoring, supports fuzzy matching, and ships with React components and a Next.js Route Handler out of the box.

Install

npm install searchcraft

Quick Start

import { createSifter } from "searchcraft";

const sifter = createSifter({
  schema: {
    title: { weight: 2 },
    body: true,
    tags: true,
  },
  documents: [
    { title: "Getting Started", body: "Welcome to the docs.", tags: ["intro"] },
    { title: "API Reference", body: "Full API documentation.", tags: ["api"] },
    { title: "Deployment Guide", body: "Deploy to production.", tags: ["ops"] },
  ],
});

const results = sifter.search("api documentation");
// [{ item: { title: "API Reference", ... }, score: 1.234, matches: [...] }]

Schema Definition

A schema tells Sifter which fields to index and how to weight them.

const schema = {
  // Full form: configure weight and searchability
  title: { weight: 3, searchable: true },

  // Shorthand: `true` means searchable with default weight (1)
  body: true,

  // Not searchable (won't be indexed)
  id: false,

  // Custom weight, default searchable
  tags: { weight: 1.5 },
};

| Option | Type | Default | Description | | ------------ | ------- | ------- | ------------------------------------ | | weight | number | 1 | Relative importance for scoring | | searchable | boolean | true | Whether the field is indexed |

Search API

Basic Search

const results = sifter.search("deploy production");

All query terms use AND semantics -- every term must appear in a document for it to match.

Fuzzy Search

const results = sifter.search("deploymnt", { fuzzy: true });
// Matches "deployment" (edit distance <= 2)

Options

sifter.search("query", {
  limit: 20,       // Max results (default: 10)
  offset: 0,       // Skip N results for pagination
  fuzzy: true,     // Levenshtein distance <= 2
  threshold: 0.5,  // Minimum score to include
});

Mutating the Index

// Add a document
sifter.add({ title: "New Page", body: "Content here." });

// Remove documents matching a predicate
sifter.remove((doc) => doc.title === "Old Page");

// Force rebuild (e.g., after bulk mutations)
sifter.rebuild();

// Check document count
console.log(sifter.size);

Search Result Shape

interface SearchResult<T> {
  item: T;              // The original document
  score: number;        // BM25 relevance score
  matches: MatchInfo[]; // Where terms matched
}

interface MatchInfo {
  field: string;                 // Which field matched
  positions: [number, number][]; // Token positions [start, end]
}

React Components

import { SifterProvider, SearchBox, SearchResults, useSearch, useSifter } from "searchcraft/react";

SifterProvider

Wrap your search UI in a provider:

import { createSifter } from "searchcraft";
import { SifterProvider, SearchBox, SearchResults } from "searchcraft/react";

const sifter = createSifter({ schema, documents });

function App() {
  return (
    <SifterProvider sifter={sifter}>
      <SearchBox placeholder="Search docs..." debounce={300} />
      <SearchResults renderItem={(result, i) => (
        <div key={i}>
          <h3>{result.item.title}</h3>
          <p>Score: {result.score.toFixed(2)}</p>
        </div>
      )} />
    </SifterProvider>
  );
}

SearchBox Props

| Prop | Type | Default | Description | | --------------- | ----------------------------------------- | ------------- | ------------------------------ | | placeholder | string | "Search..." | Input placeholder text | | onResults | (results: SearchResult[]) => void | -- | Callback when results change | | debounce | number | 200 | Debounce delay in ms | | searchOptions | SearchOptions | -- | Options passed to each query | | className | string | -- | CSS class for the input |

Hooks

function MyComponent() {
  // Access the sifter instance directly
  const sifter = useSifter();

  // Search with state management
  const { results, isSearching, search } = useSearch("initial query");

  return (
    <button onClick={() => search("new query")}>
      Search ({results.length} results)
    </button>
  );
}

Next.js API Route

Create a search endpoint with zero boilerplate:

// app/api/search/route.ts
import { createSearchHandler } from "searchcraft/next";
import { sifter } from "@/lib/search";

export const GET = createSearchHandler(sifter);

Query parameters:

| Param | Type | Default | Description | | ----------- | ------ | ------- | ------------------------ | | q | string | -- | Search query (required) | | limit | number | 10 | Max results | | offset | number | 0 | Skip N results | | fuzzy | string | -- | "true" or "1" | | threshold | number | 0 | Minimum score |

Example request:

GET /api/search?q=deploy+guide&limit=5&fuzzy=true

Response:

{
  "results": [
    {
      "item": { "title": "Deployment Guide", "body": "Deploy to production." },
      "score": 1.847,
      "matches": [{ "field": "title", "positions": [[0, 1]] }]
    }
  ],
  "query": "deploy guide",
  "total": 1
}

Performance

Sifter builds an in-memory inverted index. Guidance for index sizes:

| Documents | Fields | Approx. Memory | Index Build Time | | --------- | ------ | --------------- | ---------------- | | 1,000 | 3 | ~2 MB | ~50ms | | 10,000 | 3 | ~20 MB | ~500ms | | 100,000 | 3 | ~200 MB | ~5s |

For datasets beyond 100k documents, consider a dedicated search service. Sifter is designed for content sites, documentation, product catalogs, and similar use cases where the full dataset fits comfortably in memory.

License

MIT