CS145 NanoMem

Starter release: October 16, 2026.

Where to run:

  1. Colab (recommended): Enable a GPU if available: Runtime → Change runtime type → GPU, then set EMBED_DEVICE = "cuda" in Configuration. CPU also works; leave EMBED_DEVICE = "cpu".

  2. Local Jupyter: If you have an M4-or-newer Mac and are comfortable with Jupyter, you can run locally. Other capable machines work too.

The optional bonus benchmark must run in the specified Colab runtime.

Project instructions and grading

Provided: dataset loaders, embedding and storage helpers, SQLite baselines, evaluation helpers, and tests.

You implement: BM25, LSH, fusion, routing, and benchmark drivers. For Tiers 93–100, also build and query within the memory budget. Incremental loading and disk partitioning are suggested approaches, not a required streaming framework.

Using the project page and notebook: We’ve aimed to keep the project page and notebook consistent. The project page gives the full requirements and grading; the notebook provides compact guidance, function contracts, and starter code.

The supplied helpers support the standard datasets through T87; you still implement the assigned components. For T93–T100, expect to adapt or replace the loading and indexing helpers to meet the larger-data and memory requirements. Test changes on a small subset before scaling up.

You’re welcome to adapt the starter code within the project requirements. If something seems inconsistent or buggy, check the project page and ask privately on Ed; include the error and a small reproducible example for bugs. Note important assumptions in your notebook so we can take them into account when reviewing your work.

How to use this notebook

Run Setup once per runtime; expand supplied code only when needed. Set your dataset and device in Configuration, then complete the phases below. Avoid Run all until your implementations are complete.

Important: Optional checkpoints (Phase 1 onward). Uncomment and run the Drive checkpoint cell after Setup to save files written to CACHE_DIR and INDEX_DIR in your Google Drive. Recommended for longer runs; not required for Phase 0. This does not automatically save objects in memory.

Setup

Supplied utilities — run these cells, but you do not need to read or rewrite them. They define loaders, embeddings, SQLite references, and evaluation helpers. The large-data loader does not download anything until you call it in Tier 93.

After running all cells in this Setup section, jump to Phase 0. Run its baseline cells and fill in the proposal summary. Your retrieval implementation begins in Phase 1; look for YOU IMPLEMENT and NotImplementedError.

Install dependencies
# @title Install dependencies
!pip install -q pyarrow numpy sentence-transformers datasets sqlite-vec tqdm psutil
Configuration and core utilities
# @title Configuration and core utilities
import gc
import hashlib
import json
import os
import pickle
import random
import re
import sqlite3
import sys
import time
from collections import Counter, defaultdict
from pathlib import Path
from typing import Optional

import numpy as np
import pyarrow as pa
import pyarrow.parquet as pq
import psutil
import sqlite_vec
from sentence_transformers import SentenceTransformer
from tqdm import tqdm

random.seed(42)
np.random.seed(42)

EMBEDDING_MODEL = "all-MiniLM-L6-v2"  # 384-dim, fast, good enough
EMBEDDING_DIM = 384
INDEX_DIR = Path("./nanomem_index")
INDEX_DIR.mkdir(exist_ok=True)
CACHE_DIR = Path("./bench_cache")
CACHE_DIR.mkdir(exist_ok=True)

PROC = psutil.Process(os.getpid())


def mem_mb() -> float:
    """Current RSS in MB. Useful for the writeup section."""
    return PROC.memory_info().rss / 1_000_000


class MemoryBudget:
    """Track peak memory (RSS) across a block of code plus across the whole run.

    Use as a context manager around any code where memory matters.
    Required at Tier 93 (target 2000) and Tier 100 (target 1000).

        with MemoryBudget("Phase 1 build", limit_mb=2000) as mb:
            inv_idx.build(docs)
            mb.update()                # sample again after allocation-heavy steps
        # prints: [mem] Phase 1 build: peak 1340 MB / budget 2000 MB

    At the end of the run:
        print(f"Run peak: {MemoryBudget.run_peak():.0f} MB")

    The eval harness uses this automatically and reports peak_mb in
    every result dict, so your scoreboard table can quote it.

    Note: this reports `psutil` peak RSS, not the conceptual working set.
    At Tier 93/100, peak RSS will often read higher than 2 GB even when
    you only have one shard live at a time, because Python doesn't return
    freed pages to the OS. We assess live memory use by reading
    your notebook; this number is a soft signal. **Keep the `[mem]`
    prints visible in the submitted notebook.**
    """

    _run_peak: float = 0.0

    def __init__(self, label: str, limit_mb: float | None = None):
        self.label = label
        self.limit_mb = limit_mb
        self.start: float = 0.0
        self.peak: float = 0.0

    def __enter__(self):
        self.start = mem_mb()
        self.peak = self.start
        return self

    def update(self) -> float:
        cur = mem_mb()
        self.peak = max(self.peak, cur)
        MemoryBudget._run_peak = max(MemoryBudget._run_peak, self.peak)
        return cur

    def __exit__(self, *args):
        self.update()
        msg = f"[mem] {self.label}: peak {self.peak:.0f} MB"
        if self.limit_mb is not None:
            msg += f" / budget {self.limit_mb:.0f} MB"
            if self.peak > self.limit_mb:
                msg += " (RSS above target; inspect live working set)"
        print(msg)
        return False  # do not suppress exceptions

    @classmethod
    def run_peak(cls) -> float:
        """Highest RSS seen inside any MemoryBudget block so far."""
        return cls._run_peak

    @classmethod
    def reset(cls):
        cls._run_peak = 0.0


def memory_breakdown(components: dict, label: str = "memory profile") -> dict:
    """Print a per-component memory breakdown table.

    Use this to figure out *where* your memory is going before you decide
    what to optimize at Tier 93 or Tier 100. Pass a dict of
    {label: object}; values can be:

      - numpy.ndarray:    uses .nbytes (exact)
      - InvertedIndex:    walks postings dict + doc_lengths
      - LSHIndex:         walks hyperplane arrays + bucket dicts
      - int / float:      treated as MB directly (use for measurements
                          you took elsewhere, e.g., measured native allocations)
      - anything else:    sys.getsizeof(obj) / 1_000_000 (approximate)

    Returns the {label: mb} dict so you can also persist it.

    **Keep the printed table visible in your notebook for Tier 93/100.**
    These are approximate component sizes, not proof of a peak-memory bound.
    Include embeddings and document text separately from their indexes.
    Account for shared state, native allocations, and temporary buffers.

    Measure components on your own data. Choose partition sizes from those
    measurements, leaving room for shared state and temporary buffers.
    """
    import sys
    sizes = {}
    for k, v in components.items():
        if isinstance(v, np.ndarray):
            sizes[k] = v.nbytes / 1_000_000
        elif isinstance(v, (int, float)):
            sizes[k] = float(v)
        elif hasattr(v, 'postings') and hasattr(v, 'doc_lengths'):
            n = sys.getsizeof(v.postings)
            for term, plist in v.postings.items():
                n += sys.getsizeof(term) + sys.getsizeof(plist) + len(plist) * 56
            n += sys.getsizeof(v.doc_lengths)
            sizes[k] = n / 1_000_000
        elif hasattr(v, 'bands'):
            n = 0
            for hp, buckets in v.bands:
                n += hp.nbytes if isinstance(hp, np.ndarray) else 0
                n += sys.getsizeof(buckets)
                for _bid, doc_ids in buckets.items():
                    n += sys.getsizeof(doc_ids) + len(doc_ids) * 28
            sizes[k] = n / 1_000_000
        else:
            sizes[k] = sys.getsizeof(v) / 1_000_000

    total = sum(sizes.values())
    print(f"\n=== {label} ===")
    print(f"{'COMPONENT':<28} {'MB':>10} {'%':>6}")
    print("-" * 46)
    for k in sorted(sizes, key=lambda x: -sizes[x]):
        mb = sizes[k]
        pct = (100 * mb / total) if total else 0
        print(f"{k:<28} {mb:>10.1f} {pct:>5.1f}%")
    print("-" * 46)
    print(f"{'TOTAL':<28} {total:>10.1f}  100.0%")
    print(f"process peak so far:        {MemoryBudget.run_peak():>7.1f} MB")
    return sizes

Embedding model (supplied helpers, frozen)

