Compare commits

..

2 Commits

Author SHA1 Message Date
Andrew Kane
75cf54a1e2 Updated changelog [skip ci] 2024-03-11 19:53:03 -07:00
Andrew Kane
569fd36396 Improved performance of parallel HNSW index builds 2024-03-11 18:32:39 -07:00
10 changed files with 18 additions and 427 deletions

View File

@@ -73,7 +73,6 @@ jobs:
postgres-version: 14 postgres-version: 14
- run: | - run: |
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && ^ call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat" && ^
cd %TEMP% && ^
nmake /NOLOGO /F Makefile.win && ^ nmake /NOLOGO /F Makefile.win && ^
nmake /NOLOGO /F Makefile.win install && ^ nmake /NOLOGO /F Makefile.win install && ^
nmake /NOLOGO /F Makefile.win installcheck && ^ nmake /NOLOGO /F Makefile.win installcheck && ^

View File

@@ -1,7 +1,3 @@
## 0.7.0 (unreleased)
- Added subscript function for vectors
## 0.6.2 (unreleased) ## 0.6.2 (unreleased)
- Reduced lock contention with parallel HNSW index builds - Reduced lock contention with parallel HNSW index builds

110
README.md
View File

@@ -26,7 +26,7 @@ make
make install # may need sudo make install # may need sudo
``` ```
See the [installation notes](#installation-notes---linux-and-mac) if you run into issues See the [installation notes](#installation-notes) if you run into issues
You can also install it with [Docker](#docker), [Homebrew](#homebrew), [PGXN](#pgxn), [APT](#apt), [Yum](#yum), [pkg](#pkg), or [conda-forge](#conda-forge), and it comes preinstalled with [Postgres.app](#postgresapp) and many [hosted providers](#hosted-postgres). There are also instructions for [GitHub Actions](https://github.com/pgvector/setup-pgvector). You can also install it with [Docker](#docker), [Homebrew](#homebrew), [PGXN](#pgxn), [APT](#apt), [Yum](#yum), [pkg](#pkg), or [conda-forge](#conda-forge), and it comes preinstalled with [Postgres.app](#postgresapp) and many [hosted providers](#hosted-postgres). There are also instructions for [GitHub Actions](https://github.com/pgvector/setup-pgvector).
@@ -44,15 +44,12 @@ Then use `nmake` to build:
```cmd ```cmd
set "PGROOT=C:\Program Files\PostgreSQL\16" set "PGROOT=C:\Program Files\PostgreSQL\16"
cd %TEMP%
git clone --branch v0.6.1 https://github.com/pgvector/pgvector.git git clone --branch v0.6.1 https://github.com/pgvector/pgvector.git
cd pgvector cd pgvector
nmake /F Makefile.win nmake /F Makefile.win
nmake /F Makefile.win install nmake /F Makefile.win install
``` ```
See the [installation notes](#installation-notes---windows) if you run into issues
You can also install it with [Docker](#docker) or [conda-forge](#conda-forge). You can also install it with [Docker](#docker) or [conda-forge](#conda-forge).
## Getting Started ## Getting Started
@@ -413,39 +410,13 @@ You can use [Reciprocal Rank Fusion](https://github.com/pgvector/pgvector-python
## Performance ## Performance
### Tuning
Use a tool like [PgTune](https://pgtune.leopard.in.ua/) to set initial values for Postgres server parameters.
### Loading
Use `COPY` for bulk loading data ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/bulk_loading.py)).
```sql
COPY items (embedding) FROM STDIN WITH (FORMAT BINARY);
```
Add any indexes *after* loading the initial data for best performance.
### Indexing
See index build time for [HNSW](#index-build-time) and [IVFFlat](#index-build-time-1).
In production environments, create indexes concurrently to avoid blocking writes.
```sql
CREATE INDEX CONCURRENTLY ...
```
### Querying
Use `EXPLAIN ANALYZE` to debug performance. Use `EXPLAIN ANALYZE` to debug performance.
```sql ```sql
EXPLAIN ANALYZE SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5; EXPLAIN ANALYZE SELECT * FROM items ORDER BY embedding <-> '[3,1,2]' LIMIT 5;
``` ```
#### Exact Search ### Exact Search
To speed up queries without an index, increase `max_parallel_workers_per_gather`. To speed up queries without an index, increase `max_parallel_workers_per_gather`.
@@ -459,7 +430,7 @@ If vectors are normalized to length 1 (like [OpenAI embeddings](https://platform
SELECT * FROM items ORDER BY embedding <#> '[3,1,2]' LIMIT 5; SELECT * FROM items ORDER BY embedding <#> '[3,1,2]' LIMIT 5;
``` ```
#### Approximate Search ### Approximate Search
To speed up queries with an IVFFlat index, increase the number of inverted lists (at the expense of recall). To speed up queries with an IVFFlat index, increase the number of inverted lists (at the expense of recall).
@@ -467,7 +438,7 @@ To speed up queries with an IVFFlat index, increase the number of inverted lists
CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 1000); CREATE INDEX ON items USING ivfflat (embedding vector_l2_ops) WITH (lists = 1000);
``` ```
### Vacuuming ## Vacuuming
Vacuuming can take a while for HNSW indexes. Speed it up by reindexing first. Vacuuming can take a while for HNSW indexes. Speed it up by reindexing first.
@@ -476,41 +447,6 @@ REINDEX INDEX CONCURRENTLY index_name;
VACUUM table_name; VACUUM table_name;
``` ```
## Monitoring
Monitor performance with [pg_stat_statements](https://www.postgresql.org/docs/current/pgstatstatements.html) (be sure to add it to `shared_preload_libraries`).
```sql
CREATE EXTENSION pg_stat_statements;
```
Get the most time-consuming queries with:
```sql
SELECT query, calls, ROUND((total_plan_time + total_exec_time) / calls) AS avg_time_ms,
ROUND((total_plan_time + total_exec_time) / 60000) AS total_time_min
FROM pg_stat_statements ORDER BY total_plan_time + total_exec_time DESC LIMIT 20;
```
Note: Replace `total_plan_time + total_exec_time` with `total_time` for Postgres < 13
Monitor recall by comparing results from approximate search with exact search.
```sql
BEGIN;
SET LOCAL enable_indexscan = off; -- use exact search
SELECT ...
COMMIT;
```
## Scaling
Scale pgvector the same way you scale Postgres.
Scale vertically by increasing memory, CPU, and storage on a single instance. Use existing tools to [tune parameters](#tuning) and [monitor performance](#monitoring).
Scale horizontally with [replicas](https://www.postgresql.org/docs/current/hot-standby.html), or use [Citus](https://github.com/citusdata/citus) or another approach for sharding ([example](https://github.com/pgvector/pgvector-python/blob/master/examples/citus.py)).
## Languages ## Languages
Use pgvector from any language with a Postgres client. You can even generate and store vectors in one language and query them in another. Use pgvector from any language with a Postgres client. You can even generate and store vectors in one language and query them in another.
@@ -616,17 +552,7 @@ SELECT pg_size_pretty(pg_relation_size('index_name'));
#### Why isnt a query using an index? #### Why isnt a query using an index?
The query needs to have an `ORDER BY` and `LIMIT`, and the `ORDER BY` must be the result of a distance operator, not an expression. The cost estimation in pgvector < 0.4.3 does not always work well with the planner. You can encourage the planner to use an index for a query with:
```sql
-- index
ORDER BY embedding <=> '[3,1,2]' LIMIT 5;
-- no index
ORDER BY 1 - (embedding <=> '[3,1,2]') DESC LIMIT 5;
```
You can encourage the planner to use an index for a query with:
```sql ```sql
BEGIN; BEGIN;
@@ -704,7 +630,7 @@ Function | Description | Added
avg(vector) → vector | average | avg(vector) → vector | average |
sum(vector) → vector | sum | 0.5.0 sum(vector) → vector | sum | 0.5.0
## Installation Notes - Linux and Mac ## Installation Notes
### Postgres Location ### Postgres Location
@@ -746,24 +672,12 @@ If compilation fails and the output includes `warning: no such sysroot directory
### Portability ### Portability
By default, pgvector compiles with `-march=native` on some platforms for best performance. However, this can lead to `Illegal instruction` errors if trying to run the compiled extension on a different machine.
To compile for portability, use: To compile for portability, use:
```sh ```sh
make OPTFLAGS="" make OPTFLAGS=""
``` ```
## Installation Notes - Windows
### Missing Header
If compilation fails with `Cannot open include file: 'postgres.h': No such file or directory`, make sure `PGROOT` is correct.
### Permissions
If installation fails with `Access is denied`, re-run the installation instructions as an administrator.
## Additional Installation Methods ## Additional Installation Methods
### Docker ### Docker
@@ -948,12 +862,6 @@ make installcheck REGRESS=functions # regression test
make prove_installcheck PROVE_TESTS=test/t/001_ivfflat_wal.pl # TAP test make prove_installcheck PROVE_TESTS=test/t/001_ivfflat_wal.pl # TAP test
``` ```
To enable assertions:
```sh
make clean && PG_CFLAGS="-DUSE_ASSERT_CHECKING" make && make install
```
To enable benchmarking: To enable benchmarking:
```sh ```sh
@@ -966,6 +874,12 @@ To show memory usage:
make clean && PG_CFLAGS="-DHNSW_MEMORY -DIVFFLAT_MEMORY" make && make install make clean && PG_CFLAGS="-DHNSW_MEMORY -DIVFFLAT_MEMORY" make && make install
``` ```
To enable assertions:
```sh
make clean && PG_CFLAGS="-DUSE_ASSERT_CHECKING" make && make install
```
To get k-means metrics: To get k-means metrics:
```sh ```sh

View File

@@ -1,7 +0,0 @@
-- complain if script is sourced in psql, rather than via CREATE EXTENSION
\echo Use "ALTER EXTENSION vector UPDATE TO '0.7.0'" to load this file. \quit
CREATE FUNCTION vector_subscript(internal) RETURNS internal
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
ALTER TYPE vector SET (SUBSCRIPT = vector_subscript);

View File

@@ -20,16 +20,12 @@ CREATE FUNCTION vector_recv(internal, oid, integer) RETURNS vector
CREATE FUNCTION vector_send(vector) RETURNS bytea CREATE FUNCTION vector_send(vector) RETURNS bytea
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE; AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE FUNCTION vector_subscript(internal) RETURNS internal
AS 'MODULE_PATHNAME' LANGUAGE C IMMUTABLE STRICT PARALLEL SAFE;
CREATE TYPE vector ( CREATE TYPE vector (
INPUT = vector_in, INPUT = vector_in,
OUTPUT = vector_out, OUTPUT = vector_out,
TYPMOD_IN = vector_typmod_in, TYPMOD_IN = vector_typmod_in,
RECEIVE = vector_recv, RECEIVE = vector_recv,
SEND = vector_send, SEND = vector_send,
SUBSCRIPT = vector_subscript,
STORAGE = external STORAGE = external
); );

View File

@@ -129,7 +129,7 @@ HnswPtrDeclare(HnswNeighborArray, HnswNeighborArrayRelptr, HnswNeighborArrayPtr)
HnswPtrDeclare(HnswNeighborArrayPtr, HnswNeighborsRelptr, HnswNeighborsPtr); HnswPtrDeclare(HnswNeighborArrayPtr, HnswNeighborsRelptr, HnswNeighborsPtr);
HnswPtrDeclare(char, DatumRelptr, DatumPtr); HnswPtrDeclare(char, DatumRelptr, DatumPtr);
struct HnswElementData typedef struct HnswElementData
{ {
HnswElementPtr next; HnswElementPtr next;
ItemPointerData heaptids[HNSW_HEAPTIDS]; ItemPointerData heaptids[HNSW_HEAPTIDS];
@@ -144,7 +144,7 @@ struct HnswElementData
BlockNumber neighborPage; BlockNumber neighborPage;
DatumPtr value; DatumPtr value;
LWLock lock; LWLock lock;
}; } HnswElementData;
typedef HnswElementData * HnswElement; typedef HnswElementData * HnswElement;
@@ -155,12 +155,12 @@ typedef struct HnswCandidate
bool closer; bool closer;
} HnswCandidate; } HnswCandidate;
struct HnswNeighborArray typedef struct HnswNeighborArray
{ {
int length; int length;
bool closerSet; bool closerSet;
HnswCandidate items[FLEXIBLE_ARRAY_MEMBER]; HnswCandidate items[FLEXIBLE_ARRAY_MEMBER];
}; } HnswNeighborArray;
typedef struct HnswPairingHeapNode typedef struct HnswPairingHeapNode
{ {

View File

@@ -436,7 +436,7 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
int m = buildstate->m; int m = buildstate->m;
char *base = buildstate->hnswarea; char *base = buildstate->hnswarea;
/* Wait if another process needs exclusive lock on entry lock */ /* Wait if another process needs exclusive lock */
LWLockAcquire(entryWaitLock, LW_EXCLUSIVE); LWLockAcquire(entryWaitLock, LW_EXCLUSIVE);
LWLockRelease(entryWaitLock); LWLockRelease(entryWaitLock);
@@ -450,7 +450,7 @@ InsertTupleInMemory(HnswBuildState * buildstate, HnswElement element)
/* Release shared lock */ /* Release shared lock */
LWLockRelease(entryLock); LWLockRelease(entryLock);
/* Tell other processes to wait and get exclusive lock */ /* Get exclusive lock */
LWLockAcquire(entryWaitLock, LW_EXCLUSIVE); LWLockAcquire(entryWaitLock, LW_EXCLUSIVE);
LWLockAcquire(entryLock, LW_EXCLUSIVE); LWLockAcquire(entryLock, LW_EXCLUSIVE);
LWLockRelease(entryWaitLock); LWLockRelease(entryWaitLock);

View File

@@ -4,18 +4,11 @@
#include "catalog/pg_type.h" #include "catalog/pg_type.h"
#include "common/shortest_dec.h" #include "common/shortest_dec.h"
#include "executor/execExpr.h"
#include "fmgr.h" #include "fmgr.h"
#include "hnsw.h" #include "hnsw.h"
#include "ivfflat.h" #include "ivfflat.h"
#include "lib/stringinfo.h" #include "lib/stringinfo.h"
#include "libpq/pqformat.h" #include "libpq/pqformat.h"
#include "nodes/makefuncs.h"
#include "nodes/nodeFuncs.h"
#include "nodes/subscripting.h"
#include "parser/parse_coerce.h"
#include "parser/parse_expr.h"
#include "parser/parse_node.h"
#include "port.h" /* for strtof() */ #include "port.h" /* for strtof() */
#include "utils/array.h" #include "utils/array.h"
#include "utils/builtins.h" #include "utils/builtins.h"
@@ -425,180 +418,6 @@ vector_send(PG_FUNCTION_ARGS)
PG_RETURN_BYTEA_P(pq_endtypsend(&buf)); PG_RETURN_BYTEA_P(pq_endtypsend(&buf));
} }
/*
* Transform the subscript expressions
*/
static void
vector_subscript_transform(SubscriptingRef *sbsref, List *indirection, ParseState *pstate, bool isSlice, bool isAssignment)
{
A_Indices *ai;
Node *subexpr;
if (list_length(indirection) != 1)
ereport(ERROR,
(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
errmsg("vector allows only one subscript"),
parser_errposition(pstate,
exprLocation((Node *) indirection))));
ai = linitial_node(A_Indices, indirection);
if (isSlice)
{
if (ai->lidx)
{
subexpr = transformExpr(pstate, ai->lidx, pstate->p_expr_kind);
/* If it's not int4 already, try to coerce */
subexpr = coerce_to_target_type(pstate,
subexpr, exprType(subexpr),
INT4OID, -1,
COERCION_ASSIGNMENT,
COERCE_IMPLICIT_CAST,
-1);
if (subexpr == NULL)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("vector subscript must have type integer"),
parser_errposition(pstate, exprLocation(ai->lidx))));
}
else if (!ai->is_slice)
{
/* Make a constant 1 */
subexpr = (Node *) makeConst(INT4OID,
-1,
InvalidOid,
sizeof(int32),
Int32GetDatum(1),
false,
true); /* pass by value */
}
else
{
/* Slice with omitted lower bound, put NULL into the list */
subexpr = NULL;
}
sbsref->reflowerindexpr = list_make1(subexpr);
}
else
Assert(ai->lidx == NULL && !ai->is_slice);
if (ai->uidx)
{
subexpr = transformExpr(pstate, ai->uidx, pstate->p_expr_kind);
/* If it's not int4 already, try to coerce */
subexpr = coerce_to_target_type(pstate,
subexpr, exprType(subexpr),
INT4OID, -1,
COERCION_ASSIGNMENT,
COERCE_IMPLICIT_CAST,
-1);
if (subexpr == NULL)
ereport(ERROR,
(errcode(ERRCODE_DATATYPE_MISMATCH),
errmsg("array subscript must have type integer"),
parser_errposition(pstate, exprLocation(ai->uidx))));
}
else
{
/* Slice with omitted upper bound, put NULL into the list */
Assert(isSlice && ai->is_slice);
subexpr = NULL;
}
sbsref->refupperindexpr = list_make1(subexpr);
if (isSlice)
sbsref->refrestype = sbsref->refcontainertype;
else
sbsref->refrestype = FLOAT4OID;
}
/*
* Fetch a vector element
*/
static void
vector_subscript_fetch(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
{
SubscriptingRefState *sbsrefstate = op->d.sbsref.state;
Vector *vec = DatumGetVector(*op->resvalue);
int index = DatumGetInt32(sbsrefstate->upperindex[0]);
if (index < 1 || index > vec->dim)
*op->resnull = true;
else
*op->resvalue = Float4GetDatum(vec->x[index - 1]);
}
/*
* Fetch a vector slice
*/
static void
vector_subscript_fetch_slice(ExprState *state, ExprEvalStep *op, ExprContext *econtext)
{
SubscriptingRefState *sbsrefstate = op->d.sbsref.state;
if (sbsrefstate->upperprovided[0] && sbsrefstate->upperindexnull[0])
*op->resnull = true;
else if (sbsrefstate->lowerprovided[0] && sbsrefstate->lowerindexnull[0])
*op->resnull = true;
else
{
Vector *vec = DatumGetVector(*op->resvalue);
int upperindex = sbsrefstate->upperprovided[0] ? DatumGetInt32(sbsrefstate->upperindex[0]) : vec->dim;
int lowerindex = sbsrefstate->lowerprovided[0] ? DatumGetInt32(sbsrefstate->lowerindex[0]) : 1;
int dim;
Vector *result;
if (upperindex > vec->dim)
upperindex = vec->dim;
if (lowerindex < 1)
lowerindex = 1;
dim = upperindex - lowerindex + 1;
CheckDim(dim);
result = InitVector(dim);
for (int i = 0; i < dim; i++)
result->x[i] = vec->x[lowerindex + i - 1];
*op->resvalue = PointerGetDatum(result);
}
}
/*
* Set up execution state for a vector subscript operation
*/
static void
vector_exec_setup(const SubscriptingRef *sbsref, SubscriptingRefState *sbsrefstate, SubscriptExecSteps *methods)
{
methods->sbs_check_subscripts = NULL;
if (sbsrefstate->numlower != 0)
methods->sbs_fetch = vector_subscript_fetch_slice;
else
methods->sbs_fetch = vector_subscript_fetch;
methods->sbs_assign = NULL;
methods->sbs_fetch_old = NULL;
}
/*
* Subscript handler
*/
PGDLLEXPORT PG_FUNCTION_INFO_V1(vector_subscript);
Datum
vector_subscript(PG_FUNCTION_ARGS)
{
static const SubscriptRoutines sbsroutines = {
.transform = vector_subscript_transform,
.exec_setup = vector_exec_setup,
.fetch_strict = true, /* fetch returns NULL for NULL inputs */
.fetch_leakproof = true, /* fetch returns NULL for bad subscript */
.store_leakproof = false /* ... but assignment throws error */
};
PG_RETURN_POINTER(&sbsroutines);
}
/* /*
* Convert vector to vector * Convert vector to vector
* This is needed to check the type modifier * This is needed to check the type modifier

View File

@@ -24,112 +24,6 @@ SELECT '[1e37]'::vector * '[1e37]';
ERROR: value out of range: overflow ERROR: value out of range: overflow
SELECT '[1e-37]'::vector * '[1e-37]'; SELECT '[1e-37]'::vector * '[1e-37]';
ERROR: value out of range: underflow ERROR: value out of range: underflow
SELECT ('[1,2,3]'::vector)[0];
vector
--------
(1 row)
SELECT ('[1,2,3]'::vector)[1];
vector
--------
1
(1 row)
SELECT ('[1,2,3]'::vector)[2];
vector
--------
2
(1 row)
SELECT ('[1,2,3]'::vector)[3];
vector
--------
3
(1 row)
SELECT ('[1,2,3]'::vector)[4];
vector
--------
(1 row)
SELECT ('[1,2,3]'::vector)[1:1];
vector
--------
[1]
(1 row)
SELECT ('[1,2,3]'::vector)[1:2];
vector
--------
[1,2]
(1 row)
SELECT ('[1,2,3]'::vector)[2:4];
vector
--------
[2,3]
(1 row)
SELECT ('[1,2,3]'::vector)[-2:2];
vector
--------
[1,2]
(1 row)
SELECT ('[1,2,3]'::vector)[2:1];
ERROR: vector must have at least 1 dimension
SELECT ('[1,2,3]'::vector)[:];
vector
---------
[1,2,3]
(1 row)
SELECT ('[1,2,3]'::vector)[:2];
vector
--------
[1,2]
(1 row)
SELECT ('[1,2,3]'::vector)[2:];
vector
--------
[2,3]
(1 row)
SELECT ('[1,2,3]'::vector)[:4];
vector
---------
[1,2,3]
(1 row)
SELECT ('[1,2,3]'::vector)[-2:];
vector
---------
[1,2,3]
(1 row)
SELECT ('[1,2,3]'::vector)[NULL];
vector
--------
(1 row)
SELECT ('[1,2,3]'::vector)[NULL:2];
vector
--------
(1 row)
SELECT ('[1,2,3]'::vector)[2:NULL];
vector
--------
(1 row)
SELECT ('[1,2,3]'::vector)[1][1];
ERROR: vector allows only one subscript
SELECT '[1,2,3]'::vector = '[1,2,3]'; SELECT '[1,2,3]'::vector = '[1,2,3]';
?column? ?column?
---------- ----------

View File

@@ -6,26 +6,6 @@ SELECT '[1,2,3]'::vector * '[4,5,6]';
SELECT '[1e37]'::vector * '[1e37]'; SELECT '[1e37]'::vector * '[1e37]';
SELECT '[1e-37]'::vector * '[1e-37]'; SELECT '[1e-37]'::vector * '[1e-37]';
SELECT ('[1,2,3]'::vector)[0];
SELECT ('[1,2,3]'::vector)[1];
SELECT ('[1,2,3]'::vector)[2];
SELECT ('[1,2,3]'::vector)[3];
SELECT ('[1,2,3]'::vector)[4];
SELECT ('[1,2,3]'::vector)[1:1];
SELECT ('[1,2,3]'::vector)[1:2];
SELECT ('[1,2,3]'::vector)[2:4];
SELECT ('[1,2,3]'::vector)[-2:2];
SELECT ('[1,2,3]'::vector)[2:1];
SELECT ('[1,2,3]'::vector)[:];
SELECT ('[1,2,3]'::vector)[:2];
SELECT ('[1,2,3]'::vector)[2:];
SELECT ('[1,2,3]'::vector)[:4];
SELECT ('[1,2,3]'::vector)[-2:];
SELECT ('[1,2,3]'::vector)[NULL];
SELECT ('[1,2,3]'::vector)[NULL:2];
SELECT ('[1,2,3]'::vector)[2:NULL];
SELECT ('[1,2,3]'::vector)[1][1];
SELECT '[1,2,3]'::vector = '[1,2,3]'; SELECT '[1,2,3]'::vector = '[1,2,3]';
SELECT '[1,2,3]'::vector = '[1,2]'; SELECT '[1,2,3]'::vector = '[1,2]';