Preparing Text for Embedding

Ma Mahalakshmi V Updated 16 Sep 2026
7 min read ·Lesson 117 of 224

Text Preparation for Embedding

An embedding model turns whatever text it is given into a vector — it does not know or care whether that text is clean prose or a mess of HTML tags, repeated whitespace, and boilerplate navigation links scraped along with the real content. The quality of a semantic search system is bounded by the quality of the text that goes into the embedding call, which makes text preparation one of the highest-leverage steps in the whole pipeline, and one that is easy to skip when a demo works on tidy sample data and then quietly underperforms on real data.

Why Raw Text Often Produces Poor Embeddings

Three problems show up repeatedly with real-world text sources (scraped web pages, exported PDFs, database fields written by humans over years):

  1. Boilerplate dilutes meaning. A support article's true content might be 200 words, but if it is scraped with a 150-word navigation menu, cookie notice, and footer attached, a large fraction of the text that gets embedded has nothing to do with the article's actual topic. The resulting vector partially represents "generic website chrome" rather than the article's subject.
  2. Formatting noise adds no semantic value but changes the input. Excess whitespace, HTML tags, markdown symbols, and control characters are not stripped automatically by the embedding model — they are tokenized like any other characters, spending part of the input on symbols that carry no meaning.
  3. Inconsistent preprocessing between indexing time and query time breaks comparisons. If documents are lowercased and stripped of punctuation before embedding, but user queries are embedded raw, the two are not being compared on equal footing, and any preprocessing-related distortion adds noise rather than being consistent enough to cancel out.

A Basic Cleaning Pipeline

Start with the transformations that are almost always safe: removing markup, collapsing whitespace, and trimming boilerplate patterns known ahead of time.

import re

def strip_html(text: str) -> str:
    """Remove HTML tags, leaving the text content."""
    return re.sub(r"<[^>]+>", " ", text)

def collapse_whitespace(text: str) -> str:
    """Collapse runs of whitespace (including newlines) into single spaces."""
    return re.sub(r"\s+", " ", text).strip()

def clean_text(text: str) -> str:
    """Apply the basic cleaning pipeline in a fixed, documented order."""
    text = strip_html(text)
    text = collapse_whitespace(text)
    return text

raw = """
<div class="article">
  <h1>Return Policy</h1>
  <p>Items   can be returned      within 30 days
  of purchase.</p>
</div>
"""
print(clean_text(raw))
# "Return Policy Items can be returned within 30 days of purchase."

strip_html uses a regular expression to remove anything between angle brackets — this is a pragmatic tool for well-formed HTML fragments, not a full HTML parser, so it can misbehave on malformed markup or text that legitimately contains a < character (like if x < 10). For content scraped from real web pages, a proper HTML parser (such as BeautifulSoup, covered as a dependency in earlier units) is more robust than a regular expression and should be preferred when available; the regex version here is shown because it has no external dependency and illustrates the idea clearly. collapse_whitespace matters because inconsistent spacing and line breaks add tokens that carry no semantic content, and normalizing them makes near-identical documents produce more consistent embeddings.

Deciding What to Normalize — and What Not To

Not every normalization that seems reasonable actually helps. This is where preparing text for embeddings differs from preparing text for older techniques like keyword search.

TransformationUsually helps embeddings?Why
Removing HTML/markdown markupYesMarkup tokens are noise relative to the content.
Collapsing whitespaceYesExtra whitespace tokens add no meaning and vary run-to-run.
LowercasingUsually unnecessaryModern embedding models are trained on natural, mixed-case text and already capture that "Apple" (company) and "apple" (fruit) differ partly because of context, not just case. Forcing lowercase can discard a real signal.
Removing stopwords ("the", "is", "and")Usually harmfulEmbedding models are trained on natural language, including stopwords, and use them as part of sentence structure. Stopword removal was a keyword-search-era technique for TF-IDF style matching, not something that helps modern embedding models.
Removing punctuationSituationalPunctuation contributes to sentence structure; strip only characters you know are noise (like stray markup artifacts), not all punctuation broadly.
Truncating to a maximum lengthYes, when neededEmbedding models accept a limited number of tokens per input; text beyond that limit is either rejected or silently truncated by the API depending on the model, so intentional, controlled truncation (or better, the chunking approach in Lesson 8) is safer than relying on undocumented default behavior.

