Хранение текста в полудоступном для поиска, но компактном формате

Я хотел бы получить наборы данных Google N-Gram для использования на некоторых стандартных аппаратных средствах. Проблема в том, что эти маленькие серверы не могут справиться с объемом данных, которые необходимо хранить.

Это заставило меня задуматься о том, как другие большие текстовые системы, такие как WordNET или поисковые системы, решают эту проблему. Интересно, есть ли способ нормализовать данные, но при этом сделать их доступными для поиска?

Вернемся к N-Gram. Моя идея состоит в том, чтобы хранить все слова из 1-Gram в базе данных вместе с идентификатором. Затем используйте этот идентификатор для создания отношений в цепочках +2 Gram так же, как вы бы отслеживали отношения друзей в социальной сети — два идентификатора в строке.

TABLE Words
{
    id
    word
}

TABLE TWOGRAM
{
    first_word_id
    second_word_id
}

TABLE THREEGRAM
{
    first_word_id
    second_word_id
    third_word_id
}

TABLE FOURGRAM
{
    first_word_id
    second_word_id
    third_word_id
    forth_word_id
}

Есть ли более эффективный способ компактного хранения всех этих данных?

Возможно, вместо сжатия текста можно было бы выполнить хеширование пар (или последовательностей) слов, чтобы уменьшить размер хранилища, сохраняя при этом возможность поиска так же, как и пароли.


person Xeoncross    schedule 07.10.2011    source источник
comment
Google перечисляет 360 миллиардов граммов, но вы будете хранить только одну строку для каждого уникального английского слова. Это правильно?   -  person Mike Sherrill 'Cat Recall'    schedule 09.10.2011
comment
@Catcall, да. Вместо того, чтобы хранить 360 миллиардов 1 грамм снова и снова для каждой пары или группы 2+, лучше сохранить слова один раз, а затем использовать меньшие целые числа для представления слов.   -  person Xeoncross    schedule 10.10.2011


Ответы (2)


Типичный способ справиться с этой ситуацией — разделить данные на небольшие блоки (например, строки по 2 КБ), сгруппировать данные внутри блоков по столбцам, а затем сжать преобразованный блок с помощью алгоритма быстрого сжатия. Экономия может быть довольно существенной.

[Изменить] Немного больше подробностей по запросу: Целью здесь является обработка небольших блоков сжатых данных, поэтому необходимо поддерживать требования к памяти на разумном уровне. «разумный» зависит от вашей целевой системы, поэтому это может быть 256 строк, 2 КБ или 8 КБ.

Однако в каждой строке есть несколько полей. Так что сжатие напрямую существенной экономии не принесет (например, zip "всего" х5). К счастью, разделительный символ между этими полями хорошо известен (0x09), поэтому можно получить начало и конец каждого поля.

Идея состоит в том, чтобы сгруппировать все «Поля 1» вместе, затем «Поля 2», затем «Поля 3» и так далее. Если блок, извлеченный из файла .csv, состоит из 2 КБ строк, мы знаем, что у нас есть 2 КБ полей каждого типа.

Затем простой алгоритм быстрого сжатия творит чудеса с этой преобразованной структурой. Корреляции очень сильны, поскольку последовательные поля, как правило, имеют много общего. 10-кратная степень сжатия неудивительна для таких данных. Наборы данных Google N-Gram, вероятно, будут еще более благоприятными.

Поскольку ваша цель состоит в том, чтобы хранить в памяти как можно больше данных для поиска в них, рекомендуется сохранять каждый блок достаточно маленьким (примерно от 256 до 8 КБ) и использовать очень быстрый алгоритм сжатия/распаковки. Таким образом, распаковка будет достаточно быстрой, чтобы стать незначительной частью вашего алгоритма. Например, что-то вроде LZ4 обеспечивает скорость декомпрессии около ~ 1 ГБ / с.

По поводу поиска: все зависит от того, что вы хотите искать, но если речь идет о поиске точной N-граммы, то нам повезло, так как они уже отсортированы в лексикографическом порядке.

