Oihana PHP Arango

FacetCountsQueryTrait uses trait:short

Builds the AQL query that computes per-value **facet counts** for several dimensions at once, alongside (not replacing) the document list.

Each requested dimension is a key of the model's $this->facets whitelist (the filterable facets become the counted facets). One LET sub-query per dimension counts values over the same conjunctive filter as the list, so the buckets reflect the currently filtered set:

LET category = (FOR doc IN @@coll FILTER <same filters> COLLECT value = doc.category WITH COUNT INTO count SORT count DESC, value ASC RETURN { value, count })
LET status   = (FOR doc IN @@coll FILTER <same filters> COLLECT value = doc.status   WITH COUNT INTO count SORT count DESC, value ASC RETURN { value, count })
RETURN { category, status }

Supported types: the scalar Facet::FIELD, the array-membership Facet::IN family (Facet::LIST, Facet::LIST_FIELD, Facet::LIST_FIELD_SORTED) and the two linked facets Facet::EDGE and Facet::JOIN; other facet types are skipped. A Facet::PROPERTY carrying the [*] array-expansion marker (e.g. offers[*].priceCurrency) unwinds the object array and counts the sub-field per element — see FacetCountsQueryTrait::buildFacetCountSubquery().

A linked dimension counts the documents reached through the relation it already filters on — an INBOUND edge traversal or a key-join — bucketing on a field of the related document named by Facet::VALUE (default _key):

LET location = (FOR doc IN @@coll FILTER <same filters> FOR doc_location IN INBOUND doc places_edges COLLECT value = doc_location.name WITH COUNT INTO count SORT count DESC, value ASC RETURN { value, count })

The unwinding facet types (the [*] expansion, the Facet::IN family and the linked facets) count rows by default, so a document reaching the same value several times — a repeated array element, two parallel edges to the same vertex, several joined documents — is counted several times, diverging from the equivalent ?filter= existence test, which counts documents. Declaring Facet::DISTINCT => true on such a facet switches its bucket count to COUNT_DISTINCT( doc._key ), so the count reflects distinct root documents and matches the filter. The flag is opt-in (default unchanged) and a no-op on the scalar Facet::FIELD type, which already counts one row per document.

Top-N buckets. Every dimension returns all its values by default, which a sidebar showing ten entries does not need. Facet::LIMIT => n closes the shared tail with a LIMIT n placed after the sort, so what survives is the n biggest buckets. It is read once for every type — the scalar field, the unwound array, the [*] sub-field and the linked relations — because they all end on the same tail. Arango::FACET_COUNTS_LIMIT overrides it per request, so the declaration is a default rather than a ceiling.

A total order. Buckets are sorted count DESC, value ASC. The second criterion is what makes a top-N mean something: ordering by the count alone leaves equal-count buckets in whatever order the server produced, and AQL guarantees no stable sort — so a LIMIT n falling inside a run of equal counts could keep a different subset from one request to the next. The bucket value is unique per bucket (it is the COLLECT key), so the order is total.

Permission. The dimension gate runs before the type is dispatched, so it covers the linked types unchanged — but the two guards do not weigh the same for them: the projection inheritance (isAttributeAuthorized) looks the dimension up in the main model's fields, which cannot say anything about a field of another collection. A linked facet is therefore gated by the Field::REQUIRES declared on the facet itself, exactly as on the filtering side.

Tags
see
FacetCountsQueryTrait::buildFacetCountsQuery()

The entry point.

Table of Contents

Constants

FACET_COUNT_ITEM  : string = 'item'
The unwind loop variable for array-membership facets (kept distinct from {@see FacetCountsQueryTrait::FACET_COUNT_VALUE} to avoid a name collision).
FACET_COUNT_VALUE  : string = 'value'
The bucket value attribute name in the returned rows (`{ value, count }`).

Methods

