Uploading Documents for Retrieval

Ma Mahalakshmi V Updated 16 Sep 2026
8 min read ·Lesson 78 of 224

Two-Step vs. One-Step Uploads

Getting a document into a vector store is conceptually two separate operations: uploading the raw file to OpenAI's file storage, and attaching that file to a specific vector store so it gets chunked, embedded, and indexed. The SDK gives you both a low-level path that makes this explicit and a convenience path that does both at once.

The Explicit Two-Step Path

from openai import OpenAI

client = OpenAI()

with open("employee-handbook.pdf", "rb") as f:
    uploaded_file = client.files.create(
        file=f,
        purpose="assistants",
    )

print(uploaded_file.id)

attachment = client.vector_stores.files.create(
    vector_store_id="vs_68f2a1c9e4b8...",
    file_id=uploaded_file.id,
)

print(attachment.status)

Note: The required purpose value for files destined for vector stores, and the exact method names under client.vector_stores.files, can change between SDK versions — confirm both against current OpenAI documentation before relying on them in production.

Walking through this: client.files.create uploads the raw bytes of employee-handbook.pdf to OpenAI's file storage and returns a File object with its own ID (separate from any vector store ID). At this point the file exists on OpenAI's servers but is not part of any vector store and cannot be searched yet. The second call, client.vector_stores.files.create, takes that already-uploaded file and attaches it to a specific vector store, which triggers the actual chunking and embedding pipeline. The returned attachment.status tells you whether that processing has completed, is in progress, or failed — the same asynchronous behavior described in Lesson 2.

This two-step path matters when you want to reuse the same uploaded file across multiple vector stores (a legal policy document that belongs in both a "legal" store and a "employee onboarding" store, for instance) without uploading the bytes twice.

The Convenience One-Step Path

For the common case — one file, one destination store, uploaded and attached together — the SDK provides a helper that collapses both steps:

with open("employee-handbook.pdf", "rb") as f:
    result = client.vector_stores.files.upload_and_poll(
        vector_store_id="vs_68f2a1c9e4b8...",
        file=f,
    )

print(result.status)

This does the same two operations as above, but also polls the vector store attachment until it reaches a terminal state (completed or failed) before returning, instead of returning immediately with a status of in_progress. That polling behavior is exactly what you want in a script that uploads a document and then needs to know, synchronously, whether it succeeded — you avoid writing your own retry loop.

Uploading Multiple Files in Batch

Uploading documents one at a time in a loop works, but for anything beyond a handful of files, a batch upload is both faster and easier to monitor:

import glob

file_paths = glob.glob("docs/*.pdf")
file_streams = [open(path, "rb") for path in file_paths]

batch = client.vector_stores.file_batches.upload_and_poll(
    vector_store_id="vs_68f2a1c9e4b8...",
    files=file_streams,
)

print(batch.status)
print(batch.file_counts.completed, "of", batch.file_counts.total, "succeeded")

for stream in file_streams:
    stream.close()

Note: Method names under file_batches, batch size limits, and supported file types are version-specific — verify current limits and supported formats in the official documentation before building a production ingestion pipeline around them.

This example collects every PDF in a local docs/ directory, opens each as a binary file stream, and submits them as a single batch to the vector store, then waits for the whole batch to finish processing. Batching matters for two practical reasons: it reduces the number of round trips to the API compared to uploading files one by one, and batch.file_counts gives you an aggregate view of how many files in the batch succeeded versus failed, which is far more useful for a bulk ingestion job than checking each file's status individually. Closing each file stream afterward is good hygiene — leaving many open file handles in a long-running ingestion process is an easy way to hit operating-system file-descriptor limits.

What Happens to a Document Behind the Scenes

Once a file is attached to a vector store, several things happen automatically that are worth understanding, because they explain what you can and cannot control at upload time:

  1. Text extraction. For formats like PDF or DOCX, the text content is extracted from the document, including handling multi-column layouts and, in supported cases, text within simple tables.
  2. Chunking. The extracted text is split into chunks, roughly the size of a few paragraphs each, sized to balance two competing goals: chunks small enough that each one is topically coherent (so a similarity match is meaningful), and large enough that each one carries enough surrounding context to be useful once retrieved.
  3. Embedding. Each chunk is passed through an embedding model to produce its vector representation.
  4. Indexing. The embeddings are stored in a structure that supports fast approximate nearest-neighbor search at query time.

You do not write code for any of these four steps yourself when using file_search — that automation is the entire value proposition versus the custom pipeline in Unit 10. What you can influence is the input quality (Lesson 7 covers preparing documents so this pipeline performs better) and, in some SDK versions, chunking parameters passed at attachment time.

Supported File Types and Practical Limits

