Алгоритм поиска 10 лучших поисковых терминов - PullRequest
113 голосов
/ 16 июля 2010

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

"Вас попросили разработать какое-либо программное обеспечение для постоянного отображения 10 лучших поисковых терминов в Google. Вам предоставляется доступ к каналу, который предоставляет бесконечный поток поисковых терминов в реальном времени, которые в настоящее время ищутся в Google. Опишите, какой алгоритм и структуры данных, которые вы использовали бы для реализации этого. Вы должны разработать два варианта:

(i) Показать 10 самых популярных поисковых запросов за все время (т. Е. С тех пор, как вы начали читать канал).

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

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

Первая часть запрашивает 10 наиболее часто встречающихся элементов в непрерывно растущей подпоследовательности бесконечного списка. Я изучил алгоритмы выбора, но не смог найти какие-либо онлайн-версии для решения этой проблемы.

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

Проблема осложняется тем фактом, что список топ-10 постоянно обновляется, поэтому каким-то образом вам нужно вычислять топ-10 по скользящему окну.

Есть идеи?

Ответы [ 16 ]

53 голосов
/ 16 июля 2010

Обзор оценки частоты

Существуют некоторые хорошо известные алгоритмы, которые могут предоставлять оценки частоты для такого потока, используя фиксированный объем памяти. Одним из них является Frequent, от Misra and Gries (1982). Из списка n предметов он находит все предметы, которые встречаются более n / k раз, используя счетчики k - 1 . Это обобщение алгоритма Бойера и Мура Majority (Фишер-Зальцберг, 1982), где k равно 2. Манку и Мотвани LossyCounting (2002) и Метвалли SpaceSaving (2005) алгоритмы имеют аналогичные требования к пространству, но могут обеспечить более точные оценки при определенных условиях.

Важно помнить, что эти алгоритмы могут предоставлять только оценки частоты. В частности, оценка Misra-Gries может занижать фактическую частоту на (н / к) пунктов.

Предположим, что у вас был алгоритм, который мог бы положительно идентифицировать элемент только , если он встречается более 50% времени. Заполните этот алгоритм потоком N различных элементов, а затем добавьте еще N - 1 копий одного элемента, x , всего 2N - 1 шт. Если алгоритм сообщает вам, что x превышает 50% от общего количества, он должен был быть в первом потоке; если это не так, x не было в начальном потоке. Чтобы алгоритм сделал это определение, он должен хранить начальный поток (или некоторую сводку, пропорциональную его длине)! Таким образом, мы можем доказать себе, что пространство, необходимое для такого «точного» алгоритма, было бы & Omega; ( N ).

Вместо этого эти частотные алгоритмы, описанные здесь, дают оценку, идентифицирующую любой элемент, который превышает пороговое значение, наряду с некоторыми элементами, которые опускаются ниже его с определенным запасом. Например, алгоритм Majority , использующий один счетчик, всегда даст результат; если какой-либо элемент превышает 50% потока, он будет найден. Но это также может дать вам предмет, который встречается только один раз. Вы не узнаете, не сделав второй проход по данным (используя, опять же, один счетчик, но ища только этот элемент).

Частый алгоритм

Вот простое описание алгоритма Misra-Gries Frequent . Демейн (2002) и другие оптимизировали алгоритм, но это дает вам суть.

Укажите пороговую долю, 1 / k ; любой предмет, который встречается более н / к раз будет найден. Создать пустую карту (например, красно-черное дерево); ключи будут поисковыми терминами, а значения будут счетчиком для этого термина.

  1. Посмотрите на каждый предмет в потоке.
  2. Если термин существует на карте, увеличьте связанный счетчик.
  3. В противном случае, если на карте меньше k - 1 записей, добавьте термин на карту со счетчиком один.
  4. Однако, если на карте уже есть k - 1 записей, уменьшайте счетчик в каждой записи. Если какой-либо счетчик достигнет нуля во время этого процесса, удалите его с карты.

Обратите внимание, что вы можете обрабатывать бесконечное количество данных с фиксированным объемом памяти (только карта фиксированного размера). Объем требуемой памяти зависит только от порогового значения, а размер потока не имеет значения.