Embedding helpers
# @title Embedding helpers
# CPU works locally and in free Colab; change to "cuda" if your Colab has a GPU.
EMBED_DEVICE = "cpu"
import torch
torch.set_num_threads(min(4, os.cpu_count() or 1))
_embed_model = None


def get_embedder() -> SentenceTransformer:
    global _embed_model
    if _embed_model is None:
        print("Loading embedding model (one-time)...")
        _embed_model = SentenceTransformer(EMBEDDING_MODEL, device=EMBED_DEVICE)
    return _embed_model


def embed_text(text: str) -> np.ndarray:
    """Return a (EMBEDDING_DIM,) float32 unit vector."""
    v = get_embedder().encode([text], convert_to_numpy=True)[0].astype(np.float32)
    return v / (np.linalg.norm(v) + 1e-12)


def embed_batch(texts: list[str], batch_size: int = 32) -> np.ndarray:
    """Return an (N, EMBEDDING_DIM) float32 matrix of unit vectors."""
    M = get_embedder().encode(
        texts, batch_size=batch_size, show_progress_bar=True, convert_to_numpy=True
    ).astype(np.float32)
    norms = np.linalg.norm(M, axis=1, keepdims=True) + 1e-12
    return M / norms


def cached_embed_batch(doc_ids: list[str], texts: list[str], cache_path: Path) -> np.ndarray:
    """Compute embeddings once, save to Parquet, reload on subsequent calls.

    Use this to reuse embeddings across runs. Time a small batch on your
    machine before estimating the full run; document length affects runtime.
    The cache key is `cache_path`; pick a name that includes the dataset and
    tier (e.g., `bench_cache/codesearchnet_250k_embs.parquet`).

    On Colab specifically: Colab's local disk does NOT persist across session
    restarts. Mount Google Drive and point `cache_path` there:

        from google.colab import drive
        drive.mount('/content/drive')
        CACHE_DIR = Path("/content/drive/MyDrive/nanomem_cache")
        CACHE_DIR.mkdir(exist_ok=True)
        embs = cached_embed_batch(doc_ids, texts, CACHE_DIR / "codesearchnet_250k.parquet")

    For Tier 100, cache bounded batches separately so you can resume without
    holding the full embedding matrix in memory.

    The sidecar fingerprint includes document IDs, text, model configuration,
    and normalization. Changing any of these invalidates the cached embeddings.
    """
    if len(doc_ids) != len(texts):
        raise ValueError("Provide one text per document ID.")
    model = get_embedder()
    settings = {"model": EMBEDDING_MODEL, "dimension": EMBEDDING_DIM,
                "max_seq_length": model.max_seq_length,
                "prompts": getattr(model, "prompts", {}),
                "default_prompt": getattr(model, "default_prompt_name", None),
                "normalization": "l2-float32-v1"}
    key = hashlib.sha256()
    key.update(json.dumps(settings, sort_keys=True, default=str).encode())
    for doc_id, text in zip(doc_ids, texts):
        key.update(json.dumps([doc_id, text], ensure_ascii=False).encode("utf-8"))
        key.update(b"\n")
    key = key.hexdigest()
    cache_path.parent.mkdir(parents=True, exist_ok=True)
    key_path = cache_path.with_suffix(cache_path.suffix + ".key")
    if cache_path.exists() and key_path.exists() and key_path.read_text().strip() == key:
        _, cached_embs = load_embeddings_parquet(cache_path)
        print(f"  [embed cache HIT] {cache_path.name} ({cached_embs.shape[0]} docs)")
        return cached_embs
    reason = "no cache" if not cache_path.exists() else "input or model changed"
    print(f"  [embed cache MISS: {reason}] computing embeddings for {len(texts)} docs...")
    embs = embed_batch(texts)
    save_embeddings_parquet(doc_ids, embs, cache_path)
    key_path.write_text(key)
    print(f"  [embed cache WROTE] {cache_path.name}")
    return embs

Tokenizer (supplied helpers, modifiable)

Tokenizer
# @title Tokenizer
_TOKEN_RE = re.compile(r"[A-Za-z_][A-Za-z0-9_]*")


def tokenize(text: str) -> list[str]:
    """Lowercase identifier-style tokenizer.

    Splits on non-identifier characters; keeps `verify_jwt` as one token.
    Modify if you want, but you do not have to.
    """
    return [t.lower() for t in _TOKEN_RE.findall(text)]

Storage helpers (supplied helpers)

Storage helpers
# @title Storage helpers
def save_documents_json(docs: list[dict], path: Path) -> None:
    with open(path, "w") as f:
        for d in docs:
            f.write(json.dumps(d) + "\n")


def load_documents_json(path: Path) -> list[dict]:
    docs = []
    with open(path) as f:
        for line in f:
            docs.append(json.loads(line))
    return docs


def save_embeddings_parquet(doc_ids: list[str], embeddings: np.ndarray, path: Path) -> None:
    """One Parquet file. doc_id (string) + embedding (fixed-size list of float32)."""
    embeddings = np.ascontiguousarray(embeddings, dtype=np.float32)
    if embeddings.shape != (len(doc_ids), EMBEDDING_DIM):
        raise ValueError("Provide one embedding of the expected dimension per document.")
    table = pa.table({
        "doc_id": pa.array(doc_ids, type=pa.string()),
        "embedding": pa.FixedSizeListArray.from_arrays(
            pa.array(embeddings.reshape(-1), type=pa.float32()),
            EMBEDDING_DIM,
        ),
    })
    pq.write_table(table, path, compression="snappy")


def load_embeddings_parquet(path: Path) -> tuple[list[str], np.ndarray]:
    table = pq.read_table(path)
    doc_ids = table.column("doc_id").to_pylist()
    column = table.column("embedding").combine_chunks()
    embs = column.flatten().to_numpy(zero_copy_only=False).reshape(-1, EMBEDDING_DIM)
    return doc_ids, embs


def brute_force_cosine(query_vec: np.ndarray, embeddings: np.ndarray, top_k: int = 10) -> list[int]:
    """Return the top_k indices into `embeddings` by cosine similarity."""
    scores = embeddings @ query_vec
    return list(np.argsort(-scores)[:top_k])

LSH math helpers (supplied helpers)

LSH math helpers
# @title LSH math helpers
def random_projection_matrix(n_hyperplanes: int, dim: int = EMBEDDING_DIM) -> np.ndarray:
    """Returns (n_hyperplanes, dim) standard-normal hyperplanes."""
    return np.random.randn(n_hyperplanes, dim).astype(np.float32)


def hash_vector(vec: np.ndarray, hyperplanes: np.ndarray) -> int:
    """Project vec onto each hyperplane; return bit signature as int."""
    bits = (hyperplanes @ vec > 0).astype(np.uint8)
    sig = 0
    for b in bits:
        sig = (sig << 1) | int(b)
    return sig


def hash_batch(M: np.ndarray, hyperplanes: np.ndarray) -> np.ndarray:
    """Vectorized hash of an (N, dim) matrix. Returns (N,) int64 sigs."""
    bits = (M @ hyperplanes.T > 0).astype(np.uint8)
    n_hp = bits.shape[1]
    weights = (1 << np.arange(n_hp - 1, -1, -1)).astype(np.int64)
    return (bits.astype(np.int64) * weights).sum(axis=1)

SQLite reference helpers (used in Phase 0 and Phase 5)

SQLite reference helpers
# @title SQLite reference helpers
def open_sqlite_db(path: Path) -> sqlite3.Connection:
    """Open a SQLite connection with the sqlite-vec extension loaded."""
    path.parent.mkdir(parents=True, exist_ok=True)
    conn = sqlite3.connect(str(path))
    try:
        conn.execute("CREATE VIRTUAL TABLE temp.fts_check USING fts5(text)")
        conn.execute("DROP TABLE temp.fts_check")
        conn.enable_load_extension(True)
        sqlite_vec.load(conn)
        conn.enable_load_extension(False)
    except (AttributeError, sqlite3.Error) as exc:
        conn.close()
        raise RuntimeError("SQLite needs FTS5 and loadable extensions. Use the supplied Colab setup or a Python build with both enabled.") from exc
    return conn


def initialize_sqlite_corpus(conn: sqlite3.Connection) -> None:
    """Create empty reference tables. Call once before inserting batches.

    This resets the reference corpus; do not call between batches.
    """
    with conn:
        conn.execute("DROP TABLE IF EXISTS docs_fts")
        conn.execute("DROP TABLE IF EXISTS docs_vec")
        conn.execute("DROP TABLE IF EXISTS doc_id_map")
        conn.execute("CREATE VIRTUAL TABLE docs_fts USING fts5(doc_id UNINDEXED, content)")
        conn.execute(f"CREATE VIRTUAL TABLE docs_vec USING vec0(doc_idx INTEGER PRIMARY KEY, embedding float[{EMBEDDING_DIM}])")
        conn.execute("CREATE TABLE doc_id_map(doc_idx INTEGER PRIMARY KEY, doc_id TEXT UNIQUE)")


def append_sqlite_batch(conn: sqlite3.Connection, docs: list[dict], embeddings: np.ndarray) -> None:
    """Append one bounded batch; retain earlier batches. IDs must be unique.

    Initialize once, then embed and append each batch. A failed batch rolls back.
    """
    embeddings = np.ascontiguousarray(embeddings, dtype=np.float32)
    if embeddings.shape != (len(docs), EMBEDDING_DIM) or not np.isfinite(embeddings).all():
        raise ValueError("Provide one finite embedding of the expected dimension per document.")
    if len({d["doc_id"] for d in docs}) != len(docs):
        raise ValueError("Document IDs must be unique.")
    start = conn.execute("SELECT COALESCE(MAX(doc_idx), -1) + 1 FROM doc_id_map").fetchone()[0]
    with conn:
        conn.executemany("INSERT INTO doc_id_map(doc_idx, doc_id) VALUES (?, ?)",
                         ((start+i, d["doc_id"]) for i,d in enumerate(docs)))
        conn.executemany("INSERT INTO docs_fts(doc_id, content) VALUES (?, ?)",
                         ((d["doc_id"], d["text"]) for d in docs))
        conn.executemany("INSERT INTO docs_vec(doc_idx, embedding) VALUES (?, ?)",
                         ((start+i, embeddings[i].tobytes()) for i in range(len(docs))))


def build_sqlite_corpus(conn: sqlite3.Connection, docs: list[dict], embeddings: np.ndarray) -> None:
    """Rebuild a small reference corpus. Larger runs use initialize + append."""
    initialize_sqlite_corpus(conn)
    append_sqlite_batch(conn, docs, embeddings)


def fts5_query(conn: sqlite3.Connection, query: str, top_k: int = 10) -> list[tuple[str, float]]:
    """BM25 query via FTS5. Returns [(doc_id, score), ...] sorted by relevance."""
    tokens = re.findall(r"[A-Za-z0-9_]+", query)
    if not tokens:
        return []
    fts_query = " OR ".join(f'"{t}"' for t in tokens)
    try:
        rows = conn.execute(
            "SELECT doc_id, -bm25(docs_fts) FROM docs_fts WHERE docs_fts MATCH ? ORDER BY rank LIMIT ?",
            (fts_query, top_k)).fetchall()
        return [(r[0], r[1]) for r in rows]
    except sqlite3.OperationalError as exc:
        raise RuntimeError("FTS5 query failed; inspect the index and query instead of treating this as zero matches.") from exc


def vec_query(conn: sqlite3.Connection, query_vec: np.ndarray, top_k: int = 10) -> list[tuple[str, float]]:
    """Exact vector KNN via sqlite-vec. Returns [(doc_id, score), ...].

    Score is negative L2 distance: higher is better, but it is not cosine similarity.
    """
    rows = conn.execute(
        """SELECT m.doc_id, -v.distance FROM docs_vec v
           JOIN doc_id_map m ON v.doc_idx = m.doc_idx
           WHERE v.embedding MATCH ? AND k = ? ORDER BY distance LIMIT ?""",
        (query_vec.tobytes(), top_k, top_k)).fetchall()
    return [(r[0], r[1]) for r in rows]

Corpus loaders

Benchmark note: CodeSearchNet queries come from docstrings included in the indexed functions, so keyword matching has an advantage. Compare your implementation against its corresponding SQLite baseline; vector search need not outperform keyword search.

Corpus loaders
# @title Corpus loaders
# These helpers load small corpora into memory. For Tiers 93/100, adapt loading to stay within the memory budget; fixed counts do not establish byte size.
TIER_SIZES = {"tiny": 1_000, "25k": 25_000, "250k": 250_000}


def load_codesearchnet(tier: str = "tiny") -> tuple[list[dict], list[dict]]:
    """Load CodeSearchNet at the chosen tier. Returns (docs, eval_set).

    eval_set is built from docstrings: query is the first nonempty line of the
    docstring; relevant_doc_ids is [the function whose docstring it was].
    """
    from datasets import load_dataset

    if tier not in TIER_SIZES:
        raise ValueError(f"unknown tier {tier!r}; choose from {list(TIER_SIZES)}")
    n = TIER_SIZES[tier]
    use_streaming = True

    ds = load_dataset(
        "code-search-net/code_search_net", "python", split="train",
        streaming=use_streaming,
    )
    docs, eval_pool = [], []
    for row in ds:
        if len(docs) >= n:
            break
        code = row["func_code_string"]
        if not code:
            continue
        doc_id = f"python/{row['repository_name']}/{row['func_name']}::{len(docs)}"
        docs.append({"doc_id": doc_id, "text": code})
        docstring = (row.get("func_documentation_string") or "").strip()
        if docstring:
            first = next((line.strip() for line in docstring.split("\n") if line.strip()), "")
            if first and 20 <= len(first) <= 200:
                eval_pool.append({"query": first, "relevant_doc_ids": [doc_id]})
    if len(docs) < n:
        raise ValueError(f"Requested {n:,} documents but source supplied {len(docs):,}. Use a smaller tier or extend the loader with documented sources.")
    rng = random.Random(42)
    eval_n = min(500, len(eval_pool))
    eval_set = rng.sample(eval_pool, eval_n)
    return docs, eval_set


def load_msmarco(tier: str = "tiny") -> tuple[list[dict], list[dict]]:
    """Load MS MARCO at the chosen tier. Returns (docs, eval_set).

    eval_set is real Bing/SQ queries from the BeIR/msmarco-qrels training split,
    filtered to those whose relevant docs are in the loaded subset.
    """
    from datasets import load_dataset

    if tier not in TIER_SIZES:
        raise ValueError(f"unknown tier {tier!r}; choose from {list(TIER_SIZES)}")
    n = TIER_SIZES[tier]

    corpus_ds = load_dataset("BeIR/msmarco", "corpus", split="corpus", streaming=True)
    queries_ds = load_dataset("BeIR/msmarco", "queries", split="queries", streaming=True)
    qrels_ds = load_dataset("BeIR/msmarco-qrels", split="train", streaming=True)

    docs = []
    docs_in_subset = set()
    for i, row in enumerate(corpus_ds):
        if i >= n:
            break
        doc_id = str(row["_id"])
        text = ((row.get("title") or "") + " " + (row.get("text") or "")).strip()
        docs.append({"doc_id": doc_id, "text": text})
        docs_in_subset.add(doc_id)

    qid_to_text = {str(row["_id"]): row["text"] for row in queries_ds}
    qid_to_relevant = {}
    for row in qrels_ds:
        qid = str(row["query-id"])
        did = str(row["corpus-id"])
        if did in docs_in_subset and int(row["score"]) > 0:
            qid_to_relevant.setdefault(qid, []).append(did)
    eligible = [q for q, r in qid_to_relevant.items() if r and q in qid_to_text]
    if len(docs) < n:
        raise ValueError(f"Requested {n:,} documents but source supplied {len(docs):,}. Use a smaller tier or extend the loader with documented sources.")
    rng = random.Random(42)
    eval_n = min(500, len(eligible))
    eval_set = [{"query": qid_to_text[q], "relevant_doc_ids": qid_to_relevant[q]}
                for q in rng.sample(eligible, eval_n)]
    return docs, eval_set

Eval harness (supplied helpers)

Evaluation helpers
# @title Evaluation helpers
def recall_at_k(retrieved: list[str], relevant: list[str], k: int = 5) -> float:
    """Fraction of distinct relevant documents found in the first k results."""
    relevant_ids = set(relevant)
    return len(set(retrieved[:k]) & relevant_ids) / len(relevant_ids) if relevant_ids else 0.0


def mean_reciprocal_rank(retrieved: list[str], relevant: list[str]) -> float:
    rel_set = set(relevant)
    for i, doc in enumerate(retrieved, start=1):
        if doc in rel_set:
            return 1.0 / i
    return 0.0


def run_eval(query_fn, eval_set: list[dict], top_k: int = 10,
             memory_limit_mb: float | None = None) -> dict:
    """Run query_fn against an eval set. Reports R@1, R@5, R@10, MRR@10,
    p50/p95 latency, AND peak RSS during the run.

    Pass `memory_limit_mb=2000` for Tier 93 or `memory_limit_mb=1000`
    for Tier 100 to label the RSS report with the tier target. This
    does not enforce or measure the live working-set cap. Report live
    components separately with memory_breakdown and explain how your implementation stays within the memory budget.
    """
    r1, r5, r10, mrrs, lat = [], [], [], [], []
    with MemoryBudget("eval run", limit_mb=memory_limit_mb) as mb:
        for ex in eval_set:
            t0 = time.perf_counter()
            results = query_fn(ex["query"])
            lat.append((time.perf_counter() - t0) * 1000)
            mb.update()
            retrieved = [d for d, _ in results] if (results and isinstance(results[0], tuple)) else (results or [])
            r1.append(recall_at_k(retrieved, ex["relevant_doc_ids"], 1))
            r5.append(recall_at_k(retrieved, ex["relevant_doc_ids"], 5))
            r10.append(recall_at_k(retrieved, ex["relevant_doc_ids"], 10))
            mrrs.append(mean_reciprocal_rank(retrieved[:10], ex["relevant_doc_ids"]))
    return {
        "n": len(eval_set),
        "recall@1": float(np.mean(r1)),
        "recall@5": float(np.mean(r5)),
        "recall@10": float(np.mean(r10)),
        "mrr@10": float(np.mean(mrrs)),
        "p50_ms": float(np.percentile(lat, 50)),
        "p95_ms": float(np.percentile(lat, 95)),
        "peak_mb": mb.peak,
    }

Fixed Tier 93 data (supplied utility)

Run this definition with Setup. Call it only when you reach Tier 93.

Tier 93 data helper
# @title Tier 93 data helper
# Supplied utility: defines a loader; downloads nothing until you call it.
def prepare_tier93_data(data_dir="nanomem_scale"):
    import hashlib, importlib.util, urllib.request
    from pathlib import Path
    root = Path(data_dir)
    root.mkdir(parents=True, exist_ok=True)
    helper_path = root / "nanomem_benchmark_v1.py"
    payload = urllib.request.urlopen(
        "https://cs145.web.app/projects/nanomem/bonus/benchmark_v1.py"
    ).read()
    if hashlib.sha256(payload).hexdigest() != "c3acbaf9bfb77dd4bb471df1b67b8a3d371df09b4e9afc9ba4af96c9baaed1c1":
        raise ValueError("Helper version mismatch")
    helper_path.write_bytes(payload)
    spec = importlib.util.spec_from_file_location("nanomem_benchmark", helper_path)
    utility = importlib.util.module_from_spec(spec)
    spec.loader.exec_module(utility)
    data = utility.load_bonus_benchmark(root, config_sha256="75744dcb478a27f4ef75972361bcf015d626e88633cda85110da122d2ce2456f")
    utility.prepare_documents(data)
    return utility, data

Tiny dataset for unit tests

Tiny test data
# @title Tiny test data
TINY_CORPUS = [
    {"doc_id": "auth/jwt.py:1", "text": "def verify_jwt(token, secret): return jwt.decode(token, secret, algorithms=['HS256'])"},
    {"doc_id": "auth/jwt.py:2", "text": "def issue_jwt(user_id, secret, ttl=3600): return jwt.encode({'sub': user_id, 'exp': time.time() + ttl}, secret)"},
    {"doc_id": "auth/scopes.py:1", "text": "def verify_jwt_with_scope(token, secret, required_scope): payload = verify_jwt(token, secret); assert required_scope in payload['scopes']"},
    {"doc_id": "config/parser.py:1", "text": "def parse_config(path): with open(path) as f: return json.load(f)"},
    {"doc_id": "config/parser.py:2", "text": "def merge_configs(base, override): merged = dict(base); merged.update(override); return merged"},
    {"doc_id": "utils/load_json.py:1", "text": "def load_json(path): with open(path, 'r') as fh: return json.load(fh)"},
    {"doc_id": "utils/load_json.py:2", "text": "def save_json(obj, path): with open(path, 'w') as fh: json.dump(obj, fh, indent=2)"},
    {"doc_id": "db/connection.py:1", "text": "def connect_postgres(dsn): conn = psycopg2.connect(dsn); conn.autocommit = False; return conn"},
    {"doc_id": "db/connection.py:2", "text": "def close_connection(conn): conn.commit(); conn.close()"},
    {"doc_id": "db/transactions.py:1", "text": "def with_transaction(conn, fn): try: result = fn(conn); conn.commit(); return result; except: conn.rollback(); raise"},
    {"doc_id": "search/tokenize.py:1", "text": "def tokenize(text): return [w.lower() for w in re.split(r'\\W+', text) if w]"},
    {"doc_id": "search/bm25.py:1", "text": "def bm25_score(tf, df, n_docs, doc_len, avg_len, k1=1.2, b=0.75): idf = math.log((n_docs - df + 0.5) / (df + 0.5))"},
    {"doc_id": "cache/redis_client.py:1", "text": "def cache_get(key): return redis.get(key)"},
    {"doc_id": "cache/redis_client.py:2", "text": "def cache_set(key, value, ttl=300): redis.setex(key, ttl, value)"},
    {"doc_id": "models/user.py:1", "text": "class User: def __init__(self, id, email): self.id = id; self.email = email"},
    {"doc_id": "models/user.py:2", "text": "def get_user_by_email(session, email): return session.query(User).filter_by(email=email).first()"},
    {"doc_id": "api/routes.py:1", "text": "@app.route('/login', methods=['POST']) def login(): user = authenticate(request.json); return issue_jwt(user.id, SECRET)"},
    {"doc_id": "api/routes.py:2", "text": "@app.route('/logout', methods=['POST']) def logout(): revoke_token(request.headers['Authorization'])"},
    {"doc_id": "tests/test_auth.py:1", "text": "def test_jwt_roundtrip(): token = issue_jwt('user123', 'secret'); assert verify_jwt(token, 'secret')['sub'] == 'user123'"},
    {"doc_id": "tests/test_config.py:1", "text": "def test_parse_config(): cfg = parse_config('test.json'); assert cfg['db'] == 'postgres'"},
]

Configuration

Choose the dataset and embedding device here after running Setup.

# Small-data setting: tiny = 1,000; 25k = 25,000; 250k = 250,000 documents per corpus.
# Change this, then rerun Phase 0 and the phase execution cells you need.
DATASET = "tiny"
assert DATASET in {"tiny", "25k", "250k"}, "Use the bounded-memory scaling sections for Tiers 93/100."

# Match your runtime: "cuda" for a Colab GPU; otherwise "cpu".
EMBED_DEVICE = "cpu"

Phase 0: SQLite reference setup

Instructions

Run the cells below with DATASET = "tiny" first (1,000 documents per corpus). Then change DATASET in Configuration to "25k", rerun that setting cell, and rerun Phase 0 for the proposal.

No NanoMem implementation is needed here. Inspect the results, fill in your proposal summary, and stop here for the proposal.

Phase 0 baseline helpers
# @title Phase 0 baseline helpers
# Supplied setup: later phases contain the search algorithms you implement.
def setup_phase0(tier="tiny", queries=None):
    state = {}
    for corpus, loader in [("codesearchnet", load_codesearchnet), ("msmarco", load_msmarco)]:
        docs, eval_set = loader(tier)
        embs = cached_embed_batch([d["doc_id"] for d in docs], [d["text"] for d in docs],
                                  CACHE_DIR / f"{corpus}_{tier}.parquet")
        conn = open_sqlite_db(INDEX_DIR / f"{corpus}_phase0.db")
        build_sqlite_corpus(conn, docs, embs)
        query = (queries or {}).get(corpus) or (eval_set[0]["query"] if eval_set else None)
        if not query:
            raise ValueError(f"Choose a query for {corpus}.")
        results = {"fts5": fts5_query(conn, query, top_k=3),
                   "vector": vec_query(conn, embed_text(query), top_k=3)}
        stats = {"documents": len(docs), "text_bytes": sum(len(d["text"].encode("utf-8")) for d in docs),
                 "embedding_bytes": embs.nbytes, "dimensions": embs.shape[1]}
        print(corpus, stats, "query:", query)
        text_by_id = {d["doc_id"]: d["text"] for d in docs}
        for method, hits in results.items():
            print(method)
            for doc_id, score in hits:
                print(doc_id, "score:", round(score, 4), "text:", text_by_id[doc_id][:300])
        state[corpus] = dict(conn=conn, docs=docs, embs=embs, eval_set=eval_set, query=query, results=results, stats=stats)
    return state


