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
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_COUNTSholds the dimensions). - $bindVars : array<string|int, mixed> = []
-
The bind variables, populated by reference.
- $docRef : string = AQL::DOC
-
The document reference.
Tags
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
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
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
FORsegment shared by every dimension — the bound collection, or the bound View with itsSEARCHsegment when the View search is active. - $filter : string|null
-
The shared
FILTERclause. - $docRef : string
-
The document reference.
- $limitOverride : int|false|null = null
-
The request-level bucket limit (
Arango::FACET_COUNTS_LIMIT): an integer overriding the declaration,falsefor explicitly unlimited, or null when the declaration decides.
Tags
Return values
string|nullfacetCountCollect()
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 ASCclause. - $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
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=allinto — 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
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, orAQL::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
Return values
array{0: string, 1: arrayThe related document reference,
and the AQL fragments reaching it (a FOR, plus a FILTER for a join).