Per-query tuning pipeline

Copy Markdown View Source

This document is the canonical request-time tuning pipeline specification for Scrypath’s Meilisearch-first search path. It describes how search-time options flow from configuration through validation to Meilisearch, how that relates to index-time settings, and what operators and implementers should rely on for stable contracts versus illustrative prose.

Scope and non-goals

In scope: Plane B (search request) semantics, merge precedence, pipeline stages, mapping categories for Meilisearch search parameters, error and telemetry expectations at a normative level, federation pointers, and recipes that stay thin relative to dedicated guides.

Non-goals:

  • Replacing index-time documentation. Synonym sets, default ranking rule order, typo policies, and other Plane A concerns remain in guides/relevance-tuning.md and the managed reindex lifecycle. This guide links there instead of duplicating index settings narrative.
  • Teaching Meilisearch ranking theory. Prefer links to vendor reference and release notes over copying vendor prose blocks.
  • Promising a public multi-backend abstraction. Scrypath is Meilisearch-first here; an internal adapter seam exists but is not a semver-stable portability layer for adopters.
  • Embedding secrets in examples. Transport configuration and API keys belong in operator docs and application configuration, not copy-pasted into search-option examples.
  • Normative stability for human strings. Exception.message/1, raw HTTP bodies, log lines, and NimbleOptions copy may change in patch releases unless explicitly documented otherwise.

Two-plane model and precedence

Plane A — index settings: Declared on use Scrypath in settings:, translated to Meilisearch index configuration, applied through managed reindex and drift-aware tooling. The declared schema is the source of intent for what the index should look like after a successful apply.

Plane B — search request: The JSON (or equivalent) payload for POST …/indexes/{uid}/search and each query object inside /multi-search. Plane B parameters tune a single search (filters, sort, facets retrieval, pagination, ranking-score knobs, and similar) without re-resolving the full Plane A map on every call. Operational drift between declared Plane A and the live server is an operator visibility problem (diff tasks, reindex), not something Plane B silently “fixes” on each request.

Stack for Plane B keywords (weakest → strongest):

  1. Meilisearch defaults for omitted search fields (vendor baseline).
  2. Live index settings on the server (operational truth for attributes, embedders, etc.).
  3. Scrypath allowlisted per-request options after validation and projection (what the library forwards this release).
  4. Per-entry tuple keywords in search_many/2 winning over shared keywords for duplicate top-level keys (see Scrypath.MultiSearch.Entries — entry side wins in Keyword.merge/3 with a conflict function favoring the entry).
  5. Optional :per_query tuning map (search-time, carried on %Scrypath.Query{} when used) — same right-bias story as other tuning maps; nested map merge defaults align with Plane A :settings_merge semantics unless this guide or guides/relevance-tuning.md explicitly documents an exception.

Configuration cascade for runtime (repo, URL, backend) — Scrypath.Config.resolve!/1:

Application.get_env(:scrypath, :defaults, []) is merged with per-repo Application.get_env(otp_app, repo)[:scrypath] (when :repo / :otp_app resolution succeeds), then merged again with explicit per-call keywords; explicit call-time keys win. That stack addresses transport and library runtime, not the full Plane A settings: map.

Nested maps: Default shallow replace for duplicate keys at each level; :deep (where the pipeline exposes it) is opt-in only, matching the index-time settings-merge posture in guides/relevance-tuning.md (avoid accidental deep-merge clobber of large maps).

Pipeline stages

Ordered stages describe responsibility boundaries; exact function names may evolve, but the ordering is normative for reasoning and docs:

  1. Normalize and validate — Canonical keys, allowlists, and {:invalid_options, _} / {:validation, _} style failures before any HTTP work. Unknown keys should not silently become wire noise unless an explicitly labeled escape hatch documents semver coverage.
  2. Merge — Apply the Plane B precedence rules above (including search_many/2 shared vs per-entry merge and shared-only federation rail keys).
  3. Project — Translate Scrypath option keywords into Meilisearch camelCase request fields the adapter sends.
  4. Dispatch — Backend search/3 or native search_many/2 (federation) with configured timeouts.
  5. Decode and hydrate — Parse engine responses, optional record hydration, facet maps, and ranking score fields when requested.
  6. Surface result or error — Tagged tuples for domain failures; {:ok, _} for success including partial federation success with per-schema failures listed.

Meilisearch mapping and version stance

Principle-based categories (normative framing):

CategoryMeaning
Pass-through search parametersFields Meilisearch documents on Search / Multi-search query objects that the adapter forwards without semantic rewrite, subject to allowlisting and naming translation.
Index prerequisitesPlane A constraints (filterable, sortable, displayed, embedder availability) required before a Plane B knob becomes meaningful. Document as a short matrix with links to vendor reference rather than copying vendor tables.
Explicitly index-boundSynonym sets, ranking rule order, default typo policy, and similar — remain Plane A; per-query docs explain why and point to guides/relevance-tuning.md.

Shipped search-time ranking exemplars (Plane B; normative here, implemented in Scrypath.search/3, search_many/2, and %Scrypath.Query{} / :per_query):

  • rankingScoreThreshold — May change which hits appear, and can interact with hit counts, estimated totals, facet distributions, and pagination semantics. Operators should read vendor guidance on threshold behavior and performance.
  • showRankingScore — Surfaces ranking-related score fields in the response when the engine supports it.
  • showRankingScoreDetails — Treat as debug / tuning only: richer response shape and higher cost; enable deliberately in non-production or bounded dashboards.

Also anchor existing first-class options Scrypath already models (filter, sort, facets, pagination, retrieve / highlight / crop, etc.) as participating in the same validate → merge → project → dispatch story.

Deferred (documented rationale):

  • Vector / hybrid / personalization / enterprise-only knobs unless a future documented public release expands scope — keep behind any documented escape hatch with clear semver expectations.
  • Per-query mutation of synonyms or full rankingRules replacement — deferred to index lifecycle (Plane A) to avoid split-brain between declared schema and live search.
  • Full federated product tutorial — deferred to guides/multi-index-search.md except for compatibility notes in this guide.

Minimum Meilisearch version: Scrypath’s CI and support statement pin a floor Meilisearch version (see project README.md / release docs). Features such as rankingScoreThreshold and related score fields require a server at or above the vendor version that introduced them — verify against Meilisearch release notes and Search API reference before enabling in production.

Error taxonomy

Stable contracts are tags and tuple shapes, not human strings.

LayerExamplesAdopter guidance
Options validation{:error, {:validation, message}} (NimbleOptions wrapper), {:error, {:invalid_options, _}} familyMatch on the discriminant after {:error, …}; do not assert on full message text for control flow.
Query shape / domain:unknown_facet, facet bucket conflicts, {:validation_failed, schema, reason} (multi-search preflight)Pattern match tags; semver treats these shapes as API.
HTTP / transportTimeouts, connection errors, non-success HTTP classified by the backendOften wrapped or annotated by the adapter; treat as operational incidents with retry policy outside the library.
Engine semantics4xx/422 from Meilisearch for impossible requestsSurface as {:error, _} shapes the backend documents; do not rely on raw body text in tests.

Telemetry catalog

Telemetry is a public observability contract for event names and documented metadata keys. Additive metadata in minor releases is acceptable; renaming or removing events or documented keys is breaking.

EventSpan / executeDocumented metadata (non-exhaustive)When
[:scrypath, :search]SpanModule, config-derived fields via Telemetry.common_metadata/3, optional search_scope, scoped_facet for facet-scoped searchesAround single-index search/3 work
[:scrypath, :search_many]Spanschema_count, raw_entry_count, stop metadata from resultsAround multi-search orchestration
[:scrypath, :search_many, :partial]ExecutePer-schema failure summaries where applicableWhen some indexes fail but the API returns partial success

Refer to lib/scrypath/search.ex for the authoritative emit points as the code evolves.

Federation and search_many

Scrypath.search_many/2 merges shared and per-entry options with per-entry winning on duplicate keys. Federation rail keys (:federation_limit, :federation_offset, :hydration_timeout, :federation_timeout, :max_schemas, etc.) remain shared-only and are rejected on entry tuples.

Federated payloads differ from independent multi-search (weights, merge ordering, facetsByIndex, global pagination). Canonical narrative: guides/multi-index-search.md. This pipeline spec does not duplicate that guide; it only states that Plane B tuning keys must remain compatible with the per-entry vs shared merge story unless a future decision explicitly introduces a shared-only exception list.

Recipes (Phoenix and LiveView)

  • Controller / context: Build a single keyword list per request (params → allowed keys only), merge over repo defaults, pass to Scrypath.search/3. Keep ranking-score debug options behind feature flags.
  • LiveView: Treat search assigns as request-scoped Plane B state; reload index-time Plane A through reindex workflows when operators change schema settings.
  • Dashboards: For search_many/2, render per-schema sections from declaration order; never assume merged facets across indexes unless the engine and guide explicitly describe that behavior.

Implementation readiness checklist

Use this checklist before extending Plane B per-query search-time options or changing merge/projection behavior. Every item should be satisfied in writing and in code review:

  • [ ] Plane A vs Plane B is documented for the team; no plan relies on “schema-as-runtime-truth on every search.”
  • [ ] Right-biased merge is implemented for all new tuning maps, including search_many/2 entry vs shared behavior.
  • [ ] Nested map default is shallow replace with :deep opt-in only, consistent with existing settings-merge semantics.
  • [ ] Allowlist posture is enforced for library-owned keys; any escape hatch is explicitly labeled and semver-scoped.
  • [ ] Meilisearch version floor is verified for rankingScoreThreshold / score field features used in production.
  • [ ] Error taxonomy tests match on tuple tags, not exception strings or HTTP bodies.
  • [ ] Telemetry events above are emitted with documented metadata keys and are covered in changelog when changed.
  • [ ] Multi-search compatibility is covered without duplicating guides/multi-index-search.md — links and merge rules stay single-sourced.
  • [ ] Operator honesty — drift, threshold effects on counts/facets, and partial federation are visible in UX copy or operator runbooks, not only internal comments.

When all boxes are checked, changes to the per-query runtime may proceed with changelog and contract-test coverage as usual.