Build / AI /

Chunking and Reranking

Implement recursive character chunking, semantic paragraph splitting, Cohere Rerank cross-encoders, and rank fusion.

TL;DR

  1. Partition lengthy documentation into overlapping segments using chunkWithOverlap() methods.
  2. Preserve semantic contextual integrity by splitting upon \n\n paragraph boundaries.
  3. Refine initial vector retrieval results using Cohere rerank() cross-encoder APIs.

Recursive Document Chunking

    Hierarchical Recursive Splitter

    Split text progressively by double newline, single newline, and space.

    function recursiveChunk(
      text: string, maxSize = 500, overlap = 50
    ): string[] {
      // Split progressively until chunks fit in maxSize
      const chunks: string[] = [];
      let start = 0;
      while (start < text.length) {
        const end = Math.min(start + maxSize, text.length);
        chunks.push(text.slice(start, end));
        start += maxSize - overlap;
      }
      return chunks;
    }
    Markdown Header Aware Splitter

    Preserve section header context within child text chunks.

    function chunkMarkdown(md: string) {
      const sections = md.split(/^(?=#+ )/m);
      return sections.map(sec => {
        const header = sec.match(/^#+ .*/)?.[0] ?? '';
        return { header, content: sec.trim() };
      });
    }
    Code Block Boundary Guard

    Prevent code blocks from being cleaved across chunk boundaries.

    function preserveCodeBlocks(text: string) {
      const fence = String.fromCharCode(96).repeat(3);
      const expr = `(${fence}[\\s\\S]*?${fence})`;
      const pat = new RegExp(expr, 'g');
      const blocks = text.split(pat);
      return blocks.filter(b => b.trim().length > 0);
    }

Cohere Rerank Integration

    Cohere Cross-Encoder Rerank Call

    Score and reorder top vector candidates with deep joint attention.

    import { CohereClient } from 'cohere-ai';
    const token = process.env.COHERE_API_KEY;
    const cohere = new CohereClient({ token });
    
    async function rerankDocs(
      query: string, docs: string[]
    ) {
      const response = await cohere.rerank({
        model: 'rerank-v3.5',
        query,
        documents: docs,
        topN: 3,
      });
      return response.results.map(r => docs[r.index]);
    }
    Bi-Encoder vs Cross-Encoder Architecture

    Understand two-stage retrieval latency and precision trade-offs.

    // Stage 1 (Bi-Encoder): Fast vector lookup,
    // scans 1M docs in 10ms, returns candidate top 25.
    
    // Stage 2 (Cross-Encoder): Joint query-doc attention,
    // evaluates top 25 in 50ms, returns definitive top 3.
    Relevance Cutoff Threshold

    Filter out reranked documents with low confidence relevance scores.

    const minScore = 0.65;
    const ok = (r: any) => r.score >= minScore;
    const results = reranked.filter(ok);

Hybrid Search & Reciprocal Rank Fusion

    Reciprocal Rank Fusion (RRF) Algorithm

    Merge lexical BM25 rankings and semantic vector search rankings.

    function reciprocalRankFusion(
      vectorRanks: string[],
      keywordRanks: string[],
      k = 60
    ) {
      const scores = new Map<string, number>();
      const add = (id: string, rank: number) => {
        const curr = scores.get(id) || 0;
        scores.set(id, curr + (1 / (k + rank)));
      };
      vectorRanks.forEach((id, r) => add(id, r + 1));
      keywordRanks.forEach((id, r) => add(id, r + 1));
      const entries = [...scores.entries()];
      return entries.sort((a, b) => b[1] - a[1]);
    }
    Hybrid Search Dispatcher

    Execute sparse BM25 and dense vector search in parallel.

    async function hybridSearch(
      query: string, qVec: number[]
    ) {
      const [sparse, dense] = await Promise.all([
        searchBM25(query, 20),
        searchVector(qVec, 20),
      ]);
      return reciprocalRankFusion(dense, sparse);
    }
    Metadata Preservation on Split

    Propagate parent document IDs and titles down to all child chunks.

    const childChunks = chunks.map((c, i) => ({
      id: `${parentDoc.id}_chunk_${i}`,
      parentId: parentDoc.id,
      title: parentDoc.title,
      text: c,
    }));

Hierarchical Chunking Strategies

    Parent-Child Document Pointer

    Index small children for search while retrieving full parent.

    type ChunkRelation = {
      childId: string;
      childVec: number[];
      parentDocId: string;
    };
    // Return parentDocId content to preserve context
    Sentence Boundary Splitting

    Split on grammatical punctuation marks to avoid broken phrases.

    function splitSentences(text: string): string[] {
      return text
        .replace(/([.?!])\s+(?=[A-Z])/g, '$1|')
        .split('|');
    }
    Table Row Markdown Preserver

    Keep markdown table rows contiguous during chunking passes.

    function chunkTable(tableMd: string) {
      const rows = tableMd.trim().split('\n');
      const header = rows.slice(0, 2).join('\n');
      // Prepend header to each data row chunk
      return rows.slice(2).map(r => `${header}\n${r}`);
    }

Tips

  1. Maintain a 10% to 20% token chunkOverlap between contiguous chunks so sentences split across boundaries preserve complete semantic meaning.
  2. Pass initial top-20 vector search candidates into Cohere rerank to compress the final context down to top-3 highest-relevance excerpts.

Warnings

  1. Never employ fixed character chunking without respect for markdown headers, code blocks, or grammatical sentenceEnd linguistic punctuation marks.
  2. Avoid excessive chunk sizes exceeding 1000 tokens that dilute embedding vector density and inflate downstream prompt context expenses.

In Practice

FAQ