Module: Redis::Commands::Search::ResultParser
- 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 collapse
-
.aggregate(reply) ⇒ AggregateResult?
FT.AGGREGATE / FT.CURSOR READ -> AggregateResult.
-
.config_get(reply) ⇒ Hash?
FT.CONFIG GET -> Hash.
-
.decode_value(value) ⇒ Object
JSON-decode a value, returning it unchanged when it is not a parseable JSON String.
- .hashify_fields(fields, decode_fields) ⇒ Object
-
.hashify_info(reply) ⇒ Hash?
FT.INFO / FT.CONFIG GET -> Hash.
-
.hybrid(reply) ⇒ HybridResult?
FT.HYBRID -> HybridResult.
-
.normalize_aggregate_row(row) ⇒ Hash
A COLLECT reducer column is an array of entry-maps:
Array<Hash>under RESP3 butArray<[k, v, ...]>(flat key/value arrays) under RESP2. -
.normalize_score(score) ⇒ Hash{String => Object}
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?
FT.SEARCH -> SearchResult.
-
.search_resp2(reply, with_scores, with_payloads, no_content, decode_fields) ⇒ SearchResult
Parse a RESP2 flat-array FT.SEARCH reply into a SearchResult.
-
.search_resp3(reply, decode_fields) ⇒ SearchResult
Parse a RESP3 map FT.SEARCH reply into a SearchResult.
-
.spellcheck(reply) ⇒ Hash{String => Array<Hash>}?
FT.SPELLCHECK -> { term => [{ "suggestion" => ..., "score" => ... }, ...] }.
-
.syndump(reply) ⇒ Hash{String => Array}?
FT.SYNDUMP -> { term => [group_id, ...] }.
Class Method Details
.aggregate(reply) ⇒ AggregateResult?
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].
308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 |
# 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?
FT.CONFIG GET -> Hash. RESP2 returns nested [[option, value], ...] pairs; RESP3 a map.
394 395 396 397 398 399 |
# 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
JSON-decode a value, returning it unchanged when it is not a parseable JSON String.
478 479 480 481 482 483 484 |
# 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) ⇒ Object
461 462 463 464 465 466 467 468 469 470 471 472 |
# 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?
FT.INFO / FT.CONFIG GET -> Hash. RESP2 returns a flat [k, v, ...] array; RESP3 already returns a native map.
383 384 385 386 387 388 |
# 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?
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.
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 |
# 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
A COLLECT reducer column is an array of entry-maps: Array<Hash> under RESP3 but
Array<[k, v, ...]> (flat key/value arrays) under RESP2. Normalize both to
Array<Hash> 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.
338 339 340 341 342 343 344 345 346 347 |
# 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}
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.
451 452 453 454 455 456 457 458 459 |
# 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?
FT.SEARCH -> SearchResult
235 236 237 238 239 240 241 242 243 |
# 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
Parse a RESP2 flat-array FT.SEARCH reply into a SearchResult.
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 280 281 282 283 284 |
# 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
Parse a RESP3 map FT.SEARCH reply into a SearchResult.
291 292 293 294 295 296 297 298 299 |
# 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>}?
FT.SPELLCHECK -> { term => [{ "suggestion" => ..., "score" => ... }, ...] }
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 |
# 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}?
FT.SYNDUMP -> { term => [group_id, ...] }. RESP2 is a flat [term, [ids], ...] array.
405 406 407 408 409 410 |
# 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 |