Полнотекстовый поиск в MongoDB

Материал из Национальной библиотеки им. Н. Э. Баумана
Последнее изменение этой страницы: 13:38, 13 мая 2018.

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

Принцип работы

Используя полнотекстовый поиск, мы можете определить текстовый индекс на любом поле документа, значением которого является строка или массив строк. Когда мы создаем текстовый индекс на поле, MongoDB создает токены и стеммы для текстового содержимого этого поля, и в соответствии этому настраивает индексы. MongoDB сам отбрасывает стоп-слова - слова, знаки, символы, которые самостоятельно не несут никакой смысловой нагрузки. Список стоп-слов можно посмотреть на github в файлах stop_words [1] для всех поддерживаемых языков. Также MongoDB может посчитать относительный рейтинг, обозначающую релевантность результата.

Синтаксис

{
  $text:
    {
      $search: <string>,
      $language: <string>,
      $caseSensitive: <boolean>,
      $diacriticSensitive: <boolean>
    }
}
  • $search - строка слов, которые мы ищем;
  • $language - язык, определяющий список стоп-слов для поиска и правила для стемминга (по умолчанию используется язык, определенный для индекса);
  • $caseSensitive - булева переменная для подключения поиска с учетом регистра (по умолчанию false);
  • $diacriticSensitive - булева переменная для подключения поиска с учетом диакритических знаков (по умолчанию false).

Ограничения

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

Основные команды

Начало работы и добавление данных

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

 mongod --setParameter textSearchEnabled=true

В текущей версии MongoDB данная команда не требуется, поэтому просто запустим MongoDB:

 sudo service mongod start
 mongo

Для создания новой БД используется команда use. Если же она уже создана, то мы просто переключимся на неё. Для получения списка всех БД существует запрос: show dbs. Создадим для примера базу данных, в которой будет храниться имя отца, имя сына и его возраст. Назовем базу данных "testdb". Для того, чтобы она отображалась необходимо внести в коллекцию (например, "contact") хотя бы одну запись с помощью insert.

> use testdb
> db.contact.insert({"surname": "Петров", "name": "Иван Геннадьевич", "son": "Кирилл", age: 5})
> db.contact.insert({"surname": "Филатов", "name": "Иван Васильевич", "son": "Сергей", age: 10})
> db.contact.insert({"surname": "Соболев", "name": "Евгений Александрович", "son": "Иван", age: 3})
> db.contact.insert({"surname": "Широков", "name": "Антон Васильевич", "son": "Игорь", age: 8})
> db.contact.insert({"surname": "Тарасов", "name": "Роман Андреевич", "son": "Иван", age: 7})

Вывести все записи (документы) можно с помощью find:

> db.contact.find()

Вывод всех документов в более читабельном виде с помощью pretty():

> db.contact.find().pretty()

Индексация одного поля и поиск

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

> db.contact.createIndex({"name": "text"})

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

> db.contact.createIndex({"name": "text"}, {default_language: "russian"})

Поищем документы, содержащие ключевое слово "Иван" в поле "name", используя оператор $text:

> db.contact.find({$text: {$search: "Иван"}}) 

Данный запрос возвращает следующие документы (в примерах будут опущены id объектов):

{ "_id" : ObjectId("..."), "surname": "Петров", "name": "Иван Геннадьевич", "son": "Кирилл", age: 5}
{ "_id" : ObjectId("..."), "surname": "Филатов", "name": "Иван Васильевич", "son": "Сергей", age: 10}

Поищем документы, содержащие ключевое слово "Иван Васильевич" в поле "name":

> db.contact.find({$text: {$search: "Иван Васильевич"}})
{ "_id" : ObjectId("..."), "surname": "Петров", "name": "Иван Геннадьевич", "son": "Кирилл", age: 5}
{ "_id" : ObjectId("..."), "surname": "Филатов", "name": "Иван Васильевич", "son": "Сергей", age: 10}
{ "_id" : ObjectId("..."), "surname": "Широков", "name": "Антон Васильевич", "son": "Игорь", age: 8}

По умолчанию поиск по фразе использует OR поиск по всем представленным словам, то есть он будет искать все документы, которые содержат любое из слов. В случае если же вы хотите выполнить запрос по всей фразе целиком (логическое AND), то это можно сделать указав двойные кавычки в тексте поискового запроса:

> db.contact.find({$text: {$search: "\"Иван Васильевич\""}}) 
{ "_id" : ObjectId("..."), "surname": "Филатов", "name": "Иван Васильевич", "son": "Сергей", age: 10}

