Composable Graphs allow you to query across multiple LlamaIndex VectorStoreIndex instances, enabling complex question-answering over disparate data sources.
Here’s a small example:
from llama_index.core import VectorStoreIndex, Settings
from llama_index.core.node_parser import SentenceSplitter
from llama_index.core.indices.composability.graph import ComposableGraph
from llama_index.core.objects import ObjectStore
from llama_index.core.storage.storage_context import StorageContext
from llama_index.core.vector_stores import SimpleVectorStore
from llama_index.embeddings.openai import OpenAIEmbedding
from llama_index.llms.openai import OpenAI
from llama_index.core.schema import Document
# Configure LLM and Embedding Model
Settings.llm = OpenAI(model="gpt-3.5-turbo")
Settings.embed_model = OpenAIEmbedding(model="text-embedding-ada-002")
# Create a document for index 1
doc1 = Document(text="Alice is a software engineer at Google.")
# Create a document for index 2
doc2 = Document(text="Bob is a data scientist at Meta.")
# Create index 1
vector_store1 = SimpleVectorStore()
storage_context1 = StorageContext.from_defaults(vector_store=vector_store1)
index1 = VectorStoreIndex.from_documents([doc1], storage_context=storage_context1)
# Create index 2
vector_store2 = SimpleVectorStore()
storage_context2 = StorageContext.from_defaults(vector_store=vector_store2)
index2 = VectorStoreIndex.from_documents([doc2], storage_context=storage_context2)
# Create the ComposableGraph
graph = ComposableGraph.from_indices([index1, index2])
# Query the graph
query_engine = graph.query_engine
response = query_engine.query("Who works at Google?")
print(response)
response = query_engine.query("What is Bob's job?")
print(response)
This code sets up two separate VectorStoreIndex objects, each holding a small document. It then combines these indices into a ComposableGraph and queries it. The graph intelligently routes the query to the relevant index, demonstrating its ability to unify data from multiple sources.
The core problem Composable Graphs solve is the fragmentation of knowledge. Imagine you have customer support tickets in one index, product documentation in another, and internal wikis in a third. Without a unified query layer, answering a question that spans these sources would require manual searching and correlation. Composable Graphs automate this, acting as a sophisticated router for your queries.
Internally, a ComposableGraph orchestrates queries by leveraging a router. When you submit a query, the graph’s query engine uses a high-level LLM to determine which sub-index (or indices) are most relevant to the query. It then dispatches the query to the appropriate index’s query engine, retrieves the results, and synthesizes a final answer. This routing mechanism is configurable, allowing you to define how the graph decides where to send queries.
The main levers you control are the indices you add to the graph and the QueryEngine’s configuration. You can add any LlamaIndex BaseIndex subclass (e.g., VectorStoreIndex, KeywordTableIndex, ListIndex) to a ComposableGraph. The QueryEngine itself can be customized with different retrievers, response synthesizers, and even custom routing logic using Selector subclasses like PydanticSingleSelector or LLMSingleSelector.
When creating the ComposableGraph, you can also assign "names" to your sub-indices. This isn’t just for display; it’s crucial for the routing LLM to understand the context of each index. For example, if you have an index for "customer_support_tickets" and another for "product_manuals," the LLM can use these names to make more informed routing decisions.
# Example with named indices
graph = ComposableGraph.from_indices(
[index1, index2],
index_summaries={
index1: "Information about employees and their companies.",
index2: "Details on job roles and responsibilities."
}
)
The index_summaries argument is key here. It provides descriptive text that the LLM uses to understand the content of each index, vastly improving routing accuracy. Without it, the LLM might only see generic VectorStoreIndex objects, leading to less precise query distribution.
The most surprising aspect of Composable Graphs is how effectively they abstract away the underlying data distribution. You can add, remove, or update indices without changing the core query logic. The graph dynamically adapts, treating a collection of indices as a single, coherent knowledge base. This makes it incredibly powerful for building applications that need to query evolving and distributed datasets.
The next step after mastering Composable Graphs is exploring advanced routing strategies, such as creating custom routers or using more sophisticated selectors to guide query distribution based on complex conditions.