Skip to main content
Related: Client, Multi-search Guide

Overview

Federate multiple labeled Relations into a single Typesense multi-search request while preserving order and mapping results back to labels.
  • Pure builder: collects labeled relations, no HTTP
  • Order preserved: results map back in insertion order
  • Unique labels: labels are case-insensitive and must be unique
  • Common params: common: is shallow-merged into each per-search payload; per-search keys win
  • No URL knobs in body: cache options are handled as URL/common params by the client

DSL

res = SearchEngine.multi_search(common: { query_by: SearchEngine.config.default_query_by }) do |m|
  m.add :books, Book.where(category_id: 5).select(:id, :name).per(10)
  m.add :publishers,   Publisher.where('name:~rud').per(5)
end

res[:books].found
res.dig(:publishers).to_a
res.labels #=> [:books, :publishers]

Label rules

  • Accepts String or Symbol
  • Canonicalization: label.to_s.downcase.to_sym
  • Must be unique (case-insensitive)

Common params merge

  • Merge precedence: per-search params override common: keys
  • URL-only keys filtered from bodies: use_cache, cache_ttl (these live in URL opts)
Example:
res = SearchEngine.multi_search(common: { q: 'milk', per_page: 50 }) do |m|
  m.add :books, Book.all.per(10) # per_page: 10 overrides common 50
  m.add :publishers,   Publisher.all           # per_page not present, inherits 50
end

Guardrails

  • Unique labels: accept String or Symbol; canonicalization is label.to_s.downcase.to_sym and labels must be unique (case-insensitive).
  • No URL-only knobs in bodies: use_cache, cache_ttl are URL opts only and are filtered from both common: and per-search bodies.
  • Actionable errors: duplicate labels, invalid relation (missing bound collection), or exceeding SearchEngine.config.multi_search_limit raise ArgumentError before any network call.
  • Per-search api_key: unsupported in Typesense multi-search; set SearchEngine.config.api_key instead.

Mapping (Relation → per-search payload)

Relation aspectPer-search key
query (q, default *)q
fields to searchquery_by
filters (AST / where)filter_by
order (order)sort_by
select (select)include_fields
pagination (page/per)page, per_page
infix (config or override)infix
Example payload shape:
{
  collection: "books",
  q: "*",
  query_by: SearchEngine.config.default_query_by,
  filter_by: "category_id:=5",
  include_fields: "id,name",
  per_page: 10
}

Compile flow

Per-search API key policy

Per-search api_key is not supported by the underlying Typesense multi-search API. Passing a non-nil api_key to m.add raises an ArgumentError. Use the global SearchEngine.config.api_key instead.

Result mapping

The helper pairs Typesense responses back to the original labels and model classes, returning a SearchEngine::Multi::ResultSet by default. For a dedicated wrapper with additional Hash-like APIs, see MultiResult below.
  • #[] / #dig(label)SearchEngine::Result
  • #labels[:label_a, :label_b, …] in insertion order
  • #to_h{ label: Result, ... }
  • #each_pair → iterate (label, result) in order

MultiResult

A lightweight, ordered wrapper over the raw multi-search list that exposes labeled Result objects and stable insertion order. Hydration uses:
  1. The model class captured alongside each label at request time
  2. Fallback to the collection registry when the raw item exposes a collection
  3. Fallback to OpenStruct otherwise
  • Accessors: #[], #dig, #labels, #keys, #to_h, #each_label
  • Label canonicalization: label.to_s.downcase.to_sym
  • Order: deterministic and identical to the order of m.add
Usage (shape matches the default helper):
mr = SearchEngine.multi_search_result { |m| m.add :books, rel1; m.add :publishers, rel2 }
mr[:books].found
mr.dig(:publishers).to_a
mr.labels # => [:books, :publishers]

URL opts, caching, and limits

  • URL-level caching knobs are passed as URL options only and never included in per-search bodies.
  • url_opts are built from config: { use_cache: SearchEngine.config.use_cache, cache_ttl: SearchEngine.config.cache_ttl_s }.
  • A hard cap on the number of searches is enforced via SearchEngine.config.multi_search_limit (default: 50). Exceeding this limit raises before any network call.

Raw response helper

If you prefer the raw response returned by the Typesense client, use:
raw = SearchEngine.multi_search_raw(common: { query_by: SearchEngine.config.default_query_by }) do |m|
  m.add :books, Book.where(category_id: 5).per(10)
  m.add :publishers,   Publisher.where('name:~rud').per(5)
end
Errors are mapped to SearchEngine::Errors and, when available, the first failing label and status are included in the error message.

Memoization & Ergonomics

  • Single roundtrip: the HTTP request is performed exactly once by Client#multi_search.
  • The raw response array is stored privately inside MultiResult and hydration into { label => Result } occurs once.
  • All accessors and helpers operate purely in-memory and never perform HTTP.
Helpers:
  • #to_h returns a shallow copy of the mapping; insertion order preserved
  • #each_label yields (label, result) in order; returns Enumerator without a block
  • #map_labels is a convenience implemented via each_label and is equally pure

See also


Observability

Observability · DX · Testing Multi-search emits a single event around the network call:
  • Event: search_engine.multi_search
  • Payload: { searches_count, labels, http_status, source: :multi }
  • Duration: available as ev.duration for subscribers
Redaction policy: payload does not include per-search bodies, q, or filter_by. Labels are considered safe. Example compact log line shape:
[se.multi] count=2 labels=products,publishers status=200 duration=12.3ms cache=true ttl=60