buildFacetCountsQuery()  : string
Builds the multi-`LET` facet-counts query, or an empty string when nothing is countable.
buildFilteredScope()  : array{0: string, 1: ?string}
Builds the `FOR` segment and the conjunctive `FILTER` clause shared by the list-family queries.
assertPositiveLimit()  : int
Guards a bucket limit, wherever it came from: a positive integer passes, anything else is refused naming its origin.
buildFacetCountSubquery()  : string|null
Builds one dimension's counting sub-query, or null for an unsupported type.
facetCountCollect()  : array<int, string>
The shared `COLLECT value = <expr> … SORT count DESC, value ASC RETURN { value, count }` tail.
facetCountLimit()  : int|null
Resolves how many buckets a dimension keeps: the number of the `LIMIT`, or null when it is unlimited (the default).
facetCountRelation()  : array{0: string, 1: array}
Resolves how a linked facet reaches the documents it counts: the `FOR` opening the related documents and — for a key-join — the `FILTER` tying them to the main one.

Constants

FACET_COUNT_ITEM

The unwind loop variable for array-membership facets (kept distinct from {@see FacetCountsQueryTrait::FACET_COUNT_VALUE} to avoid a name collision).

private string FACET_COUNT_ITEM = 'item'

FACET_COUNT_VALUE

The bucket value attribute name in the returned rows (`{ value, count }`).

private string FACET_COUNT_VALUE = 'value'

Methods

buildFacetCountsQuery()

Builds the multi-`LET` facet-counts query, or an empty string when nothing is countable.

public buildFacetCountsQuery([array<string|int, mixed> $init = [] ][, array<string|int, mixed> &$bindVars = [] ][, string $docRef = AQL::DOC ]) : string
Parameters
$init : array<string|int, mixed> = []

The list query options (Arango::FACET_COUNTS holds the dimensions).

$bindVars : array<string|int, mixed> = []

The bind variables, populated by reference.

$docRef : string = AQL::DOC

The document reference.

Tags
throws
BindException
ConstantException
ContainerExceptionInterface
DependencyException
NotFoundException
NotFoundExceptionInterface
ReflectionException
UnsupportedOperationException
ValidationException
Return values
string —

The compiled AQL query, or an empty string.

buildFilteredScope()

Builds the `FOR` segment and the conjunctive `FILTER` clause shared by the list-family queries.

protected buildFilteredScope([array<string|int, mixed> $init = [] ][, array<string|int, mixed> &$bindVars = [] ]) : array{0: string, 1: ?string}
Parameters
$init : array<string|int, mixed> = []

The query options.

$bindVars : array<string|int, mixed> = []

The bind variables, populated by reference.

Tags
throws
BindException
ConstantException
ContainerExceptionInterface
DependencyException
NotFoundException
NotFoundExceptionInterface
ReflectionException
UnsupportedOperationException
ValidationException
Return values
array{0: string, 1: ?string} —

The [ $for, $filter ] pair.

assertPositiveLimit()

Guards a bucket limit, wherever it came from: a positive integer passes, anything else is refused naming its origin.

private assertPositiveLimit(mixed $limit, string $origin) : int
Parameters
$limit : mixed

The candidate limit.

$origin : string

What is being blamed, as the message reads it.

Tags
throws
ValidationException

When the limit is not a positive integer.

Return values
int —

The validated limit.

buildFacetCountSubquery()

Builds one dimension's counting sub-query, or null for an unsupported type.

private buildFacetCountSubquery(array<string|int, mixed> $facet, string $key, string $for, string|null $filter, string $docRef[, int|false|null $limitOverride = null ]) : string|null
Parameters
$facet : array<string|int, mixed>

The facet definition (Facet::TYPE, Facet::PROPERTY).

$key : string

The facet key (default property).

$for : string

The pre-built FOR segment shared by every dimension — the bound collection, or the bound View with its SEARCH segment when the View search is active.

$filter : string|null

The shared FILTER clause.

$docRef : string

The document reference.

$limitOverride : int|false|null = null

The request-level bucket limit (Arango::FACET_COUNTS_LIMIT): an integer overriding the declaration, false for explicitly unlimited, or null when the declaration decides.

Tags
throws
BindException
ConstantException
ReflectionException
UnsupportedOperationException
ValidationException
Return values
string|null

facetCountCollect()

The shared `COLLECT value = <expr> … SORT count DESC, value ASC RETURN { value, count }` tail.

