-
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathfastify-search-api.js
More file actions
117 lines (94 loc) · 2.88 KB
/
Copy pathfastify-search-api.js
File metadata and controls
117 lines (94 loc) · 2.88 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/**
* Fastify vector search API example.
*
* Install dependency first:
* npm install fastify
*
* Start:
* node examples/fastify-search-api.js
*/
const Fastify = require('fastify');
const { FaissIndex, normalizeVectors } = require('../src/js');
const dims = 4;
const app = Fastify({ logger: true });
const index = new FaissIndex({ type: 'FLAT_IP', dims });
function flattenVectors(vectors) {
return new Float32Array(vectors.flatMap((row) => Array.from(row)));
}
function validateVectorRows(vectors) {
if (!Array.isArray(vectors)) {
return 'vectors must be an array of numeric vectors';
}
if (vectors.length === 0) {
return 'vectors must contain at least one vector';
}
for (const row of vectors) {
if (!Array.isArray(row) && !ArrayBuffer.isView(row)) {
return 'each vector must be an array or TypedArray';
}
const values = Array.from(row);
if (values.length !== dims) {
return `each vector must contain exactly ${dims} numbers`;
}
if (values.some((value) => typeof value !== 'number' || !Number.isFinite(value))) {
return 'vectors must only contain finite numbers';
}
}
return null;
}
function validateQueryVector(query) {
if (!Array.isArray(query) && !ArrayBuffer.isView(query)) {
return 'query must be an array or TypedArray of numbers';
}
const values = Array.from(query);
if (values.length !== dims) {
return `query must contain exactly ${dims} numbers`;
}
if (values.some((value) => typeof value !== 'number' || !Number.isFinite(value))) {
return 'query must only contain finite numbers';
}
return null;
}
function parseNeighborCount(value, fallback = 5) {
if (value === undefined) {
return fallback;
}
const parsed = Number(value);
if (!Number.isInteger(parsed) || parsed <= 0 || parsed > 100) {
return null;
}
return parsed;
}
app.post('/index', async (request, reply) => {
const error = validateVectorRows(request.body?.vectors);
if (error) {
return reply.code(400).send({ error });
}
const vectors = flattenVectors(request.body.vectors);
await index.add(normalizeVectors(vectors, dims));
return {
ok: true,
stats: index.getStats(),
};
});
app.post('/search', async (request, reply) => {
const error = validateQueryVector(request.body?.query);
if (error) {
return reply.code(400).send({ error });
}
const k = parseNeighborCount(request.body?.k, 5);
if (k === null) {
return reply.code(400).send({ error: 'k must be a positive integer between 1 and 100' });
}
const query = normalizeVectors(new Float32Array(Array.from(request.body.query)), dims);
const results = await index.search(query, k);
return {
labels: Array.from(results.labels),
distances: Array.from(results.distances),
};
});
app.get('/info', async () => index.inspect());
app.listen({ port: 3001 }).catch((error) => {
app.log.error(error);
process.exitCode = 1;
});