Solr - Lucene

jueves, 17 de febrero de 2011

Caches en Lucene/Solr

El presente post describirá las diferentes caches existentes en Solr y Lucene, sus parámetros más comunes y algunos puntos a tener en cuenta a la hora de utilizarlas, o no.


Funcionamiento 

Las caches que nos facilitan Solr y Lucene tienen en esencia el mismo objetivo que cualquier cache: devolver lo que se está buscando de manera más rápida.
Las caches en Solr están asociadas a un “Index Searcher”, es decir, se crean al iniciarse este y finalizan cuando este termina. El motivo de este funcionamiento es porque el índice cambia cuando se realiza un “commit”, mientras esto no ocurra, los documentos que contiene el índice son siempre los mismos y por eso las caches contienen datos validos. Cuando esto ocurre, se va preparando o “auto-calentando” (autowarm) un nuevo “Index Search” que estará vinculado al nuevo índice. Durante este periodo de “auto-calentamiento” se importa información del “Index Searcher” actual al que se está preparando; con ella, las caches. El “Index Searcher” actual sigue activo hasta que termina de atender todos los pedidos que tenía. Al finalizar comienza el nuevo “Index Searcher” a funcionar; este es un proceso se repite con cada “commit” y/o “optimize”.
Las caches se van llenando a medida que se ejecutan las distintas “queries”; hasta llegar a la cantidad máxima de entradas. A medida que se van ejecutando diferentes “queries” mayor cantidad de entradas tendremos en nuestras caches, por ende mayor probabilidad de aciertos.
Para evitar perder toda la información recolectada por una cache durante “su vida” se utiliza el parámetro “autowarmCount”.  Este indica la cantidad de datos que regenerara de la cache que está dejando de utilizarse en la cache que va a empezar a utilizarse.
Los diferentes parámetros para configurar cada tipo de cache se encuentran en archivo solrconfig.xml. 


Parámetros para la mayoría de las caches 

“Class” 
En este atributo se especifica la implementación de caches que que quiere utilizar. Existen dos implementaciones incluidas en Solr: 
  • “solr.search.LRUCache”, se usan las entradas mas recientemente utilizadas de la cache y se descartan las menos usadas. 
  • “solr.search.FastLRUCache”, tiene la misma funcionalidad que “LRUCache”, pero si al eliminar las entradas menos usadas no se llega al limite estipulado (“acceptableWaterMark”), fuerza la eliminación de mas entradas sin importar el orden de acceso a las mismas. 
  • Para reducir los tiempos sincronización, y de esta manera utilizar mas eficientemente multiples núcleos en una CPU, “FastLRUCache” se basa en la implementación “ConcurrentLRUCache”  y otras técnicas. 
Al mismo tiempo, podrían desarrollarse implementaciones personalizadas y especificar en este atributo su nombre completo. El único requisito es que las mismas implementen la interfaz org.apache.solr.search.SolrCache<K, V>. 
Se implementan sobre filterCache, queryResultCache, documentCache, fieldValueCache. Se explayaran mas adelante. 

“size”, La máxima cantidad de entradas en la cache. 

“initialSize”, Es el tamaño inicial del mapa (“key-value”). 

“autowarmCount” 
La cantidad de entradas que se importaran de la cache vieja.En Solr 4.0 el “autowarmCount” puede ser especificado como porcentaje (ej. "90%"). Esto es evaluado con respecto a la cantidad de ítems que existen en la cache. 

“minSize” (opcional) 
Sólo se aplica en FastLRUCache. Cuando la cache alcanza su tamaño, la cache trata de bajarlo hasta su “minSize”. Por defecto es 0.9 * size. 

“acceptableSize” (opcional) 
Sólo funciona en FastLRUCache. Si la cache no puede bajar su tamaño hasta “minSize” lo hace hasta “acceptableSize”. El valor por defecto es 0.95 * size. 

