Как быстро найти цепочку ключей/значений на основе строк

Приветствуем stackoverflowers!

У меня есть список слов из 200 000 записей строк, средняя длина строки составляет около 30 символов. Этот список слов является ключом, и для каждого ключа у меня есть объект домена. Я хотел бы найти объекты домена в этой коллекции, зная только часть ключа. И.Е. строка поиска "kov" соответствовала бы, например, ключевому "stackoverflow".

В настоящее время я использую тройное дерево поиска (TST), которое обычно найдет элементы в течение 100 миллисекунд. Это слишком медленно для моих требований. Реализация TST может быть улучшена с небольшими оптимизациями, и я мог бы попытаться сбалансировать дерево. Но я подумал, что эти вещи не дадут мне 5x - 10x улучшение скорости, на которое я нацеливаюсь. Я предполагаю, что причиной столь медленной является то, что я в основном должен посетить большинство узлов в дереве.

Любые идеи о том, как улучшить скорость алгоритма? Есть ли другие алгоритмы, над которыми я должен смотреть?

Спасибо заранее, Oskar

Ответы

Ответ 1

Массив суффикса и индекс q-графы

Если ваши строки имеют строгую верхнюю границу размера, вы можете рассмотреть использование массива суффикса: Просто наложите все свои строки на одну и ту же максимальную длину с помощью специального символа (например, null char). Затем объедините все строки и постройте над ними индекс массива суффиксов.

Это дает вам время выполнения m * log n, где m - длина строки запроса, а n - общая длина ваших комбинированных строк. Если это все еще недостаточно, и ваш m имеет фиксированную небольшую длину, а ваш алфавит Σ ограничен по размеру (например, Σ < 128 разных символов), вы можете дополнительно построить индекс q-gram. Это позволит найти в постоянное время. Однако для таблицы q-gram требуются записи Σ m (= 8 MiB в случае всего 3 символа и 1 GiB для 4 символов!).

Уменьшение индекса

Возможно, уменьшить размер таблицы q-gram (экспоненциально, в лучшем случае), отрегулировав хэш-функцию. Вместо того, чтобы присваивать уникальный номер всем возможным q-граммам, вы можете использовать функцию хэш-функции с потерями. Затем таблица должна будет хранить списки возможных индексов массива суффиксов вместо одной записи массива суффикса, соответствующей точному совпадению. Это повлечет за собой, что поиск больше не является постоянным, потому что все записи в списке должны быть рассмотрены.

Кстати, я не уверен, знакомы ли вы с , как работает индекс q-gram, поскольку Интернет не помогает в этом вопросе. Я упомянул об этом раньше в другой теме. Поэтому я включил описание и алгоритм построения в тесте

Ответ 2

Здесь ВАГ для вас. Я нахожусь в NO WAY Knuthian в своем алгоритме подкованных

Хорошо, поэтому naiive Trie кодирует строковые ключи, начиная с корня дерева и перемещая ветки, соответствующие каждой букве в ключе, начиная с первой буквы ключа. Таким образом, ключ "foo" будет отображаться на (root)->f->fo->foo, и значение будет сохранено в местоположении, на которое указывает "foo" node.

Вы ищете ЛЮБОЕ подстроку внутри ключа, а не подстроки, которые начинаются с начала ключа.

Итак, что вам нужно сделать, ассоциируется с node с ЛЮБЫМ ключом, который содержит определенную подстроку. В примере foo, который я дал раньше, вы бы не нашли ссылку на значение foo под узлами "f" и "fo". В TST, который поддерживает тип поиска, который вы хотите сделать, вы не только найдете объект foo под всеми тремя узлами ('f', 'fo' и 'foo'), вы также найдете его под "o" и "oo".

Есть несколько очевидных последствий для расширения дерева поиска для поддержки такого типа индексирования. Во-первых, вы просто взорвали размер дерева. Ошеломляюще. Если вы можете сохранить его и использовать его эффективным образом, ваш поиск займет время O (1). Если ваши ключи остаются статическими, и вы можете найти способ разбить индекс, чтобы не использовать огромный штраф IO при его использовании, это может привести к снижению стоимости.

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

С яркой стороны вы также можете обнаружить, что вы можете сжать дерево через токенизацию (например, сжатие zip) или сжатием узлов, которые не ветвятся (т.е. если у вас есть "w" → "o", → 'o' → , а первый 'o' не веткится, вы можете смело свернуть его до 'w' → 'oo'). Может быть, даже хеш нечестивых задниц может облегчить ситуацию...

Во всяком случае, WAG, как я уже сказал.

Ответ 3

Получаете ли вы какие-либо преимущества, если ваши ключи trie сопоставимы с размером машинного регистра? Итак, если вы находитесь на 32-битной коробке, вы можете сравнить 4 символа одновременно, а не каждый символ в отдельности? Я не знаю, как плохо это увеличило бы размер вашего приложения.

Ответ 4

Можно ли "хешировать" ключевое значение? в основном есть 2-е дерево, будут все возможные значения для поиска указателя на список ключей в 1-е дерево.

Вам понадобятся 2 дерева; 1-й является хэш-значением для объекта домена. 2-е дерево - это строки поиска для хеш-значения. второе дерево имеет несколько ключей к одному и тому же значению хеширования.

Пример дерево 1: STCKVRFLW → объект домена

