Estructura del índice en buscadores que procesan en el cliente

Si el buscador interno se basa en un índice de palabras que no es muy extenso, es posible enviar al cliente dicho índice y que sea el navegador quien realice el proceso de búsqueda usando Javascript y manejo del DOM. Una solución consiste en crear un archivo de texto plano con el índice de palabras clave, algo así como lo que vimos en el tema anterior:

...
table       86
tbody       86,12
...

Así las palabras clave apuntan a los números de documento donde se encuentran. A su vez podemos tener otro archivo de índices con las URL:

...
12       /ijk/lmn.html
86       /abc/def/ghi.html
...

Esto se puede enviar al cliente mediante variables de tipo string dentro de un archivo .js, por ejemplo:

var indice = "...;table=86;tbody=86,12;...";
var claves = "...;12=/ijk/lmn.html;...;86=/abc/def/ghi.html;...";

Luego en el navegador podríamos aplicar la función split() para convertir esas cadenas en los correspondientes arrays y trabajar sobre ellos.

Pero cuando hice mi buscador con Javascript no usé este concepto de índice de claves sino que construí el índice exclusivamente usando los encabezados identificados. Esto es un ejemplo del HTML del índice ubicado en el <body> del documento:

<div id="resultados"></div>
<div id="indiceContenidos">
    ...
    <div id="cabecera" class="item">
        ...  
        <div style="text-indent:3em">
            <a href="/temas/xhtml-css/cabecera.html#h0" class="h0">
                Elementos XHTML de cabecera
            </a>
        </div>
        ...
    </div>
    ...
</div>

El índice se envía dentro del documento, como elementos HTML, ubicado en el contenedor con id="indiceContenidos" que inicialmente tiene las propiedades de estilo overflow: hidden; height: 0; con lo que no será mostrado en pantalla pero su contenido está presente. Luego cada página se encierra en un contenedor como id="cabecera", donde el identificador es el nombre del documento sin la extensión. Por lo tanto este contenedor tendrá todos los vínculos a los encabezados del documento cabecera.html. Cada vínculo <a> se encierra también en otro <div> para dotarlo de indentado con estilo.

En esencia se trata de cargar una variable global con una colección de elementos <a> del índice, acción que se realiza con la carga de la página. Luego el usuario al buscar realmente itera por esa colección extrayendo el texto interior del elemento y comprobando la coincidencia con la cadena de búsqueda. La ventaja de enviar al cliente el índice como elementos HTML es que puede usarse directamente como un índice de contenidos, o mejor dicho, una tabla de contenidos, para lo cual con Javascript cambiamos las propiedades de estilo del contenedor con id="indiceContenidos":

conten.style.overflow = "visible";
conten.style.height = "auto";

La gran ventaja de estos buscadores con Javascript es que no consumen recursos del servidor al procesar las búsquedas. Todo se hace en el navegador. Pero el tamaño es una limitación que condiciona estos buscadores, por lo que para un mayor volumen de encabezados ya no sería eficiente enviárselo al navegador del cliente. Este buscador incluye 862 vínculos a todos los encabezados de los 16 documentos que componen el glosario XHTML+CSS. El buscador se compone entonces de:

  • Documento busquedas.html de 152KB con los vínculos a los encabezados.
  • Javascript busquedas.js de 7KB para manejar el proceso de búsquedas.
  • Estilo externo en parte del archivo xhtmlcss-estilo.css. Este ocupa 5KB pero lo concerniente al buscador es una cantidad menor.

En lo que sigue se expone como funciona este buscador. Pero partimos de que ya hemos construido el índice según la estructura señalada y que volvemos a repetir de forma esquemática:

<div id="indiceContenidos">
    ...
    <div id="documento-1">
        ...  
        <div><a>...</a></div>
        <div><a>...</a></div>
        ...
    </div>
    ...
    <div id="documento-n">
        ...  
        <div><a>...</a></div>
        <div><a>...</a></div>
        ...
    </div>
    ...    
</div>

Para construir este índice de forma automática puede consultar la herramienta de un tema siguiente que habla de construir los índices. Se trata de una utilidad que le permitirá fabricar este índice partiendo de los documentos html seleccionados, guardándose el HTML generado en un archivo de texto. En esa página se explica como usar esa herramienta.

Estructura HTML-CSS del buscador con Javascript

Visualmente nuestro buscador tiene esta apariencia:

busquedas

Se trata de un cuadro de texto para la cadena de búsqueda, un botón para iniciar la búsqueda, otro para abrir las opciones inferiores y un botón para mostrar el índice. Las opciones de búsqueda permiten buscar entradas que contengan todas las palabras o sólo alguna palabra. Además también pueden buscarse sólo palabras completas y diferenciar mayúsculas de minúsculas.