Или же можете использовать поиск по отрицанию. Добавив к слову префикс - (знак минус), можно убрать все документы, которые содержат это слово:

> db.contact.find({$text: {$search: "Иван -Васильевич"}}) 
{ "_id" : ObjectId("..."), "surname": "Петров", "name": "Иван Геннадьевич", "son": "Кирилл", age: 5}

Удаление индексов

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

> db.contact.getIndexes()

У нас установлен индекс "name_text", удалим его с помощью метода dropIndex:

> db.contact.dropIndex("name_text") 

Либо можно удалить все индексы с помощью следующего запроса:

> db.contact.dropIndex("*") 

Индексация нескольких полей

Чтобы искать текст по нескольким полям, создадим новый индекс и попробуем выполнить поиск:

> db.contact.createIndex({"name": "text", "son": "text"}, {default_language: "russian"})
> db.contact.find({$text: {$search: "Иван"}}) 
{ "_id" : ObjectId("..."), "surname": "Петров", "name": "Иван Геннадьевич", "son": "Кирилл", age: 5}
{ "_id" : ObjectId("..."), "surname": "Филатов", "name": "Иван Васильевич", "son": "Сергей", age: 10}
{ "_id" : ObjectId("..."), "surname": "Соболев", "name": "Евгений Александрович", "son": "Иван", age: 3}
{ "_id" : ObjectId("..."), "surname": "Тарасов", "name": "Роман Андреевич", "son": "Иван", age: 7} 

Как мы видим, MongoDB нашел нужный текст и в поле "name", и в поле "son".

Составной индекс

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

> db.contact.createIndex({"age": 1, "son": "text"}, {default_language: "russian"})
> db.contact.find({age: 7, $text: {$search: "Иван"}}) 
{ "_id" : ObjectId("..."), "surname": "Тарасов", "name": "Роман Андреевич", "son": "Иван", age: 7} 

Как мы видим, MongoDB обрезал лишние результаты по полю "age".

Индексация внутреннего документа

Чтобы можно было искать среди любого текстового контента в документах, используют спецификатор $**, который проиндексирует все поля документа:

> db.contact.createIndex({"$**":"text"}, {default_language: "russian"}) 

Попробуем что-нибудь найти:

> db.contact.find({$text: {$search: "Соболев"}}) 
{ "_id" : ObjectId("..."), "surname": "Соболев", "name": "Евгений Александрович", "son": "Иван", age: 3}

Как мы видим, не указывая индекс для поля "surname", MongoDB нашел нужный текст.

Оценка и сортировка

Когда производится поиск по нескольким полям, часто случается, что одно поле будет более важным, чем другое - т.е. будет иметь больший вес. В MongoDB в момент создания индекса можно указать веса индексируемых полей с помощью опции weights. По умолчанию вес равен 1. Установим для поля "son" вес равный 2. Затем отсортируем результаты с помощью опции sort по релевантности textScore:

> db.contact.createIndex({"name": "text", "son": "text"}, {"weights": { name: 1, son: 2}}, {default_language: "russian"})
> db.contact.find({$text: {$search: "Иван"}}, {score: {$meta: "textScore"}}).sort({score:{$meta:"textScore"}})
{ "_id" : ObjectId("..."), "surname": "Соболев", "name": "Евгений Александрович", "son": "Иван", age: 3, "score": 2}
{ "_id" : ObjectId("..."), "surname": "Тарасов", "name": "Роман Андреевич", "son": "Иван", age: 7, "score": 2})
{ "_id" : ObjectId("..."), "surname": "Петров", "name": "Иван Геннадьевич", "son": "Кирилл", age: 5, "score": 0.75}
{ "_id" : ObjectId("..."), "surname": "Филатов", "name": "Иван Васильевич", "son": "Сергей", age: 10, "score": 0.75} 

А теперь добавим такой документ, чтобы ключевое слово "Иван" фигурировало в двух полях:

