Как быстро найти строковый ключ / коллекцию значений - PullRequest
13 голосов
/ 14 октября 2008

Привет, товарищ по стеку!

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

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

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

Заранее спасибо, Oskar

Ответы [ 7 ]

13 голосов
/ 14 октября 2008

Суффиксный массив и q -граммный индекс

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

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

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

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

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

Подтверждение концепции

Я написал очень маленькое подтверждение концепции C # (поскольку вы заявили иначе, что работали с C #). Это работает, однако это очень медленно по двум причинам. Во-первых, создание массива суффиксов просто сортирует суффиксы. Одно это имеет время выполнения n 2 log n . Есть намного лучшие методы. Хуже всего то, что я использую SubString для получения суффиксов. К сожалению, .NET создает копии всего суффикса для этого. Чтобы использовать этот код на практике, убедитесь, что вы используете методы на месте, которые не копируют никакие данные без необходимости. То же самое верно для извлечения q -грамм из строки.

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

Тем не менее, легко заметить, что эта реализация, по сути, ожидала получения с постоянным временем (если словарь хорошо себя ведет)! Это настоящее достижение, которое невозможно превзойти с помощью дерева поиска / trie!

class QGramIndex {
    private readonly int m_Maxlen;
    private readonly string m_Data;
    private readonly int m_Q;
    private int[] m_SA;
    private Dictionary<string, int> m_Dir = new Dictionary<string, int>();

    private struct StrCmp : IComparer<int> {
        public readonly String Data;
        public StrCmp(string data) { Data = data; }
        public int Compare(int x, int y) {
            return string.CompareOrdinal(Data.Substring(x), Data.Substring(y));
        }
    }

    private readonly StrCmp cmp;

    public QGramIndex(IList<string> strings, int maxlen, int q) {
        m_Maxlen = maxlen;
        m_Q = q;

        var sb = new StringBuilder(strings.Count * maxlen);
        foreach (string str in strings)
            sb.AppendFormat(str.PadRight(maxlen, '\u0000'));
        m_Data = sb.ToString();
        cmp = new StrCmp(m_Data);
        MakeSuffixArray();
        MakeIndex();
    }

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

    private void MakeSuffixArray() {
        // Approx. runtime: n^3 * log n!!!
        // But I claim the shortest ever implementation of a suffix array!
        m_SA = Enumerable.Range(0, m_Data.Length).ToArray();
        Array.Sort(m_SA, cmp);
    }

    private int FindInArray(int ith) {
        return Array.BinarySearch(m_SA, ith, cmp);
    }

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

    private string QGram(int i) {
        return i > m_Data.Length - m_Q ?
            m_Data.Substring(i) :
            m_Data.Substring(i, m_Q);
    }

    private void MakeIndex() {
        for (int i = 0; i < m_Data.Length; ++i) {
            int pos = FindInArray(i);
            if (pos < 0) continue;
            m_Dir[QGram(i)] = pos;
        }
    }
}

Пример использования:

static void Main(string[] args) {
    var strings = new [] { "hello", "world", "this", "is", "a",
                           "funny", "test", "which", "i", "have",
                           "taken", "much", "too", "far", "already" };

    var index = new QGramIndex(strings, 10, 3);

    var tests = new [] { "xyz", "aki", "ake", "muc", "uch", "too", "fun", "est",
                         "hic", "ell", "llo", "his" };

    foreach (var str in tests) {
        int pos = index[str];
        if (pos > -1)
            Console.WriteLine("\"{0}\" found in \"{1}\".", str, strings[pos]);
        else
            Console.WriteLine("\"{0}\" not found.", str);
    }
}
2 голосов
/ 14 октября 2008

Вот ВАГ для тебя. Я ни в коем случае не Knuthian в моем алгоритме здравого смысла

Ладно, наивный Трие кодирует строковые ключи, начиная с корня дерева и перемещаясь вниз по ветвям, которые соответствуют каждой букве в ключе, начиная с первой буквы ключа. Таким образом, ключ «foo» будет отображен на (root)->f->fo->foo, а значение будет сохранено в месте, указанном узлом «foo».

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

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

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

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

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

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

0 голосов
/ 29 марта 2017

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

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

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

Преимущество метода индекса 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

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

0 голосов
/ 15 октября 2008

/ РЕДАКТИРОВАТЬ: Мой друг только что указал глупое предположение в моей конструкции таблицы q-грамм. Построение может быть сделано намного проще - и, следовательно, намного быстрее. Я отредактировал исходный код и объяснение, чтобы отразить это. Я думаю, что это может быть окончательное решение .

Вдохновленный комментарием Рафаля Доугирда к моему предыдущему ответу, я обновил свой код. Я думаю, что это заслуживает собственного ответа, так как он также довольно длинный. Вместо заполнения существующих строк этот код строит индекс по исходному массиву строк. Вместо сохранения одной позиции в массиве суффиксов хранится пара: индекс целевой строки и позиция суффикса в этой строке. В результате нужен только первый номер. Однако второе число необходимо для построения таблицы 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 для конструктора.

0 голосов
/ 14 октября 2008

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

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

0 голосов
/ 14 октября 2008

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

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

Пример дерево 1: STCKVRFLW -> доменный объект

дерево 2: стек -> STCKVRFLW, STCK более -> STCKVRFLW, VRBRD, VR

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

0 голосов
/ 14 октября 2008

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

...