Esquemas en Solr

Esquemas en Solr
Siguiendo con esta serie dedicada al buscador Solr vamos a avanzar en su configuración y utilización.

En esta ocasión hablaremos del esquema. El esquema es el corazón del buscador ya que define los campos de que dispone y cómo van a ser tratados en al indexarse y al realizar las consultas.

Solr nos provee con los tipos de datos, analizadores y filtros necesarios para hacer que el buscador se comporte exactamente como necesitemos. Además siendo código abierto siempre podemos hacernos nuestros propios analizadores si fuera necesario.

 

El esquema

El esquema define la estructura de datos del indice y como van a ser analizados los datos en el momento del indexado.

Lo primero que debemos definir en nuestra estrategia de indexado es qué datos vamos a almacenar en el indice y con qué objeto. Solr no es un motor de base de datos y no la sustituye, pero puede almacenar determinados datos de cada registro, evitando que tengamos que ir a la base de datos para recuperarlos.

Habitualmente el resultado de la búsqueda se presenta como un listado que muestra una serie de datos del registro en cuestión. Independientemente de si un campo participa o no en el indexado, podemos almacenarlo en el indice y recuperarlo junto con los resultados de nuestras consultas.

Podríamos dividir las aplicación de los campos en los siguientes grupos:

Campos para la búsqueda de texto
Son los campos principales de búsqueda. Un mismo dato puede analizarse de diferentes formas generando distintos campos de búsqueda en el esquema.
Facetas y filtros
Campos que nos permiten afinar la búsqueda, ya sea sesgando el resultado con filtros o rangos o distribuyendo los resultados según taxonomías
Campos almacenados
No participan en el resultado de la búsqueda, pero nos aportan información que podemos visualizar.

Un campo puede pertenecer a uno o más de estos grupos. Es por tanto importante que sepamos que campos queremos incorporar y cual va a ser su papel en el buscador.

 

Esta es la estructura del fichero schema.xml. Observamos tres zonas la definición de tipos, los campos, y la configuración.

<?xml version="1.0" ?>

<schema name="Ejemplo" version="1.1">
    <!-- tipos de datos -->
    <types>
        <fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
    </types>

    <!-- Campos del esquema -->
    <fields>
        <field name="id" type="string" indexed="true"  stored="true"  multiValued="false" required="true"/>
    </fields>

    <!-- configuración del esquema -->
    <uniqueKey>id</uniqueKey>
    <defaultSearchField>id</defaultSearchField>
    <solrQueryParser defaultOperator="OR"/>
</schema>

 

Tipos de datos

Solr dispone de distintos tipos de datos que podemos modificar para conseguir que se comporten de una forma u otra. Entre ellos podemos encontrar varios tipos de enteros y reales, booleanos, fechas, etc.

Vamos a describir los diferentes tipos para ver su aplicación.

 

Tipos numéricos

En cuanto a los campos numéricos. Solr tiene un representación aproximada de los tipos Java; int, float, long y double. Solr puede guardar los campos numéricos simultáneamente en distintos niveles de precisión. Esto aumenta la velocidad de las querys por rango, pero aumenta también el tamaño del índice.

Numéricos estándar:

<fieldType name="int" class="solr.TrieIntField" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="float" class="solr.TrieFloatField" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="long" class="solr.TrieLongField" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="double" class="solr.TrieDoubleField" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>

Numéricos “rápidos”. A menor valor de precisionStep (no cero) mayor tamaño del trie:

<fieldType name="tint" class="solr.TrieIntField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="tfloat" class="solr.TrieFloatField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="tlong" class="solr.TrieLongField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
<fieldType name="tdouble" class="solr.TrieDoubleField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>

 

Fechas

Disponemos de un tipo fecha para nuestro esquema. El formato de fecha válido es 2012-01-30T23:59:59Z. También para este tipo tenemos la opción de ajustar el atributo precisionStep.

<fieldType name="date" class="solr.TrieDateField" omitNorms="true" precisionStep="0" positionIncrementGap="0"/>
<fieldType name="tdate" class="solr.TrieDateField" omitNorms="true" precisionStep="6" positionIncrementGap="0"/>

 

Otros Tipos

