Skip to main content
Lesson 7 of 20 · API Tools: Search, Files, and Code Interpreter

File Search: Index and Query Documents

Lesson 7 of 20 Developer
On this page
  1. File Search: Index and Query Documents
  2. Create a Vector Store
  3. Upload and Index Files
  4. Query with File Search
  5. Examine Search Results
  6. Configure Search
  7. Manage Vector Store Lifecycle
  8. Complete Example: Documentation Assistant
  9. Key Takeaways

File Search: Index and Query Documents

File Search lets the model look through your private documents. You upload files to a vector store, and the model automatically queries them whenever it needs specific information. This is the foundation of RAG built directly into the OpenAI API.

Create a Vector Store

The vector store is the organizational unit for File Search, and the structure you choose here will stick with you for a long time. A single store for the entire company is easy to administer but mixes documents with unrelated vocabularies, which degrades relevance. Separate stores by team or domain require a bit more management but make each search significantly more precise. When in doubt, separate them: merging later is easier than untangling a mess.

from openai import OpenAI

client = OpenAI()

# Create the vector store
vector_store = client.vector_stores.create(
    name="product-documentation"
)
print(f"Vector store created: {vector_store.id}")

Upload and Index Files

Indexing is asynchronous, and this is the most common source of error on the first try: you add files, query immediately, and the model says it found nothing—not because the search failed, but because nothing was indexed yet. Always check the status before querying, as shown in the lifecycle section.

A word on formats: the list is broad, but not all files are equal. A PDF generated from a word processor extracts cleanly; a scanned PDF without a text layer adds nothing, since there are literally no words to index.

import os

# Upload a single file
with open("user-guide.pdf", "rb") as f:
    file_obj = client.files.create(file=f, purpose="assistants")

# Attach to the vector store
client.vector_stores.files.create(
    vector_store_id=vector_store.id,
    file_id=file_obj.id
)

# Batch upload
folder = "./documentation/"
for filename in os.listdir(folder):
    path = os.path.join(folder, filename)
    if os.path.isfile(path):
        with open(path, "rb") as f:
            file_obj = client.files.create(file=f, purpose="assistants")
        client.vector_stores.files.create(
            vector_store_id=vector_store.id,
            file_id=file_obj.id
        )
        print(f"Indexed: {filename}")

The call is intentionally straightforward: you declare the tool, ask your question, and the model decides whether to search. All the RAG work—chunking, vectorization, ranking, and injection into context—happens on the server. This saves considerable time at startup, and the next lesson covers the settings you need to know when defaults aren’t enough.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What is the refund procedure for premium clients?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [vector_store.id]
    }]
)

print(response.output_text)

The model automatically searches your documents and formulates an answer based on the content it finds.

Examine Search Results

Annotations are what distinguish a verifiable answer from a mere assertion. Expose them to your users: on an internal knowledge base, the ability to trace back to the original document completely changes the level of trust in the system—and helps you spot cases where the model missed the mark.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="What SLAs are guaranteed in the contract?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [vector_store.id]
    }]
)

# Iterate through annotations
for item in response.output:
    if item.type == "message":
        for block in item.content:
            if hasattr(block, "annotations"):
                for ann in block.annotations:
                    if ann.type == "file_citation":
                        print(f"File: {ann.file_id}")
                        print(f"Position: {ann.start_index}-{ann.end_index}")

Number of Results

Increasing the number of passages doesn’t mechanically improve the answer. Beyond ten or so, weakly relevant excerpts dilute the good ones and consume billed context. Raise this value only when the answer is visibly incomplete, not as a matter of principle.

response = client.responses.create(
    model="gpt-5.6-terra",
    input="List all penalty clauses in the contract",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [vector_store.id],
        "max_num_results": 20  # Default: 5, max: 50
    }]
)

Metadata Filtering

Metadata is the most underused lever for precision. Filtering by year, department, or document type before the vector search eliminates noise upfront that ranking would otherwise have to filter out. This also lets you answer “according to the current procedure” rather than mixing three successive versions of the same document.

# Upload with metadata
with open("contract-2026.pdf", "rb") as f:
    file_obj = client.files.create(file=f, purpose="assistants")

client.vector_stores.files.create(
    vector_store_id=vector_store.id,
    file_id=file_obj.id,
    attributes={
        "doc_type": "contract",
        "year": 2026,
        "client": "acme-corp"
    }
)

# Filtered search
response = client.responses.create(
    model="gpt-5.6-terra",
    input="What is the due date?",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [vector_store.id],
        "filters": {
            "type": "eq",
            "key": "doc_type",
            "value": "contract"
        }
    }]
)

Compound Filters

Combine multiple criteria using logical operators:

response = client.responses.create(
    model="gpt-5.6-terra",
    input="Summarize contractual obligations for 2026",
    tools=[{
        "type": "file_search",
        "vector_store_ids": [vector_store.id],
        "filters": {
            "type": "and",
            "filters": [
                {"type": "eq", "key": "doc_type", "value": "contract"},
                {"type": "gte", "key": "year", "value": 2026}
            ]
        }
    }]
)

Manage Vector Store Lifecycle

Monitor Indexing

# Check vector store status
vs = client.vector_stores.retrieve(vector_store.id)
print(f"Status: {vs.status}")
print(f"Files: {vs.file_counts.completed} indexed, "
      f"{vs.file_counts.in_progress} in progress, "
      f"{vs.file_counts.failed} failed")

Remove Obsolete Files

# List files in the vector store
files = client.vector_stores.files.list(vector_store.id)
for f in files.data:
    print(f"{f.id} - {f.status}")

# Detach a file
client.vector_stores.files.delete(
    vector_store_id=vector_store.id,
    file_id="file_abc123"
)

Complete Example: Documentation Assistant

def documentation_assistant(question: str, vs_id: str) -> dict:
    """Assistant that answers based on internal documentation."""
    response = client.responses.create(
        model="gpt-5.6-terra",
        instructions=(
            "You are a documentation assistant. Answer solely based on the "
            "provided documents. If the information is not in the documents, "
            "state that clearly. Cite your sources."
        ),
        input=question,
        tools=[{
            "type": "file_search",
            "vector_store_ids": [vs_id],
            "max_num_results": 10
        }]
    )

    return {
        "answer": response.output_text,
        "model": response.model,
        "tokens": response.usage.total_tokens
    }

Key Takeaways

  • Create a vector store and upload your files to enable File Search
  • The model decides on its own when to search documents
  • max_num_results controls search depth
  • Metadata and filters enable targeted search
  • Monitor indexing status before querying