@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 ADDED
@@ -0,0 +1,117 @@
1
+ # Simple vector creation with automatic batching
2
+ ![npm version](https://img.shields.io/npm/v/@instant.dev/vectors?label=) ![Build Status](https://app.travis-ci.com/instant-dev/vectors.svg?branch=main)
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
@@ -0,0 +1,5 @@
1
+ const VectorManager = require('./core/vector_manager.js');
2
+
3
+ module.exports = {
4
+ VectorManager
5
+ };
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
+ };