boolean
Almacena true/false
binary
Campo binario. Para almacenar los datos deben codificarse en Base64
random
Se utiliza para hacer ordenaciones aleatorias de los resultados mediante campos dinámicos.
ignored
No es un tipo como tal, es descartado ya que ni se indexa ni se guarda. Cualquier campo que tenga indicado este tipo será ignorado en el proceso de indexado.
location
Almacena una coordenada geográfica indicando su longitud y latitud. Permite Hacer filtros u ordenaciones por distancia. Google Shopping, mediante su propio sistema de geolocalización, es capaz de indicarte las tiendas mas cercanas a tu posición actual con stock del producto que buscas. Con este tipo de datos podemos hacer exactamente esto ;)
geohash
Similar al tipo anterior, pero guarda un hash obtenido con la longitud y latitud del punto, en vez de guardar el par de datos.
point
Similar a los anteriores pero permite localización en espacios n-dimensionales.
<fieldType name="boolean" class="solr.BoolField" sortMissingLast="true" omitNorms="true"/>
<fieldtype name="binary" class="solr.BinaryField"/>
<fieldType name="random" class="solr.RandomSortField" indexed="true" />
<fieldtype name="ignored" stored="false" indexed="false" multiValued="true" class="solr.StrField" />
<fieldType name="location" class="solr.LatLonType" subFieldSuffix="_coordinate"/>
<fieldtype name="geohash" class="solr.GeoHashField"/>
<fieldType name="point" class="solr.PointType" dimension="2" subFieldSuffix="_d"/>

 

Tipos de texto

Hasta este momento hemos visto una gran variedad de tipos de datos en Solr. Pero su verdadera potencia está en los “textos”. En este ámbito tenemos dos tipos básicos; el tipo string (solr.StrField) y text (solr.TextField).

El string almacena cadenas tal cual las recibe, es case sensitive y sólo permite búsquedas literales.

<fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>

 

El tipo text en cambio, permite todo tipo de tokenizaciones y filtros. Es con mucho el tipo mas versátil del buscador.

La definición de un tipo texto genérico puede ser algo así:

<fieldType name="text" class="solr.TextField" positionIncrementGap="100">
    <analyzer type="index">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" enablePositionIncrements="true" />
        <filter class="solr.LowerCaseFilterFactory"/>
    </analyzer>
    <analyzer type="query">
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="stopwords.txt" enablePositionIncrements="true" />
        <filter class="solr.LowerCaseFilterFactory"/>
    </analyzer>
</fieldType>

Según lo hemos configurado. Hemos creado un campo que permitirá búsquedas por palabras y que no será sensible a las mayúsculas. Además no tendrá en cuenta una serie de palabras indicadas en el fichero stopwords.txt guardado en el directorio conf

Podemos ajustar el tipo a las peculiaridades de distintos idiomas a la hora de tokenizar y de hacer steming (búsqueda de raíz semántica). En la documentación de Solr podemos encontrar el siguiente ejemplo de campo de texto para nuestro idioma:

<!-- Spanish -->
<fieldType name="text_es" class="solr.TextField" positionIncrementGap="100">
    <analyzer>
        <tokenizer class="solr.StandardTokenizerFactory"/>
        <filter class="solr.LowerCaseFilterFactory"/>
        <filter class="solr.StopFilterFactory" ignoreCase="true" words="lang/stopwords_es.txt" format="snowball" enablePositionIncrements="true"/>
        <filter class="solr.SpanishLightStemFilterFactory"/>
        <!-- more aggressive: <filter class="solr.SnowballPorterFilterFactory" language="Spanish"/> -->
    </analyzer>
</fieldType>

 

Añadir campos al esquema

 

Una vez hemos definido los tipo de de datos que necesitamos en nuestro esquema, sólo tenemos que añadir cada campo en la sección “fields” de nuestro esquema.

<field name="id" type="string" indexed="true" stored="true" multiValued="false" required="true"/>

 

name
Nombre del campo.
type
Tipo de datos a mostrar.
indexed
Indica si el campo es analizado e incorporado al indice como “buscable”. Para buscar, facetar o filtrar los resultados por un campo es imprescindible que esté indexado.
stored
Indica si en el índice de Solr debe almacenar el valor y retornarlo con los resultados.
multivalued
Convierte el campo en una colección que admite multiples valores.
required
Indica si es un campo obligatorio o no.
compressed
(true/false) Indica si el campo debe almacenarse comprimido o no. Reduce el tamaño del índice, pero afecta al rendimiento tanto de los procesos de indexado como de consulta. Sólo es aplicable para TextField y StrField. En los escenarios en que he utilizado Solr el rendimiento siempre a primado sobre las consideraciones de espacio. En condiciones normales el almacenamiento es más barato que la CPU.

Hay algunos atributos más pero se salen del alcance de este post.

Campos dinámicos

