Использование jq для извлечения общих префиксов в структуре данных JSON

У меня есть набор данных JSON с примерно 8,7 миллионами пар значений ключей, извлеченных из хранилища Redis, где каждый ключ гарантированно будет 8-значным числом, а ключ представляет собой 8-буквенно-цифровое значение, т.е.

[{
"91201544":"INXX0019",
"90429396":"THXX0020",
"20140367":"ITXX0043",
 ...
}]

Чтобы уменьшить использование памяти Redis, я хочу преобразовать это в хэш хэшей, где ключ префикса хэша — это первые 6 символов ключа (см. эта ссылка), а затем сохраните ее обратно в Redis.

В частности, я хочу, чтобы моя результирующая структура данных JSON (которую я затем напишу некоторый код для анализа этой структуры JSON и создания командного файла Redis, состоящего из HSET и т. д.) больше походила на

[{
 "000000": { "00000023": "INCD1234",
             "00000027": "INCF1423",
              ....
           },
 ....
 "904293": { "90429300": "THXX0020",
             "90429302": "THXX0024",
             "90429305": "THXY0013"}
 }]

Поскольку меня впечатлил jq, я пытаюсь стать более опытным в программирование в функциональном стиле, я хотел использовать jq для этой задачи. Пока я придумал следующее:

% jq '.[0] | to_entries | map({key: .key, pfx: .key[0:6], value: .value}) | group_by(.pfx)'

Это дает мне что-то вроде

[
  [
    {
      "key": "00000130",
      "pfx": "000001",
      "value": "CAXX3231"
    },
    {
      "key": "00000162",
      "pfx": "000001",
      "value": "CAXX4606"
    }
  ],
  [
    {
      "key": "00000238",
      "pfx": "000002",
      "value": "CAXX1967"
    },
    {
      "key": "00000256",
      "pfx": "000002",
      "value": "CAXX0727"
    }
  ],
  ....
]

Я пробовал следующее:

% jq 'map(map({key: .pfx, value: {key, value}})) 
      | map(reduce .[] as $item ({}; {key: $item.key, value: [.value[], $item.value]} )) 
      | map( {key, value: .value | from_entries} ) 
      | from_entries'

который дает мне правильный результат, но также выводит ошибку для каждого уменьшения (я считаю)

jq: error: Cannot iterate over null

Конечный результат

{
   "000001": {
     "00000130": "CAXX3231",
     "00000162": "CAXX4606"
   },
   "000002": {
     "00000238": "CAXX1967",
     "00000256": "CAXX0727"
   },
   ...
}

что правильно, но как я могу избежать появления этого предупреждения stderr?


person EJT    schedule 18.07.2014    source источник


Ответы (3)


Я не уверен, что здесь достаточно данных, чтобы оценить источник проблемы. Мне трудно поверить, что то, что вы пробовали, приводит к этому. Я получаю ошибки с этим на всем пути.

Вместо этого попробуйте этот фильтр:

.[0]
    | to_entries
    | group_by(.key[0:6])
    | map({
          key:   .[0].key[0:6],
          value: map(.key=.key[6:8]) | from_entries
      })
    | from_entries

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

[{
    "91201544":"INXX0019",
    "90429396":"THXX0020",
    "20140367":"ITXX0043",
    "00000023":"INCD1234",
    "00000027":"INCF1423",
    "90429300":"THXX0020",
    "90429302":"THXX0024",
    "90429305":"THXY0013"
}]

Результаты в этом:

{
  "000000": {
    "23": "INCD1234",
    "27": "INCF1423"
  },
  "201403": {
    "67": "ITXX0043"
  },
  "904293": {
    "00": "THXX0020",
    "02": "THXX0024",
    "05": "THXY0013",
    "96": "THXX0020"
  },
  "912015": {
    "44": "INXX0019"
  }
}
person Jeff Mercado    schedule 18.07.2014
comment
Это использовалось jq-1.4, но да, ваш метод определенно лучше. Я упустил то, что вы можете протолкнуть массив вниз на один уровень, используя map внутри map, т.е. map({key: .[0].key[0:6], value: map(.key=.key[6:8]) | from_entries}) - person EJT; 18.07.2014
comment
Да, мне не удалось заставить ваш фильтр работать здесь. И они якобы используют 1.4. - person Jeff Mercado; 19.07.2014
comment
У меня была ошибка в первом извлечении, которую я сейчас исправил, должно быть key[0:6], а не key[0-6]. И на jqplay.com действительно появляется, что проблема заключается в попытке использовать reduce. jqplay просто выводит ту же ошибку, что и в командной строке, jq: error: Cannot iterate over null. Когда я запускаю его в реальной среде командной строки, jq напечатает эту ошибку, но продолжит и успешно распечатает результат. - person EJT; 20.07.2014
comment
На самом деле часть, которую вы отредактировали (хотя ее все еще нужно исправить), я понял, что вы пытаетесь сделать. Это была не та проблема, которую я видел. Второй, который вы пробовали, я не смог приступить к работе. Даже прохождение в результате первого фильтра. У вас остался оригинальный фильтр? jqplay.org не показывает ничего, кроме ошибок. - person Jeff Mercado; 20.07.2014
comment
Да, я наконец заставил это работать при использовании jqplay.org (+1 за то, что показал мне это!) Вот что я закончил тем, что изменил полный фильтр на: .[0] | to_entries | map({key: .key, pfx: .key[0:6], value: .value}) | group_by(.pfx) | map(map({key: .pfx, value: {key, value}})) | map(reduce ( .[] | iterables) as $item ({}; {key: $item.key, value: [$item.value, if (.value | iterables) then .value[] else . end]} )) | map( {key, value: .value | from_entries} ) | from_entries. Но, очевидно, ваше решение намного проще и менее запутанно, поэтому я буду использовать его. - person EJT; 20.07.2014

Я понимаю, что это не то, о чем вы просите, но, просто для справки, я думаю, что будет НАМНОГО быстрее сделать это с помощью встроенного в Redis сценария Lua.

И оказывается, что это немного проще:

for _,key in pairs(redis.call('keys', '*')) do
  local val = redis.call('get', key)
  local short_key = string.sub(key, 0, -2)
  redis.call('hset', short_key, key, val)
  redis.call('del', key)
end

Это будет сделано на месте без переноса из/в Redis и преобразования в/из JSON.

Запустите его из консоли как:

$ redis-cli eval "$(cat script.lua)" 0
person max taldykin    schedule 18.07.2014
comment
Ты прав. Это работало быстрее с lua на моем ноутбуке с тактовой частотой 2,x ГГц примерно за 5 1/2 минут. В исходной базе данных есть ключи 8MM+. Использование извлечения в .rdb, затем rdb в json, обработка json с помощью jq, а затем повторное выполнение с использованием redis-cli --pipe заняло в общей сложности около 11 минут. - person EJT; 20.07.2014

Для справки, jq group_by полагается на сортировку, которая, конечно, заметно замедлит работу, когда входные данные достаточно велики. Следующее выполняется примерно на 40% быстрее, даже если входной массив содержит всего 100 000 элементов:

def compress:
  . as $in
  | reduce keys[] as $key ({};
      $key[0:6] as $k6
      | $key[6:] as $k2
      | .[$k6] += {($k2): $in[$key]} );

.[0] | compress

Учитывая ввод Джеффа, вывод идентичен.

person peak    schedule 14.12.2015