File Search and Vector Stores

Ma Mahalakshmi V Updated 16 Sep 2026
11 min read ·Lesson 38 of 224

The Problem File Search Solves

Unit 7, Lesson 2 covered sending a PDF or other document directly to the model as input_file, and Unit 7, Lesson 5 built a project around answering questions from a single uploaded document. That approach works well for a handful of documents queried directly, but it doesn't scale to a genuinely large document collection — hundreds or thousands of files, a full knowledge base, a company's entire internal documentation. Sending every document in a large collection as input_file on every request would be enormously wasteful (and for a large enough collection, would exceed any practical size limit), since most of a large collection is irrelevant to any single question asked against it.

File search solves this by separating two concerns: first, indexing a document collection once, ahead of time, into a structure optimized for finding relevant pieces of text quickly; and second, at request time, automatically retrieving only the small number of passages actually relevant to a given question and feeding just those into the model, rather than the entire collection. This retrieve-then-generate pattern is often called retrieval-augmented generation, and file search is the platform's built-in implementation of it.

Vector Stores: The Underlying Index

A vector store is the object that holds an indexed document collection. Creating one and uploading files to it is a separate, one-time step from actually querying it.

vector_store = client.vector_stores.create(name="Company Policy Documents")

with open("employee_handbook.pdf", "rb") as f:
    client.vector_stores.files.upload(vector_store_id=vector_store.id, file=f)

with open("expense_policy.pdf", "rb") as f:
    client.vector_stores.files.upload(vector_store_id=vector_store.id, file=f)

Note: The exact method names for creating vector stores and uploading files to them, and the exact supported file types, can vary by SDK version. Confirm the current interface against your installed SDK version's documentation before relying on these specifics.

Behind the scenes, the platform breaks each uploaded document into smaller chunks, converts each chunk into a numerical representation (an embedding — the subject of Unit 10) capturing its meaning, and stores those representations in a structure that supports fast similarity search. None of that machinery is something you need to implement yourself; it is precisely the value a built-in tool provides over building a retrieval system from scratch, which would otherwise require choosing a chunking strategy, an embedding model, and a vector database, and wiring all three together correctly.

Once a vector store is populated, enabling file search on a request and pointing it at that vector store lets the model automatically retrieve relevant passages and use them to answer a question.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="How many vacation days do employees get in their first year?",
    tools=[{"type": "file_search", "vector_store_ids": [vector_store.id]}],
)

print(response.output_text)

The model never sees the full contents of employee_handbook.pdf and expense_policy.pdf in this request — only whichever chunks the retrieval step determined were most relevant to the specific question asked. This is the core efficiency gain over Unit 7's input_file approach: cost and context usage scale with the size of the relevant excerpt, not with the size of the entire underlying document collection, which is what makes file search practical for collections far too large to send in full on every request.

Inspecting What Was Retrieved

As with web search, a response using file search includes structured output items describing the retrieval step, which is worth inspecting both for debugging and for giving users visibility into which documents informed an answer.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What is the maximum reimbursable amount for a client dinner?",
    tools=[{"type": "file_search", "vector_store_ids": [vector_store.id]}],
)

for item in response.output:
    if item.type == "file_search_call":
        print(f"File search performed with query: {item.queries}")
    elif item.type == "message":
        for content_item in item.content:
            for annotation in getattr(content_item, "annotations", []):
                if getattr(annotation, "type", None) == "file_citation":
                    print(f"Cited file: {annotation.filename}")

Note: The exact structure of file_search_call items and file citation annotations can vary by SDK version. Confirm the current output shape against your installed SDK version's documentation.

Surfacing which specific file a piece of an answer was drawn from — mirroring the citation guidance Lesson 1 gave for web search — matters for the same underlying reason: it lets a user (an employee checking the reimbursement policy, say) verify the answer against the actual source document rather than trusting a synthesized answer without any way to check it, which is especially important for policy or compliance questions where getting the specific number wrong has real consequences.

Comparing File Search to input_file

Unit 7, Lesson 2 and Lesson 5 covered sending a document directly with input_file. It's worth being precise about when each approach is the right one, since they solve related but distinct problems.

