Skip to content

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

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Open
mjkaufer opened this issue Feb 15, 2024 · 0 comments
Open

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

mjkaufer opened this issue Feb 15, 2024 · 0 comments

Comments

@mjkaufer
Copy link

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
# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant