perf(schema): cache compiled JSONPath expressions in document validation - #735
Draft
vishal-bala wants to merge 2 commits into
Draft
perf(schema): cache compiled JSONPath expressions in document validation#735vishal-bala wants to merge 2 commits into
vishal-bala wants to merge 2 commits into
Conversation
`extract_from_json_path` called `jsonpath_ng.parse` on every
invocation, and `validate_object` calls it once per field per document
on the JSON storage path. Field paths are fixed when the schema is
constructed, so every parse after the first repeated work already done.
Parsing is the expensive half of a JSONPath lookup: jsonpath-ng builds
its grammar with PLY, so one `parse("$.t0")` measures 1.83 ms against
microseconds to evaluate the result.
Measured here on an 11-field JSON schema with validation enabled:
reparsing every field 18.683 ms/doc 53 docs/s
cache warm 0.062 ms/doc 16,138 docs/s
a 302x difference, or about 5 hours of PLY parsing on a one-million
document load. `SearchIndex` recommends `validate_on_load=True` in its
own docstring, so this sat on a signposted path.
Move the parse behind an `lru_cache` keyed on the raw path. Keying
before the leading-`$` normalisation means `field` and `$.field` occupy
separate entries compiling to equivalent expressions, which is harmless
and keeps the lookup on the cheapest path.
Sharing one parsed expression across calls is safe because the
expression holds no per-evaluation state: `find()` reads it and returns
fresh match objects. There is a test for that specifically, since it is
the assumption the cache rests on.
Not addressed here: `SchemaModelGenerator.get_model_for_schema` builds
its cache key by re-serialising the whole schema per document, which
dominates the remaining cost on the HASH path. Fixing it means keying
on schema identity plus a mutation counter, which changes what happens
when a schema is mutated in place, so it wants its own change.
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.
Motivation
extract_from_json_pathcalledjsonpath_ng.parseon every invocation, andvalidate_objectcalls it once per field per document on the JSON storage path. Field paths are fixed when the schema is constructed, so every parse after the first repeated work already done.Parsing is the expensive half of a JSONPath lookup. jsonpath-ng builds its grammar with PLY, so a single
parse("$.t0")measures 1.83 ms against microseconds to evaluate the result. Measured on an 11-field JSON schema with validation enabled:That is a 302x difference, or roughly five hours spent inside the PLY parser on a one-million document load.
SearchIndexrecommendsvalidate_on_load=Truein its own docstring, so the cost sat on a signposted path.Changes
Caching the compiled expression
The parse moves behind an
lru_cachein a new_compile_json_pathhelper, andextract_from_json_pathbecomes a lookup plus an evaluation. The cache is keyed on the raw path, before the leading-$normalisation, sofieldand$.fieldoccupy separate entries that compile to equivalent expressions. That duplication is harmless and it keeps the lookup on the cheapest available path.Sharing one parsed expression across calls is safe because the expression holds no per-evaluation state:
find()reads the expression and returns fresh match objects.Tests
Four tests cover the behaviour rather than the timing, since a timing assertion would be flaky. Two count calls into
jsonpath_parseto confirm that a repeated path compiles once and that distinct paths are not collapsed onto one expression. One evaluates a single cached expression against several objects, including one where the path misses, which guards the no-shared-state assumption the cache rests on. One confirms both spellings of a path remain equivalent.Notes
SchemaModelGenerator.get_model_for_schemabuilds its cache key by re-serialising the whole schema per document, which dominates the remaining cost on the HASH storage path. Addressing it means keying on schema identity plus a mutation counter, which changes what happens when a schema is mutated in place, so it is left for a separate change rather than folded in here.Release Notes
Loading documents into a JSON index with
validate_on_load=Trueis substantially faster. JSONPath expressions derived from the schema are now compiled once and reused, instead of being re-parsed for every field of every document.