Public API¶
Twenty classes. Everything else in src/ is @internal and may move in a
patch release — see what counts as public for
why the line is drawn there.
ApiBoundaryTest fails the build if this list and the annotations ever
disagree, so what follows cannot quietly drift from the code.
Entry point¶
Formatter¶
static create(?Options $options = null): Formatter
options(): Options
describe(array|string $request, ?string $index = null): Digest
explain(array|string $request, ?string $index = null): Explanation
lazy(array|string $request, ?string $index = null): LazyDigest
$request is a search body, an ['index' => …, 'body' => …] envelope as
produced by opensearch-php, or the JSON string of either. $index overrides
whatever the envelope carried.
describe() does the work immediately. lazy() defers all of it until
something reads the result — use it when the record may be filtered out.
explain() returns the digest plus every normalisation rule that fired.
Throws
InvalidQueryException when $request is neither an array nor decodable
JSON. A body it cannot understand does not throw — it renders as
type(?).
Results¶
Digest¶
index(): string
text(): string
signature(): string
hash(): string
notes(): array
toArray(): array
Implements JsonSerializable and __toString() (which returns text()).
toArray() gives the compact {idx, q, sig, hash, notes} object — without q
under Options::withText(false), where text() returns the signature because
there is no literal line to return.
LazyDigest¶
digest(): Digest
Implements JsonSerializable and __toString(). Nothing is parsed until one of
those is called; both memoise, so reading it twice costs once.
Explain\Explanation¶
digest(): Digest
rules(): Rule[]
has(string $rule): bool
ruleIds(): string[]
toArray(): array
has() takes a Rule:: constant. Casting to string gives the human-readable
table.
Explain\Rule¶
id(): string
count(): int
details(): string[]
description(): string
One rule that actually fired, with how many times and on what. A rule that had
nothing to do is never reported. The 22 Rule:: constants name every rule —
MUST_FILTER_MERGED, BOOST_DROPPED, TERMS_LOOKUP, EXTENSION_RENDERED and
the rest.
Configuration¶
Options¶
Immutable; every with*() returns a copy.
static create(): Options
static fromArray(array $spec): Options
withNormalization(Normalization $normalization): Options
withMaxClauses(?int $maxClauses): Options
withMaxValues(?int $maxValues): Options
withMaxLength(?int $maxLength): Options
withIndexNormalizer(IndexNormalizer $indexNormalizer): Options
withRedactor(?callable $redactor): Options
withAggNames(bool $includeAggNames): Options
withText(bool $emitText): Options
withHashLength(int $hashLength): Options
withHashVersion(string $hashVersion): Options
withClauseRenderer(string $type, ClauseRenderer $renderer): Options
Each has a matching getter. fromArray() accepts the nine keys in
Options::KEYS and throws InvalidOptionException on an unknown key or a wrong
type — the redactor and clause renderers have no array form, being callables and
objects.
The redactor is called as fn(string $field, mixed $value): mixed before a
value is rendered.
withText(false) drops the readable line: it is never rendered, toArray()
emits idx / sig / hash, and text() returns the signature. See
when the values may not leave the building.
See Options for what each one does to the output.
Normalization¶
static none(): Normalization
static values(): Normalization // the default
static structural(): Normalization
static fromLevel(string $level): Normalization
level(): string
erasesValues(): bool
erasesCardinality(): bool
erasesPagination(): bool
Constants: NONE, VALUES, STRUCTURAL.
IndexNormalizer¶
static datePatterns(): IndexNormalizer // the default
static identity(): IndexNormalizer
static custom(callable $rewrite): IndexNormalizer
static fromMode(string $mode): IndexNormalizer
normalize(string $index): string
Constants: DATE_PATTERNS, IDENTITY. datePatterns() collapses
logs-2026.08.13 to logs-*, so a daily index does not mint a new fingerprint
every midnight.
custom() takes fn(string $index): string, called once per name in a
comma-separated list. Your rule runs first, then datePatterns(). It has no
array form and is not a MODES entry — a callable cannot come out of a
configuration file, the same line withRedactor() sits on. See
an index name only you can read.
Extension¶
Extension\ClauseRenderer¶
render(array $body): ?RenderedClause
Implement it to teach the library a query type it leaves opaque, and register it
with Options::withClauseRenderer(). Return null for a body you do not
recognise — the clause then stays type(?), which is true, where a guess would
be a fingerprint built on a misreading.
A renderer is only ever consulted for a type the library does not model natively. See teaching it a query type.
Extension\RenderedClause¶
static on(string $field, string $label): RenderedClause
static fieldless(string $label): RenderedClause
withParam(string $name, scalar|null $value): RenderedClause
field(): string
label(): string
params(): array
Immutable. Parameter names survive into the signature, their values do not — the
same rule every other clause follows. A numeric parameter name throws
InvalidOptionException.
Failures¶
Exception\InvalidQueryException¶
Thrown by describe(), explain() and lazy() when the input is neither an
array nor decodable JSON. Extends InvalidArgumentException.
Exception\InvalidOptionException¶
Thrown by Options::fromArray() on an unknown key or a wrong type, by
Normalization::fromLevel() and IndexNormalizer::fromMode() on an unknown
name, and by RenderedClause::withParam() on a numeric name.
Transport¶
See Capture at the transport for which of the three to use and why — the answer depends on what your client transports over, not on its name.
Http\DigestingClient¶
__construct(ClientInterface $inner, SearchObserver $observer, ?Formatter $formatter = null, string $basePath = '')
sendRequest(RequestInterface $request): ResponseInterface
A PSR-18 client that digests every _search and _msearch passing through it.
Anything else passes straight through. $basePath is the path prefix the cluster
is mounted under, if it is behind a proxy — without it that prefix is read as the
index name.
Http\Guzzle\DigestMiddleware¶
__construct(SearchObserver $observer, ?Formatter $formatter = null, string $basePath = '')
__invoke(callable $handler): callable
The same capture as a Guzzle middleware — $stack->push(new
DigestMiddleware($observer)). Needed for the requests a PSR-18 decorator cannot
see: asynchronous and pooled ones.
Http\Ring\DigestingHandler¶
__construct(callable $next, SearchObserver $observer, ?Formatter $formatter = null, string $basePath = '')
__invoke(array $request): mixed
The same capture as a ezimuel/ringphp handler — $builder->setHandler(new
DigestingHandler(ClientBuilder::defaultHandler(), $observer)). Needed for the
clients that transport over ringphp, which predates PSR-7 and so has neither a
sendRequest() to decorate nor a handler stack to push onto:
elasticsearch-php 7.x and opensearch-php ≤ 2.3.
Whatever the wrapped handler returns is passed back — an array untouched, a future proxied, so an asynchronous request stays asynchronous.
Http\SearchObserver¶
observe(ObservedSearch $search): void
What to do with a search once it has been seen. It may throw: all three integrations catch everything an observer does, because a digest is never worth a failed request.
Http\ObservedSearch¶
digest(): LazyDigest
tookMillis(): ?int
elapsedMillis(): float
statusCode(): ?int
position(): ?int
One search seen going out, and what came back. The digest is still lazy, so an
observer that drops this search has not paid to parse it. tookMillis() is null
for every line of an _msearch, whose response reports one took for the whole
batch, and when the response body could not be read without disturbing it.
statusCode() is null when the request never got a response.
Http\LoggingObserver¶
__construct(LoggerInterface $logger, string $level = LogLevel::INFO, string $message = 'opensearch.search')
Writes one PSR-3 record per search, with os and took in the shape the
dashboard pack maps.
Monolog¶
Monolog\DigestProcessor¶
__construct(?Formatter $formatter = null, string $requestKey = 'query', string $indexKey = 'index')
__invoke(array|LogRecord $record)
One class for both Monolog 2 and 3. Push it once and every log record carrying a
search body under $requestKey gets it replaced by the digest.
Monolog\SafeDigest¶
The object that actually lands in your log context — a LazyDigest that cannot
take the record down with it. If parsing fails while Monolog is formatting, it
serialises the error rather than throwing, because losing a digest is acceptable
and losing the log line is not.