Cómo buscar rápidamente una colección de clave/valor basada en cadena

¡Hola compañeros stackoverflowers!

Tengo una lista de palabras de 200,000 entradas de cadenas, la longitud promedio de las cadenas es de alrededor de 30 caracteres. Esta lista de palabras es la clave y para cada tecla tengo un objeto de dominio. Me gustaría encontrar los objetos de dominio en esta colección solo conociendo una parte de la clave. ES DECIR. la cadena de búsqueda "kov" coincidiría, por ejemplo, con la clave "stackoverflow".

Actualmente estoy usando un árbol de búsqueda ternaria (TST), que generalmente encontrará los artículos dentro de 100 milisegundos. Sin embargo, esto es demasiado lento para mis requerimientos. La implementación de TST podría mejorarse con algunas optimizaciones menores y podría intentar equilibrar el árbol. Pero pensé que estas cosas no me darían la mejora de velocidad 5x - 10x a la que aspiro. Asumo que la razón de ser tan lenta es que básicamente tengo que visitar la mayoría de los nodos en el árbol.

¿Alguna idea sobre cómo mejorar la velocidad del algoritmo? ¿Hay algún otro algoritmo que debería estar mirando?

Gracias por adelantado, Oskar

13
Aprendí algo nuevo hoy: Trie.
agregado el autor Will, fuente
¿En qué idioma estás trabajando? Esta información es necesaria ya que todos los idiomas no manejan búsquedas y colecciones de la misma manera
agregado el autor WolfmanDragon, fuente
Creo que debería ser "Trie" o "Ternary Search Tree".
agregado el autor Tomalak, fuente
Ese es el tipo de preguntas que me encantan: nada supera un buen desafío de vez en cuando ... :-)
agregado el autor Konrad Rudolph, fuente
A. ¿Podría explicar cómo logró usar el TST para lo que parece ser una búsqueda de algo que no es prefijo ni sufijo? (En su ejemplo, "kov" no es ni prefijo ni sufijo "stackoverflow"), es decir, ¿puede describir la forma en que inserta </​​b> elementos en el TST? B. ¿Puede usted, digamos, de nuevo para su ejemplo específico de "kov", describir cómo su implementación de la función TST búsqueda SÉ cómo/cuándo excluir ciertos nodos de la inspección (de nuevo bajo el supuesto de A que ¿Estás buscando un término que no sea prefijo ni sufijo?
agregado el autor MrCC, fuente

7 Respuestas

Suffix Array y q -gram index

Si sus cadenas tienen un límite superior estricto en el tamaño, puede considerar el uso de una matriz de sufijos : simplemente rellene todas las cadenas con la misma longitud máxima utilizando un carácter especial (por ejemplo, el carácter nulo). A continuación, concatene todas las cadenas y cree un índice de matriz de sufijos sobre ellas.

This gives you a lookup runtime of m * log n where m is the length of your query string and n is the overall length of your combined strings. If this still isn't good enough and your m has a fixed, small length, and your alphabet Σ is restricted in size (say, Σ < 128 different characters) you can additionally build a q-gram index. This will allow retrieval in constant time. However, the q-gram table requires Σm entries (= 8 MiB in the case of just 3 characters, and 1 GiB for 4 characters!).

Hacer el índice más pequeño

Es posible que reduzca el tamaño de la q -gran tabla (exponencialmente, en el mejor de los casos) ajustando la función hash. En lugar de asignar un número único a cada posible q -gram, puede emplear una función hash con pérdida. La tabla tendría que almacenar listas de posibles índices de matriz de sufijos en lugar de solo una entrada de matriz de sufijos correspondiente a una coincidencia exacta. Esto implicaría que la búsqueda ya no es constante, porque todas las entradas en la lista tendrían que ser consideradas.

Por cierto, no estoy seguro si está familiarizado con cómo funciona un q -índice de índice ya que Internet no es útil en este tema. Ya he mencionado esto en otro tema. Por lo tanto, he incluido una descripción y un algoritmo para la construcción en mi tesis de licenciatura .

Prueba de concepto

I've written a very small C# Prueba de concepto (since you stated otherwise that you worked with C#). It works, however it is very slow for two reasons. First, the suffix array creation simply sorts the suffixes. This alone has runtime n2 log n. There are far superior methods. Worse, however, is the fact that I use SubString to obtain the suffixes. Unfortunately, .NET creates copies of the whole suffix for this. To use this code in practice, make sure that you use in-place methods which do not copy any data around unnecessarily. The same is true for retrieving the q-grams from the string.

Sería incluso mejor no construir la cadena m_Data </​​code> utilizada en mi ejemplo. En su lugar, podría guardar una referencia a la matriz original y simular todos mis accesos SubString trabajando en esta matriz.

Aún así, es fácil ver que esta implementación esencialmente ha esperado una recuperación de tiempo constante (¡si el diccionario se comporta bien)! ¡Este es un gran logro que no puede ser superado por un árbol de búsqueda/trie!

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

    private struct StrCmp : IComparer {
        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 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;
        }
    }
}