“cleanupThread” (opcional) 
Sólo aplicable en FastLRUCache, por defecto viene apagado (“false”). 
Si se activa, la limpieza correrá en un hilo separado dedicado. Se considera utilizarlo para tamaño (“size”) de cache muy grande. Cuando el tamaño de la cache alcanza el máximo esto activa la limpieza, la cual puede llevar bastante tiempo. 


Diferentes tipos de cache 

“queryResultCache” 
Guarda resultados de búsquedas, es decir, conjuntos de “IDs” de documentos ordenados por algún criterio de la “query” que devuelve el resultado; los resultados de las “queries” normales.
Ejemplos basados en el índice generado con los “exampledocs” del trunk de Solr:
 

“filterCache”  
Almacena todos los conjuntos de IDs de documentos sin ordenar que den respuesta a una “query”.  
Dependiendo de la cantidad de documentos que contenga el índice, la cache se organiza de dos maneras diferentes.  
En el caso de que existan pocos documentos dentro del índice, la cache guarda todos los Ids correspondientes a cada valor de campo de cada filtro.  
Cuando los documentos en el índice son muchos, esta cache almacena vectores de bit por cada valor de campo de cada filtro. Este bit es un valor booleano que indica si el documento en cuestión tiene, o no, coincidencia con el filtro realizado. Cada posición de bit dentro del vector tiene su correspondencia con el ID de documento que genera Lucene internamente, como una secuencia de números enteros; no es el ID definido en el “schema.xml” como clave única. 
Tiene tres diferentes propósitos: 
  1. Guarda los resultados de cualquier “Filter Query” (parámetros de fq) que Solr explícitamente pidió que se ejecute. Cada filtro es ejecutado y “cacheado” separadamente, cuando es tiempo de usarlos para limitar el número de resultados devueltos por una query lo hace usando un conjunto de intersecciones. Este es el principal objetivo de esta cache. 
  2. Es usada en algunos casos para “Faceting”; cuando está habilitado el método “TermEnum”, se agrega una entrada en la cache por cada “Term” (termino) evaluado. 
Si la opción de configuración “<useFilterForSortedQuery/>” de solrconfig.xml está habilitada (“true”), se utilizará para ordenamiento. Ejemplo basado en el indice generado con los “exampledocs” del trunk de solr: 
q=manu:Apple&fq=name:iPod&fq=cat:electronics 
q=manu:Belkin&fq=name:iPod&fq=cat:electronics 

Los filtros name y cat (“fq”), son almacenadas en este tipo de cache; no varían entre ambas “queries”.
El campo manu de la “query” es variable entre las distintas consultas y por eso no se incluye como una “Filter Query” y será guardado en la “queryResultCache”. 

“fieldValueCache” 
Guarda los valores de los términos que son de rápido acceso. Se utiliza para el “faceting” sobre campos “multi-valuados” o “tokenizados”.
Es similar a la “FieldCache de Lucene”, se produce una entrada en la cache por cada campo. Utiliza “un-inverted Field”. 

“documentCache” 
Guarda Documentos Lucene que fueron buscados y traídos del disco rígido.
Esta cache no se puede usar como origen para usar el “autowarm”. En su lugar, puede utilizarse “static warming” que puede forzarse a ser ejecutado cuando un “newSearcher” o “firstSearcher” se va a comenzar. 
El tamaño para esta cache debería ser siempre mayor que “<max_results>” * “<max_concurrent_queries>” para asegurar que Solr no tendrá que ir a buscar al disco rígido un documento mientras se está haciendo un pedido. Cuanto más documentos guardemos en esta cache mayor será el consumo de memoria. 
Cada Documento en la cache contiene una lista de referencias a los campos. Cuando está habilitado “enableLazyFieldLoading” (enableLazyFieldLoading = true) y hay “documentCache”, los documentos que son traídos desde el “IndexReader” sólo contendrán los campos especificados en “fl”. El resto de los campos serán marcados como “LOAD_LAZY”.
Cuando posteriormente haya un “hit” en la cache sobre esa “uniqueKey”, los campos cargados serán usados directamente, de ser requeridos. En cambio, los campos marcados como “LOAD_LAZY”, serán obtenidos desde el “IndexReader” y los documentos actualizarán sus referencias a estos campos; los cuales no estarán más marcados como “LOAD_LAZY”. 
Por lo tanto, el mismo objeto podrá ser reusado para distintos parámetros de “fl”, pero los campos para ese documento crecerá según los parámetros de “fl”. 

