Совсем недавно у меня была такая же проблема. Вот что я сделал:
Я создал класс для хранения только идентификатора и текста для каждого объекта (в моем случае я назвал его sku (номер элемента) и описанием). Это создает меньший объект, который использует меньше памяти, поскольку используется только для поиска. Я все равно возьму полноценные объекты из базы данных после того, как найду совпадения.
public class SmallItem
{
private int _sku;
public int Sku
{
get { return _sku; }
set { _sku = value; }
}
// Size of max description size + 1 for null terminator.
private char[] _description = new char[36];
public char[] Description
{
get { return _description; }
set { _description = value; }
}
public SmallItem()
{
}
}
После создания этого класса вы можете создать массив (в моем случае я фактически использовал List) этих объектов и использовать его для поиска по всему вашему приложению. Инициализация этого списка занимает немного времени, но вам нужно беспокоиться об этом только при запуске. В основном просто запустите запрос к своей базе данных и возьмите данные, необходимые для создания этого списка.
Когда у вас есть список, вы можете быстро просмотреть его, найдя любые слова, которые захотите. Поскольку это содержит, он также должен находить слова в словах (например, сверло вернет сверло, сверло, сверла и т. Д.). Для этого мы написали самодельную неуправляемую функцию C # contains. Он принимает строковый массив слов (так что вы можете искать более одного слова ... мы используем его для поиска "И" ... описание должно содержать все слова, переданные в ... "ИЛИ" в настоящее время не поддерживается в этом примере). По мере поиска в списке слов он формирует список идентификаторов, которые затем передаются обратно вызывающей функции. Если у вас есть список идентификаторов, вы можете легко запустить быстрый запрос в своей базе данных, чтобы вернуть полнофункциональные объекты на основе быстро проиндексированного идентификатора. Следует отметить, что мы также ограничиваем максимальное количество возвращаемых результатов. Это можно было вынуть. Это просто удобно, если кто-то вводит что-то вроде "e" в качестве поискового запроса. Это принесет много результатов.
Вот пример настраиваемой функции Contains:
public static int[] Contains(string[] descriptionTerms, int maxResults, List<SmallItem> itemList)
{
// Don't allow more than the maximum allowable results constant.
int[] matchingSkus = new int[maxResults];
// Indexes and counters.
int matchNumber = 0;
int currentWord = 0;
int totalWords = descriptionTerms.Count() - 1; // - 1 because it will be used with 0 based array indexes
bool matchedWord;
try
{
/* Character array of character arrays. Each array is a word we want to match.
* We need the + 1 because totalWords had - 1 (We are setting a size/length here,
* so it is not 0 based... we used - 1 on totalWords because it is used for 0
* based index referencing.)
* */
char[][] allWordsToMatch = new char[totalWords + 1][];
// Character array to hold the current word to match.
char[] wordToMatch = new char[36]; // Max allowable word size + null terminator... I just picked 36 to be consistent with max description size.
// Loop through the original string array or words to match and create the character arrays.
for (currentWord = 0; currentWord <= totalWords; currentWord++)
{
char[] desc = new char[descriptionTerms[currentWord].Length + 1];
Array.Copy(descriptionTerms[currentWord].ToUpper().ToCharArray(), desc, descriptionTerms[currentWord].Length);
allWordsToMatch[currentWord] = desc;
}
// Offsets for description and filter(word to match) pointers.
int descriptionOffset = 0, filterOffset = 0;
// Loop through the list of items trying to find matching words.
foreach (SmallItem i in itemList)
{
// If we have reached our maximum allowable matches, we should stop searching and just return the results.
if (matchNumber == maxResults)
break;
// Loop through the "words to match" filter list.
for (currentWord = 0; currentWord <= totalWords; currentWord++)
{
// Reset our match flag and current word to match.
matchedWord = false;
wordToMatch = allWordsToMatch[currentWord];
// Delving into unmanaged code for SCREAMING performance ;)
unsafe
{
// Pointer to the description of the current item on the list (starting at first char).
fixed (char* pdesc = &i.Description[0])
{
// Pointer to the current word we are trying to match (starting at first char).
fixed (char* pfilter = &wordToMatch[0])
{
// Reset the description offset.
descriptionOffset = 0;
// Continue our search on the current word until we hit a null terminator for the char array.
while (*(pdesc + descriptionOffset) != '\0')
{
// We've matched the first character of the word we're trying to match.
if (*(pdesc + descriptionOffset) == *pfilter)
{
// Reset the filter offset.
filterOffset = 0;
/* Keep moving the offsets together while we have consecutive character matches. Once we hit a non-match
* or a null terminator, we need to jump out of this loop.
* */
while (*(pfilter + filterOffset) != '\0' && *(pfilter + filterOffset) == *(pdesc + descriptionOffset))
{
// Increase the offsets together to the next character.
++filterOffset;
++descriptionOffset;
}
// We hit matches all the way to the null terminator. The entire word was a match.
if (*(pfilter + filterOffset) == '\0')
{
// If our current word matched is the last word on the match list, we have matched all words.
if (currentWord == totalWords)
{
// Add the sku as a match.
matchingSkus[matchNumber] = i.Sku.ToString();
matchNumber++;
/* Break out of this item description. We have matched all needed words and can move to
* the next item.
* */
break;
}
/* We've matched a word, but still have more words left in our list of words to match.
* Set our match flag to true, which will mean we continue continue to search for the
* next word on the list.
* */
matchedWord = true;
}
}
// No match on the current character. Move to next one.
descriptionOffset++;
}
/* The current word had no match, so no sense in looking for the rest of the words. Break to the
* next item description.
* */
if (!matchedWord)
break;
}
}
}
}
};
// We have our list of matching skus. We'll resize the array and pass it back.
Array.Resize(ref matchingSkus, matchNumber);
return matchingSkus;
}
catch (Exception ex)
{
// Handle the exception
}
}
После того, как у вас есть список подходящих skus, вы можете перебирать массив и создавать команду запроса, которая возвращает только совпадающие skus.
Чтобы получить представление о производительности, вот что мы обнаружили (выполнив следующие шаги):
- Искать ~ 171,000 позиций
- Создать список всех подходящих элементов
- Запрашивать базу данных, возвращая только совпадающие элементы
- Создавайте полноценные элементы (аналогично классу SmallItem, но с гораздо большим количеством полей)
- Заполните сетку данных развернутыми объектами элементов.
На наших мобильных устройствах весь процесс занимает 2-4 секунды (занимает 2 секунды, если мы достигаем предела совпадений до того, как мы обыскали все элементы ... занимает 4 секунды, если нам нужно сканировать каждый элемент).
Я также пробовал делать это без неуправляемого кода и с использованием String.IndexOf (и пробовал String.Contains ... имел такую же производительность, как IndexOf, как и должно). Так было намного медленнее ... около 25 секунд.
Я также пробовал использовать StreamReader и файл, содержащий строки [Sku Number] | [Description]. Код был похож на пример неуправляемого кода. Таким образом, полное сканирование заняло около 15 секунд. Неплохо для скорости, но не отлично. Однако метод file и StreamReader имеет одно преимущество перед тем, как я вам показал. Файл можно создать заранее. Способ, который я вам показал, требует памяти и начального времени для загрузки списка при запуске приложения. Для наших 171 000 элементов это занимает около 2 минут. Если вы можете позволить себе ждать этой начальной загрузки каждый раз при запуске приложения (что, конечно, можно делать в отдельном потоке), то такой поиск - самый быстрый способ (по крайней мере, который я нашел).
Надеюсь, это поможет.
PS - Спасибо Dolch за помощь с некоторым неуправляемым кодом.
person
Jason Down
schedule
16.01.2009