Repository navigation
Conversation
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 1
Open (2)
What changed in this PR
This PR fixes incorrect mixed-index aggregations (count/min/max/sum/mean) and ordering behavior across Lucene/Elasticsearch/Solr so results match in-memory traversal semantics, especially for missing values, limits/ranges, and transactional changes.
Changes:
- Make mixed-index aggregations return “no result” (null → no traverser output) when aggregating over no values, and restrict when index-side aggregation is allowed.
- Fix Lucene/Solr ordering so documents missing the ordered field sort last in both directions; improve Lucene min/max handling with existence filtering and multi-valued fields.
- Adjust Elasticsearch/Solr aggregation parsing to preserve nulls and distinguish “no values” from numeric zero; add new backend tests covering these cases.
| File | Description |
|---|---|
| janusgraph-solr/src/test/resources/solr/core-template/schema.xml | Sets sortMissingLast="true" for numeric/date types in test schema to match JanusGraph ordering semantics. |
| janusgraph-solr/src/main/java/org/janusgraph/diskstorage/solr/SolrIndex.java | Uses stats count to return null for min/max/sum/mean over no values instead of backend-specific defaults. |
| janusgraph-lucene/src/main/java/org/janusgraph/diskstorage/lucene/SumCollector.java | Reworks collector into a stats-style collector tracking count/sum/min/max across stored field values. |
| janusgraph-lucene/src/main/java/org/janusgraph/diskstorage/lucene/LuceneIndex.java | Ensures missing-field ordering sorts last, fixes min/max via existence filtering, and bases avg on value-count. |
| janusgraph-es/src/main/java/org/janusgraph/diskstorage/es/rest/RestElasticSearchClient.java | Parses full aggregation result objects to preserve nulls; uses stats to detect “no values” for sum. |
| janusgraph-es/src/main/java/org/janusgraph/diskstorage/es/rest/RestAggValue.java | Supports null value and adds count/sum fields for stats aggregation parsing. |
| janusgraph-es/src/main/java/org/janusgraph/diskstorage/es/ElasticSearchClient.java | Changes avg contract to return Double (nullable) to represent “no values”. |
| janusgraph-dist/src/assembly/static/conf/solr/schema.xml | Updates distributed Solr schema to sort missing numeric/date values last. |
| janusgraph-core/src/main/java/org/janusgraph/graphdb/tinkerpop/optimize/step/JanusGraphMixedIndexAggStep.java | Restricts index aggregation to cases matching traversal semantics; handles null results and range offset for counts. |
| janusgraph-core/src/main/java/org/janusgraph/diskstorage/indexing/IndexProvider.java | Documents null semantics for aggregations over no values. |
| janusgraph-core/src/main/java/org/janusgraph/core/MixedIndexAggQuery.java | Documents that null indicates no values for min/max/mean/sum. |
| janusgraph-backend-testutils/src/main/java/org/janusgraph/graphdb/JanusGraphIndexTest.java | Adds regression tests for mixed-index aggregations and ordering, including missing values and limit/range behavior. |
| docs/index-backend/solr.md | Documents Solr schema requirements for ordering elements missing the ordered field. |
| docs/changelog.md | Adds upgrade notes explaining changed aggregation semantics and API signature updates. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Fixes JanusGraph#5031 * JanusGraphMixedIndexAggStep emits no result when the index answers a min, max, sum or mean with null, which IndexProvider.queryAggregation now documents as "no values", so an aggregation of no values has no result, as when TinkerPop aggregates the values itself. * The step only stands in for the traversal where the index gives the same answer: for exactly one mixed index subquery, as a has() on a key which doesn't exist, or one a composite index answers, failed with IndexOutOfBoundsException or ClassCastException; outside a transaction with uncommitted changes, which the index doesn't hold, whichever elements they touch; for a key which is ENABLED in the index; and, but for a count, without a limit or an offset, as the index aggregates every match. A count stops at the limit of the traversal, where the index query's own limit may be capped by query.hard-max-limit or query.smart-limit, and leaves out the offset of a range. * Lucene sorts documents without the field after the others in either direction, as JanusGraph orders elements in memory and Elasticsearch does, where it sorted them as 0 or, for strings ascending, first. The minimum and maximum of a single-valued field come from the first, in that order, of the matching documents which have a value, null when none has; those of a LIST or SET field, which has no doc values to sort by, and every sum and mean come from StatsCollector (SumCollector before), which reads every stored value of the matching documents and counts them. The mean divides by the number of values instead of the number of documents. * Elasticsearch keeps the null value of a min, max or avg aggregation of no values, which became 0, and computes a sum from the stats aggregation, whose count tells a sum of no values from a sum of 0. * Solr asks its stats for the count of values as well and returns null for a count of 0, where a minimum or maximum failed and a sum was 0 and a mean NaN. The numeric and date types of the test and example schemas declare sortMissingLast="true". * Tests: JanusGraphIndexTest.testMixedIndexAggregationsOfTheValuesThereAre and testMixedIndexOrdersElementsWithoutTheKeyLast, for every index backend. Docs: changelog entry, and the Solr page says how the schema decides where elements without a value go. Co-Authored-By: Oleksandr Porunov <alexandr.porunov@gmail.com> Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Signed-off-by: Oleksandr Porunov <alexandr.porunov@gmail.com>
efc457b to
b8310c5
Compare
There was a problem hiding this comment.
Warning
Copilot couldn't run its full agentic review because it didn't start before the timeout. Make sure your repository has a runner available, or add a copilot-code-review.yml file specifying one with the runs-on attribute. See the docs for more details.
Copilot review overview
Review effort: Lite
Findings: 1