дерево 2: стек → STCKVRFLW, STCK over → STCKVRFLW, VRBRD, VR

Таким образом, используя поиск на втором дереве, вы получите список ключей для поиска на 1-м дереве.

Ответ 5

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

В худшем случае это O (n), но вы получите только это, если ваши строковые записи почти идентичны. Словарь поиска, вероятно, будет довольно большим, поэтому, вероятно, неплохо сохранить его на диске или использовать реляционную базу данных: -)

Ответ 6

/EDIT: Мой друг просто указал на глупое предположение в моей конструкции таблицы q-gram. Конструкцию можно сделать намного проще - и, следовательно, намного быстрее. Я отредактировал исходный код и объяснение, чтобы отразить это. Я думаю, что это может быть окончательное решение .

Вдохновленный комментарием Rafał Dowgird к моему предыдущему ответу, я обновил свой код. Я думаю, что это заслуживает собственного ответа, поскольку оно также довольно долгое. Вместо заполнения существующих строк этот код строит индекс над исходным массивом строк. Вместо хранения одной позиции массив суффикса хранит пару: индекс целевой строки и положение суффикса в этой строке. В результате требуется только первое число. Однако второе число необходимо для построения таблицы q-gram.

Новая версия алгоритма создает таблицу q-gram, перейдя через суффиксный массив вместо исходных строк. Это сохраняет двоичный поиск массива суффикса. Следовательно, время выполнения конструкции падает от O (n * log n) до O (n) (где n - размер массива суффиксов).

Обратите внимание, что, как и мое первое решение, использование SubString приводит к большому количеству ненужных копий. Очевидным решением является написать метод расширения, создающий упрощенную оболочку вместо копирования строки. Затем сравнение должно быть слегка адаптировано. Это остается как упражнение для читателя.; -)

using Position = System.Collections.Generic.KeyValuePair<int, int>;

class QGramIndex {
    private readonly int m_Q;
    private readonly IList<string> m_Data;
    private Position[] m_SA;
    private Dictionary<string, int> m_Dir;

    public QGramIndex(IList<string> strings, int q) {
        m_Q = q;
        m_Data = strings;
        MakeSuffixArray();
        MakeIndex();
    }

    public int this[string s] { get { return FindInIndex(s); } }

    private int FindInIndex(string s) {
        int idx;
        if (!m_Dir.TryGetValue(s, out idx))
            return -1;
        return m_SA[idx].Key;
    }

    private void MakeSuffixArray() {
        int size = m_Data.Sum(str => str.Length < m_Q ? 0 : str.Length - m_Q + 1);
        m_SA = new Position[size];
        int pos = 0;
        for (int i = 0; i < m_Data.Count; ++i)
            for (int j = 0; j <= m_Data[i].Length - m_Q; ++j)
                m_SA[pos++] = new Position(i, j);

        Array.Sort(
            m_SA,
            (x, y) => string.CompareOrdinal(
                m_Data[x.Key].Substring(x.Value),
                m_Data[y.Key].Substring(y.Value)
            )
        );
    }

    private void MakeIndex() {
        m_Dir = new Dictionary<string, int>(m_SA.Length);

        // Every q-gram is a prefix in the suffix table.
        for (int i = 0; i < m_SA.Length; ++i) {
            var pos = m_SA[i];
            m_Dir[m_Data[pos.Key].Substring(pos.Value, 5)] = i;
        }
    }
}

Использование такое же, как в другом примере, минус требуемый аргумент maxlen для конструктора.

Ответ 7

Для эффективного запроса большого набора текста вы можете использовать концепцию Edit Distance/Prefix Edit Distance.

Изменить расстояние ED (x, y): минимальное количество преобразований для перехода от x к y

Но вычисление ED между каждым термином и текстом запроса является ресурсом и требует много времени. Поэтому вместо вычисления ED для каждого термина сначала мы можем извлечь возможные совпадающие термины, используя метод Qgram Index. а затем применить вычисление ЭД на этих выбранных условиях.

Преимущество метода индекса Qgram заключается в поддержке Нечеткого поиска.

Одним из возможных подходов к адаптации индекса QGram является построение Инвертированного индекса с использованием Qgrams. Там мы сохраняем все слова, которые состоят из определенного Qgram (вместо хранения полной строки вы можете использовать уникальный идентификатор для каждой строки).

col: col mbia, col ombo, gan col a, ta col ama

Затем при запросе мы вычисляем количество общих Qgrams между текстом запроса и доступными терминами.

Example: x = HILLARY, y = HILARI(query term)
Qgrams
$$HILLARY$$ -> $$H, $HI, HIL, ILL, LLA, LAR, ARY, RY$, Y$$
$$HILARI$$ -> $$H, $HI, HIL, ILA, LAR, ARI, RI$, I$$
number of q-grams in common = 4

Для терминов с большим количеством общих Qgrams мы вычисляем ED/PED в соответствии с термином запроса, а затем предлагаем термин для конечного пользователя.

вы можете найти реализацию этой теории в следующем проекте. Не стесняйтесь задавать любые вопросы. https://github.com/Bhashitha-Gamage/City_Search

Чтобы узнать больше о редактировании расстояния, префикс Edit Distance Qgram index, пожалуйста, просмотрите следующее видео профессора доктора Ханны Баст https://www.youtube.com/embed/6pUg2wmGJRo (Занятие начинается с 20:06)