Session: Querying: How to Perform a Faceted (Aggregated) Search

To execute facet (aggregation) query via the session query method, use aggregate_by, aggregate_by_facets, or aggregate_using.
This will scope you to the aggregation query builder, where you'll be allowed to define single or multiple facets for the query using a straightforward and fluent API.

Syntax

def aggregate_by(
    self, builder_or_facet: Union[Callable[[FacetBuilder], None], FacetBase]
) -> AggregationDocumentQuery[_T]: ...

def aggregate_by_facets(self, facets: List[FacetBase]) -> AggregationDocumentQuery[_T]: ...

def aggregate_using(self, facet_setup_document_id: str) -> AggregationDocumentQuery[_T]: ...
Parameters
builder_or_facet Union[Callable[[FacetBuilder], None] Builder with a fluent API that constructs a FacetBase instance
-or-
FacetBase
implementation defining the scope of the facet and its options (either Facet or RangeFacet)
facets List[FacetBase] Items containing FacetBase implementations
facet_setup_document_id str ID of a document containing FacetSetup

Facet & RangeFacet

Facet vs RangeFacet

RangeFacet allows you to split the results of the calculations into several ranges, in contrast to Facet where whole spectrum of results will be used to generate a single outcome.

class FacetBase(ABC):
    def __init__(self):
        self.display_field_name: Union[None, str] = None
        self.options: Union[None, FacetOptions] = None
        self.aggregations: Dict[FacetAggregation, Set[FacetAggregationField]] = {}

class Facet(FacetBase):
    def __init__(self, field_name: str = None):
        super().__init__()
        self.field_name = field_name
class RangeFacet(FacetBase):
    def __init__(self, parent: Optional[FacetBase] = None):
        super().__init__()
        self.ranges: List[str] = []
class FacetAggregation(enum.Enum):
    NONE = "None"
    MAX = "Max"
    MIN = "Min"
    AVERAGE = "Average"
    SUM = "Sum"

Builder

def by_ranges(self, range_: RangeBuilder, *ranges: RangeBuilder) -> FacetOperations[_T]: ...

def by_field(self, field_name: str) -> FacetOperations[_T]: ...

def with_display_name(self, display_name: str) -> FacetOperations[_T]: ...

def with_options(self, options: FacetOptions) -> FacetOperations[_T]: ...

def sum_on(self, path: str, display_name: Optional[str] = None) -> FacetOperations[_T]: ...

def min_on(self, path: str, display_name: Optional[str] = None) -> FacetOperations[_T]: ...

def max_on(self, path: str, display_name: Optional[str] = None) -> FacetOperations[_T]: ...

def average_on(self, path: str, display_name: Optional[str] = None) -> FacetOperations[_T]: ...
Parameters
*ranges RangeBuilder A list of aggregated ranges
field_name str Points to the index field that should be used for operation (by_ranges, by_field) or to document field that should be used for aggregation (sum_on, min_on, max_on, average_on)
display_name str If set, results of a facet will be returned under this name
options FacetOptions Non-default options that should be used for operation
path str Points to the index field that should be used for operation (by_ranges, by_field) or to document field that should be used for aggregation (sum_on, min_on, max_on, average_on)

Options

def __init__(self):
    self.page_size: int = constants.int_max
    self.start: Union[None, int] = None
    self.term_sort_mode: FacetTermSortMode = FacetTermSortMode.VALUE_ASC
    self.include_remaining_terms: bool = False
Options
term_sort_mode FacetTermSortMode Indicates how terms should be sorted (VALUE_ASC, VALUE_DESC, COUNT_ASC, COUNT_DESC)
include_remaining_terms bool Indicates if remaining terms should be included in results
start Union[None, int] Used to skip given number of facet results in the outcome
page_size int Used to limit facet results to the given value

Example I

facet_options = FacetOptions.default_options()
facet_options.term_sort_mode = FacetTermSortMode.COUNT_DESC
facet_options.start = 0

facet1 = Facet("manufacturer")
facet1.options = facet_options

facet2 = RangeFacet()
facet2.ranges = [
    "cost < 200",
    "cost between 200 and 400",
    "cost between 400 and 600",
    "cost between 600 and 800",
    "cost >= 800",
]
facet2.aggregations = {FacetAggregation.AVERAGE: {FacetAggregationField("cost")}}

facet3 = RangeFacet()
facet3.ranges = [
    "megapixels < 3",
    "megapixels between 3 and 7",
    "megapixels between 7 and 10",
    "megapixels >= 10",
]

facets = (
    session.query_index("Camera/Costs", Camera)
    .aggregate_by(facet1)
    .and_aggregate_by(facet2)
    .and_aggregate_by(facet3)
    .execute()
)
from index 'Camera/Costs' 
select 
facet(manufacturer), 
facet(cost < 200, cost >= 200 AND cost < 400, cost >= 400 AND cost < 600, cost >= 600 AND cost < 800, cost >= 800),
facet(megapixels < 3, megapixels >= 3 AND megapixels < 7, megapixels >= 7 AND megapixels < 10, megapixels >= 10)

Example II

options = FacetOptions()
options.start = 0
options.term_sort_mode = FacetTermSortMode.COUNT_DESC

cost_builder = RangeBuilder.for_path("cost")
megapixels_builder = RangeBuilder.for_path("megapixels")

facet_result = (
    session.query_index("Camera/Costs", Camera)
    .aggregate_by(lambda builder: builder.by_field("manufacturer").with_options(options))
    .and_aggregate_by(
        lambda builder: builder.by_ranges(
            cost_builder.is_less_than(200),
            cost_builder.is_greater_than_or_equal_to(200).is_less_than(400),
            cost_builder.is_greater_than_or_equal_to(400).is_less_than(600),
            cost_builder.is_greater_than_or_equal_to(600).is_less_than(800),
            cost_builder.is_greater_than_or_equal_to(800),
        ).average_on("cost")
    )
    .and_aggregate_by(
        lambda builder: builder.by_ranges(
            megapixels_builder.is_less_than(3),
            megapixels_builder.is_greater_than_or_equal_to(3).is_less_than(7),
            megapixels_builder.is_greater_than_or_equal_to(7).is_less_than(10),
            megapixels_builder.is_greater_than_or_equal_to(10),
        )
    )
).execute()
from index 'Camera/Costs' 
select 
facet(manufacturer), 
facet(cost < 200, cost >= 200 AND cost < 400, cost >= 400 AND cost < 600, cost >= 600 AND cost < 800, cost >= 800),
facet(megapixels < 3, megapixels >= 3 AND megapixels < 7, megapixels >= 7 AND megapixels < 10, megapixels >= 10)

Example III

facet_setup = FacetSetup()

facet_manufacturer = Facet()
facet_manufacturer.field_name = "manufacturer"
facet_setup.facets = [facet_manufacturer]

camera_facet = RangeFacet()
camera_facet.ranges = [
    "cost < 200",
    "cost between 200 and 400",
    "cost between 400 and 600",
    "cost between 600 and 800",
    "cost >= 800",
]

megapixels_facet = RangeFacet()
megapixels_facet.ranges = [
    "megapixels < 3",
    "megapixels between 3 and 7",
    "megapixels between 7 and 10",
    "megapixels >= 10",
]

facet_setup.range_facets = [camera_facet, megapixels_facet]

session.store(facet_setup, "facets/CameraFacets")
session.save_changes()

facets = session.query_index("Camera/Costs", Camera).aggregate_using("facets/CameraFacets").execute()
from index 'Camera/Costs' 
select facet(id('facets/CameraFacets'))

aggregate_by only supports aggregation by a single field.
If you want to aggregate by multiple fields, emit a single field that contains all values.