Why does lowercasing and stopword removal, which helped older keyword-based systems, often hurt here? Classic keyword search (TF-IDF, BM25) counts literal word overlap, so normalizing case and removing very common words reduces noise in that literal counting. Embedding models work completely differently: they are trained on large amounts of natural text and learn contextual meaning, including the meaning carried by function words and capitalization. Applying keyword-search-era normalization to embedding input removes signal the model was trained to use, rather than removing noise.

Deduplication Before Embedding

Embedding the same or near-identical text multiple times wastes API cost and, more importantly, can bias a search index by making one underlying piece of content appear to dominate simply because it exists in more copies.

def deduplicate_exact(texts: list[str]) -> list[str]:
    """Remove exact duplicate strings, preserving first-seen order."""
    seen = set()
    unique = []
    for text in texts:
        key = collapse_whitespace(text).lower()
        if key not in seen:
            seen.add(key)
            unique.append(text)
    return unique

def test_deduplicate_exact():
    texts = [
        "Return policy for electronics.",
        "Return  policy for electronics.",   # extra whitespace, otherwise identical
        "Shipping times vary by region.",
    ]
    result = deduplicate_exact(texts)
    assert len(result) == 2
    print("PASS: deduplicate_exact collapses whitespace-only duplicates")

test_deduplicate_exact()

This function normalizes each text (whitespace-collapsed, lowercased) only to build a comparison key for deduplication — it does not alter the text that actually gets embedded, which preserves the "don't force lowercase into the model input" guidance above while still catching duplicates that differ only in casing or spacing. Exact deduplication like this catches copy-pasted or re-scraped content; catching near-duplicates (two paragraphs that say the same thing in different words) requires comparing embeddings themselves, which is what Lesson 4's similarity techniques and the duplicate-detection ideas in Lesson 5 are for — that is a semantic problem, not a text-cleaning one.

Handling Length: Truncation and the Case for Chunking

Every embedding model has a maximum input length measured in tokens (not characters or words). Text longer than that limit needs to be handled deliberately.

def truncate_to_char_budget(text: str, max_chars: int) -> str:
    """A rough, dependency-free length guard.

    This is a coarse approximation — tokens and characters are not
    the same unit — but it prevents obviously oversized inputs from
    reaching the API at all. A tokenizer-based check is more precise
    when precision matters (see Unit 12 for tokenization details).
    """
    if len(text) <= max_chars:
        return text
    return text[:max_chars].rsplit(" ", 1)[0]

Truncating a long document to fit a length limit is a blunt tool: it silently discards everything past the cutoff, which may include the most relevant part of the document. For any document long enough to risk truncation, the better approach is chunking — splitting it into smaller, independently embedded pieces so that no information is thrown away and a search query can match the specific chunk that is actually relevant. Lesson 8 in this unit is dedicated entirely to chunking strategy; this lesson's truncation helper is a safety net for the rare oversized input that slips through, not a substitute for chunking a genuinely long document.

Common Mistakes

  • Applying keyword-search preprocessing (lowercasing, stopword removal) to embedding input. This was covered above: it removes signal that modern embedding models rely on rather than removing noise.
  • Cleaning documents at index time but not queries at search time (or vice versa). If HTML stripping and whitespace collapsing are applied to stored documents but the live user query is embedded raw, the two are prepared inconsistently, which adds avoidable noise to every comparison.
  • Ignoring boilerplate in scraped or exported content. Repeated navigation text, footers, and disclaimers embedded alongside real content dilute the vector's meaning and can cause unrelated documents to appear similar simply because they share the same boilerplate.

