· 8 min read

What Actually Gets Indexed

A 1026-line file produces 26 chunks. Most boundaries fall mid-function, with no awareness of code structure. Here's exactly what gets lost — and why it matters for search quality.

Code RAG on a real production codebase — AllInterviews, a Node.js/Express backend with 422 JS/TS files. The problem: indexing source code for semantic search so agents can find relevant code when diagnosing and fixing bugs.


Why code is hard to index

Some retrieval targets are easy. A support ticket, a bug report, a log entry — each is a purpose-built document written in natural language about one thing. It was designed to be found.

Code RAG indexes source files. A source file is not a document — it’s hundreds or thousands of lines written for a compiler, not a search engine. Functions share files. Files share modules. A single file can contain ten unrelated functions, a hundred imported names, and boilerplate that’s identical across every file in the repo.

You can’t index a file as one document. You have to chunk it. And how you chunk it determines everything about what you can find.


How the current chunker works

The chunker in app/services/rag.py is simple: split by line count.

CHUNK_LINES = 50    # target lines per chunk
OVERLAP_LINES = 10  # lines shared between adjacent chunks

Take a file. Split it into windows of 50 lines. Each window starts 40 lines after the previous one (50 − 10 overlap). So a 1026-line file produces 26 chunks, each sharing 10 lines with its neighbors.

The overlap exists for a specific reason: if a function boundary falls near the end of a chunk, the next chunk will still contain enough of that function’s opening lines to be searchable. Without overlap, a 5-line function that starts at line 48 of a 50-line window would appear as 2 lines in one chunk and 3 lines in the next — neither enough to be useful.

The 10-line overlap is a hedge against boundary misses. It’s not a guarantee.


What the chunks look like in practice

routes/services/image.js is 1026 lines long. The indexer produces 26 chunks. Here’s the full list:

chunk  lines    1–  50   const aws = require("aws-sdk");
chunk  lines   41–  90   [blank — overlap with chunk 1]
chunk  lines   81– 130   })  [mid-S3 config]
chunk  lines  121– 170   Key: baseFileKey,
chunk  lines  161– 210   } else {
chunk  lines  201– 250   try {
chunk  lines  241– 290   }
chunk  lines  281– 330   const compressedKeyIndex = ...
chunk  lines  321– 370   Bucket: bucket,
chunk  lines  361– 410   }
chunk  lines  401– 450   }
chunk  lines  441– 490   if (payload.keysToAdd) {
chunk  lines  481– 530   if (!imageObj.source || !imageObj.destination)
chunk  lines  521– 570   data.append("file", imageBuffer, ...)
chunk  lines  561– 610   }
chunk  lines  601– 650   async function s3ObjectExists(s3Client, bucket, key)
chunk  lines  641– 690   // Delete original image
chunk  lines  681– 730   } catch (err) {
chunk  lines  721– 770   await axios.delete(deleteUrl, ...)
chunk  lines  761– 810   [blank]
chunk  lines  801– 850   };
chunk  lines  841– 890   const images = await Promise.allSettled(...)
chunk  lines  881– 930   let originalImages = ...
chunk  lines  921– 970   }
chunk  lines  961–1010   const image = await fetch(...)
chunk  lines 1001–1026   removeFile,