No conviene mostrar el patrón de búsqueda si este buscador se destinara a un sitio de uso general. Pero como aquí el objetivo es mostrar todos los detalles posibles, se incluye ese patrón de búsqueda. Así incluso el usuario interesado puede probar otros patrones. Desde el punto de vista de la seguridad hemos de recordar que este buscador funciona en el navegador del usuario, por lo que los errores por patrones indebidos sólo afectarán al navegador y no al servidor.

Por último hay un limitador para el número máximo de resultados, pues para ciertas búsquedas como letras por ejemplo, el número de resultados puede ser muy alto. Pero de todas formas la mejor manera de comprender todo esto es ponerlo en ejecución.

La estructura general por bloques es la siguiente:

<div class="vinculos">
    ...Aquí van la cadena de búsqueda y botones...
</div> 
<div id="pagina-contenido" >
    <div id="opcionesBusca">OTRAS OPCIONES DE BÚSQUEDA:<br />
        ...Aquí van  las opciones...
    </div>
    <div id="resultados">
        ...Aquí se ponen los resultados de la búsqueda...  
    </div>
    <div id="indiceContenidos">
        ...Aquí va el índice de vínculos...
    </div>    
</div>

El estilo CSS específico para este documento es:

div#indiceContenidos {
    overflow: hidden; 
    height: 0;
    }
div#indiceContenidos a {
    text-decoration: none; 
    }
div#vinculosIndice {
    display: none; 
    }
div#resultados {
    border: rgb(49, 99, 98) solid 1px; 
    }
div#resultados a {
    text-decoration: none; 
    color: navy; 
    }
div#opcionesBusca {
    border: rgb(49, 99, 98) solid 1px; 
    overflow: hidden; 
    height: 0;
    }

El contenedor con id="resultados" está en principio vacío de contenido, pues ahí se ponen de forma dinámica los resultados de cada búsqueda. El contenedor con class="vinculos" tiene esta estructura:

<div class="vinculos">
    Cadena: <input type="text" id="cadBusca" value="" size="40" 
        onblur="patronear()" />
    <input type="button" id="botonBuscar" value="Buscar" 
        onclick="buscar()" />
    Encontrados: <em id="encontrados">0</em>
    <input type="button" id="masOpciones" value="Opciones"
        onclick="opciones()" />
    <input type="button" id="verIndice" value="Índice"
        onclick="indiceContenidos()" /> 
    <div id="vinculosIndice">...</div>
</div>

El cuadro de texto para la cadena de búsqueda incluye un evento onblur de tal forma que al salir de ese elemento se actualiza el patrón de búsqueda con la función patronear() de Javascript, funciones que se incluyen en el módulo busquedas.js. El botón para iniciar la búsqueda llama a la función buscar(). Las otras funciones son opciones() e indiceContenidos() que muestran el cuadro de opciones y el índice total de contenidos, puesto que ambos contenedores están inicialmente ocultos tal como se declaró en el CSS. El índice total de contenidos con id="indiceContenidos" se oculta con height:0 pues necesitamos tenerlo cargado en la página porque vamos a realizar la búsqueda iterando por los elementos <a>, así que aunque no se visualizan sí están presentes. Veámos ahora la estructura del cuadro de opciones:

<div id="opcionesBusca">
    Conjuntivas:<input type="checkbox" id="conjuntiva" />
    Palabra completa:<input type="checkbox" id="palabraCompleta" />
    Diferencia mayúsculas/minúsculas:<input type="checkbox" id="caseMM" />
    Patrón:<span class="monospace">/</span>
        <input type="text" id="patronBusca" value="" size="40" 
            class="monospace" />
        <span class="monospace">/</span>
        <input type="text" id="opcionesFlags" size="5" value="" 
        class="monospace" />
    <input type="button" id="actualizaPatron" value="Actualizar patrón" 
        onclick="patronear()" />
    Resultados máximos:<input type="text" id="maxResulta" value="100" 
        size="5" />, 
    número de búsquedas máximas:<input type="text" id="maxBusca" 
        value="0" size="5" /> de un total de 
        <span id="iterTotal">0</span><br /> 
</div> 

Por último el contenedor de resultados (con id="resultados") está vacío inicialmente y del contenedor de índices (con id="indiceContenidos") ya se expuso su estructura en el apartado anterior.

El Javascript para hacer funcionar el buscador interno