На этом этапе достаточно сохранить в таблицу первую N-грамму каждого блока. При поиске конкретной N-граммы нужно просто найти, в каком блоке она находится. Первая N-грамма блока обязательно ‹=. N-грамм следующего блока обязательно >. Распаковка блока, как видно выше, должна быть незначительной частью алгоритма.

Даже с блоками из 2 КБ строк вам может понадобиться много «заголовков N-грамм» для хранения, поэтому тривиальный поиск пузырьков может быть довольно долгим. Рекомендуется поиск по дереву или по оси.

После того, как блок загружен, все еще необходимо выполнить поиск в нем, чтобы найти правильную N-грамму. Можно использовать ту же тактику, что и раньше (загрузить N-граммы в таблицу и выполнять поиск по ней).

person Cyan    schedule 10.10.2011
comment
Можете ли вы объяснить это немного больше? Вы имеете в виду 2000 строк слов или словосочетаний? Кроме того, как это все еще доступно для поиска без необходимости сжимать каждую группу 2k для поиска? - person Xeoncross; 10.10.2011
comment
Ну ладно для хранения и поиска. Но я предполагаю, что преобразование столбца/строки также может стоить довольно много циклов процессора. - person Cyan; 11.10.2011
comment
@Attract Я все еще не слежу за тобой. Когда вы говорите поле 1 или поле 2, вы говорите о столбцах поля CSV? Если да, то вы хотите хранить каждый CSV в виде отдельной строки в базе данных и распаковывать его на лету? Если это так, то это не кажется хорошей идеей, поскольку CSV-файлы имеют размер более 100 МБ каждый. Некоторые из ваших идей звучат как хорошие идеи, но вам нужно добавить в текст недостающие существительные, чтобы мы знали, о чем вы думаете. - person Xeoncross; 14.10.2011
comment
Да, это основная идея: по сути, транспонируйте матрицу, чтобы столбцы стали линиями, и сжимайте транспозицию. Однако не делайте этого со всем файлом: как вы говорите, говорите, они слишком велики. Разделите их на более мелкие части (например, по 256 КБ каждая), переместите часть и сожмите ее. Обратная операция (декодирование, транспонирование, возврат исходного фрагмента) может выполняться на лету. Алгоритмы сжатия достаточно быстры, чтобы не находиться на критическом пути в настоящее время. Как сказал Cyan, алгоритм транспонирования, если он плохо оптимизирован, может стоить больше, чем само сжатие! - person Cyan; 15.10.2011

Вместо того, чтобы хранить 360 миллиардов 1 граммов снова и снова для каждой пары или группы 2+, лучше сохранить слова один раз, а затем использовать меньшие целые числа для представления слов.

(Я резюмировал свои оценки ниже.)

Трудно сказать наверняка, что здесь лучше использовать целые числа. Вам нужны более точные оценки того, сколько места на диске вам нужно, сколько места на диске у вас есть и сколько места на диске вы можете себе позволить.

Статистика говорит нам, что средняя длина слова в английском языке составляет 5,1 символа. В вашем приложении это то же самое, что 5,1 байта. Средняя длина двух слов составляет около 10,2 байта; длина двух целых чисел составляет 8 байт.

В файле номер 71 около 66 миллионов 2 граммов (выбранных случайным образом). При размере 10,2 байта на запись вы получаете около 673 мегабайт для этого файла. (Это предполагает, что вы собираетесь хранить только слова, а не количество.) Для 100 2-граммовых файлов вам потребуется от 52 до 67 гигабайт (без учета индексов). Добавьте 50% за наше глубокое невежество. 100 гигов покроют 2 грамма.

Если вы храните счетчики со словами, этот файл составляет 1,6 гига, и 100 из них должны составлять около 160 гигов. Таким образом, у нас есть диапазон от 100 до 160 гигабайт для хранения 2 граммов.

Я оставлю вам оценку пространства, необходимого для индексов.

