place

@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 is a separate concern
Storage interfaces don't get a baked-in 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.

ts
// 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.

ts
// 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.

ts
// 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