2025, Oct 01 11:18

Как фильтровать None в метаданных LlamaIndex: два простых подхода

Разбираем, почему фильтр LlamaIndex не находит значения None в метаданных, и показываем два проверенных способа обхода с примерами кода и настройкой Ollama.

Фильтрация по метаданным, в которых встречаются значения, похожие на null, легко даёт сбой в конвейерах извлечения. Если вы работаете с LlamaIndex и рассчитываете, что фильтр со значением None найдёт документы, где в метаданных явно записано None, вы получите пустой результат. Ниже — краткий разбор поведения, минимальный пример кода и два простых подхода, которые позволяют надёжно выполнять такие запросы.

Воспроизведение проблемы

В примере ниже создаётся VectorStoreIndex с единственным TextNode, в метаданных которого поле start_date установлено в None. Фильтр метаданных с FilterOperator.EQ и value=None не возвращает ни одного узла.

from llama_index.core import VectorStoreIndex
from llama_index.core.schema import TextNode
from llama_index.core.vector_stores import (
    MetadataFilter,
    MetadataFilters,
    FilterOperator,
)

sample_node = TextNode(
    text="This document has None in the metadata",
    id_="node_01",
    metadata={"start_date": None},
)

idx = VectorStoreIndex([sample_node])

print("Index nodes:", [n.metadata for n in idx.docstore.docs.values()])

null_date_rule = MetadataFilter(key="start_date", operator=FilterOperator.EQ, value=None)
rule_set = MetadataFilters(filters=[null_date_rule])
fetcher = idx.as_retriever(filters=rule_set, similarity_top_k=1)
results = fetcher.retrieve("this")

print("Retrieved nodes:", [(n.node_id, n.metadata) for n in results])

Как видно из вывода, в метаданных действительно хранится значение None, однако фильтр ничего не находит.

Index nodes:
 [{'start_date': None}]
Retrieved nodes:
 []

Что происходит и почему

Это ожидаемое поведение LlamaIndex: значение None нельзя отфильтровать. Даже если в метаданных поле явно установлено в None, фильтр со значением None не сопоставит его. Если вам нужна семантика «null», необходимо сохранять конкретное, фильтруемое значение.

Как заставить это работать на практике

Есть два практичных подхода. Первый — сериализовать «пустое» значение как строку и фильтровать по этой строке. Второй — сохранять пустую строку и фильтровать по пустой строке. Оба варианта делают поле фильтруемым, не меняя остальную логику извлечения.

Если дополнительно требуется настроить эмбеддинги и LLM через Ollama, перед построением индекса можно использовать следующую конфигурацию.

from llama_index.core import VectorStoreIndex, Settings
from llama_index.embeddings.ollama import OllamaEmbedding
from llama_index.llms.ollama import Ollama

emb_backend = OllamaEmbedding(
    model_name="llama3.2",
    base_url="http://localhost:11434",
)

Settings.embed_model = emb_backend

Первый вариант сохраняет сторожевое значение строкой "None" и выполняет фильтрацию с помощью str(None).

from llama_index.core.schema import TextNode
from llama_index.core.vector_stores import (
    MetadataFilter,
    MetadataFilters,
    FilterOperator,
)
from llama_index.core import VectorStoreIndex

DocA = TextNode(
    text="This document has None in the metadata",
    id_="node_01",
    metadata={"start_date": "None"},
)
DocB = TextNode(
    text="This document has start date in the metadata",
    id_="node_02",
    metadata={"start_date": "20/03/2023"},
)

idx = VectorStoreIndex([DocA, DocB])

print("Index nodes:", [d.metadata for d in idx.docstore.docs.values()])

null_date_rule = MetadataFilter(key="start_date", operator=FilterOperator.EQ, value=str(None))
rule_set = MetadataFilters(filters=[null_date_rule])
fetcher = idx.as_retriever(filters=rule_set, similarity_top_k=1)
results = fetcher.retrieve("this")

print("Retrieved nodes:", [(x.node_id, x.metadata) for x in results])

Ожидаемый вывод показывает, что узел со сторожевой строкой совпадает по фильтру.

Index nodes:
 [{'start_date': 'None'}, {'start_date': '20/03/2023'}]
Retrieved nodes:
 [('node_01', {'start_date': 'None'})]

Второй вариант сохраняет пустую строку и фильтрует по пустой строке.

from llama_index.core.schema import TextNode
from llama_index.core.vector_stores import (
    MetadataFilter,
    MetadataFilters,
    FilterOperator,
)
from llama_index.core import VectorStoreIndex

DocA = TextNode(
    text="This document has None in the metadata",
    id_="node_01",
    metadata={"start_date": ""},
)
DocB = TextNode(
    text="This document has start date in the metadata",
    id_="node_02",
    metadata={"start_date": "20/03/2023"},
)

idx = VectorStoreIndex([DocA, DocB])

print("Index nodes:", [d.metadata for d in idx.docstore.docs.values()])

null_date_rule = MetadataFilter(key="start_date", operator=FilterOperator.EQ, value="")
rule_set = MetadataFilters(filters=[null_date_rule])
fetcher = idx.as_retriever(filters=rule_set, similarity_top_k=1)
results = fetcher.retrieve("this")

print("Retrieved nodes:", [(x.node_id, x.metadata) for x in results])

Ожидаемый вывод подтверждает: фильтрация по пустой строке возвращает нужный узел.

Index nodes:
 [{'start_date': ''}, {'start_date': '20/03/2023'}]
Retrieved nodes:
 [('node_01', {'start_date': ''})]

Почему это важно

В системах извлечения, где маршрутизация или постфильтрация опираются на метаданные, тихие пропуски документов обходятся дорого. Понимание того, что в LlamaIndex значение None не фильтруется, помогает нормализовать «пустые» данные на этапе загрузки и сохранить предсказуемое поведение запросов.

Выводы

Если поле может быть концептуально пустым, сохраняйте явное фильтруемое представление. Используйте строку "None" и запрашивайте str(None) или храните пустую строку и фильтруйте по ней. Важно поддерживать единообразие представления как при индексации, так и при фильтрации, чтобы извлекатели VectorStoreIndex возвращали ожидаемые узлы.

Статья основана на вопросе на StackOverflow от Gino и ответе от Ajeet Verma.