Standards Docs
Concepts

Search

Typed full-text search across records via a mandatory SearchAdapter (Meilisearch by default).

standards exposes typed search across records through a SearchAdapter composed onto the DatabaseAdapter. The adapter is mandatory — without one, bootstrap throws SearchAdapterRequiredError.

// @noverify
import { useSearchRecords } from "@stndrds/react";

function ContactSearch({ q }: { q: string }) {
  const { data, isLoading } = useSearchRecords("contact", q);

  if (isLoading) return null;
  return (
    <ul>
      {data?.data.map((r) => (
        <li key={r.id}>{String(r.values.firstName)}</li>
      ))}
    </ul>
  );
}

useSearchRecords(objectName, query, options?) accepts an optional options object with limit, offset, sorts, filters, minQueryLength (default 1), and a select transformer.

How it works

When MeilisearchSearchAdapter is configured on the backend, records sync to Meili on create, update, and delete. Search queries hit Meili and return results ranked by relevance. The adapter is required at construction time — the runtime no longer ships a Postgres ILIKE fallback path, and bootstrap will throw SearchAdapterRequiredError if no SearchAdapter is composed.

For paginated search with infinite scroll, use useInfiniteSearchRecords(objectName, query, options?). It accepts the same options plus pageSize (default 20) and exposes fetchNextPage / hasNextPage from TanStack Query.

Filter rules

FilterRule is the typed query shape used to constrain records:

// @noverify
import type { FilterRule } from "@stndrds/schema";

// Equality
const byStage: FilterRule = { attribute: "stage", operator: "eq", value: "qualified" };

// Range
const recentDeals: FilterRule = { attribute: "createdAt", operator: "gte", value: "2025-01-01" };

// List membership
const multiStatus: FilterRule = { attribute: "status", operator: "in", value: ["open", "pending"] };

Pass a FilterState (combinator + rules array) to useSearchRecords via the filters option. For adapter-level selection and how the backend chooses between Meili and Postgres, see adapters.

Edge-property filters

You can filter records by a property on a qualified edge — for example, "contacts whose relationship to this company has role = "primary"". Set property on the FilterRule alongside the reference attribute:

// @noverify
import type { FilterRule } from "@stndrds/schema";

const primaryContacts: FilterRule = {
  attribute: "company",  // relation attribute with .qualifyWith()
  property: "role",
  operator: "eq",
  value: "primary",
};

Edge-property filters are resolved inside the search adapter. The Meilisearch adapter projects edge properties into the indexed document (under edge_properties.*) at write time, so rule.property predicates evaluate directly against the index — no separate ID lookup. See relations for how qualified edges are declared with .qualifyWith().

Breaking in vNEXT: DatabaseAdapter.search is now mandatory. The legacy ILIKE fallback has been removed, and SearchUnavailableError no longer exists — bootstrap throws SearchAdapterRequiredError instead. Compose MeilisearchSearchAdapter (or another SearchAdapter) at adapter construction time.

On this page