Filter-aware aliases: pre_filter enforcement + filtered BM25 statistics - #226
Open
DarshitChanpura wants to merge 3 commits into
Open
DarshitChanpura wants to merge 3 commits into
DarshitChanpura wants to merge 3 commits into
Conversation
DarshitChanpura
force-pushed
the
feat/filter-aware-aliases
branch
4 times, most recently
from
August 5, 2026 00:00
6c050b2 to
8ff81da
Compare
Owner
Author
End-to-end flow (one line per hop)Where the substance is: most files just carry the
Default is |
DarshitChanpura
force-pushed
the
feat/filter-aware-aliases
branch
from
August 19, 2026 20:11
8ff81da to
5f6d399
Compare
DarshitChanpura
force-pushed
the
feat/filter-aware-aliases
branch
from
August 19, 2026 20:31
5f6d399 to
30c4db9
Compare
…hange) Adds the API scaffolding for the filter-aware-alias work without changing any query behavior: - new @experimentalapi enum AliasFilter.Enforcement { POST_FILTER, PRE_FILTER } - optional, non-null enforcement field on AliasFilter, defaulting to POST_FILTER so every existing caller and the two-arg constructor are unaffected - wire-format is version-gated (V_3_8_0+): older peers neither read nor write the field and receivers default to POST_FILTER, so mixed-version clusters keep today's behavior - enforcement participates in equals/hashCode/toString PRE_FILTER is recorded and serialized only; no code path acts on it yet. The behavior (applying the filter before scoring so BM25 statistics reflect only the visible subset) lands in a follow-up. Tests: default-is-POST_FILTER, same-version round-trip, pre-3.8 BWC default, and enforcement-in-equality. 5/5 green in AliasFilterTests. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
…sible subset Threads a new 'enforcement' setting (post_filter | pre_filter) end-to-end through the alias pipeline and, when pre_filter is set, applies the alias filter BEFORE scoring so BM25 collection statistics (N, df) reflect only the documents the alias admits. This makes relevance scoring a function of the visible subset alone, whereas post-filtering (the default) scores over the whole shard and filters afterward. Layers: - REST/transport: 'enforcement' on IndicesAliasesRequest.AliasActions (parsed, xcontent, version-gated wire format at V_3_8_0) - cluster state: enforcement on AliasAction.Add -> AliasMetadata (gated wire + xcontent + equals/hashCode) - bridge: IndicesService.buildAliasFilter maps any resolved alias marked pre_filter to AliasFilter.Enforcement.PRE_FILTER - search: DefaultSearchContext wraps the query in ConstantScoreQuery(MUST=query, FILTER=aliasFilter) for pre_filter; the default post_filter path is unchanged Default is post_filter, so existing behavior and mixed-version clusters are unaffected. This is the constant-score increment; filtered CollectionStatistics / TermStatistics, prefix scoring, and dfs aggregation are follow-ups. Test: FilterAwareAliasIT proves, on a live cluster, that a term confined to the filtered-out subset scores the same as an absent term through a pre_filter alias, while through a post_filter alias it scores lower; both views return the same visible set. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
Completes the A-track for pre_filter aliases beyond the constant_score increment. A3/A4 - filtered statistics (ContextIndexSearcher): When an alias is enforced pre_filter with the filtered_stats scoring sub-behavior, collectionStatistics(field) and termStatistics(term) are computed over a lazily-built, live-docs-aware visible-doc bitset derived from the alias filter, so BM25 relevance ranking reflects only the visible subset. Guarded by Lucene's stats invariants (null for absent field/term; ttf>=df; sumDocFreq>=docCount). A6 - dfs_query_then_fetch: the DFS phase's per-shard statistics use the same filtered overrides, so coordinator-aggregated stats also reflect the visible subset. The filtered branch is evaluated before the aggregatedDfs branch; the three cases (non-dfs, dfs-phase compute, dfs query-phase read) are documented. Scoring-mode gate: filtered_stats is opt-in via the opensearch.filter_aware_alias.filtered_stats system property, read per request (not a static final) so it is toggleable without a JVM restart; constant_score remains the default pre_filter behavior. Prefix scoring: the query-path match_phrase_prefix score also reflects only the visible subset under pre_filter. Raw term-dictionary APIs (terms/suggest/ _termvectors) are a separate surface, not covered here. Tests - FilterAwareAliasIT proves, on a live cluster: under pre_filter a term confined to the filtered-out subset scores the same as an absent term while under post_filter it scores lower; the same holds for prefix scoring; and the filtered_stats BM25 score EQUALS the score from a physically-filtered visible-only index (ground-truth N/df correctness). Regression: 101 tests across alias/rollover/dfs/ContextIndexSearcher green. Default behavior (post_filter) unchanged. Signed-off-by: Darshit Chanpura <dchanp@amazon.com>
DarshitChanpura
force-pushed
the
feat/filter-aware-aliases
branch
from
August 25, 2026 19:04
30c4db9 to
a59a846
Compare
3 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds filter-aware aliases to OpenSearch: a filtered alias can now enforce its
filter before query statistics and term enumeration are computed, instead of
only post-filtering the result set. When enabled, this closes a scoring /
term-dictionary side-channel that plain filtered aliases — and, by extension, the
security plugin's DLS post-filtering — leave open, at zero storage and full
freshness.
Background — the side-channel
A filtered alias narrows the result set to matching documents, but the query
still executes against the whole shard:
(
idf = log(1 + (N - df + 0.5)/(df + 0.5)), whereN/dfspan every document,including hidden ones). A term appearing only in hidden documents has an
inflated
df, which depresses its score. A restricted user who can write adocument and read back its score can detect hidden terms — the ExactOracle.
prefix/wildcard/fuzzy/match_phrase_prefix) expandsagainst the full term dictionary — the PrefixOracle.
Both exist because the filter is applied after statistics and the term
dictionary have been consulted over all documents.
What this adds
A new
enforcementsetting on an aliasaddaction:post_filter(default)pre_filterpre_filterhas two scoring sub-behaviors:constant_score(default forpre_filter) — the query is wrapped inConstantScoreQuery(BooleanQuery(MUST=query, FILTER=aliasFilter)); no IDF iscomputed, so there is no scoring channel to leak. Fits the common security
case where ranking is not required.
filtered_stats— real BM25 scoring, butCollectionStatistics(
N_f,docCount_f) andTermStatistics(df_f,ttf_f) are computed overthe visible-document bitset, so ranking works over the visible subset and still
leaks nothing. Opt-in via the
opensearch.filter_aware_alias.filtered_statssystem property (read perrequest), keeping
constant_scorethe default while this sub-behavior isexperimental.
Design / data flow
enforcementfield is on the wire only between nodes atV_3_8_0+; oldernodes never see it and behave as
post_filter, so mixed-version clusters areunaffected.
AliasFilter.Enforcementis@ExperimentalApi.dfs_query_then_fetch: the DFS phase's per-shard statistics use the samefiltered overrides, so coordinator-aggregated stats are filtered too. The
filtered branch is evaluated before the
aggregatedDfsbranch; the three cases(non-dfs query, dfs-phase compute, dfs query-phase read) are documented in
ContextIndexSearcher.Testing
FilterAwareAliasITproves the behavior on a live cluster:testPreFilterClosesScoringSideChannelWhilePostFilterLeaks— through apost_filteralias a hidden-only term scores below a control term (theleak); through
pre_filterthey score identically (closed). Both viewsreturn the same visible set.
testPreFilterClosesPrefixScoringChannel— same contrast for amatch_phrase_prefixexpansion (the PrefixOracle scoring path).testFilteredStatisticsMatchPhysicallyFilteredIndex— thefiltered_statsBM25 score equals the score from a physically-filtered visible-only index
(ground-truth
df/Ncorrectness, not merely "lower").Regression: 101 tests across alias / rollover / dfs /
ContextIndexSearchergreen;
AliasFilterTestscovers serialization + pre-3.8 wire BWC.Scope / follow-ups (not in this PR)
terms/suggest/_termvectorswithterm statistics bypass the per-request query filter and are not covered;
they need the enumeration itself intersected with the alias bitset. Documented
in
FILTER_AWARE_ALIASES.md.opensearch-project/securityrepo; design is sketched inFILTER_AWARE_ALIASES.md. This PR provides the core mechanism they build on.filtered_statsas a first-class wire-level scoring mode — currently gatedbehind a system property rather than an alias field.
Backward compatibility
Default is
post_filter— existing aliases, queries, and mixed-version clustersbehave exactly as before. No default path changes.
See
server/.../action/admin/indices/alias/FILTER_AWARE_ALIASES.mdfor the fulldesign note.