def run_phase_0_tests(state):
    assert set(state) == {"codesearchnet", "msmarco"}
    for corpus, s in state.items():
        assert len(s["docs"]) > 0
        assert s["embs"].shape == (len(s["docs"]), EMBEDDING_DIM)
        assert s["results"]["vector"], f"{corpus}: vector search returned no results"
        # A query can legitimately have no lexical hits. Test indexing separately
        # using a token drawn from this corpus rather than the word 'function'.
        token = next(t for d in s["docs"] for t in tokenize(d["text"]) if t)
        assert fts5_query(s["conn"], token, top_k=1), f"{corpus}: lexical index check failed"
        print(corpus, "baseline checks passed")


def search_demo(corpus, query, top_k=3, method="both"):
    """Display ranked results from the existing Phase 0 indexes; no fusion."""
    from html import escape
    from IPython.display import HTML, display
    if method not in ("keyword", "vector", "both"):
        raise ValueError('Choose method="keyword", "vector", or "both".')
    if "phase0" not in globals() or corpus not in phase0:
        raise ValueError("Run the Phase 0 baseline first; choose codesearchnet or msmarco.")
    if not query.strip():
        raise ValueError("Enter a query.")
    if not isinstance(top_k, int) or top_k < 1:
        raise ValueError("top_k must be a positive integer.")
    s = phase0[corpus]
    print(f"Searching {s['stats']['documents']:,} documents in {corpus}: {query}")
    results = {}
    if method in ("keyword", "both"):
        results["keyword"] = fts5_query(s["conn"], query, top_k)
    if method in ("vector", "both"):
        results["vector"] = vec_query(s["conn"], embed_text(query), top_k)
    labels = {"keyword": "Keyword · BM25 score", "vector": "Vector · negative L2 distance"}
    headers = "<th>Rank</th>" + "".join(f"<th>{labels[m]}</th>" for m in results)
    rows = []
    for i in range(max(1, max(len(hits) for hits in results.values()))):
        columns = [f"<td>{i + 1}</td>"]
        for hits in results.values():
            if i >= len(hits):
                columns.append("<td>No match</td>")
                continue
            doc_id, score = hits[i]
            text = s["conn"].execute("SELECT content FROM docs_fts WHERE doc_id = ?", (doc_id,)).fetchone()[0]
            columns.append(f'<td style="vertical-align:top;overflow-wrap:anywhere"><strong>{escape(str(doc_id))}</strong><br>Score: {score:.4f}<br>{escape(text[:350])}</td>')
        rows.append("<tr>" + "".join(columns) + "</tr>")
    display(HTML('<table style="width:100%;table-layout:fixed"><thead><tr>' + headers + '</tr></thead><tbody>' + "".join(rows) + '</tbody></table>'))
    return results
# Run the supplied baseline. Rerunning closes the previous database connections first.
if "phase0" in globals():
    for previous in phase0.values():
        previous["conn"].close()
    del phase0
phase0 = setup_phase0(tier=DATASET)
run_phase_0_tests(phase0)

Try query variants

Keyword search matches words; vector search looks for similar meaning. Both retrieve documents, not a generated answer. Compare ranks and snippets; scores use different scales.

For each dataset, run the supplied query with method = "both", then write and run two variants. Keep all three ranking tables visible in your submitted notebook; no extra paragraph is needed. You can also try "keyword" or "vector" separately. These cells reuse your indexes. RRF comes later.

# CodeSearchNet: find a Python function
method = "both"  # "keyword", "vector", or "both"
queries = [
    "How do I check a password against its stored hash?",  # Query 1
    "",  # Query 2: write your own variant.
    "",  # Query 3: try another wording.
]
for number, query in enumerate(queries, 1):
    if query.strip():
        print(f"\nQuery {number}")
        _ = search_demo("codesearchnet", query, method=method)
# MS MARCO: find a passage
method = "both"  # "keyword", "vector", or "both"
queries = [
    "How can I roast coffee beans at home?",  # Query 1
    "",  # Query 2: write your own variant.
    "",  # Query 3: try another wording.
]
for number, query in enumerate(queries, 1):
    if query.strip():
        print(f"\nQuery {number}")
        _ = search_demo("msmarco", query, method=method)

Proposal: answer these questions

Fill in the answer column. Keep the query ranking tables and measured sizes above.

Question Your answer
Who is on your team, and how will you divide the work? [Answer]
Which tier are you targeting? [Answer]
Will you use local Jupyter, Colab, or both? [Answer]
What is your initial custom-fusion idea? [Answer]
For Tier 93/100, what data and memory-management plan will you use? [Answer, or not applicable]

Stop here for the proposal. Phase 1 begins your own implementation.

Save Colab checkpoints to Drive

For Colab, checkpoints are optional and recommended from Phase 1 onward; they are not required for Phase 0. Local Jupyter users can skip this cell. Uncomment and run the cell below after Setup, then approve access to Google Drive.

Check that files appear in MyDrive/nanomem_checkpoints and that Drive has enough space. Enabling checkpoints does not move existing local files or save objects in memory.

Without Drive checkpoints, embeddings go to bench_cache/ and indexes to nanomem_index/. Downloads use the Hugging Face cache. Colab’s local files can disappear when the runtime resets; saving the notebook does not save them.

# Optional: uncomment this cell to keep checkpoints in Google Drive.
# Recommended from Phase 1 onward; not required for Phase 0.
# Run AFTER Setup and BEFORE building indexes or computing embeddings.
# from pathlib import Path
# from google.colab import drive
# drive.mount("/content/drive")
# CHECKPOINT_DIR = Path("/content/drive/MyDrive/nanomem_checkpoints")
# CACHE_DIR = CHECKPOINT_DIR / "embeddings"
# INDEX_DIR = CHECKPOINT_DIR / "indexes"
# for folder in (CACHE_DIR, INDEX_DIR):
#     folder.mkdir(parents=True, exist_ok=True)
# print("Checkpoint folder:", CHECKPOINT_DIR)

Phase 1: Inverted index and BM25

Instructions

Implement BM25, run the tests, and compare Recall@5 with FTS5; explain design choices and quality gaps.

class InvertedIndex:
    """YOU IMPLEMENT.

    Required attributes after build:
      self.postings:        dict[str, list[tuple[int, int]]]   # term -> [(doc_idx, tf), ...]
      self.doc_lengths:     list[int]                          # tokens per doc
      self.avg_doc_length:  float
      self.n_docs:          int
      self.doc_ids:         list[str]                          # doc_idx -> doc_id
    """

    def __init__(self) -> None:
        self.postings: dict[str, list[tuple[int, int]]] = {}
        self.doc_lengths: list[int] = []
        self.avg_doc_length: float = 0.0
        self.n_docs: int = 0
        self.doc_ids: list[str] = []

    def build(self, documents: list[dict]) -> None:
        """Ingest a list of {'doc_id': str, 'text': str} into the index."""
        # YOUR CODE HERE.
        # 1. Iterate documents, tokenize, count term frequencies.
        # 2. Append (doc_idx, tf) to self.postings[term] for each term.
        # 3. Track doc_lengths and doc_ids.
        # 4. Compute avg_doc_length.
        raise NotImplementedError("Phase 1: implement InvertedIndex.build")

    def df(self, term: str) -> int:
        return len(self.postings.get(term, []))

    def save(self, path: Path) -> None:
        with open(path, "wb") as f:
            pickle.dump({
                "postings": self.postings, "doc_lengths": self.doc_lengths,
                "avg_doc_length": self.avg_doc_length, "n_docs": self.n_docs,
                "doc_ids": self.doc_ids,
            }, f)

    @classmethod
    def load(cls, path: Path) -> "InvertedIndex":
        idx = cls()
        with open(path, "rb") as f:
            d = pickle.load(f)
        idx.postings = d["postings"]; idx.doc_lengths = d["doc_lengths"]
        idx.avg_doc_length = d["avg_doc_length"]; idx.n_docs = d["n_docs"]
        idx.doc_ids = d["doc_ids"]
        return idx


