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.
Ran full test suite.
Verified thatDisjointFacetEnginebuilds 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 |
|---|---|
| ee52788d43f67517ef8a2c9cbc2afd768ce89a28 | |
| 452e8e874e05dbbd64c8bad9d9e57c86485cc28e | |
| 00533e465f9c2ca0d8a5edd219293d104b6196a4 |
| 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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
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 |
|
|
|
f-string is missing placeholders Column: 32 Error code: F541 |
|
|
|
f-string is missing placeholders Column: 14 Error code: F541 |
|
|
|
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 |
|
|
|
'time' imported but unused Column: 1 Error code: F401 |
|
|
|
'dataclasses.asdict' imported but unused Column: 1 Error code: F401 |
|
|
|
'dataclasses.field' imported but unused Column: 1 Error code: F401 |
|
|
|
f-string is missing placeholders Column: 16 Error code: F541 |
|
|
|
'datetime.timezone' imported but unused Column: 1 Error code: F401 |
|
|
|
'django.db.models' imported but unused Column: 1 Error code: F401 |
|
|
|
blank line contains whitespace Column: 1 Error code: W293 |
|
|
|
blank line contains whitespace Column: 1 Error code: W293 |
|
|
|
line too long (80 > 79 characters) Column: 80 Error code: E501 |
|
|
|
local variable 'rrs2' is assigned to but never used Column: 9 Error code: F841 |
|
|
|
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 |
|
|
|
'django.contrib.auth.models.User' imported but unused Column: 1 Error code: F401 |
|
|
|
missing whitespace after ':' Column: 21 Error code: E231 |
|
|
|
'reviewboard.search.facets.cache.FacetCache' imported but unused Column: 1 Error code: F401 |
|
- 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)
Checks run (2 succeeded)
- Change Summary:
-
Added the Any/All toggle.
Fixed permission issues for group searches.
Updated schema lables to correctly match issue status
Madetest_faceted_aggregationmuch more verbose, ensuring each filter works properly. - Commits:
-
Summary ID Author 9d6264ed7f45b8ba2d3f6c134eee21960a005a21 DanielCasaresIglesias dce9e5f371006b906a6de9c9281c75f3f26efaa1 Daniel Casares-Iglesias b27d978e486ace6500164cf5f4daf9fb555e164b Daniel Casares-Iglesias - Diff:
-
Revision 5 (+17698 -32)
Checks run (1 failed, 1 succeeded)
flake8
-
Warning: Showing 30 of 56 failures.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Testing Done:
-
~ Verified that
DisjointFacetEnginebuilds the correct msearch body structure,~ Ran full test suite.
+ 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 that FilterBuildergenerates correct Elasticsearch clauses for each filtertype and that the permission filter is always injected. Verified that FacetedSearchEngineroutes full searches to the active group and count-onlyqueries 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.
- Change Summary:
-
Updated styling
- Commits:
-
Summary ID dce9e5f371006b906a6de9c9281c75f3f26efaa1 b27d978e486ace6500164cf5f4daf9fb555e164b ee52788d43f67517ef8a2c9cbc2afd768ce89a28 452e8e874e05dbbd64c8bad9d9e57c86485cc28e 23a73ce313846d4d573c2929027ee2519912d4ba - Diff:
-
Revision 6 (+22759 -457)
Checks run (1 failed, 1 succeeded)
flake8
-
Warning: Showing 30 of 36 failures.
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
- Commits:
-
Summary ID ee52788d43f67517ef8a2c9cbc2afd768ce89a28 452e8e874e05dbbd64c8bad9d9e57c86485cc28e 23a73ce313846d4d573c2929027ee2519912d4ba ee52788d43f67517ef8a2c9cbc2afd768ce89a28 452e8e874e05dbbd64c8bad9d9e57c86485cc28e 2a206452a8e36b6a964996e727ae9a88fdde797b - Diff:
-
Revision 7 (+22796 -478)
- Commits:
-
Summary ID ee52788d43f67517ef8a2c9cbc2afd768ce89a28 452e8e874e05dbbd64c8bad9d9e57c86485cc28e 2a206452a8e36b6a964996e727ae9a88fdde797b ee52788d43f67517ef8a2c9cbc2afd768ce89a28 452e8e874e05dbbd64c8bad9d9e57c86485cc28e 00533e465f9c2ca0d8a5edd219293d104b6196a4 - Diff:
-
Revision 8 (+22795 -479)