Skip to content

andymerskin/spotr

Repository files navigation

Spotr logo

Spotr

A powerful fuzzy search library for client-side collections in TypeScript.

npm version bundle size CI/CD

HomepageExamples

Features

  • Zero Dependencies - Custom Levenshtein implementation, no external deps
  • Fuzzy Matching - Configurable threshold (0-1) for fuzzy string matching
  • Nested Field Support - Dot notation for deeply nested object properties
  • Keywords - Add custom filtering logic for exact keyword matches
  • Framework Integrations - React, Vue, Svelte, Solid, and Preact support
  • TypeScript - Full type definitions included

Installation

npm install spotr

CDN

You can use Spotr via a CDN:

  • unpkg: https://unpkg.com/spotr
  • jsDelivr: https://cdn.jsdelivr.net/npm/spotr
  • esm.sh: https://esm.sh/spotr

Basic Usage

import { Spotr } from 'spotr';

type Game = {
  title: string;
  genres: string[];
  releaseYear: number;
  completed: boolean;
};

const games = new Spotr<Game>({
  collection: gamesArray,
  threshold: 0.3,
  fields: [
    { name: 'title', weight: 1 },
    { name: 'releaseYear', weight: 0.8 },
  ],
  limit: 20,
});

const { results, matchedKeywords, tokens, warnings } = games.query('witcher');
// results: Array<{ item: Game, score: number }>

Options

collection (required)

Array or Set of objects to search.

collection: gamesArray; // or new Set(gamesArray)

fields (required)

Properties to search against with weight configuration. Supports dot notation for nested objects.

fields: [
  'title', // string shorthand, weight: 1
  { name: 'title', weight: 1 }, // full config
  { name: 'email', weight: 0.7 }, // lower priority
  { name: 'address.city', weight: 0.8 }, // nested field
];

keywords (optional)

Keywords that trigger specific collection filtering before fuzzy matching.

keywords: {
  mode: 'and',
  definitions: [
    {
      name: 'completed',
      triggers: ['done', 'complete', 'finished'],
      handler: (collection) => collection.filter(item => item.completed),
    },
    {
      name: 'platform',
      triggers: ['ps4', 'ps5', 'xbox', 'pc', 'switch'],
      handler: (collection, matchedTerms) =>
        collection.filter(item =>
          matchedTerms.some(term =>
            item.platforms.some(p => p.toLowerCase().includes(term))
          )
        ),
    },
  ],
}

threshold (optional)

Global fuzzy matching threshold. Default: 0.3

  • 0 = match anything (no filtering)
  • 0.3 = recommended default
  • 1 = exact match required

limit (optional)

Maximum number of results to return. Default: Infinity

caseSensitive (optional)

Enable case-sensitive matching. Default: false

minMatchCharLength (optional)

Minimum query length to trigger matching. Default: 1

maxStringLength (optional)

Maximum string length limit for both search query tokens and collection field values before truncation. Default: 1000

  • Search query tokens (search terms) exceeding this limit are truncated
  • Collection field values exceeding this limit are truncated
  • Warnings are added to result.warnings when truncation occurs
  • Used for performance optimization to prevent slowdowns with very long strings in fuzzy matching

Result Type

interface SpotrResult<T> {
  results: ScoredResult<T>[]; // Array of { item, score }
  matchedKeywords: MatchedKeyword[]; // Keywords that matched
  tokens: string[]; // Non-keyword search terms
  warnings: string[]; // Warnings (e.g., missing nested paths)
}

React Hook

import { useSpotr } from 'spotr/react';

function GameSearch({ games, searchQuery }) {
  const spotr = useSpotr({
    collection: games,
    fields: [{ name: 'title', weight: 1 }],
  });

  const { results } = useMemo(
    () => spotr.query(searchQuery),
    [spotr, searchQuery]
  );

  return <ResultsTable results={results} />;
}

Vue Composable

import { useSpotr } from 'spotr/vue';

const games = ref<Game[]>([]);
const query = ref('');

const spotr = useSpotr(() => ({
  collection: games.value,
  fields: [{ name: 'title', weight: 1 }],
}));

