Visitar URL original
openCypher: the Cypher 25 `SEARCH` clause is parsed and dropped, so a vector search without `SCORE AS` returns every node of the label · Issue #10776 · ArcadeData/arcadedb · GitHub
Skip to content

openCypher: the Cypher 25 SEARCH clause is parsed and dropped, so a vector search without SCORE AS returns every node of the label #10776

Closed

Description

@tae898

Problem

Neo4j 2026.01 added the Cypher 25 SEARCH subclause for vector indexes, and Neo4j 2026.04 deprecated db.index.vector.queryNodes() in its favour (SEARCH):

MATCH (d:Doc)
SEARCH d IN (VECTOR INDEX docs FOR $q WHERE d.lang = 'en' LIMIT 10) SCORE AS s
RETURN d.name, s

ArcadeDB's grammar has the rule (searchClause, Cypher25Parser.g4 L177-L179, used by matchClause at L233), but nothing builds or runs it: visitMatchClause in CypherASTBuilder never reads ctx.searchClause(). So the clause is parsed and dropped. Without SCORE AS that is a silent wrong answer: the MATCH runs as if SEARCH were not there and returns every node of the label, not the LIMIT nearest ones. With SCORE AS the query fails, because the score variable was never bound.

Evidence

Upstream main 1e30c60371, embedded, JDK 25, the program below: 50 Doc vertices with random 4-dimensional vectors, an LSM_VECTOR index Doc[embedding] (COSINE), one query vector $q.

query result
CALL db.index.vector.queryNodes('Doc[embedding]', 3, $q) YIELD node, score RETURN node.name AS name, score 3 rows, ok
MATCH (d:Doc) SEARCH d IN (VECTOR INDEX `Doc[embedding]` FOR $q LIMIT 3) RETURN d.name AS name 50 rows (every Doc), no error
MATCH (d:Doc) SEARCH d IN (VECTOR INDEX `Doc[embedding]` FOR $q LIMIT 3) SCORE AS s RETURN d.name AS name, s CommandSemanticException: UndefinedVariable: Variable 's' not defined
the same with WHERE d.name <> 'd0' inside the parentheses the same exception

Expected

SEARCH runs on the LSM_VECTOR index it names: FOR is the query vector, LIMIT is k, SCORE AS binds the similarity score (Neo4j returns 0.0 to 1.0, highest first, like queryNodes here), and a WHERE inside the parentheses filters within the index search, so LIMIT 10 returns 10 matching rows. That last part is the Neo4j-standard answer to #10685 (queryNodes filters only after the top k). Until it is implemented, refusing a SEARCH clause with a clear error would at least stop the wrong answer.

Related: #10678 asks for Neo4j's CREATE VECTOR INDEX (on main it is refused: Only standard, RANGE and TEXT index types are supported), #10685. I can implement SEARCH for vector indexes, with the in-index filter, in a PR if you agree.

CypherVectorProbe.java (run: javac -cp 'lib/*' CypherVectorProbe.java && java -cp 'lib/*:.' CypherVectorProbe /tmp/dbprobe)
// What ArcadeDB's openCypher does with Neo4j's vector syntax (2026-10-27): the db.index.vector.queryNodes procedure, the Cypher 25 SEARCH clause
// (Neo4j 2026.01+, the replacement of queryNodes since Neo4j 2026.04) and CREATE VECTOR INDEX. Prints one CHECK/INFO line per case; never exits nonzero
// on an unsupported feature (that is the finding), only on a setup failure.
import com.arcadedb.database.Database;
import com.arcadedb.database.DatabaseFactory;
import com.arcadedb.query.sql.executor.Result;
import com.arcadedb.query.sql.executor.ResultSet;
import java.util.*;

public class CypherVectorProbe {
  static List<Result> run(Database db, String q, Map<String, Object> p) {
    List<Result> out = new ArrayList<>();
    try (ResultSet rs = db.query("opencypher", q, p)) { while (rs.hasNext()) out.add(rs.next()); }
    return out;
  }

