Skip to content

[Redis 8.10] Add FT.AGGREGATE REDUCE COLLECT support with complex-value FieldValue - #3878

Open
uglide wants to merge 26 commits into
rework-codecs-for-redis-search-modulefrom
feat/ft-aggregate-collect-reducer-v2
Open

[Redis 8.10] Add FT.AGGREGATE REDUCE COLLECT support with complex-value FieldValue#3878
uglide wants to merge 26 commits into
rework-codecs-for-redis-search-modulefrom
feat/ft-aggregate-collect-reducer-v2

Conversation

@uglide

@uglide uglide commented Jul 31, 2026

Copy link
Copy Markdown
Collaborator

Adds the experimental REDUCE COLLECT reducer for FT.AGGREGATE, which gathers per-row field projections within each GROUPBY group. To carry its nested column values, FieldValue is extended from a scalar-only wrapper into a tagged union (Kind: SCALAR, ARRAY, MAP, NULL), and SearchReplyParser decodes nested reply structures recursively instead of failing on the ByteBuffer cast.

Key Decisions & Assumptions

  • Stacked on fix: rework search API generics to replace unnecessary type params with String #3716; the FieldValue extension assumes that reply model and should be rebased if it changes.
  • Parsing keeps raw protocol shapes (a RESP2 collected entry stays ARRAY, RESP3 MAP); normalization happens at read time via a lenient asMap() that interprets a flat even-length array as key/value pairs. Callers read collected columns uniformly as asList() → per-entry asMap() on both protocols. Any even-length scalar array (for example a TOLIST column) is accepted by this view; documented as caller responsibility.
  • COLLECT stays in AggregateArgs (the aggregateutils package serves FT.HYBRID only).

Behavioral / Conceptual Changes

  • Nested reply values (COLLECT, TOLIST) now parse into FieldValue arrays/maps; previously the parser failed on them. Scalar columns decode byte-identically to before.
  • Accessing a FieldValue through the wrong kind (for example asString() on an array) throws IllegalStateException; an odd-length pair array is rejected rather than silently truncated.
  • The builder validates COLLECT usage: FIELDS * and explicit fields are mutually exclusive, and a reducer without fields fails at encode time.

Testing

New FieldValueUnitTests cover kind discrimination, the pair-normalizing asMap(), immutability, and structural equality. SearchReplyCollectParserUnitTests verify RESP2/RESP3 collected-column parsing produces equal normalized entries, null preservation inside entries, and TOLIST-style arrays. Command-builder tests assert the exact REDUCE COLLECT wire format; an integration test runs a full COLLECT aggregation, self-skipping on servers without search-enable-unstable-features.

Notes

  • COLLECT is gated behind search-enable-unstable-features; both the server feature and this API are marked @Experimental.
  • HybridReplyParser still decodes field values as scalars only; FT.HYBRID cannot emit COLLECT today, so this is a known limitation, not a regression.

Note

Cursor Bugbot is generating a summary for commit 03a3031. Configure here.

viktoriya-kutsarova and others added 26 commits July 22, 2026 16:24
…pdate all affected files.

Add additional unit and integration tests for the WITHSCORE param.
@uglide uglide changed the title [Redis 8.10]Add support ft aggregate collect reducer [Redis 8.10] Add FT.AGGREGATE REDUCE COLLECT support with complex-value FieldValue Jul 31, 2026

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 03a3031e49

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

* @return {@code this} {@link SugAddArgs}.
*/
public SugAddArgs<K, V> payload(V payload) {
public SugAddArgs payload(String payload) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve binary suggestion payloads

When a connection uses ByteArrayCodec or another non-UTF-8 value codec, changing the payload from V to String and encoding it with args.add(...) makes arbitrary binary FT.SUGADD payloads impossible to send; the corresponding Suggestion.getPayload() change also forces replies through UTF-8 and can corrupt previously round-trippable bytes. Keep payloads codec-backed or expose a binary-safe alternative.

Useful? React with 👍 / 👎.

Comment on lines +169 to +170
} catch (RedisCommandExecutionException e) {
assumeTrue(false, "FT.AGGREGATE REDUCE COLLECT not supported by this Redis Search build: " + e.getMessage());

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Let unexpected COLLECT failures fail the test

On a Redis build that supports COLLECT, any server-side rejection—including malformed argument counts, invalid syntax, or a regression in this builder—is converted into an aborted assumption, so the new integration test reports success without exercising the feature. Restrict the skip to the specific unsupported-feature response and rethrow other command errors.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

for (Map<String, String> result : reply.getResults()) {
for (HybridReply.HybridResult<String> hybridResult : reply.getResults()) {
Map<String, FieldValue> result = hybridResult.getFields();
if ("apple".equals(result.get("brand"))) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Compare the brand's scalar value

After result changed to Map<String, FieldValue>, result.get("brand") is a FieldValue, so neither String comparison can ever match and all AVG/MIN/MAX assertions are skipped even when the returned reducer values are wrong. Compare result.get("brand").asString() and assert that the expected groups were found.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

*
* @author Viktoriya Kutsarova
*/
class AggregateArgsTest {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Rename the new unit tests for Surefire

AggregateArgsTest and the newly added HybridReplyTest are pure unit tests, but pom.xml excludes **/*Test from Surefire and routes that suffix to Failsafe, whose integration phase is disabled by the normal mvn test workflow. Rename both classes to end in UnitTests so these assertions run in the unit suite.

AGENTS.md reference: AGENTS.md:L131-L134

Useful? React with 👍 / 👎.

* @param value the raw field value exactly as returned by the server. Must not be {@code null}.
* @return a {@link Kind#SCALAR} {@link FieldValue} view over the given bytes
*/
public static FieldValue of(byte[] value) {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add @SInCE to every new FieldValue API

FieldValue.of(byte[]) is a newly introduced public method but lacks the required @since 7.7 tag; the same omission occurs on other new public members such as asBytes(), asString(), and isNull(). Add the release tag to each new public element.

AGENTS.md reference: AGENTS.md:L145-L146

Useful? React with 👍 / 👎.

CommandArgs<K, V> args = new CommandArgs<>(codec).add(index).add(fieldName);

return createCommand(FT_TAGVALS, new ValueListOutput<>(codec), args);
return createCommand(FT_TAGVALS, new StringListOutput<>(codec), args);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Decode FT.TAGVALS entries through the value codec

When a Tag field was written through a non-identity value codec, FT.TAGVALS returns the encoded field bytes; replacing ValueListOutput with StringListOutput interprets those bytes directly as UTF-8 instead of applying codec.decodeValue, so serialized or binary tag values no longer round-trip and may be corrupted. Keep the codec-backed List<V> result or expose a raw binary-safe representation.

Useful? React with 👍 / 👎.

@uglide
uglide changed the base branch from main to rework-codecs-for-redis-search-module August 19, 2026 11:50
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.

3 participants