Use this file to discover all available pages before exploring further.
Building a retrieval system from scratch means parsing documents, chunking text, generating embeddings, managing a vector database, writing retrieval logic, and wiring it all into your LLM calls. The file search tool in the Responses API handles all of that for you. Upload your files, create a vector store, and pass the store ID in your API call — OpenAI takes care of chunking, embedding, retrieval scoring, and synthesis. The result is grounded, document-cited answers in a single request.
Upload your document using the Files API with purpose="assistants". The file is stored on OpenAI’s servers and made available for indexing.
from openai import OpenAIclient = OpenAI()with open("knowledge_base.pdf", "rb") as f: file = client.files.create(file=f, purpose="assistants")print(file.id) # file-abc123
2
Create a vector store
Create a vector store and associate your uploaded file with it. OpenAI reads the file, splits it into chunks, generates embeddings, and indexes them automatically.
Indexing is asynchronous. Check vector_store.file_counts.completed to confirm the file has finished processing before querying.
3
Query using file search
Pass the vector store ID in the file_search tool definition. The model retrieves relevant context and generates a grounded answer.
response = client.responses.create( model="gpt-4o", input="What does the document say about pricing?", tools=[{ "type": "file_search", "vector_store_ids": [vector_store.id] }])print(response.output_text)
You can query the vector store directly — without an LLM call — using the vector search API. This is useful for inspecting retrieval quality or building custom ranking logic.
search_results = client.vector_stores.search( vector_store_id=vector_store.id, query="What's the refund policy?")for result in search_results.data: print(f"{result.filename} score={result.score:.4f}") print(result.content[0].text[:200]) print()
Scores are calculated using hybrid search — a combination of BM25 keyword matching and semantic similarity — and range from 0 to 1. Higher scores indicate greater relevance.
To see exactly which chunks were retrieved and used to generate an answer, include output[*].file_search_call.search_results in the include parameter.
response = client.responses.create( model="gpt-4o", input="Summarize the key findings from the Q4 report.", tools=[{ "type": "file_search", "vector_store_ids": [vector_store.id] }], include=["output[*].file_search_call.search_results"])# The file search call is output[0]; the text response is output[1]file_search_call = response.output[0]for chunk in file_search_call.search_results: print(f"From: {chunk.filename} score={chunk.score:.4f}")# Extract cited filenames from annotationsannotations = response.output[1].content[0].annotationscited_files = {a.filename for a in annotations}print("Files cited in response:", cited_files)print(response.output[1].content[0].text)
The annotations list on the text output item maps citation markers in the response text to the specific file chunks that were used. This gives you full traceability from answer back to source.
Because the Responses API is stateful, you can run multi-turn conversations that reference documents without re-specifying the vector store on every turn.
# First turnresponse = client.responses.create( model="gpt-4o", input="What are the main topics covered in the handbook?", tools=[{ "type": "file_search", "vector_store_ids": [vector_store.id] }])print(response.output_text)# Follow-up — context is preserved automaticallyfollow_up = client.responses.create( model="gpt-4o", input="Which of those topics covers remote work policies?", previous_response_id=response.id, tools=[{ "type": "file_search", "vector_store_ids": [vector_store.id] }])print(follow_up.output_text)
Python, JavaScript, TypeScript, Java, C, C++, Go, Ruby, PHP, Shell, and more
Individual file size is limited to 512 MB. A single vector store can hold up to 10,000 files. For files larger than this limit, consider splitting them before uploading.
OpenAI automatically splits your documents into overlapping chunks of approximately 800 tokens each. You do not need to choose a chunking strategy — the default works well for most document types.During retrieval, chunks are ranked using a hybrid scoring model:
Semantic search — embedding-based cosine similarity captures meaning and paraphrase
Keyword search (BM25) — term frequency matching captures exact phrases and named entities
Hybrid reranking — scores from both signals are fused and reranked before the top results are passed to the model
In practice, this means a query like “What’s Deep Research?” returns highly relevant chunks even when the document uses phrasing like “our deep research capability” rather than the exact query string.
You can attach metadata to files at upload time and filter on it during search. This is useful for multi-tenant applications or document collections with logical partitions (by department, date range, or document type).
# Attach metadata when creating the vector store file associationclient.vector_stores.files.create( vector_store_id=vector_store.id, file_id=file.id, attributes={"department": "engineering", "year": 2024})# Filter at query timeresponse = client.responses.create( model="gpt-4o", input="What were the engineering team's goals?", tools=[{ "type": "file_search", "vector_store_ids": [vector_store.id], "filters": { "type": "eq", "key": "department", "value": "engineering" } }])
Metadata filtering narrows the retrieval pool before scoring. If your filter is too restrictive, the model may not find relevant chunks and will say so in its response rather than hallucinating.