  public static void main(String[] a) {
    try (DatabaseFactory f = new DatabaseFactory(a[0])) {
      if (f.exists()) f.open().drop();
      try (Database db = f.create()) {
        final int n = 50, dim = 4; final Random r = new Random(7);
        db.command("sql", "CREATE VERTEX TYPE Doc"); db.command("sql", "CREATE PROPERTY Doc.name STRING"); db.command("sql", "CREATE PROPERTY Doc.embedding ARRAY_OF_FLOATS");
        db.transaction(() -> { for (int i = 0; i < n; i++) { float[] v = new float[dim]; for (int j = 0; j < dim; j++) v[j] = (float) r.nextGaussian();
          db.command("sql", "INSERT INTO Doc SET name = ?, embedding = ?", "d" + i, v); } });
        db.command("sql", "CREATE INDEX ON Doc (embedding) LSM_VECTOR METADATA {\"dimensions\": 4, \"similarity\": \"COSINE\"}");
        final List<Float> q = List.of(0.5f, -0.2f, 0.1f, 0.9f); final Map<String, Object> p = Map.of("q", q);
        String[][] cases = {
          {"queryNodes", "CALL db.index.vector.queryNodes('Doc[embedding]', 3, $q) YIELD node, score RETURN node.name AS name, score"},
          {"SEARCH", "MATCH (d:Doc) SEARCH d IN (VECTOR INDEX `Doc[embedding]` FOR $q LIMIT 3) SCORE AS s RETURN d.name AS name, s"},
          {"SEARCH, no SCORE", "MATCH (d:Doc) SEARCH d IN (VECTOR INDEX `Doc[embedding]` FOR $q LIMIT 3) RETURN d.name AS name"},
          {"SEARCH+WHERE", "MATCH (d:Doc) SEARCH d IN (VECTOR INDEX `Doc[embedding]` FOR $q WHERE d.name <> 'd0' LIMIT 3) SCORE AS s RETURN d.name AS name, s"},
        };
        for (String[] c : cases) {
          try { List<Result> rows = run(db, c[1], p);
            System.out.println("\n" + (rows.size() == 3 ? "CHECK ok   " : "CHECK FAIL ") + c[0] + ": " + rows.size() + " rows (expected 3 of " + n + ")" + (rows.isEmpty() ? "" : "; first " + rows.get(0).toJSON()));
          } catch (Exception e) { System.out.println("INFO unsupported " + c[0] + ": " + e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage()).split("\n")[0]); }
        }
        try { db.command("opencypher", "CREATE VECTOR INDEX v2 IF NOT EXISTS FOR (n:Doc2) ON n.embedding OPTIONS {indexConfig: {`vector.dimensions`: 4, `vector.similarity_function`: 'cosine'}}");
          System.out.println("INFO CREATE VECTOR INDEX accepted; Doc2 index exists: " + (db.getSchema().existsType("Doc2") ? db.getSchema().getType("Doc2").getAllIndexes(false).size() : 0));
        } catch (Exception e) { System.out.println("INFO unsupported CREATE VECTOR INDEX: " + e.getClass().getSimpleName() + ": " + String.valueOf(e.getMessage()).split("\n")[0]); }
      }
    }
    System.out.println("RESULT java=" + System.getProperty("java.version"));
  }
}

Activity

  1. added this to the 26.11.1 milestone on Oct 27, 2026
  2. self-assigned this
    on Oct 27, 2026
  3. added a commit that references this issue on Oct 27, 2026
  4. tae898 commented on Oct 27, 2026

    @tae898
    ContributorAuthor

    Verified on 8b99f38712 (upstream main, which contains the merge of #10783, 086c36e968), with jars from the CI build of that commit.

    The probe of this issue (50 Doc vertices, LSM_VECTOR COSINE), JDK 21.0.12 and 25.0.4, same lines on both:

    query before (1e30c60371) after (8b99f38712)
    MATCH ... SEARCH ... LIMIT 3 without SCORE AS 50 rows 3 rows
    the same with SCORE AS s UndefinedVariable: Variable 's' not defined 3 rows, top s 0.9480383 (equal to queryNodes' score)
    with WHERE d.name <> 'd0' inside the parentheses the same exception 3 rows

    At scale (SIFT 100k, an indexed grp property, 200 queries per selectivity, recall@10 against the exact top 10 over the matching rows, JDK 25), the merge build reproduces what the PR head showed: SEARCH ... WHERE d.grp < $t LIMIT 10 returns 10 rows at every selectivity, with recall@10 0.9955 (50%), 0.9995 (10%), 0.9995 (1%) and 1.0000 (0.1%); on 1993631cad, before the merge, the same query returned all 100,000 nodes. Thanks.

  5. robfrank commented on Oct 28, 2026

    @robfrank
    Collaborator

    Fixed in #10783. The Cypher 25 SEARCH ... IN (VECTOR INDEX ...) subclause of a MATCH was parsed and then dropped, so the query returned every node and the SCORE AS variable was undefined. It now runs the vector index search first, binds the node and its score, and the MATCH pattern and WHERE then filter those nearest results; the inner WHERE is applied per candidate and the search widens until k matching rows are found.

    Still open: #10782 (the widening stops at 100k candidates per index, so a very selective inner WHERE can return fewer than k rows); Neo4j-named indexes (CREATE VECTOR INDEX docs ...) are still refused, tracked in #10678.


    ❤️ 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.

  6. removed
    in progressThe team is actively working on this issue
    on Oct 28, 2026
  7. 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

bugopencypherseverity: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