Как искать в большом массиве JSON и находить записи по нескольким ключам - PullRequest
3 голосов
/ 29 октября 2019

У меня очень большой набор данных, который организован следующим образом:

users = [
    {
        username: "Bill",
        gender: "Male",
        details: {
            city: "NY"
        }
    },
    {
        username: "Mary",
        gender: "Female",
        details: {
            city: "LA"
        }
    }
]

Мне нужен быстрый способ поиска нескольких записей по нескольким значениям из нескольких ключей.

У меня есть точка-разделенный список ключей:

keys = ["gender", "details.city"]

Мне нужно сделать что-то вроде этого (написано в псевдокоде):

my_users = users.any? {|user|
  keys.each do |key|
    user.key == "NY"
  end
}

Я знаю, что это не сработает. Одна из причин, по которой это не сработает, заключается в том, что мой список ключей разделен точками, поэтому я могу либо разделить его на массив ключей, как в ['gender'] и ['details']['city'], либо преобразовать хэш пользователя в точкуотделенный объект с помощью метода, подобного:

def to_o
  JSON.parse to_json, object_class: OpenStruct
end

Ответы [ 4 ]

2 голосов
/ 29 октября 2019

Для линейного поиска хорошим решением является решение demir.

Для угла "должен быть быстрый" вы можете обнаружить, что сканирование O (n) через ваш массив пользователей выполняется слишком медленно. Чтобы облегчить это, вам может потребоваться создать индекс:

require "set"
class Index
  def initialize(dataset)
    @index = make_index(dataset)
  end

  def find(conditions = {})
    conditions.inject(Set.new) { |o, e| o | @index[e.join(".")] }.to_a
  end

  private

  def make_keys(record, prefix = [])
    record.flat_map do |key, val|
      case val
      when Hash
        make_keys val, [key]
      else
        (prefix + [key, val]).join(".")
      end
    end
  end

  def make_index(dataset)
    dataset.each_with_object({}) do |record, index|
      make_keys(record).each { |key| (index[key] ||= []) << record }
    end
  end
end

index = Index.new(users)
p index.find("gender" => "Male", "details.city" => "NY")
# => [{:username=>"Bill", :gender=>"Male", :details=>{:city=>"NY"}}]

Это занимает O (n) времени и требует дополнительной памяти для создания индекса один раз, но тогда каждый поиск набора данных должен происходить в O (1 раз. Если вы выполните кучу поиска после настройки набора данных один раз, возможно, вам подойдет что-то подобное.

2 голосов
/ 29 октября 2019

Я надеюсь, что этот метод работает так, как вы хотите

def search(users, keys, value)
  users.select do |user|
    keys.any? do |key|
      user.dig(*key.split('.').map(&:to_sym)) == value
    end
  end
end

search(users, keys, 'NY')
#=> [{ :username => "Bill", :gender => "Male", :details => { :city => "NY" } }]
1 голос
/ 29 октября 2019

Вы можете динамически перемещаться по хешу с помощью Hash#dig, который был представлен в Ruby 2.3.0:

def select_users(users, conditions)
  users.select do |user|
    conditions.select do |key, value|
      user.dig(*key.to_s.split(".").map(&:to_sym)) == value
    end.length == conditions.length
  end
end

Это предполагает, что входными данными для условий является хеш, такой как:

{ "gender" => "Male", "details.city" => "NY" }

И оптимизирован для тестирования нескольких условий за один линейный проход. Вы также можете поддерживать массивы для проверки на включение:

def select_users(users, conditions)
  users.select do |user|
    conditions.select do |key, value|
      actual = user.dig(*key.to_s.split(".").map(&:to_sym))
      if value.is_a?(Array)
        value.includes?(actual)
      else
        actual == value
      end
    end.length == conditions.length
  end
end
# get users where city is "NY", "Detroit" or "Los Angeles"
select_users(dataset, { "gender" => "Male", "details.city" => ["NY", "Detroit", "Los Angeles"] })
0 голосов
/ 29 октября 2019

Код в вопросе (в частности, any?) предполагает, что объект должен определить, существует ли для любого хеша h в users,

h[:gender] == city #=> true

или хешg для которых:

g = h[:details]
g[:city] == city   #=> true

Код

def city_present?(users, *key_groups, city)
  key_arr = key_groups.map { |s| s.split('.').map(&:to_sym) }
  users.any? { |h| key_arr.any? { |keys| h.dig(*keys) == city } }
end

Примеры

Для users приведено ввопрос и

city_present?(users, "gender", "details.city", 'NY') #=> true
city_present?(users, "gender", "details.city", 'LA') #=> true
city_present?(users, "gender", "details.city", 'TO') #=> false

Пояснение

См. Hash # dig . key_arr найдено равным:

[[:gender], [:details, :city]]

Повторные поиски

Принятие предложения @ ChrisHeald, если users было большим, и должны были проводиться повторные поискидля разных значений имеет смысл создать набор значений, связанных с key_groups. Это можно сделать следующим образом.

require 'set'

def values_present(users, *key_groups)
  key_arr = key_groups.map { |s| s.split('.').map(&:to_sym) }
  users.each_with_object(Set.new) do |h,set|
    key_arr.each do |keys|
      v = h.dig(*keys)
      set << v unless v.nil?
    end
  end
end

values_present(users, "gender", "details.city")     
  #=> #<Set: {"Male", "NY", "Female", "LA"}>
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...