Skip to content

Commit 4f95724

Browse files
committed
First commit
0 parents  commit 4f95724

11 files changed

Lines changed: 358 additions & 0 deletions

File tree

.github/workflows/build.yml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: build
2+
on: [push, pull_request]
3+
jobs:
4+
build:
5+
if: "!contains(github.event.head_commit.message, '[skip ci]')"
6+
runs-on: ubuntu-latest
7+
steps:
8+
- uses: actions/checkout@v2
9+
- uses: actions/setup-node@v2
10+
- run: npm install
11+
- uses: ankane/setup-postgres@v1
12+
with:
13+
database: pgvector_node_test
14+
- run: |
15+
sudo apt-get update && sudo apt-get install postgresql-server-dev-13
16+
git clone --branch v0.1.7 https://github.com/ankane/pgvector.git
17+
cd pgvector
18+
make
19+
sudo make install
20+
- run: npm test

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
/node_modules/
2+
/package-lock.json

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
## 0.1.0 (unreleased)
2+
3+
- First release

LICENSE.txt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) 2021 Andrew Kane
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in
13+
all copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21+
THE SOFTWARE.

README.md

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
# pgvector-node
2+
3+
[pgvector](https://github.com/ankane/pgvector) support for Node.js
4+
5+
Supports [Sequelize](https://github.com/sequelize/sequelize) and [node-postgres](https://github.com/brianc/node-postgres)
6+
7+
[![Build Status](https://github.com/ankane/pgvector-node/workflows/build/badge.svg?branch=master)](https://github.com/ankane/pgvector-node/actions)
8+
9+
## Installation
10+
11+
Run:
12+
13+
```sh
14+
npm install pgvector
15+
```
16+
17+
And follow the instructions for your database library:
18+
19+
- [Sequelize](#sequelize)
20+
- [node-postgres](#node-postgres)
21+
22+
## Sequelize
23+
24+
Register the type
25+
26+
```js
27+
const { Sequelize } = require('sequelize');
28+
const pgvector = require('pgvector/sequelize');
29+
30+
pgvector.registerType(Sequelize);
31+
```
32+
33+
Add a vector field
34+
35+
```js
36+
Item.init({
37+
factors: {
38+
type: DataTypes.VECTOR(3)
39+
}
40+
}, ...);
41+
```
42+
43+
Insert a vector
44+
45+
```js
46+
await Item.create({factors: [1, 2, 3]});
47+
```
48+
49+
Get the nearest neighbors to a vector
50+
51+
```js
52+
const items = await Item.findAll({
53+
order: [sequelize.literal(`factors <-> '[1, 2, 3]'`)],
54+
limit: 5
55+
});
56+
```
57+
58+
## node-postgres
59+
60+
Register the type
61+
62+
```js
63+
const pgvector = require('pgvector/pg');
64+
65+
await pgvector.registerType(client);
66+
```
67+
68+
Insert a vector
69+
70+
```js
71+
const factors = [1, 2, 3];
72+
await client.query('INSERT INTO items (factors) VALUES ($1)', [pgvector.toSql(factors)]);
73+
```
74+
75+
Get the nearest neighbors to a vector
76+
77+
```js
78+
const result = await client.query('SELECT * FROM items ORDER BY factors <-> $1 LIMIT 5', [pgvector.toSql(factors)]);
79+
```
80+
81+
## History
82+
83+
View the [changelog](https://github.com/ankane/pgvector-node/blob/master/CHANGELOG.md)
84+
85+
## Contributing
86+
87+
Everyone is encouraged to help improve this project. Here are a few ways you can help:
88+
89+
- [Report bugs](https://github.com/ankane/pgvector-node/issues)
90+
- Fix bugs and [submit pull requests](https://github.com/ankane/pgvector-node/pulls)
91+
- Write, clarify, or fix documentation
92+
- Suggest or add new features
93+
94+
To get started with development:
95+
96+
```sh
97+
git clone https://github.com/ankane/pgvector-node.git
98+
cd pgvector-node
99+
npm install
100+
npm test
101+
```

package.json

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
{
2+
"name": "pgvector",
3+
"version": "0.1.0",
4+
"description": "pgvector support for Node.js",
5+
"homepage": "https://github.com/ankane/pgvector-node",
6+
"license": "MIT",
7+
"authors": [
8+
"ankane"
9+
],
10+
"repository": {
11+
"type": "git",
12+
"url": "https://github.com/ankane/pgvector-node.js"
13+
},
14+
"exports": {
15+
"./pg": "./pg/index.js",
16+
"./sequelize": "./sequelize/index.js",
17+
"./package.json": "./package.json"
18+
},
19+
"files": [
20+
"pg",
21+
"sequelize",
22+
"utils"
23+
],
24+
"engines": {
25+
"node": ">= 12"
26+
},
27+
"scripts": {
28+
"test": "jest"
29+
},
30+
"devDependencies": {
31+
"jest": "^27.0.4",
32+
"pg": "^8.6.0",
33+
"sequelize": "^6.6.2"
34+
}
35+
}

pg/index.js

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
const utils = require('../utils');
2+
3+
async function registerType(client) {
4+
const result = await client.query('SELECT typname, oid, typarray FROM pg_type WHERE typname = $1', ['vector']);
5+
if (result.rowCount < 1) {
6+
throw new Error('vector type not found in the database');
7+
}
8+
const oid = result.rows[0].oid;
9+
client.setTypeParser(oid, 'text', function(value) {
10+
return utils.fromSql(value);
11+
});
12+
}
13+
14+
function toSql(value) {
15+
if (!Array.isArray(value)) {
16+
throw new Error('expected array');
17+
}
18+
return utils.toSql(value);
19+
}
20+
21+
module.exports = {registerType, toSql};

pg/index.test.js

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
const { Client } = require('pg');
2+
const pgvector = require('./index');
3+
4+
const client = new Client({database: 'pgvector_node_test'});
5+
6+
beforeAll(async () => {
7+
await client.connect();
8+
const sql = `
9+
CREATE EXTENSION IF NOT EXISTS vector;
10+
DROP TABLE IF EXISTS items;
11+
CREATE TABLE IF NOT EXISTS items (
12+
id serial primary key,
13+
factors vector(3)
14+
);
15+
`;
16+
await client.query(sql);
17+
await pgvector.registerType(client);
18+
});
19+
20+
afterAll(async () => {
21+
await client.end();
22+
});
23+
24+
beforeEach(async () => {
25+
await client.query('DELETE FROM items');
26+
});
27+
28+
test('works', async () => {
29+
await client.query('INSERT INTO items (factors) VALUES ($1)', [pgvector.toSql([1, 2, 3])]);
30+
const { rows } = await client.query('SELECT * FROM items ORDER BY factors <-> $1 LIMIT 5', [pgvector.toSql([1, 2, 3])]);
31+
expect(rows[0].factors).toStrictEqual([1, 2, 3]);
32+
});
33+
34+
test('bad object', () => {
35+
expect(() => {
36+
pgvector.toSql({hello: 'world'});
37+
}).toThrowError('expected array');
38+
});

sequelize/index.js

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
const util = require('util');
2+
const utils = require('../utils');
3+
4+
function registerType(Sequelize) {
5+
const DataTypes = Sequelize.DataTypes;
6+
const PgTypes = DataTypes.postgres;
7+
const ABSTRACT = DataTypes.ABSTRACT.prototype.constructor;
8+
9+
class VECTOR extends ABSTRACT {
10+
constructor(dimensions) {
11+
super();
12+
this._dimensions = dimensions;
13+
}
14+
15+
toSql() {
16+
if (this._dimensions === undefined) {
17+
return 'VECTOR';
18+
}
19+
if (!Number.isInteger(this._dimensions)) {
20+
throw new Error('expected integer');
21+
}
22+
return util.format('VECTOR(%d)', this._dimensions);
23+
}
24+
25+
_stringify(value) {
26+
return utils.toSql(value);
27+
}
28+
29+
static parse(value) {
30+
return utils.fromSql(value);
31+
}
32+
}
33+
34+
VECTOR.prototype.key = VECTOR.key = 'vector';
35+
36+
DataTypes.VECTOR = Sequelize.Utils.classToInvokable(VECTOR);
37+
DataTypes.VECTOR.types.postgres = ['vector'];
38+
39+
PgTypes.VECTOR = function VECTOR() {
40+
if (!(this instanceof PgTypes.VECTOR)) {
41+
return new PgTypes.VECTOR();
42+
}
43+
DataTypes.VECTOR.apply(this, arguments);
44+
};
45+
util.inherits(PgTypes.VECTOR, DataTypes.VECTOR);
46+
PgTypes.VECTOR.parse = DataTypes.VECTOR.parse;
47+
PgTypes.VECTOR.types = {postgres: ['vector']};
48+
DataTypes.postgres.VECTOR.key = 'vector';
49+
}
50+
51+
module.exports = {registerType};

sequelize/index.test.js

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
const { Sequelize, DataTypes, Model } = require('sequelize');
2+
const pgvector = require('./index');
3+
4+
pgvector.registerType(Sequelize);
5+
6+
class Item extends Model {}
7+
8+
const sequelize = new Sequelize('postgres://localhost/pgvector_node_test', {
9+
logging: false
10+
});
11+
12+
Item.init({
13+
factors: {
14+
type: DataTypes.VECTOR(3)
15+
}
16+
}, {
17+
sequelize,
18+
modelName: 'Item'
19+
});
20+
21+
beforeAll(async () => {
22+
await sequelize.authenticate();
23+
await sequelize.query('CREATE EXTENSION IF NOT EXISTS vector');
24+
await Item.sync({force: true});
25+
});
26+
27+
afterAll(() => {
28+
sequelize.close();
29+
});
30+
31+
test('works', async () => {
32+
await Item.create({factors: [1, 2, 3]});
33+
const items = await Item.findAll({
34+
order: [sequelize.literal(`factors <-> '[1, 1, 1]'`)],
35+
limit: 5
36+
});
37+
expect(items[0].factors).toStrictEqual([1, 2, 3]);
38+
});
39+
40+
test('bad value', () => {
41+
expect.assertions(1);
42+
return Item.create({factors: 'bad'}).catch(e => expect(e.message).toMatch('malformed vector literal'));
43+
});
44+
45+
test('dimensions', () => {
46+
expect(DataTypes.VECTOR(3).toSql()).toBe('VECTOR(3)');
47+
});
48+
49+
test('no dimensions', () => {
50+
expect(DataTypes.VECTOR().toSql()).toBe('VECTOR');
51+
});
52+
53+
test('bad dimensions', () => {
54+
expect(() => {
55+
DataTypes.VECTOR('bad').toSql()
56+
}).toThrowError('expected integer');
57+
});

0 commit comments

Comments
 (0)