Added support for Redis Query Engine#1356
Conversation
Implement modular Search architecture with complete feature parity to redis-py: Core Components: - Schema and field definitions (TextField, NumericField, TagField, GeoField, VectorField, GeoShapeField) - Query builder with fluent API and advanced query syntax - Index management and operations (create, drop, alter, info) - Aggregation framework with reducers and grouping - Hybrid search combining text and vector queries - Result parsing and formatting Search Features: - Full-text search with stemming, phonetic matching, and stop words - Vector similarity search supporting FLAT, HNSW, and SVS-VAMANA algorithms - Geospatial search with radius and polygon queries - Numeric and tag filtering - Aggregations with grouping, sorting, applying, and reducing - Hybrid search with score combination (RRF, linear) - Auto-complete/suggestions with fuzzy matching - Spell checking and synonym management - Query profiling and explain Field Types: - TextField: full-text search with weights, phonetic matching, withsuffixtrie - NumericField: range queries with sortable option - TagField: exact-match filtering with case sensitivity and separators - GeoField: geospatial queries with radius search - VectorField: vector similarity with multiple algorithms and distance metrics - GeoShapeField: polygon-based geospatial queries Advanced Features: - IndexDefinition for fine-grained index control - Query parameters for dynamic queries - Multiple dialect support (1, 2, 3, 4) - Cursor-based pagination for large result sets - Score explanations and custom scorers - Highlighting and summarization Testing: - Add comprehensive test suite with 44 tests across 8 test files - Test coverage for all search features and edge cases - Hybrid search tests with vector + text queries - Aggregation tests with complex pipelines - Vector similarity tests with multiple algorithms Documentation: - Add 6 example files demonstrating all features: - search_quickstart.rb: basic search operations - search_ft_queries.rb: advanced query syntax - search_aggregations.rb: aggregation pipelines - search_geo.rb: geospatial queries - search_range.rb: numeric range queries - search_vector_similarity.rb: vector search - search_with_hashes.rb: search with Redis hashes
🛡️ Jit Security Scan Results✅ No security findings were detected in this PR
Security scan by Jit
|
There was a problem hiding this comment.
Pull request overview
This PR adds first-class Redis Query Engine (RediSearch FT.*) support to redis-rb, following the existing “module commands + higher-level object layer” pattern used for RedisJSON. It introduces a Redis::Commands::Search command surface, builder/DSL objects for schemas and queries, and protocol-normalizing reply parsing (RESP2/RESP3), plus tests and runnable examples.
Changes:
- Add
Redis::Commands::Searchwith ~30ft_*command methods and a higher-levelIndexAPI (schema/query/aggregation/hybrid builders). - Add
ResultParser+ typed result objects to reshape RESP2/RESP3 replies into consistent Ruby objects. - Add documentation (
specs/query-engine.md), tests (online lint suite runner + offline unit tests), examples, and changelog entry.
Reviewed changes
Copilot reviewed 27 out of 27 changed files in this pull request and generated 13 comments.
Show a summary per file
| File | Description |
|---|---|
| test/modules/search_offline_test.rb | Adds offline unit tests for builders and reply reshaping. |
| test/modules/commands_on_search_test.rb | Hooks the shared Lint::Search suite into module-capable server tests. |
| specs/query-engine.md | Documents architecture/layers and usage guidance for builders vs raw ft_*. |
| lib/redis/commands/modules/search/schema.rb | Implements schema DSL (Schema, SchemaDefinition, vector-field DSL). |
| lib/redis/commands/modules/search/result.rb | Adds typed result objects and RESP2/RESP3 reply reshaping (ResultParser). |
| lib/redis/commands/modules/search/query.rb | Implements fluent query builder and predicate DSL. |
| lib/redis/commands/modules/search/miscellaneous.rb | Implements ft_* command methods and reply reshaping hooks. |
| lib/redis/commands/modules/search/index.rb | Adds high-level Index wrapper API around schema/prefix/search/aggregate/etc. |
| lib/redis/commands/modules/search/index_definition.rb | Adds IndexDefinition and IndexType for FT.CREATE “definition” clause. |
| lib/redis/commands/modules/search/hybrid.rb | Adds FT.HYBRID builders and post-processing/cursor helpers. |
| lib/redis/commands/modules/search/field.rb | Adds field classes for schema rendering and query predicate helpers. |
| lib/redis/commands/modules/search/dialect.rb | Introduces DEFAULT_DIALECT = 2. |
| lib/redis/commands/modules/search/aggregation.rb | Adds AggregateRequest, reducers, sort wrappers, and cursor helper. |
| lib/redis/commands/modules/search.rb | Loads all Search module components. |
| lib/redis/commands.rb | Wires Search into the top-level Redis::Commands mixin. |
| examples/search/search_with_json.rb | Example using Search with RedisJSON documents. |
| examples/search/search_with_hashes.rb | Example using Search with HASH documents and the Index API. |
| examples/search/search_vector_similarity.rb | Example demonstrating vector similarity search patterns and KNN usage. |
| examples/search/search_raw_ft_commands.rb | Example showing direct use of raw ft_* methods. |
| examples/search/search_range.rb | Example of numeric range querying patterns. |
| examples/search/search_quickstart.rb | Quickstart example for basic indexing and querying. |
| examples/search/search_hybrid.rb | Example of FT.HYBRID via builder objects and Index API. |
| examples/search/search_geo.rb | Example of geo/geoshape queries and dialect usage. |
| examples/search/search_ft_queries.rb | Example suite showing various FT.SEARCH query patterns. |
| examples/search/search_aggregations.rb | Example suite showing aggregation pipelines and reducers. |
| CHANGELOG.md | Notes the new Query Engine feature in the Unreleased changelog. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 90ca351. Configure here.
| @index_type = index_type | ||
| # Array() makes the prefix handling nil-safe (nil -> []) and wraps a lone String prefix | ||
| # into a one-element list, so PREFIX always emits the correct count. | ||
| @prefixes = Array(prefix) |
There was a problem hiding this comment.
In case a list with prefixes is provided, the call to Arrays(prefix) will return the same list instance, and if the caller later changes the values in the original prefix instance - the changes will affect the current object as well. Maybe it worths duplicating the input list to ensure that the content of the IndexDefinition won't change due to some external object update.
| # @return [Integer] the cursor id | ||
| # @return [Integer] the +MAXIDLE+ in milliseconds (0 to omit) | ||
| # @return [Integer] the +COUNT+ (batch size, 0 to omit) | ||
| attr_accessor :cid, :max_idle, :count |
There was a problem hiding this comment.
I think the max_idle should be removed from this class. According to the official docs, max_idle is not supported for ft.cursor-read, and this object seems to be used only for this command (the max_idle for ft.aggregate is specified above).
| # | ||
| # @param [String, Symbol] name the document attribute the field indexes | ||
| # @param [String] coord_system the coordinate system, {FLAT} or {SPHERICAL} | ||
| def initialize(name, coord_system = FLAT, **options) |
There was a problem hiding this comment.
The default server value is SPHERICAL. By providing here FLAT as default coord_system we never allow the default server behaviour to be used.
| end | ||
|
|
||
| begin | ||
| json? ? @redis.json_set(key, "$", fields) : @redis.hset(key, fields) |
There was a problem hiding this comment.
add for JSON indexes currently writes the provided kwargs directly at the JSON root via json_set(key, "$", fields).
That only works when schema paths are root-level ($.name AS name), but not for nested paths like $.user.name AS name.
index.add("1", name: "Ann") stores {"name":"Ann"}, while the index expects {"user":{"name":"Ann"}}.
We should either document/test that JSON add requires the full JSON document shape, or reject alias-shaped writes for nested JSON paths with a clear error.
| def wait_for_index(index_name, timeout = 5.0) | ||
| deadline = now + timeout | ||
| loop do | ||
| info = r.ft_info(index_name) |
There was a problem hiding this comment.
You should have some error handling around this command, but still inside the loop - it will raise an error if the index is not yet existing and will basically silently just exit and won't wait.
| r.hset("doc1", embedding: [0.1, 0.9, 0.2, 0.8].pack("f*"), tag: "a") | ||
| r.hset("doc2", embedding: [0.2, 0.8, 0.3, 0.7].pack("f*"), tag: "b") | ||
| r.hset("doc3", embedding: [0.8, 0.2, 0.7, 0.3].pack("f*"), tag: "c") | ||
|
|
There was a problem hiding this comment.
You can include wait_for_index() here as well.
|
|
||
| r.hset("doc1", title: "foo", embedding: [0.1, 0.9].pack("f*")) | ||
| r.hset("doc2", title: "bar", embedding: [0.8, 0.2].pack("f*")) | ||
|
|
| @return_fields_decode ||= {} | ||
|
|
||
| @return_fields << field | ||
| @return_fields_decode[field] = decode_field |
There was a problem hiding this comment.
When as_field is used, Redis returns the value under the alias, but decode_fields is keyed by the original field/path here. For return_field("$.user", as_field: "user"), the parser sees "user" and won’t decode because the map contains "$.user". This should key decoding by as_field || field.

