Module: Redis::Commands::Search::ResultParser
| Relationships & Source Files | |
| Defined in: | lib/redis/commands/modules/search/result.rb |
Overview
Reply reshaping for the Query Engine.
Every parser normalises both RESP2 (flat arrays) and RESP3 (native maps) replies to
the same Ruby objects, so the public API is stable regardless of the protocol the
underlying connection negotiates. RESP3 is the default protocol; the RESP2 branches exist
for protocol: 2 connections.
Class Method Summary
-
.aggregate(reply) ⇒ AggregateResult?
mod_func
FT.AGGREGATE / FT.CURSOR READ ->
AggregateResult. -
.config_get(reply) ⇒ Hash?
mod_func
FT.CONFIG GET -> Hash.
-
.decode_value(value) ⇒ Object
mod_func
JSON-decode a value, returning it unchanged when it is not a parseable JSON String.
- .hashify_fields(fields, decode_fields) mod_func
-
.hashify_info(reply) ⇒ Hash?
mod_func
FT.INFO / FT.CONFIG GET -> Hash.
-
.hybrid(reply) ⇒ HybridResult?
mod_func
FT.HYBRID ->
HybridResult. -
.normalize_aggregate_row(row) ⇒ Hash
mod_func
A COLLECT reducer column is an array of entry-maps:
Arrayunder RESP3 butArray<[k, v, ...]>(flat key/value arrays) under RESP2. -
.normalize_score(score) ⇒ Hash{String => Object}
mod_func
Turn a flat [field, value, ...] array (RESP2) or a native map (RESP3) of returned fields into a hash, JSON-decoding values whose field is flagged in
decode_fields. -
.search(reply, with_scores: false, with_payloads: false, no_content: false, decode_fields: {}) ⇒ SearchResult?
mod_func
FT.SEARCH ->
SearchResult. -
.search_resp2(reply, with_scores, with_payloads, no_content, decode_fields) ⇒ SearchResult
mod_func
Parse a RESP2 flat-array FT.SEARCH reply into a
SearchResult. -
.search_resp3(reply, decode_fields) ⇒ SearchResult
mod_func
Parse a RESP3 map FT.SEARCH reply into a
SearchResult. -
.spellcheck(reply) ⇒ Hash{String => Array<Hash>}?
mod_func
FT.SPELLCHECK -> { term => [{ "suggestion" => ..., "score" => ...
-
.syndump(reply) ⇒ Hash{String => Array}?
mod_func
FT.SYNDUMP -> { term => [group_id, ...] }.
Class Method Details
.aggregate(reply) ⇒ AggregateResult? (mod_func)
FT.AGGREGATE / FT.CURSOR READ -> AggregateResult
Without a cursor the reply is [num_rows, row, row, ...] (RESP2) or a map with "results" (RESP3). With WITHCURSOR the reply is [aggregate_reply, cursor_id].
# File 'lib/redis/commands/modules/search/result.rb', line 308
def aggregate(reply) return reply if reply.nil? cursor = nil body = reply # WITHCURSOR wraps the aggregate body and the cursor id in a 2-element array. The # body is a flat array under RESP2 and a native map under RESP3. if reply.is_a?(Array) && reply.length == 2 && reply[1].is_a?(Integer) && (reply[0].is_a?(Array) || reply[0].is_a?(Hash)) body = reply[0] cursor = reply[1] end rows = if body.is_a?(Hash) # RESP3 (body["results"] || []).map { |row| hashify_fields(row["extra_attributes"] || row, {}) } else # RESP2: first element is the row count, the rest are rows body[1..-1].to_a.map { |row| hashify_fields(row, {}) } end AggregateResult.new(rows.map { |row| normalize_aggregate_row(row) }, cursor: cursor) end
.config_get(reply) ⇒ Hash? (mod_func)
FT.CONFIG GET -> Hash. RESP2 returns nested [[option, value], ...] pairs; RESP3 a map.
# File 'lib/redis/commands/modules/search/result.rb', line 394
def config_get(reply) return reply if reply.nil? return reply if reply.is_a?(Hash) reply.to_h end
.decode_value(value) ⇒ Object (mod_func)
JSON-decode a value, returning it unchanged when it is not a parseable JSON String.
# File 'lib/redis/commands/modules/search/result.rb', line 478
def decode_value(value) return value unless value.is_a?(String) ::JSON.parse(value) rescue ::JSON::ParserError value end
.hashify_fields(fields, decode_fields) (mod_func)
[ GitHub ]# File 'lib/redis/commands/modules/search/result.rb', line 461
def hashify_fields(fields, decode_fields) return {} if fields.nil? # Reply field names are strings, but callers may key decode_fields with symbols # (e.g. Query#return_field(:brand)). Normalize so decoding is caller-type-independent. decode = decode_fields.transform_keys(&:to_s) pairs = fields.is_a?(Hash) ? fields.to_a : fields.each_slice(2).to_a pairs.each_with_object({}) do |(name, value), acc| acc[name] = decode[name.to_s] ? decode_value(value) : value end end
.hashify_info(reply) ⇒ Hash? (mod_func)
FT.INFO / FT.CONFIG GET -> Hash. RESP2 returns a flat [k, v, ...] array; RESP3 already returns a native map.
# File 'lib/redis/commands/modules/search/result.rb', line 383
def hashify_info(reply) return reply if reply.nil? return reply if reply.is_a?(Hash) Hash[*reply] end
.hybrid(reply) ⇒ HybridResult? (mod_func)
FT.HYBRID -> HybridResult
The reply is a native map under RESP3 and a flat [k, v, ...] array under RESP2. A WITHCURSOR reply carries per-leg cursor ids ("SEARCH"/"VSIM") instead of a result page.
# File 'lib/redis/commands/modules/search/result.rb', line 356
def hybrid(reply) return reply if reply.nil? map = reply.is_a?(Hash) ? reply : Hash[*reply] if map.key?("SEARCH") || map.key?("VSIM") return HybridResult.new( warnings: map["warnings"] || [], search_cursor: map["SEARCH"], vsim_cursor: map["VSIM"] ) end rows = Array(map["results"]).map { |row| hashify_fields(row, {}) } HybridResult.new( rows: rows, total: map["total_results"], warnings: map["warnings"] || [], execution_time: map["execution_time"] ) end
.normalize_aggregate_row(row) ⇒ Hash (mod_func)
A COLLECT reducer column is an array of entry-maps: Array under RESP3 but
Array<[k, v, ...]> (flat key/value arrays) under RESP2. Normalize both to
Array so callers see a uniform type regardless of protocol. Scalar values and
arrays of scalars (e.g. TOLIST) are left untouched, and the RESP3 case is idempotent.
# File 'lib/redis/commands/modules/search/result.rb', line 338
def normalize_aggregate_row(row) row.each_with_object({}) do |(key, value), acc| acc[key] = if value.is_a?(Array) && !value.empty? && value.all? { |e| e.is_a?(Array) || e.is_a?(Hash) } value.map { |entry| hashify_fields(entry, {}) } else value end end end
.normalize_score(score) ⇒ Hash{String => Object} (mod_func)
Turn a flat [field, value, ...] array (RESP2) or a native map (RESP3) of returned
fields into a hash, JSON-decoding values whose field is flagged in decode_fields.
Normalize a WITHSCORES value to a Float so Document#score is the same type regardless of
protocol (RESP2 returns the score as a bulk string, RESP3 as a native double). With
EXPLAINSCORE the RESP2 value is [score, explanation]; coerce the score part and keep
the explanation. Anything non-numeric is returned untouched.
# File 'lib/redis/commands/modules/search/result.rb', line 451
def normalize_score(score) case score when nil then nil when Array then [normalize_score(score[0]), *score[1..]] else Float(score) end rescue ArgumentError, TypeError score end
.search(reply, with_scores: false, with_payloads: false, no_content: false, decode_fields: {}) ⇒ SearchResult? (mod_func)
FT.SEARCH -> SearchResult
# File 'lib/redis/commands/modules/search/result.rb', line 235
def search(reply, with_scores: false, with_payloads: false, no_content: false, decode_fields: {}) return reply if reply.nil? if reply.is_a?(Hash) # RESP3 search_resp3(reply, decode_fields) else # RESP2 flat array search_resp2(reply, with_scores, with_payloads, no_content, decode_fields) end end
.search_resp2(reply, with_scores, with_payloads, no_content, decode_fields) ⇒ SearchResult (mod_func)
Parse a RESP2 flat-array FT.SEARCH reply into a SearchResult.
# File 'lib/redis/commands/modules/search/result.rb', line 253
def search_resp2(reply, with_scores, with_payloads, no_content, decode_fields) total = reply[0] documents = [] index = 1 while index < reply.length id = reply[index] index += 1 score = nil if with_scores score = normalize_score(reply[index]) index += 1 end payload = nil if with_payloads payload = reply[index] index += 1 end attributes = {} unless no_content attributes = hashify_fields(reply[index], decode_fields) index += 1 end documents << Document.new(id, attributes: attributes, score: score, payload: payload) end SearchResult.new(total, documents) end
.search_resp3(reply, decode_fields) ⇒ SearchResult (mod_func)
Parse a RESP3 map FT.SEARCH reply into a SearchResult.
# File 'lib/redis/commands/modules/search/result.rb', line 291
def search_resp3(reply, decode_fields) documents = (reply["results"] || []).map do |row| attributes = hashify_fields(row["extra_attributes"], decode_fields) Document.new(row["id"], attributes: attributes, score: normalize_score(row["score"]), payload: row["payload"]) end SearchResult.new(reply["total_results"], documents, warnings: reply["warning"] || []) end
.spellcheck(reply) ⇒ Hash{String => Array<Hash>}? (mod_func)
FT.SPELLCHECK -> { term => [{ "suggestion" => ..., "score" => ... }, ...] }
# File 'lib/redis/commands/modules/search/result.rb', line 416
def spellcheck(reply) return reply if reply.nil? # RESP3: { "results" => { term => [{ suggestion => score }, ...] } } if reply.is_a?(Hash) results = reply["results"] || {} return results.each_with_object({}) do |(term, suggestions), acc| acc[term] = suggestions.flat_map do |entry| entry.map { |suggestion, score| { "suggestion" => suggestion, "score" => score } } end end end # RESP2: [["TERM", term, [[score, suggestion], ...]], ...] parsed = {} reply.each do |entry| next unless entry[0] == "TERM" parsed[entry[1]] = entry[2].map do |score, suggestion| { "suggestion" => suggestion, "score" => score } end end parsed end
.syndump(reply) ⇒ Hash{String => Array}? (mod_func)
FT.SYNDUMP -> { term => [group_id, ...] }. RESP2 is a flat [term, [ids], ...] array.
# File 'lib/redis/commands/modules/search/result.rb', line 405
def syndump(reply) return reply if reply.nil? return reply if reply.is_a?(Hash) Hash[*reply] end