Целые числа экономят 2,2 байта на слово. Но хранение двух целых чисел означает, что вам всегда придется выполнять два соединения, чтобы получить реальные данные. (Хранение пяти целых чисел для 5 граммов означает, что вам потребуется пять соединений, чтобы получить реальные данные.) Хранение самих слов не требует соединений для получения реальных данных.

Если вы также храните счетчики, вы можете сэкономить место, сохранив внешний ключ для ngram вместо использования отдельных слов. Таким образом, вы можете хранить

ngram_id  ngram_text
--
143564    five pounds

в одной таблице и

ngram_id    year    match_count  page_count  volume_count 
--
143564      1999    4            3           3
143564      2000    2            2           1
143564      2001    1            1           1
143564      2002    1            1           1
143564      2003    2            2           2
143564      2004    1            1           1
143564      2005    6            6           5
143564      2006    30           21          17
143564      2007    39           37          26
143564      2008    44           41          28

в другой.

Для этой конкретной 2-граммы текст занимает 11 байт, а целое число занимает 4. Экономия 7 байтов на каждой из 10 строк, 70 байтов. Требуется одно соединение для получения реальных данных. Для этого подхода я бы оценил около 130 гигабайт для всех 2 граммов, исключая индексы и таблицу, которая предоставляет внешние ключи.

Сводка моих оценок пространства, необходимого для хранения 2 граммов, на основе 100 файлов с 66 миллионами строк. Исключая пространство для индексов и общие накладные расходы на хранение (которые, в зависимости от СУБД, могут быть значительными).

                           row_len  gigabytes  joins
----------------------------------------------------
words        with counts     163.2      1,077      0
two integers with counts     128.0        845    2-5

words        without counts   10.2         67      0
two integers without counts    8           53    2-5

one integer  with counts      20          132      1
one integer  without counts    4           26 (for completeness, but not really useful)

Многотерабайтные дисковые массивы в наши дни не так уж и дороги. Вы все собираетесь зарабатывать на этом деньги?

person Mike Sherrill 'Cat Recall'    schedule 10.10.2011
comment
Очень хорошие моменты, иногда оптимизация того не стоит. Однако позвольте мне добавить еще немного пищи для размышлений. Стандартный столбец integer в PostgreSQL/MySQL использует 4 байта. Однако это правда, что это всего лишь ~ 2 миллиарда, чего недостаточно для всех 360 миллиардов граммов Google. Тем не менее, по большей части это просто шум/орфографические ошибки/смайлики, поскольку проекты WordNet и moby классифицировали только 200 тысяч английских слов. Итак, если я урежу результаты до 2 миллиардов слов, я смогу уменьшить размер хранилища, возможно, на 50%. Поиск также будет быстрее, поскольку сравнение целых чисел выполняется быстрее, чем строк. - person Xeoncross; 10.10.2011
comment
Поэтому, если бы я хотел найти все 2-граммы для данного слова, я бы сначала получил идентификатор слова SELECT id FROM word WHERE word = ?, а затем я мог бы найти в таблице 2-грамм пары для этого слова SELECT * FROM TWOGRAM WHERE first_word_id = ? OR second_word_id = ?. Это позволит базе данных выполнять поиск гораздо быстрее, чем по строке. Он также может хранить больше слов в памяти из-за уменьшенного размера целых чисел. Кстати, я, конечно, собираюсь разделить слово статистика, как вы упомянули, во вторую таблицу. Хорошая идея. - person Xeoncross; 10.10.2011
comment
Целые числа имеют около 4 миллиардов значений. В PostgreSQL нет целых чисел без знака, но нет никаких причин, по которым вы не можете использовать отрицательные числа. Не думайте, что целые числа с пятью соединениями будут быстрее, чем строки без соединений. Вам нужно протестировать его от начала до конца. - person Mike Sherrill 'Cat Recall'; 10.10.2011
comment
Да, я думал о 2B подписанных целых числах (ради PostgreSQL). Однако я не уверен, почему вы упоминаете соединения. В примере, который я привел, нет соединений. Возможно, у вас есть конкретная реализация, о которой вы думаете, которая нуждается в соединениях...? - person Xeoncross; 11.10.2011