Best Practices

  • Apply the exact same cleaning function to documents and queries. Put the cleaning logic in one shared function (like clean_text above) and call it from both the indexing path and the query path — never duplicate the logic in two places where it can drift out of sync.
  • Prefer chunking over truncation for long documents. Truncation discards information; chunking preserves it while keeping each piece within the model's input limit.
  • Deduplicate before embedding, not after. Removing duplicates before the API call saves cost directly, rather than embedding the duplicate and discarding the result afterward.

0 Comments

Reviewed before they appear

No comments yet.

OpenAI SDK
Introduction to the OpenAI SDK Setting Up Python Creating an API Key Your First Call — client.responses.create() and response.output_text Understanding Billing, Credits, and What a Request Costs Why Responses Replaced Chat Completions Anatomy of a Request: model, input, and instructions Anatomy of a Response: The Typed output Array, Not Just Text Roles: User, Assistant, and Developer/System Choosing a Model, and Reading the Models Page Instead of Memorizing Names Instructions vs. Input Writing Prompts That Get Consistent Results Few-Shot Examples Reasoning Models and the reasoning Parameter Debugging a Prompt That Misbehaves Why Streaming Matters for User Experience stream=True and Iterating Over Events Handling the Event Types You Actually Care About Background Mode for Long-Running Jobs Project — Add Live Streaming to Your Chatbot The Problem With Parsing Free Text JSON Schema and Strict Mode Pydantic Models With the SDK's Parse Helpers Handling Refusals and Validation Failures Project — A Resume-to-JSON Extractor Working With input_image input_file, PDFs, and the Files API Image Generation Speech-to-Text and Text-to-Speech Project: A PDF Question-Answering Script What Function Calling Is Defining a Tool Schema The Full Loop Multiple Tools Errors, Timeouts, and Untrusted Arguments Project: A Weather Assistant Web Search File Search and Vector Stores Code Interpreter Remote MCP Servers and Connectors Project: A Research Assistant What an Embedding Is, Without the Maths Generating and Storing Embeddings Similarity Search From Scratch Hosted Vector Stores vs. Rolling Your Own A Small RAG App Over a Folder of Notes Agents vs. a Single API Call — When You Need One pip install openai Giving Agents Tools Handoffs and Multi-Agent Triage Guardrails and Approvals Tracing and Observing What Your Agent Did A Multi-Agent Support Desk Error Codes and What Each One Means Retries, Timeouts, and Backoff Rate Limits and Spend Limits Prompt Caching and Cost Optimisation The Batch API for Bulk Work Async Clients and Concurrency Moderation and Safety Best Practices Designing the App Backend With FastAPI Streaming to a Simple Frontend Deploying and a Cost/Safety Checklist Why Web Search Is Useful for Current Information Using the Web Search Tool with the Responses API Configuring Search Behavior for Application Use Cases Understanding Citations and Source Attribution Where to Go Next Building a Research Assistant with Web Search Combining Web Search with Structured Outputs Handling Conflicting or Low-Quality Web Sources Reducing Unsupported Claims with Grounded Generation Testing Freshness-Sensitive AI Answers Production Considerations for Web-Grounded Applications Understanding File Search and Retrieval-Augmented Generation Creating and Organizing Vector Stores Uploading Documents for Retrieval Connecting Vector Stores to Responses API Requests Designing Document Metadata and Filtering Strategies Building a PDF Question-Answering Application Improving Retrieval Quality With Better Document Preparation Handling Missing Evidence and Retrieval Failures Combining File Search With Web Search Building a Production Knowledge-Base Assistant What the Code Interpreter Tool Is Designed For Running Python-Based Analysis Through the OpenAI SDK Uploading Datasets for Analysis Analyzing CSV and Spreadsheet Data Generating Charts and Data Summaries Handling Generated Files and Downloadable Artifacts Building a Data-Analysis Assistant Combining Code Execution with Structured Outputs Validating Generated Calculations and Results Security and Sandbox Considerations for Code Execution Understanding Multimodal Input with the OpenAI SDK Sending Images to a Model Image Analysis from URLs and Uploaded Files Extracting Text and Information from Screenshots Building an Image-Question-Answering Application Combining Image Input with Structured Output Analyzing Multiple Images in One Request Handling Image Quality and Input Limitations Designing Multimodal Prompts for Reliable Results Building a Practical Vision-Powered Python Application Understanding Speech-to-Text and Text-to-Speech Workflows Transcribing Audio with the OpenAI SDK Working with Uploaded Audio Files Handling Timestamps and Transcription Metadata Building a Meeting Transcription Workflow Generating Spoken Responses from Text Handling Long Audio and Processing Failures Combining Audio with Text and Tool Calling Building an End-to-End Python Voice Application What Embeddings Are and When to Use Them Generating Embeddings With the OpenAI API Preparing Text for Embedding Comparing Vectors With Cosine Similarity Building a Simple Semantic Search Engine in Python Storing Embeddings in a Database Metadata Filtering for Semantic Search Chunking Strategies for Better Retrieval Evaluating Semantic Search Quality Building a Document Similarity Application When Batch Processing Makes Sense Designing Large-Volume AI Processing Pipelines Using Asynchronous Python with the OpenAI SDK Running Concurrent Requests Safely Controlling Concurrency and Avoiding Rate Limits Tracking Batch Job Progress Handling Partial Failures in Bulk Workloads Retrying Failed Items Without Duplicating Successful Work Designing Resumable AI Processing Jobs Building a Production Batch-Processing Pipeline Batch Processing Makes Sense Large-Scale AI Processing Pipelines Async Python with OpenAI SDK Safe Concurrent Requests Concurrency & Rate Limits Batch Progress Tracking Partial Failure Handling Safe Retry Handling Resumable AI Jobs Production Batch Pipeline System–User Data Separation Reusable App Instructions Prompt Templates & Variables Extraction & Classification Prompts Summarization & Transformation Prompts Explicit Output Requirements Prompt Version Management Prompt Testing & Evaluation Reusable Python Prompt Library API Key Security Secure API Key Storage Secure Secret Management Prompt Injection Prevention Trusted vs. Untrusted Content Tool Argument Validation Sensitive Data Handling Secure Logging AI Action Authorization Production AI Security Checklist Why AI Applications Need Evaluation Beyond Unit Tests Unit Testing OpenAI SDK Integration Code Mocking API Responses in Python Tests Testing Structured Outputs Against Schemas Testing Tool-Calling Workflows Building a Small Evaluation Dataset Measuring Accuracy, Consistency, and Failure Rates Regression Testing Prompts and Model Changes Human Evaluation Versus Automated Evaluation Creating a Repeatable Evaluation Pipeline AI Request Monitoring Token Cost Management Usage Metrics Design Reducing Model Calls Prompt & Context Optimization Model Selection & Optimization AI Caching Strategies Interactive Latency Optimization Usage Dashboards & Budget Alerts Performance & Cost Checklist Every API Call Starts Fresh Fixing API Statelessness Server-Side Conversation Memory Limits of Response Chaining What We're Building Conversation Memory Challenges Preparing an OpenAI SDK Application for Deployment Environment-Specific Configuration for Development and Production Deploying a Python AI Service with Docker Container Health Checks and Startup Configuration Managing Secrets in Cloud Deployments Background Workers for Long-Running AI Tasks Queues and Asynchronous Job Architectures Scaling AI Workloads Horizontally Monitoring Production Incidents and Failures Production Deployment Checklist for OpenAI SDK Applications Reusable OpenAI Service Classes AI Client Dependency Injection Typed AI Responses Python Configuration Management AI Request Decorators Centralized AI Error Handling Clean SDK Abstractions Reusable OpenAI Utilities Internal AI Python Libraries SDK Integration Maintenance Production AI Chatbot Document Q&A System Web Research Assistant Customer Support Agent AI Data Analysis Assistant Image Analysis App Meeting Transcription & Summary Semantic Document Search Multi-Tool AI Agent Production OpenAI SDK App Why "It Looked Fine When I Tested It" Isn't Enough Timing Note Status Note Pre-Decision Status Note Current Availability Note
Ask about this post
AI Ask about this post

Ask questions about Preparing Text for Embedding and get answers drawn from it.

Signed-in readers only.