const results = computed(() => spotr.value?.query(query.value));

Svelte Store

import { writable, derived } from 'svelte/store';
import { createSpotr } from 'spotr/svelte';

const spotr = createSpotr({
  collection: games,
  fields: [{ name: 'title', weight: 1 }],
});

const query = writable('');
const results = derived([spotr, query], ([$spotr, $query]) =>
  $spotr.query($query)
);
// Use $query and $results in your Svelte template

Solid Hook

import { createSignal, createMemo } from 'solid-js';
import { createSpotr } from 'spotr/solid';

const spotr = createSpotr({
  collection: games,
  fields: [{ name: 'title', weight: 1 }],
});

const [query, setQuery] = createSignal('');
const results = createMemo(() => spotr().query(query()));
// spotr is a getter; use spotr().query(query()) in a memo for reactive results

Preact Hook

import { useSpotr } from 'spotr/preact';

function GameSearch({ games, searchQuery }) {
  const spotr = useSpotr({
    collection: games,
    fields: [{ name: 'title', weight: 1 }],
  });

  const { results } = useMemo(
    () => spotr.query(searchQuery),
    [spotr, searchQuery]
  );

  return <ResultsTable results={results} />;
}

API Methods

query(search: string): SpotrResult<T>

Search the collection with a string query.

queryAsync(search: string): Promise<SpotrResult<T>>

Async search with optional debouncing.

setCollection(collection: T[] | Set<T>): void

Update the collection.

collection: T[] (getter)

Access the current collection.

Development

This project uses Bun as the package manager. After cloning the repository, install dependencies with:

bun install

Core Scripts

  • bun run build - Build the library (outputs to dist/)
  • bun run test - Run all tests once
  • bun run test:watch - Run tests in watch mode
  • bun run test:coverage - Run tests with coverage report
  • bun run test:coverage:watch - Run tests with coverage in watch mode
  • bun run typecheck - Type check the library (tsc --noEmit)
  • bun run audit - Audit spotr, all examples, root, and site for vulnerabilities and apply fixes (scripts/audit-all.ts)
  • bun run lint - Lint the codebase (ESLint)
  • bun run format - Format the codebase (Prettier, whole repo from root)
  • bun run format:check - Check formatting without modifying
  • bun run validate - Full validation (format check, lint, typecheck, test coverage, examples typecheck, and build)
  • bun run size:check - Verify bundle size under 5KB
  • bun run bench - Run Vitest benchmarks
  • bun run clean - Remove dist and coverage directories

Example Scripts

  • bun run examples:typecheck - Type check all example applications
  • bun run examples:sync - Sync shared files from examples/shared/ to all examples
  • bun run examples:install - Install dependencies for all examples
  • bun run examples:update - Update dependencies for all examples
  • bun run examples:dev - Launch interactive dev servers for a selected framework (starts all 5 examples on ports 5173-5177)
  • bun run examples:warm - Warm up StackBlitz cache by visiting all 25 example pages (used in CI after deployment)

Setup Scripts

  • bun run prepare - Setup Husky git hooks (runs automatically on install)

Releasing

To release a new version to npm, use the release script:

bun run release

The script automates the release process: it runs validate (format check, lint, typecheck, test coverage, examples typecheck, and build) and a bundle size check (enforcing a 5KB gzipped limit) before proceeding with version bumping, commit amending, and publishing.

Note: Due to a known npm bug with workspaces, npm version may not automatically commit and tag changes when the package.json is in a subdirectory. The release script automatically detects this and creates the commit and tag manually if needed.

You can also pass a version bump type directly:

bun run release patch    # for patch releases
bun run release minor    # for minor releases
bun run release major    # for major releases
bun run release prerelease    # increment existing pre-release
bun run release premajor --preid beta    # start major pre-release with beta tag
bun run release preminor --preid=rc    # start minor pre-release with rc tag

See CONTRIBUTING.md for the complete release process and manual release steps.

License

MIT

About

A powerful fuzzy search library for client-side collections

Topics

Resources

License

Contributing

Security policy

Stars

4 stars

Watchers

0 watching

Forks

Contributors

Languages