Полнотекстовый поиск на мобильном устройстве? - PullRequest
4 голосов
/ 10 ноября 2008

Мы скоро приступим к разработке нового мобильного приложения. Это конкретное приложение будет использоваться для интенсивного поиска текстовых полей. Какие-либо предложения от группы в целом о том, какой механизм базы данных лучше всего подходит для таких поисков на мобильной платформе?

Особенности включают Windows Mobile 6, и мы будем использовать .Net CF. Кроме того, некоторые текстовые поля могут содержать от 35 до 500 символов. Устройство будет работать двумя разными способами: пакетным и WiFi. Конечно, для Wi-Fi мы можем просто отправлять запросы в полноценный движок БД и просто возвращать результаты. Этот вопрос сосредоточен вокруг «пакетной» версии, в которой будет размещена база данных, загруженная информацией на флэш-накопителях / съемных картах памяти устройств.

В любом случае, я знаю, что в SQLCE есть некоторая базовая индексация, но вы не попадете в настоящие причудливые «полнотекстовые» индексы в стиле, пока не получите полноценную версию, которая, конечно, недоступна на мобильной платформе. .

Пример того, как будут выглядеть данные:

"Фартук Карпентер регулируемый кожаный контейнер карман талии пояса аппаратных средств" и т. Д. И т. Д.

Я еще не принимал участие в оценке каких-либо других конкретных вариантов, поскольку, как я полагаю, я бы использовал опыт этой группы, чтобы сначала указать мне некоторые конкретные пути.

Любые предложения / советы?

Ответы [ 2 ]

5 голосов
/ 16 января 2009

Совсем недавно у меня была такая же проблема. Вот что я сделал:

Я создал класс для хранения только идентификатора и текста для каждого объекта (в моем случае я назвал его 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:

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 секунд для всего сканирования. Не так уж плохо для скорости, но не отлично. Метод файла и StreamReader имеет одно преимущество перед тем, как я вам показал. Файл может быть создан заранее. Способ, который я показал вам, требует памяти и начального времени для загрузки списка при запуске приложения. Для наших 171 000 наименований это занимает около 2 минут. Если вы можете позволить себе ждать этой начальной загрузки при каждом запуске приложения (что, конечно, можно сделать в отдельном потоке), то поиск по этому пути является самым быстрым (по крайней мере, я нашел).

Надеюсь, это поможет.

PS - Спасибо Dolch за помощь с неуправляемым кодом.

2 голосов
/ 10 ноября 2008

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

http://incubator.apache.org/lucene.net/ http://lucene.apache.org/java/docs/

...