Aspectinput_file (Unit 7)File search (this lesson)
Best forA small number of specific documents relevant to the current requestA large collection where only a small, unknown-in-advance subset is relevant to any given question
SetupNone — attach the file directly to the requestRequires creating a vector store and uploading files ahead of time
Cost per requestScales with the size of the documents sentScales with the size of the retrieved passages, not the whole collection
The model seesThe entire document (or documents) sentOnly the specific chunks retrieval determined were relevant
Typical use"Answer this question about this specific report" (Unit 7, Lesson 5's project)"Answer this question, drawing from our entire internal knowledge base"

A useful rule of thumb: if you already know which one or two documents are relevant to a given question before you ask it, input_file is simpler and avoids retrieval's inherent uncertainty about whether the right passage was actually found. If the relevant document (or even which document) isn't known in advance, and the collection is too large to send in full, file search is the tool built for exactly that situation.

Managing a Vector Store Over Time

A vector store is a persistent resource — documents can be added or removed from it independently of any specific query, letting a knowledge base stay current as underlying documents change.

# Add a newly published document to an existing vector store
with open("updated_travel_policy.pdf", "rb") as f:
    client.vector_stores.files.upload(vector_store_id=vector_store.id, file=f)

# Remove an outdated document
client.vector_stores.files.delete(vector_store_id=vector_store.id, file_id="file-abc123")

# List what's currently indexed
files_in_store = client.vector_stores.files.list(vector_store_id=vector_store.id)
for file_entry in files_in_store.data:
    print(file_entry.id, file_entry.status)

Treating a vector store as a living resource that needs upkeep — removing a superseded policy document when a new version is published, for instance — matters in practice: file search has no way to know that expense_policy.pdf has since been replaced by updated_travel_policy.pdf unless the outdated file is actually removed from the store, and an outdated document left in an otherwise current knowledge base is a realistic source of a subtly wrong answer that looks just as confident and well-cited as a correct one.

A single request can search across more than one vector store at once, letting an application organize document collections by category (a "Policies" store, a "Product Documentation" store, an "Engineering Runbooks" store) while still supporting a combined query when needed.

policy_store = client.vector_stores.create(name="Policies")
product_docs_store = client.vector_stores.create(name="Product Documentation")

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What's our return policy, and does the Model X support USB-C charging?",
    tools=[{"type": "file_search", "vector_store_ids": [policy_store.id, product_docs_store.id]}],
)

Organizing document collections into separate, purpose-specific vector stores (rather than one large undifferentiated store containing everything) mirrors Unit 8, Lesson 4's guidance on grouping related tools by area of responsibility — it makes it straightforward to scope a specific request to only the relevant subset of documents (a customer support feature might only ever search product_docs_store, while an internal HR tool might only search a separate policies store), rather than always searching the same single undifferentiated collection regardless of what a given feature actually needs access to.

Combining File Search With Structured Outputs

File search's retrieved content can feed into a structured-output request exactly as Unit 7, Lesson 5's PDF question-answering project used input_file content, applying the same tiered-confidence pattern to a much larger underlying document collection.

from pydantic import BaseModel
from enum import Enum

class AnswerConfidence(str, Enum):
    HIGH = "high"
    PARTIAL = "partial"
    NOT_FOUND = "not_found"

class PolicyAnswer(BaseModel):
    confidence: AnswerConfidence
    answer: str
    source_document: str | None

response = client.responses.parse(
    model="gpt-5.6-terra",
    instructions="Answer using only the retrieved policy documents. If the documents don't address the question, say so.",
    input="What's the policy on remote work for new hires?",
    tools=[{"type": "file_search", "vector_store_ids": [policy_store.id]}],
    text_format=PolicyAnswer,
)
print(response.output_parsed)

This combines three separate techniques from across the course into one request: file search (this lesson) retrieves the relevant passages, client.responses.parse() with text_format (Unit 6) shapes the final answer into a validated structure, and the AnswerConfidence enum (following the same tiered-outcome design Unit 7, Lesson 5 introduced) lets the response honestly represent whether the retrieved documents actually addressed the question, rather than forcing a single confident-looking string regardless of how well-supported the answer actually is.