private facetCountCollect(string $expression, string $sort[, string|null $distinctKey = null ][, int|null $limit = null ]) : array<int, string>

By default the bucket count is the number of unwound rows (WITH COUNT INTO count). When $distinctKey is provided (opt-in Facet::DISTINCT => true on an unwinding facet), the count becomes the number of DISTINCT root documents in the bucket (AGGREGATE count = COUNT_DISTINCT( <distinctKey> )), so a document whose array repeats the same sub-field value is counted once — matching the ?filter= existence semantics. The aggregate is deliberately named Group::COUNT_NAME (count) so the derived RETURN { value, count } and the SORT count DESC, value ASC clause stay identical in both modes.

When $limit is provided (opt-in Facet::LIMIT => n), a LIMIT n closes the tail after the sort, so what survives is the n biggest buckets rather than an arbitrary n of them.

Parameters
$expression : string

The value expression to group on.

$sort : string

The pre-built SORT count DESC, value ASC clause.

$distinctKey : string|null = null

The root document key expression to count distinctly (e.g. doc._key), or null for the default per-element count.

$limit : int|null = null

The number of buckets to keep, or null for all of them.

Tags
throws
BindException
ReflectionException
UnsupportedOperationException
Return values
array<int, string> —

The [ COLLECT, SORT, (LIMIT,) RETURN ] fragments.

facetCountLimit()

Resolves how many buckets a dimension keeps: the number of the `LIMIT`, or null when it is unlimited (the default).

private facetCountLimit(array<string|int, mixed> $facet, string $key[, int|false|null $override = null ]) : int|null

Two voices can speak, and the request has the last word. Facet::LIMIT declares the dimension's default — the sane cap a sidebar wants on a large vocabulary — and Arango::FACET_COUNTS_LIMIT overrides it for one request, raising it as well as lowering it. The override has three states, which is why it is not merely an integer:

  • null (absent) → the declaration decides;
  • a positive integer → that many buckets, whatever was declared;
  • false → explicitly unlimited, cancelling a declared limit. It is what the controller translates ?facetCountsLimit=all into — the model never sees the HTTP word.

A non-positive or non-integer limit is refused rather than ignored, on either side, and the reason is the helper it would reach: aqlLimit() answers an empty string to 0 or less, which emits no LIMIT clause at all — so a limit asking for nothing would silently return everything, the exact opposite of what it says. A limit that cannot be honoured shows.

Parameters
$facet : array<string|int, mixed>

The facet definition.

$key : string

The facet key, named in the refusal message.

$override : int|false|null = null

The request-level override, or null for none.

Tags
throws
ValidationException

When the declared limit, or the override, is not a positive integer.

Return values
int|null —

The number of buckets to keep, or null when unlimited.

facetCountRelation()

Resolves how a linked facet reaches the documents it counts: the `FOR` opening the related documents and — for a key-join — the `FILTER` tying them to the main one.

private facetCountRelation(array<string|int, mixed> $facet, string $key, string $type, string $docRef) : array{0: string, 1: array}

The two types differ only in that reaching, exactly as they do on the filtering side (HasFacetEdge vs HasFacetJoin): an INBOUND traversal needs no predicate — it already targets the right vertices — while a join opens the whole collection and narrows it with its match. The join anchor is shared with the filtering facets through resolveFacetJoin, so a declaration counts over exactly the relation it filters on.

The declared names are guarded by assertAttributeName(): they are config-trusted, but a missing edge collection or joined collection would otherwise compile to a truncated FOR … IN — a broken query blamed on the request rather than on the declaration.

Parameters
$facet : array<string|int, mixed>

The facet definition (AQL::EDGE, or AQL::COLLECTION / AQL::KEY / Facet::PROPERTY / AQL::ARRAY).

$key : string

The facet key; drives the related document reference (doc_<key>).

$type : string

The facet type (Facet::EDGE or Facet::JOIN).

$docRef : string

The main document reference.

Tags
throws
ConstantException
ReflectionException
ValidationException
Return values
array{0: string, 1: array} —

The related document reference, and the AQL fragments reaching it (a FOR, plus a FILTER for a join).

On this page

Search results