Pgvector is a well known extension for PostgreSQL. It provides a fast and robust vector type which is quite useful for heavy computations. Pgvector also provides a sparse version of a vector to save space.
This section covers how to use types provided by the extension with PG2.
First, install pgvector as the official readme file prescribes. Now that you
have it installed, try a simple table with the vector column:
(def conn
(jdbc/get-connection {...}))
(pg/query conn "create temp table test (id int, items vector)")
(pg/execute conn "insert into test values (1, '[1,2,3]')")
(pg/execute conn "insert into test values (2, '[1,2,3,4,5]')")
(pg/execute conn "select * from test order by id")
;; [{:id 1, :items "[1,2,3]"} {:id 2, :items "[1,2,3,4,5]"}]
It works, but we got the result unparsed: the :items field in each row is a
string. This is because, to take a custom type into account when encoding and
decoding data, you need to specify something. Namely, pass the :with-pgvector?
flag to the config map as follows:
(def config
{:host "127.0.0.1"
:port 5432
:user "test"
:password "test"
:database "test"
:with-pgvector? true})
(def conn
(jdbc/get-connection config))
Now the strings are parsed into a Clojure vector of double values:
(pg/execute conn "select * from test order by id")
[{:id 1, :items [1.0 2.0 3.0]}
{:id 2, :items [1.0 2.0 3.0 4.0 5.0]}]
To insert a vector, pass it as a Clojure vector as well:
(pg/execute conn "insert into test values ($1, $2)"
{:params [3 [1 2 3 4 5]]})
It can be also a lazy collection of numbers produced by a map call:
(pg/execute conn "insert into test values ($1, $2)"
{:params [4 (map inc [1 2 3 4 5])]})
The vector column above doesn't have an explicit size. Thus, vectors of any
size can be stored in that column. You can limit the size by providing it in
parentheses:
(pg/query conn "create temp table test2 (id int, items vector(5))")
Now if you pass a vector of a different size, you'll get an error response from the database:
(pg/execute conn "insert into test2 values (1, '[1,2,3]')")
;; Server error response: {severity=ERROR, code=22000, file=vector.c, line=77,
;; function=CheckExpectedDim, message=expected 5 dimensions, not 3,
;; verbosity=ERROR}
The vector type supports both text and binary modes of PostgreSQL wire
protocol.
The pgvector extension provides a special sparsevec type to store vectors
where only certain elements are filled. All the rest elements are considered as
zero. For example, you have a vector of 1000 items where the 3rd item is 42.001,
and 10th item is 99.123. Storing it as a native vector of 1000 double numbers is
inefficient. It can be written as follows which takes much less:
{3:42.001,10:99.123}/1000
The sparsevec Postgres type acts exactly like this: internally, it's a sort of
a map that stores the size (1000) and the {index -> value} mapping. An
important note is that indexes are counted from one, not zero (see the
README.md file of the extension for details).
PG2 provides a special wrapper for a sparse vector. A brief demo:
(pg/execute conn "create temp table test3 (id int, v sparsevec)")
(pg/execute conn "insert into test3 values (1, '{2:42.00001,7:99.00009}/9')")
(pg/execute conn "select * from test3")
;; [{:v <SparseVector {2:42.00001,7:99.00009}/9>, :id 1}]
The v field above is an instance of the org.pg.type.SparseVector
class. Let's look at it closer:
;; put it into a separate variable
(def -sv
(-> (pg/execute conn "select * from test3")
first
:v))
(type -sv)
org.pg.type.SparseVector
The -sv value has a number of interesting traits. To turn in into a native
Clojure map, just deref it:
@-sv
{:nnz 2, :index {1 42.00001, 6 99.00009}, :dim 9}
It mimics the nth access as the standard Clojure vector does:
(nth -sv 0) ;; 0.0
(nth -sv 1) ;; 42.00001
(nth -sv 2) ;; 0.0
To turn in into a native vector, just pass it into the vec function:
(vec -sv)
[0.0 42.00001 0.0 0.0 0.0 0.0 99.00009 0.0 0.0]
There are several ways you can insert a sparse vector into the database. First, pass an ordinary vector:
(pg/execute conn "insert into test3 values ($1, $2)"
{:params [2 [5 2 6 0 2 5 0 0]]})
Internally, zero values get eliminated, and the vector is transformed into a
SparseVector instance. Now read it back:
(pg/execute conn "select * from test3 where id = 2")
[{:v <SparseVector {1:5.0,2:2.0,3:6.0,5:2.0,6:5.0}/8>, :id 2}]
The second way is to pass a SparseVector instance produced by the
pg.type/->sparse-vector function. It accepts the size of the vector and a
mapping of {index => value}:
(require '[pg.type :as t])
(pg/execute conn "insert into test3 values ($1, $2)"
{:params [3 (t/->sparse-vector 9 {0 523.23423
7 623.52346})]})
Finally, you can pass a string representation of a sparse vector:
(pg/execute conn "insert into test3 values ($1, $2)"
{:params [3 "{1:5.0,2:2.0,3:6.0,5:2.0,6:5.0}/8"]})
Like the vector type, sparsevec can be also limited to a certain size:
create table ... (id int, items sparsevec(5))
The sparsevec type supports both binary and text Postgres wire protocol.
The text above assumes you have the pgvector extension installed globally
meaning it is hosted in the public schema. Sometimes though, extensions are
setup per schema. For example only a schema named sales has access to the
pgvector extension but nobody else.
If it's your case and you installed pgvector into a certain schema, the
standard :with-pgvector? flag won't work. By default, PG2 scans the pg_types
table for the public.vector and public.sparsevec types. Since the schema
name is not public but sales, you need to specify it by passing a special
option called :type-map. It's a map where keys are fully qualified type names
(either a keyword or a string), and values are predefined instances of the
IProcessor interface:
(def config
{:host "127.0.0.1"
:port 5432
:user "test"
:password "test"
:database "test"
:type-map {"sales.vector" t/vector
"sales.sparsevec" t/sparsevec}})
You can rely on keywords as well:
(def config
{:host "127.0.0.1"
:port 5432
:user "test"
:password "test"
:database "test"
:type-map {:sales/vector t/vector
:sales/sparsevec t/sparsevec}})
The t alias references the pg.type namespace.
Now if you install the extension into the statistics schema as well, add it
into the map:
(def config
{:host "127.0.0.1"
:port 5432
:user "test"
:password "test"
:database "test"
:type-map {:sales/vector t/vector
:sales/sparsevec t/sparsevec
:statistics/vector t/vector
:statistics/sparsevec t/sparsevec}})
Should you make a mistake in a fully qualified type name, it will be ignored, and you'll get value from the database unparsed. The actual value depends on the binary encoding and decoding options of a connection. By default, it uses text protocol so you'll get a string like "[1, 2, 3]". For binary encoding and decoding, you'll get a byte array that holds raw Postgres payload.
Can you improve this documentation?Edit on GitHub
cljdoc builds & hosts documentation for Clojure/Script libraries
| Ctrl+k | Jump to recent docs |
| ← | Move to previous article |
| → | Move to next article |
| Ctrl+/ | Jump to the search field |