Tuning Retrieval: Result Count and Filters

File search typically exposes a small number of configuration options controlling how retrieval behaves, worth adjusting deliberately rather than leaving at whatever default applies.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What's the policy on expensing client meals?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [policy_store.id],
        "max_num_results": 3,
        "filters": {"type": "eq", "key": "department", "value": "finance"},
    }],
)

Note: The exact set of supported configuration options for file search (max_num_results, metadata filters, and any others) and their exact syntax can vary by SDK version. Confirm current options against your installed SDK version's documentation.

max_num_results bounds how many retrieved chunks are fed into the model — a smaller number reduces cost and keeps the model focused on only the most relevant passages, while a larger number gives the model more surrounding context at the expense of additional cost, mirroring the same "more input isn't automatically better" theme Unit 3 raised for prompt construction generally. Metadata filters, where supported, narrow retrieval to documents matching specific attributes (a department, a document type, a date range) attached to files when they were uploaded — useful when a vector store intentionally holds documents from multiple categories but a specific request should only ever draw from one of them, which is a finer-grained alternative to fully separating documents into distinct vector stores as the earlier multi-store example did.

Testing Retrieval-Dependent Logic Without Real Vector Stores

Following this course's established dependency-injection pattern, code that processes a file-search response (extracting citations, checking confidence, and so on) can be tested with a fake response object, without needing a real populated vector store or a real model call.

class FakeAnnotation:
    def __init__(self, filename):
        self.type = "file_citation"
        self.filename = filename

class FakeContentItem:
    def __init__(self, annotations):
        self.annotations = annotations

class FakeMessageItem:
    def __init__(self, content):
        self.type = "message"
        self.content = content

class FakeResponse:
    def __init__(self, output):
        self.output = output

def extract_file_citations(response) -> list[str]:
    filenames = []
    for item in response.output:
        if item.type != "message":
            continue
        for content_item in item.content:
            for annotation in getattr(content_item, "annotations", []):
                if getattr(annotation, "type", None) == "file_citation":
                    filenames.append(annotation.filename)
    return filenames

def test_extract_file_citations():
    fake_response = FakeResponse(output=[
        FakeMessageItem(content=[FakeContentItem(annotations=[FakeAnnotation("employee_handbook.pdf")])]),
    ])
    result = extract_file_citations(fake_response)
    assert result == ["employee_handbook.pdf"]
    print("PASS: extract_file_citations correctly pulls filenames from a fake response")

test_extract_file_citations()

Building this small hierarchy of fake classes mirrors the same testing pattern this course has used for every other tool-augmented response shape (web search in Lesson 1, function calls throughout Unit 8): it lets citation-extraction and response-processing logic be verified deterministically, without depending on a populated vector store, real document content, or a real, potentially non-deterministic model call to actually exercise retrieval.

Common Mistakes

Sending an entire large document collection with input_file instead of using file search, incurring unnecessary cost and risking exceeding practical size limits, when only a small subset of the collection is actually relevant to any given question.

Letting outdated documents remain in a vector store after they've been superseded, producing confident, well-cited answers based on policy or information that is no longer current.

Not surfacing file citations in a user-facing feature, removing the ability to verify a retrieval-based answer against its actual source document.

Using file search for a request where the relevant document is already known in advance, adding retrieval's inherent uncertainty (whether the right passage was actually found) to a case where input_file would have been simpler and more direct.

Best Practices

Use file search for large, evolving document collections, and input_file for a small, already-identified set of documents, matching the tool to the actual scale and certainty of the retrieval problem.

Keep vector stores current by removing superseded documents as they're replaced, rather than letting a knowledge base silently accumulate outdated content alongside current content.

Organize documents into purpose-specific vector stores rather than one large undifferentiated collection, making it straightforward to scope a given feature's search to only the relevant subset of documents.

Surface file citations in any user-facing feature built on file search, and combine file search with structured outputs and a tiered-confidence schema when an application needs to distinguish a well-supported answer from one the retrieved documents didn't actually address.

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 File Search and Vector Stores and get answers drawn from it.

Signed-in readers only.