Статьи

MongoDB Полнотекстовый поиск и регулярные выражения

Сегодня мы хотели бы познакомить вас с новым полнотекстовым поиском MongoDB и сравнить его возможности и производительность с простыми регулярными выражениями, которые в настоящее время являются современными для поиска в  MongoDB . Мы предоставим фрагменты кода, поясняющие, как использовать обе функции в приложении Java, а также эмпирическую оценку производительности.

Что такое полнотекстовый поиск MongoDB?

MongoDB Полнотекстовый поиск — это  новая функция  в MongoDB 2.4. Однако до сих пор он находится в бета-состоянии и не рекомендуется использовать в производственных системах.

Почему бы не продолжать использовать регулярные выражения?

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

Тем не менее, поиск с помощью регулярных выражений действительно легко реализовать с помощью Spring Data, как показано в следующем фрагменте кода:

import org.springframework.data.mongodb.core.query.*;
import org.springframework.data.mongodb.core.MongoOperations;

public List<Movie> searchInDescription(String searchString, int limit, int offset) {
   Criteria criteria = Criteria.where("description").regex(searchString);
   Query query = Query.query(criteria);
   // apply pagination, sorting would also be specified here
   query.limit(limit);
   query.skip(offset);
   return mongoOperations.find(query, Movie.class);
}

Как использовать полнотекстовый поиск MongoDB?

К сожалению, это немного сложнее, так как  Spring Data  еще не поддерживает эту функцию. Для реализации нашего собственного решения мы должны понять, как можно выполнить полнотекстовый поиск в оболочке Mongo:

> db.collection.runCommand("text", {search:"action", project:{"_id":1}})
{ "queryDebugString" : "day||||||",
  "language" : "english",
  "results" : [
     { "score" : 0.5089285714285714,
       "obj" : {
          "_id" : ObjectId("51c175a20364281420b1d17d")
          }
     }
  ],
  "stats" : {
     "nscanned" : 1,
     "nscannedObjects" : 0,
     "n" : 1,
     "nfound" : 1,
     "timeMicros" : 77
  },
  "ok" : 1
}

Команда возвращает один документ json со всеми объектами, которые соответствуют запросу, и некоторую статистику по только что выполненному поиску. Перевод этого в Java-код, который извлекает идентификаторы всех совпадений, работает следующим образом.

import com.mongodb.*;
import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.MongoOperations;

public Collection<ObjectId> findMatchingIds(String searchString) {
   CommandResult result = executeFullTextSearch(searchString);
   return extractSearchResultIds(result);
}

private CommandResult executeFullTextSearch(String searchString) {
   BasicDBObject textSearch = new BasicDBObject();
   textSearch.put("text", Movie.COLLECTION_NAME);
   textSearch.put("search", searchString);
   textSearch.put("limit", SEARCH_LIMIT); // override default of 100
   textSearch.put("project", new BasicDBObject("_id", 1));
   return mongoOperations.executeCommand(textSearch);
}

private Collection<ObjectId> extractSearchResultIds(CommandResult result) {
   Set<ObjectId> objectIds = new HashSet<ObjectId>();
   BasicDBList resultList = (BasicDBList) commandResult.get("results");
   Iterator<Object> it = resultList.iterator();
   while (it.hasNext()) {
      BasicDBObject resultContainer = (BasicDBObject) it.next();
      BasicDBObject resultObj = (BasicDBObject) resultContainer.get("obj");
      ObjectId resultId = (ObjectId) resultObj.get("_id");
      objectIds.add(resultId);
   }
   return objectIds;
}

Обратите внимание, что в поле для поиска отсутствует индикатор! Поскольку MongoDB поддерживает только один текстовый индекс на коллекцию, эта информация неявно указывается после ее определения в оболочке

db.collection.ensureIndex({"description":"text"})

или из приложения Java

mongoOperations.getCollection(Movie.COLLECTION_NAME)
   .ensureIndex(new BasicDBObject("description", "text"));

Чтобы предоставить результаты поиска с разбивкой по страницам и пользовательской сортировкой для уровня пользовательского интерфейса приложения, нам нужен еще один стандартный запрос Spring Data, который делает именно это.

import org.bson.types.ObjectId;
import org.springframework.data.mongodb.core.query.*;
import org.springframework.data.mongodb.core.MongoOperations;