Ejemplo de uso:

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);
    }
}
13
agregado
¿Hay alguna forma de dividir una tabla de q-gram para que no se rompa el disco?
agregado el autor Will, fuente
No estoy enterado de ninguna manera. Su mejor opción podría ser reducir el alfabeto mezclando varios caracteres con la misma tecla y, por lo tanto, reduciendo el tamaño de la tabla exponencialmente. Sin embargo, debe encargarse de las colisiones.
agregado el autor Konrad Rudolph, fuente
@ Rafał: estoy rellenando las cadenas para que pueda calcular el índice fácilmente desde la posición en la matriz de sufijos. Existen otras soluciones, pero estas requieren modificar la matriz de sufijos, dificultando la construcción.
agregado el autor Konrad Rudolph, fuente
Una matriz de sufijo es mejor que un árbol de sufijo porque se puede almacenar mucho más espacio-eficientemente. Lo que es más importante, necesita un sufijo matriz para crear el índice q-gram de manera eficiente (al menos no conozco ningún algoritmo para crear un índice q-gram para un árbol de sufijos).
agregado el autor Konrad Rudolph, fuente
@ Rafał: "Encontrar el hilo original por el sufijo debe ser rápido" - ¿Cómo? Sin embargo, reconozco que rellenar la cuerda generalmente no es una buena manera. Sería mejor construir la matriz de sufijos sobre la matriz de cadenas. Esto es posible, aunque un poco más difícil. Actualizaré mi texto en consecuencia.
agregado el autor Konrad Rudolph, fuente
@ Rafał: echa un vistazo a mi publicación de seguimiento. Sin embargo, en respuesta a su propuesta de registro (N): tenga en cuenta que su N aquí no es solo 200,000, sino que es la cantidad de todos los sufijos, que es mucho más alta.
agregado el autor Konrad Rudolph, fuente
¿Por qué es necesario rellenar las cuerdas? ¿La matriz de sufijos es mejor que un árbol de sufijos?
agregado el autor Rafał Dowgird, fuente
Buenos puntos sobre el árbol. De vuelta al relleno, según tengo entendido, puede obtener el sufijo completo de la tabla ("kov" -> "koverflow"). Encontrar la cadena original por el sufijo debe ser rápida (o incluso por el prefijo, si construye la tabla a partir de cadenas invertidas). ¿Correcto?
agregado el autor Rafał Dowgird, fuente
Puede encontrar la cadena por sufijo en el tiempo O (log (N)) si mantiene una tabla adicional de las cadenas ordenadas por su reverso. O mantenga las cadenas ordenadas de forma natural y construya la matriz de sufijos a partir de cadenas invertidas, obteniendo prefijos en lugar de sufijos.
agregado el autor Rafał Dowgird, fuente

Here's a WAG for you. I am in NO WAY Knuthian in my algorithm savvy

Okay, so the naiive Trie encodes string keys by starting at the root of the tree and moving down branches that match each letter in the key, starting at the first letter of the key. So the key "foo" would be mapped to (root)->f->fo->foo and the value would be stored in the location pointed to by the 'foo' node.

Está buscando CUALQUIER subcadena dentro de la clave, no solo subcadenas que comienzan al principio de la clave.

Entonces, lo que necesita hacer es asociar un nodo con CUALQUIER tecla que contenga esa subcadena particular. En el ejemplo de foo que di antes, NO habrías encontrado una referencia al valor de foo bajo los nodos 'f' y 'fo'. En un TST que admite el tipo de búsqueda que está buscando hacer, no solo encontrará el objeto foo en los tres nodos ('f', 'fo' y 'foo'), también lo encontrará. debajo de 'o' y 'oo' también.

Hay un par de consecuencias obvias para expandir el árbol de búsqueda para admitir este tipo de indexación. Primero, acabas de explotar el tamaño del árbol. Asombrosamente. Si puede almacenarlo y usarlo de manera eficiente, sus búsquedas tomarán O (1) vez. Si sus llaves permanecen estáticas, y puede encontrar una manera de dividir el índice para que no se aplique una gran penalización de E/S al usarlo, esto puede amortizarse para que valga la pena.

En segundo lugar, descubrirá que las búsquedas de cadenas pequeñas darán como resultado números masivos de visitas, lo que puede hacer que su búsqueda sea inútil a menos que, por ejemplo, ponga una longitud mínima en los términos de búsqueda.

On the bright side, you might also find that you can compress the tree via tokenization (like zip compression does) or by compressing nodes that don't branch down (i.e., if you have 'w'->'o'->'o'-> and the first 'o' doesn't branch, you can safely collapse it to 'w'->'oo'). Maybe even a wicked-ass hash could make things easier...

De todos modos, WAG como dije.

2
agregado
¿No es esto lo mismo que el índice q-gram de lo que Konrad estaba hablando?
agregado el autor Pacerier, fuente