La estructura de variables globales y funciones de este módulo Javascript es la siguiente:

  • Variables globales
    • inputPatron: Una referencia al elemento <input type="text" id="patronBusca"> para almacenar el patrón.
    • inputOpcionesFlags: Una referencia al elemento <input type="text" id="opcionesFlags"> que almacena las opciones o flags del patrón.
    • vinculos: Una variable que contendrá la colección de elementos <a> del índice.
    • hasta: Un entero que inicialmente contiene el número de elementos de ese índice.
    • particulas: Una variable de tipo string que contiene una lista de partículas (preposiciones, adverbios, etc) que serán eliminadas de la cadena de búsqueda.
  • Funciones
    • window.onload = function(): Inicializar buscador con la carga de la página.
    • function patronear(): Preparar patrón de búsqueda.
    • function buscar(): Buscar ese patrón.
    • function opciones(): Mostrar u ocultar cuadro opciones.
    • function indiceContenidos(): Mostrar u ocultar índice de contenidos.

No voy a exponer el código completo de este Javascript. Con navegadores como Firefox puede descargar este código con facilidad y consultarlo. La función de inicialización con la carga de la página simplemente se encarga de referenciar las variables globales y llenar el array o colección de vínculos. Las funciones que muestran u ocultan los contenedores de opciones o índice no tiene ninguna complejidad. Sólo veremos las funciones para preparar el patrón y para buscar.

Cuando salimos del cuadro de la cadena de búsqueda se ejecuta el evento onblur que llama a la funcion patronear(), que es la encargada de preparar el patrón.

function patronear() {
    document.getElementById("encontrados").innerHTML = 0;
    inputPatron.value = "";
    var cadenaBusca = document.getElementById("cadBusca").value;
    var palabraCompleta = document.getElementById("palabraCompleta").checked;
    if (cadenaBusca != "") {
        //escapa caracteres reservados de expresiones regulares
        cadenaBusca = cadenaBusca.replace(/([\$\(\)\*\+\.\[\]\?\\\/\^\{\}\|])/g, "\\$1");
        //elimina espacios al inicio o final
        cadenaBusca = cadenaBusca.replace(/^\s+|\s+$/g, "");
        //suprime las preposiciones, artículos y otras palabras intermedias, es 
        //decir rodeadas de un espacio por ambos lados.
        var patron = new RegExp("\\b(?:" + particulas + ")\\b", "gi");
        cadenaBusca = cadenaBusca.replace(patron, " ");
        //Convierte más de un espacio en uno
        cadenaBusca = cadenaBusca.replace(/\s+/g, " ");        
        //Quita los espacios iniciales y finales por haber partículas ahí
        cadenaBusca = cadenaBusca.replace(/^\s+|\s+$/g, "");
        if ((cadenaBusca == "") || (cadenaBusca == " ")){
            inputPatron.value = "";
        } else {
            //reemplaza los espacios intermedios por la alternativa | o conjuntiva .*? 
            //pero diferenciando si buscamos en palabra completa o no
            var conj = "\|"
            if (document.getElementById("conjuntiva").checked) {
                conj = ".*?";
            }
            if (palabraCompleta){
                cadenaBusca = cadenaBusca.replace(/\s+/g, "\\b" + conj + "\\b");
                cadenaBusca = "\(?:\\b" + cadenaBusca + "\\b\)";
            } else {
                cadenaBusca = cadenaBusca.replace(/\s+/g, conj);
                cadenaBusca = "\(?:" + cadenaBusca + "\)";
            }
            inputPatron.value = cadenaBusca;
            var opciones = "";
            var difMayusMinus = document.getElementById("caseMM").checked;
            if (!difMayusMinus) {
                opciones = opciones + "i";
            }
            inputOpcionesFlags.value = opciones;
        }
    }    
}

El patrón que preparamos tiene dos campos, uno con la expresión regular que se guardará en el <input type="text" id="patronBusca"> y que teníamos referenciado en la variable global inputPatron, y por otro lado los flags o modificadores de la expresión regular que almacenamos en el <input type="text" id="opcionesFlags"> y que también referenciamos en la variable global inputOpcionesFlags. Estas variables luego serán consultadas en la funcion buscar().

El patrón se construye escapando los caracteres reservados de expresiones regulares y luego eliminando las particulas de uso frecuente. Se trata de la variable global particulas que contiene una lista separada por la barra vertical de preposiciones, adverbios, artículos y otras palabras de uso frecuente en el lenguaje y que no aportan nada a la búsqueda. Eliminamos espacios en el inicio o final de la cadena de búsqueda y convertimos varios espacios en uno sólo. Si al final la cadena contiene algo más que un espacio entonces construimos el patron.

En este momento tendremos una cadena de búsqueda con palabras separadas por espacio. Si el <input type="checkbox" id="conjuntiva" /> está desactivado es que vamos a buscar alguna de esas palabras. Construimos el patrón separando las alternativas con la barra vertical (una disyuntiva). En otro caso estamos buscando todas las palabras y se construye separándolas con .*? que equivale a una conjuntiva. Luego incorporamos en su caso los delimitadores \b para palabra completa. En el cuadro de flags agregamos la opción i si es que la búsqueda es insensible mayúsculas-minúsculas, es decir, no diferencia mayúsculas de minúsculas.

Al salir de la función patronear() ya tenemos en el cuadro <input type="text" id="patronBusca"> el patrón y en el cuadro <input type="text" id="opcionesFlags"> los flags. Cuando se pulse el botón corresondiente ejecutamos la función buscar():

function buscar() {
    document.getElementById("encontrados").innerHTML = 0;
    var divResultados = document.getElementById("resultados");
    divResultados.innerHTML = "";
    var cadBusca = inputPatron.value;
    var opciones = inputOpcionesFlags.value;
    var maxBusca = 1 * document.getElementById("maxBusca").value;
    var maxResulta = 1 * document.getElementById("maxResulta").value;
    var maxResultaBase =  maxResulta;
    var item = 0;
    if (cadBusca != "") { 
        var patron = new RegExp(cadBusca, opciones);
        document.getElementById("iterTotal").innerHTML = hasta;
        if (maxBusca < hasta) {
            hasta = maxBusca;
        }
        for (var i=0; i<hasta; i++) {
            var cadena = getInnerText(vinculos[i]);
            var resultado = cadena.match(patron) ;
            if (resultado != null) {
                item++;
                if (item <= maxResulta) {
                    divResultados.innerHTML += ' [' + item +
                    '] <a href="' + vinculos[i].href + '">' +
                    vinculos[i].innerHTML + '</a><br />';
                } else {
                    var men = "";
                    if (maxResulta == maxResultaBase) {
                        men = " primeros ";
                    } else {
                        men = " siguientes ";
                    }
                    document.getElementById("encontrados").innerHTML = item;
                    var mensaje = window.confirm("Se muestran los" + men + maxResultaBase + 
                            " resultados. ¿Continuar buscando?");
                    if (mensaje) {
                        maxResulta += maxResultaBase;
                    } else {
                        break;
                    }
                }
            }
        }
    }
    document.getElementById("encontrados").innerHTML = item;
} 

La variable maxBusca limita el número máximo de iteraciones en la colección de vínculos. La variable maxResulta controla el número de resultados a devolver. Luego declaramos la expresión regular con new RegExp(cadBusca, opciones). En este caso usamos un string en cadBusca, el patrón construido antes con la función patronear(), agregando los flags. El proceso de búsqueda consiste en iterar por la colección de vínculos extrayendo el texto interior de esos elementos <a>. Lo hacemos con la función getInnerText() del módulo general.js con funciones que unifican el comportamiento de los navegadores al extraer el texto interior de un elemento. Para ello es necesario vincular ese módulo en la cabecera del documento de búsquedas:

<script type="text/javascript" src="alguna_carpeta/general.js" 
charset="ISO-8859-1"></script>

Si no desea incorporar ese módulo general.js puede ubicar la función getInnerText() directamente en el módulo busquedas.js. Sobre ese texto interior del elemento <a> aplicamos el patrón de búsqueda, obteniendo un array de coincidencias en la variable resultado. Controlamos que el número de resultados sea menor que los declarados para presentar (variable maxResulta), en cuyo caso los vamos acumulando con innerHTML construyendo dinámicamente elementos vínculo. Si sobrepasa maxResulta preguntamos al usuario si seguimos iterando dándole la opción de romper el bucle.


Son posibles muchas mejoras, pero esencialmente he mostrado una forma para hacer un buscador interno con Javascript. Su gran ventaja es que se ejecuta en el navegador del usuario, donde la devolución de resultados es más rápida y además no consume recursos del servidor. Las desventajas son principalmente la necesidad de que el usuario tenga activado Javascript y el tamaño del archivo de índices que debe ser transferido desde el servidor. Este aspecto puede mejorarse si en lugar de hacer un índice HTML lo hiciéramos con sólo texto, pero aún así este buscador siempre estará limitado por un tamaño máximo de ese archivo índice. No parece adecuado enviarle al cliente un documento incluyendo el archivo de índice con tamaño que exceda demasiado los 150KB.

En estos momentos he implementado en este sitio otro buscador basado en índices almacenados como arrays serializados en archivos de texto y ejecutándose en el servidor que se expone en el tema siguiente. Pero aún así mantengo este buscador con Javascript pues, independientemente de su calidad, es un buen ejercicio para aprender aspectos de Javascript como manejo del DOM o expresiones regulares.