Подсчет поисков

В этом контексте, возможно, вы буферизуете один час поиска и выполняете этот процесс на данных этого часа. Если вы можете сделать второй проход по журналу поиска за этот час, вы сможете получить точное число появлений наиболее важных «кандидатов», определенных в первом проходе. Или, может быть, это нормально, чтобы сделать один проход и сообщить обо всех кандидатах, зная, что любой предмет, который должен быть там, включен, а любые дополнения - просто шум, который исчезнет в следующий час.

Любые кандидаты, которые действительно превышают порог интереса, сохраняются в виде сводки. Сохраняйте эти сводные данные за месяц, отбрасывая самые старые каждый час, и вы получите хорошее приближение к наиболее распространенным поисковым терминам.

46 голосов
/ 16 июля 2010

Ну, похоже, очень много данных, возможно, с непомерной стоимостью хранения всех частот. Когда объем данных настолько велик , что мы не можем надеяться сохранить его все, мы входим в область алгоритмов потока данных .

Полезная книга в этой области: Мутукришнан - «Потоки данных: алгоритмы и приложения»

Тесно связанная с этим проблема, которую я выбрал из приведенного выше: Манку, Мотвани - «Приблизительная частота для потоков данных» [pdf]

Кстати, Мотвани из Стэнфорда (правка) был автором очень важной "Рандомизированных алгоритмов" книги. 11-я глава этой книги посвящена этой проблеме . Редактировать: Извините, неверная ссылка, эта конкретная глава посвящена другой проблеме. После проверки я рекомендую раздел 5.1.2 книги Мутукришнана , доступной онлайн.

Хех, хороший вопрос для интервью.

19 голосов
/ 16 июля 2010

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

Входные данные

Входные данные представляют собой бесконечный поток английских слов или фраз (мы называем их tokens).

Выход

  1. Вывод top N токенов, которые мы видели до сих пор (из всех токенов, которые мы видели!)
  2. Output topN токенов в историческом окне, скажем, в последний день или на прошлой неделе.

Применение этого исследования состоит в том, чтобы найти горячую тему или тенденции темы в Twitter или Facebook.У нас есть сканер, который сканирует веб-сайт, который генерирует поток слов, которые поступают в систему.Затем система будет выводить слова или фразы с максимальной частотой либо в целом, либо исторически.Представьте, что в последние пару недель фраза «Кубок мира» будет многократно появляться в Twitter.Так же как и «осьминог Павел».:)

Строка в целые числа

Система имеет целочисленный идентификатор для каждого слова.Хотя в Интернете есть почти бесконечные возможные слова, но после накопления большого набора слов возможность поиска новых слов становится все ниже и ниже.Мы уже нашли 4 миллиона разных слов и присвоили уникальный идентификатор каждому.Весь этот набор данных может быть загружен в память в виде хэш-таблицы, занимая примерно 300 МБ памяти.(Мы реализовали нашу собственную хеш-таблицу. Реализация Java требует огромных затрат памяти)

Каждая фраза может быть идентифицирована как массив целых чисел.

Это важно, потому что сортировка и сравнение по целым числам намного быстрее , чем по строкам.

Архивные данные

Система хранит архивные данные для каждого токена.В основном это пары (Token, Frequency).Однако таблица, в которой хранятся данные, будет настолько огромной, что нам придется разделить таблицу физически.Схема однократного разбиения основана на nграммах токена.Если токен является одним словом, это 1 грамм.Если токен состоит из двух слов, это 2грамма.И это продолжается.Примерно в 4 грамма у нас есть 1 миллиард записей с размером таблицы около 60 ГБ.

Обработка входящих потоков

Система будет поглощать входящие предложения, пока память не будет полностью использована (Yaнам нужен MemoryManager).После принятия N предложений и сохранения в памяти система делает паузу и начинает разбивать каждое предложение на слова и фразы.Каждый токен (слово или фраза) считается.

Для очень частых токенов они всегда хранятся в памяти.Для менее частых токенов они сортируются на основе идентификаторов (помните, что мы переводим строку в массив целых чисел) и сериализуем в файл на диске.

(Однако для вашей проблемы, поскольку вы учитываете только слова, вы можете поместить все частотно-частотные карты только в память. Тщательно разработанная структура данных потребует только 300 МБ памяти для 4 миллионов различных слов. Некоторыеподсказка: используйте ASCII char для представления строк), и это очень приемлемо.

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

Конец дня

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

Система сбросит карту памяти в файл на диске (отсортируйте ее).Теперь проблемой становится слияние набора отсортированных файлов на диске.Используя аналогичный процесс, мы получили бы один отсортированный дисковый файл в конце.

Затем последняя задача - объединить отсортированный дисковый файл в архивную базу данных.В зависимости от размера архивной базы данных алгоритм работает, как показано ниже, если он достаточно большой:

   for each record in sorted disk file
        update archive database by increasing frequency
        if rowcount == 0 then put the record into a list
   end for

   for each record in the list of having rowcount == 0
        insert into archive database
   end for

Интуиция заключается в том, что через некоторое время количество вставок станет все меньше и меньше.Все больше и больше будет только обновление.И это обновление не будет оштрафовано индексом.

Надеюсь, это все объяснение поможет.:)

4 голосов
/ 16 июля 2010

Вы можете использовать хеш-таблицу в сочетании с бинарным деревом поиска . Реализуйте словарь <search term, count>, который сообщает вам, сколько раз был выполнен поиск по каждому поисковому запросу.

Очевидно, что итерирование всей хеш-таблицы каждый час, чтобы получить топ-10, является очень плохим. Но мы говорим об Google, поэтому вы можете предположить, что все первые десять получат, скажем, более 10 000 просмотров (хотя, вероятно, это намного больше). Поэтому каждый раз, когда количество поисковых терминов превышает 10 000, вставьте его в BST. Затем каждый час вам нужно только получить первые 10 из BST, которые должны содержать относительно немного записей.

Это решает проблему топ-10 всех времен.


Действительно сложная часть связана с тем, что один член занимает место в ежемесячном отчете (например, «переполнение стека» может иметь 50 000 обращений за последние два месяца, но только 10 000 за последний месяц, тогда как «амазонка») может иметь 40 000 за последние два месяца, но 30 000 за прошедший месяц. Вы хотите, чтобы «amazon» появлялся до «переполнения стека» в вашем ежемесячном отчете). Чтобы сделать это, я бы сохранил для всех основных (более 10 000 поисковых запросов за все время) 30-дневный список, в котором указано, сколько раз этот термин был найден в каждый день. Список будет работать как очередь FIFO: вы удаляете первый день и вставляете новый каждый день (или каждый час), но затем вам может понадобиться хранить больше информации, что означает больше памяти / места. Если память не проблема, сделайте это, иначе пойти на то «приближение», о котором они говорят).

Это похоже на хорошее начало. Затем вы можете позаботиться о сокращении терминов, которые имеют> 10 000 показов, но их не было много в течение длительного времени, и тому подобное.

3 голосов
/ 16 июля 2010

дело I)

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

O (1) поиск списка первой десятки и максимальная O (log (n)) вставка в хеш-таблицу (при условии коллизий, управляемых самобалансирующимся двоичным деревом).

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

Кроме того, мы также поддерживаем список «часов» в виде списка FIFO (очередь). Каждый элемент 'hour' будет содержать список всех поисков, выполненных в этот конкретный час. Так, например, наш список часов может выглядеть так:

Time: 0 hours
      -Search Terms:
          -free stuff: 56
          -funny pics: 321
          -stackoverflow: 1234
Time: 1 hour
      -Search Terms:
          -ebay: 12
          -funny pics: 1
          -stackoverflow: 522
          -BP sucks: 92

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

Итак, допустим, что мы находимся в 721 час и готовы посмотреть на первый час в нашем списке (выше). Мы сократили бы количество свободного материала на 56 в хеш-таблице, смешные картинки на 321 и т. Д., А затем полностью удалили бы час из списка, поскольку нам больше не нужно было бы на него смотреть.

Причина, по которой мы поддерживаем отсортированный список всех терминов, который допускает быстрые запросы, заключается в том, что каждый час после прохождения терминов поиска, начиная с 720 часов назад, мы должны убедиться, что список из первой десятки остается отсортированным. Так, например, когда мы уменьшаем значение «бесплатные вещи» на 56 в хеш-таблице, мы проверяем, где они теперь находятся в списке. Поскольку это самобалансирующееся двоичное дерево, все это может быть успешно выполнено за O (log (n)).


Редактировать: жертвуя точностью ради космоса ...

Может быть полезно также реализовать большой список в первом, как во втором. Затем мы могли бы применить следующую оптимизацию пространства в обоих случаях: Запустить задание cron, чтобы удалить все, кроме верхних x элементов в списке. Это снизит требования к пространству (и, как следствие, ускорит выполнение запросов в списке). Конечно, это приведет к приблизительному результату, но это разрешено. x можно рассчитать перед развертыванием приложения на основе доступной памяти и динамически корректировать, если станет доступно больше памяти.

2 голосов
/ 17 июля 2010

А как насчет адаптации "алгоритма замены страницы часов" (также известного как "второй шанс")?Я могу представить, что он будет работать очень хорошо, если поисковые запросы распределены равномерно (это означает, что большинство поисковых терминов появляются регулярно, а не 5 миллионов раз подряд, а затем никогда больше).

Вот визуальное представление алгоритма:clock page replacement algorithm

2 голосов
/ 16 июля 2010

Топ 10 поисковых запросов за прошедший месяц

Использование эффективного индексирования памяти / структуры данных, такой как плотно упакованные попытки (из записей в Википедии попытки ) приблизительно определяет некоторую связь между требованиями к памяти и n - количеством терминов.

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

Здесь также есть предположение, которое интерпретирует «последний месяц» как фиксированное окно. Но даже если месячное окно сдвигается, вышеуказанная процедура показывает принцип (скольжение может быть аппроксимировано фиксированными окнами заданного размера).

Это напоминает мне о базе данных циклического перебора за исключением того, что некоторые статистические данные рассчитываются за «все время» (в том смысле, что сохраняются не все данные; rrd консолидирует периоды времени без учета деталей путем усреднения, суммируя или выбирая максимальные / минимальные значения, в данной задаче утраченная деталь - это информация о низкочастотных элементах, что может привести к ошибкам).

Предположение 1

Если мы не можем держать идеальную статистику за весь месяц, то мы должны быть в состоянии найти определенный период P, для которого мы должны быть в состоянии хранить идеальную статистику. Например, предполагая, что у нас есть точная статистика за некоторый период времени P, который входит в месяц n раз.
Идеальная статистика определяет функцию f(search_term) -> search_term_occurance.

Если мы сможем сохранить в памяти все n таблицы совершенных характеристик, то скользящая месячная статистика может быть рассчитана следующим образом:

  • добавить статистику для нового периода
  • убрать статистику за самый старый период (поэтому мы должны сохранить n таблицы идеальных характеристик)

Однако, если мы будем хранить только топ-10 на агрегированном уровне (ежемесячно), мы сможем отбросить много данных из полной статистики за фиксированный период. Это уже дает рабочую процедуру, которая установила (предполагая верхнюю границу таблицы идеальных показателей для периода P) требования к памяти.

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

Это можно компенсировать, сохраняя информацию о более чем 10 лучших терминах, например, 100 лучших, надеясь, что первые 10 будут правильными.

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

(Решая, какие записи должны стать частью статистики, можно также отслеживать и отслеживать тенденции; например, если линейная экстраполяция вхождений в каждом периоде P для каждого термина говорит вам, что термин станет значимым через месяц или два, вы уже можете начать отслеживать его. Аналогичный принцип применяется для удаления поискового термина из отслеживаемого пула.)

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

