How to filter by metadata in LlamaIndex

Semantic similarity can rank text from every node in an index even when only one tenant, source, or document class is eligible for a request. Metadata filters narrow that candidate set at retrieval time, so the returned nodes satisfy the required field values as well as the text query.

LlamaIndex uses ExactMatchFilter for one equality rule and MetadataFilters to group rules for a retriever. Setting FilterCondition.AND requires every rule in the group to match before index.as_retriever() returns a node.

The in-memory SimpleVectorStore supports the exact-match check without an external database. Other vector stores can support different operator sets and nested combinations, so confirm the chosen integration before replacing equality with range, text-match, or containment operators.

Steps to filter LlamaIndex retrieval by metadata:

  1. Create the script foundation for three metadata-bearing nodes.
    metadata_filters_check.py
    from llama_index.core import Settings, VectorStoreIndex
    from llama_index.core.embeddings import MockEmbedding
    from llama_index.core.schema import TextNode
    from llama_index.core.vector_stores import (
        ExactMatchFilter,
        FilterCondition,
        MetadataFilters,
    )
     
    Settings.embed_model = MockEmbedding(embed_dim=8)
     
    nodes = [
        TextNode(text="Refund requests require billing review.", metadata={"tenant": "atlas", "source": "billing"}),
        TextNode(text="Shipping claims require logistics review.", metadata={"tenant": "atlas", "source": "logistics"}),
        TextNode(text="Refund exports require finance approval.", metadata={"tenant": "contoso", "source": "billing"}),
    ]
     
    index = VectorStoreIndex(nodes)

    MockEmbedding keeps the retrieval check local and deterministic without an API key or model download.

  2. Append a result printer below the index definition.
    def print_matches(label, matches):
        print(f"{label}_count={len(matches)}")
        for item in sorted(matches, key=lambda match: match.node.metadata["source"]):
            metadata = item.node.metadata
            print(f"tenant={metadata['tenant']} source={metadata['source']}")
  3. Append an exact tenant filter below the result printer.
    tenant_filters = MetadataFilters(
        filters=[ExactMatchFilter(key="tenant", value="atlas")]
    )
    tenant_retriever = index.as_retriever(
        similarity_top_k=3,
        filters=tenant_filters,
    )
    tenant_matches = tenant_retriever.retrieve("review requests")
    print_matches("tenant_atlas", tenant_matches)
  4. Append the combined tenant-and-source filter after the first result print.
    tenant_source_filters = MetadataFilters(
        filters=[
            ExactMatchFilter(key="tenant", value="atlas"),
            ExactMatchFilter(key="source", value="billing"),
        ],
        condition=FilterCondition.AND,
    )
    tenant_source_retriever = index.as_retriever(
        similarity_top_k=3,
        filters=tenant_source_filters,
    )
    tenant_source_matches = tenant_source_retriever.retrieve("refund review")
     
    assert {item.node.metadata["tenant"] for item in tenant_matches} == {"atlas"}
    assert [item.node.metadata for item in tenant_source_matches] == [
        {"tenant": "atlas", "source": "billing"}
    ]
     
    print_matches("tenant_atlas_source_billing", tenant_source_matches)

    Metadata value types must match the indexed nodes. The string "2026" and the integer 2026 are different exact-match values.

  5. Compare the completed metadata_filters_check.py file with this consolidated version.
    metadata_filters_check.py
    from llama_index.core import Settings, VectorStoreIndex
    from llama_index.core.embeddings import MockEmbedding
    from llama_index.core.schema import TextNode
    from llama_index.core.vector_stores import (
        ExactMatchFilter,
        FilterCondition,
        MetadataFilters,
    )
     
     
    Settings.embed_model = MockEmbedding(embed_dim=8)
     
    nodes = [
        TextNode(text="Refund requests require billing review.", metadata={"tenant": "atlas", "source": "billing"}),
        TextNode(text="Shipping claims require logistics review.", metadata={"tenant": "atlas", "source": "logistics"}),
        TextNode(text="Refund exports require finance approval.", metadata={"tenant": "contoso", "source": "billing"}),
    ]
     
    index = VectorStoreIndex(nodes)
     
     
    def print_matches(label, matches):
        print(f"{label}_count={len(matches)}")
        for item in sorted(matches, key=lambda match: match.node.metadata["source"]):
            metadata = item.node.metadata
            print(f"tenant={metadata['tenant']} source={metadata['source']}")
     
     
    tenant_filters = MetadataFilters(
        filters=[ExactMatchFilter(key="tenant", value="atlas")]
    )
    tenant_retriever = index.as_retriever(
        similarity_top_k=3,
        filters=tenant_filters,
    )
    tenant_matches = tenant_retriever.retrieve("review requests")
    print_matches("tenant_atlas", tenant_matches)
     
    tenant_source_filters = MetadataFilters(
        filters=[
            ExactMatchFilter(key="tenant", value="atlas"),
            ExactMatchFilter(key="source", value="billing"),
        ],
        condition=FilterCondition.AND,
    )
    tenant_source_retriever = index.as_retriever(
        similarity_top_k=3,
        filters=tenant_source_filters,
    )
    tenant_source_matches = tenant_source_retriever.retrieve("refund review")
     
    assert {item.node.metadata["tenant"] for item in tenant_matches} == {"atlas"}
    assert [item.node.metadata for item in tenant_source_matches] == [
        {"tenant": "atlas", "source": "billing"}
    ]
     
    print_matches("tenant_atlas_source_billing", tenant_source_matches)
  6. Run the completed script from the directory that contains /metadata_filters_check.py.
    $ python metadata_filters_check.py
    tenant_atlas_count=2
    tenant=atlas source=billing
    tenant=atlas source=logistics
    tenant_atlas_source_billing_count=1
    tenant=atlas source=billing

    The assertions stop the script if the tenant filter leaks another tenant or the AND group returns anything except the atlas billing node.