“User/Generic Caches” 
Los usuarios que hayan realizado “plugins” personalizados de Solr para sus aplicaciones podrán configurar caches genéricas que serán mantenidas por Solr; pueden tener opcionalmente “autowarm” si un regenerador personalizado se especifica.
Por ejemplo una cache de usuario podría ser implementada para realizar sugerencias de palabras mal escritas a través de “spellcheckers”; en el siguiente blog se desarrolla el tema “spellchecker”. 

“The Lucene FieldCache” 
Esta cache no es manejada por Solr, por lo cual no tiene opciones de configuración y se utiliza “static warming”; no puede ser “autowarmed”.
Se usa sobre campos “monovaluados” y que no están “tokenizados”; es decir, campos donde si uno dice el número de documento se puede saber con certeza el valor del termino en ese campo.
Es una cache de bajo nivel que tiene Lucene. La utiliza para acceder eficientemente a todos los valores de un campo en memoria en lugar de ir a buscarlo al disco. Es necesaria para el ordenamiento y puede ser utiliza para realizar “faceting” con Solr, entre otras cosas.
Esta cache almacena por cada campo (“field”) el valor de cada documento en memoria; un vector de tamaño relacionado a la cantidad de documentos indexados.
Si los documentos son del tipo ordinal (“integer”, “long”, etc.) se comparan rápidamente entre ellos para ordenarlos.
Si los documentos son del tipo “string” y se le da el lugar necesario a la cache para que realice las comparaciones entre ellos, la cache será un vector de strings (String[]). Se guardara dentro del vector los términos (“term”) de cada documento por cada campo del índice. Se guardará en un vector la representacion de String[] para buscar en él. El ordenamiento en este caso es mas lento que en los enteros (“integer”) porque hay que comparar strings.
Si los documentos son del tipo “string”, pero no se especifica el lugar necesario para realizar las comparaciones, se generaran dos vectores. El primero contendra todos los terminos únicos (“unique terms”) del indice y el segundo guardará enteros (“integers”) que representan los documento en el indice. En este método el acceso a los datos es más lento, por tener dos vectores desreferencias, pero el ordenamiento se hace sobre enteros en vez de “strings”.

Ejemplo:
[Chevrolet, Dodge, Fiat, Peugeot]   [0, 2, 1, 3, 2, 1, 1, 1, 4, 3, 2, 2, 1, 4] 
       4 terminos únicos                             14 documentos 

Analizando el segundo vector del ejemplo podemos observar que el documento 0 no coincide con ningún termino del primer vector, el documento 1 contiene el elemento 1 del primer vector (Dodge) y asi continuamos hasta terminar el vector.
Si reemplazamos en el segundo vector los valores del primer vector, a modo de ejemplo y para ser mas representativo el ejemplo, quedaria: 

[0, Dodge, Chevrolet, Fiat, Dodge, Chevrolet, Chevrolet, Chevrolet, Peugeot, Fiat, Dodge, Dodge, Chevrolet, Peugeot] 


