-
Notifications
You must be signed in to change notification settings - Fork 157
Expand file tree
/
Copy pathindex.ts
More file actions
144 lines (129 loc) · 5.11 KB
/
Copy pathindex.ts
File metadata and controls
144 lines (129 loc) · 5.11 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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
/**
* GitHub Stars Vectorize — 极简代理 Worker
*
* 纯 Vectorize 存/查/删代理,不持有任何 AI Key。
* 前端负责 Embedding 生成,Worker 只负责向量存储和检索。
*/
interface Env {
VECTORIZE: Vectorize;
AUTH_TOKEN: string;
}
interface QueryRequest {
vector: number[];
topK?: number;
threshold?: number;
}
interface DeleteRequest {
ids: string[];
}
interface UpsertRequest {
vectors: VectorizeVector[];
}
const CORS_HEADERS: Record<string, string> = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
};
function jsonResponse(data: unknown, status = 200): Response {
return Response.json(data, {
status,
headers: CORS_HEADERS,
});
}
export default {
async fetch(request: Request, env: Env): Promise<Response> {
// CORS 预检
if (request.method === 'OPTIONS') {
return new Response(null, { headers: CORS_HEADERS });
}
// 认证
if (!env.AUTH_TOKEN) {
return jsonResponse({ success: false, error: 'Server auth not configured' }, 500);
}
const token = request.headers.get('Authorization')?.replace('Bearer ', '') ?? '';
if (token !== env.AUTH_TOKEN) {
return jsonResponse({ success: false, error: 'Unauthorized' }, 401);
}
const url = new URL(request.url);
try {
// POST /upsert — 批量写入向量
if (request.method === 'POST' && url.pathname === '/upsert') {
const { vectors } = (await request.json()) as UpsertRequest;
if (!Array.isArray(vectors) || vectors.length === 0) {
return jsonResponse({ success: false, error: 'vectors array required' }, 400);
}
await env.VECTORIZE.upsert(vectors);
return jsonResponse({ success: true, upserted: vectors.length });
}
// POST /query — 向量相似度查询
if (request.method === 'POST' && url.pathname === '/query') {
const { vector, topK = 20, threshold = 0.35 } = (await request.json()) as QueryRequest;
if (!Array.isArray(vector) || vector.length === 0) {
return jsonResponse({ success: false, error: 'vector array required' }, 400);
}
// returnMetadata:'all' caps topK at 50; clamp to avoid silent truncation
const clampedTopK = Math.min(topK, 50);
const matches = await env.VECTORIZE.query(vector, {
topK: clampedTopK,
returnMetadata: 'all' as const,
});
// 过滤低分结果
const filtered = matches.matches.filter((m) => m.score >= threshold);
return jsonResponse({ success: true, matches: filtered });
}
// POST /delete — 删除指定向量
if (request.method === 'POST' && url.pathname === '/delete') {
const { ids } = (await request.json()) as DeleteRequest;
if (!Array.isArray(ids) || ids.length === 0) {
return jsonResponse({ success: false, error: 'ids array required' }, 400);
}
await env.VECTORIZE.deleteByIds(ids);
return jsonResponse({ success: true, deleted: ids.length });
}
// POST /cleanup — 删除不在 keepIds 列表中的向量(清理已 unstar 的仓库)
if (request.method === 'POST' && url.pathname === '/cleanup') {
const { keepIds } = (await request.json()) as { keepIds: string[] };
if (!Array.isArray(keepIds)) {
return jsonResponse({ success: false, error: 'keepIds array required' }, 400);
}
const keepSet = new Set(keepIds);
const info = await env.VECTORIZE.describe();
if ((info.vectorCount ?? 0) === 0) {
return jsonResponse({ success: true, deleted: 0 });
}
// 使用 query + 零向量采样,循环删除不在 keepSet 中的向量
// Vectorize binding 不支持 listVectors,topK 上限 100
const dimensions = info.dimensions ?? 1536;
let totalDeleted = 0;
const zeroVector = new Array(dimensions).fill(0);
// 最多迭代 10 轮(覆盖最多 1000 个向量)
for (let round = 0; round < 10; round++) {
const result = await env.VECTORIZE.query(zeroVector, {
topK: 100,
returnMetadata: 'none',
});
const staleIds = result.matches
.filter((m) => !keepSet.has(m.id))
.map((m) => m.id);
if (staleIds.length === 0) break;
await env.VECTORIZE.deleteByIds(staleIds);
totalDeleted += staleIds.length;
}
return jsonResponse({ success: true, deleted: totalDeleted });
}
// GET /status — 返回索引信息
if (request.method === 'GET' && url.pathname === '/status') {
const info = await env.VECTORIZE.describe();
return jsonResponse({
success: true,
vectorCount: info.vectorCount ?? 0,
dimensions: info.dimensions ?? 0,
});
}
return jsonResponse({ error: 'Not Found' }, 404);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
return jsonResponse({ success: false, error: message }, 500);
}
},
};