En condiciones normales, Solr conoce los campos antes de recibirlos. Pero hay escenarios en los que nos encontramos con metadatos que no podemos prever. Para esto, Solr aporta una solución muy flexible. Podemos definir campos dinámicamente manteniendo el tipado de datos.

<dynamicField name="*_d"  type="double"  indexed="true"  stored="true"/>
<dynamicField name="*_i"  type="integer"  indexed="true"  stored="true"/>
<dynamicField name="*_s"  type="string"  indexed="true"  stored="true"/>
<dynamicField name="*_txt"  type="text"  indexed="true"  stored="true"/>
<dynamicField name="ref_*"  type="text"  indexed="true"  stored="true"/>

En estos ejemplos, vemos como hemos definido cinco campos dinámicos, que aplicarán un tipo u otro en función de un prefijo o sufijo en el nombre del campo. Si Solr recibe un campo llamado comentario_cliente_253_txt lo indexa y almacena como texto.

 

Configuración adicional

Tras la sección encontramos otras configuraciones del esquema

uniqueKey
Indica que campo contiene un indicador único para los documentos en Solr. No es imprescindible tener un identificador único,de penderá de la aplicación que le estemos dando al buscador, pero habitualmente será necesario.

Cuando Solr reciba un documento con un identificador que ya tiene lo considerará una actualización. Eliminará el documento antiguo que será sustituido por el nuevo. También es útil para hacer borrados selectivos y es necesario para la funcionalidad de elevación de Solr.

defaultSearchField
Si no explicitamos un campo la búsqueda se realizará sobre en campo por defecto.
defaultOperator
Indica el operador que aplicará Solr si no decimos lo contrario veremos más sobre esto cuando veamos como hacer consultas.
<uniqueKey>id</uniqueKey>
<defaultSearchField>text</defaultSearchField>
<solrQueryParser defaultOperator="OR"/>

 

Copiado de campos (copyField)

Como ya hemos visto, Solr puede analizar los datos de entrada de maneras distintas. Y es posible que nos interese poder hacer diferentes tipos de búsquedas para un mismo campo. Cuando copiamos un campo no creamos una versión idéntica del anterior. Volcamos los datos del primer campo en el segundo, y en función del tipo seleccionado se realizan un análisis y búsqueda diferenes.

<copyField source="nombre" dest="text"/>

Tanto el campo de origen como el de destino deben estar creados en la sección de campos del esquema.

 

Caso práctico

Como a estas alturas ya tenemos suficientes recursos vamos a ver un ejemplo práctico.

Pongamos que tenemos una tienda online de aparatos electrónicos. Sabemos que tenemos nombres de productos del tipo SmartMusic 200 (Si, me lo he inventado ;) )

El problema con este tipo de literales es que nuestros clientes es posible que lo busquen de formas muy variadas.

  • Smart-Music 200
  • Smart Music 200
  • Smartmusic 200
  • SmartMusic-200

Obviamente queremos que nuestro producto aparezca en todos los casos, e incluso al buscar smart o music.

También se da el caso de que queremos, que sin complicarnos mucho, tengamos resultados más o menos relevantes para cualquier búsqueda.

 

Nuestros requerimientos podrían ser los siguientes:

  1. Buscar el nombre por fragmentos de palabras (lo que hemos comentado).
  2. Distribuir los resultados por categoría (facetar).
  3. Filtrar por rango de precios.
  4. No tener que acceder a la base de datos para componer el listado de resultados.
  5. Sabemos que nuestros productos pueden tener múltiples identificadores únicos (Part number, EAN13, etc.).
  6. Devolver resultados de forma sencilla.

 