def bm25_query(index: InvertedIndex, query: str, top_k: int = 10,
               k1: float = 1.2, b: float = 0.75) -> list[tuple[str, float]]:
    """YOU IMPLEMENT.

    Tokenize the query, look up each term's posting list, score each doc that
    contains at least one query term using BM25, return top_k as
    [(doc_id, score), ...] sorted by score descending.
    """
    # YOUR CODE HERE.
    raise NotImplementedError("Phase 1: implement bm25_query")

Phase 1 tests

Phase 1 tests and comparison helpers
# @title Phase 1 tests and comparison helpers
def run_phase_1_tests() -> None:
    idx = InvertedIndex()
    idx.build(TINY_CORPUS)
    assert idx.n_docs == 20, f"expected 20 docs, got {idx.n_docs}"
    assert idx.df("verify_jwt") >= 2, f"expected df('verify_jwt')>=2, got {idx.df('verify_jwt')}"
    assert idx.df("redis") == 2, f"expected df('redis')==2, got {idx.df('redis')}"
    print("  ✓ inverted_index_build")

    save_path = INDEX_DIR / "phase1_test.pkl"
    idx.save(save_path)
    idx2 = InvertedIndex.load(save_path)
    assert idx2.n_docs == idx.n_docs
    assert idx2.df("verify_jwt") == idx.df("verify_jwt")
    print("  ✓ inverted_index_save_load")

    results = bm25_query(idx, "verify jwt token", top_k=5)
    assert len(results) > 0
    assert "auth/jwt.py:1" in [d for d, _ in results][:3], f"top-3 missing JWT doc: {results}"
    print("  ✓ bm25_basic_recall")

    results = bm25_query(idx, "verify_jwt_with_scope", top_k=5)
    assert results[0][0] == "auth/scopes.py:1", f"expected scopes doc first, got {results}"
    print("  ✓ bm25_identifier_match")

    results = bm25_query(idx, "kubernetes deployment yaml", top_k=5)
    assert results == [] or all(s == 0 for _, s in results)
    print("  ✓ bm25_no_match")

    print("\nPhase 1: ALL PASS")


def phase_1_parity(corpus: str = "codesearchnet", tier: str = "25k") -> None:
    """Compare your BM25 against FTS5 on the same eval set.

    Reports the gap for analysis; there is no fixed recall-gap cutoff.
    """
    print(f"  loading {corpus} {tier}...")
    if corpus == "codesearchnet":
        docs, eval_set = load_codesearchnet(tier)
    else:
        docs, eval_set = load_msmarco(tier)
    print(f"  {len(docs):,} docs, {len(eval_set):,} eval queries")

    inv_idx = InvertedIndex()
    inv_idx.build(docs)

    # SQLite reference
    db_path = INDEX_DIR / f"phase1_parity_{corpus}.db"
    if db_path.exists():
        db_path.unlink()
    conn = open_sqlite_db(db_path)
    embs_dummy = np.zeros((len(docs), EMBEDDING_DIM), dtype=np.float32)
    build_sqlite_corpus(conn, docs, embs_dummy)

    yours = run_eval(lambda q: bm25_query(inv_idx, q, top_k=10), eval_set)
    fts5 = run_eval(lambda q: fts5_query(conn, q, 10), eval_set)
    print(f"\n  Your BM25 R@5:  {yours['recall@5']:.3f}  (p50 {yours['p50_ms']:.1f} ms)")
    print(f"  FTS5 BM25 R@5:  {fts5['recall@5']:.3f}  (p50 {fts5['p50_ms']:.1f} ms)")
    gap = fts5['recall@5'] - yours['recall@5']
    print(f"  Gap:            {gap*100:+.1f} pts")
    conn.close()
# Run after implementing Phase 1.
run_phase_1_tests()
for corpus in ("codesearchnet", "msmarco"):
    phase_1_parity(corpus=corpus, tier=DATASET)

Your notes: FTS5 comparison and design choices

[Your answer]

Phase 2: LSH vector search

Instructions

Implement LSH, run the tests, and compare Recall@5 with sqlite-vec; explain the speed/recall tradeoff.

class LSHIndex:
    """YOU IMPLEMENT.

    Suggested structure:
      self.bands: list[tuple[np.ndarray, dict[int, list[int]]]]
        # for each band: (hyperplanes (bits_per_band, dim), signature -> [doc_idx, ...])
      self.embeddings: np.ndarray   # (N, dim) for exact re-rank
      self.doc_ids:    list[str]
    """

    def __init__(self, n_bands: int = 60, bits_per_band: int = 3) -> None:
        self.n_bands = n_bands
        self.bits_per_band = bits_per_band
        self.bands: list[tuple[np.ndarray, dict[int, list[int]]]] = []
        self.embeddings: np.ndarray = np.zeros((0, EMBEDDING_DIM), dtype=np.float32)
        self.doc_ids: list[str] = []

    def build(self, doc_ids: list[str], embeddings: np.ndarray) -> None:
        """YOUR CODE.

        1. Generate `n_bands` random hyperplane matrices (each (bits_per_band, dim)).
        2. Hash all embeddings against each band; bucket doc_idx by signature.
        3. Store self.embeddings, self.doc_ids, self.bands.
        """
        raise NotImplementedError("Phase 2: implement LSHIndex.build")

    def query(self, query_vec: np.ndarray, top_k: int = 10,
              candidate_budget: int = 1000) -> list[tuple[str, float]]:
        """YOUR CODE.

        1. Hash query_vec against each band; collect candidate doc_idx from any matching bucket.
        2. RANK candidates by hit count (how many bands they appeared in).
           Naive set-union-then-truncate breaks at scale. See FAQ.
        3. Take top `candidate_budget` candidates by hit count.
        4. Exact-cosine re-rank candidates against self.embeddings.
        5. Return top_k as [(doc_id, score), ...].
        """
        raise NotImplementedError("Phase 2: implement LSHIndex.query")

Phase 2 tests

Phase 2 tests and comparison helpers
# @title Phase 2 tests and comparison helpers
def run_phase_2_tests() -> None:
    docs = TINY_CORPUS
    embs = embed_batch([d["text"] for d in docs])

    idx = LSHIndex(n_bands=10, bits_per_band=4)
    idx.build([d["doc_id"] for d in docs], embs)
    assert len(idx.bands) == 10
    assert idx.embeddings.shape == (20, EMBEDDING_DIM)
    print("  ✓ lsh_build_no_crash")

    idx2 = LSHIndex(n_bands=20, bits_per_band=4)
    idx2.build([d["doc_id"] for d in docs], embs)
    target_idx = 0
    results = idx2.query(embs[target_idx], top_k=5)
    result_ids = [d for d, _ in results]
    assert docs[target_idx]["doc_id"] in result_ids, (
        f"self-retrieval failed: {docs[target_idx]['doc_id']} not in {result_ids}"
    )
    print("  ✓ lsh_self_retrieval")

    idx3 = LSHIndex(n_bands=30, bits_per_band=3)
    idx3.build([d["doc_id"] for d in docs], embs)
    queries = ["how do I parse a config file", "verify a JWT token", "redis cache"]
    overlaps = []
    for q in queries:
        qv = embed_text(q)
        bf_top = set(docs[i]["doc_id"] for i in brute_force_cosine(qv, embs, top_k=5))
        lsh_top = set(d for d, _ in idx3.query(qv, top_k=5))
        overlaps.append(len(bf_top & lsh_top) / 5)
    avg = sum(overlaps) / len(overlaps)
    assert avg >= 0.4, f"avg LSH/brute overlap {avg:.2f} too low"
    print(f"  ✓ lsh_recall_vs_brute_force (avg overlap {avg:.2f})")

    print("\nPhase 2: ALL PASS")


