Skip to content

ast: index a lookup into a collection in base data - #9235

Merged
srenatus merged 2 commits into
open-policy-agent:mainfrom
srenatus:sr/rzrvxlusyklz
Sep 21, 2026
Merged

srenatus merged 2 commits into
open-policy-agent:mainfrom
srenatus:sr/rzrvxlusyklz

Conversation

@srenatus

@srenatus srenatus commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

data.groups.g1.members[input.subject] was recorded as "this reference must be defined", so a ruleset of one rule per group had nothing to discriminate on. The lookup is now recorded as what it asks -- the last element has to be a key of the collection at the ground prefix -- and gather asks the collection: one hash lookup into an object or a set, nothing stored per member, nothing to bound or configure. value in collection asks for values rather than keys, which base data answers only by being walked, and is left alone.

Note

Only asked where every candidate is evaluated anyway. A ruleset whose definitions agree on their value lets a caller stop at the first that holds (= EE), and gather() would have to ask about all of them to exclude any -- the work evaluation was about to do, the lookup being what the rule tests. A resolver says its caller does not stop by implementing IndexEveryCandidateEvaluated, as partial evaluation never does.

                                  before          after
  partial eval, 100 rules      187577 ns/op    61043 ns/op
  partial eval, 500 rules      658317 ns/op   173877 ns/op
  500 rules, no early exit     356424 ns/op   149821 ns/op
  500 rules, early exit        352908 ns/op   359272 ns/op

The index carries 0.50 MB against 0.46 MB for 500 rules over 50 groups of 20000 members each -- a million members, none of them on the trie.


Update I've pushed a second commit to brush up the CHANGELOG.md section on RI: the prefix/suffix was missing from it, and the other two sections took too much space. Now it's one RI section with a couple of related points, and a href to the docs.

@netlify

netlify Bot commented Sep 18, 2026

Copy link
Copy Markdown

Deploy Preview for openpolicyagent ready!

Name Link
🔨 Latest commit ff28b6c
🔍 Latest deploy log https://app.netlify.com/projects/openpolicyagent/deploys/6aad2040671dc50008241758
😎 Deploy Preview https://deploy-preview-9235--openpolicyagent.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@srenatus
srenatus force-pushed the sr/rzrvxlusyklz branch 3 times, most recently from 39cbbc7 to 0616dd4 Compare September 21, 2026 06:32

@johanfylling johanfylling left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM 👍

Comment thread CHANGELOG.md Outdated

Authored by @sueun-dev

### Rule indexing sees a lookup into a collection in base data

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be folded into Rule indexing improvements above in it's entirety?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally. Thought this was already gone 🙈

Comment thread v1/ast/index.go Outdated
@@ -439,6 +572,18 @@ type valueMapper struct {
}

// refID identifies one of the references an index is built on.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd wager this should remain being attached to type refID int32 below.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yup 🙃

Comment thread v1/ast/index.go Outdated
// evaluation was about to do -- the lookup being what the rule tests.
//
// Whether a caller stops is the caller's own business, not IndexResult.EarlyExit's:
// partial evaluation evaluates every candidate under a ruleset that permits stopping.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll admit the PE note here gave me a hint without needing to look for implementations of IndexEveryCandidateEvaluated; but that said, should we be keeping implementation specific info out of the indexer?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I suppose so... I mean, the check is deliberately added to that PE behaves the way we want -- it used to be something like "does it EE?" but then it turned out that under PE, EE is ignored. So it took a turn towards "every candidate is evaluated" as the more-or-less direct opposite of EE 🔀

Comment thread v1/ast/index.go

// resolve answers for a reference, asking the resolver the first time only.
func (c *resolveCache) resolve(resolver ValueResolver, ref Ref) (Value, error) {
if c.keyOK && RefEqual(c.keyRef, ref) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we banking on ref staying somewhat stable for a lookup, and not oscillating between values?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This is also only for data refs -- they won't change within a single index lookup.

Comment thread v1/ast/index.go
}
// Base data read from JSON holds no set, but a store keeping ast.Value can,
// and so can a `with` statement replacing the collection.
case Set:

@johanfylling johanfylling Sep 21, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Couldn't key also be a number, for which we can do direct indexing into arrays; or am I missing some nuance here?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Our array's Get would end up being a bounds check (like for objects, or sets, we don't check the value, only its existence, even if it's false). So to help with indexing, we'd only differentiate rule bodies referencing arrays of different lengths. That seems very uncommon. Let's revisit if someone raises a need for this. Sounds good?

`data.groups.g1.members[input.subject]` was recorded as "this reference must be
defined", so a ruleset of one rule per group had nothing to discriminate on. The
lookup is now recorded as what it asks -- the last element has to be a key of the
collection at the ground prefix -- and gather asks the collection: one hash lookup
into an object or a set, nothing stored per member, nothing to bound or configure.
`value in collection` asks for values rather than keys, which base data answers
only by being walked, and is left alone.

Only asked where every candidate is evaluated anyway. A ruleset whose definitions
agree on their value lets a caller stop at the first that holds, and gather would
have to ask about all of them to exclude any -- the work evaluation was about to
do, the lookup being what the rule tests. A resolver says its caller does not stop
by implementing IndexEveryCandidateEvaluated, as partial evaluation never does.

                                  before          after
  partial eval, 100 rules      187577 ns/op    61043 ns/op
  partial eval, 500 rules      658317 ns/op   173877 ns/op
  500 rules, no early exit     356424 ns/op   149821 ns/op
  500 rules, early exit        352908 ns/op   359272 ns/op

The index carries 0.50 MB against 0.46 MB for 500 rules over 50 groups of 20000
members each -- a million members, none of them on the trie.

Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
The prefix and suffix matching merged earlier had no entry at all, and the
indexer had collected two of its own.


Signed-off-by: Stephan Renatus <stephan.renatus@gmail.com>
@srenatus
srenatus enabled auto-merge (squash) September 21, 2026 15:14
@srenatus
srenatus merged commit 0ef4095 into open-policy-agent:main Sep 21, 2026
43 checks passed
@srenatus
srenatus deleted the sr/rzrvxlusyklz branch September 21, 2026 15:20
@github-actions

Copy link
Copy Markdown

Benchmark comparison 237455042e28a112971d0e1d55f9f1abf97794c80ef4095b2993c19734ebdf292a2e46fe9763d934: no changes ≥15%.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants