Skip to section
Foundationsfor rotation-free search
Section 48 of 5292% of course
Contents
Chapter 8 Section 8.2 75 min

Part VI · The implementation

The encoders enforce the mathematical layout

Read model and Rust packing code against the derivation.

Define success before you read the code

Let's set our expectations. For a query, coefficient i has to equal round(Δ * qᵢ) for 0 ≤ i < D, and everything else must be zero. For corpus vector j, coefficient Dj + D − 1 − i has to equal round(Δ * vⱼ,ᵢ). Also, we can't try to pack more than N/D vectors into a single polynomial. If we turn those statements into code, they become our test assertions.

Start with the simplest version in Python

The Python model lays out the logic cleanly, without any cryptography getting in the way:

model/doolittle_model.py:585
def encode_query(q_vec: list[float]) -> list[int]:
    coeffs = [0] * N
    for i, v in enumerate(q_vec):
        coeffs[i] = round(v * DELTA)
    return coeffs

def encode_db_poly(vecs: list[list[float]]) -> list[int]:
    coeffs = [0] * N
    for j, vec in enumerate(vecs):
        for i, v in enumerate(vec):
            coeffs[D * j + (D - 1 - i)] = round(v * DELTA)
    return coeffs

It matches our math perfectly. The query goes straight into physical position i. The corpus vector goes into position Dj + D − 1 − i. Both get multiplied by our scale factor Δ and rounded to integers.

Worked example

Example: D=4 with short vectors

Say our query is q = [2, 3]. The packed query starts as [2Δ, 3Δ, 0, 0]. Now take two corpus vectors: [5, 7] and [11]. They pack in as [0, 0, 7Δ, 5Δ | 0, 0, 0, 11Δ]. Notice how short vectors get padded with zeros inside their D-wide block? We don't shrink the block, because doing so would move the target score tap. The targets are still safely sitting at indices 3 and 7.

def assert_query_layout(q, coeffs, D, delta):
    assert len(q) <= D
    assert coeffs[:len(q)] == [round(x * delta) for x in q]
    assert all(x == 0 for x in coeffs[len(q):])

def assert_corpus_slot(vec, coeffs, j, D, delta):
    for i, value in enumerate(vec):
        assert coeffs[D*j + D-1-i] == round(value * delta)

How the production Rust client handles it

client-rust/src/cipher.rs:67
/// vector j, element i lands at coefficient D·j + D − 1 − i,
/// so the negacyclic product with a query carries ⟨q, v_j⟩·Δ² at
/// coefficient D·j + D − 1 with no rotations.
pub fn encrypt_corpus_block(
    &self,
    vectors: &[&[f32]],
    block: ScoreDimension,
) -> Result<Ciphertext, Error>

The Rust code does the exact same math, but wraps it in production-grade safety: checking for empty inputs, ensuring the block width is supported, enforcing the N/D vector limit, and making sure no individual vector overflows the dimension D.

Why should we read the dimension 'D' from the corpus metadata instead of just checking len(query)?

Because a short query might be intentionally zero-padded to fit a larger supported block size. If you try to dynamically set D based on the query's length, you'll miscalculate the target score taps and accidentally scramble the math against the existing corpus.

Check your understanding

Why don't we reverse the query array like we do with the corpus array?

Section summary

  • The Python model acts as our readable, executable specification.
  • The Rust API adds the necessary boundaries and error handling for production.
  • Both implementations strictly follow the same index math.

Repository layer · second pass

How should source code make the mathematical layout auditable?

Encoder code should read like the derivation: validate D and capacity, compute block base, compute reversed position, scale and round, and store. The target generator should share the same parameter object. Clever iterator chains are less valuable than names that expose Dj+D−1−i.

Cross-language golden vectors keep implementations aligned. Serialize parameters, packed coefficient arrays, targets, and expected integer products for Python, Rust, C#, and RTL-facing tests.

Reasoning chain

  1. 1

    Locate validation before allocation/encryption.

  2. 2

    Match each index expression to a proved formula.

  3. 3

    Check numeric conversion and overflow.

  4. 4

    Confirm zero padding.

  5. 5

    Generate targets from identical parameters.

  6. 6

    Compare with golden fixtures.

Worked trace

Review one assignment

  1. base=j*D.
  2. destination=base+(D−1−i).
  3. value=round(Δ*v[j][i]).
  4. Assert destination lies inside block j.

Result. The assignment is a direct executable statement of the layout.

Executable lens · Python

Make the hidden state visible

def pack(vectors,n,d,scale):
    out=[0]*n
    for j,v in enumerate(vectors):
        for i,value in enumerate(v):
            out[j*d+d-1-i]=round(scale*value)
    return out

Retype this example, predict each intermediate value, and then change one input that touches a boundary.

Misconception clinic

Tempting mistakes

  • Reversing the query because a helper is named reverse.
  • Letting integer conversion overflow before modular reduction.

Retrieval and transfer

Close the book first

  1. Annotate encoder source with proof equations.
  2. Create a golden fixture with negatives and zeros.
  3. Test maximum batch and one-too-many vector.