Skip to content

[Request] Search-UI URL Parsing in Ruby #417

@mjkaufer

Description

@mjkaufer

Hey! I'm trying to decode elastic URL queries from elastic's search-ui package. I've written a lot of manual code to convert a query string to an app search compatible filter/query combo. I based most of it off of the urlToState in URLManager from search-ui

I was wondering if the team would be open to including an official version of this parsing logic in this library, so that we can parse search-ui query strings as app search queries from ruby

Code is below – if you think it's a good idea, I can make a PR to incorporate this into the codebase, or if one of y'all would like to tackle this from the ground up, that would work too – thanks!

# typed: true
module ElasticService
  class Util
    extend T::Sig

    # TODO: Probably need to support nested filters?
    sig { params(query_string: String).returns(T.untyped) }
    def self.query_string_to_app_search_query(query_string)
      query_object_raw = raw_nested_query_to_elastic_query(
        Rack::Utils.parse_nested_query(query_string.delete_prefix("?"))
      )

      cleaned_filters = query_object_raw['filters'].map do |filter_entry|
        {
          "any" => filter_entry["values"].map do |sub_filter|
            cleaned_sub_filter = sub_filter
            if cleaned_sub_filter.class == Hash
              # Elastic gets angry if we add extra fields (:
              cleaned_sub_filter = cleaned_sub_filter.except("name")
            end
            {filter_entry["field"] => cleaned_sub_filter}
          end
        }
      end

      filters = {
        "all" => cleaned_filters
      }

      {
        "query" => query_object_raw['q'] || '',
        "filters" => filters,
      }
    end


    def self.parse_escaped_string(value_raw)
      value = CGI.unescape(value_raw)
      if /^n_.*_n$/.match(value)
        # Remove first and last 2 characters from string
        return value.delete_prefix("n_").delete_suffix("_n").to_f
      end

      if /^b_.*_b$/.match(value)
        # Remove first and last 2 characters from string
        return value.delete_prefix("b_").delete_suffix("_b").to_bool
      end

      # Need rfc3339 for elastic to be happy
      # but JSON stringifies to iso8601
      return Date.iso8601(value).rfc3339 rescue value
    end

    # Rack's parse_nested_query doesn't convert array-like things to arrays!
    # This handles that, so that we can manipulate the list of filters without
    # introducing "holes" into our filter array
    # Also handles elastic primitive encoding (i.e. "n_123_n") and converts date strings properly
    def self.raw_nested_query_to_elastic_query(nested_query, key_context = nil)
      if nested_query.class == Hash
        nested_query = T.let(nested_query, Hash)
        # If every key is an int, convert to array
        if nested_query.keys.all? {|k| T.let(Integer(k, exception: false), T.nilable(Integer)) && k.to_i >= 0}
          max_index = nested_query.keys.map(&:to_i).max
          backing_array = Array.new(max_index + 1)
          nested_query.each do |k, v|
            backing_array[k.to_i] = v
          end
          # Recurse now that it's been transformed to an array
          return raw_nested_query_to_elastic_query(backing_array)
        else
          return nested_query.map do |k, v| 
            [
              k,
              raw_nested_query_to_elastic_query(v, k)
            ]
          end.to_h
        end
      elsif nested_query.class == Array
        return nested_query.map {|el| raw_nested_query_to_elastic_query(el)}
      elsif nested_query.class == String
          return parse_escaped_string(nested_query)
      else
        return nested_query
      end
    end
  end
end

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions