Skip to content

[FIX] Apply the metadata filter in Neptune Analytics vector search - #675

Draft
noel-improv wants to merge 3 commits into
awslabs:mainfrom
noel-improv:fix/neptune-analytics-topk-filter
Draft

noel-improv wants to merge 3 commits into
awslabs:mainfrom
noel-improv:fix/neptune-analytics-topk-filter

Conversation

@noel-improv

@noel-improv noel-improv commented Nov 5, 2026 •

Copy link
Copy Markdown
Collaborator

Description

On Neptune Analytics, NeptuneIndex.top_k accepted filter_config and never used it, so vector search returned the nearest nodes from the whole tenant whatever filter the caller passed. This applies the filter in the query, as the OpenSearch, PGVector and S3 Vectors indexes do, and fixes the case-insensitive match in the shared openCypher filter builder, which the new path exposed.

Changes

  • NeptuneIndex.top_k adds a WHERE on the source the query already matches. The filter runs after the nearest-neighbour search, so a filtered call asks for 5 * top_k neighbours (FILTER_OVER_FETCH_FACTOR) and keeps the first top_k that match. The docstring says a filtered call can return fewer than top_k.
  • A filter that matches nothing returns [] without embedding or querying, as the S3 Vectors and OpenSearch indexes do. top_k is read with int() and floored at 1.
  • A filter operator the openCypher builder cannot express (IN, NIN, CONTAINS, ANY, ALL) now raises ValueError, as it does for PGVector and S3 Vectors. Before this change Neptune Analytics ignored the filter, so these calls used to return unfiltered results.
  • TEXT_MATCH_INSENSITIVE renders toLower(source.<key>) instead of source.<key>.toLower(), which is not openCypher.
  • New integration test neptune_analytics_vector_filter.NeptuneAnalyticsVectorFilter in lexical.short. It runs on the neptune-graph env-type and skips on other vector stores.

Problem

Where a later step filters the results (the traversal retrievers' FilterByMetadata), callers got fewer than top_k results or none. Where nothing filters later, they got results the filter should have excluded. entity_vss_provider.py and vector_utils.py pass the filter straight through.

Related issue (if any): #

Testing

  • Unit tests added/updated
  • Integration tests added (as appropriate)
  • Existing tests pass (pytest)
  • Tested manually (describe below)

Unit: tests/unit 2793 passed (the one error is the known test_boto3_botocore_version_alignment). Reverting neptune_vector_indexes.py fails 6 of the new tests; reverting graph_utils.py fails the case-insensitive test.

Integration: lexical.short on --env-type neptune-graph, 26 passed and 3 skipped (the two PGVector tests and the GPU reranker, as expected for that env-type). NeptuneAnalyticsVectorFilter passed all four assertions: the unfiltered query returned chunks from all four corpus pages, the filtered query returned 3 chunks all from the filtered page, and a case-insensitive filter on the same page ran and returned the same source. MetadataFilteringQuery and the filter-injection and empty-OR safety tests also passed.

Not covered live: the shortfall a selective filter can cause when its matches lie outside the 5 * top_k nearest nodes, which the four-document corpus is too small to reach.

Checklist

  • Code follows existing style and conventions
  • License headers present on new files
  • Documentation updated (if applicable)
  • No breaking changes (or clearly documented)

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

…s as valid openCypher

filter_config_to_opencypher_filters compared the versioning bounds
directly, so with versioning on a source built before it was enabled,
which has no valid_from or valid_to, was dropped. Every other version
check coalesces a missing bound; this now does the same.

TEXT_MATCH_INSENSITIVE rendered source.`key`.toLower(), which is not
openCypher. It now renders toLower(source.`key`).
NeptuneIndex.top_k accepted filter_config and never used it, so vector
search returned the nearest nodes from the whole tenant whatever filter
the caller passed. The filter now runs as a WHERE on the source the
query already matches.

The filter runs after the nearest-neighbour search, so a filtered call
asks the index for five times top_k and keeps the first top_k that
match. A filter that matches nothing returns no results without a
query, as the OpenSearch and S3 Vectors indexes do.

A Neptune Analytics integration test in lexical.short checks that a
filtered top_k returns only the matching source, including with a
case-insensitive filter. It skips on other vector stores.
@noel-improv noel-improv self-assigned this Nov 6, 2026
…nd test an unsupported operator

A graph written since versioning shipped always has valid_from and valid_to,
and older graphs are upgraded by upgrade_for_versioning.py, so the coalesce
only changed results on un-upgraded graphs, including what delete_sources
matched. An operator the query cannot express now has a test that it raises
before any query runs.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant