@place/search
Reactive search over @place/reactivity collections. v0.1 ships exactly one primitive — searchable(). It takes a reactive list plus a field extractor and returns a function that, given a reactive query, yields a reactive filtered list. Substring match, case-insensitive, AND of whitespace-separated tokens.
search method — that would lock every future store into re-implementing the same filter. searchable() stays a standalone primitive that works over any reactive list. Ranking, fuzzy match, and inverted indexes are deferred until a real workload demands them.searchable(items, options)
items is a getter for the reactive list; options.fields returns the strings to search within for one item. The call returns (query: () => string) => () => readonly T[] — pass the query getter, get back a getter that recomputes when either the items or the query change.
// searchable(items, options) — reactive search over a reactive
// collection. Substring match, case-insensitive, AND-of-tokens.
import { state } from '@place/reactivity'
import { searchable } from '@place/search'
interface Note { title: string; content: string; tags: string[] }
const notes = state<Note[]>([])
const query = state('')
// searchable() returns a function that takes a query getter and
// yields a getter for the filtered list. Both are reactive: the
// result recomputes when the items OR the query change.
const filtered = searchable(
() => notes(),
{ fields: (n) => [n.title, n.content, ...n.tags] },
)(() => query())
filtered() // → readonly Note[], reactive on items + query
Tokenization
The query is split on whitespace; an item matches when every non-empty token appears in some field (substring match). An empty query returns the unfiltered list. Pass caseSensitive: true to require exact case.
// Tokenization: the query is split on whitespace; an item matches
// when EVERY non-empty token appears in some field (substring).
// An empty query returns the unfiltered list.
query.set('rust async')
// matches items where some field contains 'rust' AND some field
// contains 'async' — order-independent, case-insensitive.
// Case-sensitive match — opt in:
searchable(items, { fields: (n) => [n.title], caseSensitive: true })
Searching a collection
searchable() composes with @place/data — pass the collection's all() as the reactive items source.
// Composes with @place/data — pass the collection's all() as the
// reactive items source.
import { collection } from '@place/data'
import { searchable } from '@place/search'
const c = collection<Note>(noteState)
const results = searchable(
() => c.all(),
{ fields: (n) => [n.title, n.content] },
)(() => query())
// Render — recomputes when notes change or the query changes:
<ul>{() => results().map((n) => <li>{n.title}</li>)}</ul>
See also
- @place/data —
collection() - state · watch · derived