Joel Stevick

Appendix B · section 15 of 32

ask/vectors.ts

From Finding It by Meaning.

import { fileURLToPath } from "node:url";
import { LAWS, type Law } from "./corpus.js";

// The meaning half of retrieval. BM25 finds a law that shares the question's
// words; this finds one that shares its meaning -- "can I leave my car right
// by a fireplug", or the same question in Spanish against English law text,
// neither of which shares a word with "within 15 feet of a fire hydrant".
//
// Only ids come back from here. The words of every law shown, and the citation
// under it, are read from the corpus by id and checked exactly as before, so
// the index can rank badly but cannot put text on screen.

/** Multilingual, so a Spanish question lands near the English law that answers it. */
export const EMBEDDING_MODEL = "@cf/baai/bge-m3";

/** How many neighbours a question asks for. */
export const NEAR_LAWS = 8;

// Below this a neighbour is not a match, only the nearest thing there was.
// Set for recall, not precision: a law that does not answer costs a few
// tokens and is declined by the model, with the checker behind it; a law
// that does answer and is cut here is a learner told their manual does not
// say.
export const NEAR_ENOUGH = 0.5;

export type Embed = (texts: string[]) => Promise<number[][]>;

/** What is embedded for a law: its rule and the card's question, which is how a learner would ask it. */
export function embeddingText(law: Pick<Law, "law" | "question">): string {
  return `${law.law} ${law.question}`.trim();
}

// Workers AI over Cloudflare's REST API. Absent credentials mean the stage
// does not exist: the caller gets `undefined` and retrieves by words alone.
export function workersAiEmbedder(): Embed | undefined {
  const account = process.env.CLOUDFLARE_ACCOUNT_ID;
  const token = process.env.CLOUDFLARE_API_TOKEN;
  if (!account || !token) return undefined;
  return async (texts) => {
    const response = await fetch(
      `https://api.cloudflare.com/client/v4/accounts/${account}/ai/run/${EMBEDDING_MODEL}`,
      {
        method: "POST",
        headers: { Authorization: `Bearer ${token}`, "Content-Type": "application/json" },
        body: JSON.stringify({ text: texts }),
      },
    );
    const parsed = (await response.json()) as {
      success: boolean;
      errors: unknown;
      result?: { data: number[][] };
    };
    if (!parsed.success || parsed.result === undefined) {
      throw new Error(`embedding failed: ${response.status} ${JSON.stringify(parsed.errors)}`);
    }
    if (parsed.result.data.length !== texts.length) {
      throw new Error(`asked to embed ${texts.length} texts, given ${parsed.result.data.length} vectors`);
    }
    return parsed.result.data;
  };
}

/** How alike two vectors point: 1 for the same direction, 0 for unrelated. */
export function cosine(a: number[], b: number[]): number {
  let dot = 0;
  let aa = 0;
  let bb = 0;
  for (let i = 0; i < a.length; i += 1) {
    dot += a[i] * b[i];
    aa += a[i] * a[i];
    bb += b[i] * b[i];
  }
  return dot / (Math.sqrt(aa) * Math.sqrt(bb));
}

export interface Index {
  ids: string[];
  vectors: number[][];
}

/** At most this many texts in one embedding request. */
const PER_REQUEST = 100;

export async function buildIndex(laws: Law[], embed: Embed): Promise<Index> {
  const vectors: number[][] = [];
  for (let at = 0; at < laws.length; at += PER_REQUEST) {
    vectors.push(...(await embed(laws.slice(at, at + PER_REQUEST).map(embeddingText))));
  }
  return { ids: laws.map((law) => law.id), vectors };
}

/** The ids nearest a vector, best first, cut below `cutoff`. */
export function nearest(
  index: Index,
  vector: number[],
  topK = NEAR_LAWS,
  cutoff = NEAR_ENOUGH,
): { id: string; score: number }[] {
  return index.ids
    .map((id, i) => ({ id, score: cosine(vector, index.vectors[i]) }))
    .filter((hit) => hit.score >= cutoff)
    .sort((a, b) => b.score - a.score)
    .slice(0, topK);
}

export type NearLaws = (question: string) => Promise<string[]>;

export function nearLawsFor(index: Index, embed: Embed): NearLaws {
  return async (question) => {
    const [vector] = await embed([question]);
    return nearest(index, vector).map((hit) => hit.id);
  };
}

async function main(): Promise<void> {
  const question = process.argv.slice(2).join(" ");
  if (question.length === 0) {
    console.error('usage: tsx ask/vectors.ts "<question>"');
    process.exit(1);
  }
  const embed = workersAiEmbedder();
  if (embed === undefined) {
    console.error("CLOUDFLARE_ACCOUNT_ID and CLOUDFLARE_API_TOKEN are not set; without them this stage does not exist.");
    process.exit(1);
  }
  const index = await buildIndex(LAWS, embed);
  const [vector] = await embed([question]);
  for (const hit of nearest(index, vector, NEAR_LAWS, 0)) {
    const law = LAWS.find((each) => each.id === hit.id);
    console.log(`${hit.score.toFixed(3)} ${hit.score >= NEAR_ENOUGH ? " " : "x"} ${hit.id}  ${law?.law ?? ""}`);
  }
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
  main().catch((error: unknown) => {
    console.error(error instanceof Error ? error.message : String(error));
    process.exit(1);
  });
}