Статьи

Читатели почти в реальном времени с Lucene’s SearcherManager и NRTManager

В прошлый раз я описал полезный класс SearcherManager, появившийся в следующей (3.5.0) версии Lucene, для периодического повторного открытия IndexSearcher, когда его необходимо разделить нескольким потокам. Этот класс представляет очень простой API получения / выпуска, скрывающий потокобезопасные сложности открытия и закрытия базовых IndexReaders.

Но в этом примере использовался IndexReader не в режиме, близком к реальному времени (NRT), который имеет относительно высокое время обработки, чтобы изменения индекса становились видимыми, поскольку сначала необходимо вызвать IndexWriter.commit.

Если у вас есть доступ к IndexWriter, который активно меняет индекс (т. Е. Он находится в той же JVM, что и ваши поисковики), используйте вместо этого читатель NRT! Читатели NRT позволяют вам отделить
долговечность от сбоев оборудования / ОС от
видимостиизменений в новом IndexReader. Как часто вы фиксируете (для долговечности) и как часто вы открываете (чтобы увидеть новые изменения) становятся полностью отдельными решениями. Эта
модель контролируемой согласованности, которую демонстрирует Lucene, представляет собой хорошее сочетание «лучшего из обоих миров» между традиционными
моделями
немедленной и
возможной согласованности.

Поскольку повторное открытие считывателя NRT обходит дорогостоящий коммит и разделяет некоторые структуры данных непосредственно в ОЗУ вместо записи / чтения в / из файлов, это обеспечивает
чрезвычайно быстрое время выполнения изменений в отображении изменений индекса, видимых поисковикам. Частые повторные открытия, такие как каждые 50 миллисекунд, даже при относительно высоких скоростях индексации, легко достижимы на современном оборудовании.

К счастью, использовать SearcherManager с читателями NRT тривиально: используйте конструктор, который использует IndexWriter вместо Directory:

  boolean applyAllDeletes = true;
  ExecutorService es = null;
  SearcherManager mgr = new SearcherManager(writer, applyAllDeletes,
                                            new MySearchWarmer(), es);

Это сообщает SearcherManager, что его источником для новых IndexReaders является предоставленный экземпляр IndexWriter (вместо экземпляра Directory). После этого используйте SearcherManager,
как и раньше .

Как правило, для логического applyAllDeletes устанавливается значение true, означающее, что каждый вновь открытый считыватель должен применять все предыдущие операции удаления (deleteDocuments или updateDocument / s) вплоть до этой точки.

Иногда ваше использование не требует удаления. Например, возможно, вы со временем индексируете несколько версий каждого документа, всегда удаляя старые версии, но во время поиска у вас есть какой-то способ игнорировать старые версии. Если это так, вы можете вместо этого передать applyAllDeletes = false. Это значительно ускорит время выполнения, так как поиск первичного ключа, необходимый для разрешения удалений, может быть дорогостоящим. Однако, если вы используете транк Lucene (который в конечном итоге будет выпущен как 4.0), другой вариант — использовать MemoryCodec в поле id, чтобы
значительно сократить время поиска первичного ключа .

Обратите внимание, что некоторые или даже все предыдущие удаления могут быть применены, даже если вы передадите false. Кроме того, ожидающие удаления никогда не
теряютсяесли вы передадите false: они останутся в буфере и все равно будут применены.

Если у вас есть какие-то поиски, которые могут допускать непримененные удаления, а другие нет, то вполне можно создать два SearcherManager, один из которых применяет удаление, а другой — нет.

Если вы передаете ненулевое значение ExecutorService, то каждый сегмент в индексе можно искать одновременно; это способ получить параллелизм внутри одного поискового запроса. Большинство приложений не требуют этого, потому что параллелизма между несколькими запросами достаточно. Также не ясно, является ли это эффективным в целом, поскольку оно добавляет накладные расходы на сегмент, а доступный параллелизм является функцией вашей структуры индекса. Напротив, полностью оптимизированный индекс не будет иметь параллелизма! Большинство приложений должны пройти ноль.

NRTManager

Что если вам нужно быстрое время выполнения считывателей NRT, но вам нужен контроль, когда определенные изменения индекса становятся видимыми для определенных поисков? Используйте NRTManager!