<schema name="varios" version="1.4">
   
    <types>
        <fieldType name="string" class="solr.StrField" sortMissingLast="true" omitNorms="true"/>
        <fieldType name="int" class="solr.TrieIntField" precisionStep="0" omitNorms="true" positionIncrementGap="0"/>
        <fieldType name="tdouble" class="solr.TrieDoubleField" precisionStep="8" omitNorms="true" positionIncrementGap="0"/>
        <fieldType name="tdate" class="solr.TrieDateField" omitNorms="true" precisionStep="6" positionIncrementGap="0"/>
        <fieldType name="text" class="solr.TextField" positionIncrementGap="100">
            <analyzer type="index">
                <tokenizer class="solr.StandardTokenizerFactory"/>
                <filter class="solr.LowerCaseFilterFactory"/>
            </analyzer>
            <analyzer type="query">
                <tokenizer class="solr.StandardTokenizerFactory"/>
                <filter class="solr.LowerCaseFilterFactory"/>
            </analyzer>
        </fieldType>
        <fieldtype name="text_splited" class="solr.TextField">
            <analyzer type="query">
                <tokenizer class="solr.WhitespaceTokenizerFactory"/>
                <filter class="solr.WordDelimiterFilterFactory"
                   generateWordParts="1"
                   generateNumberParts="1"
                   splitOnCaseChange="1"
                   catenateWords="0"
                   catenateNumbers="0"
                   catenateAll="0"
                   preserveOriginal="1"
                   />
                <filter class="solr.LowerCaseFilterFactory"/>
                <filter class="solr.StopFilterFactory"/>
                <filter class="solr.PorterStemFilterFactory"/>
            </analyzer>
            <analyzer type="index">
            <tokenizer class="solr.WhitespaceTokenizerFactory"/>
                <filter class="solr.WordDelimiterFilterFactory"
                   generateWordParts="1"
                   generateNumberParts="1"
                   splitOnCaseChange="1"
                   catenateWords="1"
                   catenateNumbers="1"
                   catenateAll="0"
                   preserveOriginal="1"
                   />
                <filter class="solr.LowerCaseFilterFactory"/>
                <filter class="solr.StopFilterFactory"/>
                <filter class="solr.PorterStemFilterFactory"/>
            </analyzer>
        </fieldtype>
    </types>
   
    <fields>
        <field name="id" type="int" indexed="true" stored="true" required="true" />
        <field name="ref" type="string" indexed="true" stored="true" multiValued="true" />
        <field name="nombre" type="text" indexed="true" stored="true" />
        <field name="categoria" type="string" indexed="true" stored="true" />
        <field name="marca" type="string" indexed="true" stored="true" />
        <field name="descripcion" type="text" indexed="true" stored="false" />
        <field name="url" type="text" indexed="false" stored="true" />
        <field name="url_img" type="text" indexed="false" stored="true" />
        <field name="precio" type="tdouble" indexed="true" stored="true" />
        <field name="stock" type="int" indexed="true" stored="true" />
        <field name="fecha_alta" type="tdate" indexed="true" stored="true" />
       
        <!-- "Campo vago" -->
        <field name="text" type="text" indexed="true" stored="false" multiValued="true" />
        <!-- Para tokenización alternativa de campo nombre -->
        <field name="nombre_buscar" type="text_splited" indexed="true" stored="false" />
    </fields>
   
    <!-- Volcado a "campo vago" -->
    <copyField source="nombre" dest="text"/>
    <copyField source="ref" dest="text"/>
    <copyField source="categoria" dest="text"/>
    <copyField source="descripcion" dest="text"/>
    <copyField source="marca" dest="text"/>

    <!-- copiamos nombre a un nuevo tipo distinto-->
    <copyField source="nombre" dest="nombre_buscar"/>
   
    <uniqueKey>id</uniqueKey>
    <defaultSearchField>text</defaultSearchField>
    <solrQueryParser defaultOperator="OR"/>
</schema>

 

Veamos como hemos solucionado los problemas planteados mediante este esquema. Hemos añadido al esquema los datos de nuestro catálogo ajustándonos a los requerimientos funcionales.

 

1. Buscar por nombre dividiendo palabras

Para hacer búsquedas dividiendo palabras, hemos configurado el tipo “text_splited” partiendo de un TextField. Además de la división habitual por espacios hemos indicado que debe dividir cuando se encuentra un cambio de mayúsculas a minúsculas y viceversa, cuando encuentra números y guiones.

 

2. Facetar por categoría

El campo categoría es de tipo string por ser “literal”, hemos indexado el campo para poder facetar y hacer filtros con este criterio. Veremos como funcionan facetas y filtros en otro post.

 

3. Filtrar por rango de precios

El campo precio está indexado. Además hemos utilizado la versión trie del tipo para optimizar el rendimiento.

 

4. Retornar datos suficientes como para pintar el listado

Hemos almacenado en el índice los campos necesarios para pintar cada elemento el el resultado de búsqueda: nombre, enlace, imagen, categoría marca, precio, stock y fecha de alta. Podemos mostrar un listado como este:

 

Listado de resultados

 

No debéis preocuparos por el peso. Solr no devolverá todos los resultados que se ajusten a la búsqueda. Tan solo nos devolverá la primera página.

 

5. Múltiples identificadores

Utilizamos el campo ref para almacenar los diferentes identificadores de nuestros productos. Hemos utilizado un campo string ya que el identificador es alfanumérico y sólo debe admitir concordancias exactas. Es un campo multivaluado lo que nos permite indicar tantas referencias como sean necesarias.

 

