Module: Redis::Commands::Search

Included in:
Redis::Commands
Defined in:
lib/redis/commands/modules/search/miscellaneous.rb,
lib/redis/commands/modules/search.rb,
lib/redis/commands/modules/search/field.rb,
lib/redis/commands/modules/search/index.rb,
lib/redis/commands/modules/search/query.rb,
lib/redis/commands/modules/search/hybrid.rb,
lib/redis/commands/modules/search/result.rb,
lib/redis/commands/modules/search/schema.rb,
lib/redis/commands/modules/search/dialect.rb,
lib/redis/commands/modules/search/aggregation.rb,
lib/redis/commands/modules/search/index_definition.rb

Overview

Redis Query Engine (RediSearch, the FT.* command family). See query-engine 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.

Defined Under Namespace

Modules: CombinationMethods, IndexType, ResultParser, VectorSearchMethods Classes: AggregateRequest, AggregateResult, Asc, CombineResultsMethod, Cursor, Desc, Document, Field, GeoField, GeoShapeField, HybridCursorQuery, HybridFilter, HybridPostProcessingConfig, HybridQuery, HybridResult, HybridSearchQuery, HybridVsimQuery, Index, IndexDefinition, NumericField, Predicate, PredicateCollection, Query, RangePredicate, Reducers, Schema, SchemaDefinition, SearchResult, SortbyField, TagEqualityPredicate, TagField, TextField, TextMatchPredicate, VectorField, VectorFieldDefinition

Constant Summary collapse

DEFAULT_DIALECT =

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 the dialect: option to +ft_search+/+ft_aggregate+.

2

Instance Method Summary collapse

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 Index bound to this client.

Unlike #ft_create (which returns "OK"), this returns a stateful Index that remembers the schema and prefix and exposes +#add+/+#search+/+#aggregate+/etc.

Examples:

index = redis.create_index("idx", schema, prefix: "doc")
index.add("1", title: "hello")
index.search("hello").total # => 1

Parameters:

  • name (String)

    the index name

  • schema (Search::Schema)

    the field schema

  • storage_type (String) (defaults to: "hash")

    the data type to index (+"hash"+ or "json")

  • prefix (String, nil) (defaults to: nil)

    key prefix for indexed/added documents

  • stopwords (Array<String>, nil) (defaults to: nil)

    custom stopword list

  • max_text_fields (Boolean) (defaults to: false)

    emit MAXTEXTFIELDS

  • skip_initial_scan (Boolean) (defaults to: false)

    emit SKIPINITIALSCAN

  • definition (Search::IndexDefinition, nil) (defaults to: nil)

    prebuilt index definition clause

Returns:

Raises:



112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
# 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, **options
)
  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, **options
    )
  rescue ArgumentError => e
    raise Redis::CommandError, e.message
  end
end

#ft_aggregate(index_name, query, *args) ⇒ Search::AggregateResult

Run an aggregation pipeline, or read the next batch of a cursor.

Examples:

with an AggregateRequest

req = Redis::Commands::Search::AggregateRequest.new("*")
        .group_by("@category", Redis::Commands::Search::Reducers.count.as("n"))
redis.ft_aggregate("idx", req) # => #<AggregateResult ...>

Parameters:

  • index_name (String)

    the index name

  • query (Search::AggregateRequest, Search::Cursor, String)

    an aggregate request, a cursor to read, or a raw query string followed by raw pipeline args

  • args (Array)

    raw pipeline tokens when query is a String (e.g. "GROUPBY", 1, "@x", "REDUCE", "COUNT", 0, "AS", "n")

Returns:



316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
# 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.

Parameters:

  • alias_name (String)

    the alias

  • index_name (String)

    the index the alias points to

Returns:

  • (String)

    "OK"



533
534
535
# 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.

Parameters:

  • alias_name (String)

    the alias

Returns:

  • (String)

    "OK"



550
551
552
# 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+).

Parameters:

  • index_name (String)

    the index name