> db.contact.insert({"surname": "Денисов", "name": "Иван Антонович", "son": "Иван", age: 4})
> db.contact.find({$text: {$search: "Иван"}}, {score: {$meta: "textScore"}}).sort({score:{$meta:"textScore"}})
{ "_id" : ObjectId("..."), "surname": "Денисов", "name": "Иван Антонович", "son": "Иван", age: 4, "score": 2.75}
{ "_id" : ObjectId("..."), "surname": "Соболев", "name": "Евгений Александрович", "son": "Иван", age: 3, "score": 2}
{ "_id" : ObjectId("..."), "surname": "Тарасов", "name": "Роман Андреевич", "son": "Иван", age: 7, "score": 2})
{ "_id" : ObjectId("..."), "surname": "Петров", "name": "Иван Геннадьевич", "son": "Кирилл", age: 5, "score": 0.75}
{ "_id" : ObjectId("..."), "surname": "Филатов", "name": "Иван Васильевич", "son": "Сергей", age: 10, "score": 0.75} 

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

Ограничение количества

Мы можем ограничить количество результатов с помощью опции limit:

> db.contact.find({$text: {$search: "Иван"}}).limit(2)
{ "_id" : ObjectId("..."), "surname": "Денисов", "name": "Иван Антонович", "son": "Иван", age: 4, "score": 2.75}
{ "_id" : ObjectId("..."), "surname": "Соболев", "name": "Евгений Александрович", "son": "Иван", age: 3, "score": 2} 

Стемминг

Создадим для наглядности новую базу данных из 2-3 предложений, создадим индекс и выполним пару запросов:

> use stemming
> db.stem.insert({"phrase": "каждый хозяин гуляет со своей собакой"})
> db.stem.insert({"phrase": "ты можешь не любить этого"})
> db.stem.insert({"phrase": "но собаки очень любят гулять на улице"})
> db.stem.createIndex({"phrase": "text"}, {default_language: "russian"})
> db.stem.find({$text: {$search: "собака"}})
{ "_id" : ObjectId("..."), "phrase": "каждый хозяин гуляет со своей собакой"}
{ "_id" : ObjectId("..."), "phrase": "но собаки очень любят гулять на улице"}
> db.stem.find({$text: {$search: "любить"}})
{ "_id" : ObjectId("..."), "phrase": "ты можешь не любить этого"}
{ "_id" : ObjectId("..."), "phrase": "но собаки очень любят гулять на улице"} 

Посмотрим как работает процесс нахождения основы слова. Выполним запрос:

> db.stem.find({$text: {$search: "гулять на улице"}})
{ "_id" : ObjectId("..."), "phrase": "каждый хозяин гуляет со своей собакой"}
{ "_id" : ObjectId("..."), "phrase": "но собаки очень любят гулять на улице"} 

А теперь выведем дополнительную статистику с помощью explain:

> db.stem.find({$text: {$search: "гулять на улице"}}).explain(true) 

Результат очень длинный, но нам нужно обратить внимание только на "parsedTextQuery":

"parsedTextQuery" : {
        "terms" : [
                "гуля",
                "улиц"
        ],
        "negatedTerms" : [ ],
        "phrases" : [ ],
        "negatedPhrases" : [ ]
}

Как мы видим, MongoDB отбросил стоп-слово "на" и обрезал окончания у слов "гулять" и "улице". Поиск выполнился только по словам "гуля" и "улиц".

Пример

Пример использования полнотекстового поиска в MongoDB версии 3.2.19:

Вывод

Полнотекстовый поиск в MongoDB не является полноценной заменой традиционным поисковым решениям типа Elasticsearch или SOLR, но является одной из самых востребованных возможностей в MongoDB. Специальные запросы, индексирование и агрегирование в реальном времени предоставляют мощные способы доступа к данным и их анализа.

Из плюсов можно выделить:

  • эффективное использование индексов;
  • поддержка нескольких языков;
  • возможность стемминга, отбрасывание стоп-слов, поиск по фразам, ранжирование;
  • взвешенный текстовый поиск.

Минусы:

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

Источники

  1. Github[Электронный ресурс]: mongodb / Дата обращения: 10.04.2018. — Режим доступа: https://github.com/mongodb/mongo/blob/master/src/mongo/db/fts

Ссылки

  1. Habrahabr [Электронный ресурс]: Полнотекстовый поиск в MongoDB / Дата обращения: 10.04.2018. — Режим доступа: https://habrahabr.ru/post/174457/
  2. Mongodb [Электронный ресурс]: Evaluation Query Operators $text / Дата обращения: 10.04.2018. — Режим доступа: https://docs.mongodb.com/manual/reference/operator/query/text/
  3. Tutsplus [Электронный ресурс]: Полнотекстовый поиск в MongoDB / Дата обращения: 10.04.2018. — Режим доступа: https://code.tutsplus.com/ru/tutorials/full-text-search-in-mongodb--cms-24835