Consideraciones a tener presente  
  • Caches muy grandes consumen mucha memoria. El tener la mayor     cantidad posible de documentos dentro de la cache no significa que sea lo mejor o presente el mejor desempeño. 
  • En caso de tener “queries” concurrentes (al mismo tiempo) se sugiere poner el tamaño de cache a 10 * “cantidad de documentos que se devuelven”. Esto es para garantizar que cada “query” tenga el  espacio necesario en cache al momento de utilizarla. 
  • Verificar siempre de la pagina de administración de Solr la “cache statistics” los valores mostrados, estos nos guiaran a obtener la mejor performance.
  • “Hit rate” muy bajos significan que la cache está perjudicando el desempeño en vez de beneficiarlo. 
  • “Eviction rate” muy altos pueden significar que la cache es muy chica y quizás también este perjudicando el desempeño de Solr. 


Conclusión 
Se debe realizar un gran análisis para determinar si es necesario o no utilizar las caches; observar los resultados en “statistics” para ello. 
Si bien las caches tienen como objetivo mejorar el desempeño de Solr, una mala utilización de las mismas puede ser más perjudicial que beneficioso; consumo innecesario de recursos, realizar “autowarming” sin sentido, etc.

Agradecimientos: 
A Tom, Emma y Diego por ayudarme a realizar este post. 

Referencias:

miércoles, 2 de febrero de 2011

Ejemplo de Solr para sugerir términos en las búsquedas.


El siguiente texto no pretende ser una guía detallada de cómo hacer las cosas, sino una suerte de referencia en los puntos tratados; una humilde devolución a la comunidad, en este mi primer post, que espero a alguna persona le sea de utilidad.


Pre-requisitos:
Para seguir el ejemplo que se planteará es necesario tener instalado Apache Solr. Solr es una plataforma de búsqueda del proyecto open source Apache Lucene, esta desarrollada en Java y funciona dentro de de un contenedor Servlet, como puede ser Jetty que viene embebido dentro del ejemplo de Solr y el cual utilizaré, o Tomcat si se prefiere. También es necesario tener instalado MySQL y Solrj, que es un cliente java que usaré para interactuar con Solr; también viene dentro de él.
La idea de este ejemplo es más didáctica y de estadística que de funcionalidad.


Lo que pretendo, es generar una herramienta que sugiera términos de búsqueda, basados en las búsquedas realizadas por otros usuarios en el pasado. Para ello montare dos núcleos diferentes dentro de Solr.
El primero tendrá guardados muchos
Tweets de Twitter y será con el que interactuará el usuario final; utilizaré un template el cual se puede configurar de diversas maneras, según las necesidades de cada cliente; parte se explicará a medida que se avance en el ejemplo.
El segundo núcleo contendrá las frases buscadas por los usuarios en el primer núcleo, después de ser analizadas y filtradas, y con estas frases les irá sugiriendo a los usuarios las palabras a escribir, a medida que escriben en una caja de texto; se profundizará sobre este tema en breve.
Para poder realizar lo antes enunciado se necesitará configurar distintos archivos de Solr, se utilizará MySQL y también una aplicación hecha en Java; el siguiente
vinculo muestra los lineamientos sobre los que me basé para realizar el presente ejemplo.


Manos a la obra!
Primero, si tengo Solr iniciado lo detengo.
Son necesarias dos carpetas para guardar todo lo referente a los núcleos; para este ejemplo core1 y core2. Primero creo la carpeta core1 dentro de la raíz del Solr (“Solr Home”), dentro de esta carpeta muevo, no copio, la carpeta “conf”. Ahora copio el core1, también en la raíz de Solr, y le cambio el nombre a core2. La siguiente imagen muestra el esquema de archivos:

  


Por último debo crear el archivo solr.xml, que es el encargado de decirle a Solr cuantos núcleos tiene corriendo y su ubicación, entre otras cosas; mirar su wiki mayor referencia.
Este sería el código a agregar dentro de solr.xml:
<?xml version="1.0" encoding="UTF-8" ?>
<solr persistent="true" sharedLib="lib">
       <cores adminPath="/admin/cores">
               <core name="core1" instanceDir="core1">
                       <property name="solr.data.dir" value="solr/core1/data"/>
               </core>
               <core name="core2" instanceDir="core2">
                       <property name="solr.data.dir" value="solr/core2/data"/>
               </core>
       </cores>
</solr>

Para probar el funcionamiento enciendo Solr, si no cambiamos la configuración por defecto, ingreso a la siguiente dirección y me tendría que responder solr.

El primer núcleo (core1), como ya mencione anteriormente, contiene el índice principal con todos los Tweets completos utilizados para este ejemplo y se utilizará para responder todas las consultas realizadas por el usuario. Para que los usuarios puedan interactuar con Solr se implemento una interfaz gráfica con Velocity, la cual me permite organizar las búsquedas de distintas maneras utilizando el VelocityResponseWriter; no es el objetivo de este texto mostrar la configuración del mismo.
El segundo (core2), también ya mencionado anteriormente, contendrá un índice con todas las frases que los usuarios buscan en el core1. El mismo me permitirá sugerirle al usuario, a medida que ingresa texto en la caja de búsqueda, las posibles frases que desea escribir; por dar un ejemplo, si quisiese escribir “argentina” y voy escribiendo “ar”, me podría sugerir “arco”, “argumento”, “arrendar”, etc., si estas palabras estuvieran guardadas dentro del índice.
Para poder ingresar (indexar) en el core2 las frases buscadas por el usuario, en el core1, es necesario analizar las búsquedas realizadas. La manera de hacerlo en este ejemplo, es habilitando el log que genera Jetty.

Del log obtendré las frases buscadas y por medio de la biblioteca Solrj le preguntaré a Solr, si las mismas se corresponden con los datos indexados dentro del índice del core1.
Por ejemplo, si en el índice del core1 tuviera almacenados documentos relativos a la fauna, y el usuario buscase la palabra “motherboard”, no tendría sentido indexar esta palabra en el core2; recuerden esto es un ejemplo, que podría realizarse de otras maneras, pero la idea es cubrir varios tópicos distintos en un mismo ejemplo.
Para habilitar el log de Jetty, apago Solr si esta encendido, y modifico el archivo jetty.xml que se encuentra dentro de la carpeta /etc. Si observo dentro del archivo aprecio que en una parte del xml está deshabilitado el código para que guarde el log; lo habito y reinicio el servidor para que tome los cambios; los archivos de logs serán guardados por su fecha en /logs.

Parte modificada del xml:
<Ref id="RequestLog">
    <Set name="requestLog">
        <New id="RequestLogImpl" class="org.mortbay.jetty.NCSARequestLog">
            <Set name="filename">
               <SystemProperty name="jetty.logs" default="./logs"/>
                  /yyyy_mm_dd.request.log
            </Set>
           <Set name="filenameDateFormat">yyyy_MM_dd</Set>
           <Set name="retainDays">90</Set>
           <Set name="append">true</Set>
           <Set name="extended">true</Set>
           <Set name="logCookies">true</Set>
           <Set name="LogTimeZone">GMT</Set>
       </New>
    </Set>
</Ref>


Una vez obtenidas las frases y si contienen o no resultados, analizaré la frecuencia de búsqueda de las mismas. La cantidad de repeticiones de cada frase en log me indicará la ponderación de las mismas.
Por ejemplo, si en el log figura “casa” cuatro veces y “casamiento” tres veces, al ir escribiendo “cas”, por la ponderación otorgada, se sugerirá primero “casa” y luego “casamiento”.
Una vez concluida la selección de frases, su frecuencia y si tienen resultados, o no, debo guardar toda esta información en una base de datos; para ello utilicé MySQL.
Esquema para la base de datos:

  


Modificar schema.xml (core2) para que coincidan los campos del autosuggest con los del índice.
<!--campos para usar autosuggest-->
<field name="user_query" type="edgytext" indexed="true" stored="true" omitNorms="true" omitTermFreqAndPositions="true" />
<field name="count" type="int" indexed="true" stored="false" omitNorms="true" omitTermFreqAndPositions="true" />
 
Habilitar “Data Import Handler” para poder comunicar a Solr con MySQL; al hacerlo, se explica a continuación cómo, le digo a Solr en que campos de su índice deberá guardar los valores provenientes de MySQL y de que campos; es un emparejamiento de datos entre Solr y MySQL.
Para lograr lo antes enunciado modifico solrconfig.xml para que tenga el siguiente código:
<requestHandler name="/indexer/autosuggest" class="org.apache.solr.handler.dataimport.DataImportHandler">
<lst name="defaults">
               <str name="config">dih-config.xml</str>
       </lst>
       <lst name="invariants">
               <str name="optimize">false</str>
       </lst>
</requestHandler>


A continuación creo el archivo dih-config.xml a la misma altura de path que solrconfig.xml (/core2/conf). 
 Agrego el siguiente código:
<?xml version="1.0"?>
<dataConfig>
        <!-- Datos para entablar comunicación con el driver de MySQL,
        y por ende la base de datos -->
        <dataSource type="JdbcDataSource" readOnly="true"
        driver="com.mysql.jdbc.Driver" 
        url="jdbc:mysql://localhost:3306/solr" user="root" password=""/>
<document name="autoSuggester">
<!-- realiza una consulta contra MySQL; trae como resultado “id”,
“query” y “contador” pero sólo de los valores en los cuales el
resultado sea igual a 1; esto trae todas las búsquedas que realizo
el usuario en el core1 y que tuvieron al menos un valor por
respuesta. -->
<entity name="main" query="select id, query, contador from
autosuggest where resultado = 1">
    <!-- “field column” es el campo de MySQL y “user_query”
     es el campo del índice de Solr donde se guardaran los
     valores obtenidos; acá se produce el emparejamiento. -->
    <field column="query" name="user_query"/>
    <field column="contador" name="count"/>
                            <field column="id" name="id"/>
                         </entity>
</document>
</dataConfig>


Parte del código java (agradecimiento a  Juan “Grande”) que toma los frases consultadas, y su frecuencia, del log de Jetty, analiza si los mismos tienen coincidencias a través de Solrj, los guarda en MySQL y luego los indexa, utilizando “Data Import Handler”; obtiene los datos de MySQL y los guarda en el core2.
El Parámetro necesario para llamar la aplicación es la dirección y nombre del archivo del log.


SolrLogImporter.java
package com.plugtree.solr;
public class SolrLogImporter {
      
private static Logger log = LoggerFactory.getLogger(SolrLogImporter.class);

private Collection<LogQuery> queries = new LinkedList<LogQuery>();
      
       private SolrServer solrServer;
       public SolrLogImporter(String url) throws MalformedURLException {
           solrServer = new CommonsHttpSolrServer(url);
       }
      
       private void loadLog(String filename, String handler) throws IOException, SolrServerException
       {
           BufferedReader in = new BufferedReader(new FileReader(filename));
           String s;
           Pattern p = Pattern.compile(".*GET " + handler + "\\?q=([^&]*).*");
           s = in.readLine();
           while(s != null) {
               Matcher m = p.matcher(s);
               if (m.matches()) {
                   String query = m.group(1);
                   query = URLDecoder.decode(query, "UTF-8");
                   queries.add(new LogQuery(query, hasResults(query)));
               }
               s = in.readLine();
           }
           in.close();
       }
      
       private boolean hasResults(String q) throws SolrServerException {
           SolrQuery solrQuery = new SolrQuery();
           solrQuery.setQuery(q);
           solrQuery.setQueryType("dismax");
      
           QueryResponse solrResponse = solrServer.query(solrQuery);
           SolrDocumentList results = solrResponse.getResults();
      
           return results.size()>0;
      }
  
      private void updateDatabase() throws SQLException, ClassNotFoundException {
          Class.forName("com.mysql.jdbc.Driver");
          Connection con = DriverManager.getConnection ("jdbc:mysql://localhost/solr","root", "");

PreparedStatement querySt = con.prepareStatement(
"SELECT contador, id FROM autosuggest WHERE query = ?");

PreparedStatement updateSt = con.prepareStatement(
"UPDATE autosuggest SET contador = ?, resultado = ? WHERE id = ?");
                   
          PreparedStatement insertSt = con.prepareStatement(
          "INSERT INTO autosuggest (id, query, resultado, contador) VALUES (?, ?, ?, ?)");
              
for(LogQuery q: queries) {
    querySt.setString(1, q.getQ());
    ResultSet rs = querySt.executeQuery();
                      
    if(rs.first()) {
        updateSt.setInt(1, rs.getInt(1)+1);
        updateSt.setInt(2, q.hasResults() ? 1 : 0);
        updateSt.setString(3, rs.getString(2));
        updateSt.executeUpdate();
    } else {
         /* TODO Usar ID entero que se autoincremente */
         insertSt.setString(1, UUID.randomUUID().toString());
         insertSt.setString(2, q.getQ());
         insertSt.setInt(3, q.hasResults() ? 1 : 0);
         insertSt.setInt(4, 1);
         insertSt.executeUpdate();
             }
          }
       }
   
       private void updateIndex (String url) throws MalformedURLException, IOException {
           URL dir =  new URL(url);
  InputStream response = dir.openStream();
  BufferedReader reader = new BufferedReader(new InputStreamReader(response));
  reader.close();
       }
      
       public static void main(String[] args) {
           try {
               SolrLogImporter solrLogImporter = 
               new SolrLogImporter("http://localhost:8983/solr/core1/");            
      solrLogImporter.loadLog(args[0], "/solr/core1/browse");
      try {
          solrLogImporter.updateDatabase();
          try {
              solrLogImporter.updateIndex 
              ("http://localhost:8983/solr/core2/indexer/autosuggest?command=full-import");
          catch (MalformedURLException e) {
              e.printStackTrace();
          } catch (IOException e) {
              e.printStackTrace();
          }
      } catch(SQLException ex) {
          log.error("Unable to update database", ex);
      } catch(ClassNotFoundException ex) {
          log.error("Unable to load SQL driver", ex);
  }
} catch(IOException ex) {
   log.error("Unable to read log file", ex);
} catch(SolrServerException ex) {
   log.error("Unable to query Solr server", ex);
}
    }
}

Mira el código completo acá.


Para que funcione el autosuggest es necesario modificar parte del template de velocity, para que coincidan los nombres de las variables de nuestros datos (índice), que llame a la dirección correcta (core1, core2) y que sepa que campos debe devolver para realizar el autosuggest; los archivos se encuentran dentro de /core1/conf/velocity.
Doc.vm -> modificar nombre de los campos de nuestro índice.
head.vm -> esto debe apuntar al core2, que realiza el auto-suggest, pero el archivo es del core1 quien lo llama; utiliza JQuery para realizar su trabajo (para mayor referencia dirigirse a su página principal). 

La lógica del código debería ser similar a lo siguiente:
<script>
    $(document).ready(function(){
        extraParams = {
            'q': function() { return 'user_query:' + $("\#q").val();},
            'sort':'count desc, user_query asc',
            'wt': 'velocity',
            'v.template': 'campo'
        };
              
        $("\#q").autocomplete('http://localhost:8983/solr/core2/select', {                      'extraParams':extraParams
        });
    });
</script>
campo.vm -> va dentro del core2 y es el que devuelve los valores del campo al ser llamado por jquery. Ejemplo del código:

#foreach($doc in $response.results)
       $doc.getFieldValue('user_query')
#end


Agradezco a Diego, Juan, Tomas y Matías por ayudarme en la confección del mismo.