def phase_2_parity(corpus: str = "codesearchnet", tier: str = "25k") -> None:
    """Compare your LSH against sqlite-vec's exact KNN on the same eval set."""
    print(f"  loading {corpus} {tier}...")
    if corpus == "codesearchnet":
        docs, eval_set = load_codesearchnet(tier)
    else:
        docs, eval_set = load_msmarco(tier)
    print(f"  {len(docs):,} docs, {len(eval_set):,} queries")
    embs = cached_embed_batch([d["doc_id"] for d in docs], [d["text"] for d in docs],
                              CACHE_DIR / f"{corpus}_{tier}.parquet")

    lsh_idx = LSHIndex(n_bands=60, bits_per_band=3)
    lsh_idx.build([d["doc_id"] for d in docs], embs)

    db_path = INDEX_DIR / f"phase2_parity_{corpus}.db"
    if db_path.exists():
        db_path.unlink()
    conn = open_sqlite_db(db_path)
    build_sqlite_corpus(conn, docs, embs)

    yours = run_eval(lambda q: lsh_idx.query(embed_text(q), top_k=10), eval_set)
    vec = run_eval(lambda q: vec_query(conn, embed_text(q), 10), eval_set)
    print(f"\n  Your LSH R@5:        {yours['recall@5']:.3f}  (p50 {yours['p50_ms']:.1f} ms)")
    print(f"  sqlite-vec KNN R@5:  {vec['recall@5']:.3f}  (p50 {vec['p50_ms']:.1f} ms)")
    gap = vec['recall@5'] - yours['recall@5']
    print(f"  Gap:                 {gap*100:+.1f} pts")
    conn.close()
# Run after implementing Phase 2.
run_phase_2_tests()
for corpus in ("codesearchnet", "msmarco"):
    phase_2_parity(corpus=corpus, tier=DATASET)

Your notes: sqlite-vec comparison and speed/recall tradeoff

[Your answer]

Phase 3: Hybrid retrieval and query planner

Instructions

Implement RRF, routing, and custom fusion; run the tests and explain your custom method.

def reciprocal_rank_fusion(ranked_lists: list[list[tuple[str, float]]],
                           k: int = 60, top_k: int = 10) -> list[tuple[str, float]]:
    """YOU IMPLEMENT.

    Standard RRF: for each (doc_id, score) appearing in any ranked_list at rank r,
    add 1.0 / (k + r) to its fused score. Sort, return top_k.
    """
    raise NotImplementedError("Phase 3: implement reciprocal_rank_fusion")


def hybrid_query(inv_idx: InvertedIndex, lsh_idx: LSHIndex, query: str,
                 top_k: int = 10) -> list[tuple[str, float]]:
    """YOU IMPLEMENT.

    Run BM25 + LSH, fuse with RRF, return top_k.
    """
    raise NotImplementedError("Phase 3: implement hybrid_query")


def query_planner(query: str) -> str:
    """YOU IMPLEMENT.

    Inspect the query text. Return one of: 'bm25', 'lsh', 'hybrid'.

    Hints:
    - Contains snake_case or camelCase identifiers? -> probably bm25
    - Looks like natural language ("how do I", "what is")? -> probably lsh
    - Mixed? -> hybrid
    """
    raise NotImplementedError("Phase 3: implement query_planner")


def custom_fusion(inv_idx: InvertedIndex, lsh_idx: LSHIndex, query: str,
                  top_k: int = 10) -> list[tuple[str, float]]:
    """YOU IMPLEMENT.

    A fusion variant of YOUR design that is meaningfully different from RRF.
    Examples: weighted score fusion (normalize then combine), confidence
    fall-through (use BM25 if it is confident, else fuse), agreement boost
    (RRF + extra weight when both signals agree on the top-1).

    Document what you did and why in a comment at the top of this function.
    """
    raise NotImplementedError("Phase 3: implement custom_fusion")

Phase 3 tests

Phase 3 tests
# @title Phase 3 tests
def run_phase_3_tests() -> None:
    a = [("doc1", 0.9), ("doc2", 0.5), ("doc3", 0.1)]
    b = [("doc2", 0.8), ("doc4", 0.7), ("doc1", 0.3)]
    fused = reciprocal_rank_fusion([a, b], k=60, top_k=4)
    fused_ids = [d for d, _ in fused]
    assert set(fused_ids[:2]) == {"doc1", "doc2"}, f"top-2 should be doc1,doc2: {fused_ids}"
    print("  ✓ rrf_basic")

    a2 = [("doc1", 0.9), ("doc2", 0.5)]
    fused2 = reciprocal_rank_fusion([a2], k=60, top_k=2)
    assert [d for d, _ in fused2] == ["doc1", "doc2"]
    print("  ✓ rrf_single_list")

    assert query_planner("verify_jwt_with_scope") == "bm25"
    assert query_planner("how do I parse a config file") == "lsh"
    print("  ✓ planner_routes")

    print("\nPhase 3: ALL PASS")
# Run after implementing Phase 3.
run_phase_3_tests()

Your notes: How your custom fusion works

[Your answer]

Phase 4: Two-dataset benchmark

Instructions

Benchmark all five methods on both datasets using identical queries; save recall, MRR@10, latency, and memory results.

def run_two_corpus_benchmark(tier: str = "25k") -> dict:
    """YOU IMPLEMENT.

    For each corpus in [codesearchnet, msmarco]:
      1. Load with the supplied loader.
      2. Compute embeddings.
      3. Build both indexes (InvertedIndex, LSHIndex).
      4. Run each retriever (bm25, lsh, hybrid, custom_fusion, planner-routed)
         through run_eval against the eval_set.
      5. Print a side-by-side table.

    Return a nested dict: {corpus_name: {retriever_name: metrics}}.
    """
    raise NotImplementedError("Phase 4: implement run_two_corpus_benchmark")


def run_phase_4_tests() -> None:
    """Sanity check the driver runs end-to-end on tiny."""
    out = run_two_corpus_benchmark(tier="tiny")
    assert "codesearchnet" in out and "msmarco" in out
    for corpus, retrievers in out.items():
        assert "bm25" in retrievers
        assert "lsh" in retrievers
        for name, m in retrievers.items():
            assert "recall@5" in m and "p50_ms" in m
    print("\nPhase 4: ALL PASS")
# Run after implementing the benchmark driver.
run_phase_4_tests()
benchmark_results = run_two_corpus_benchmark(tier=DATASET)

Phase 5: SQLite comparison

Instructions

Add FTS5 and sqlite-vec to the comparison using identical data and queries; reuse Phase 4 results.

def run_production_scoreboard(tier: str = "25k") -> dict:
    """YOU IMPLEMENT.

    For each corpus, on top of your Phase 4 results, add:
      - FTS5 BM25 metrics (use the supplied fts5_query)
      - sqlite-vec exact KNN metrics (use the supplied vec_query)

    Print one row per retriever, with columns for recall, MRR, latency, and memory.

    Return a dict of the same shape as Phase 4, with the production rows added.
    """
    raise NotImplementedError("Phase 5: implement run_production_scoreboard")


def run_phase_5_tests() -> None:
    out = run_production_scoreboard(tier="tiny")
    assert out, "Missing benchmark results"
    for corpus, retrievers in out.items():
        assert "FTS5 BM25" in retrievers or "fts5_bm25" in retrievers, f"{corpus}: missing FTS5 row"
        assert "sqlite-vec KNN" in retrievers or "sqlite_vec_knn" in retrievers, f"{corpus}: missing sqlite-vec row"
    print("\nPhase 5: ALL PASS")
# Run after implementing the SQLite comparison driver.
run_phase_5_tests()
comparison_results = run_production_scoreboard(tier=DATASET)

Phase 6: Findings and AI use

Instructions

Answer these three questions in half a page to one page total. Reuse your notes from earlier phases. Figures, code, and benchmark tables are outside this limit.

1. What did your results show?

[Your findings, supported by the benchmark results.]

2. How did you use and check AI assistance?

[Give 2–3 examples. If you did not use AI, describe 2–3 debugging or design decisions and how you tested them.]

3. How does one of your algorithms work?

[Explain it in plain English.]


Final check (after Phases 0–5)

Final test helper
# @title Final test helper
def run_all_tests(phase0_state) -> None:
    print("=" * 60)
    print("RUNNING ALL PHASE TESTS")
    print("=" * 60)
    print("\nPhase 0:")
    run_phase_0_tests(phase0_state)
    print("\nPhase 1:")
    run_phase_1_tests()
    print("\nPhase 2:")
    run_phase_2_tests()
    print("\nPhase 3:")
    run_phase_3_tests()
    print("\nPhase 4:")
    run_phase_4_tests()
    print("\nPhase 5:")
    run_phase_5_tests()
    print("\n" + "=" * 60)
    print("ALL PHASES PASS")
    print("=" * 60)
