Visitar URL original
Cypher: db.index.vector.queryNodes filters only after the top-k; point users to vector.neighbors with where/filter (docs), or accept a filter argument · Issue #10685 · ArcadeData/arcadedb · GitHub
Skip to content

Cypher: db.index.vector.queryNodes filters only after the top-k; point users to vector.neighbors with where/filter (docs), or accept a filter argument #10685

Closed
ArcadeData/arcadedb-docs
#506

Description

@tae898

db.index.vector.queryNodes(index, k, vector) (the Neo4j-compatible form, exactly three arguments) has no filter, so a tenant or property filter can only be applied to its k results afterwards. With k equal to the wanted row count, a query whose nearest neighbours mostly belong to another tenant returns fewer rows than asked, or none. The pre-filter exists, through vector.neighbors with filter (a RID list or subquery) and, from 26.11.1, where (#10169 / #10185), and it works from Cypher too via CALL vector.neighbors(...). But nothing in the Cypher docs points a Bolt or Cypher user from queryNodes to it.

This is what the Graphiti driver evaluators ran into in getzep/graphiti#1310 ("the global top-k before the tenant filter", k = limit returning 2 of 10 rows).

Measured with 900 vectors of tenant b, all nearer the query than any of the 100 of tenant a, k = 10, on 26.10.2 and on main ad9ebddead:

Query 26.10.2 main
Cypher CALL db.index.vector.queryNodes(idx, 10, $q) YIELD node ... WHERE node.tenant = 'a' 0 rows 0 rows
queryNodes with a fourth (options) argument error: expects 3 arguments same
Cypher CALL vector.neighbors(..., {where: ...}) where rejected 10 rows, all tenant a
SQL vector.neighbors with filter: (SELECT @rid ...) 10 rows, all tenant a 10 rows, all tenant a

Two options, either would help: a sentence in the Cypher vector-search docs saying that queryNodes filters after the top-k and that CALL vector.neighbors(..., {where: ...}) (26.11.1) or filter is the pre-filtered form; or an optional fourth argument on queryNodes that maps to the same pre-filter.

Activity

  1. added
    documentationImprovements or additions to documentation
    enhancementNew feature or request
    severity:minorCosmetic, documentation, edge case, or a workaround exists
    vectorVector index and embedding search
    on Oct 23, 2026
  2. added this to the 26.11.1 milestone on Oct 23, 2026
  3. tae898 commented on Oct 24, 2026

    @tae898
    ContributorAuthor

    For the docs side, a multi-tenant hybrid example could help, because both sources of a vector.fuse need the tenant filter, not only the vector one. This statement returns only rows of the requested tenant on main (26.11.1-SNAPSHOT), and with filter: (SELECT @rid FROM Fact WHERE tenant = :tenant) instead of where it gives the same rows on 26.10.2:

    SELECT record.fid AS fid, record.text AS fact, score,
           record.out('MENTIONS').out('RELATES')[tenant = :tenant].name AS related_entities
    FROM (SELECT expand(`vector.fuse`(
        `vector.neighbors`('Fact[embedding]', :qvec, :k, { where: { tenant: :tenant } }),
        (SELECT @rid, $score FROM Fact
          WHERE tenant = :tenant AND SEARCH_INDEX('Fact[text]', :keywords) = true
          ORDER BY $score DESC LIMIT :k),
        { fusion: 'RRF' })))
    ORDER BY score DESC LIMIT :top

    The [tenant = :tenant] guard on the hops matters too: with one cross-tenant RELATES edge in the data, the hops without it return the other tenant's entities. The full runnable version, with a check that every row belongs to the tenant, is example 29 in ArcadeData/arcadedb-python#32.

  4. self-assigned this
    on Oct 27, 2026
  5. added a commit that references this issue on Oct 28, 2026
  6. robfrank commented on Oct 28, 2026

    @robfrank
    Collaborator

    Fixed in #10808. db.index.vector.queryNodes(index, k, vector) now takes an optional fourth options map with the same pre-filter as vector.neighbors (for example {where: {tenant: $tenant}}), tested by the index as it meets each candidate, so a filter no longer shrinks the top k. The three-argument form still filters after the top k, as in Neo4j.

    Still open: the documentation update in ArcadeData/arcadedb-docs (say that the 3-argument form filters after the top k, show the pre-filtered forms and the multi-tenant vector.fuse example); no docs issue has been filed yet.


    ❤️ Thanks for helping make ArcadeDB better! ArcadeDB is free and open source, sustained by its community. If it's useful to you or your company, please consider becoming a sponsor to keep development going.

  7. removed
    in progressThe team is actively working on this issue
    on Oct 28, 2026
  8. added a commit that references this issue on Oct 28, 2026
  9. tae898 commented on Oct 29, 2026

    @tae898
    ContributorAuthor

    Verified on both JDKs with a SIFT repro: 100,000 vectors, EUCLIDEAN; 20 rows chosen at random get tenant = 1 and tag = 'small', every other row tenant = 0; tenant has a NOTUNIQUE index; 50 queries; the truth is the exact top 10 among the matching rows. The merge of #10808 (81c30122fc) and its parent never got CI jars on my side, so this compares c3af6d9028 (an ancestor of the merge's parent) with 9d9aa076ab (which contains the merge), on JDK 21.0.12 and 25.0.4.1:

    shape before c3af6d9028 after 9d9aa076ab
    queryNodes(idx, 10, $q) ... WHERE node.tenant = 1 (old post-filter form) 0 rows per query, 50 of 50 queries short 0 rows per query, 50 of 50 short, 0 wrong rows, recall@10 0.0000
    queryNodes(idx, 10, $q, {where: {tenant: 1}}) engine error: expects 3 arguments but got 4 10 rows per query, 0 of 50 short, 0 wrong rows, recall@10 1.0000
    queryNodes(idx, 10, $q, {where: "tag = 'small'"}) engine error: expects 3 arguments but got 4 10 rows per query, 0 of 50 short, 0 wrong rows, recall@10 1.0000
    queryNodes(idx, 10, $q, {where: {tenant: 0}}) (broad control) engine error: expects 3 arguments but got 4 10 rows per query, 0 of 50 short, 0 wrong rows, recall@10 0.9960

    Results match on both JDKs. Both pre-filter forms answer 10 rows for every query, all of them matching, with recall@10 1.0. The post-filter form returns nothing, because only 20 of 100,000 rows match.

    One observation, not a claim yet. The median time per query was 219 to 229 ms for both pre-filter shapes, against 1.0 to 1.1 ms for the broad control. That holds even though tenant is indexed (the tag shape has no index and costs the same). It looks like the graph search walks far to collect 10 matches when only 0.02% of the rows qualify. I will measure how this scales with selectivity before saying more. Thanks for the fix.

  10. added a commit that references this issue on Nov 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

documentationImprovements or additions to documentationenhancementNew feature or requestopencypherseverity:minorCosmetic, documentation, edge case, or a workaround existsvectorVector index and embedding search

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions