Skip to main content
Related: Relation, Query DSL, DX, Debugging

Intro

Use Relation to compose safe, immutable searches and the Query DSL to express predicates. Prefer it over raw client calls for:
  • Safety: quoting, validation, and redaction are centralized
  • Immutability: AR‑style chainers return new relations without mutation
  • Debuggability: explain, to_curl, dry_run! visualize requests with zero I/O
See also: Relation and Query DSL.

Building a query

where accepts hashes, raw strings, or template fragments with placeholders. Multiple where calls compose with AND semantics. Basics:
  • Primitives: where(active: true)active:=true
  • Arrays (IN): where(brand_id: [1, 2, 3])brand_id:=[1, 2, 3]
  • Template + args: where(["price >= ?", 100]), where(["price <= ?", 200])
  • Raw escape hatch: where("price:>100 && price:<200")
Notes:
  • Field names are validated against model attributes when declared
  • Placeholders are strictly arity‑checked and safely quoted by the sanitizer
  • OR semantics require a raw fragment (e.g., “a:=1 || b:=2”) or higher‑level AST usage (see Query DSL)

Chaining

Use AR‑style chainers to add sorting, selection, and pagination. Chain order does not matter; the compiler emits a deterministic param set. Verbatim example (chaining):
SearchEngine::Book.where(price: 100..200).order(updated_at: :desc).page(2).per(20)
What it shows:
  • where(…): adds predicates (see edge‑case note on Ruby Range below)
  • where.not(…): negates predicates; special handling for array‐empty with empty_filtering: (see below)
  • order(updated_at: :desc): compiles to sort_by: “updated_at:desc”
  • page(2).per(20): compiles to page: 2, per_page: 20
Range note: Ruby Range (e.g., 100..200) is not a first‑class numeric range literal in filter_by. Prefer two comparators:
SearchEngine::Book
  .where(["price >= ?", 100])
  .where(["price <= ?", 200])
See: DX to preview compiled params. NOT-IN is rendered as NOT IN […] in explain output.

Grouping

Group by a single field and optionally control per‑group hit count and whether missing values form their own group:
rel = SearchEngine::Book.group_by(:author_id, limit: 1, missing_values: true)
rel.to_typesense_params
# => { q: "*", query_by: "title, description", group_by: "author_id", group_limit: 1,
#      group_missing_values: true }
Caveats and interactions:
  • Limits: group_limit must be a positive integer when present
  • Missing values: included only when true
  • Ordering: Group order and within‑group hit order are preserved
  • Selection: Selection applies to hydrated hits; nested includes are unaffected
  • Sorting: Sort is applied before grouping; within‑group order follows backend order
See: Grouping, Relation Guide, Field Selection. Back to top ⤴

Advanced chaining & options

Fine-tune the request with low-level controls:
  • search(q): set the query string (default ”*”).
  • limit(n) / offset(n): set strict limit/offset (alternative to page/per).
  • cache(bool): toggle use_cache for this request.
  • use_synonyms(bool): toggle use_synonyms.
  • use_stopwords(bool): toggle use_stopwords (maps to remove_stop_words).
  • options(hash): shallow-merge arbitrary options into the request (e.g. options(prioritize_exact_match: true)).
  • unscope(*parts): remove state pieces (e.g. unscope(:where, :order)).
SearchEngine::Book
  .search("potter")
  .limit(5)
  .cache(false)
  .options(prioritize_exact_match: false)

Additional chainers → params

ChainerEffectTypesense param
search(q)Set query string (default "*")q
options(opts)Shallow-merge low-level options (e.g., query_by, infix, selection.strict_missing)Various body keys
limit(n) / offset(n)Limit/offset alternative when page/per are absentper_page, page
cache(true/false/nil)Toggle URL-level caching per calluse_cache (URL)
use_synonyms(val)Enable/disable synonymsenable_synonyms
use_stopwords(val)Enable/disable stopwords (inverted)remove_stop_words
unscope(*parts)Remove relation state (e.g., :where, :order, :select, :limit, :offset, :page, :per)N/A (state cleared)
Notes:
  • use_stopwords(false) sets remove_stop_words=true; nil clears the override.
  • limit/offset map to per_page/page only when page/per are not set.
  • Prefer dedicated chainers for joins/selection/grouping/presets; options is shallow on the params hash.

Joins & presets (orientation)

  • Joins: Declare associations on the model, apply with .joins(:assoc), then filter and select using nested shapes (e.g., where(authors: { last_name: "Rowling" }), include_fields(authors: [:first_name])). See Joins and Field Selection.
  • Presets: Attach server‑side bundles of defaults and choose a mode (:merge, :only, :lock). See Presets and DX.
Keep deeper usage to those pages; this guide focuses on composition basics.

Debugging

Zero‑I/O helpers:
rel = SearchEngine::Book.where(published: true).order(updated_at: :desc).page(2).per(20)
rel.to_params_json           # redacted request body as JSON
rel.to_curl                  # single‑line, redacted curl
rel.dry_run!                 # { url:, body:, url_opts: } — no network I/O
puts rel.explain             # concise human summary
  • All outputs are redacted and stable for copy‑paste
  • dry_run! validates and returns a redacted body; no HTTP requests are made
  • Use explain to preview grouping, joins, presets/curation, conflicts, and events
See: DX, Observability. Back to top ⤴

Compiler mapping

Relation state compiles into Typesense params deterministically. High‑level mapping: See: Compiler for precedence, quoting rules, and join context.

Edge cases

  • Quoting: strings are double‑quoted; booleans true/false; nilnull; arrays are one‑level flattened and quoted element‑wise. Time inputs are normalized per field type: numeric :time/:datetime fields prefer epoch seconds; :time_string/:datetime_string use ISO8601 (see Compiler).
  • Booleans vs strings: boolean fields coerce “true”/“false”; other fields treat strings literally (see Joins).
  • Empty arrays: membership operators require non‑empty arrays. If you enable empty_filtering: true on an array attribute, the following rewrites apply:
    • .where(promotion_ids: [])promotion_ids_empty:=true
    • .where.not(promotion_ids: [])promotion_ids_empty:=false
    For joined fields, rewrite is applied only when the joined collection has empty_filtering enabled for that field (hidden $assoc.field_empty exists). Otherwise an empty array remains invalid.
  • Range endpoints: express with two comparators (see Chaining note above)
  • nil/missing: nil compiles to null; use not_eq(field, nil) or not_in(field, [nil]) to exclude nulls
  • Unicode/locale: collation/tokenization follow index settings; normalize inputs in your app if needed
  • Joined fields: require .joins(:assoc) before filtering/sorting/selection on $assoc.field (see Joins).
  • Grouping field: base fields only; joined paths like $assoc.field are rejected (see Grouping).
  • Special characters: raw fragments are passed through; prefer templates for quoting
Back to top ⤴

Selection, grouping & faceting

See Faceting for first-class faceting DSL: facet_by, facet_query, compiler mapping and result helpers.
Related links: Relation, Query DSL, DX, Joins, Field Selection, Compiler, Grouping, Observability