The first thing to notice: chunk boundaries fall in the middle of function bodies constantly. Lines 81–130 starts with }) — closing a callback that started before the chunk. Lines 121–170 starts with Key: baseFileKey, — the middle of an object literal. Lines 161–210 starts with } else { — mid-conditional.

A reader dropped into any of these chunks cold has no idea what function they’re in.


A specific case: moveAndRemoveFileFromS3

The function responsible for the S3 NoSuchKey incident:

// line 479
async function moveAndRemoveFileFromS3(bucket, imageObj) {
  try {
    if (!imageObj.source || !imageObj.destination) return;
    if (imageObj.source === imageObj.destination) return;

    await s3
      .copyObject({
        Bucket: bucket,
        CopySource: `${bucket}/${imageObj.source}`,
        Key: `${imageObj.destination}`,
      })
      .promise();
    await s3.deleteObject({ Bucket: bucket, Key: imageObj.source }).promise();
  } catch (error) {
    console.log("moveAndRemoveFileFromS3 error", error, bucket, imageObj);
  }
}
// ends at line 495

It’s 17 lines. The 50-line chunker splits it across two chunks:

Chunk 441–490 — contains the function declaration at line 479, but the first 38 lines (441–478) are a completely different function. The moveAndRemoveFileFromS3 body is cut off at line 490, mid-way through the .copyObject() call.

Chunk 481–530 — starts inside the function body at line 481, with the overlap giving it lines 481–490 again. This chunk has the copyObject, deleteObject, and catch block. But it has no function declaration — a reader (or embedding model) looking at this chunk doesn’t know what function they’re in.

Here’s what each chunk actually looks like when you read them:

Chunk 441–490:

// lines 441-478: unrelated function (keysToAdd/keysToRemove logic)
if (payload.keysToAdd) {
  for (let i = 0; i < payload.keysToAdd.length; i++) {
    images.push(payload.keysToAdd[i]);
    compressAndUploadtoS3(bucket, payload.keysToAdd[i], compressedImageFilePath);
    compressedImages.push(compressedImageFilePath);
  }
}
...
function replaceFunc(sourceObjArr, index, destinationObjArr) { ... }

// lines 479-490: start of moveAndRemoveFileFromS3
async function moveAndRemoveFileFromS3(bucket, imageObj) {
  try {
    if (!imageObj.source || !imageObj.destination) return;
    if (imageObj.source === imageObj.destination) return;
    await s3
      .copyObject({
        Bucket: bucket,
        CopySource: `${bucket}/${imageObj.source}`,
        Key: `${imageObj.destination}`,
      })
      .promise();   // <-- CUT OFF HERE

Chunk 481–530:

// No function declaration — starts inside the body
    if (!imageObj.source || !imageObj.destination) return;
    if (imageObj.source === imageObj.destination) return;

    await s3
      .copyObject({
        Bucket: bucket,
        CopySource: `${bucket}/${imageObj.source}`,
        Key: `${imageObj.destination}`,
      })
      .promise();
    await s3.deleteObject({ Bucket: bucket, Key: imageObj.source }).promise();
  } catch (error) {
    console.log("moveAndRemoveFileFromS3 error", error, bucket, imageObj);
  }
}

// lines 497-530: next function (slugify)
function slugify(text) { ... }
async function uploadImageByConfig(imageUrl, config, ...) { ... }

Neither chunk is self-contained. The first has the function name but not the full body. The second has the full body but no function name.


Why overlap doesn’t fully fix this

The 10-line overlap puts lines 481–490 in both chunks. That helps: the second chunk recovers most of the function body even though the declaration is in the first chunk.

But overlap can’t fix the dilution problem. The first chunk is 80% unrelated code. Its embedding is pulled toward whatever keysToAdd/keysToRemove logic is about, not toward S3 copy-and-delete operations. The second chunk has the body but no function name — a query for moveAndRemoveFileFromS3 finds the first chunk, not the second.

Overlap is a partial fix for the boundary problem. It doesn’t help when a short function lands at the end of a chunk with a lot of unrelated context before it.


What a good chunk looks like

For contrast, look at s3ObjectExists — it starts at line 601, well within the 601–650 chunk:

async function s3ObjectExists(s3Client, bucket, key) {
  try {
    await s3Client.headObject({ Bucket: bucket, Key: key }).promise();
    return true;
  } catch (err) {
    if (err.code === 'NotFound') {
      return false;
    }
    throw err;
  }
}

This function is 12 lines, starts 1 line into the chunk, and is followed by the start of deleteImageVariantsFromS3. The chunk has the complete function with its declaration and is easy to understand without surrounding context.

Queries for S3 key existence, headObject, NotFound — they all land here cleanly.

The difference between s3ObjectExists (easy to find) and moveAndRemoveFileFromS3 (hard to find) isn’t about what the functions do. It’s about where their declarations happen to fall relative to a 50-line window.


The structural problem

Fixed line-count chunking is unaware of code structure. It doesn’t know what a function is. It doesn’t know where one ends and another begins. It slices the file into equal-size strips and embeds whatever falls in each strip.

This works reasonably well for large functions — a 100-line function will dominate at least one chunk, and that chunk will have a coherent semantic identity. It breaks down for small functions (under 50 lines) that share a chunk with neighbors, especially when the function boundary doesn’t align with the chunk boundary.

routes/services/image.js is 1026 lines with many small utility functions. The file is a collection of tightly scoped helpers. The 50-line chunker is a poor fit for it.

Part 2 measures exactly how much this costs in search quality.


Key takeaways

  • 26 chunks for a 1026-line file — one chunk every ~40 lines
  • Most chunk boundaries fall mid-function, with no awareness of code structure
  • 10-line overlap reduces but doesn’t eliminate the boundary problem
  • Short functions that land near the end of a chunk are the worst case
  • Functions that start near the beginning of a chunk are fine
  • The fix is function-boundary chunking — one chunk per function, sized to the function, not to an arbitrary line count

Next: Part 2 — measuring the retrieval cost of line-count chunking with a concrete eval suite, before touching the chunker.