Полнотекстовый поиск на мобильном устройстве?

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

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

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

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

"фартук плотник регулируемый кожаный контейнер карман пояс аппаратный пояс" и т. д. и т. д.

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

Есть предложения / советы?


person Mat Nadrofsky    schedule 09.11.2008    source источник
comment
Все еще ищу ответы на этот вопрос.   -  person Mat Nadrofsky    schedule 22.11.2008


Ответы (2)


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

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

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

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

person Wayne Bloss    schedule 09.11.2008
comment
Спасибо за предложение. Я посмотрю на него и посмотрю, что влечет за собой перенос на C #. Меня беспокоит только то, что сейчас он выглядит неактивным, поскольку последний крупный релиз проекта был в апреле 2007 года. - person Mat Nadrofsky; 10.11.2008
comment
Этот сайт (stackoverflow.com) фактически использует Lucene.Net. Если вы посмотрите вокруг (возможно, в их подкастах), вы сможете найти их заметки о том, как они это использовали. - person Wayne Bloss; 12.11.2008
comment
@wizlb - я считаю, что lucene.net запланирован, но еще не реализован блог. stackoverflow.com/2008/11/ - person converter42; 21.11.2008