# Final smoke check after completing Phases 0–5. Reuses Phase 0 data.
run_all_tests(phase0)

Scaling (Tiers 93 and 100)

Scale, memory limits, and evaluation requirements

Tier 93: Build and query within the memory budget

Data source and evaluation · Partitioning

Use the supplied loader below: 709,254 documents (5 GB of title/body text), 50 development queries, and 500 final queries. Everyone uses the same data and labels. No T4 or Colab runtime is required for this tier.

Run the preparation cell below once, then add your bounded-memory build, seven-method comparison, and memory evidence in this section. Reuse these saved indexes for the optional bonus; no second build is needed if they cover this exact subset.

Fixed MS MARCO data and saved IDs

Bounded-memory build and retrieval

Development (50 queries) and final evaluation (500 separate queries)

Larger-corpus benchmark and SQLite comparison

For SQLite, call initialize_sqlite_corpus(conn) once, then append_sqlite_batch(conn, docs, embeddings) for each batch. Both helpers are supplied.

Build/query memory and disk reads

Keep the Tier 87 results in Phases 4–5. Add the larger run here.

# Higher tiers only: downloads/prepares about 5 GB of text on first use.
# Leave False for Phase 0 and Tiers 80/87. Completed data is verified and reused.
RUN_TIER93 = False
if RUN_TIER93:
    scale_utils, tier93_data = prepare_tier93_data()
    # Iterate tier93_data.documents(); do not load it all with list().
    # Use tier93_data.development for tuning and tier93_data.evaluation for final results.
    print("Documents:", tier93_data.config["document_count"])
    print("Text bytes:", tier93_data.config["text_bytes"])
# YOUR WORK: Tier 93 build, evaluation, and memory measurements.
# Add your implementation and keep the resulting outputs visible here.

Tier 100: Full MS MARCO document corpus

Requirements · Memory reference

Full MS MARCO document build and checkpoints

Full-corpus benchmark and SQLite comparison

Build/query memory and disk reads

Keep the Tier 93 results above. Add the full MS MARCO v1 document-corpus run here (3,213,835 documents). Reuse the Tier 93 development and final queries and their relevance labels; do not tune on final queries. Build and query with a working set below 1 GB.

Full-corpus evaluation

Reuse the saved Tier 93 final query set and document relevance labels with run_eval(). Evaluate all five methods and both SQLite references against the full MS MARCO document corpus.

# YOUR WORK: Tier 100 build, evaluation, and memory measurements.
# Add your implementation and keep the resulting outputs visible here.

Optional CLI scaffold

Optional CLI template
# @title Optional CLI template
CLI_TEMPLATE = '''
"""nanomem CLI. Usage:
    python nanomem.py index --corpus path/to/corpus/
    python nanomem.py query "how do I parse a config file"
"""
import argparse, json
from pathlib import Path

INDEX_DIR = Path("./nanomem_index")

def cmd_index(args):
    # Load corpus, build inverted index + LSH index, save to INDEX_DIR.
    raise NotImplementedError

def cmd_query(args):
    # Load indexes, run hybrid_query, print top-K as JSON.
    raise NotImplementedError

if __name__ == "__main__":
    p = argparse.ArgumentParser(prog="nanomem")
    sub = p.add_subparsers(dest="cmd", required=True)
    p_index = sub.add_parser("index"); p_index.add_argument("--corpus", required=True)
    p_query = sub.add_parser("query"); p_query.add_argument("query_text", type=str)
    args = p.parse_args()
    if args.cmd == "index": cmd_index(args)
    elif args.cmd == "query": cmd_query(args)
'''

Optional bonus benchmark

Skip this section unless entering the bonus competition. It is disabled by default and adds no required work.

Benchmark setup and measurement rules

Reuse your Tier 93 data and saved indexes. Prepare the required dataset in Tier 93 before running this section. This competition times only your planner; the required Tier 93 evaluation compares all five methods and both SQLite references.

  1. Runtime: In Colab, choose Runtime → Change runtime type → Python 3 → T4 GPU, with standard RAM. Reconnect and rerun Setup. This helper checks the GPU/RAM and sets CPU math threads to 1; it records the actual CPU, GPU, RAM, and package versions. Local runtimes do not qualify.

  2. Data and queries: The supplied benchmark selects 709,254 MS MARCO v1 full documents, containing 5,000,000,958 bytes of title/body text. Everyone uses identical document IDs, 50 development queries, and 500 evaluation queries. Tune only on the development set. The first preparation downloads several GB and writes about 5 GB locally; interrupted downloads restart. Save completed data/indexes to Drive if needed. For timed runs, copy your index to /content so Drive latency is not part of the comparison.

  3. Your engine: Build or load your own index over this entire subset. Reuse your Tier 93 indexes if they cover this exact subset; otherwise build once using your Tier 93 code. Connect the adapter functions in the next cell. Save build-time memory evidence to a text file. Return original document IDs, merging chunk hits if you chunk documents. Count all live index/model/buffer components against the 2 GB limit, including GPU allocations. Use the supplied MiniLM model; use EMBED_DEVICE = "cuda" before building/loading the embedding model if your planner uses vector retrieval.

  4. Measure and submit: Ten development queries warm up the engine, then all 500 evaluation queries run once in a fixed shuffled order. Timing includes query embedding, retrieval, disk reads, and GPU synchronization; index construction is excluded. Do not cache answers or query embeddings, tune on evaluation queries, or hard-code relevant IDs. Keep the displayed report and embedded benchmark record in this notebook; no separate attachment is needed. The score is mean Recall@5 ÷ median query latency (ms). Staff review the saved results and finalize awards.

Colab CPUs and host load vary even with a T4. The report records these differences for staff review; the runtime checks do not establish identical hardware or prove the live-memory bound. The top five valid entries receive 0–5 bonus points each.

# 1. Optional competition runtime. Tier 93 setup provides data and helpers.
RUN_BONUS = False
if RUN_BONUS:
    if "tier93_data" not in globals():
        raise RuntimeError("Run the Tier 93 data preparation cell first.")
    bonus = scale_utils
    benchmark = tier93_data
    bonus.configure_bonus_runtime()
# 2. Connect your Tier 93 implementation. These adapters are student work.
# Build once; load saved indexes on later runs. Do not use the tiny-data engine.
def load_or_build_bonus_engine(documents):
    # documents is an iterator of {"doc_id": ..., "text": ...}; do not list() it.
    # Build/load all documents, saving build-memory logs and source document IDs.
    raise NotImplementedError("Connect your bounded-memory index build/load code")

def bonus_search(query, k):
    # Route this raw-text query through your planner, then run your retrieval.
    # Return up to k original document IDs, or (document_id, score) pairs.
    raise NotImplementedError("Connect planner-routed retrieval using bonus_engine")

def bonus_indexed_document_ids():
    # Yield each original document ID actually indexed, once (not the input manifest).
    raise NotImplementedError("Read IDs from your saved index metadata")

def bonus_memory_components():
    # Return {"component_name": live_bytes}, including the model, index, and buffers.
    # Values are BYTES. Supplement these query-boundary snapshots with build logs.
    raise NotImplementedError("Connect your live-component memory accounting")

BUILD_MEMORY_EVIDENCE = "/content/bonus_build_memory.txt"
if RUN_BONUS:
    bonus_engine = load_or_build_bonus_engine(benchmark.documents())
# 3. Run and save results inside this notebook. Keep both outputs below.
if RUN_BONUS:
    import base64, contextlib, io
    from IPython.display import display
    # Capture the helper's legacy ZIP instructions; submission is now notebook-only.
    with contextlib.redirect_stdout(io.StringIO()):
        bonus_report = bonus.run_bonus_benchmark(
            search=bonus_search,
            benchmark=benchmark,
            indexed_document_ids=bonus_indexed_document_ids(),
            memory_components=bonus_memory_components,
            build_memory_evidence=BUILD_MEMORY_EVIDENCE,
            output_dir="/content/bonus_results",
        )
    bonus.display_bonus_report(bonus_report)
    archive = Path("/content/bonus_results.zip").read_bytes()
    display({
        "application/json": {"nanomem_bonus_archive": base64.b64encode(archive).decode("ascii")},
        "text/plain": "Benchmark record saved in this output. Keep it in your submitted notebook; no separate upload is needed.",
    }, raw=True)

Submitting to Gradescope