Как оптимизировать запрос Core Data для полнотекстового поиска
Можно ли оптимизировать запрос Core Data при поиске совпадающих слов в тексте? (Этот вопрос также относится к мудрости пользовательских SQL и Core Data на iPhone.)
Я работаю над новым (iPhone) приложением, которое является ручным справочным инструментом для научной базы данных. Основной интерфейс - это стандартное табличное представление, доступное для поиска, и я хочу, чтобы ответ типа "как есть", когда пользователь вводит новые слова. Матчи слов должны быть префиксами слов в тексте. Текст состоит из 100 000 слов.
В моем прототипе я кодировал SQL напрямую. Я создал отдельную таблицу слов, содержащую каждое слово в текстовых полях основного объекта. Я проиндексировал слова и выполнил поиск по строкам
SELECT id, * FROM textTable
JOIN (SELECT DISTINCT textTableId FROM words
WHERE word BETWEEN 'foo' AND 'fooz' )
ON id=textTableId
LIMIT 50
Это выполняется очень быстро. Использование IN, вероятно, будет работать так же хорошо, т.е.
SELECT * FROM textTable
WHERE id IN (SELECT textTableId FROM words
WHERE word BETWEEN 'foo' AND 'fooz' )
LIMIT 50
LIMIT имеет решающее значение и позволяет быстро отображать результаты. Я уведомляю пользователя о том, что их слишком много для отображения, если предел достигнут. Это kludgy.
Я потратил последние несколько дней на размышления о преимуществах перехода на Core Data, но я беспокоюсь об отсутствии контроля в схеме, индексировании и запросе на важный запрос.
Теоретически NSPredicate
textField MATCHES '.*\bfoo.*'
будет работать, но я уверен, что он будет медленным. Такой поиск текста кажется настолько распространенным, что мне интересно, что такое обычная атака? Создаете ли вы сущность слова, как я уже говорил выше, и используем предикат "word BEGINSWITH" foo? Будет ли это работать так же быстро, как мой прототип? Будет ли Core Data автоматически создавать нужные индексы? Я не могу найти явных средств для консультирования постоянного хранилища об индексах.
Я вижу некоторые полезные преимущества Core Data в приложении для iPhone. Ошибки и другие соображения памяти позволяют эффективно извлекать базы данных для запросов таблицы, не устанавливая произвольные ограничения. Управление графом объектов позволяет легко перемещать объекты без написания большого количества SQL. В будущем возможности миграции будут приятными. С другой стороны, в ограниченной среде ресурсов (iPhone) я волнуюсь, что автоматически созданная база данных будет раздута метаданными, ненужными обратными отношениями, неэффективными типами данных атрибутов и т.д.
Должен ли я погружаться или действовать осторожно?
Ответы
Ответ 1
Я сделал обходное решение. Я думаю, что это похоже на этот пост. Я добавил исходный код амальгамы в проект Core Data, а затем создал полнотекстовый класс поиска, который не был подклассом управляемого объекта. В классе FTS я #import "sqlite3.h"
(исходный файл) вместо структуры sqlite. Класс FTS сохраняет другой файл .sqlite, чем постоянное хранилище Core Data.
Когда я импортирую свои данные, объект Core Data хранит rowid связанного объекта FTS как целочисленный атрибут. У меня есть статический набор данных, поэтому я не беспокоюсь о ссылочной целостности, но код для поддержания целостности должен быть тривиальным.
Для выполнения FTS я MATCH
запрашивает класс FTS, возвращая набор rowids. В моем классе управляемых объектов я запрашиваю соответствующие объекты с помощью [NSPredicate predicateWithFormat:@"rowid IN %@", rowids]
. Я избегаю общения с такими отношениями "многие-ко-многим".
Улучшение производительности является драматическим. Мой набор данных - 142287 строк, включающий в себя 194 МБ (основные данные) и 92 МБ (FTS с удалением стоп-слов). В зависимости от частоты поискового запроса мои поисковые запросы длились от нескольких секунд до 0,1 секунды для нечастых терминов (< 100 ударов) и 0,2 секунды для частых терминов (более 2000 просмотров).
Я уверен, что с моим подходом есть множество проблем (раздувание кода, возможные конфликты пространства имен, потеря некоторых функций Core Data), но, похоже, он работает.
Ответ 2
Чтобы следить за этим вопросом, я обнаружил, что запрос - медленная работа с использованием Core Data. Я поцарапал себе голову на это много часов.
Как и в примере SQL в моем вопросе, есть два объекта: textTable и слова, в которых слова содержат каждое слово, оно индексируется, и между textTable и словами существует многозначное отношение. Я заполнил базу данных всего за 4000 слов и 360 объектов textTable. Предположим, что отношение textTable к объекту слов называется поисковым словом, тогда я могу использовать предикат для объекта textTable, который выглядит как
predicate = [NSPredicate predicateWithFormat:@"ANY searchWords.word BEGINSWITH %@", query];
(Я могу добавить конъюнкции этого предиката для нескольких терминов запроса.)
В iPhone этот запрос занимает несколько секунд. Ответ на мой ручной код SQL с использованием большего набора тестов был мгновенным.
Но это даже не конец. Существуют ограничения для NSPredicate, которые делают довольно простые запросы медленными и сложными. Например, представьте в приведенном выше примере, что вы хотите отфильтровать с помощью кнопки области. Предположим, что сущность слов содержит все слова во всех текстовых полях, но область охвата ограничивает ее словами из определенных полей. Таким образом, слова могут иметь атрибут "источник" (например, заголовок и текст сообщения электронной почты).
Естественно, что тогда полный текст будет игнорировать исходный атрибут, как в примере выше, но отфильтрованный запрос ограничит поиск определенным значением источника. Это, казалось бы, простое изменение требует SUBQUERY. Например, это не работает:
ANY searchWords.word BEGINSWITH "foo" AND ANY searchWords.source = 3
поскольку сущности, которые являются истинными для двух выражений, могут быть разными. Вместо этого вам нужно сделать что-то вроде:
SUBQUERY(searchWords, $x, $x.word BEGINSWITH "foo" AND $x.source = 3)[email protected] > 0
Я обнаружил, что эти подзапросы, возможно, не удивительно, медленнее, чем предикаты, использующие "ЛЮБОЙ".
В этот момент мне очень любопытно, как программисты Cocoa эффективно используют Core Data для полнотекстового поиска, потому что меня обескураживают как скорость оценки предикатов, так и выразительность NSPredicates. Я подбежал к стене.
Ответ 3
Погрузитесь.
Вот один из способов:
- Поместите свои записи в постоянное хранилище основных данных.
- Используйте
NSFetchedResultsController
для управления набором результатов на ваших объектах Word
(эквивалент Core Data с таблицей слов SQL)
- Используйте
UISearchDisplayController
для применения NSPredicate
в результирующем наборе в режиме реального времени
Как только у вас есть результирующий набор через NSFetchedResultsController
, довольно просто применить предикат. По моему опыту, он тоже будет отзывчивым. Например:
if ([self.searchBar.text length]) {
_predicate = [NSPredicate predicateWithFormat:[NSString stringWithFormat:@"(word contains[cd] '%@')", self.searchBar.text]];
[self.fetchedResultsController.fetchRequest setPredicate:_predicate];
}
NSError *error;
if (![self.fetchedResultsController performFetch:&error]) {
// handle error...
}
NSLog(@"filtered results: %@", [self.fetchedResultsController fetchedObjects]);
будет фильтровать результирующий набор [self.fetchedResultsController fetchedObjects]
на лету, делая поиск без учета регистра на Word
.
Ответ 4
После борьбы с этой же проблемой я столкнулся с серией сообщений, в которых у автора была такая же проблема, и придумал это решение, Он сообщает об улучшении от 6-7 секундного поиска до 0,13-0,05 секунды.
Его набор данных для FTS составлял 79 документов (размер файла 175k, 3600 дискретных токенов, 10000 ссылок). Я еще не пробовал его решения, но думал, что отправлю как можно скорее. См. Также часть 2 его сообщений для его документации по проблеме и Часть 1 для его документации по набору данных.