Search: Add DisjointFacetEngine, FilterBuilder, FacetCache, and search groups
Review Request #15052 — Created May 13, 2026 and updated
Adds the core faceted search engine, the
reviewboard/search/facets/package,
along with benchmark tooling for performance validation.Key components:
-DisjointFacetEngine(aggregations.py) builds and executes a single
Elasticsearchmsearchbody per request containing: a paginated results
query, one aggregation sub-query perfaceted=TrueFilterSpec(each
excluding its own filter clause so bucket counts remain accurate when the
filter is active — the disjoint property), and one option-count query per
compound filter choice.
-FilterSpec/FilterBuilder(schema.py,builder.py) provide a
declarative schema for filter dimensions and translate active filter values
into Elasticsearchbool.filterclauses. Compound filters (ship-it, issues,
reviews, file attachments) use custombuild_fncallables.
-FacetCache(cache.py) caches aggregation results per group, query, and
filter state for the lifetime of a request.
-FacetedSearchEngine(engine.py) orchestrates searches across
ReviewRequestSearchGroup,UserSearchGroup, andReviewGroupSearchGroup.
For the active group it runs a full search with aggregations; for inactive
groups it runs a count-only query so sidebar tab totals are accurate.
-ReviewRequestSearchGroupadditionally handles the Contents filter, which
restricts the text query to specific ES fields and optionally runs a parallel
query against comment indexes to surface review requests with matching
comments.
- Also addspopulate_bench_datamanagement command andbench_manager.pyfor
populating and benchmarking persistent datasets at 1k, 10k, 100k, and 1M
scale across Postgres, MySQL, and MariaDB.
Verified that
DisjointFacetEnginebuilds the correct msearch body structure,
enforces the disjoint property on aggregation sub-queries, and produces exactly
one msearch call per search regardless of how many filters are active. Verified
thatFilterBuildergenerates correct Elasticsearch clauses for each filter
type and that the permission filter is always injected. Verified that
FacetedSearchEngineroutes full searches to the active group and count-only
queries to inactive groups.Performance benchmarks were run at 10k, 100k, and 1M review requests on
Postgres, MySQL, and MariaDB. A 6-filter faceted search against a 1M-document
index completes in 4.7ms with latency remaining flat across all scales. Full
results are in the attached benchmark report.
| Summary | ID | Author |
|---|---|---|
| 9d6264ed7f45b8ba2d3f6c134eee21960a005a21 | DanielCasaresIglesias |
| Description | From | Last Updated |
|---|---|---|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
'django.core.management.base.CommandError' imported but unused Column: 1 Error code: F401 |
|
|
|
missing whitespace after ':' Column: 23 Error code: E231 |
|
|
|
line too long (81 > 79 characters) Column: 80 Error code: E501 |
|
|
|
multiple statements on one line (def) Column: 5 Error code: E704 |
|
|
|
multiple spaces before keyword Column: 35 Error code: E272 |
|
|
|
multiple statements on one line (def) Column: 5 Error code: E704 |
|
|
|
multiple statements on one line (def) Column: 5 Error code: E704 |
|
|
|
multiple spaces before keyword Column: 33 Error code: E272 |
|
|
|
multiple statements on one line (def) Column: 5 Error code: E704 |
|
|
|
multiple spaces before keyword Column: 34 Error code: E272 |
|
|
|
multiple statements on one line (def) Column: 5 Error code: E704 |
|
|
|
multiple spaces before keyword Column: 33 Error code: E272 |
|
|
|
multiple statements on one line (def) Column: 5 Error code: E704 |
|
|
|
multiple spaces before keyword Column: 34 Error code: E272 |
|
|
|
line too long (83 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (81 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (81 > 79 characters) Column: 80 Error code: E501 |
|
|
|
f-string is missing placeholders Column: 32 Error code: F541 |
|
|
|
f-string is missing placeholders Column: 14 Error code: F541 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
f-string is missing placeholders Column: 10 Error code: F541 |
|
|
|
multiple spaces before operator Column: 27 Error code: E221 |
|
|
|
multiple spaces before operator Column: 28 Error code: E221 |
|
|
|
multiple spaces before operator Column: 26 Error code: E221 |
|
|
|
multiple spaces before operator Column: 28 Error code: E221 |
|
|
|
f-string is missing placeholders Column: 13 Error code: F541 |
|
|
|
no newline at end of file Column: 32 Error code: W292 |
|
|
|
'time' imported but unused Column: 1 Error code: F401 |
|
|
|
expected 1 blank line, found 0 Column: 5 Error code: E301 |
|
|
|
expected 1 blank line, found 0 Column: 5 Error code: E301 |
|
|
|
expected 1 blank line, found 0 Column: 5 Error code: E301 |
|
|
|
expected 1 blank line, found 0 Column: 5 Error code: E301 |
|
|
|
expected 1 blank line, found 0 Column: 5 Error code: E301 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
f-string is missing placeholders Column: 16 Error code: F541 |
|
|
|
'datetime.timezone' imported but unused Column: 1 Error code: F401 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
continuation line over-indented for visual indent Column: 47 Error code: E127 |
|
|
|
continuation line over-indented for visual indent Column: 47 Error code: E127 |
|
|
|
line too long (81 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (82 > 79 characters) Column: 80 Error code: E501 |
|
|
|
'django.db.models' imported but unused Column: 1 Error code: F401 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (82 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
'reviewboard.search.facets.cache.FacetCache' imported but unused Column: 1 Error code: F401 |
|
|
|
line too long (83 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (81 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (82 > 79 characters) Column: 80 Error code: E501 |
|
|
|
blank line contains whitespace Column: 1 Error code: W293 |
|
|
|
blank line contains whitespace Column: 1 Error code: W293 |
|
|
|
blank line contains whitespace Column: 1 Error code: W293 |
|
|
|
blank line contains whitespace Column: 1 Error code: W293 |
|
|
|
blank line contains whitespace Column: 1 Error code: W293 |
|
|
|
line too long (81 > 79 characters) Column: 80 Error code: E501 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
- Commits:
-
Summary ID Author 5374b963609df18752e686d913433b2e1f1d8505 DanielCasaresIglesias 442abf12ff7610466b2e5f13584e7eaff19619cf DanielCasaresIglesias - Diff:
-
Revision 2 (+16382 -2)
Checks run (1 failed, 1 succeeded)
flake8
- Commits:
-
Summary ID Author 442abf12ff7610466b2e5f13584e7eaff19619cf DanielCasaresIglesias f889b02773719e8e15e86308f86bffe086c342c8 DanielCasaresIglesias - Diff:
-
Revision 3 (+16394 -2)
Checks run (1 failed, 1 succeeded)
flake8
- Commits:
-
Summary ID Author f889b02773719e8e15e86308f86bffe086c342c8 DanielCasaresIglesias 9d6264ed7f45b8ba2d3f6c134eee21960a005a21 DanielCasaresIglesias - Diff:
-
Revision 4 (+16394 -2)