fix(opensearch): rank the wildcard graph view from a complete candidate set - #3707
fix(opensearch): rank the wildcard graph view from a complete candidate set#3707YashvantHange wants to merge 4 commits into
Conversation
…te set Degree came from two terms aggregations on the edge index, one per endpoint field, each capped at its own size. An entity whose in-degree and out-degree both fell outside their respective top-N never reached the ranking however high its undirected degree was. That is not a pathological shape: the max_nodes cutoff normally lands in the degree-1/degree-2 band, so an entity with one in-edge and one out-edge falling outside both lists is ordinary for a large graph. Edge documents now carry `endpoints`, a multi-valued keyword holding both ids, so one terms aggregation returns one bucket per entity with its true undirected degree. A self-loop stores a single value, which also makes the ranking agree with node_degree, where the loop's one document is counted once. Existing indexes get the mapping and a scripted backfill at startup, guarded by an index _meta flag so a completed run costs one mapping read rather than a rescan. Unlike the canonical-edge-id migration next door, this one degrades instead of failing startup: the legacy aggregations are exactly what shipped before, so a cluster that cannot complete the backfill keeps serving the approximate ranking and retries on the next startup. get_popular_labels ranked from the same two aggregations and shares the new path. The aggregation now sets an explicit shard_size and logs when the response still reports a non-zero doc_count_error_upper_bound, so residual multi-shard count error is surfaced rather than hidden. Closes HKUDS#3613.
|
@codex review |
|
Codex Review: Didn't find any major issues. Delightful! Reviewed commit: ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
If Codex has suggestions, it will comment; otherwise it will react with 👍. Codex can also answer questions or update the PR. Try commenting "@codex address that feedback". |
|
Reviewed this branch merged onto current Three things to fix before this merges. 1.
|
…creation Review follow-ups on the endpoints degree ranking. `endpoints` duplicates the top-level source/target and is storage-internal, so exclude it from `KnowledgeGraphEdge.properties` alongside the other fields `_construct_graph_edge` already filters, instead of surfacing it on every edge in a `/graphs` response and in the WebUI edge panel. A freshly created edges index set the in-memory readiness flag but wrote no `_meta`, so the next startup recounted missing-field docs on an index that cannot hold one. Stamp the flag into the `indices.create` body, making it durable from the moment the index exists. The mapping test asserted on the source text of `_create_indices_if_not_exist`, so a rename or a reflow broke it while the mapping stayed correct, and a literal moved into dead code would still pass. Assert on the request the storage sends instead, which covers the `_meta` stamp in the same body.
|
Thanks — all three fixed in 144ccf9. 1. I deliberately left 2. I checked it cannot clobber the canonical-edge-id flag on the same index: that migration writes 3. The source-text assertion. Replaced with On the progress-logging note. I left it. That backfill is a single |
|
@codex review |
Codex Review SummaryThis comment shows the latest Codex review activity on this pull request.
ℹ️ About Codex in GitHubYour team has set up Codex to review pull requests in this repo. Reviews are triggered when you
Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 144ccf93f3
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…e flag The index _meta flag was read as a durable guarantee that every edge doc carries `endpoints`. It cannot be one: nothing fences writes against an old-version process, so during a rolling deploy a worker on the previous release can index an edge without the field after this one has stamped the flag. Every later startup then returned on the flag alone, leaving that edge outside the degree aggregation permanently, with no operation short of clearing the flag by hand to bring it back. The flag now records that a backfill completed, and each startup that finds an existing index revalidates coverage with a `terminate_after` count before trusting it; a dirty index re-runs the backfill and re-stamps. That closes the window rather than eliminating it -- an edge written by an old worker after the last startup stays uncovered until the next one -- which is as far as this goes without a write fence, and it costs ranking quality alone. A failed revalidation falls back to the flag rather than below it. Degrading a healthy migrated index to the approximate ranking for the life of the process would make this check the likeliest cause of the degradation it exists to prevent. The missing-docs query is now defined once and shared by the probe and the backfill, so the two cannot drift on what "missing" means.
|
Fixed in bd05a2e. The report is right, and it lands on something you and I both noted last round as an accepted cost — an old-version worker writing edges after the flag is set. What neither of us followed through on is that the flag makes the damage permanent: without a coverage check, no later startup ever looks again, so that edge stays outside the aggregation until someone clears the mapping flag by hand. A window that heals on the next restart and one that never heals are different bugs. So the flag no longer means "the field is universal", only "a backfill completed". Every startup that finds an existing index revalidates coverage with a Two things worth your eye: The guard must not degrade what it guards. A search rejection or a transport error from the probe would otherwise fall into the existing The re-stamp is now reachable, and The window is narrowed, not closed: an edge written by an old-version worker after the last startup stays uncovered until the next one, and there is no write fence available to do better. That residual is stated in the docstring rather than left implicit, and it costs ranking quality alone — never a wrong edge, only an entity ranked from a degree short of its real one. The startup that creates the index is exempt from the probe and safely so, since it holds the data-init lock and the index it stamps is empty; that is now written down too. Four new tests: the probe's shape and bound, a flagged-but-dirty index being backfilled again with the sibling flag preserved, a repair that does not finish keeping the legacy ranking, and probe failure falling back to the flag. Three fail against the pre-fix source. |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: bd05a2efbe
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
The coverage-revalidation probe added last round guarded against a rolling-deploy writer dirtying a flagged index, but its own failure path defeated that guard: a search rejection or transport error on the probe set _edge_endpoints_ready=True outright, trusting the flag unverified on exactly the index most likely to actually be dirty. Fall through to the backfill path instead. It costs one recount: a transient failure on a genuinely clean index re-stamps the flag in the same startup, a genuinely dirty index gets repaired in the same startup instead of waiting for the next one, and a store that is still unreachable degrades to the legacy ranking through the same outer handler every other failure in this method already falls back to.
|
Confirmed and fixed in d9e6313. The finding is right, and reproducing it shows exactly why: with the index flagged and the probe ( Fixed by falling through to the backfill path instead of returning early on probe failure. That path already knows how to answer the question the probe couldn't:
Three new tests pin this: probe failure never sets the ready flag on its own, a transient failure on a clean index self-heals within the same startup, and a transient failure on a genuinely dirty index gets repaired within the same startup. Two of the three fail against the pre-fix code (confirmed by reverting locally and re-running) — the third (self-heal on a clean index) happened to pass either way since a clean recount is harmless regardless of what the probe-failure branch does with the flag, so it's there to pin the good path going forward rather than to prove the bug. |
|
@danielaskdd Fixed the revalidation-flag finding in d9e6313 (details above). Could you re-review when you have a moment? |
|
@codex review |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: d9e6313d29
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| "must": [ | ||
| {"exists": {"field": "source_node_id"}}, | ||
| {"exists": {"field": "target_node_id"}}, | ||
| ], |
There was a problem hiding this comment.
Backfill the surviving endpoint of partial edge documents
When a legacy or malformed edge contains only source_node_id or only target_node_id, this must clause excludes it from both the backfill and readiness checks, allowing the index to be marked ready while that document has no endpoints value. The new aggregation then ignores the edge entirely, even though the previous per-field aggregation and node_degree() count its surviving endpoint, so wildcard and popular-label rankings can silently demote that valid entity; backfill the available endpoint or keep the legacy path while such documents remain.
Useful? React with 👍 / 👎.
Description
OpenSearchGraphStorageranked the*graph view by degree read from two terms aggregations on the edge index, one per endpoint field, each capped at its ownsize. The union of two separately-truncated top-N lists is not the global top-N: an entity whose in-degree and out-degree both fall outside their respective lists never reaches the sort, however high its undirected degree is. Themax_nodescutoff normally lands in the degree-1/degree-2 band, so an entity with one in-edge and one out-edge outside both lists is an ordinary shape for a large graph, not a pathological one.Edge documents now carry
endpoints, a multi-valued keyword holding both ids, so a single terms aggregation returns one bucket per entity holding true undirected degree. The candidate set the ranking sorts is then the global top by degree.Of the two storage shapes proposed in the issue, this takes the
endpointsfield rather than materializing per-node degree onto the node document. Materialized degree would have to be maintained on every edge write and delete, adding a hot-path cost and a correctness surface that the field approach does not need.Related Issues
Closes #3613. Follows #3593, which changed only how the existing ranking broke ties.
Changes Made
endpointskeyword field on the edges mapping, written by bothupsert_edgeandupsert_edges_batch. A self-loop stores a single value, which also makes the ranking agree withnode_degree: that method counts the loop's one document once, while the pair of aggregations counted it on both the source and the target side._ensure_edge_endpoints_readylands the mapping and backfills existing indexes with a scriptedupdate_by_query, guarded by an index_metaflag so a completed run costs one mapping read per startup rather than a rescan. The sameput_mapping+ value-backfill pattern as_ensure_scheduling_tiebreaker_ready._degree_map_from_edge_indexis the single ranking source for both_get_knowledge_graph_allandget_popular_labels, which ranked from the same two aggregations (checklist item 4 in the issue).shard_sizeand logs when the response still reports a non-zerodoc_count_error_upper_bound.BaseGraphStorage.get_knowledge_graphcontract updated.The backfill degrades rather than failing startup
Worth a look during review, since it differs from the canonical-edge-id migration next door. That one is fail-fast because it has no correct fallback. This one does: the legacy aggregations are exactly the behaviour that shipped, so a cluster that cannot complete the backfill (permissions, a transport error, a large index mid-reindex) keeps serving the approximate ranking and retries on the next startup, rather than refusing to start over a ranking-quality issue. The
_metaflag is set only after a recount confirms no documents still lack the field, so a partial backfill cannot be mistaken for a complete one.On the second approximation
Terms aggregations are count-approximate across shards independently of the truncation above. Degree is exact at the single-shard default (
OPENSEARCH_NUMBER_OF_SHARDS=1). On a multi-shard index this sets an explicitshard_sizeto shrink the error and logs the residual bound when the cluster reports one, rather than hiding it. Removing it entirely would need a composite aggregation paging every term, which cannot order by count. The contract docstring says this rather than claiming the caveat is fully retired (checklist item 5).Checklist
Additional Notes
New
tests/kg/opensearch_impl/test_opensearch_degree_ranking.pyuses a stub with faithfultermssemantics (buckets ordered bydoc_countdescending then key ascending, only the topsizereturned) so one edge fixture drives both aggregation shapes. It pins the issue's minimal case in both directions: the legacy union omitsA, theendpointsaggregation returns it with its true degree 2. Write-path, backfill-completion, partial-backfill and degradation-on-error cases are covered too.Thirteen existing stubs in
test_opensearch_storage.pyhard-coded the two-aggregation response shape and were updated to answer either shape from the same described graph.tests/kg/opensearch_implpasses (375) andruff check .is clean. The widertests/kgrun shows 11 failures on my machine that are Windows-only and predate this branch (module 'signal' has no attribute 'SIGKILL'in the lock and reservation tests); none of those files touch OpenSearch.