Также мог бы существовать другой подход, который не фиксирован в объеме памяти (строго говоря, ни то, что выше), который бы определял минимальное значение с точки зрения случаев / периода (день, месяц, год, все время), для которого вести статистику Это может гарантировать максимальную ошибку в каждой статистике во время агрегации (см. Циклический перебор снова).

2 голосов
/ 16 июля 2010

Точное решение

Во-первых, решение, которое гарантирует правильные результаты, но требует много памяти (большая карта).

вариант "За все время"

Ведение хэш-карты с запросами в качестве ключей и их количеством в качестве значений. Кроме того, сохраните список из 10 самых частых запросов на данный момент и счет 10-го наиболее частого числа (порог).

Постоянно обновляйте карту по мере чтения потока запросов. Каждый раз, когда число превышает текущее пороговое значение, выполните следующие действия: удалите 10-й запрос из списка «Топ-10», замените его на только что обновленный запрос и обновите пороговое значение.

Вариант "Прошлый месяц"

Сохраните тот же список «Топ 10» и обновите его так же, как указано выше. Также сохраняйте аналогичную карту, но на этот раз сохраняйте векторы 30 * 24 = 720 отсчетов (по одному на каждый час) в качестве значений. Каждый час выполняйте следующие действия для каждого ключа: удаляйте самый старый счетчик из вектора, добавляйте новый (инициализированный в 0) в конце. Удалите ключ с карты, если вектор равен нулю. Кроме того, каждый час вы должны рассчитывать список «Топ-10» с нуля.

Примечание: Да, на этот раз мы храним 720 целых вместо одного, но ключей намного меньше (у исторического варианта действительно длинный хвост).

Аппроксимация

Эти приближения не гарантируют правильное решение, но они занимают меньше памяти.

  1. Обрабатывать каждый N-й запрос, пропуская остальные.
  2. (Только для варианта на все времена). На карте должно быть не более M пар ключ-значение (M должно быть настолько большим, насколько вы можете себе позволить). Это своего рода кеш LRU: каждый раз, когда вы читаете запрос, которого нет на карте, удаляйте наименее последний использованный запрос с номером 1 и заменяйте его текущим обработанным запросом.
2 голосов
/ 16 июля 2010

Грубое мышление ...

Для топ-10 за все время

  • Использование коллекции хэшей, в которой хранится счетчик для каждого термина (условия очистки и т. Д.)
  • Сортированный массив, который содержит текущие топ-10, термин / число добавляется в этот массив всякий раз, когда число термов становится равным или большим, чем наименьшее число в массиве

Для ежемесячных топ-10, обновляемых ежечасно:

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

Ошибка ... имеет смысл? Я не думал об этом, как в реальной жизни

Ах да, забыл упомянуть, что ежечасное «копирование / выравнивание», необходимое для ежемесячной статистики, может фактически использовать тот же код, который использовался для 10 лучших за все время, хороший побочный эффект.

0 голосов
/ 10 декабря 2017

Проблема не всегда решаема, когда у вас фиксированный объем памяти и «бесконечный» (очень очень большой) поток токенов.

Грубое объяснение ...

Чтобы понять почему, рассмотрим поток токенов, который имеет определенный токен (т. Е. Слово) на каждые N токенов во входном потоке.

Также предположим, что память может хранить ссылки (слово id и число) набольшинство M токенов.

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

Это не зависит от подробностей алгоритма top-N.Это зависит только от предела M.

Чтобы понять, почему это так, рассмотрим входящий поток, состоящий из групп из двух идентичных токенов:

T a1 a2 a3 ... a-M T b1 b2 b3 ... b-M ...

, где a и bвсе действительные токены не равны T.

Обратите внимание, что в этом потоке T появляется дважды для каждого ai и bi.Тем не менее, он появляется достаточно редко, чтобы его можно было сбросить из системы.

Начиная с пустой памяти, первый токен (T) займет место в памяти (ограничено символом M).Тогда a1 будет занимать слот вплоть до a- (M-1), когда исчерпан M.

Когда прибывает aM, алгоритм должен отбросить один символ, поэтому пусть это будет T. Следующий символбудет b-1, что приведет к сбросу a-1 и т. д.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...