/EDITAR: Un amigo mío acaba de señalar una suposición estúpida en mi construcción de la tabla de q-gram. La construcción puede hacerse mucho más simple y, en consecuencia, mucho más rápida. He editado el código fuente y la explicación para reflejar esto. Creo que podría ser la solución final .

Inspirado por el comentario de Rafał Dowgird a mi respuesta anterior, he actualizado mi código. Creo que esto merece una respuesta propia, sin embargo, ya que también es bastante larga. En lugar de rellenar las cadenas existentes, este código crea el índice sobre la matriz original de cadenas. En lugar de almacenar una sola posición, la matriz de sufijos almacena un par: el índice de la cadena objetivo y la posición del sufijo en esa cadena. En el resultado, solo se necesita el primer número. Sin embargo, el segundo número es necesario para la construcción de la tabla q -gram.

La nueva versión del algoritmo construye la tabla q -gram al recorrer la matriz de sufijos en lugar de las cadenas originales. Esto guarda la búsqueda binaria de la matriz de sufijos. En consecuencia, el tiempo de ejecución de la construcción baja de O ( n * log n ) a O ( n ) (donde n es el tamaño de la matriz de sufijos).

Tenga en cuenta que, al igual que mi primera solución, el uso de SubString da como resultado muchas copias innecesarias. La solución obvia es escribir un método de extensión que cree un contenedor ligero en lugar de copiar la cadena. La comparación tiene que ser ligeramente adaptada. Esto se deja como un ejercicio para el lector. ;-)

using Position = System.Collections.Generic.KeyValuePair;

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

    public QGramIndex(IList 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(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;
        }
    }
}

El uso es el mismo que en el otro ejemplo, menos el argumento maxlen requerido para el constructor.

0
agregado

¿Tendría alguna ventaja con sus llaves comparables al tamaño del registro de la máquina? Entonces, si estás en un cuadro de 32 bits, puedes comparar 4 caracteres a la vez en lugar de cada personaje individualmente. No sé qué tan malo aumentaría el tamaño de tu aplicación.

0
agregado

¿Sería posible "hash" el valor clave? Básicamente, un segundo árbol mostrará todos los valores posibles para buscar una lista de llaves en el primer árbol.

Vas a necesitar 2 árboles; El primero es un valor hash para el objeto de dominio. el segundo árbol es las cadenas de búsqueda para el valor hash. el segundo árbol tiene varias claves para el mismo valor hash.

Example tree 1: STCKVRFLW -> domain object

tree 2: stack -> STCKVRFLW,STCK over -> STCKVRFLW, VRBRD, VR

Entonces, al buscar en el 2 ° árbol, se obtiene una lista de las claves para buscar en el 1er árbol.

0
agregado

Elija un tamaño de cadena de búsqueda mínimo (por ejemplo, cuatro caracteres). Repase su lista de entradas de cadenas y cree un diccionario de cada subcadena de cuatro caracteres, asignando a una lista de entradas en la que aparece la subcadena. Cuando realice una búsqueda, busque los primeros cuatro caracteres de la cadena de búsqueda para buscar un conjunto inicial, luego limite ese conjunto inicial solo a aquellos que coinciden con la cadena de búsqueda completa.

El peor caso de esto es O (n), pero solo lo obtendrás si tus entradas de cadena son casi todas idénticas. Es probable que el diccionario de búsqueda sea bastante grande, por lo que probablemente sea una buena idea almacenarlo en un disco o usar una base de datos relacional :-)

0
agregado

Para consultar un conjunto grande de texto de manera eficiente, puede utilizar el concepto Editar distancia de distancia de edición de prefijo.

Editar distancia ED (x, y): cantidad mínima de transfroms para llegar de xa y

Pero calcular ED entre cada término y texto de consulta consume recursos y consume mucho tiempo. Por lo tanto, en lugar de calcular ED para cada término primero, podemos extraer posibles términos coincidentes mediante una técnica denominada Índice de Qgram . y luego aplica el cálculo ED en esos términos seleccionados.

Una ventaja de la técnica de índice Qgram es que es compatible con búsqueda difusa .

Un posible enfoque para adaptar el índice QGram es construir un índice invertido usando Qgrams. Allí almacenamos todas las palabras que constan de Qgram particular (en lugar de almacenar una cadena completa puede usar una ID única para cada cadena).

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

Luego, cuando consultamos, calculamos el número de Qgrams comunes entre el texto de la consulta y los términos disponibles.

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

Para los términos con un alto número de Qgramos comunes, calculamos el ED/PED contra el término de la consulta y luego sugerimos el término al usuario final.

you can find an implementation of this theory in following project. Feel free to ask any questions. https://github.com/Bhashitha-Gamage/City_Search

Para estudiar más sobre Editar distancia, Prefijo Editar índice de Qgram, ver el siguiente video del Prof. Dr. Hannah Bast https://www.youtube.com/embed/6pUg2wmGJRo (La lección comienza a partir de las 20:06 )

0
agregado