Skip to content

Add post-filter support for VSIM vector search results#1570

Open
hailangx wants to merge 18 commits intomainfrom
haixu/vector-filter-postprocessing
Open

Add post-filter support for VSIM vector search results#1570
hailangx wants to merge 18 commits intomainfrom
haixu/vector-filter-postprocessing

Conversation

@hailangx
Copy link
Member

@hailangx hailangx commented Feb 20, 2026

Adds post-filter support for VSIM vector search results by introducing a JSON-attribute filter expression engine and integrating it into VectorManager after similarity search.

Changes:

  • Introduces a tokenizer/parser/evaluator for filter expressions over JSON attributes (and/or/not, comparisons, arithmetic, in, grouping).
  • Integrates post-filtering into VectorManager for both value-based and element-based similarity search paths.
  • Adds unit tests for the filter engine and RESP integration tests for VSIM ... FILTER ....

Supported syntax documented at Vector Filter Expressions (VSIM ... FILTER)
website/docs/dev/vector-sets.md

VSIM supports FILTER <expression> for attribute-based post filtering.

VSIM query source can be either ELE <element-id> or VALUES <dimensions> <v1> ... <vN>

Examples

VSIM movies ELE dune FILTER '.year >= 1980 and .rating > 7'
VSIM movies ELE dune FILTER '.genre == "action" && .rating > 8.0'
VSIM movies ELE dune FILTER '"classic" in .tags'
VSIM movies ELE dune FILTER '(.year - 2000) ** 2 < 100 and .rating / 2 > 4'
VSIM movies VALUES 3 0.12 0.34 0.56 FILTER '.year >= 1980 and .rating > 7'

Expression syntax

  • Arithmetic: +, -, *, /, %, **
  • Comparison: ==, !=, >, <, >=, <=
  • Logical: and, or, not (also &&, ||, !)
  • Containment: in
  • Grouping: parentheses ()

Field access uses dot notation (for example, .year, .rating, .genre).

Supported values

  • Numbers
  • Strings
  • Booleans (true / false, evaluated as 1 / 0)
  • Arrays (for in when the right side is an attribute array)

Operator precedence (high to low)

  1. primary / parentheses
  2. unary (not, !, unary -)
  3. power (**, right-associative)
  4. multiplicative (*, /, %)
  5. additive (+, -)
  6. containment (in)
  7. comparison (>, <, >=, <=)
  8. equality (==, !=)
  9. logical and (and, &&)
  10. logical or (or, ||)

Notes

  • Keywords are lowercase (and, or, not, in, true, false)
  • Missing attributes are treated as non-matching (null/falsy)
  • Array literals inside expressions (for example, .director in ["a","b"]) are not currently supported

   Implement JSON-path-based filter expressions that are evaluated against
   vector element attributes after similarity search. The filter engine
   includes a tokenizer, expression parser, and evaluator supporting
   comparison operators, logical operators (and/or/not), arithmetic,
   string equality, containment (in), and parenthesized grouping.

   Integrate post-filtering into VectorManager for both VSIM code paths,
   rejecting requests that specify a filter without WITHATTRIBS.
@hailangx hailangx marked this pull request as ready for review February 20, 2026 00:54
Copilot AI review requested due to automatic review settings February 20, 2026 00:54
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds post-filter support for VSIM vector search results by introducing a JSON-attribute filter expression engine and integrating it into VectorManager after similarity search.

Changes:

  • Introduces a tokenizer/parser/evaluator for filter expressions over JSON attributes (and/or/not, comparisons, arithmetic, in, grouping).
  • Integrates post-filtering into VectorManager for both value-based and element-based similarity search paths.
  • Adds unit tests for the filter engine and RESP integration tests for VSIM ... FILTER ....

Reviewed changes

Copilot reviewed 7 out of 7 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
libs/server/Resp/Vector/VectorManager.cs Applies post-filtering to VSIM results and evaluates expressions against per-element attributes.
libs/server/Resp/Vector/Filter/VectorFilterTokenizer.cs Tokenizes filter expressions into numbers/strings/identifiers/operators/keywords.
libs/server/Resp/Vector/Filter/VectorFilterParser.cs Parses tokens into an expression AST with operator precedence.
libs/server/Resp/Vector/Filter/VectorFilterExpression.cs Defines AST node types for literals, member access, unary, and binary ops.
libs/server/Resp/Vector/Filter/VectorFilterEvaluator.cs Evaluates the AST against JsonElement attribute data.
test/Garnet.test/VectorFilterTests.cs Unit tests for tokenizer/parser/evaluator behavior.
test/Garnet.test/RespVectorSetTests.cs Adds RESP-level tests verifying VSIM filtering behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Contributor

Copilot AI commented Feb 20, 2026

@hailangx I've opened a new pull request, #1571, to work on those changes. Once the pull request is ready, I'll request review from you.

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Copy link
Contributor

Copilot AI commented Feb 20, 2026

@hailangx I've opened a new pull request, #1572, to work on those changes. Once the pull request is ready, I'll request review from you.

Copilot AI and others added 2 commits February 19, 2026 17:09
* Initial plan

* Avoid per-result allocation in EvaluateFilter by using Utf8JsonReader with ParseValue

Co-authored-by: hailangx <3389245+hailangx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hailangx <3389245+hailangx@users.noreply.github.com>
…ly (#1572)

* Initial plan

* Fetch attributes internally for filtering when not returning them

Co-authored-by: hailangx <3389245+hailangx@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: hailangx <3389245+hailangx@users.noreply.github.com>
@harsha-simhadri
Copy link

CAn you link the specification of expression syntax you are implementing here

@hailangx
Copy link
Member Author

@microsoft-github-policy-service agree company="Microsoft"

@hailangx
Copy link
Member Author

CAn you link the specification of expression syntax you are implementing here
added into the vector set document

Copy link
Contributor

@kevin-montrose kevin-montrose left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think some fundamental reworking is needed here, exceptions and allocations need to go - I've left a bunch of guideline comments to get us pointed in the right direction. It's not quite an exhaustive review - there are minor optimizations and style things we can revisit latter when we're closer to mergeable.

return numResults;
}

var filterStr = Encoding.UTF8.GetString(filter);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We definitely do not want to work in terms of strings here, that's some expensive validation plus an allocation in a hot path.

var filteredCount = 0;

// Parse the filter expression once, then evaluate per result
var tokens = VectorFilterTokenizer.Tokenize(filterStr);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally we would be able to do this tokenize and parse in one pass - failing that the definitely shouldn't be allocating a new List on each run. Tokenize into offsets and lengths, try and keep it on stack if we can and promote to a heap allocated array only when we exceed some reasonable maximum (512 bytes is probably fine there).

var distWritePos = 0;
var attrWritePos = 0;

for (var i = 0; i < numResults; i++)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will copy many id, attribute, and distance value if filters exclude anything - which we'd expect to be common. For large results sets (or large attributes) that a bunch of work.

A better approach would be to have the ValueSimilarity and ElementSimilarity calls indicate a match count and a set of passing elements (probably actually a bitmap) and then let NetworkVSIM handle skipping elements. Then we have no extra copying.

/// Discriminated union value type to eliminate boxing of doubles/strings
/// throughout the filter evaluation pipeline.
/// </summary>
[StructLayout(LayoutKind.Auto)]
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: this is unnecessary

/// </summary>
internal static class VectorFilterParser
{
public static Expr ParseExpression(List<Token> tokens, int start, out int end)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If I'm reading this correctly, it's extremely recursive - which is very scary. Is a stack overflow possible with reasonable filters here? And how complex a filter can we test with before something breaks?

One complication is that Windows and Linux tend to use different default stack sizes - we mostly dev on Windows, but deployments are more commonly Linux.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

refactor it to one pass postfix (reverse-Polish) design which redis is using, so only a bound stack. will be used.

/// Evaluates parsed expression trees against JSON attribute data.
/// Returns FilterValue (a struct) to avoid boxing allocations on every evaluation.
/// </summary>
internal static class VectorFilterEvaluator
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As a general note, this feels overly complicated.

We're taking a filter that we've parsed and a whole JSON document. But the filter only applies over top level elements of that document... so really, this is operating over the filter and a dictionary (a dictionary that contains no other dictionaries at that).

I've noted elsewhere we should remove most of these allocations - a natural-ish approach would be to a filter, the attribute span, and a collection of (length, offset) pairs to top level attributes.

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.

6 participants