Skip to content

[RFC] VECTOR support: cross-dialect API design #18452

Description

@wikirik-agent

Issue Creation Checklist

  • I understand that my issue will be automatically closed if I don't fill in the requested information
  • I have read the contribution guidelines

Feature Description

Describe the feature you'd like to see implemented

This RFC sets out the public API for vector support in Sequelize v7. #18227 by @prajalg started it, with an Oracle implementation. This RFC turns that work into an API that fits the other dialects too.

Everything below is a proposal open for discussion. @prajalg, and anyone else interested in vector support, please comment here: questions, objections and better ideas are all welcome. The aim is for this issue to be the one place future dialect PRs (pgvector, MariaDB, SQL Server, Db2, …) can point to.

Delivery plan

Vector support ships as a stack of Oracle-first PRs. Each one is reviewable and mergeable on its own:

  1. DataTypes.VECTOR + the Oracle implementation. feat(core): add VECTOR datatype support with Oracle vector integration #18227 would be reduced to this.
  2. sql.vectorDistance, the distance expression, with the Oracle implementation.
  3. Vector indexes, with the Oracle implementation.

Between these PRs, maintainers plan to prototype pgvector, and possibly other dialects, against the abstraction before PR 2 and PR 3 are final. That isn't expected from the PR author.

1. Data type

DataTypes.VECTOR                                                  // float32, dimensions optional where supported
DataTypes.VECTOR(1536)                                            // float32
DataTypes.VECTOR({ dimensions: 1536, elementType: 'int8' })
DataTypes.VECTOR({ dimensions: 1536, typedArray: true })          // read as Float32Array instead of number[]

type VectorElementType = 'float16' | 'float32' | 'float64' | 'int8' | 'binary';
  • Options. dimensions and elementType. The element type is a closed, typed union, so arbitrary strings aren't accepted. There's no positional element-type argument.
  • Default element type. float32, rendered explicitly. On Oracle, VECTOR(768) becomes VECTOR(768, FLOAT32), not Oracle's "any format".
  • Omitting dimensions. Allowed only where the dialect supports flexible dimensions (Oracle renders VECTOR(*, FLOAT32)). Elsewhere it throws.
  • Unknown options are rejected at runtime. For example, storage or sparse throw rather than being silently dropped. See also Validate options at runtime: reject unsupported and unknown options, stricter typings #18450.
  • Validation.
    • Values must be non-empty.
    • Every element must be a finite number.
    • The length must match dimensions when it's set.
    • int8 values must be in range.
    • Typed arrays must match the element type.
  • Not in scope for now.
    • Sparse vectors: a separate SPARSE_VECTOR type may come later.
    • Oracle's "any element type" column (VECTOR(*, *)).
    • An int32 element type (only Snowflake has one).

Capability flag. This uses the same pattern as DECIMAL and INTS. Maximum dimensions are tracked per element type, because they differ, e.g. SQL Server float32 1998 vs float16 3996.

supports.dataTypes.VECTOR: false | {
  elementTypes: Partial<Record<VectorElementType, { maxDimensions: number }>>;
  optionalDimensions: boolean;
};

2. Values in and out

  • Writes accept number[] or the typed array for the element type (Float32Array, Float64Array, Int8Array).
  • Reads return number[] by default on every dialect, so values are JSON-safe and match what embedding APIs produce. Each dialect's parseDatabaseValue normalises to this.
  • typedArray: true (per attribute) makes reads return the typed array for the element type. With the opt-in, float16 reads as Float32Array, because Float16Array isn't available on Node 22.
  • binary vectors are always a packed Uint8Array (dimensions / 8 bytes), on both read and write.
  • Change tracking. Values are compared element-wise across array kinds, so reloading a row and setting an equal array doesn't mark the attribute as changed.
  • Dialects that can't bind a vector as is wrap the bind parameter through the data type's getBindParamSql (e.g. CAST(@p AS VECTOR(n))), bulk inserts included.

3. Distance expression

sql.vectorDistance(left: Expression, right: Expression | VectorValue, metric: VectorMetric)

type VectorMetric = 'cosine' | 'euclidean' | 'euclideanSquared' | 'manhattan' | 'dot' | 'hamming' | 'jaccard';

await Document.findAll({
  order: [sql.vectorDistance(sql.attribute('embedding'), queryVector, 'cosine')],
  limit: 10,
});
  • A sql.* builder built on DialectAwareFn, like sql.random and sql.unquote. There are no sequelize.* instance methods and, for now, no per-metric aliases.
  • The metric is required. There's no "default metric", because on Oracle and MariaDB the default depends on which index exists.
  • Every metric returns a distance (smaller means closer, so ASC gives the nearest rows) on every dialect. 'dot' is the negative inner product, following Oracle DOT, SQL Server 'dot' and pgvector <#>. Where a dialect only has a similarity, the builder emulates the distance (e.g. Snowflake 1 - VECTOR_COSINE_SIMILARITY).
  • Operands.
    • The left operand must be an Expression, typically sql.attribute('embedding'), which handles field mapping, aliases and $include.attr$. A raw string throws with a hint.
    • The right operand can be an expression (column vs column works) or a literal vector. A literal vector is bound (or escaped) through the VECTOR data type, never inlined as text.
  • Rendering must be "bare" and match the index. No wrapper or cast on the column side; pgvector uses the operator form (<=>); Oracle uses VECTOR_DISTANCE(col, :q, COSINE). This way vector indexes can be used.
  • Capability flag, separate from the data type (MySQL Community has the type but no distance functions):
    supports.vectorDistance: false | { metrics: readonly VectorMetric[] };
  • No Sequelize-side check that a metric fits an element type (e.g. jaccard on float32). The database raises that error.
  • Raw sql.fn('VECTOR_DISTANCE', …) keeps working unchanged, as the escape hatch for dialect-specific forms.

