Module: Redis::Commands::Search
Overview
::Redis Query Engine (RediSearch, the FT.* command family). See
../../query-engine.html for the layered design and an abstractions-vs-raw-commands guide.
Replies are reshaped by ResultParser into SearchResult /
Document (for FT.SEARCH), AggregateResult (for FT.AGGREGATE /
FT.CURSOR READ), or plain Hashes/Arrays. Reshaping is identical under RESP2 and RESP3.
Constant Summary
-
DEFAULT_DIALECT =
# File 'lib/redis/commands/modules/search/dialect.rb', line 11
Default query dialect used for FT.SEARCH and FT.AGGREGATE
DEFAULT_DIALECT. Dialect 2 is the recommended baseline: it supports modern query syntax such as vector (KNN) and geoshape predicates, whereas the server's built-in default is dialect 1. Override per query via Query#dialect / AggregateRequest#dialect or thedialect:option toft_search+/+ft_aggregate.2
Instance Method Summary
-
#create_index(name, schema, storage_type: "hash", prefix: nil, stopwords: nil, max_text_fields: false, skip_initial_scan: false, definition: nil, **options) ⇒ Search::Index
Create an index and return a high-level
Indexbound to this client. -
#ft_aggregate(index_name, query, *args) ⇒ Search::AggregateResult
Run an aggregation pipeline, or read the next batch of a cursor.
-
#ft_aliasadd(alias_name, index_name) ⇒ String
Add an alias for an index.
-
#ft_aliasdel(alias_name) ⇒ String
Remove an index alias.
-
#ft_aliaslist(index_name) ⇒ Array<String>
List all aliases associated with an index (Redis 8.10+).
-
#ft_aliasupdate(alias_name, index_name) ⇒ String
Repoint an existing alias to a different index (or create it if absent).
-
#ft_alter(index_name, field_or_args) ⇒ String
Add a field to an existing index's schema (+FT.ALTER ...
-
#ft_config_get(option) ⇒ Hash
Get a runtime
QueryEngine configuration option. -
#ft_config_set(option, value) ⇒ String
Set a runtime
QueryEngine configuration option. -
#ft_create(index_name, schema, storage_type = nil, prefix: nil, stopwords: nil, max_text_fields: false, skip_initial_scan: false, definition: nil, temporary: nil, no_term_offsets: false, no_highlight: false, no_field_flags: false, no_term_frequencies: false, **_options) ⇒ String
Create a search index over HASH or JSON keys.
-
#ft_cursor_del(index_name, cursor_id) ⇒ String
Discard an aggregation cursor.
-
#ft_cursor_read(index_name, cursor_id) ⇒ Search::AggregateResult
Read the next batch of results from an aggregation cursor.
-
#ft_dictadd(dict_name, *terms) ⇒ Integer
Add terms to a custom dictionary.
-
#ft_dictdel(dict_name, *terms) ⇒ Integer
Remove terms from a custom dictionary.
-
#ft_dictdump(dict_name) ⇒ Array<String>
Dump all terms in a custom dictionary.
-
#ft_dropindex(index_name, delete_documents: false) ⇒ String
Drop an index.
-
#ft_explain(index_name, query) ⇒ String
Return the execution plan for a query without running it.
-
#ft_hybrid_search(index_name, query:, combine_method: nil, post_processing: nil, params_substitution: nil, timeout: nil, cursor: nil, **_options) ⇒ Search::HybridResult
Run a hybrid (lexical + vector) search.
-
#ft_info(index_name) ⇒ Hash
Return information and statistics about an index.
-
#ft_profile(index_name, *args) ⇒ Array
Profile the execution of a
SEARCHorAGGREGATEquery (timing/heuristics). -
#ft_search(index_name, query, **options) ⇒ Search::SearchResult
Searchan index. -
#ft_spellcheck(index_name, query, distance: nil, include: nil, exclude: nil) ⇒ Hash{String=>Array<Hash>}
Perform spelling correction over a query against an index.
-
#ft_sugadd(key, string, score, options = {}) ⇒ Integer
Add a suggestion string to an auto-complete dictionary.
-
#ft_sugdel(key, string) ⇒ Integer
Delete a string from a suggestion dictionary.
-
#ft_sugget(key, prefix, options = {}) ⇒ Array<String>
Get auto-complete suggestions for a prefix.
-
#ft_suglen(key) ⇒ Integer
Get the number of entries in a suggestion dictionary.
-
#ft_syndump(index_name) ⇒ Hash{String=>Array<String>}
Dump the synonym groups of an index.
-
#ft_synupdate(index_name, group_id, *terms, skip_initial_scan: false) ⇒ String
Add or update a synonym group on an index.
-
#ft_tagvals(index_name, field_name) ⇒ Array<String>
List the distinct values of a TAG field.
Instance Method Details
#create_index(name, schema, storage_type: "hash", prefix: nil, stopwords: nil, max_text_fields: false, skip_initial_scan: false, definition: nil, **options) ⇒ Search::Index
Create an index and return a high-level Search::Index bound to this client.
Unlike #ft_create (which returns "OK"), this returns a stateful Search::Index that
remembers the schema and prefix and exposes +#add+/+#search+/+#aggregate+/etc.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 112
def create_index( name, schema, storage_type: "hash", prefix: nil, stopwords: nil, max_text_fields: false, skip_initial_scan: false, definition: nil, ** ) raise Redis::CommandError, "schema must be a Schema object" unless schema.is_a?(Schema) begin Index.create( self, name, schema, storage_type, prefix: prefix, stopwords: stopwords, max_text_fields: max_text_fields, skip_initial_scan: skip_initial_scan, definition: definition, ** ) rescue ArgumentError => e raise Redis::CommandError, e. end end
#ft_aggregate(index_name, query, *args) ⇒ Search::AggregateResult
Run an aggregation pipeline, or read the next batch of a cursor.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 316
def ft_aggregate(index_name, query, *args) command = case query when AggregateRequest ["FT.AGGREGATE", index_name] + query.to_redis_args when Cursor ["FT.CURSOR", "READ", index_name] + query.build_args else # Raw query string: default to DIALECT 2 (matching redis-py) unless one was passed. base = ["FT.AGGREGATE", index_name, query, *args] base += ["DIALECT", DEFAULT_DIALECT] unless args.flatten.map(&:to_s).include?("DIALECT") base end send_command(command) { |reply| ResultParser.aggregate(reply) } end
#ft_aliasadd(alias_name, index_name) ⇒ String
Add an alias for an index.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 533
def ft_aliasadd(alias_name, index_name) send_command(["FT.ALIASADD", alias_name, index_name]) end
#ft_aliasdel(alias_name) ⇒ String
Remove an index alias.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 550
def ft_aliasdel(alias_name) send_command(["FT.ALIASDEL", alias_name]) end
#ft_aliaslist(index_name) ⇒ Array<String>
List all aliases associated with an index (Redis 8.10+).
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 559
def ft_aliaslist(index_name) send_command(["FT.ALIASLIST", index_name]) end
#ft_aliasupdate(alias_name, index_name) ⇒ String
Repoint an existing alias to a different index (or create it if absent).
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 542
def ft_aliasupdate(alias_name, index_name) send_command(["FT.ALIASUPDATE", alias_name, index_name]) end
#ft_alter(index_name, field_or_args) ⇒ String
Add a field to an existing index's schema (+FT.ALTER ... SCHEMA ADD+).
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 349
def ft_alter(index_name, field_or_args) args = [index_name, "SCHEMA", "ADD"] if field_or_args.respond_to?(:to_args) args += field_or_args.to_args elsif field_or_args.is_a?(Array) args += field_or_args else raise ArgumentError, "field_or_args must be a Field object or an array" end send_command([:"FT.ALTER", *args]) end
#ft_config_get(option) ⇒ Hash
Get a runtime Search::Query Engine configuration option.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 602
def ft_config_get(option) send_command(["FT.CONFIG", "GET", option]) { |reply| ResultParser.config_get(reply) } end
#ft_config_set(option, value) ⇒ String
Set a runtime Search::Query Engine configuration option.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 594
def ft_config_set(option, value) send_command(["FT.CONFIG", "SET", option, value]) end
#ft_create(index_name, schema, storage_type = nil, prefix: nil, stopwords: nil, max_text_fields: false, skip_initial_scan: false, definition: nil, temporary: nil, no_term_offsets: false, no_highlight: false, no_field_flags: false, no_term_frequencies: false, **_options) ⇒ String
Create a search index over HASH or JSON keys.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 37
def ft_create( index_name, schema, storage_type = nil, prefix: nil, stopwords: nil, max_text_fields: false, skip_initial_scan: false, definition: nil, temporary: nil, no_term_offsets: false, no_highlight: false, no_field_flags: false, no_term_frequencies: false, ** ) raise ArgumentError, "schema must be a Schema object" unless schema.is_a?(Schema) args = [index_name] # Use IndexDefinition if provided, otherwise use legacy parameters if definition # The definition supplies the ON/PREFIX/FILTER clause. When it doesn't declare an index # type, fall back to the storage_type keyword so e.g. a JSON index isn't silently created # as HASH. Normalize to the uppercase HASH/JSON form some Query Engine versions require. args += ["ON", storage_type.to_s.upcase] if definition.index_type.nil? && storage_type args += definition.args else # Normalize the ON token to HASH/JSON; a lowercase value is rejected by some # Query Engine versions (and IndexType/the docs use the uppercase form). args += ["ON", storage_type.to_s.upcase] if storage_type args += ["PREFIX", 1, "#{prefix}:"] if prefix end args << "MAXTEXTFIELDS" if max_text_fields if temporary args << "TEMPORARY" args << temporary end args << "NOOFFSETS" if no_term_offsets args << "NOHL" if no_highlight args << "NOFIELDS" if no_field_flags args << "NOFREQS" if no_term_frequencies args << "SKIPINITIALSCAN" if skip_initial_scan # Stopwords if stopwords args += ['STOPWORDS', stopwords.size] stopwords.each do |stopword| args << stopword end end # Schema Fields args += ["SCHEMA"] schema.fields.each do |field| args += field.to_args end send_command([:"FT.CREATE", *args]) end
#ft_cursor_del(index_name, cursor_id) ⇒ String
Discard an aggregation cursor.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 376
def ft_cursor_del(index_name, cursor_id) send_command(["FT.CURSOR", "DEL", index_name, cursor_id]) end
#ft_cursor_read(index_name, cursor_id) ⇒ Search::AggregateResult
Read the next batch of results from an aggregation cursor.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 367
def ft_cursor_read(index_name, cursor_id) send_command(["FT.CURSOR", "READ", index_name, cursor_id]) { |reply| ResultParser.aggregate(reply) } end
#ft_dictadd(dict_name, *terms) ⇒ Integer
Add terms to a custom dictionary.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 568
def ft_dictadd(dict_name, *terms) send_command(["FT.DICTADD", dict_name] + terms) end
#ft_dictdel(dict_name, *terms) ⇒ Integer
Remove terms from a custom dictionary.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 577
def ft_dictdel(dict_name, *terms) send_command(["FT.DICTDEL", dict_name] + terms) end
#ft_dictdump(dict_name) ⇒ Array<String>
Dump all terms in a custom dictionary.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 585
def ft_dictdump(dict_name) send_command(["FT.DICTDUMP", dict_name]) end
#ft_dropindex(index_name, delete_documents: false) ⇒ String
Drop an index.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 297
def ft_dropindex(index_name, delete_documents: false) args = ["FT.DROPINDEX", index_name] args << 'DD' if delete_documents send_command(args) end
#ft_explain(index_name, query) ⇒ String
Return the execution plan for a query without running it.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 338
def ft_explain(index_name, query) send_command(["FT.EXPLAIN", index_name, query]) end
#ft_hybrid_search(index_name, query:, combine_method: nil, post_processing: nil, params_substitution: nil, timeout: nil, cursor: nil, **_options) ⇒ Search::HybridResult
Run a hybrid (lexical + vector) search.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 400
def ft_hybrid_search( index_name, query:, combine_method: nil, post_processing: nil, params_substitution: nil, timeout: nil, cursor: nil, ** ) args = ["FT.HYBRID", index_name] args.concat(query.args) args.concat(combine_method.args) if combine_method args.concat(post_processing.build_args) if post_processing if params_substitution args << "PARAMS" << params_substitution.size * 2 params_substitution.each do |key, value| args << key.to_s << value end end args.concat(["TIMEOUT", timeout]) if timeout args.concat(cursor.build_args) if cursor # NOTE: unlike FT.SEARCH/FT.AGGREGATE, FT.HYBRID does NOT accept a DIALECT token (the # server rejects it: "DIALECT is not supported in FT.HYBRID or any of its subqueries"). # Its legs use the server's search-default-dialect config, so do not append DEFAULT_DIALECT. send_command(args) { |reply| ResultParser.hybrid(reply) } end
#ft_info(index_name) ⇒ Hash
Return information and statistics about an index.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 288
def ft_info(index_name) send_command(["FT.INFO", index_name]) { |reply| ResultParser.hashify_info(reply) } end
#ft_profile(index_name, *args) ⇒ Array
Profile the execution of a SEARCH or AGGREGATE query (timing/heuristics).
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 385
def ft_profile(index_name, *args) send_command(["FT.PROFILE", index_name] + args) end
#ft_search(index_name, query, **options) ⇒ Search::SearchResult
Search an index.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 173
def ft_search(index_name, query, **) args = [index_name, query] args << "NOCONTENT" if [:no_content] args << "VERBATIM" if [:verbatim] args << "NOSTOPWORDS" if [:no_stopwords] # EXPLAINSCORE requires WITHSCORES, so emit WITHSCORES whenever an explanation is asked for. args << "WITHSCORES" if [:with_scores] || [:explain_score] args << "WITHPAYLOADS" if [:with_payloads] args << "SCORER" << [:scorer] if [:scorer] args << "EXPLAINSCORE" if [:explain_score] args << "LANGUAGE" << [:language] if [:language] args << "SLOP" << [:slop] if [:slop] args << "INORDER" if [:in_order] args << "TIMEOUT" << [:timeout] if [:timeout] args << "EXPANDER" << [:] if [:] if [:limit_fields] && ![:limit_fields].empty? args << "INFIELDS" << [:limit_fields].size args.concat([:limit_fields].map(&:to_s)) end args << "LIMIT" << [:limit][0] << [:limit][1] if [:limit] # Handle both :sortby (array format) and :sort_by (convenience format) if [:sortby] args << "SORTBY" << [:sortby][0] << [:sortby][1] elsif [:sort_by] # Default to ASC when :asc is omitted (matching Index#search, Query#sort_by and the # server's own SORTBY default); only an explicit asc: false sorts descending. direction = [:asc] == false ? 'DESC' : 'ASC' args << "SORTBY" << [:sort_by] << direction end [:filter]&.each do |field, min, max| args << "FILTER" << field << min << max end [:geo_filter]&.each do |field, lon, lat, radius, unit| args << "GEOFILTER" << field << lon << lat << radius << unit end if [:limit_ids] && ![:limit_ids].empty? args << "INKEYS" << [:limit_ids].size args.concat([:limit_ids]) end if [:return] && ![:return].empty? args << "RETURN" << [:return].size args.concat([:return]) end if [:summarize] args << "SUMMARIZE" if [:summarize][:fields]&.any? args << "FIELDS" << [:summarize][:fields].size args.concat([:summarize][:fields].map(&:to_s)) end args << "FRAGS" << [:summarize][:frags] if [:summarize][:frags] args << "LEN" << [:summarize][:len] if [:summarize][:len] args << "SEPARATOR" << [:summarize][:separator] if [:summarize][:separator] end if [:highlight] args << "HIGHLIGHT" if [:highlight][:fields]&.any? args << "FIELDS" << [:highlight][:fields].size args.concat([:highlight][:fields].map(&:to_s)) end if [:highlight][:] args << "TAGS" << [:highlight][:][0] << [:highlight][:][1] end end if [:params] if [:params].is_a?(Hash) args << "PARAMS" << ([:params].length * 2) [:params].each do |k, v| args << k.to_s << v end else args << "PARAMS" << [:params].length args.concat([:params]) end end # Default to DIALECT 2 (matching redis-py) unless the caller specified one. An explicit # dialect: nil (e.g. flowing from an unset Query option) falls back to the default too, so # it behaves the same as omitting the keyword rather than deferring to the server default. dialect = [:dialect] || DEFAULT_DIALECT args << "DIALECT" << dialect # WITHSCORES is emitted for explain_score too (EXPLAINSCORE requires it), so the RESP2 # parser must expect a score column in the same cases or it mis-reads the reply layout. with_scores = [:with_scores] || [:explain_score] with_payloads = [:with_payloads] no_content = [:no_content] decode_fields = [:decode_fields] || {} send_command(["FT.SEARCH"] + args.flatten.compact) do |reply| ResultParser.search( reply, with_scores: !!with_scores, with_payloads: !!with_payloads, no_content: !!no_content, decode_fields: decode_fields ) end end
#ft_spellcheck(index_name, query, distance: nil, include: nil, exclude: nil) ⇒ Hash{String=>Array<Hash>}
Perform spelling correction over a query against an index.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 487
def ft_spellcheck(index_name, query, distance: nil, include: nil, exclude: nil) args = ["FT.SPELLCHECK", index_name, query] args += ["DISTANCE", distance] if distance args += ["TERMS", "INCLUDE", include] if include args += ["TERMS", "EXCLUDE", exclude] if exclude send_command(args) { |reply| ResultParser.spellcheck(reply) } end
#ft_sugadd(key, string, score, options = {}) ⇒ Integer
Add a suggestion string to an auto-complete dictionary.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 431
def ft_sugadd(key, string, score, = {}) args = ["FT.SUGADD", key, string, score] args << 'INCR' if [:incr] args << 'PAYLOAD' << [:payload] if [:payload] send_command(args) end
#ft_sugdel(key, string) ⇒ Integer
Delete a string from a suggestion dictionary.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 470
def ft_sugdel(key, string) send_command(["FT.SUGDEL", key, string]) end
#ft_sugget(key, prefix, options = {}) ⇒ Array<String>
Get auto-complete suggestions for a prefix.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 448
def ft_sugget(key, prefix, = {}) args = ["FT.SUGGET", key, prefix] args << 'FUZZY' if [:fuzzy] args << 'WITHSCORES' if [:withscores] || [:with_scores] args << 'WITHPAYLOADS' if [:withpayloads] || [:with_payloads] args << 'MAX' << [:max] if [:max] send_command(args) end
#ft_suglen(key) ⇒ Integer
Get the number of entries in a suggestion dictionary.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 461
def ft_suglen(key) send_command(["FT.SUGLEN", key]) end
#ft_syndump(index_name) ⇒ Hash{String=>Array<String>}
Dump the synonym groups of an index.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 514
def ft_syndump(index_name) send_command(["FT.SYNDUMP", index_name]) { |reply| ResultParser.syndump(reply) } end
#ft_synupdate(index_name, group_id, *terms, skip_initial_scan: false) ⇒ String
Add or update a synonym group on an index.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 503
def ft_synupdate(index_name, group_id, *terms, skip_initial_scan: false) args = [index_name, group_id] args << "SKIPINITIALSCAN" if skip_initial_scan args += terms send_command(["FT.SYNUPDATE"] + args) end
#ft_tagvals(index_name, field_name) ⇒ Array<String>
List the distinct values of a TAG field.
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 524
def ft_tagvals(index_name, field_name) send_command(["FT.TAGVALS", index_name, field_name]) end