@instant.dev/vectors 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +117 -0
- package/core/vector_manager.js +187 -0
- package/index.js +5 -0
- package/package.json +25 -0
- package/test/runner.js +19 -0
- package/test/tests/vectors.js +147 -0
package/README.md
ADDED
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
# Simple vector creation with automatic batching
|
|
2
|
+
 
|
|
3
|
+
|
|
4
|
+
## Batch create vectors without thinking about it
|
|
5
|
+
|
|
6
|
+
When you're creating a lot of vectors - for example, indexing a bunch of documents
|
|
7
|
+
at once using [OpenAI embeddings](https://platform.openai.com/docs/guides/embeddings) -
|
|
8
|
+
you quickly run into IO-related performance issues. Your web requests will be throttled
|
|
9
|
+
if you make too many parallel API requests, so OpenAI allows for batched requests
|
|
10
|
+
via the [OpenAI embeddings API](https://platform.openai.com/docs/api-reference/embeddings).
|
|
11
|
+
However, this API only allows for a maximum of 8,191 tokens per request:
|
|
12
|
+
about 32,764 characters.
|
|
13
|
+
|
|
14
|
+
**Solution:** `@instant.dev/vectors` provides a simple `VectorManager` utility that performs
|
|
15
|
+
automatic, efficient batch creation of vectors via APIs. It will automatically collect
|
|
16
|
+
vector creation requests over a 100ms (configurable) timeframe and batch them to minimize
|
|
17
|
+
vector creation requests.
|
|
18
|
+
|
|
19
|
+
It is most useful in web server contexts where multiple user requests may be
|
|
20
|
+
creating vectors at the same time. If you rely on the same `VectorManager` instance
|
|
21
|
+
all of these disparate requests will be efficiently batched.
|
|
22
|
+
|
|
23
|
+
## Installation and Importing
|
|
24
|
+
|
|
25
|
+
To use this library you'll need to also work with a vector creation tool, like OpenAI.
|
|
26
|
+
|
|
27
|
+
```shell
|
|
28
|
+
npm i @instant.dev/vectors --save # vector management
|
|
29
|
+
npm i openai --save # openai for the engine
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
CommonJS:
|
|
33
|
+
|
|
34
|
+
```javascript
|
|
35
|
+
const { VectorManager } = require('@instant.dev/vectors');
|
|
36
|
+
const OpenAI = require('openai');
|
|
37
|
+
|
|
38
|
+
const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY});
|
|
39
|
+
const Vectors = new VectorManager();
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
ESM:
|
|
43
|
+
|
|
44
|
+
```javascript
|
|
45
|
+
import { VectorManager } from '@instant.dev/vectors';
|
|
46
|
+
import { Configuration, OpenAIApi } from "openai";
|
|
47
|
+
const configuration = new Configuration({
|
|
48
|
+
organization: "YOUR_ORG_ID",
|
|
49
|
+
apiKey: process.env.OPENAI_API_KEY,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const openai = new OpenAIApi(configuration);
|
|
53
|
+
const Vectors = new VectorManager();
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Usage
|
|
57
|
+
|
|
58
|
+
Once you've imported and instantiated the package, you can use it like so.
|
|
59
|
+
|
|
60
|
+
Set a batch engine:
|
|
61
|
+
|
|
62
|
+
```javascript
|
|
63
|
+
// values will automatically be batched appropriately
|
|
64
|
+
Vectors.setEngine(async (values) => {
|
|
65
|
+
const embeddingResult = await openai.embeddings.create({
|
|
66
|
+
model: 'text-embedding-ada-002',
|
|
67
|
+
input: values,
|
|
68
|
+
});
|
|
69
|
+
return embeddingResult.data.map(entry => entry.embedding);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Create a single vector:
|
|
74
|
+
|
|
75
|
+
```javascript
|
|
76
|
+
let vector = await Vectors.create(`Something to vectorize!`);
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Create multiple vectors, will automatically batch:
|
|
80
|
+
|
|
81
|
+
```javascript
|
|
82
|
+
const myStrings = [
|
|
83
|
+
`Some string!`,
|
|
84
|
+
`Can also be a lot longer`,
|
|
85
|
+
`W`.repeat(1000),
|
|
86
|
+
// ...
|
|
87
|
+
];
|
|
88
|
+
|
|
89
|
+
let vectors = await Promise.all(myStrings.map(str => Vectors.create(str)));
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
Create multiple vectors with `batchCreate` utility:
|
|
93
|
+
|
|
94
|
+
```javascript
|
|
95
|
+
const myStrings = [
|
|
96
|
+
`Some string!`,
|
|
97
|
+
`Can also be a lot longer`,
|
|
98
|
+
`W`.repeat(1000),
|
|
99
|
+
// ...
|
|
100
|
+
];
|
|
101
|
+
|
|
102
|
+
let vectors = await Vectors.batchCreate(myStrings);
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
# Acknowledgements
|
|
106
|
+
|
|
107
|
+
Special thank you to [Scott Gamble](https://x.com/threesided) who helps run all
|
|
108
|
+
of the front-of-house work for instant.dev 💜!
|
|
109
|
+
|
|
110
|
+
| Destination | Link |
|
|
111
|
+
| ----------- | ---- |
|
|
112
|
+
| Home | [instant.dev](https://instant.dev) |
|
|
113
|
+
| GitHub | [github.com/instant-dev](https://github.com/instant-dev) |
|
|
114
|
+
| Discord | [discord.gg/puVYgA7ZMh](https://discord.gg/puVYgA7ZMh) |
|
|
115
|
+
| X / instant.dev | [x.com/instantdevs](https://x.com/instantdevs) |
|
|
116
|
+
| X / Keith Horwood | [x.com/keithwhor](https://x.com/keithwhor) |
|
|
117
|
+
| X / Scott Gamble | [x.com/threesided](https://x.com/threesided) |
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Convert strings and other objects to vectors, while automatically batching
|
|
3
|
+
* requests to minimize network and / or processor IO
|
|
4
|
+
* Defaults are set to work best with OpenAI's text-embedding-ada-002 model
|
|
5
|
+
*/
|
|
6
|
+
class VectorManager {
|
|
7
|
+
|
|
8
|
+
constructor () {
|
|
9
|
+
this.maximumBatchSize = 7168 * 4; // 4 tokens per word, estimated
|
|
10
|
+
this.maximumParallelRequests = 10; // 10 requests simultaneously max
|
|
11
|
+
this.fastQueueTime = 10; // time to wait if no other entries are added
|
|
12
|
+
this.waitQueueTime = 100; // time to wait to collect entries if 1+ entries are added
|
|
13
|
+
/**
|
|
14
|
+
* @private
|
|
15
|
+
*/
|
|
16
|
+
this._vectorize = null;
|
|
17
|
+
/**
|
|
18
|
+
* @private
|
|
19
|
+
*/
|
|
20
|
+
this._queue = [];
|
|
21
|
+
/**
|
|
22
|
+
* @private
|
|
23
|
+
*/
|
|
24
|
+
this._results = new WeakMap();
|
|
25
|
+
/**
|
|
26
|
+
* @private
|
|
27
|
+
*/
|
|
28
|
+
this._timeout = null;
|
|
29
|
+
/**
|
|
30
|
+
* @private
|
|
31
|
+
*/
|
|
32
|
+
this._processing = false;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* @private
|
|
37
|
+
*/
|
|
38
|
+
async __sleep__ (t) {
|
|
39
|
+
return new Promise(r => setTimeout(() => r(true), t));
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @private
|
|
44
|
+
*/
|
|
45
|
+
async __dequeue__ () {
|
|
46
|
+
this._processing = true;
|
|
47
|
+
clearTimeout(this._timeout);
|
|
48
|
+
const queue = this._queue.slice();
|
|
49
|
+
this._timeout = null;
|
|
50
|
+
this._queue = [];
|
|
51
|
+
await this.batchVectorize(queue);
|
|
52
|
+
this._processing = false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* @private
|
|
58
|
+
*/
|
|
59
|
+
async vectorizeValues (values) {
|
|
60
|
+
const vectors = await this._vectorize(values);
|
|
61
|
+
return vectors;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @private
|
|
66
|
+
*/
|
|
67
|
+
async batchVectorize (queue) {
|
|
68
|
+
const strValues = queue.map(item => this.convertToString(item.value));
|
|
69
|
+
const batches = [[]];
|
|
70
|
+
let curBatchSize = 0;
|
|
71
|
+
while (strValues.length) {
|
|
72
|
+
const str = strValues.shift().slice(0, this.maximumBatchSize);
|
|
73
|
+
let n = batches.length - 1;
|
|
74
|
+
curBatchSize += str.length;
|
|
75
|
+
if (curBatchSize > this.maximumBatchSize) {
|
|
76
|
+
batches.push([str]);
|
|
77
|
+
n = batches.length - 1;
|
|
78
|
+
curBatchSize = str.length;
|
|
79
|
+
} else {
|
|
80
|
+
batches[n].push(str);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
let i = 0;
|
|
84
|
+
while (batches.length) {
|
|
85
|
+
const parallelBatches = batches.splice(0, this.maximumParallelRequests);
|
|
86
|
+
const parallelVectors = await Promise.all(parallelBatches.map(strValues => this.vectorizeValues(strValues)));
|
|
87
|
+
parallelVectors.forEach((vectors, j) => {
|
|
88
|
+
vectors = Array.isArray(vectors)
|
|
89
|
+
? vectors
|
|
90
|
+
: [];
|
|
91
|
+
parallelBatches[j].forEach((str, k) => {
|
|
92
|
+
if (vectors[k]) {
|
|
93
|
+
this._results.set(queue[i++], vectors[k]);
|
|
94
|
+
} else {
|
|
95
|
+
this._results.set(queue[i++], -1);
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Converts the provided value to a string for easy vectorization
|
|
105
|
+
* @param {any} value
|
|
106
|
+
* @returns {string}
|
|
107
|
+
*/
|
|
108
|
+
convertToString (value) {
|
|
109
|
+
return (value === null || value === void 0)
|
|
110
|
+
? ''
|
|
111
|
+
: typeof value === 'object'
|
|
112
|
+
? JSON.stringify(value)
|
|
113
|
+
: (value + '');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Sets the vector engine: takes in an array of values with a maximum string length of this.maximumBatchSize
|
|
118
|
+
* @param {function} fnVectorize Expects a single argument, values, that is an array
|
|
119
|
+
* @returns {boolean}
|
|
120
|
+
*/
|
|
121
|
+
setEngine (fnVectorize) {
|
|
122
|
+
if (typeof fnVectorize !== 'function') {
|
|
123
|
+
throw new Error(`.setEngine(fn) expects a valid function`);
|
|
124
|
+
} else if (fnVectorize.constructor.name !== 'AsyncFunction') {
|
|
125
|
+
throw new Error(`.setEngine(fn) expects an Asynchronous function`);
|
|
126
|
+
}
|
|
127
|
+
this._vectorize = fnVectorize;
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async createTimeout () {
|
|
132
|
+
this._timeout = setTimeout(() => {
|
|
133
|
+
if (this._processing) {
|
|
134
|
+
this.createTimeout();
|
|
135
|
+
} else if (this._queue.length <= 1) {
|
|
136
|
+
this.__dequeue__();
|
|
137
|
+
} else {
|
|
138
|
+
this._timeout = setTimeout(
|
|
139
|
+
() => this.__dequeue__(),
|
|
140
|
+
this.waitQueueTime - this.fastQueueTime
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
}, this.fastQueueTime);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Creates a vector from a value
|
|
148
|
+
* @param {any} value Any value. null and undefined are converted to empty strings, non-string values are JSONified
|
|
149
|
+
* @returns
|
|
150
|
+
*/
|
|
151
|
+
async create (value) {
|
|
152
|
+
if (!this._vectorize) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
`Could not vectorize: no vector engine has been set.\n` +
|
|
155
|
+
`Use Instant.Vectors.setEngine(fn) to enable.`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
const item = {value};
|
|
159
|
+
this._queue.push(item);
|
|
160
|
+
if (!this._timeout) {
|
|
161
|
+
this.createTimeout();
|
|
162
|
+
}
|
|
163
|
+
let result = null;
|
|
164
|
+
while (!(result = this._results.get(item))) {
|
|
165
|
+
await this.__sleep__(10);
|
|
166
|
+
}
|
|
167
|
+
this._results.delete(item);
|
|
168
|
+
if (!Array.isArray(result)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
`Could not vectorize: vector engine did not return a valid vector for input "${value}"`
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/**
|
|
177
|
+
* Batch creates vectors from an array
|
|
178
|
+
* @param {array} values Any values. null and undefined are converted to empty strings, non-string values are JSONified
|
|
179
|
+
* @returns
|
|
180
|
+
*/
|
|
181
|
+
async batchCreate (values) {
|
|
182
|
+
return Promise.all(values.map(value => this.create(value)));
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
module.exports = VectorManager;
|
package/index.js
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@instant.dev/vectors",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Utility for batch creating vectors via API",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"test": "mocha ./test/runner.js"
|
|
8
|
+
},
|
|
9
|
+
"keywords": [
|
|
10
|
+
"vectors",
|
|
11
|
+
"vector",
|
|
12
|
+
"management",
|
|
13
|
+
"openai",
|
|
14
|
+
"pinecone",
|
|
15
|
+
"pgvector"
|
|
16
|
+
],
|
|
17
|
+
"author": "Keith Horwood",
|
|
18
|
+
"license": "MIT",
|
|
19
|
+
"devDependencies": {
|
|
20
|
+
"chai": "^4.3.10",
|
|
21
|
+
"dotenv": "^16.3.1",
|
|
22
|
+
"mocha": "^10.2.0",
|
|
23
|
+
"openai": "^4.11.0"
|
|
24
|
+
}
|
|
25
|
+
}
|
package/test/runner.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
require('dotenv').config({path: '.test.env'});
|
|
2
|
+
const child_process = require('child_process');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
|
|
6
|
+
let args = [];
|
|
7
|
+
try {
|
|
8
|
+
args = JSON.parse(process.env.npm_argv);
|
|
9
|
+
args = args.slice(3);
|
|
10
|
+
} catch (e) {
|
|
11
|
+
args = [];
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
describe('Test Suite', function() {
|
|
15
|
+
|
|
16
|
+
let testFilenames = fs.readdirSync('./test/tests');
|
|
17
|
+
testFilenames.forEach(filename => require(`./tests/${filename}`)());
|
|
18
|
+
|
|
19
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
const OpenAI = require('openai');
|
|
2
|
+
const openai = new OpenAI({apiKey: process.env.OPENAI_API_KEY});
|
|
3
|
+
|
|
4
|
+
const { VectorManager } = require('../../index.js');
|
|
5
|
+
|
|
6
|
+
module.exports = (InstantORM, Databases) => {
|
|
7
|
+
|
|
8
|
+
const expect = require('chai').expect;
|
|
9
|
+
|
|
10
|
+
const Vectors = new VectorManager();
|
|
11
|
+
|
|
12
|
+
describe('VectorManager', async () => {
|
|
13
|
+
|
|
14
|
+
it('Should fail to vectorize without vector engine set', async () => {
|
|
15
|
+
|
|
16
|
+
const testPhrase = `I am extremely happy`;
|
|
17
|
+
|
|
18
|
+
let vector;
|
|
19
|
+
let error;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
vector = await Vectors.create(testPhrase);
|
|
23
|
+
} catch (e) {
|
|
24
|
+
error = e;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
expect(error).to.exist;
|
|
28
|
+
expect(error.message).to.contain('Could not vectorize: no vector engine has been set');
|
|
29
|
+
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('Should fail to vectorize with a bad vector engine', async () => {
|
|
33
|
+
|
|
34
|
+
const testPhrase = `I am extremely happy`;
|
|
35
|
+
|
|
36
|
+
Vectors.setEngine(async (values) => {
|
|
37
|
+
// do nothing
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
let vector;
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
vector = await Vectors.create(testPhrase);
|
|
44
|
+
} catch (e) {
|
|
45
|
+
error = e;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
expect(error).to.exist;
|
|
49
|
+
expect(error.message).to.contain('Could not vectorize: vector engine did not return a valid vector for input "I am extremely happy"');
|
|
50
|
+
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('Should succeed at vectorizing when vector engine is set properly', async () => {
|
|
54
|
+
|
|
55
|
+
const testPhrase = `I am extremely happy`;
|
|
56
|
+
let testVector;
|
|
57
|
+
|
|
58
|
+
Vectors.setEngine(async (values) => {
|
|
59
|
+
const embedding = await openai.embeddings.create({
|
|
60
|
+
model: 'text-embedding-ada-002',
|
|
61
|
+
input: values,
|
|
62
|
+
});
|
|
63
|
+
const vectors = embedding.data.map((entry, i) => {
|
|
64
|
+
let embedding = entry.embedding;
|
|
65
|
+
if (values[i] === testPhrase) {
|
|
66
|
+
testVector = embedding;
|
|
67
|
+
}
|
|
68
|
+
return embedding;
|
|
69
|
+
});
|
|
70
|
+
return vectors;
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const vector = await Vectors.create(testPhrase);
|
|
74
|
+
|
|
75
|
+
expect(vector).to.deep.equal(testVector);
|
|
76
|
+
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('Should create more vectors', async () => {
|
|
80
|
+
|
|
81
|
+
const vectorMap = {};
|
|
82
|
+
|
|
83
|
+
Vectors.setEngine(async (values) => {
|
|
84
|
+
const embedding = await openai.embeddings.create({
|
|
85
|
+
model: 'text-embedding-ada-002',
|
|
86
|
+
input: values,
|
|
87
|
+
});
|
|
88
|
+
return embedding.data.map((entry, i) => {
|
|
89
|
+
vectorMap[values[i]] = entry.embedding;
|
|
90
|
+
return vectorMap[values[i]];
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
const strings = [
|
|
95
|
+
`I am feeling awful`,
|
|
96
|
+
`I am in extreme distress`,
|
|
97
|
+
`I am feeling pretty good`,
|
|
98
|
+
`I am so-so`
|
|
99
|
+
]
|
|
100
|
+
|
|
101
|
+
const vectors = await Promise.all(strings.map(str => Vectors.create(str)));
|
|
102
|
+
|
|
103
|
+
expect(vectors).to.exist;
|
|
104
|
+
expect(vectors.length).to.equal(4);
|
|
105
|
+
vectors.forEach((vector, i) => {
|
|
106
|
+
expect(vector).to.deep.equal(vectorMap[strings[i]]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('Should create many more vectors (50 vectors, ~4 per batch)', async function () {
|
|
112
|
+
|
|
113
|
+
this.timeout(10000);
|
|
114
|
+
|
|
115
|
+
const vectorMap = {};
|
|
116
|
+
|
|
117
|
+
Vectors.maximumBatchSize = 1000;
|
|
118
|
+
Vectors.setEngine(async (values) => {
|
|
119
|
+
const embedding = await openai.embeddings.create({
|
|
120
|
+
model: 'text-embedding-ada-002',
|
|
121
|
+
input: values,
|
|
122
|
+
});
|
|
123
|
+
return embedding.data.map((entry, i) => {
|
|
124
|
+
vectorMap[values[i]] = entry.embedding;
|
|
125
|
+
return vectorMap[values[i]];
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
let strings = Array(50).fill(0).map((_, i) => {
|
|
130
|
+
return i + '_ ' + Array(50).fill(0).map(() => {
|
|
131
|
+
return ['alpha', 'beta', 'gamma'][(Math.random() * 3) | 0]
|
|
132
|
+
}).join(' ');
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
const vectors = await Promise.all(strings.map(str => Vectors.create(str)));
|
|
136
|
+
|
|
137
|
+
expect(vectors).to.exist;
|
|
138
|
+
expect(vectors.length).to.equal(50);
|
|
139
|
+
vectors.forEach((vector, i) => {
|
|
140
|
+
expect(vector).to.deep.equal(vectorMap[strings[i]]);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
};
|