Skip to content

Migrating LlamaIndex From GPTSimpleVectorIndex to VectorStoreIndex

9 min read · updated August 11, 2026

An older LlamaIndex script fails at the import line, and the obvious fix — change the class name — produces a second import error, then a missing-method error, then a file it cannot read. Four changes are stacked on top of each other and it is worth taking them in order.

The error, and which one you have

ImportError: cannot import name 'GPTSimpleVectorIndex' from 'llama_index'

That is the first one. If you fix the class name and get a second import error naming the new class, you have hit the packaging change rather than the rename: the project was split into a slim core with separately installed integrations, and the core symbols live under llama_index.core rather than at the top level of llama_index. Those are two independent breaking changes and the error message for both is ImportError, which is why the first fix appears not to work.

A third variant: AttributeError on query, or on save_to_disk / load_from_disk. Those are the method changes described below, and they arrive only after the imports resolve.

The rename chain

The class you are looking for has been renamed twice. GPTSimpleVectorIndex became GPTVectorStoreIndex when the project unified its vector indexes behind a single interface with a pluggable vector store, and then the GPT prefix was dropped across the board, leaving VectorStoreIndex. LlamaIndex’s own documentation keeps a deprecated-terms page listing the old spellings against the current ones, which is the authority worth checking for any other class you are carrying — the LlamaIndex deprecated terms reference.

The rename is not cosmetic, and this is the part that explains why the old class could not simply be kept as an alias. The old “simple” index was one index type among several, each with its own storage assumptions. The current one is a single index type parameterised by a vector store, where the in-memory store is just the default. Everything you might have chosen at class-construction time in the old API is now chosen by which store you pass, which is why the constructor signature and the persistence model both changed with it.

Class names, import paths and package boundaries in this project have moved more than once. Treat the names on this page as the current shape at the time of writing and check the deprecated-terms reference before assuming a symbol is where you last saw it.

index.query() moved

In the old API you queried the index directly. In the current one the index builds a query engine and the query engine answers:

# old
response = index.query("What is the refund window?")

# current
query_engine = index.as_query_engine()
response = query_engine.query("What is the refund window?")

This looks like an extra line for nothing until you see what it buys. Everything that used to be a keyword argument on query — how many chunks to retrieve, which response synthesis mode to use, which post-processors to apply, whether to stream — is now configured on the engine, which means the engine is a reusable object you build once and the query call takes only the question. It also makes retrieval separable: index.as_retriever() gives you the chunks without calling a model at all, which is the single most useful debugging tool in a RAG system and did not exist as a distinct step in the old shape.

The other configuration change to expect: global settings that used to be assembled into a context object and threaded through construction are now set on a module-level settings object. If your old code builds a service context to pin an LLM or an embedding model, that is the thing to look up next, and it is a separate migration from this one.

Your saved index does not load

This is the part that turns a twenty-minute migration into an afternoon, and it is the part most answers skip. The old API wrote a single JSON file and read it back with a class method. The current one persists a storage context — document store, index store and vector store, as separate artefacts under a directory — and reloads it through a loader function.

# old
index.save_to_disk("index.json")
index = GPTSimpleVectorIndex.load_from_disk("index.json")

# current
index.storage_context.persist(persist_dir="./storage")

from llama_index.core import StorageContext, load_index_from_storage
storage_context = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(storage_context)

These are not two spellings of one format. The old file is not a storage directory and there is no supported conversion, so the honest plan is to rebuild the index from the source documents. Say that out loud at the start of the migration, because it changes the shape of the work: you need the source documents, which you may not have kept, and you need to pay the embedding cost again.

Two things follow. First, if the source documents are gone, the embeddings in that JSON file are still readable as data even though the index is not loadable as an index — you can extract text and vectors and write them into a vector store directly, which is a scripting job rather than a LlamaIndex job. Second, this is the moment to stop persisting to a local directory at all. If the index is big enough that rebuilding it hurts, it is big enough to live in a real vector store, and the current API’s whole point is that the store is a parameter.

Rebuilding also means the chunking is being redone with whatever the current defaults are, which will not match what the old index contained. If retrieval quality matters to you, fix the chunk size and overlap explicitly rather than inheriting a default that has moved — the general treatment is in chunking for RAG.

The rewrite

  1. Pin the versions before you start. Install the current core package and the integration packages you need, and record the versions in your lockfile, so that the migration is against a known target rather than against whatever resolves today.
  2. Fix the imports to the core namespace and the current class names. Run the file and let it fail; do not fix ahead of the errors.
  3. Replace index.query(...) with an explicit query engine, and move every keyword argument you were passing onto the engine constructor.
  4. Replace save_to_disk / load_from_disk with the storage context persist and load functions, and accept that the existing file will not be read.
  5. Rebuild the index from source documents with explicit chunk settings and an explicit embedding model, rather than defaults.
  6. Before you delete the old artefacts, run the same ten questions through both the old code path and the new one and compare the retrieved chunks, not the generated answers. Answers vary; chunks are the thing you are migrating.
from llama_index.core import (
    VectorStoreIndex,
    SimpleDirectoryReader,
    StorageContext,
    load_index_from_storage,
)

documents = SimpleDirectoryReader("./docs").load_data()
index = VectorStoreIndex.from_documents(documents)
index.storage_context.persist(persist_dir="./storage")

# later, in a different process
storage_context = StorageContext.from_defaults(persist_dir="./storage")
index = load_index_from_storage(storage_context)
query_engine = index.as_query_engine(similarity_top_k=5)
print(query_engine.query("What is the refund window?"))

Step six is the one that catches the silent regression. A migration that changes the chunker and the embedding model at the same time as the API will retrieve different passages, and comparing generated answers hides that behind the model’s ability to write a decent paragraph from mediocre context.