Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

g1t.sh
89 linesCodeBlameRaw
1/**
2 * Recall's choices, apart from where the passages come from: how the
3 * vector query is filtered, which matches are close enough, and how
4 * results of words and meaning are fused. Pure; hybrid search runs it
5 * over Vectorize and D1.
6 */
7
8/**
9 * The least cosine similarity a passage needs to count as being about the
10 * query. Measured on g1t's own docs folder (606 passages, chunked and
11 * embedded as here, bge-base-en-v1.5 with Workers AI's mean pooling): the
12 * best passage for 16 questions the docs answer scored 0.72 to 0.84, while
13 * the best for 16 they don't ("hello", a recipe, Postgres tuning, a
14 * vacation policy, SSO with Okta) scored 0.54 to 0.67. At 0.6, 11 of those
15 * 16 pulled in passages (up to 89 above it); 0.7 keeps every answer and
16 * keeps recall quiet when the docs say nothing about the question.
17 */
18export const MEANING_FLOOR = 0.7;
19/** Nearest passages asked of the index. */
20export const TOP_K = 24;
21/** Asked when the index can't filter by space and recall filters after: more, so enough are left. */
22export const TOP_K_UNFILTERED = 50;
23/** The most space ids put in one `$in` filter; past it, filter after the query (a Vectorize filter is at most 2 KB of JSON). */
24export const MAX_IN_FILTER = 40;
25
26/**
27 * How to ask the index: by the allowed spaces when there are few enough
28 * to name, otherwise by workspace alone, more of them, filtered after.
29 * Null when nothing may be read.
30 */
31export function vectorQueryPlan(workspaceId: string, allowed: string[]): { topK: number; filter: { workspace_id: string; space_ids?: string[] }; filterAfter: boolean } | null {
32 const ids = [...new Set(allowed)];
33 if (!ids.length) return null;
34 if (ids.length <= MAX_IN_FILTER) return { topK: TOP_K, filter: { workspace_id: workspaceId, space_ids: ids }, filterAfter: false };
35 return { topK: TOP_K_UNFILTERED, filter: { workspace_id: workspaceId }, filterAfter: true };
36}
37
38/**
39 * Hybrid search's order for people: each document's place in the word
40 * results and in the meaning results, fused by reciprocal rank (k = 60),
41 * so a page both find comes first and either alone still counts.
42 */
43export function fuseRanks(words: string[], meaning: string[], k = 60): string[] {
44 const score = new Map<string, number>();
45 const add = (ids: string[]) =>
46 [...new Set(ids)].forEach((id, rank) => {
47 score.set(id, (score.get(id) ?? 0) + 1 / (k + rank + 1));
48 });
49 add(words);
50 add(meaning);
51 return [...score.entries()].sort((a, b) => b[1] - a[1]).map(([id]) => id);
52}
53
54/**
55 * A query's embeddings, kept a minute per isolate: a search page
56 * reloaded embeds once.
57 */
58export class QueryCache {
59 private readonly entries = new Map<string, { at: number; vector: number[] }>();
60 private readonly ttlMs: number;
61 private readonly max: number;
62 constructor(ttlMs = 60_000, max = 200) {
63 this.ttlMs = ttlMs;
64 this.max = max;
65 }
66
67 get(key: string, now = Date.now()): number[] | null {
68 const hit = this.entries.get(key);
69 if (!hit) return null;
70 if (now - hit.at > this.ttlMs) {
71 this.entries.delete(key);
72 return null;
73 }
74 return hit.vector;
75 }
76
77 set(key: string, vector: number[], now = Date.now()): void {
78 if (this.entries.size >= this.max) {
79 for (const [k, v] of this.entries) if (now - v.at > this.ttlMs) this.entries.delete(k);
80 while (this.entries.size >= this.max) this.entries.delete(this.entries.keys().next().value!);
81 }
82 this.entries.set(key, { at: now, vector });
83 }
84}
85
86/** A query as the cache keys it: the same words, however spaced or cased. */
87export function queryKey(query: string): string {
88 return String(query ?? "").replace(/\s+/g, " ").trim().toLowerCase().slice(0, 2000);
89}