Skip to content

Read a materialized top-K as a step in a multi-hop query (Step 6 of #386) - #498

Merged
zipdoki merged 1 commit into
mainfrom
feat/multihop-query
Aug 31, 2026
Merged

zipdoki merged 1 commit into
mainfrom
feat/multihop-query

Conversation

@zipdoki

@zipdoki zipdoki commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Summary

A ranking gets written and can be read by naming its entity, but only through its own endpoint. Nothing can use one mid-query. Taking the accounts a user follows and then reading each one's top purchases needs two round trips and a client-side join. This adds a TOPK step to the multi-hop query so a ranking is just another hop.

The step names the source table and the declaration on it; the rows come from the ranking table that declaration points at. RankScan.from(...) resolves that table, its metric index and the start key from (topk, entity, dimensionValues). entity is a vertex like any other hop's source, so it can be a REF into an earlier step's
field. Omit it and the step reads a global ranking.

{
  "fetch": [
    {
      "type": "SCAN",
      "name": "a",
      "database": "graph",
      "table": "follows",
      "index": "created_at",
      "source": { "type": "VALUE", "value": "user1" }
    },
    {
      "type": "TOPK",
      "name": "b",
      "database": "graph",
      "table": "orders",
      "topk": "top_product_groups_1y",
      "entity": { "type": "REF", "ref": "a", "field": "tgt" },
      "limit": 10,
      "include": true
    }
  ]
}

I renamed query to fetch. There is no alias, so a payload written against query fails to parse. Nothing is released on this path, so I took the break rather than carrying two names; if someone turns out to be on the old one, the alias is a one-liner.

An empty result now carries the table's columns. TableBinding.emptyFrame replaces DataFrame.empty on the SCAN, CACHE and TOPK paths. A read that found nothing still came from a table that has columns, and dropping them turns "no rows" into "no such column" for anything that resolves names against the frame — the SQL transform stacked on
top of this cannot plan against a schema-less one. For a TOPK step the empty frame comes from the ranking table, not from the source table the step names, which costs one extra binding lookup on the empty path.

One thing this does not do: a TOPK step over several entities concatenates their rankings rather than merging them by metric, so the rows come back grouped per entity and not globally ordered. Ordering across entities is what the SQL transform in the next PR is for, and I'd rather do it there than hard-code a merge here.

Stacked on #497 — that needs to land first.

Part of #386.

Test plan

  • ./gradlew :engine:test --tests '*TopkQuerySpec*' — per-entity, global and dimension-split rankings as a hop, REF entities, empty rankings
  • ./gradlew :engine:test --tests '*ActionbaseQueryParserSpec*' — TOPK step parsing and the fetch key
  • ./gradlew :engine:test --tests '*MultiHopQuerySpec*' — existing hops still chain
  • ./gradlew :server:test --tests '*ActionbaseQueryE2ETest*' — the query endpoint over HTTP
  • ./gradlew spotlessCheck build — formatting and full build

AI Assistance

  • This PR was written largely with AI assistance.
    • Tool / model: Claude Code (Opus 5)

@zipdoki zipdoki self-assigned this Aug 20, 2026
@zipdoki
zipdoki requested a review from em3s as a code owner August 20, 2026 03:53
@dosubot dosubot Bot added the size:L This PR changes 100-499 lines, ignoring generated files. label Aug 20, 2026
@zipdoki
zipdoki removed the request for review from em3s August 20, 2026 03:53
@dosubot dosubot Bot added the enhancement New feature or request label Aug 20, 2026
@zipdoki
zipdoki force-pushed the feat/multihop-query branch from 168b835 to 1d90945 Compare August 27, 2026 04:53
@zipdoki
zipdoki force-pushed the feat/multihop-query branch from 564d0aa to 5f72cc5 Compare August 27, 2026 08:08
Base automatically changed from feat/topk-query to main August 31, 2026 00:50
@zipdoki
zipdoki force-pushed the feat/multihop-query branch from 5f72cc5 to cb9931e Compare August 31, 2026 00:50
@zipdoki

zipdoki commented Aug 31, 2026

Copy link
Copy Markdown
Contributor Author

Optimistic Merge

@zipdoki
zipdoki merged commit d4b7546 into main Aug 31, 2026
3 checks passed
@zipdoki
zipdoki deleted the feat/multihop-query branch August 31, 2026 01:03
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request size:L This PR changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant