@ouim/vectoriadb-client 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/demo.js +33 -0
- package/index.js +172 -0
- package/package.json +16 -0
- package/socket-client.js +120 -0
package/demo.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import VectoriaDB from './index.js'
|
|
2
|
+
|
|
3
|
+
async function perfDemo() {
|
|
4
|
+
const db = new VectoriaDB({ serverUrl: 'http://localhost:3001' })
|
|
5
|
+
|
|
6
|
+
console.log('Initializing remote VectoriaDB (server should already be running)...')
|
|
7
|
+
await db.initialize()
|
|
8
|
+
|
|
9
|
+
console.log('Inserting 1,000 small documents in batches of 200...')
|
|
10
|
+
const docs = Array.from({ length: 1000 }, (_, i) => ({
|
|
11
|
+
id: `doc:${i}`,
|
|
12
|
+
text: `Document number ${i}`,
|
|
13
|
+
metadata: { id: `doc:${i}`, owner: 'demo' },
|
|
14
|
+
}))
|
|
15
|
+
|
|
16
|
+
const start = Date.now()
|
|
17
|
+
for (let i = 0; i < docs.length; i += 200) {
|
|
18
|
+
const batch = docs.slice(i, i + 200)
|
|
19
|
+
await db.addMany(batch)
|
|
20
|
+
}
|
|
21
|
+
console.log('Inserted 1000 docs in', Date.now() - start, 'ms')
|
|
22
|
+
|
|
23
|
+
console.log('Running a search...')
|
|
24
|
+
const results = await db.search('Document number 5', { topK: 10 })
|
|
25
|
+
console.log('Search results:', results.slice(0, 3))
|
|
26
|
+
|
|
27
|
+
db.close()
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
perfDemo().catch(err => {
|
|
31
|
+
console.error(err)
|
|
32
|
+
process.exit(1)
|
|
33
|
+
})
|
package/index.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import SocketClient from './socket-client.js'
|
|
2
|
+
|
|
3
|
+
// Client SDK that mirrors VectoriaDB API surface (runtime validation + forwarding)
|
|
4
|
+
export default class VectoriaDB {
|
|
5
|
+
constructor(opts = {}) {
|
|
6
|
+
if (!opts.serverUrl) throw new Error('serverUrl is required')
|
|
7
|
+
this.serverUrl = opts.serverUrl
|
|
8
|
+
this.apiKey = opts.apiKey || null
|
|
9
|
+
this.requestTimeout = opts.requestTimeout || 30000
|
|
10
|
+
this._socket = new SocketClient({ serverUrl: this.serverUrl, apiKey: this.apiKey, requestTimeout: this.requestTimeout })
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// --- helper to serialize function filters ---
|
|
14
|
+
static _serializeFunction(fn) {
|
|
15
|
+
if (!fn) return null
|
|
16
|
+
if (typeof fn === 'function') return { __isFnString: true, fn: fn.toString() }
|
|
17
|
+
if (typeof fn === 'string') return { __isFnString: true, fn: fn }
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// low-level forwarder
|
|
22
|
+
async _forward(method, ...params) {
|
|
23
|
+
return this._socket.sendRequest({ method, params })
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// --- VectoriaDB API methods (as in docs) ---
|
|
27
|
+
async initialize() {
|
|
28
|
+
return this._forward('initialize')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async add(id, text, metadata) {
|
|
32
|
+
if (typeof id !== 'string') throw new TypeError('id must be string')
|
|
33
|
+
if (typeof text !== 'string') throw new TypeError('text must be string')
|
|
34
|
+
if (!metadata || typeof metadata !== 'object') throw new TypeError('metadata must be an object')
|
|
35
|
+
return this._forward('add', id, text, metadata)
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async addMany(docs) {
|
|
39
|
+
if (!Array.isArray(docs)) throw new TypeError('docs must be an array')
|
|
40
|
+
if (docs.length === 0) return { added: 0 }
|
|
41
|
+
return this._forward('addMany', docs)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async has(id) {
|
|
45
|
+
return this._forward('has', id)
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
async get(id) {
|
|
49
|
+
return this._forward('get', id)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async size() {
|
|
53
|
+
return this._forward('size')
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async update(id, updates, opts = {}) {
|
|
57
|
+
return this._forward('update', id, updates, opts)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async updateMetadata(id, metadata) {
|
|
61
|
+
return this._forward('updateMetadata', id, metadata)
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
async updateMany(updates) {
|
|
65
|
+
return this._forward('updateMany', updates)
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async remove(id) {
|
|
69
|
+
return this._forward('remove', id)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async removeMany(ids) {
|
|
73
|
+
return this._forward('removeMany', ids)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
async clear() {
|
|
77
|
+
return this._forward('clear')
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async saveToStorage() {
|
|
81
|
+
return this._forward('saveToStorage')
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async loadFromStorage() {
|
|
85
|
+
return this._forward('loadFromStorage')
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async clearStorage() {
|
|
89
|
+
return this._forward('clearStorage')
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async filter(fn) {
|
|
93
|
+
// filter is executed server-side — serialize function
|
|
94
|
+
if (typeof fn !== 'function') throw new TypeError('filter requires a function')
|
|
95
|
+
const serialized = VectoriaDB._serializeFunction(fn)
|
|
96
|
+
return this._forward('filter', serialized)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async search(queryOrVector, options = {}) {
|
|
100
|
+
const opts = { ...options }
|
|
101
|
+
if (opts.filter && typeof opts.filter === 'function') {
|
|
102
|
+
opts.filter = VectoriaDB._serializeFunction(opts.filter)
|
|
103
|
+
}
|
|
104
|
+
return this._forward('search', queryOrVector, opts)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// --- convenience collection-style API (maps to VectoriaDB primitives) ---
|
|
108
|
+
async createCollection(name) {
|
|
109
|
+
if (!name || typeof name !== 'string') throw new TypeError('collection name required')
|
|
110
|
+
// no-op on server side; client manages "collection" via metadata.owner
|
|
111
|
+
return { ok: true, collection: name }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async insert(collection, docs) {
|
|
115
|
+
if (!collection || typeof collection !== 'string') throw new TypeError('collection required')
|
|
116
|
+
if (!Array.isArray(docs)) throw new TypeError('docs must be an array')
|
|
117
|
+
|
|
118
|
+
const transformed = docs.map(d => {
|
|
119
|
+
const id = d.id || d.metadata?.id || `${collection}:${Math.random().toString(36).slice(2, 9)}`
|
|
120
|
+
const metadata = Object.assign({}, d.metadata || {}, { id, owner: collection })
|
|
121
|
+
return { id, text: d.text || d.vector?.toString() || (d.text ?? ''), metadata, vector: d.vector }
|
|
122
|
+
})
|
|
123
|
+
|
|
124
|
+
// Use addMany for textual documents; for vector-only docs we forward to addMany as well
|
|
125
|
+
return this.addMany(transformed)
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async query(collection, queryVectorOrText, opts = {}) {
|
|
129
|
+
if (!collection || typeof collection !== 'string') throw new TypeError('collection required')
|
|
130
|
+
const options = { ...opts }
|
|
131
|
+
const originalFilter = options.filter
|
|
132
|
+
|
|
133
|
+
// Build a self-contained, serializable filter that enforces owner === collection
|
|
134
|
+
// and (optionally) runs the user's filter. We inline the user's filter source
|
|
135
|
+
// so the resulting function does not rely on outer closures when evaluated
|
|
136
|
+
// on the server process.
|
|
137
|
+
const ownerLiteral = JSON.stringify(collection)
|
|
138
|
+
|
|
139
|
+
if (originalFilter) {
|
|
140
|
+
// normalize original filter to a source string
|
|
141
|
+
let origFnStr
|
|
142
|
+
if (typeof originalFilter === 'function') {
|
|
143
|
+
origFnStr = originalFilter.toString()
|
|
144
|
+
} else if (typeof originalFilter === 'string') {
|
|
145
|
+
origFnStr = originalFilter
|
|
146
|
+
} else if (originalFilter && originalFilter.__isFnString && typeof originalFilter.fn === 'string') {
|
|
147
|
+
origFnStr = originalFilter.fn
|
|
148
|
+
} else {
|
|
149
|
+
throw new TypeError('filter must be a function or serialized function')
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// inline user's filter so the combined function is self-contained server-side
|
|
153
|
+
const combinedFnStr = `(function(m){ try { const __orig = ${origFnStr}; return !!(__orig(m) && m && m.owner === ${ownerLiteral}); } catch(e) { return false } })`
|
|
154
|
+
options.filter = VectoriaDB._serializeFunction(combinedFnStr)
|
|
155
|
+
} else {
|
|
156
|
+
const ownerFnStr = `(function(m){ return m && m.owner === ${ownerLiteral}; })`
|
|
157
|
+
options.filter = VectoriaDB._serializeFunction(ownerFnStr)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
return this.search(queryVectorOrText, options)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// close socket
|
|
164
|
+
close() {
|
|
165
|
+
try {
|
|
166
|
+
this._socket.close()
|
|
167
|
+
} catch (e) {}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// CommonJS fallback (so `require('./client')` still works in many setups)
|
|
172
|
+
export { VectoriaDB }
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@ouim/vectoriadb-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"demo": "node demo.js",
|
|
8
|
+
"publish": "npm publish --access public"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"socket.io-client": "^4.8.0"
|
|
12
|
+
},
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=18"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/socket-client.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import { io } from 'socket.io-client'
|
|
2
|
+
|
|
3
|
+
function _makeId() {
|
|
4
|
+
return `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
export default class SocketClient {
|
|
8
|
+
constructor({ serverUrl, namespace = '/vectoriadb', apiKey = null, requestTimeout = 30000 } = {}) {
|
|
9
|
+
if (!serverUrl) throw new Error('serverUrl is required')
|
|
10
|
+
this.serverUrl = serverUrl.replace(/\/$/, '')
|
|
11
|
+
this.namespace = namespace.startsWith('/') ? namespace : `/${namespace}`
|
|
12
|
+
this.url = `${this.serverUrl}${this.namespace}`
|
|
13
|
+
this.apiKey = apiKey
|
|
14
|
+
this.requestTimeout = requestTimeout || 30000
|
|
15
|
+
|
|
16
|
+
this.socket = io(this.url, {
|
|
17
|
+
auth: { apiKey },
|
|
18
|
+
path: '/socket.io',
|
|
19
|
+
transports: ['websocket'],
|
|
20
|
+
autoConnect: true,
|
|
21
|
+
reconnection: true,
|
|
22
|
+
})
|
|
23
|
+
|
|
24
|
+
this._pending = new Map() // id -> { resolve, reject, timer, chunks }
|
|
25
|
+
this._offlineQueue = []
|
|
26
|
+
|
|
27
|
+
this.socket.on('connect', () => {
|
|
28
|
+
// flush queue
|
|
29
|
+
while (this._offlineQueue.length) {
|
|
30
|
+
const payload = this._offlineQueue.shift()
|
|
31
|
+
this.socket.emit('request', payload)
|
|
32
|
+
}
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
this.socket.on('connect_error', err => {
|
|
36
|
+
// reject nothing here; pending requests will timeout or be retried
|
|
37
|
+
// console.warn('connect_error', err.message)
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
this.socket.on('disconnect', reason => {
|
|
41
|
+
// keep pending promises alive; they will timeout based on requestTimeout
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
this.socket.on('response', msg => {
|
|
45
|
+
const { id, result, error } = msg || {}
|
|
46
|
+
const pending = this._pending.get(id)
|
|
47
|
+
if (!pending) return
|
|
48
|
+
|
|
49
|
+
// If streamed chunks were received, assemble them in index order.
|
|
50
|
+
if (pending.chunksMap) {
|
|
51
|
+
const chunksMap = pending.chunksMap
|
|
52
|
+
const indexes = Object.keys(chunksMap)
|
|
53
|
+
.map(k => Number(k))
|
|
54
|
+
.sort((a, b) => a - b)
|
|
55
|
+
const assembled = indexes.flatMap(i => (Array.isArray(chunksMap[i]) ? chunksMap[i] : []))
|
|
56
|
+
clearTimeout(pending.timer)
|
|
57
|
+
pending.resolve(assembled)
|
|
58
|
+
this._pending.delete(id)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
clearTimeout(pending.timer)
|
|
63
|
+
if (error) pending.reject(new Error(error.message || 'ServerError'))
|
|
64
|
+
else pending.resolve(result)
|
|
65
|
+
this._pending.delete(id)
|
|
66
|
+
})
|
|
67
|
+
|
|
68
|
+
this.socket.on('response-chunk', msg => {
|
|
69
|
+
const { id, chunk, index, totalChunks } = msg || {}
|
|
70
|
+
const pending = this._pending.get(id)
|
|
71
|
+
if (!pending) return
|
|
72
|
+
|
|
73
|
+
if (!pending.chunksMap) pending.chunksMap = {}
|
|
74
|
+
pending.chunksMap[index] = chunk
|
|
75
|
+
pending.receivedChunks = (pending.receivedChunks || 0) + 1
|
|
76
|
+
if (typeof totalChunks === 'number') pending.totalChunks = totalChunks
|
|
77
|
+
|
|
78
|
+
// reset timeout so long streams don't prematurely fail
|
|
79
|
+
if (pending.timer) {
|
|
80
|
+
clearTimeout(pending.timer)
|
|
81
|
+
pending.timer = setTimeout(() => {
|
|
82
|
+
this._pending.delete(id)
|
|
83
|
+
pending.reject(new Error('RequestTimeout'))
|
|
84
|
+
}, pending.timeoutMs || this.requestTimeout)
|
|
85
|
+
}
|
|
86
|
+
})
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
sendRequest({ method, params = [], collection = undefined, timeout = undefined } = {}) {
|
|
90
|
+
const id = _makeId()
|
|
91
|
+
const payload = { id, method, params, collection, timestamp: Date.now() }
|
|
92
|
+
const effectiveTimeout = typeof timeout === 'number' ? timeout : this.requestTimeout
|
|
93
|
+
|
|
94
|
+
const promise = new Promise((resolve, reject) => {
|
|
95
|
+
const timer = setTimeout(() => {
|
|
96
|
+
this._pending.delete(id)
|
|
97
|
+
reject(new Error('RequestTimeout'))
|
|
98
|
+
}, effectiveTimeout)
|
|
99
|
+
|
|
100
|
+
// store extra metadata so streaming chunks can reset the timer and be
|
|
101
|
+
// assembled in order on final response
|
|
102
|
+
this._pending.set(id, { resolve, reject, timer, timeoutMs: effectiveTimeout, chunksMap: null, receivedChunks: 0, totalChunks: null })
|
|
103
|
+
|
|
104
|
+
if (this.socket.connected) {
|
|
105
|
+
this.socket.emit('request', payload)
|
|
106
|
+
} else {
|
|
107
|
+
// queue for send on reconnect
|
|
108
|
+
this._offlineQueue.push(payload)
|
|
109
|
+
}
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
return promise
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
close() {
|
|
116
|
+
try {
|
|
117
|
+
this.socket.close()
|
|
118
|
+
} catch (e) {}
|
|
119
|
+
}
|
|
120
|
+
}
|