6. Devolver resultados de forma sencilla

Si observáis el esquema, veréis que hemos creado un campo de tipo text llamado también text. Este campo no está en la base de datos de nuestro comercio. Lo hemos creado sobre la marcha y hemos copiado mediante copyField el contenido de todos los campos de texto con contenido relevante.

El campo está creado como multivaluado. Esto es porque el copyField no los concatena, añade nuevas entradas a la colección. Solr analizará cada entrada aplicando las reglas de análisis del tipo text. Al buscar por ese campo, que hemos definido como campo por defecto, estará buscando las palabras que le pasemos en todo los textos copiados.

Es habitual utilizar esta forma de buscar en el indice, pero yo no soy partidario de este tipo de búsquedas. Cuando buscamos sobre tantos campos no tenemos un control real de la relevancia. Si un registro tuviera tres veces la palabra Music en su descripción, podría salir por delante del producto que hemos puesto de ejemplo sin un motivo aparente.

Solr tiene un rendimiento excepcional, es preferible una query más compleja y restrictiva y en función de los resultados lanzar una segunda búsqueda con resultados de menor relevancia.

Hacemos la búsqueda restrictiva. Si nos muestra dos resultados mostramos sólo esos resultados, ya que serán muy relevantes. Si no encuentra nada, decimos “No hemos encontrado nada, pero que tal vez puedan interesarte estos otros resultados”, y mostramos los N primeros resultados de una búsqueda más abierta.

 

 

En la próxima entrada veremos como indexar nuestra base de datos en Solr.

 

 

- —————————— -
Imagen del mp3 por kyo-tux (CC BY-NC-SA 3.0)

 

7 comentarios para “Esquemas en Solr”

  • gustavo dice:

    Muy completa la explicación del esquema, además las otros “posts” de solr están muy interesantes.

    Muchas gracias

  • Borja López dice:

    Muy buen post, y btw, muy buen blog Alberto.

    Comentas que SOLR debe preconocer el esquema de la información que se va a indexar, o crear campos dinámicos para cubrirse las espaldas en caso de recibir algo desconocido.

    Y yo me pregunto, si mi tienda electrónica tuviera varios productos a la venta (por ejemplo libros y portátiles), cada uno con una serie de atributos diferentes, ¿convendría utilizar el mismo índice con estos campos dinámicos, o por contra crear varios índices según el producto?

    Saludos.

    • Alberto Pérez dice:

      Muchas gracias Borja.

      Depende de la forma en que quieras que se comporte a la hora de buscar.

      Lo normal será que esos campos únicos por categoría sean críticos para definir el producto y por lo tanto es mejor que los conozcas de antemano y apliques una lógica de indexación y consulta ad-hoc para cada una.

      Cuando utilizas campos dinámicos, en cierto modo pierdes el control. Incluso definiendo una buena política de indexado, con muchos tipos de campo dinámico que te permitan aplicar distintos procesadores según tu criterio de negocio, en tiempo de consulta será fácil ponderar con mucho detalle.

      Respondiendo a la gallega, puedes hacer una cosa y la otra; puedes hacer un core genérico en el que indexas los productos juntos atendiendo a sus atributos comunes y luego preparar una búsqueda avanzada en cada “sub-tienda”, cada una atacando a un core más específico de cada categoría.

      Solr ofrece muy buen rendimiento, esto te permite incluso montar una capa inteligente sobre el buscador, que lance la misma consulta contra diferentes cores, y en función de lo que reciba decida que pintar.

      Saludos

      • Borja López dice:

        Otra opción rápida que leí por el foro de Lucene es añadir un atributo más en el índice (discriminante en este caso) para diferenciar qué entidades recuperar en cada caso.

        Al final me estuve documentando sobre los cores de Solr, y en un ratito lo he implementado sin problema ninguno. La solución más eficiente (y elegante at the same time), de forma que queda el core0 para un tipo de productos y el core1 para otro totalmente distinto (0..N).

        Un saludo,
        Borja.

  • mujica tho dice:

    hola mira tengo una duda cuando ago la indexacion no me agrega las mayusculas ya use todos los Tokenizer WhitespaceTokenizerFactory, StandardTokenizerFactory etc… pero sigue sin hacerlo podrias orientar porfa

Deja un comentario

Uso de cookies

Este sitio web utiliza cookies para que usted tenga la mejor experiencia de usuario. Si continúa navegando está dando su consentimiento para la aceptación de las mencionadas cookies y la aceptación de nuestra política de cookies, pinche el enlace para mayor información.plugin cookies

?>