Search & live queries
Search and live queries: subscribe to filtered subsets of a map with useQuery; the subscription updates automatically when matching records change. For text-heavy data, layer an InvertedIndex on top for BM25-ranked full-text search. Both patterns work offline — the subscription runs against local data when the server is unreachable.
Live subscriptions
Changes push to subscribers immediately — no polling required.
Predicate filters
Simple equality, range, regex, and logical operators.
Full-text search
InvertedIndex for token matching; BM25 for relevance-ranked results.
Reactive queries (React)
Use useQuery with a where clause or predicate to subscribe to a filtered view of a map. The subscription updates automatically whenever matching records change — locally or via sync.
import { useQuery, useMutation } from '@topgunbuild/react';
interface Todo {
text: string;
completed: boolean;
createdAt: number;
}
export function ActiveTodos() {
// Subscribe to incomplete todos, sorted newest first
const { data: todos, loading, error } = useQuery<Todo>('todos', {
where: { completed: false },
sort: { createdAt: 'desc' },
limit: 50,
});
const { update } = useMutation<Todo>('todos');
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<ul>
{todos.map(todo => (
<li key={todo._key}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => update(todo._key, { completed: true })}
/>
{todo.text}
</li>
))}
</ul>
);
} Reactive queries (imperative)
Outside React, use client.query(mapName, filter) to get a QueryHandle. Subscribe to the handle and call unsubscribe() when done.
See Client API reference for the full QueryFilter and QueryHandle surfaces.
import { TopGunClient } from '@topgunbuild/client';
import { IDBAdapter } from '@topgunbuild/adapters';
const client = new TopGunClient({
serverUrl: 'ws://localhost:8080',
storage: new IDBAdapter(),
});
await client.start();
const handle = client.query('orders', {
where: { status: 'pending' },
sort: { createdAt: 'desc' },
limit: 20,
});
const unsubscribe = handle.subscribe((results) => {
console.log('Pending orders:', results.length);
// results is QueryResultItem<T>[] — each item carries _key
});
// Stop receiving updates when done
unsubscribe(); Predicates
For complex filtering — range comparisons, logical operators, regex — use the Predicates builder from @topgunbuild/client. The result is passed to the predicate field of the query filter.
import { Predicates } from '@topgunbuild/client';
import { useQuery } from '@topgunbuild/react';
interface Product {
name: string;
price: number;
category: string;
inStock: boolean;
}
export function AffordableElectronics() {
const { data: products } = useQuery<Product>('products', {
predicate: Predicates.and(
Predicates.greaterThan('price', 10),
Predicates.lessThanOrEqual('price', 200),
Predicates.equal('category', 'electronics'),
Predicates.equal('inStock', true),
),
sort: { price: 'asc' },
});
return (
<ul>
{products.map(p => (
<li key={p._key}>{p.name} — ${p.price}</li>
))}
</ul>
);
} Available predicate methods
| Method | Description | Example |
|---|---|---|
equal(attr, value) | Exact match | Predicates.equal('status', 'active') |
notEqual(attr, value) | Not equal | Predicates.notEqual('type', 'draft') |
greaterThan(attr, value) | Greater than | Predicates.greaterThan('price', 100) |
greaterThanOrEqual(attr, value) | Greater or equal | Predicates.greaterThanOrEqual('stock', 0) |
lessThan(attr, value) | Less than | Predicates.lessThan('age', 18) |
lessThanOrEqual(attr, value) | Less or equal | Predicates.lessThanOrEqual('priority', 5) |
like(attr, pattern) | SQL-like pattern (% = any, _ = single char) | Predicates.like('name', '%john%') |
regex(attr, pattern) | Regular expression | Predicates.regex('email', '^.*@gmail\\.com$') |
between(attr, from, to) | Range (inclusive) | Predicates.between('price', 10, 100) |
isIn(attr, values) | Match any value in list | Predicates.isIn('status', ['active', 'pending']) |
isNull(attr) | Field is null or missing | Predicates.isNull('deletedAt') |
isNotNull(attr) | Field exists and is not null | Predicates.isNotNull('email') |
and(...predicates) | Logical AND | Predicates.and(p1, p2, p3) |
or(...predicates) | Logical OR | Predicates.or(p1, p2) |
not(predicate) | Logical NOT | Predicates.not(p1) |
isIn not in
The list-membership predicate is Predicates.isIn() (not Predicates.in()) because `in` is a reserved JavaScript keyword.
Full-text search with InvertedIndex
For text-heavy data, add an InvertedIndex to an IndexedLWWMap. The index maps tokens to document keys, enabling O(K) search (where K is the number of matching tokens) instead of a full scan.
import {
IndexedLWWMap,
simpleAttribute,
HLC,
} from '@topgunbuild/core';
interface Article {
title: string;
body: string;
author: string;
}
const hlc = new HLC('node-1');
const articles = new IndexedLWWMap<string, Article>(hlc);
// Add an inverted index on the 'title' field
const titleAttr = simpleAttribute<Article, string>('title', a => a.title);
articles.addInvertedIndex(titleAttr);
// Index a document
articles.set('a1', {
title: 'Introduction to Machine Learning',
body: 'Machine learning is a subset of artificial intelligence.',
author: 'Alice',
});
articles.set('a2', {
title: 'Deep Learning Tutorial',
body: 'Deep learning uses many-layer neural networks.',
author: 'Bob',
});
// Token search — O(K) lookup
const results = articles.queryValues({
type: 'contains',
attribute: 'title',
value: 'learning',
});
// Returns both articles ('learning' appears in both titles)
// AND semantics: all tokens must match
const narrowed = articles.queryValues({
type: 'contains',
attribute: 'title',
value: 'machine learning', // "machine" AND "learning"
});
// Returns only a1 Query types
| Query type | Semantics | Use case |
|---|---|---|
contains | All tokens must match (AND) | Search box with multiple words |
containsAll | All specified values present | Filter by required tags |
containsAny | Any token matches (OR) | Search with alternatives |
Combining queries and search
Use predicate queries for structured filtering (equality, range) and InvertedIndex for text search. Combine them by chaining queryValues on an IndexedLWWMap or by applying predicate queries to the results of a text search.
import {
IndexedLWWMap,
simpleAttribute,
HLC,
} from '@topgunbuild/core';
import { Predicates } from '@topgunbuild/client';
import { useQuery } from '@topgunbuild/react';
interface Product {
name: string;
category: string;
price: number;
inStock: boolean;
}
// Use IndexedLWWMap for text search + getMap for reactive queries
// Approach: predicate query first (from useQuery), then apply text filter client-side
// OR: use IndexedLWWMap directly for in-memory search without server round-trip
const hlc = new HLC('node-search');
const productIndex = new IndexedLWWMap<string, Product>(hlc);
const nameAttr = simpleAttribute<Product, string>('name', p => p.name);
productIndex.addInvertedIndex(nameAttr);
function searchProducts(query: string, maxPrice: number) {
// Step 1: text search — returns products whose name contains the query tokens
const textMatches = productIndex.queryValues({
type: 'contains',
attribute: 'name',
value: query,
});
// Step 2: filter by price client-side (or use a predicate query on the server map)
return textMatches.filter(p => p.price <= maxPrice && p.inStock);
}
// In React: use useQuery for the reactive layer + run text search on the result set
export function ProductSearch() {
const [searchTerm, setSearchTerm] = React.useState('');
const { data: products } = useQuery<Product>('products', {
predicate: Predicates.and(
Predicates.equal('inStock', true),
Predicates.lessThanOrEqual('price', 500),
),
});
const filtered = React.useMemo(() => {
if (!searchTerm) return products;
const lower = searchTerm.toLowerCase();
return products.filter(p => p.name.toLowerCase().includes(lower));
}, [products, searchTerm]);
return (
<div>
<input value={searchTerm} onChange={e => setSearchTerm(e.target.value)} placeholder="Search..." />
<ul>{filtered.map(p => <li key={p._key}>{p.name} — ${p.price}</li>)}</ul>
</div>
);
} Next steps
- Schema-typed data — add compile-time types to query results
- Real-time collaboration — apply live queries to shared maps
- Client API reference — full
query()andQueryHandlesurface