Summary
This PR adds first-class support for the
Redis Query Engineand aligns it with the module-command structure established by the JSON module. It started from an earlier standalone RQE implementation and was adapted to fit the currentlib/redis/commands/modules/**layout, then extended with reply reshaping, protocol compatibility, broad test coverage, and runnable examples.Kudos to @bsbodden for the initial RQE implementation
All FT.* commands are available on any Redis (and, by inheritance, Redis::Cluster) instance. The change is purely additive — no existing behavior changes.
What's included
30 ft_* command methods (lib/redis/commands/modules/search/miscellaneous.rb) covering:
ft_create, ft_alter, ft_dropindex, ft_infoft_search, ft_aggregate, ft_cursor_read/ft_cursor_del, ft_explain, ft_profileft_hybrid_searchft_sugadd/ft_sugget/ft_suglen/ft_sugdelA high-level builder/object layer (the recommended entry point):
#add/#search/#aggregate/#hybrid_search/#info/#drop/…AggregateRequest + Reducers + Asc/Desc + CursorHybridSearchQuery, HybridVsimQuery, HybridQuery, CombineResultsMethod, HybridPostProcessingConfig, HybridCursorQueryTyped, protocol-independent result objects (result.rb):
SearchResult/Document, AggregateResult, and HybridResult, produced by aResultParserthat normalizes both RESP2 (flat arrays) and RESP3 (native maps) to the same Ruby objects.FT.INFO/FT.CONFIG GET/FT.SYNDUMP/FT.SPELLCHECKreturn plain Hashes.Both styles interoperate — you can call a raw ft_* method with a hand-built query string and still get a reshaped result back.
Layering
Index (high-level) → Builders (Schema/Query/AggregateRequest/Hybrid*) → ft_* commands → ResultParser (RESP2/RESP3 → objects).See
specs/query-engine.mdfor the full design and an abstractions-vs-raw-commands guide.Behavior notes
Query, AggregateRequest, and the rawft_search/ft_aggregatestring forms — overridable per query.Redis::Distributed: not supported (FT.* is index-scoped, not key-shardable) — methods are simply absent.Redis::Clusterinherits the commands via the Commands mixin; the module test suite exercises standalone only (consistent with the JSON module).Examples & docs
examples/search/— 10 runnable examples, Index-API-first (quickstart, ft_queries, range, aggregations, geo, vector_similarity, with_hashes, with_json, hybrid), plus search_raw_ft_commands.rb showing the low-level alternative.specs/query-engine.md— architecture + usage guide.Note
Medium Risk
Large additive API surface and reply parsing across RESP2/RESP3; incorrect parsing or command wiring could break search workloads, but existing non-FT behavior is unchanged and Distributed is explicitly excluded.
Overview
Adds first-class Redis Query Engine (RediSearch
FT.*) support onRedisandRedis::Cluster, mirroring the JSON module layout underlib/redis/commands/modules/search/.Callers get ~30
ft_*methods (create/search/aggregate/hybrid, suggestions, synonyms, dictionaries, aliases, spellcheck, config, cursors) plus a high-levelIndexAPI viacreate_index: schema/field builders, fluentQuerywith a predicate DSL,AggregateRequest/Reducers, and FT.HYBRID builders (lexical + vector fusion). Replies are normalized byResultParserintoSearchResult/Document,AggregateResult, andHybridResultfor both RESP2 and RESP3; dialect 2 is the default on search/aggregate paths.CHANGELOG documents the feature (not on
Redis::Distributed). CI adds an unstable Redis image to the test matrix.examples/search/adds runnable quickstart, geo, vector, hybrid, aggregations, and raw-ft_*samples.Reviewed by Cursor Bugbot for commit 5663b85. Bugbot is set up for automated code reviews on this repo. Configure here.