Алгоритм поиска 10 самых популярных поисковых запросов

115

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

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

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

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

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

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

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

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

Любые идеи?

дель
источник
11
@BlueRaja - Это не дурацкий вопрос на собеседовании, это плохая интерпретация со стороны OP. Он не запрашивает наиболее частые элементы в бесконечном списке, он запрашивает наиболее частые элементы конечной подпоследовательности бесконечного списка. Продолжая аналогию,what is the most frequent item in the subsequence [2; 2; 3; 3; 3; 4; 4; 4; 4; 5; 5] of your sequence?
IVlad
3
@BlueRaja - Это, конечно, сложный вопрос, но я не понимаю, почему он глуп - он кажется типичным для типичной проблемы, с которой сталкиваются компании с огромными наборами данных. @IVlad - Исправлено по вашему предложению, плохая формулировка с моей стороны!
дель

Ответы:

47

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

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

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

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

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

Димитрис Андреу
источник
2
+1 Очень интересный материал, на сайтах должна быть возможность пометить что-то «читать». Спасибо, что поделился.
Ramadheer Singh
@Gollum: у меня есть папка для чтения в моих закладках; ты мог бы просто сделать это. Я знаю, что эти ссылки добавляются ко мне :)
Кэм,
+1. Алгоритмы потоковой передачи - это как раз тема здесь, и книга Мутху (единственная книга, написанная до сих пор, AFAIK) великолепна.
ShreevatsaR
1
+1. Связано: en.wikipedia.org/wiki/Online_algorithm . Кстати, Motwani недавно умер, так что, возможно , был автором более точным.
Очень странно. Я знал его по книге, но он определенно должен был стать более известным благодаря этому: «Мотвани был одним из соавторов (вместе с Ларри Пейджем, Сергеем Брином и Терри Виноградом) влиятельной ранней статьи об алгоритме PageRank, основа методов поиска Google ". ( en.wikipedia.org/wiki/Rajeev_Motwani )
Димитрис Андреу
55

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

Есть несколько хорошо известных алгоритмов, которые могут предоставить оценки частоты для такого потока, используя фиксированный объем памяти. Один из них - часто, Мисра и Грис (1982). Из списка из n элементов он находит все элементы, которые встречаются более n / k раз, используя счетчики k - 1 . Это обобщение алгоритма большинства Бойера и Мура (Fischer-Salzberg, 1982), где k равно 2. Алгоритмы LossyCounting (2002) Манку и Мотвани и SpaceSaving (2005) Метвалли имеют схожие требования к пространству, но могут обеспечить более точные оценки при определенных условиях. условия.

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

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

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

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

Вот простое описание Мишра-Грис Частого алгоритм. Demaine (2002) и другие оптимизировали алгоритм, но это дает вам суть.

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

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

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

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

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

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

Эриксон
источник
Я считаю, что это решение может действовать как фильтр, уменьшая количество интересующих вас поисковых запросов. Если термин попадает на карту, начните отслеживать его фактическую статистику, даже если он выпадает с карты. Затем вы можете пропустить второй проход по данным и составить отсортированную топ-10 из собранной вами ограниченной статистики.
Dolph
Мне нравится элегантный способ удаления из дерева малоизвестных терминов путем уменьшения счетчиков. Но когда карта "заполнится", не потребуется ли для этого шаг уменьшения для каждого нового поискового запроса? И как только это начнется, не приведет ли это к тому, что новые поисковые запросы будут быстро удалены с карты, прежде чем у них появится шанс для того, чтобы их счетчики увеличились в достаточной степени?
дель
1
@del - имейте в виду, что этот алгоритм предназначен для поиска терминов, превышающих заданную пороговую частоту, не обязательно для поиска наиболее распространенных терминов. Если наиболее распространенные термины опускаются ниже указанного порога, они, как правило, не будут найдены. Ваше беспокойство по поводу удаления новых терминов "слишком быстро" может быть связано с этим делом. Один из способов взглянуть на это: есть реальные «сигналы» в популярности, они будут заметно выделяться из «шума». Но иногда сигналы не обнаруживаются, просто статический случайный поиск.
erickson
@erickson - Правильно - я понимаю, что в этом алгоритме предполагается, что первые 10 слов равномерно распределены в окне измерения. Но пока вы сохраняете окно измерения достаточно маленьким (например, 1 час), это, вероятно, будет правильным предположением.
дель
1
@erickson, хотя равномерное распределение не является обязательным, мне интересно, как это будет работать в более реалистичном распределении (степенной закон, Zipf). Предположим, что у нас есть N различных слов, и оставим красно-черное дерево емкости K, надеясь, что в итоге получится K наиболее часто встречающихся терминов. Если совокупная частота терминов (N - K) слов больше, чем совокупная частота K наиболее часто встречающихся слов, дерево в конечном итоге гарантированно будет содержать мусор. Вы согласны?
Димитрис Андреу
19

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