NRTManager удерживает предоставленный вами экземпляр IndexWriter, а затем предоставляет те же API для внесения изменений в индекс (addDocument / s, updateDocument / s, deleteDocuments). Эти методы перенаправляют в базовый IndexWriter, но затем возвращают
токен
генерации (длинный Java), который вы можете сохранить после внесения любых изменений. Поколение только увеличивается со временем, поэтому, если вы вносите группу изменений, просто сохраните поколение, возвращенное после последнего внесенного вами изменения.

Затем, когда для данного поискового запроса требуются определенные изменения, передайте это поколение обратно в NRTManager, чтобы получить искатель, который гарантированно отразит все изменения для этого поколения.

Вот один пример использования: допустим, на вашем сайте есть форум, а вы используете Lucene для индексации и поиска по всем сообщениям на форуме. Внезапно пользователь Алиса выходит в сеть и добавляет новое сообщение; на своем сервере вы берете текст из сообщения Алисы и добавляете его в качестве документа в индекс, используя NRTManager.addDocument, сохраняя возвращенное поколение. Если она добавит несколько постов, просто сохрани последнее поколение.

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

Но сделайте вид, что сейчас вы открываете относительно редко (скажем, каждые 5 или 10 секунд), и вы должны быть уверены, что поиск Алисы охватывает ее сообщения, поэтому вы вызываете NRTManager.waitForGeneration, чтобы получить SearcherManager для поиска. Если последний поисковик уже покрывает запрошенное поколение, метод немедленно возвращается. В противном случае он блокирует, запрашивая повторное открытие (см. Ниже), пока требуемое поколение не станет видимым в поисковике, а затем возвращает его.

Если какой-то другой пользователь, например Боб, не добавляет никаких сообщений и запускает поиск, вам не нужно ждать, пока поколение Алисы станет видимым при получении искателя, поскольку это гораздо менее важно, когда изменения Алисы сразу становятся видимыми для Боба. , Между отправкой сообщений Алисы и поиском Боба (обычно!) Нет причинно-следственной связи, поэтому Боб может использовать самый последний поисковик.

Другим вариантом использования является верификатор индекса, где вы индексируете документ, а затем сразу же ищете его, чтобы выполнить сквозную проверку того, что документ «сделал это» правильно в индексе. Этот немедленный поиск должен сначала дождаться, когда возвращенное поколение станет доступным.

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

Но: не злоупотребляйте этим! Может возникнуть соблазн всегда ждать последнего поколения, которое вы проиндексировали для всех поисков, но это приведет к очень низкой пропускной способности поиска на параллельном оборудовании, поскольку все поиски будут сгруппированы, ожидая повторного открытия. При правильном использовании, только небольшое подмножество поисков должно ждать определенного поколения, такого как Алиса; остальные будут просто использовать самый последний поисковик, такой как Боб.

Управление повторным открытием немного сложнее с NRTManager, так как вы должны открывать с более высокой частотой всякий раз, когда поиск ожидает определенного поколения. Для решения этой проблемы есть полезный класс NRTManagerReopenThread; используйте это так:

  double minStaleSec = 0.025;
  double maxStaleSec = 5.0;
  NRTManagerReopenThread thread = new NRTManagerReopenThread(
                                       nrtManager,
           maxStaleSec,
           minStaleSec);
  thread.start();
  ...
  thread.close();

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

Параметр maxStaleSec устанавливает нижнюю границу частоты повторного открытия. Это используется для периодического «обычного» повторного открытия, когда нет запроса, ожидающего определенного поколения (Боб, выше); это означает, что любые изменения, внесенные в индекс более чем приблизительно 5,0 секунд назад, будут видны при поиске Боба. Обратите внимание, что эти параметры являются приблизительными целями, а не жесткими гарантиями на время оборота считывателя. Обязательно вызовите thread.close (), когда вы закончите повторное открытие (например, при закрытии приложения).

Вы также можете свободно использовать свою собственную стратегию для вызова возможно, открыть; Вам не нужно использовать NRTManagerReopenThread. Просто помните, что сделать это правильно, особенно когда поиски ожидают определенных поколений, может быть сложно!

Источник: http://blog.mikemccandless.com/2011/11/near-real-time-readers-with-lucenes.html