Fixes #5031
When a mixed index answers the query in front of
count(),min(),max(),sum()ormean(), JanusGraph computes the aggregation in the index. That answer differed from the one the traversal computes from the elements:LISTorSETkey.limit()orrange()in front, and a count ignored the range's offset.query.hard-max-limitorquery.smart-limit.has()on a key which doesn't exist, or one that a composite index answers, failed while the traversal was optimized.Separately, Lucene, and Solr with JanusGraph's schemas, sorted the elements without the key of an
order().by(key)among the others, as 0 for numbers and dates. JanusGraph puts them last when it orders elements itself, and so does Elasticsearch.The issue has the tables of what master gives on each backend.
Changes
JanusGraphMixedIndexAggStep:ENABLEDin the index;range()is taken off the count.IndexProvider.queryAggregationandMixedIndexAggQuery.executesays what null means.FieldExistsQueryfilter). Without the filter, a document without a value could tie with one holdingLong.MAX_VALUEand come first.LISTorSETfield has no doc values to sort by, so its minimum and maximum come fromStatsCollector, renamed fromSumCollector, as every sum and mean does. It reads every stored value of the matching documents and counts them.RestAggValue.valueis aDouble, so the null of a min, max or avg aggregation of no values stays null.ElasticSearchClient.avgreturnsDouble.statsaggregation, whosecounttells a sum of no values from a sum of 0.executeAggsreturns the parsed aggregation. The line that Bound Elasticsearch counts by their limit and ask aggregations for less #5013 changes is untouched, so the two merge cleanly.sortMissingLast="true"onint,long,float,doubleanddateand theirtaliases, as theirstringandbooleantypes already did.next()on an aggregation of no values now throwsNoSuchElementException, and the changed signatures.Not changed:
Longfor whole numbers and aDoublefor decimals. TinkerPop keeps the type of the values unless the sum outgrows it. The tests compare numeric values.OrderGlobalSteporders, it drops them, as theirby()produces nothing. JanusGraph folds an order that a mixed index can answer into its own step, which keeps them, and this PR makes Lucene and Solr put them where that step does.Tests
JanusGraphIndexTest.testMixedIndexAggregationsOfTheValuesThereAre(new, every index backend):LISTkey, over vertices with and without values, a match without values, and no match.Long.MAX_VALUE, and the maximum of one holding onlyLong.MIN_VALUE, each beside a vertex without a value that was indexed first.limit()andrange(), and aggregations after them.query.hard-max-limit= 1.has()on a key which doesn't exist, and one that a composite index answers.has()cases, every case is compared with the in-memory result of the same traversal and with the expected value, and the profile must show whetherJanusGraphMixedIndexAggStepanswered it.NullPointerExceptionat its first aggregation, Elasticsearch gives[0]where memory gives nothing, and Solr fails withNullPointerException.ENABLEDfilter, the index query's limit, the count's offset, the limit and offset condition, and the existence filter.JanusGraphIndexTest.testMixedIndexOrdersElementsWithoutTheKeyLast(new): Integer, Double and String keys, ascending and descending, with and withoutlimit. On master it fails with Lucene and Solr and passes with Elasticsearch.BerkeleyLuceneTest(83),LuceneIndexTest(255, 148 skipped) and the module's other tests.BerkeleyElasticsearchTest(94, 1 skipped),ElasticsearchIndexTest(287) and the module's unit tests.JanusGraphIndexTestagainst Solr 9.10.1 in process instead, throughSolrRunner'sMiniSolrCloudClusterwith the test configset. Of its 86 tests, 84 pass.testClearStorageis disabled, as in the Solr test classes, andtestOrderingListPropertycan't create its collection, whichSolrRunnerhas no configset for.{!min=true max=true sum=true mean=true count=true}over a multi-valued int point field across all values: min -7, max 10, count 3, sum 6, mean 2.0;sortMissingLast, and last in both directions with it.🤖 Generated with Claude Code