Вход

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

Выход

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

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

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

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

Тогда каждую фразу можно идентифицировать как массив целых чисел.

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

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

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

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

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

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

(Однако для вашей проблемы, поскольку вы считаете только слова, вы можете поместить всю карту частотности слов только в память. Тщательно разработанная структура данных будет занимать только 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

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

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

SiLent SoNG
источник
Я не понимаю. Какого рода значимую сортировку или сравнения можно было бы сделать в целочисленных идентификаторах слов? Разве числа не произвольны?
Димитрис Андреу
Кроме того, подсчет частот слов - это самый первый пример в статье Google MapReduce ( labs.google.com/papers/mapreduce.html ), решение которой масштабируемо с помощью нескольких строк. Вы даже можете переместить свои данные в приложение google angine и сделать такой MapReduce ( code.google.com/p/appengine-mapreduce )
Димитрис Андреу,
@ Димитрис Андреу: Сортировка по целым числам будет быстрее по строкам. Это потому, что сравнение двух целых чисел происходит быстрее, чем сравнение двух строк.
SiLent SoNG
@Dimitris Andreou: mapreduce Google - хороший распределенный подход к решению этой проблемы. Ах! Спасибо за предоставленные ссылки. Да, было бы неплохо сортировать на нескольких машинах. Хороший подход.
SiLent SoNG
@ Димитрис Андреу: До сих пор я рассматривал подход к сортировке только на одной машине. Какая хорошая идея для сортировки по распределению.
SiLent SoNG
4

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

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

Это решает проблему попадания в топ-10 за все время.


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

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

IVlad
источник
3

случай i)

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

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

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

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

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 и т. Д., А затем полностью удалили бы час 0 из списка, так как нам больше не нужно будет смотреть на него снова.

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


Изменить: жертвовать точностью ради места ...

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

кулачок
источник
2

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

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

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

Для 10 лучших за месяц, обновляемых ежечасно:

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

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

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

Р. Хилл
источник
2

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

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

Вариант "на все времена"

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

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

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

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

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

приближения

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

  1. Обрабатывать каждый N-й запрос, пропуская остальные.
  2. (Только для постоянного варианта). Храните на карте не более M пар "ключ-значение" (M должно быть настолько большим, насколько вы можете себе позволить). Это своего рода кеш LRU: каждый раз, когда вы читаете запрос, которого нет на карте, удалите последний использованный запрос со счетом 1 и замените его текущим обрабатываемым запросом.
Боло
источник
Мне нравится вероятностный подход в приближении 1. Но, используя приближение 2 (кэш LRU), что произойдет, если термины, которые изначально не были очень популярны, позже станут популярными? Разве они не будут отбрасываться каждый раз при добавлении, поскольку их количество будет очень низким?
дель
@del Вы правы, второе приближение будет работать только для определенных потоков запросов. Это менее надежно, но в то же время требует меньше ресурсов. Примечание: вы также можете комбинировать оба приближения.
Bolo
2

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

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

Вот визуальное представление алгоритма: алгоритм замены страницы часов

Дэйв О.
источник
0

Сохраните количество поисковых запросов в гигантской хеш-таблице, где каждый новый поиск приводит к увеличению определенного элемента на единицу. Следите за 20 или около того поисковыми запросами; когда элемент на 11-м месте увеличивается, проверьте, нужно ли ему поменять местами позиции с # 10 * (нет необходимости держать 10 лучших отсортированных; все, что вас волнует, - это провести различие между 10-м и 11-м).

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

простой эфир
источник
Вы захотите ограничить размер своей хеш-таблицы. Что, если вы получите поток уникальных поисковых запросов? Вы должны быть уверены, что не помешаете себе заметить термин, который ищут регулярно, но нечасто. Со временем это могло бы стать самым популярным поисковым запросом, особенно если все остальные поисковые запросы относятся к «текущим событиям», т.е. искали много сейчас, но не так много на следующей неделе. На самом деле, подобные соображения могут быть приблизительными, которые вы хотели бы сделать. Обоснуйте их, сказав, что мы не поймем такие вещи, потому что это делает алгоритм более дорогим во времени / пространстве.
cape1232
Я почти уверен, что у Google есть подсчет всего - хотя некоторые подсчеты не ведутся статически, а рассчитываются по мере необходимости.
Ether
0

иногда лучший ответ - «не знаю».

Я сделаю более глубокий удар. Моим первым побуждением было бы передать результаты в Q. Процесс будет постоянно обрабатывать элементы, поступающие в Q. Процесс будет поддерживать карту

срок -> количество

каждый раз, когда обрабатывается Q-элемент, вы просто просматриваете поисковый запрос и увеличиваете счетчик.

В то же время я буду вести список ссылок на 10 самых популярных записей на карте.

Для записи, которая в настоящее время реализована, посмотрите, не превышает ли ее счетчик счетчика наименьшей записи в первой десятке (если ее еще нет в списке). Если это так, замените наименьшее записью.

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

Они не ждут решения, они хотят посмотреть, сможете ли вы подумать. Вам не нужно сразу писать решение ...

hvgotcodes
источник
12
Структура данных называется a queue, Qэто буква :).
IVlad
3
Если бы я проводил интервью, «я не знаю, <стоп>» определенно был бы не лучшим ответом. Импровизировать. Если не знаете, разберитесь - или хотя бы попробуйте.
Стивен
на интервью, когда я 5 раз вижу кого-то с спящим режимом на его 7-страничном резюме, и они не могут сказать мне, что такое ORM, я немедленно завершаю интервью. Лучше бы они не помещали это в свое резюме, а просто говорили: «Я не знаю». Никто не знает всего. @IVIad, я притворялся разработчиком на C и пытался сэкономить биты ...;)
hvgotcodes
0

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

Алгоритм прост, но недостатком будет больший расход памяти и времени.

Джесси Яшински
источник
0

Как насчет использования Splay Tree с 10 узлами? Каждый раз, когда вы пытаетесь получить доступ к значению (поисковому запросу), которого нет в дереве, выбрасывайте любой лист, вставляйте вместо него значение и обращайтесь к нему.

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

редактировать

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

Дэйв О.
источник
0

Не знаю, правильно я это понимаю или нет. Мое решение использует кучу. Из-за 10 самых популярных элементов поиска я создаю кучу размером 10. Затем обновляю эту кучу с помощью нового поиска. Если частота нового поиска превышает верхнюю часть кучи (Max Heap), обновите ее. Откажитесь от того, с наименьшей частотой.

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

Крис
источник
0

Используйте cm-sketch для хранения количества всех поисков с самого начала, оставьте min-heap размером 10 с ним для топ-10. Для ежемесячного результата сохраняйте 30 cm-sketch / hash-table и min-heap с ним, каждое начало подсчет и обновление за последние 30, 29 .., 1 день. По прошествии дня очистите последнее и используйте его как день 1. То же самое для почасового результата, сохраните 60 хеш-таблицы и минимальную кучу и начните отсчет последних 60, 59, ... 1 минуты. По прошествии минуты очистите последнее и используйте его как минуту 1.

Ежемесячный результат точен в пределах 1 дня, ежечасный результат точен в диапазоне 1 мин.

Цзинъи Фанг
источник
0

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

Примерное объяснение ...

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

Также предположим, что память может хранить ссылки (идентификатор слова и количество) не более чем на 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 не будет оставаться резидентным в памяти достаточно долго, чтобы создать реальный счет. Короче говоря, любой алгоритм пропустит токен с достаточно низкой локальной частотой, но с высокой глобальной частотой (по длине потока).

Дэвид Маркус
источник