4. Vector indexes

indexes: [{
  fields: ['embedding'],
  type: 'VECTOR',
  vector: {
    metric: 'cosine',
    method: 'hnsw',                                  // 'hnsw' | 'ivfflat' | 'diskann', per dialect
    parameters: { m: 16, efConstruction: 64 },       // portable names, translated per dialect
    dialectOptions: { targetAccuracy: 95 },          // dialect-only options, validated by the dialect
  },
}]

supports.index.vector: false | { methods: readonly string[]; metrics: readonly VectorMetric[] };
  • Options are nested under vector and typed.
  • Portable parameter names (m, efConstruction, lists) are mapped to each dialect's keyword, e.g. Oracle NEIGHBORS/EFCONSTRUCTION/NEIGHBOR PARTITIONS, pgvector m/ef_construction/lists, MariaDB M=. Anything else goes in dialectOptions.
  • Unsupported or unknown keys, and parameters that don't fit the chosen method, throw.
  • pgvector derives the operator class (e.g. vector_cosine_ops) from the column type and metric when the type is known, and requires an explicit operator otherwise.
  • Where supports.index.vector is false, core throws on type: 'VECTOR', instead of building a regular index or emitting invalid SQL.

5. Server versions and runtimes

Every dialect with vectors needs a server above Sequelize's minimum, or an extension: Oracle 23.4, MySQL 9.0, MariaDB 11.7, SQL Server 2025, Db2 12.1.2, pgvector, sqlite-vec.

For now, capability flags stay static, and each feature documents its minimum server version. Version-, extension- and runtime-aware capabilities are discussed separately in #18449.

6. Tests

  • Public SQL output (DDL, index SQL, distance SQL) is tested in core, on all dialects, with expectsql per-dialect expectations. Unsupported dialects are expected to throw.
  • Behaviour is tested by one shared core integration suite, gated on the capability flags. It covers round trips, read types, change tracking, distance semantics per metric, attribute mapping, includes, bulkCreate, sync({ alter }) and realistic (1536-dimension) queries, and skips when the server is too old or an extension is missing. Every dialect that enables vectors gets this suite automatically.
  • Dialect internals go in packages/<dialect>/src/*.test.ts. Dialect-only DB checks stay in packages/core/test/integration/dialects/<dialect>/ for now; see Dialect-package tests: DB-backed tests next to the dialect, and a reusable conformance suite for third-party dialects #18451.
  • New tests are written in TypeScript.

7. Dialect roadmap (after Oracle)

Order Dialect Notes
1 PostgreSQL + pgvector four types (vector, halfvec, bit, sparsevec), operators, operator classes
2 MariaDB 11.7+ cosine and euclidean only; one vector index per table
2 SQL Server 2025 bind needs a cast; approximate search only through VECTOR_SEARCH
3 Db2 12.1.2+ VECTOR(?, n, FLOAT32) constructor for binds; indexes need 12.1.5
4 MySQL 9 type only; distance functions are HeatWave-only
– Snowflake only if there's demand
– SQLite, IBM i not supported for now (sqlite-vec is an extension that works through virtual tables; IBM i has no vector type)

8. Later work (out of scope)

  • Approximate-search queries. For example, a findAll option that emits FETCH APPROX on Oracle and Db2 and SET LOCAL hnsw.ef_search on pgvector. SQL Server's VECTOR_SEARCH is a separate design question. A tracking issue will follow once the distance expression lands.
  • Sparse vectors, as SPARSE_VECTOR.
  • Similarity builders (positive dot product, cosine similarity), if people ask for them.

Describe why you would like this feature to be added to Sequelize

Vector search is now built into Oracle, PostgreSQL (pgvector), MariaDB, MySQL, SQL Server, Db2 and Snowflake. Storing embeddings next to relational data and querying them through the ORM is an increasingly common need. A single, consistent API, one where the same findAll means the same thing and can use an index on every database, only works if it's designed across dialects from the start, before the first implementation freezes it.

Is this feature dialect-specific?

  • No. This feature is relevant to Sequelize as a whole.
  • Yes. This feature only applies to the following dialect(s):

Would you be willing to resolve this issue by submitting a Pull Request?

  • Yes, I have the time and I know how to start.
  • Yes, I have the time but I will need guidance.
  • No, I don't have the time, but my company or I are supporting Sequelize through donations on OpenCollective.
  • No, I don't have the time, and I understand that I will need to wait until someone from the community or maintainers is interested in implementing my feature.

The Oracle implementation is in progress in #18227.


Indicate your interest in the addition of this feature by adding the 👍 reaction. Comments such as "+1" will be removed.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCRequest for comments regarding breaking or large changespending-approvalBug reports that have not been verified yet, or feature requests that have not been accepted yet

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions