Add post-filter support for VSIM vector search results#1570
Add post-filter support for VSIM vector search results#1570
Conversation
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.
There was a problem hiding this comment.
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
VectorManagerfor 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.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
* 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>
|
CAn you link the specification of expression syntax you are implementing here |
|
@microsoft-github-policy-service agree company="Microsoft" |
|
kevin-montrose
left a comment
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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++) |
There was a problem hiding this comment.
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)] |
There was a problem hiding this comment.
nit: this is unnecessary
| /// </summary> | ||
| internal static class VectorFilterParser | ||
| { | ||
| public static Expr ParseExpression(List<Token> tokens, int start, out int end) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
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:
Supported syntax documented at Vector Filter Expressions (
VSIM ... FILTER)website/docs/dev/vector-sets.md
VSIMsupportsFILTER <expression>for attribute-based post filtering.VSIMquery source can be eitherELE <element-id>orVALUES <dimensions> <v1> ... <vN>Examples
Expression syntax
+,-,*,/,%,**==,!=,>,<,>=,<=and,or,not(also&&,||,!)in()Field access uses dot notation (for example,
.year,.rating,.genre).Supported values
true/false, evaluated as1/0)inwhen the right side is an attribute array)Operator precedence (high to low)
not,!, unary-)**, right-associative)*,/,%)+,-)in)>,<,>=,<=)==,!=)and,&&)or,||)Notes
and,or,not,in,true,false).director in ["a","b"]) are not currently supported