public List<Movie> searchInDescription(String searchString, int limit, int offset) {
   Collection<ObjectId> searchResultIds = findMatchingIds(searchString);
   Criteria criteria = Criteria.where("_id").in(searchResultIds);
   Query query = Query.query(criteria);
   // apply pagination, sorting would also be specified here
   query.limit(limit);
   query.skip(offset);
   return mongoOperations.find(query, Movie.class);
}

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

Каковы ограничения полнотекстового поиска MongoDB?

Полнотекстовый поиск не работает должным образом для действительно больших наборов данных, поскольку все совпадения возвращаются как один документ, и команда не поддерживает параметр «пропуск» для получения результатов постранично. Несмотря на то, что поле «_id» не проецируется ни на что, огромный набор совпадений не будет возвращен полностью, если результат превысит 16 МБ Mongo на лимит документа.

Как работает полнотекстовый поиск MongoDB по сравнению с регулярными выражениями?

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

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

— «фильм», который обеспечивает 3533 совпадения с полнотекстовым поиском и 3317 с регулярными выражениями (число отличается из-за функциональности стебля полнотекстового поиска)

— «газета», которая выдает 318 совпадений с полнотекстовым поиском и 320 с регулярными выражениями

— «Майзи», который поставил 2 матча в обоих случаях

Следующая гистограмма иллюстрирует соответствующие характеристики для подсчета результатов и их получения:

MongoDB Полнотекстовый поиск и регулярные выражения

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

Давайте сначала объясним результаты поиска с помощью регулярных выражений. Время для подсчета количества совпадений среди 100 000 записей примерно соответствует 200 мс. Очевидно, что это время, необходимое для сканирования всего документа коллекции по документу, поскольку здесь нельзя использовать индекс. С другой стороны, время для извлечения одной страницы очень сильно возрастает для меньшего числа результатов. Это связано с тем, что MongoDB использует индекс для перебора всех документов в правильном порядке сортировки и может немедленно остановиться, как только будет найдено 15 записей для первой страницы. Для запроса с примерно 3500 совпадениями в 100 000 документов («фильм») ожидаемое значение сканируемых документов составляет всего 15 / 0,035 = 428,6, в то время как всю коллекцию необходимо проверять на очень редкий поисковый термин, такой как «Mayzie».

Объяснить эффективность полнотекстового поиска довольно просто. В этом случае MongoDB может использовать индекс, и, следовательно, запрос всегда эффективен. Требуется лишь немного больше времени для увеличения количества матчей. Здесь важно понять, что для извлечения первой страницы необходимо выполнить полнотекстовый поиск, извлечь все идентификаторы результатов в приложении и выполнить другой стандартный запрос, как описано выше. Время, необходимое для этапа извлечения, линейно увеличивается с количеством совпадений, что является основной причиной увеличения времени поиска для больших наборов результатов, даже если возвращено только 15 записей.

Что нужно для запуска этой демонстрации самостоятельно?

Наше демонстрационное приложение использует Spring, Spring Data, Apache Wicket, Gradle и MongoDB.

Для начала загрузите код с  https://github.com/comsysto/mongo-full-text-search-movie-showcase .

Чтобы запустить MongoDB с включенным полнотекстовым поиском, выключите ваш mongod, если он в данный момент работает, а затем введите команду:

mongod --setParameter textSearchEnabled=true

Кроме того, вы можете добавить эту строку в файл mongodb.conf для постоянного включения этой функции (не рекомендуется в производственной среде):

setParameter = textSearchEnabled=true

Если вы не установили Gradle, следуйте этому  руководству . Тогда команда

gradle clean build

Для запуска команды приложения

gradle jettyRun

Что может помочь, если у вас есть проблемы?

Если полнотекстовый поиск не настроен должным образом, вы всегда получите пустой список результатов независимо от того, какой термин вы искали. Кроме того, сообщение «### MongoDB Полнотекстовый поиск не работает должным образом — не может получить результаты». будет напечатан на консоли.

Такое поведение может иметь несколько причин:

  1. Вы не запустили свой сервер mongod с опцией textSearchEnabled = true, как описано выше.
  2. Вы указали более одного текстового индекса для коллекции, который не может быть обработан MongoDB. Вы можете посмотреть это, вызвав следующее в оболочке Монго:
use movie 
db.movie.getIndexes()

[больше проблем и ловушек, о которых мы знаем]

Как продлить демонстрацию?

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

Любые вопросы?

Если у вас есть какие-либо отзывы, пожалуйста, напишите  Christan.Kroemer@comsysto.com  или Elisabeth.Engel@comsysto.com !