Chunking and Reranking
Implement recursive character chunking, semantic paragraph splitting, Cohere Rerank cross-encoders, and rank fusion.
TL;DR
- Partition lengthy documentation into overlapping segments using
chunkWithOverlap()methods. - Preserve semantic contextual integrity by splitting upon
\n\nparagraph boundaries. - Refine initial vector retrieval results using Cohere
rerank()cross-encoder APIs.
Recursive Document Chunking
Hierarchical Recursive SplitterSplit 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 SplitterPreserve 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 GuardPrevent 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 CallScore 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 ArchitectureUnderstand 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 ThresholdFilter 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) AlgorithmMerge 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 DispatcherExecute 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 SplitPropagate 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 PointerIndex small children for search while retrieving full parent.
type ChunkRelation = {
childId: string;
childVec: number[];
parentDocId: string;
};
// Return parentDocId content to preserve contextSentence Boundary SplittingSplit 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 PreserverKeep 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
- Maintain a 10% to 20% token
chunkOverlapbetween contiguous chunks so sentences split across boundaries preserve complete semantic meaning. - Pass initial top-20 vector search candidates into Cohere
rerankto compress the final context down to top-3 highest-relevance excerpts.
Warnings
- Never employ fixed character chunking without respect for markdown headers, code blocks, or grammatical
sentenceEndlinguistic punctuation marks. - Avoid excessive chunk sizes exceeding 1000 tokens that dilute embedding vector
densityand inflate downstream prompt context expenses.
In Practice
Gathers initial vector search candidates and applies Cohere Rerank cross-encoder to select top results.
- Define candidate passages retrieved from initial vector search.
- Initialize Cohere client and dispatch cross-encoder rerank call.
- Sort candidate excerpts by joint attention relevance score.
- Return top refined passages to populate RAG prompt context.
import { CohereClient } from 'cohere-ai';
const cohere = new CohereClient();
const docs = [
'The API port is configured on 8080.',
'Python 3.12 is recommended for workers.',
'PostgreSQL credentials in .env file.',
];
const q = 'Which port does the web server listen on?';
const res = await cohere.rerank({
model: 'rerank-v3.5',
query: q, documents: docs, topN: 1,
});
const best = docs[res.results[0].index];
const topScore = res.results[0].relevanceScore;
console.log(best, topScore.toFixed(3));FAQ
Fixed-size slicing cuts words in half, destroys code syntax, and cleaves related arguments across chunk boundaries, resulting in garbled embeddings.
Cohere Rerank typically processes 20-50 document candidates in 50-100 milliseconds, adding minimal overhead while improving top-3 retrieval precision by 20-35%.
Parent-child chunking indexes small, focused child chunks (100-200 tokens) for accurate vector search, but retrieves the larger parent chunk (800-1200 tokens) to supply complete context to the LLM.