Returns:

  • (Array<String>)

    the aliases pointing to the index (empty when none)

Raises:



559
560
561
# 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).

Parameters:

  • alias_name (String)

    the alias

  • index_name (String)

    the index the alias should point to

Returns:

  • (String)

    "OK"



542
543
544
# 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+).

Parameters:

  • index_name (String)

    the index name

  • field_or_args (Search::Field, Array)

    a Field (rendered via #to_args) or a raw token array

Returns:

  • (String)

    "OK"

Raises:

  • (ArgumentError)

    if field_or_args is neither a Field nor an Array



349
350
351
352
353
354
355
356
357
358
359
# 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 Query Engine configuration option.

Parameters:

  • option (String)

    the option name, or "*" for all options

Returns:

  • (Hash)

    option name => value



602
603
604
# 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 Query Engine configuration option.

Parameters:

  • option (String)

    the option name (or "*")

  • value (String, Integer)

    the value

Returns:

  • (String)

    "OK"



594
595
596
# 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.

Examples:

schema = Redis::Commands::Search::Schema.build { text_field :title }
redis.ft_create("idx", schema, prefix: "doc")
  # => "OK"

Parameters:

  • index_name (String)

    the index name

  • schema (Search::Schema)

    the field schema; the SCHEMA clause is rendered from it

  • storage_type (String, Symbol, nil) (defaults to: nil)

    the data type to index, e.g. "HASH" or "JSON" (emitted as ON <type>); ignored when definition is given

  • prefix (String, nil) (defaults to: nil)

    index keys whose name starts with "<prefix>:"; ignored when definition is given

  • stopwords (Array<String>, nil) (defaults to: nil)

    a custom stopword list (emits STOPWORDS)

  • max_text_fields (Boolean) (defaults to: false)

    emit MAXTEXTFIELDS

  • skip_initial_scan (Boolean) (defaults to: false)

    emit SKIPINITIALSCAN (do not backfill existing keys)

  • definition (Search::IndexDefinition, nil) (defaults to: nil)

    a prebuilt ON .../PREFIX/FILTER/... clause; when given it takes precedence over +storage_type+/+prefix+

  • temporary (Integer, nil) (defaults to: nil)

    index lifetime in seconds (emits TEMPORARY <seconds>)

  • no_term_offsets (Boolean) (defaults to: false)

    emit NOOFFSETS

  • no_highlight (Boolean) (defaults to: false)

    emit NOHL

  • no_field_flags (Boolean) (defaults to: false)

    emit NOFIELDS

  • no_term_frequencies (Boolean) (defaults to: false)

    emit NOFREQS

Returns:

  • (String)

    "OK"

Raises:

  • (ArgumentError)

    if schema is not a Schema



37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
# 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, **_options
)
  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.

Parameters:

  • index_name (String)

    the index name

  • cursor_id (Integer)

    the cursor id

Returns:

  • (String)

    "OK"



376
377
378
# 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.

Parameters:

  • index_name (String)

    the index name

  • cursor_id (Integer)

    the cursor id returned by a previous WITHCURSOR aggregation

Returns:



367
368
369
# 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.

Parameters:

  • dict_name (String)

    the dictionary name

  • terms (Array<String>)

    the terms to add

Returns:

  • (Integer)

    the number of new terms added



568
569
570
# 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.

Parameters:

  • dict_name (String)

    the dictionary name

  • terms (Array<String>)

    the terms to remove

Returns:

  • (Integer)

    the number of terms removed



577
578
579
# 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.

Parameters:

  • dict_name (String)

    the dictionary name

Returns:

  • (Array<String>)

    the terms in the dictionary



585
586
587
# 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.

Parameters:

  • index_name (String)

    the index name

  • delete_documents (Boolean) (defaults to: false)

    also delete the indexed documents (+DD+)

Returns:

  • (String)

    "OK"



297
298
299
300
301
# 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.

Parameters:

  • index_name (String)

    the index name

  • query (String)

    the query string

Returns:

  • (String)

    a human-readable description of the query plan



338
339
340
# 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.

Parameters:

  • index_name (String)

    the index name

  • query (Search::HybridQuery)

    the combined SEARCH + VSIM query

  • combine_method (Search::CombineResultsMethod, nil) (defaults to: nil)

    the fusion strategy (RRF/LINEAR)

  • post_processing (Search::HybridPostProcessingConfig, nil) (defaults to: nil)

    a post-fusion pipeline

  • params_substitution (Hash, nil) (defaults to: nil)

    query parameter substitutions (+PARAMS+)

  • timeout (Integer, nil) (defaults to: nil)

    query timeout in milliseconds (+TIMEOUT+)

  • cursor (Search::HybridCursorQuery, nil) (defaults to: nil)

    cursor/pagination config (+WITHCURSOR+)

Returns:

  • (Search::HybridResult)

    the fused result rows, total, warnings and execution time (or, for a WITHCURSOR query, the per-leg cursor ids)



400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
# 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, **_options
)
  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.

Examples:

redis.ft_info("idx")["num_docs"] # => 42

Parameters:

  • index_name (String)

    the index name

Returns:

  • (Hash)

    index metadata (e.g. "index_name", "num_docs", "attributes")



288
289
290
# 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).

Parameters:

  • index_name (String)

    the index name

  • args (Array)

    the profile arguments, e.g. "SEARCH", "QUERY", "<query>"

Returns:

  • (Array)

    the raw profile reply (results plus a profiling tree)



385
386
387
# 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.

Examples:

redis.ft_search("idx", "@title:hello", limit: [0, 10], with_scores: true)
  # => #<SearchResult total=1 ...>

raw vector (KNN) query

redis.ft_search("idx", "(*)=>[KNN 2 @vec $v]", params: { v: blob }, dialect: 2)

Parameters:

  • index_name (String)

    the index (or alias) name

  • query (String)

    the query string (use Query+#to_redis_args.first+ to build one)

  • options (Hash)

    search options translated to FT.SEARCH tokens

Options Hash (**options):

  • :no_content (Boolean)

    return ids only (+NOCONTENT+); documents have no fields

  • :verbatim (Boolean)

    disable stemming (+VERBATIM+)

  • :no_stopwords (Boolean)

    do not remove stopwords (+NOSTOPWORDS+)

  • :with_scores (Boolean)

    include the relevance score on each document (+WITHSCORES+)

  • :with_payloads (Boolean)

    include each document's payload (+WITHPAYLOADS+)

  • :scorer (String)

    scoring function name (+SCORER+), e.g. +"BM25"+/+"TFIDF"+

  • :explain_score (Boolean)

    include a score explanation (+EXPLAINSCORE+)

  • :language (String)

    stemming language (+LANGUAGE+)

  • :slop (Integer)

    allowed term reordering distance (+SLOP+)

  • :in_order (Boolean)

    require terms in query order (+INORDER+)

  • :timeout (Integer)

    per-query timeout in milliseconds (+TIMEOUT+)

  • :expander (String)

    custom query expander (+EXPANDER+)

  • :limit_fields (Array<String>)

    restrict full-text matching to these fields (+INFIELDS+)

  • :limit (Array(Integer, Integer))

    [offset, count] paging (+LIMIT+)

  • :sortby (Array(String, String))

    [field, "ASC"|"DESC"] (+SORTBY+)

  • :sort_by (String)

    sort field (convenience; pair with :asc)

  • :asc (Boolean)

    sort ascending when :sort_by is used (default; pass false for descending)

  • :filter (Array<Array>)

    numeric filters, each [field, min, max] (+FILTER+)

  • :geo_filter (Array<Array>)

    geo filters, each [field, lon, lat, radius, unit]

  • :limit_ids (Array<String>)

    restrict to these keys (+INKEYS+)

  • :return (Array<String>)

    only return these fields (+RETURN+)

  • :summarize (Hash)

    SUMMARIZE options (+:fields+, :frags, :len, :separator)

  • :highlight (Hash)

    HIGHLIGHT options (+:fields+, :tags)

  • :params (Hash, Array)

    query parameter substitutions (+PARAMS+)

  • :dialect (Integer)

    query dialect version (+DIALECT+)

  • :decode_fields (Hash{String=>Boolean})

    field name => whether to JSON-decode its value in the returned Document

Returns:



173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 173

def ft_search(index_name, query, **options)
  args = [index_name, query]

  args << "NOCONTENT" if options[:no_content]
  args << "VERBATIM" if options[:verbatim]
  args << "NOSTOPWORDS" if options[:no_stopwords]
  # EXPLAINSCORE requires WITHSCORES, so emit WITHSCORES whenever an explanation is asked for.
  args << "WITHSCORES" if options[:with_scores] || options[:explain_score]
  args << "WITHPAYLOADS" if options[:with_payloads]
  args << "SCORER" << options[:scorer] if options[:scorer]
  args << "EXPLAINSCORE" if options[:explain_score]
  args << "LANGUAGE" << options[:language] if options[:language]
  args << "SLOP" << options[:slop] if options[:slop]
  args << "INORDER" if options[:in_order]
  args << "TIMEOUT" << options[:timeout] if options[:timeout]
  args << "EXPANDER" << options[:expander] if options[:expander]
  if options[:limit_fields] && !options[:limit_fields].empty?
    args << "INFIELDS" << options[:limit_fields].size
    args.concat(options[:limit_fields].map(&:to_s))
  end
  args << "LIMIT" << options[:limit][0] << options[:limit][1] if options[:limit]

  # Handle both :sortby (array format) and :sort_by (convenience format)
  if options[:sortby]
    args << "SORTBY" << options[:sortby][0] << options[:sortby][1]
  elsif options[: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 = options[:asc] == false ? 'DESC' : 'ASC'
    args << "SORTBY" << options[:sort_by] << direction
  end

  options[:filter]&.each do |field, min, max|
    args << "FILTER" << field << min << max
  end

  options[:geo_filter]&.each do |field, lon, lat, radius, unit|
    args << "GEOFILTER" << field << lon << lat << radius << unit
  end

  if options[:limit_ids] && !options[:limit_ids].empty?
    args << "INKEYS" << options[:limit_ids].size
    args.concat(options[:limit_ids])
  end

  if options[:return] && !options[:return].empty?
    args << "RETURN" << options[:return].size
    args.concat(options[:return])
  end

  if options[:summarize]
    args << "SUMMARIZE"
    if options[:summarize][:fields]&.any?
      args << "FIELDS" << options[:summarize][:fields].size
      args.concat(options[:summarize][:fields].map(&:to_s))
    end
    args << "FRAGS" << options[:summarize][:frags] if options[:summarize][:frags]
    args << "LEN" << options[:summarize][:len] if options[:summarize][:len]
    args << "SEPARATOR" << options[:summarize][:separator] if options[:summarize][:separator]
  end

  if options[:highlight]
    args << "HIGHLIGHT"
    if options[:highlight][:fields]&.any?
      args << "FIELDS" << options[:highlight][:fields].size
      args.concat(options[:highlight][:fields].map(&:to_s))
    end
    if options[:highlight][:tags]
      args << "TAGS" << options[:highlight][:tags][0] << options[:highlight][:tags][1]
    end
  end

  if options[:params]
    if options[:params].is_a?(Hash)
      args << "PARAMS" << (options[:params].length * 2)
      options[:params].each do |k, v|
        args << k.to_s << v
      end
    else
      args << "PARAMS" << options[:params].length
      args.concat(options[: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 = options[: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 = options[:with_scores] || options[:explain_score]
  with_payloads = options[:with_payloads]
  no_content = options[:no_content]
  decode_fields = options[: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.

Examples:

redis.ft_spellcheck("idx", "hello wrld")
  # => { "wrld" => [{ "suggestion" => "world", "score" => 0.5 }] }

Parameters:

  • index_name (String)

    the index name

  • query (String)

    the query whose terms are checked

  • distance (Integer, nil) (defaults to: nil)

    maximum Levenshtein distance for suggestions (+DISTANCE+)

  • include (String, nil) (defaults to: nil)

    a custom dictionary to include terms from (+TERMS INCLUDE+)

  • exclude (String, nil) (defaults to: nil)

    a custom dictionary to exclude terms from (+TERMS EXCLUDE+)

Returns:

  • (Hash{String=>Array<Hash>})

    each misspelled term mapped to an array of { "suggestion" => String, "score" => Numeric }



487
488
489
490
491
492
493
494
# 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.

Parameters:

  • key (String)

    the suggestion dictionary key

  • string (String)

    the suggestion text

  • score (Numeric)

    the suggestion weight

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :incr (Boolean)

    increment the existing score instead of replacing it (+INCR+)

  • :payload (String)

    an opaque payload to store with the suggestion (+PAYLOAD+)

Returns:

  • (Integer)

    the current size of the suggestion dictionary



431
432
433
434
435
436
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 431

def ft_sugadd(key, string, score, options = {})
  args = ["FT.SUGADD", key, string, score]
  args << 'INCR' if options[:incr]
  args << 'PAYLOAD' << options[:payload] if options[:payload]
  send_command(args)
end

#ft_sugdel(key, string) ⇒ Integer

Delete a string from a suggestion dictionary.

Parameters:

  • key (String)

    the suggestion dictionary key

  • string (String)

    the suggestion text to delete

Returns:

  • (Integer)

    1 if the suggestion existed and was deleted, 0 otherwise



470
471
472
# 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.

Parameters:

  • key (String)

    the suggestion dictionary key

  • prefix (String)

    the prefix to complete

  • options (Hash) (defaults to: {})

Options Hash (options):

  • :fuzzy (Boolean)

    perform a fuzzy prefix match (+FUZZY+)

  • :with_scores (Boolean)

    also return each suggestion's score (+WITHSCORES+)

  • :with_payloads (Boolean)

    also return each suggestion's payload (+WITHPAYLOADS+)

  • :max (Integer)

    maximum number of suggestions (+MAX+)

Returns:

  • (Array<String>)

    the suggestions, interleaved with scores/payloads when requested



448
449
450
451
452
453
454
455
# File 'lib/redis/commands/modules/search/miscellaneous.rb', line 448

def ft_sugget(key, prefix, options = {})
  args = ["FT.SUGGET", key, prefix]
  args << 'FUZZY' if options[:fuzzy]
  args << 'WITHSCORES' if options[:withscores] || options[:with_scores]
  args << 'WITHPAYLOADS' if options[:withpayloads] || options[:with_payloads]
  args << 'MAX' << options[:max] if options[:max]
  send_command(args)
end

#ft_suglen(key) ⇒ Integer

Get the number of entries in a suggestion dictionary.

Parameters:

  • key (String)

    the suggestion dictionary key

Returns:

  • (Integer)

    the number of suggestions



461
462
463
# 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.

Parameters:

  • index_name (String)

    the index name

Returns:

  • (Hash{String=>Array<String>})

    each term mapped to the synonym group ids it belongs to



514
515
516
# 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.

Parameters:

  • index_name (String)

    the index name

  • group_id (String)

    the synonym group id

  • terms (Array<String>)

    the terms to add to the group

  • skip_initial_scan (Boolean) (defaults to: false)

    do not re-scan existing documents (+SKIPINITIALSCAN+)

Returns:

  • (String)

    "OK"



503
504
505
506
507
508
# 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.

Parameters:

  • index_name (String)

    the index name

  • field_name (String)

    the TAG field name

Returns:

  • (Array<String>)

    the distinct tag values (normalized to lowercase unless the field is case-sensitive)



524
525
526
# 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