file_search supports common document formats — PDF, DOCX, TXT, Markdown, and several others — but not every format is equally well-suited to automatic chunking. A clean, well-structured Markdown file with clear headings extracts and chunks far more reliably than a PDF that was originally a scanned image with no embedded text layer, because there is no text to extract from a pure image — that document would need OCR (as covered for PDFs generally in Unit 7) before it's usable here at all.

Every account also operates under storage and file-size limits that change over time as the platform evolves.

Note: Exact supported file formats, maximum file size per document, and maximum total storage per vector store are platform limits that change over time — check the current OpenAI documentation for these numbers before planning ingestion at scale.

Handling Upload Failures Gracefully

Because ingestion is asynchronous and can fail per-file (a corrupted PDF, an unsupported format, a file that exceeds a size limit), production ingestion code should never assume success:

def upload_documents(client, vector_store_id, file_paths):
    results = {"succeeded": [], "failed": []}

    for path in file_paths:
        try:
            with open(path, "rb") as f:
                outcome = client.vector_stores.files.upload_and_poll(
                    vector_store_id=vector_store_id,
                    file=f,
                )
            if outcome.status == "completed":
                results["succeeded"].append(path)
            else:
                results["failed"].append((path, outcome.status))
        except Exception as exc:
            results["failed"].append((path, str(exc)))

    return results

This function wraps each individual upload in a try/except block and separately tracks the outcome even when no exception is raised but the resulting status still isn't completed — a distinction that matters because ingestion failures often surface as a failed status rather than a Python exception. Structuring an ingestion pipeline this way means one bad file (a password-protected PDF, say) doesn't halt the entire batch, and you get a clear, actionable list of exactly which documents need attention afterward, rather than discovering gaps in your knowledge base only when a user asks about missing content.

You can test the failure-tracking logic itself, independent of any real API call, using a fake client:

class FakeOutcome:
    def __init__(self, status):
        self.status = status


class FakeFilesAPI:
    def __init__(self, statuses_by_path):
        self.statuses_by_path = statuses_by_path

    def upload_and_poll(self, vector_store_id, file):
        path = file.name
        status = self.statuses_by_path[path]
        if status == "raise":
            raise RuntimeError("simulated upload failure")
        return FakeOutcome(status)


class FakeVectorStoresAPI:
    def __init__(self, statuses_by_path):
        self.files = FakeFilesAPI(statuses_by_path)


class FakeClient:
    def __init__(self, statuses_by_path):
        self.vector_stores = FakeVectorStoresAPI(statuses_by_path)


def test_upload_documents_separates_success_and_failure():
    fake_client = FakeClient({
        "good.pdf": "completed",
        "bad.pdf": "failed",
        "broken.pdf": "raise",
    })

    results = upload_documents(
        fake_client, "vs_test", ["good.pdf", "bad.pdf", "broken.pdf"]
    )

    assert results["succeeded"] == ["good.pdf"]
    assert [path for path, _ in results["failed"]] == ["bad.pdf", "broken.pdf"]
    print("PASS: upload_documents separates success and failure correctly")


test_upload_documents_separates_success_and_failure()

This test builds fake stand-ins for the OpenAI client's nested vector_stores.files.upload_and_poll method, controlling exactly what each simulated file path returns, without making any real network call. It confirms that upload_documents correctly buckets a successful upload, a status-level failure, and an exception-raising failure into the right lists. This dependency-injection style — passing a fake object with the same shape as the real client — lets you verify your own ingestion logic in isolation, which is far faster and more reliable than testing against the live API every time you change the surrounding code.

Common Mistakes

Assuming files.create alone makes a document searchable, which it does not — a file only becomes part of a vector store's searchable index once explicitly attached via vector_stores.files.create (or a helper that does both steps), so an uploaded-but-unattached file will never be retrieved.

Not polling or checking status after a batch upload, leading to silent gaps in the knowledge base when some files fail to parse — always inspect file_counts.failed or per-file status after any ingestion run.

Re-uploading and re-attaching an entire document set on every deploy, wasting time and money on unchanged files — track which files are already successfully indexed (by storing file IDs and content hashes) and only upload what's new or changed.

Best Practices

Wrap every upload in error handling that records failures with enough detail to act on, including the file path and either the returned status or the exception message, so a failed ingestion run produces an actionable report rather than a silent partial success.

Prefer batch uploads for anything beyond a handful of files, since they reduce round trips and give you aggregate status in one call rather than requiring you to track many individual requests.

Keep a durable record, outside the vector store itself, of which source files have been uploaded and their resulting file IDs, so you can later delete, replace, or audit specific documents without having to reverse-engineer that mapping from the vector store's contents.

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 Uploading Documents for Retrieval and get answers drawn from it.

Signed-in readers only.