Recuperación de la Información sobre Modelos de Dominio

Anuncio
ii
Abstract
In recent years, advances in the eld of Information Retrieval has accelerated the provision of intelligent tools
that help us to access information in an unstructured way using natural language. These tools are capable of
searching for web pages, map locations, jobs, products, images and many other elements.
In parallel to advances in information retrieval, the eld of software engineering shows a widespread adoption of
object oriented programming, modeling business process and entities into domain models. This fact has been
strengthened in the context of enterprise applications, which support organization's business and processes.
This thesis is about the construction of a search engine over domain models, a problem called the Domain
Model Search problem.
This thesis makes a survey of the state of the art in information retrieval, domain models and their persistence.
Additionally, we make case studies of tools that solve similar problems.
As part of experimentation, there has been built a complete and extensible search engine for indexing and
retrieving information over domain models, validating this framework on dierent domains and achieving a
state of the art performance.
Keywords:
information retrieval, search engines, indexing, software engineering, domain models,
enteprise applications
Resumen
Desde hace algunos años, los avances en el campo de Recuperación de Información han acelerado la provisión de herramientas inteligentes que nos ayudan a acceder a la información de manera desestructurada
utilizando el lenguaje natural. Estas herramientas que son capaces de buscar páginas web, ubicaciones
en el mapa, empleos, productos, imágenes y muchos otros elementos.
En paralelo a los avances en materia de recuperación de la información, el campo de la ingeniería de
software muestra una amplia adopción de la programación orientada a objetos, modelando procesos y
entidades de negocio en modelos del dominio. Este hecho se ha visto potenciado en el contexto de las
aplicaciones empresariales, las cuales soportan el negocio y/o los procesos de las organizaciones.
Esta tesis trata la construcción de un motor de búsqueda sobre objetos de un modelo de dominio, lo cual
llamamos el problema de Recuperación de Información sobre Modelos de Dominio.
En este trabajo se realiza un relevamiento del estado del arte en materia de recuperación de la información,
modelos de dominio y su persistencia, tomando casos de estudio de herramientas que resuelven problemas
similares.
Como parte de la experimentación se ha construido un framework completo y extensible para la indexación
y recuperación de información sobre objetos, validándolo en distintos dominios y logrando rendimientos
propios del estado del arte.
recuperación de información, motor de búsqueda, indexación, ingeniería de software,
modelos de dominio, aplicaciones empresariales
Palabras Clave:
Índice general
1. Introducción
1.1.
Motivación
1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.1.1.
Information Retrieval
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1
1.1.2.
Modelos de Dominio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.1.3.
El problema del Domain Model Search . . . . . . . . . . . . . . . . . . . . . . . . .
2
1.1.4.
Objetivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4
1.2.
Contribución de la Tesis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
5
1.3.
Plan de Tesis
5
1.4.
Algunas convenciones adoptadas
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
2. Estado del Arte
2.1.
2.2.
2.3.
5
7
Information Retrieval
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.1.1.
Clasicación de los Sistemas de IR . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
2.1.2.
Deniciones Generales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
8
2.1.3.
Métricas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
9
2.1.4.
Modelos de Information Retrieval . . . . . . . . . . . . . . . . . . . . . . . . . . . .
12
2.1.5.
Técnicas de Matching y Acceso a Datos
. . . . . . . . . . . . . . . . . . . . . . . .
21
2.1.6.
Técnicas de Puntaje y Relevancia . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
Modelos de Dominio
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
2.2.1.
Deniciones Generales . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
44
2.2.2.
Independencia del Modelo de Dominio . . . . . . . . . . . . . . . . . . . . . . . . .
50
2.2.3.
Inversión del Control e Inyección de Dependencias
. . . . . . . . . . . . . . . . . .
51
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
54
2.3.1.
Persistencia y Ciclos de Vida en Aplicaciones Enterprise . . . . . . . . . . . . . . .
55
2.3.2.
Persistencia Manual
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
2.3.3.
Persistencia Administrada . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
56
2.3.4.
Binaria
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
56
2.3.5.
Ad-Hoc
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
57
2.3.6.
XML . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
2.3.7.
Object Relational Mapper (ORM)
. . . . . . . . . . . . . . . . . . . . . . . . . . .
59
2.3.8.
Bases de Datos Orientadas a Objetos . . . . . . . . . . . . . . . . . . . . . . . . . .
65
Persistencia de Modelos de Dominio
iii
iv
ÍNDICE GENERAL
2.4.
Casos de Estudio . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
65
2.4.1.
Apache Lucene . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
66
2.4.2.
Hibernate Search . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
70
2.4.3.
Compass
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
73
2.4.4.
Análisis de Hibernate Search y Compass . . . . . . . . . . . . . . . . . . . . . . . .
78
3. Desarrollo de la Propuesta de Solución
3.1.
3.2.
3.3.
85
Análisis General del Problema . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85
3.1.1.
Modelos de IR
85
3.1.2.
Técnicas de Matching y Acceso a Datos
3.1.3.
Procesos de Indexación
3.1.4.
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . .
86
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
93
Técnicas de Puntaje y Relevancia . . . . . . . . . . . . . . . . . . . . . . . . . . . .
98
Mapeo de Clases
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
100
3.2.1.
Introducción
3.2.2.
Conguración y Mapeo
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
100
3.2.3.
Mapeos Avanzados . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
103
Diseño del Framework de IR sobre objetos . . . . . . . . . . . . . . . . . . . . . . . . . . .
108
3.3.1.
Introducción
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
108
3.3.2.
Arquitectura del Framework . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
108
3.3.3.
Técnicas de Matching y Acceso a Datos
. . . . . . . . . . . . . . . . . . . . . . . .
112
3.3.4.
Procesos de Indexación
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
118
3.3.5.
Técnicas de Puntaje y Relevancia . . . . . . . . . . . . . . . . . . . . . . . . . . . .
122
4. Experimentación
100
129
4.1.
Tipo de Pruebas Efectuadas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
129
4.2.
Pruebas con Aplicaciones de Referencia
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
130
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
130
4.3.
4.2.1.
PetClinic
4.2.2.
Klink
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
137
4.2.3.
KStore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
145
Pruebas de Calidad y Rendimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
151
4.3.1.
Pruebas de Calidad
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
151
4.3.2.
Pruebas de Rendimiento . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
153
4.3.3.
Análisis Comparativo Cualitativo . . . . . . . . . . . . . . . . . . . . . . . . . . . .
163
5. Conclusiones
167
5.1.
Conclusiones
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
167
5.2.
Trabajos Futuros . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
168
Bibliografía
171
A. Instalación del Software y el Código Fuente
175
A.1. Instalación del Software de Pruebas . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
175
A.2. Código Fuente y Sitio Web del Proyecto . . . . . . . . . . . . . . . . . . . . . . . . . . . .
176
Capítulo 1
Introducción
En este capítulo comenzaremos a denir el problema que vamos resolver en esta tesis. Las siguientes
secciones tratarán la motivación, contribución, el plan de tesis y las convenciones que emplearemos a lo
largo de este trabajo.
1.1.
Motivación
Para explicar la motivación de esta tesis, es necesario repasar algunos conceptos básicos de Recuperación
de Información (en adelante también Information Retrieval o simplemente IR ) y Modelos de Dominio
(en adelante también Domain Model ).
Las próximas subsecciones repasan brevemente deniciones generales y conceptos necesarios para denir
el problema a resolver (motivación), el cual llamaremos el problema del Domain Model Search.
1.1.1. Information Retrieval
Como primer paso creemos conveniente denir qué es Information Retrieval. Dependiendo del autor que
tomemos, existen diferentes deniciones de IR. Veamos algunas de éstas:
IR trata acerca de la representación, almacenamiento, organización y acceso a items de información
(Baeza-Yates et al., 1999).
Information retrieval (IR) consiste en encontrar material (usualmente documentos) de naturaleza
des estructurada (usualmente texto) que satisfaga una necesidad de información sobre grandes
colecciones de documentos (usualmente almacenada en computadoras) (Manning et al., 2008).
El objetivo de un sistema de IR es encontrar información que pueda ser relevante para la consulta
realizada por el usuario (Baeza-Yates et al., 1999). Estas consultas son una representación de la necesidad
de información del usuario e ingresan al sistema mediante texto libre (eventualmente pueden existir
operadores lógicos y otros complementos, pero normalmente se trata de texto libre).
Ejemplos de sistemas de IR:
Library Book Search: www.nypl.org (New York Public Library)
Web Search: www.google.com (Google Search), www.yahoo.com (Yahoo! Search)
Online Store Product Search: www.ebay.com (eBay) , www.shopping.com (Shopping.com)
Social Network Search: www.linkedin.com (Linked-In Contact Network), www.facebook.com (Facebook Social Network)
1
2
CAPÍTULO 1.
INTRODUCCIÓN
1.1.2. Modelos de Dominio
De la misma forma que hicimos con IR, vamos a comenzar dando una denición de modelo de dominio o
domain model :
Domain Model
es un modelo de objetos que incorpora comportamiento y datos (Fowler, 2002).
Esta denición, además de ser algo escueta, es perfectible. Por esto vamos a replantearla pensando a los
modelos de dominio de la siguiente forma:
Domain Model
un diseño de objetos que representa un dominio de problema de la realidad.
Este nuevo enfoque nos permite ver a los objetos no sólo como datos y comportamiento sino como
verdaderos modelos de entes de un dominio de problema. Para terminar de comprender esto hay que
explicar a qué nos referimos con realidad y dominio de problema:
La realidad comprende cualquier tipo de idea que podamos concebir, por ejemplo: un objeto concreto, la
nada, las relaciones de amistad y enemistad, etc. Un dominio de problema es una porción de la realidad
en la cual sus entes están relacionados entre si. Ejemplos de dominios de problema podrían ser las cuentas
bancarias, un lesystem o la sincronización entre procesos.
Los domain models no son un concepto nuevo en el diseño de software orientado a objetos, sino que
implementan premisas básicas de este paradigma: dentro de un dominio particular se efectúa un análisis
según el cual se modelan entes de negocio junto a sus responsabilidades, protocolo y colaboraciones. En
un domain model encontraremos objetos como productos, personas, pagos, páginas web, etc.
Volviendo a la perspectiva de Fowler, éste propone al domain model como un patrón de arquitectura
(ver subsección 2.2.1), el cual, dependiendo del sistema particular, puede o no ser el más adecuado para
implementar dicho sistema (Fowler, 2002). En esta tesis nos ocuparemos principalmente de aplicaciones
que modelan sus entidades en modelos de dominio.
1.1.3. El problema del Domain Model Search
Introducción
Cuando se desarrolla un sistema orientado a objetos, las metodologías de análisis y diseño aportan un
buen grado de certidumbre acerca de qué entidades se van a modelar y qué operaciones van a realizar.
Sin embargo, ya sea por omisión a la hora de hacer el análisis o porque se considera innecesario, inadecuado
o fuera de alcance, no solemos contemplar la necesidad de acceder a los datos de manera desestructurada
utilizando el lenguaje natural. Veamos algunos ejemplos:
Ejemplo 1.1.1.
Para una aplicación que administra una librería, construimos un modelo del negocio
con las entidades: libros, autores y sucursales. Suponiendo que este sistema almacena sus datos en una
base de datos relacional (RDBMS), denimos formularios que ejecutan consultas predenidas en lenguaje
SQL.
Si quisiéramos responder la pregunta ¾Qué libros de ciencia cción hay en la sucursal San Martín?,
deberíamos implementar un formulario que ejecute una consulta como la siguiente:
SELECT
BOOK_TITLE
FROM
BOOKS B ,
WHERE
BOOK_STORE_MAP BSM,
S . LOCATION =
AND
' SAN MARTÍN '
STORES
AND
S
B . GENRE =
' CIENCIA
FICCIÓN '
AND
BSM . STORE_ID = S . STORE_ID
BSM . BOOK_ID = B . BOOK_ID
Los inconvenientes más claros al resolver el problema de esta forma son:
Es poco exible ya que las consultas deben ser previstas durante el análisis.
1.1.
3
MOTIVACIÓN
No se extiende automáticamente a buscar elementos en diferentes esquemas. Esto es, es capaz de
buscar libros o autores que cumplen con un criterio, pero si luego agregamos el esquema música,
no incluye automáticamente al nuevo esquema.
Se depende del RDBMS particular para contar con herramientas que permitan procesar textos. El
lenguaje SQL no está diseñado para aplicar procesos que faciliten la coincidencia entre los términos
de búsqueda y de indexación (elementos sobre los que uno busca). Si vamos a desarrollar estos
procedimientos, normalmente se deben implementar en un lenguaje procedural propietario (ejemplo:
PL/SQL).
Las búsquedas recaen enteramente sobre la base de datos, de manera que para soportar volumen de
búsquedas, se debe contar con un RDBMS de gran escala.
En este ejemplo anterior comenzamos a ver que un RDBMS no se adecua al tipo de consultas que estamos
queriendo resolver. Veamos otros ejemplos de sistemas donde tenemos requisitos de procesamiento de
lenguaje natural y un RDBMS no se ajusta a la solución:
Ejemplo 1.1.2
(Consultas a Sistema de Bibliotecas)
.
A continuación analizamos dos ejemplos de
consultas en lenguaje natural a un sistema bibliotecario:
1. Consulta: libros sobre information retrieval publicados desde 1999
a) Libros debe reconocerse como una entidad, no es un tópico. Ver que si la biblioteca no cuenta
con libros, podría sugerir revistas.
b) Information retrieval se debe reconocer como un tópico. En caso de no tener sucientes libros
del tema podríamos ofrecer resultados que traten acerca de search engines o procesamiento
del lenguaje natural.
2. Consulta: traducciones de Macbeth al español
La lógica a aplicar debe reconocer que:
a) Macbeth es una obra y no un autor,
b) entre todas las obras, buscamos sólo las del español.
Nuevamente, el ejemplo nos muestra que la lógica estándar de una base de datos no es la más adecuada
ya que la mejor respuesta requiere un análisis semántico de la consulta.
Otros ejemplos de sistemas donde podríamos efectuar consultas en lenguaje natural:
Sitio Web de Comercio Electrónico:
1. Consulta: stereo
2. Consulta: alarmas vehiculares
3. Consulta: guitarras en capital
Sistema Universitario:
1. Consulta: certicación de normas técnicas
2. Consulta: olimpiadas
En todos estos casos vemos que las preguntas son consistentes dentro del contexto que maneja el usuario
que realiza las mismas, pero no necesariamente estarán contempladas por el análisis inicial del sistema.
Además, estas consultas presentan un cierto grado de ambigüedad que las vuelve difíciles de resolver con
el mismo lenguaje SQL que utilizamos para recuperar objetos y generar reportes.
4
CAPÍTULO 1.
INTRODUCCIÓN
Herramientas Actuales
En las últimas décadas han surgido herramientas de IR que nos permiten utilizar el lenguaje natural
para encontrar información útil entre vastas colecciones de documentos, libros, audio o video. Algunos
ejemplos de esto son los buscadores web como (Google, 2009b ; Yahoo, 2009). Estas herramientas de IR
transforman nuestras expresiones del lenguaje natural en items de información (enlaces a páginas web).
Además de las los buscadores web, existen herramientas maduras que permiten hacer IR sobre documentos
de texto y están emergiendo las que permiten hacerlo sobre los objetos de nuestras aplicaciones. Dentro
de las primeras, la más popular es Apache Lucene (Apache, 2009b ). Entre las segundas debemos destacar
Hibernate Search (Hibernate, 2009b ) y Compass Project (Compass Project, 2009).
El objetivo de Lucene es indexar y recuperar documentos de forma eciente y exible. Sin embargo,
Lucene no es una herramienta adecuada para indexar objetos de un modelo de dominio. Adelantándonos
al análisis del problema, podemos señalar las deciencias que encontraron los autores de Compass al
intentar utilizar Lucene como herramienta de IR sobre objetos:
Es relativamente difícil de integrar,
Sus APIs son de bajo nivel,
No es transaccional,
No soporta búsquedas sobre todos los campos de un documento,
No indexa directamente datos de un RDBMS,
Las actualizaciones de documentos son difíciles de implementar
A esto agregamos las críticas de los autores de Hibernate Search, quienes plantean que Lucene produce
desajustes en el paradigma:
Desajuste de Sincronización (Synchronization Mismatch ): con Lucene somos responsables de mantener sincronizado el almacén de objetos (típicamente un RDBMS) y los índices de Lucene.
Desajuste Estructural (Structural Mismatch ): debemos resolver el problema de efectuar un mapeo
de objetos a documentos de texto.
Desajuste de Recuperación (Retrieval Mismatch ): al recuperar información, Lucene no devuelve
directamente nuestros objetos de dominio sino objetos genéricos de Lucene.
Estos son problemas que veremos en detalle a lo largo de esta tesis y que hacen que librerías como Apache
Lucene no sean adecuadas para indexar objetos. En en esta tesis también vamos a analizar herramientas
como Compass y Hibernate Search, las cuales fueron pensadas para indexar y recuperar objetos y sobre
las cuales también implementaremos mejoras.
Teniendo un panorama de las herramientas actuales y del problema que queremos resolver, en la próxima
subsección explicaremos el objetivo de esta tesis.
1.1.4. Objetivo
El objetivo de esta tesis se puede enunciar de la siguiente forma:
Objetivo
en esta Tesis se busca resolver el problema de aplicar las técnicas de IR para encontrar infor-
mación almacenada en los objetos de los modelos de dominio. Para esto es necesario determinar las
actividades y componentes de un motor de búsqueda sobre objetos del dominio y las alternativas
de implementación de cada una de ellas.
El objetivo implica resolver el problema planteado de forma general, esto es, permitiendo indexar objetos
provenientes de cualquier modelo de dominio.
En las próximas subsecciones se trata cómo contribuye esta Tesis a la solución del problema así como
explicamos su plan de desarrollo.
1.2.
CONTRIBUCIÓN DE LA TESIS
1.2.
5
Contribución de la Tesis
Enunciemos el aporte que hace este trabajo al entendimiento del campo del conocimiento:
Esta tesis contribuye en:
la denición acerca de cuáles son los componentes principales de un motor de búsqueda sobre
objetos, sus alternativas de implementación y las consecuencias de distintas variantes de diseño,
la implementación de una solución concreta al problema de IR sobre información contenida en
objetos, escogiendo las alternativas de implementación más adecuadas.
Para probar la adecuación de la solución al problema, se implementa una pieza de software que actúa
como un framework de recuperación de información sobre objetos, tres aplicaciones de referencia sobre
las que validar el framework y un conjunto de pruebas cuantitativas y cualitativas que lo contrastan con
las soluciones previas. Este framework reeja las mejores prácticas, criterios y premisas defendidas a lo
largo de los próximos capítulos en base al estudio del problema y el análisis de las soluciones actuales.
1.3.
Plan de Tesis
En esta sección vamos a organizar el resto de la tesis, describiendo la función que cumplen los próximos
capítulos:
El segundo capítulo describe el estado del arte. Este capítulo tiene un objetivo mixto. La primera parte
incluye el soporte tecnológico y cientíco necesario en materia de IR, diseño orientado a objetos y persistencia. El objetivo de esta primera parte es sentar las bases sobre la cual analizar los componentes del
problema, sus soluciones actuales y nuestra propuesta. En la segunda parte del capítulo tomamos como
casos de estudio para su análisis en profundidad las tres herramientas más importantes de la actualidad.
Dicho análisis producirá el aprendizaje necesario para que nuestra propuesta incorpore los aciertos y evite
los desaciertos de otras herramientas.
El tercer capítulo trata la propuesta de solución y su implementación. Con las bases teóricas adquiridas
en los capítulos previos, se analiza en profundidad cada uno de los aspectos intervinientes en el problema,
las alternativas de solución y la justicación de cada una de las elecciones. Por último, se presenta un
diseño detallado de la solución y, donde corresponda la comparación, se contrastan los casos de estudio
con la solución diseñada.
El cuarto capítulo trata acerca de la experimentación. El primer objetivo de este capítulo es validar
la solución diseñada en situaciones reales de aplicación. El segundo objetivo es establecer un análisis
comparativo de resultados entre nuestra solución y las alternativas actuales, tanto en términos cualitativos
como cuantitativos.
Finalmente, el quinto capítulo concluye la tesis con las conclusiones. Las conclusiones hacen una retrospectiva de la tesis y ponen en blanco sobre negro los avances logrados así como el avance sobre la solución
del problema. Como cierre de este trabajo planteamos las futuras lineas de investigación.
1.4.
Algunas convenciones adoptadas
En el desarrollo de esta Tesis vamos a seguir consistentemente estas convenciones:
Referencias Cruzadas: la tesis está dividida en capítulos, secciones, subsecciones y apartados. Las
referencias cruzadas que haremos a lo largo de este trabajo indicarán el nivel jerárquico del contenido
referenciado utilizando estos cuatro niveles. Todas las referencias cruzadas a secciones, ejemplos y
guras se indican entre paréntesis.
Referencias Bibliográcas: las referencias se indican entre paréntesis, utilizando el autor y el año de
la publicación. Cuando exista más de una publicación del autor en el mismo año, se agregará una
letra al nal del año, tal de desambiguar la referencia en la sección de bibliografía.
6
CAPÍTULO 1.
INTRODUCCIÓN
Numeración de Ejemplos: los ejemplos están numerados secuencialmente de manera interna a la
sección a la que pertenecen. Esto quiere decir que si tenemos cinco ejemplos en el capítulo 1 con
tres en la sección 1.1 y dos en la sección 1.2, la numeración será: 1.1.1, 1.1.2, 1.1.3, 1.2.1, 1.2.2 .
Cuando expresemos algoritmos, hagamos referencia a un identicador de un lenguaje de programación, referenciemos clases o expresemos una frase literal utilizaremos la familia tipográca Sans
Serif.
Al nal de este trabajo se incluye también un índice alfabético de palabras clave.
Para nalizar este capítulo introductorio, queremos señalar que previo al desarrollo de esta tesis el autor publicó un artículo introductorio al problema. Dicho artículo se puede encontrar en la sección de
bibliografía bajo la clave (Klas, 2009).
Capítulo 2
Estado del Arte
En este capítulo se presentan los conceptos, herramientas y tecnologías necesarias para construir un motor
de búsqueda sobre objetos.
El objetivo de este capítulo es plantear el estado del arte en cada uno de los componentes del problema
y efectuar un análisis comparativo entre las herramientas actuales.
En la sección 2.1 presentamos los conceptos y técnicas utilizadas actualmente en los sistemas de IR. Estas
técnicas serán referenciadas al analizar los casos de estudio así como en el capítulo 3 para elegir la mejor
alternativa de implementación para el motor de búsqueda.
En la sección 2.2 introducimos conceptos de diseño de software como: modelo de dominio, framework,
librería, patrones de diseño y de arquitectura, inversión del control e inyección de dependencias. Estos
conceptos serán fundamentales para analizar los casos de estudio y diseñar el motor de búsqueda sobre
objetos del capítulo 3.
En la sección 2.3 explicamos las técnicas de persistencia de objetos, las cuales veremos que interactúan
con los motores de búsqueda sobre objetos e impactan sobre sus diseños.
Finalmente, en la sección 2.4 nos apoyamos en lo visto durante todo el capítulo para analizar las herramientas más importantes de IR sobre texto y objetos. Este análisis abre la discusión acerca de cómo
implementar la solución al problema planteado, la cual nalmente se desarrollará en el capítulo 3. El
análisis de los casos de estudio permitirá que nuestra propuesta de motor de búsqueda sobre objetos
adopte sus mejores prácticas y evite que repitamos sus errores.
2.1.
Information Retrieval
En esta sección presentamos los conceptos y técnicas utilizadas actualmente en los sistemas de IR.
Comenzaremos proponiendo una clasicación para los sistemas de IR (subsección 2.1.1), para luego presentar los conceptos básicos de cualquier sistema de IR (subsección 2.1.2) y las métricas que determinan
su éxito en términos de relevancia (subsección 2.1.3).
El centro de atención de esta sección estará en presentar los modelos de IR (subsección 2.1.4) y las
técnicas que vuelven a los sistemas de IR efectivos y ecientes (subsecciones 2.1.5 y 2.1.6).
2.1.1. Clasicación de los Sistemas de IR
Como introducción a los conceptos de Information Retrieval es conveniente realizar una clasicación
funcional de las herramientas de IR.
Algunos ejemplos de distintos tipos de herramientas de information retrieval:
Para usuarios nales, de uso publico:
7
8
CAPÍTULO 2.
•
ESTADO DEL ARTE
Web Search: páginas web, documentos de ocina, imágenes, etc. Ejemplo: Google Web Search
, Yahoo Web Search.
•
Product Search: bienes de uso y consumo en mercados virtuales. Ejemplo: e-Bay, Shopping.com,
MercadoLibre.
•
Scientic Publications: sobre publicaciones académicas y revistas. Ejemplo: ACM Digital Library, IEEExplore.
•
Legal: acerca de leyes y casos judiciales. Ejemplo: LexisNexis (Lexis Nexis Research, 2009).
Para usuarios nales, de uso privado:
•
Enterprise Search: sobre páginas web en Internet e intranets, e-mails y documentos en un
repositorio corporativo. Ejemplo: Oracle Enterprise Search (Oracle, 2009b ), Google Search
Appliance (Google, 2009a ).
•
Personal Search: sobre contenido en una computadora personal. Ejemplo: Google Desktop,
Windows Search.
Para uso en desarrollo de software:
•
Text Retrieval: framework de indexación de texto para aplicaciones. Ejemplo: Apache Lucene
(Apache, 2009b ) (ver subsección 2.4.1).
•
Object Search: framework de indexación de objetos. Es el tipo de framework que analizamos
en este trabajo. Ejemplo: Hibernate Search, Compass Project (ver subsecciones 2.4.2 y 2.4.3).
La herramienta que construiremos como solución al problema propuesto entra en la categoría de herramientas para uso en desarrollo de software, más precisamente en Object Search. En la próxima
subsección vamos a introducir los conceptos básicos que son comunes a todos estos sistemas.
2.1.2. Deniciones Generales
En esta sección denimos los conceptos básicos de IR. Como primer paso, repasemos la segunda de las
deniciones de IR que vimos en la subsección (1.1.1):
Information Retrieval
consiste en encontrar material (usualmente documentos) de naturaleza deses-
tructurada (usualmente texto) que satisfaga una necesidad de información sobre grandes colecciones
de documentos (usualmente almacenada en computadoras) (Manning et al., 2008).
Esta denición de Manning está sesgada hacia la indexación y recuperación de texto como artículos,
libros, etc). Este sesgo responde a los usos clásicos que se les han dado a los sistemas de IR, los cuales
tuvieron sus inicios indexando publicaciones cientícas y registros bibliotecarios (Manning et al., 2008).
Con cierto sesgo hacia ése contexto se denieron entidades y conceptos básicos como: documento, corpus
y léxico. A continuación presentamos sus deniciones:
Documentos
son las unidades hacia las que se construye el sistema de IR.
Estructuralmente, los documentos pueden ser de naturaleza homogénea o heterogénea. Por ejemplo, un
sistema orientado a documentos homogéneos podría ser un sistema de IR sobre cheros de biblioteca
(en este caso los documentos son cheros estructuralmente idénticos). Para el caso heterogéneo podemos
tomar como referencia al buscador web Google (Google, 2009b ), el cual indexa páginas web, documentos
PDF, presentaciones PowerPoint y muchos otros formatos heterogéneos.
En el problema del Domain Model Search, los documentos son los objetos del dominio de problema, los
cuales pueden ser tanto homogéneos (objetos de una misma clase) como heterogéneos (objetos de distintas
clases).
Corpus
es el conjunto de documentos recuperables en el sistema.
2.1.
9
INFORMATION RETRIEVAL
El corpus se puede caracterizar según varios criterios, los cuales dependen de la naturaleza del sistema.
Un corpus puede ser:
Estático vs. Dinámico: en base a si el contenido de sus documentos cambia o permanece inmutable
en el tiempo.
Interno vs. Externo: dependiendo si el almacenamiento de los documentos está controlado por el
sistema o fuera de su control.
Un ejemplo de corpus estático e interno puede ser el sistema bibliotecario de registro de cheros. Este
corpus es estático porque las chas no cambian en el tiempo e interno porque están en poder exclusivo
del sistema bibliotecario. Un ejemplo de corpus dinámico y externo son las páginas de Internet, cuyo
contenido varía en el tiempo sin el control de los sistemas que las indexan.
En nuestro problema de IR sobre objetos, el corpus son los objetos persistentes del sistema. Este corpus
es interno (normalmente el sistema controla los objetos de su modelo de dominio) y puede ser tanto
dinámico como estático.
Léxico
es el conjunto de términos presentes en el corpus.
A los nes prácticos, esta denición deja algunos problemas dependientes de la manera en la cual interpretamos el contenido de los documentos. Por ejemplo el léxico de un corpus de un único documento
D = ”procedimiento
ad-hoc” puede ser
L = {procedimiento,
ad-hoc} ó
L = {procedimiento,
ad, hoc}. A
los efectos de nuestro problema, tomamos la denición de (Manning et al., 2008), la cual dene al léxico
en base a los términos que poblarán el índice invertido (ver subsección 2.1.5).
Al presentar la denición de IR mencionamos el concepto de indexación, el cual podemos denir como:
Indexación
es el proceso que incorpora los documentos al corpus y actualiza los índices.
Esta denición introduce un nuevo término: el índice. Los índices son estructuras de datos que tienen por
objetivo recuperar ecientemente entidades que cumplen con cierto criterio de ltrado u ordenamiento.
En la subsección (2.1.5) estudiaremos los índices utilizados en sistemas de IR.
2.1.3. Métricas
Las métricas en IR son medidas cuyo objetivo es mensurar la percepción de "éxito" del sistema de IR
frente a una consulta. Presentemos dos medidas básicas de IR (Baeza-Yates et al., 1999; Manning et al.,
2008):
Recall
es la fracción de documentos relevantes que ha sido recuperada del total de documentos recuper-
ables.
Precisión
es la fracción de objetos recuperados que es relevante.
Figura 2.1: Precisión y Recall. Las relaciones entre las cardinalidades de los conjuntos nos dan métricas
de éxito del sistema.
10
CAPÍTULO 2.
ESTADO DEL ARTE
Estas relaciones las podemos visualizar grácamente en el diagrama de Venn presentado en la gura (2.1):
Recall =
|RR|
|DR|
P recisión =
(2.1.1)
|RR|
|R|
(2.1.2)
Las ecuaciones (2.1.1) y (2.1.2) se pueden ver en una tabla de contingencia:
DR
R
R
DR
verdadero positivo (vp)
falsos positivos (f p)
falso negativo (f n)
verdadero negativo (vn)
Cuadro 2.1: Tabla de contingencia para documentos relevantes y recuperados.
ahora redenimos las dos medidas en base a esta tabla:
Recall =
vp
(vp + f p)
P recision =
(2.1.3)
vp
(vp + f n)
(2.1.4)
A continuación profundizamos los conceptos de recall y precisión con un ejemplo:
Ejemplo 2.1.1.
C = {di } donde di son sus documentos, supongcontiene 50 documentos relevantes. En base a sus
Dado un sistema de IR con un corpus
amos que para una query puntual
q,
el conjunto
C
R = {di }i∈h0,ji
los cuales están ordenados según la relevancia asignada por el sistema. Para este orden
algoritmos y parámetros, el sistema puede encontrar un conjunto variable de resultados
con
0 6 j 6 100,
asignado, los resultados relevantes son
A medida que ampliamos la cantidad
j
RR = {d1 , d5 , d8 , d12 , d30 , d40 , d59 , d80 }.
de resultados en
R,
obtenemos curvas de precisión y recall según
esta tabla:
Número de Resultados(|Rj |)
Documentos Relevantes
(|RRj |)
Recall
|RRj |
|DR|=50
%
Precisión
|RRj |
|Rj | %
10
3
6%
30 %
20
4
8%
20 %
30
5
10 %
17 %
40
6
10 %
15 %
50
6
10 %
12 %
60
7
14 %
12 %
70
7
14 %
10 %
80
8
16 %
10 %
90
8
16 %
9%
100
8
16 %
8%
Si gracamos precisión en función del recall para segmentos de recall de 1 % de amplitud obtenemos el
gráco (2.2):
2.1.
11
INFORMATION RETRIEVAL
Figura 2.2:
P recisión(recall)
. La curva muestra cómo varía la precisión a medida que aumentamos la
cantidad de resultados retornados.
El ejemplo (2.1.1) puso de maniesto una característica de los sistemas de IR: la solución de compromiso
entre encontrar documentos relevantes (recall ) y que éstos sean una cantidad signicativa del total de
resultados (precisión ). En general estas dos medidas están relacionadas de manera inversa, es decir,
cuando una crece la otra tiene a decrecer.
Según necesitemos favorecer el recall o la precisión, esta relación inversa nos obligará a tomar decisiones
de diseño en cada sistema de IR a construir. Por ejemplo, en un buscador web el objetivo es obtener
páginas relevantes entre los primeros 10 resultados a costa de producir falsos negativos (favorece precisión), mientras que en un sistema legal es más importante abarcar todos los casos que retornar sólo
verdaderos positivos (favorece recall). Vemos entonces que el balance óptimo entre precisión y el recall es
una solución de compromiso determinada por las características del problema que resuelve la herramienta
de IR (Manning et al., 2008).
Una forma de cuanticar la relevancia en un sistema de IR es otorgarle a los documentos una calicación
de relevancia (ranking ) respecto de la búsqueda. Este ranking podría ser un valor real
este ranking, podemos denir un nivel
α
r ∈ [0, 1]. Utilizando
a partir del cual:
di ∈ RR ⇐⇒ rdi > α
Donde
di
es un documento particular y
Notemos que cuando el parámetro
si
α = 0 ⇒ RR = DR,
rdi
su ranking para una query dada.
α → 0, estamos favoreciendo el recall por sobre la precisión (en el límite,
α → 1 tendremos bajo
lo que implica un recall del 100 %). En el otro extremo, si
recall y posiblemente mejoremos la precisión (asumiendo que en general logramos posicionar resultados
relevantes entre los primeros lugares del ranking).
Continuemos presentando las métricas de IR con dos medidas adicionales, accuracy y F-measure :
Accuracy
1 Esta medida se dene utilizando los valores de la tabla (2.1):
Accuracy =
tp + tn
tp + f p + f n + tn
Asumiendo la herramienta de IR como un clasicador binario de documentos en las clases
(recordemos que la herramienta intenta obtener
R = RR = DR),
DR
y
DR
es plausible utilizar esta métrica, la
cual es adecuada para evaluar sistemas de clasicación automática. La razón por la que normalmente
DR,
DR, lo que produciría de todos
descartaremos esta medida es porque al estar los datos fuertemente desviados hacia la categoría
podemos conseguir alto accuracy clasicando todos los documentos como
modos un sistema malo, ya que el usuario preere que retornemos algún documento antes que ninguno.
Es decir, los usuarios suelen tolerar algunos falsos positivos en favor de conseguir algunos verdaderos
positivos (Manning et al., 2008).
1 En
español el término reere a cuan "correcto" es el criterio de selección de documentos.
12
CAPÍTULO 2.
F-measure
ESTADO DEL ARTE
se utiliza para establecer una solución de compromiso entre recall y precisión. La medida F
es la media armónica ponderada de la precisión y el recall:
F =
1−α
α y α ∈ [0, 1]. Para hacer una ponderación equivalente de precisión y recall tomamos
1). Si quisiéramos darle mayor importancia a la precisión utilizaríamos β < 1 mientras que
Donde
(β
=
α P1
β2 + 1 P R
1
=
β2P + R
+ (1 − α) R1
β=
α = 12
β>1
prioriza el recall.
Ejemplo 2.1.2.
Para ver por qué la F-measure utiliza la media armónica por sobre la aritmética veamos
que: recuperando el 100 % de los documentos obtenemos como mínimo un 50 % de media aritmética
por más que hayamos recuperado 1 documento relevante sobre 10.000 recuperados. Por el contrario, si
aplicamos la media armónica obtenemos:
F =
1
× 11
10000
1
1
10000 + 1
2×
= 0,00019 ∼ 0,02 %
Con este pequeño ejemplo se ve como la media armónica (la cual es siempre menor o igual a la aritmética)
es una mejor medida para la efectividad de un sistema de IR.
En esta subsección hemos presentado las medidas básicas que nos permiten evaluar en términos cuantitativos las decisiones que tomamos al diseñar un sistema de IR. En la próxima subsección presentaremos
distintos modelos de IR, cuya efectividad se mide en términos de estas métricas.
2.1.4. Modelos de Information Retrieval
Introducción
Para explicar qué es un modelo de Information Retrieval podemos utilizar una analogía:
Normalmente los fenómenos físicos se analizan mediante un modelo de la realidad. Este modelo establece
suposiciones e idealizaciones que permiten utilizar deducciones para resolver una versión simplicada del
problema real.
En el campo de las ciencias de la computación el panorama es muy similar: los problemas de Information
Retrieval también se resuelven planteando suposiciones e idealizaciones que nos permiten simplicar la
complejidad del problema subyacente. Estas suposiciones e idealizaciones forman parte de modelos de
Information Retrieval.
Otro enfoque es el planteado por (Baeza-Yates et al., 1999): el factor central de un sistema de IR es la
determinación de cuáles son los documentos relevantes para una query. La decisión acerca de la relevancia
de los documentos suele estar dada por algoritmos de puntuación (los mejor puntuados serán los más
relevantes). A su vez, estos algoritmos de puntuación operan de acuerdo a premisas básicas respecto de la
noción de relevancia de documentos. Ésas premisas son las que producen distintos modelos de IR. Dentro
de un sistema de IR, quien determina qué es relevante es el modelo de information retrieval.
Como se desprende de los párrafos previos (especialmente del segundo enfoque), la decisión de qué modelo
de IR utilizar es central al sistema. Esta decisión tiene fuertes implicancias en la indexación, recuperación
y valoración de los documentos y determinará buena parte del éxito del sistema.
En esta subsección nos dedicamos a formalizar y conocer los modelos más populares de IR para luego
poder analizar cómo se implementan en los casos de estudio y proponer una implementación para nuestro
motor de búsqueda sobre objetos.
Formalización
Es posible establecer una formalización matemática acerca de qué es un modelo de IR. La siguiente
denición pertenece a (Baeza-Yates et al., 1999):
Denición 2.1.1.
Un modelo de Information Retrieval es una 4-upla
[D, Q, F, R (qi , dj )]
donde:
2.1.
13
INFORMATION RETRIEVAL
D
es un conjunto de representaciones de documentos en la colección.
Q
es un conjunto de representaciones de las necesidades de información del usuario llamadas
queries.
F
es un marco de trabajo (framework) para modelar representaciones de documentos, queries y sus
relaciones.
R(qi , dj )
es una función de puntaje
2 con dominio en el conjunto
Q×D
e imagen en los números
reales.
Esta denición permite unicar los modelos clásicos de IR bajo un mismo marco formal (para mayor
detalle se puede consultar Baeza-Yates et al., 1999, p. 24).
A continuación vamos a tratar los llamados modelos clásicos de IR: booleano, vectorial y probabilístico.
Modelo Booleano
El modelo booleano está basado en operaciones de conjuntos entre términos de la query y los documentos.
Las expresiones de búsqueda se convierten en una expresión booleana de términos (palabras) y operadores
AND, OR y NOT. La semántica de estos operadores lógicos se traduce en la expresión debe estar presente
en el documento, puede estar presente ó no debe estar presente. A continuación veremos un ejemplo
de una query en el sistema booleano.
Ejemplo 2.1.3 (Especicación booleana de consultas).
A continuación vemos necesidades concretas de
información y consultas booleanas para satisfacerlas.
Necesidad de información: documentos acerca del emperador Julio Caesar
query = julio AN D caesar
Necesidad de información: documentos acerca de Julio Caesar o Brutus
query = (julio AN D caesar) OR brutus
Necesidad de información: documentos acerca del emperador Julio Caesar donde no se mencione
a Brutus
query = (julio AN D caesar) AN D N OT (brutus)
El modelo booleano clasica los documentos como totalmente relevantes o totalmente irrelevantes según
cumplan con la condición booleana impuesta en la query. El modelo no incorpora intrínsecamente una
medida de relevancia o similitud entre queries y documentos. La única medida intrínseca es la dicotómica:
relevante vs. no relevante. Si bien este modelo ha sido y sigue siendo ampliamente utilizado, estos
factores son una de las críticas principales hacia él (Baeza-Yates et al., 1999; Manning et al., 2008).
Denición 2.1.2.
dj
Para el modelo booleano podemos denir la similitud entre una query
q
y un documento
como:
(
1
Similitud(q, dj ) =
0
⇐⇒ q se cumple en dj
⇐⇒ q no se cumple en dj
A continuación presentamos algunos ejemplos de consultas en el modelo booleano.
Ejemplo 2.1.4.
sí y sólo si
En este ejemplo resolvemos algunas consultas donde el documento se presenta al usuario
Similitud(q, dj ) = 1.
Denimos el conjunto
Corpus = {d1 , d2 , d3 }
y la representación de sus documentos en términos:
d1 = {caesar, dictador, romano}
d2 = {hijo, caesar, octavianus}
2 El
término puntaje se utiliza como equivalente del término ranking del inglés.
14
CAPÍTULO 2.
ESTADO DEL ARTE
d3 = {brutus, conspirador, romano}
Veamos cómo resultarían algunas búsquedas booleanas sobre este corpus:
q1 : brutus → {d3 }
q2 : caesar AN D brutus → {∅}
q3 : brutus OR (hijo AN D caesar) → {d2 , d3 }
q4 : N OT (brutus) OR (hijo AN D caesar) → {d1 , d2 }
Las principales ventajas del modelo booleano son:
su simplicidad,
el soporte formal por la teoría de conjuntos,
buen grado de control al especicar la consulta.
Las principales desventajas de este modelo nacen de su denición de similitud:
al no contar con noción de coincidencia parcial, la curva de recall vs. precisión varía bruscamente
con la adición y sustracción de términos,
necesita técnicas externas al modelo para ordenar el conjunto de resultados.
El modelo booleano original (el cual utiliza sólo AND, NOT y OR) es demasiado limitado para muchas
aplicaciones prácticas, por lo cual se ha ido extendiendo para exibilizar la recuperación. Algunas de
estas operaciones extendidas son la búsqueda por proximidad y la utilización de comodines (ver subsección 2.1.5). Estas mejoras en sus capacidades de recuperación junto a la sensación de control sobre sus
resultados son seguramente los factores que hacen que en la práctica aún sea muy utilizado.
Modelo Vectorial
En este apartado exponemos el modelo vectorial propuesto por Gerard Salton (Salton et al., 1975).
Entre las limitaciones del modelo booleano, vimos que la ausencia de una función de similitud intrínseca al
modelo nos impide priorizar gradualmente los documentos en los cuales la query se ve mejor representada.
El modelo vectorial resuelve este problema introduciendo la noción de coincidencia parcial entre queries
y documentos.
En el modelo vectorial, la función de similitud entre una query y un documento permite:
retornar documentos que no contienen todas las palabras de la búsqueda,
diferenciar la relevancia de dos documentos que contienen todas las palabras de la búsqueda,
calcular la similitud entre documentos.
A continuación presentamos algunas deniciones necesarias para trabajar en el modelo vectorial (pueden
encontrarse mayormente en Baeza-Yates et al., 1999).
Denición 2.1.3 (Pesos de Relevancia).
dj
Sea un léxico de términos
denimos:
con
wi,q :
relevancia del término
ki
en la query
q
wi,j :
relevancia del término
ki
en el documento
w ∈ <+
0.
Destaquemos dos aspectos de la denición (2.1.3):
dj
ki ∈ K
, una query
q
y un documento
2.1.
15
INFORMATION RETRIEVAL
el modelo vectorial sólo permite relevancias
los términos
ki
wi,q
y
wi,j
mayores o iguales a cero,
tienen relevancias distintas según se encuentren en el contexto de la query o del
documento.
Enunciemos otra denición para caracterizar al modelo:
Denición 2.1.4
. Sea un léxico K de t ele→
−
→
−
vectores q = (w1,q , w2,q , . . . , wt,q ) y dj =
pesos de relevancia de los términos ki ∈ K
(Representación Vectorial de Documentos y Queries)
q y un documento dj ,
(w1,j , w2,j , . . . , wt,j ), donde wi,q y wi,j son
en q y dj .
mentos, una query
denimos los
los respectivos
Vemos entonces que en el modelo vectorial las queries y los documentos se representan como vectores en
un espacio t-dimensional donde cada componente es la relevancia de cada término. Intuitivamente, los
documentos que más se asemejan a una query son los que tienen su vector
d~j
más próximo al vector
~q . Esta
correlación se puede cuanticar utilizando la función coseno, la cual otorga un valor nulo para vectores
ortogonales (irrelevantes entre sí) y un valor unitario para los vectores perfectamente correlacionados
(relevantes entre sí).
Denición 2.1.5 (Similitud en el Modelo Vectorial).
documento
dj ,
Sea un léxico
K
de
t
elementos, una query
q
y un
el modelo vectorial dene la similitud entre la query y el documento como:
t
X
wi,j × wi,q
→
− →
dj • −
q
i=1
qP
Similitud (q, dj ) = cos (q, dj ) = = qP
−
→
−
t
t
2 ×
2
qk
dj × k→
w
i=1 i,j
j=1 wi,q
wa,b ∈ <+
0,
intervalo [0, 1].
En la denición (2.1.3) establecimos que
vectorial toma valores continuos en el
(2.1.5)
por lo tanto la función de similitud del modelo
A diferencia del modelo booleano, el vectorial establece un ranking de grano no que permite ordenar
los documentos en forma gradual según su relevancia. Si bien la función de similitud es continua, esto
no quita que bien podríamos establecer un valor umbral para mostrar únicamente los documentos que
cumplen
Similitud(q, dj ) > sumbral .
Es preciso notar que la representación vectorial no modela el ordenamiento relativo entre términos dentro
de un documento o una query. Esta simplicación caracteriza al modelo como uno del tipo bag of words
(bolsa de palabras).
A continuación vemos un ejemplo en el que vemos grácamente los vectores y su similitud:
Ejemplo 2.1.5.
Supongamos un léxico K = {Brutus, Caesar} y dos documentos que contienen dichas
~1 = 1 ; 4 y d~2 = 3 ; 2 . Si efectuamos una query cuyo vector de relevancias
palabras con relevancias d
2 5
4 5
es ~
q = 13 ; 54 , tenemos un escenario como el de la gura (2.3):
16
CAPÍTULO 2.
ESTADO DEL ARTE
Figura 2.3: Relación gráca entre una query y los documentos en el modelo vectorial.
Grácamente, dado el ángulo que forma cada documento con la query, por simple inspección esperamos
que
Similitud(~q, d~1 ) > Similitud(~q, d~2 ).
Efectuando el cálculo analítico:
2
X
wi,q × wi,d1
i=1
v
Similitud ~q, d~1 = v
u 2
u 2
uX
uX
t
2
w t
w2
i,q
i=1
i,d1
i=1
2
X
wi,q × wi,d2
i=1
~
v
Similitud ~q, d2 = v
u 2
u 2
uX
uX
t
2
w t
w2
i,q
i=1
1 1 4 4
× + ×
5 5
= s 3 2 s
2 = 0, 9866
2
2 2
4
4
1
1
+
+
3
5
2
5
i,d2
1 3 4 2
× + ×
5 5
s
= 3 4 s
2 = 0, 7738
2
2 2
1
3
4
2
+
+
3
5
4
5
i=1
A diferencia del modelo booleano, vemos que por más que los dos documentos contienen ambos términos,
podemos establecer un ranking basado en la relevancia de cada término en la query y los documentos.
El modelo vectorial que hemos denido hasta aquí no menciona cómo calcular los pesos de relevancia
wi,q
y
wi,dj
(de hecho existen muchas fórmulas distintas para el cálculo de pesos). A continuación vamos
a denir algunos valores para luego describir una familia de pesos que busca resolver este problema:
Denición 2.1.6.
c (ki , dj )
c (ki , q)
|dj |
y
Dado un documento
dj ,
: cantidad de apariciones de
: cantidad de apariciones de
|q|
una query
ki
ki
en
en
q,
un léxico
K
y un término
ki ∈ K ,
denimos:
dj
q
como las longitudes del documento
dj
y la query
q
Nota: si bien las notaciones son similares, es preciso notar que |dj | y |q| no representan las normas
euclídeas de un vector sino la cantidad de términos que encontramos en el texto. Es decir, la longitud del
→
qP
X
− t
2
documento se calcula como |dj | =
c (ki , dj ) mientras que la norma es dj =
i=1 wi,j .
∀i
Si nos detenemos a analizar la ecuación (2.1.5) podemos ver que los divisores
→
− dj y
−
k→
qk
cumplen
la función de normalización por longitud del documento y la query. Dado que en el contexto de una
2.1.
17
INFORMATION RETRIEVAL
−
k→
qk
búsqueda puntual el valor de
es una constante para todos los documentos, la normalización de la
query
tiene sentido si queremos comparar puntajes entre búsquedas distintas. Por otro lado, el valor
sólo
de
−
→
dj sí es de mayor importancia ya que nos permite diferenciar documentos cuyos valores
son similares pero sus longitudes
|dj |
c (ki , dj )
son muy distintas (es decir, nos ayuda a favorecer los documentos
con concentración de términos relevantes).
A continuación presentamos la familia de fórmulas TF-IDF (Term Frequency - Inverse Document Frequency), mediante la cual podremos obtener los pesos
wi,j
Denición 2.1.7 (Term Frequency).
como la cantidad de apariciones del término
el documento
Denimos
tfi,dj
wi,q :
y
ki
en
dj :
tfi,dj = c (ki , dj )
El valor de
t
tfi,dj
(en adelante también
dentro de un documento
dj .
tft,d
tf
o simplemente
) nos indica cuán importante es un término
Esta medida es una heurística que propone una relación directa entre la
relevancia de un término en un documento y su cantidad de apariciones.
Denición 2.1.8 (Document Frequency e Inverse Document Frequency).
frequency) de un término
ki
como el número de documentos
dj
dfi ( document
ki .
Denimos el valor
que contienen el término
Recíprocamente, denimos inverse document frequency como:
idfi = log
donde
N
N
dfi
es el número total de documentos en el corpus.
El valor de idfi (en adelante simplemente idf ) se utiliza como heurística para conocer el poder de discriminación de un término ki . Esto es, si un término está presente en muchos documentos, su uso no
sirve para discriminarlos (valores bajos de
idf ),
pero si el término es muy especíco (presente en pocos
documentos) entonces su uso permite discriminar un conjunto de documentos potencialmente relevantes.
Utilizando TF-IDF podemos replantear la similitud entre
q
y
dj
como:
X
Similitudtf −idf ~q, d~j =
tfki ,dj × idfki
(2.1.6)
ki ∈q
Los valores
tf
e
idf
son ampliamente utilizados para generar fórmulas de relevancia. A continuación
analizaremos variantes de la fórmula (2.1.6) que nos permiten controlar mejor sus efectos. La descripción
de estas variantes siguen el desarrollo presente en (Manning et al., 2008).
TF sub lineal
La denición (2.1.7) implica que la relevancia de un término aumenta linealmente con
la cantidad de veces que éste aparece en un documento. Si tenemos los documentos
c (ki , d1 ) = 30
y
c (ki , d2 ) = 40;
no es intuitivamente lógico que
d2
Para evitar este problema se plantea una variación al cálculo de
wfki ,dj =
Si reemplazamos
tf
por
wf
1 + log tfki ,dj
0
si
d1
y
d2
; las cuentas
sea un 25 % más relevante que
d1 .
tf :
tfki ,dj > 0
otro caso
en la fórmula (2.1.6), obtenemos una nueva fórmula de similitud menos
sensible a la repetición de términos.
18
CAPÍTULO 2.
Normalización de TF por Máximos
ESTADO DEL ARTE
En el párrafo anterior vimos cómo podríamos utilizar
evitar que la repetición de términos produzca serios desajustes en
tf .
Sin embargo,
wf
wf
para
es una medida
local a cada término que no tiene en cuenta la longitud del documento ni la frecuencia del resto de los
términos.
Buscando resolver estos problemas denimos una nueva medida:
ntfki ,dj = a + (1 − a)
En este caso introdujimos una variable
(por ejemplo de 1 a 2). El valor de
a
tfki ,dj
tfmax(dj )
a ∈ [0, 1], la cual suaviza el impacto en transiciones leves de tfki ,dj
se ajusta típicamente en 0,4 (Manning et al., 2008).
La idea de este método es notar que los documentos largos tienen tendencia a repetir los términos, por
lo que deberíamos normalizar los valores respecto del término con más apariciones.
Este método tiene los siguientes inconvenientes:
cambios en la lista de stop words (ver subsección 2.1.5) impactan bruscamente en los valores de
ntf ,
un documento puede contener un término inusualmente frecuente que no sea representativo del
contenido del documento,
si un documento tiene una distribución de frecuencias de términos uniforme debería ser tratado de
forma distinta que uno que acumula frecuencias alrededor de un conjunto de términos.
Sumado a las características que enumeramos al comenzar este apartado, a continuación presentamos
algunas ventajas y desventajas del modelo vectorial.
Las principales ventajas son (Baeza-Yates et al., 1999):
el esquema de pesos mejora la calidad de la recuperación respecto del modelo booleano y permite
recuperar documentos parcialmente coincidentes con la query,
incorpora una función de relevancia continua inherente al modelo,
permite buscar documentos similares entre sí.
Las desventajas del modelo vectorial son:
al igual que el resto de los esquemas clásicos, los términos del léxico son considerados de manera
independiente, es decir no se modelan interdependencias entre términos (en el próximo apartado
ejemplicaremos esto al presentar el modelo de independencia binaria),
es más difícil de mantener que el sistema booleano (requiere que mantengamos datos globales como
el número de apariciones de los términos en el corpus).
Si bien el modelo ya tiene veinticinco años de antigüedad, sigue siendo adoptado en muchos sistemas de
IR gracias a que da muy buenos resultados con un costo de implementación aceptable.
Es preciso notar que el modelo vectorial no es mutuamente excluyente con el booleano sino que pueden
complementarse. En sistemas con corpus extensos, podríamos utilizar el modelo booleano para determinar
el conjunto de documentos recuperables y luego utilizar el modelo vectorial para ordenarlos y recortarlos
según un umbral. En la subsección (2.4.1) tomaremos como primer caso de estudio un sistema que utiliza
esta estrategia.
2.1.
19
INFORMATION RETRIEVAL
Modelo Probabilístico
El tercer modelo clásico que vamos a mencionar interpreta la relevancia en términos de probabilidades y
es conocido como modelo probabilístico (Robertson y Jones, 1976). Este modelo se tomó en cuenta desde
principios de la década de 1970 por su capacidad de llevar los problemas de IR a un terreno formal rme
(Manning et al., 2008).
El modelo probabilístico será presentado brevemente ya que su desarrollo es algo más extenso que los
anteriores y, como veremos más adelante, no produce resultados muy distantes del modelo vectorial.
Persiguiendo esta brevedad seguimos el desarrollo de (Baeza-Yates et al., 1999) el cual es similar al de
(Manning et al., 2008) (para más detalle se sugiere consultar este último o el trabajo original referenciado
al principio del apartado).
Dentro de los modelos probabilísticos, presentaremos el más simple conocido como modelo de indepen-
dencia binaria (binary independence model ó BIM ). Las asunciones del BIM son:
los documentos se representan de forma booleana (esto es, un vector de unos y ceros indicando la
presencia/ausencia de un término),
la ocurrencia de dos términos distintos es estadísticamente independiente entre sí,
los términos que no se encuentran en la query no afectan a los resultados,
la relevancia entre documentos es estadísticamente independiente.
Estas simplicaciones son cuestionables ya que existen casos particulares donde fallan. La segunda asunción es especialmente cuestionada ya que términos como Hong y Kong ó New y York están fuertemente
relacionados estadísticamente.
Continuando con la presentación del modelo, asumamos una noción binaria de relevancia tal que un
documento sólo puede pertenecer a uno de dos grupos: relevantes ó no relevantes. Bajo esta asunción
podemos denir una variable aleatoria:
R(d, q) =
Es decir,
R(d, q)
1
0
⇐⇒ d es relevante para la query q
⇐⇒ d no es relevante para la query q
es una variable aleatoria bidiscreta binaria, la cual no es nula sólo para los documentos
relevantes para la query. En adelante nos referimos a
R(d, q)
simplemente como
R.
Dada una necesidad de información puntual, la propuesta del modelo es presentar a los documentos por
orden decreciente de probabilidad de relevancia
P (R = 1|d, q).
El modelo probabilístico se basa entonces
en la siguiente asunción (Robertson, 1977):
Principio Probabilístico del Puntaje
3 Si la respuesta de un sistema de IR a cada pedido de un
usuario es una lista de documentos puntuados por orden decreciente de probabilidad de relevancia
para quien efectuó el pedido, donde las probabilidades se estiman de la forma más precisa posible en
la base de los datos que estaban disponibles al sistema para este propósito, entonces la efectividad
general del sistema para este usuario será la mejor que se pueda obtener en base a ésos datos.
Este principio tiene el inconveniente de que no nos da un indicio acerca de cómo calcular esta probabilidad
de relevancia, por lo que tenemos que buscar otra medida de similitud (Baeza-Yates et al., 1999).
Dada una query
Odds(dj
q,
relevante para
Denición 2.1.9.
dj
P (dj relevante para q)
.
q) =
P (dj no relevante para q)
el modelo asigna a cada documento
Sean:
un conjunto de términos
q,
el conjunto de documentos inicialmente relevantes
3 La
una medida de similitud dada por la relación
R
y su complemento
R,
versión original del inglés se puede encontrar tanto en (Robertson, 1977) como en (Manning et al., 2008).
20
CAPÍTULO 2.
wi,j ∈ [0, 1], wi,q ∈ [0, 1]
ESTADO DEL ARTE
los pesos binarios
que
la probabilidad
wi,j y wi,q tal
P R|d~j de que
un documento
dj
sea relevante para la query
la probabilidad
P R|d~j
de que un documento
dj
no sea relevante para la query
entonces el modelo probabilístico dene la similitud del documento
(ver denición 2.1.3),
dj
con la query
q,
q
q,
como:
P R|d~j
Similitud (dj , q) = P R|d~j
(2.1.7)
Utilizando la regla de Bayes sobre la ecuación (2.1.7) obtenemos:
P d~j |R × P (R)
Similitud (dj , q) = P d~j |R × P R
donde
P d~j |R se interpreta como la probabilidad de obtener dj
al azar entre los documentos relevantes
P
(R) como laprobabilidad de que un documento tomado al azar sea relevante. Como es de esperar,
P d~j |R y P R son los respectivos complementos.
Dado que P (R) y P R no varían documento a documento, podemos tomarlos como una constante y
y
quitarlos de la fórmula de similitud obteniendo:
P d~j |R
Similitud (dj , q) ∼ P d~j |R
A continuación utilizamos la asunción de independencia entre términos del BIM, lo que nos permite
expandir la ecuación anterior en una productoria término a término:





  Y
 Y
P ki |R 
P (ki |R) × 

g (d~j )=1
g (d~j )=0
 

Similitud (dj , q) ∼ 
  Y

 Y
P ki |R  × 
P ki |R 

g (d~j )=1
g (d~j )=0
donde
P (ki |R)
se interpreta como la probabilidad de que el término
al azar tomado de
R
y
g d~j
vale
1
ki
esté presente en un documento
para los términos que están en la query y
0
para los que no están
presentes.
Si tomamos logaritmos y consideramos que
P (ki |R) + P ki |R = 1, esta expresión puede ser reformulada
como:
Similitud (dj , q) ∼
t
X
"
wi,q × wi,j
i=1
#
1 − P ki |R
P (ki |R)
× log
+ log
1 − P (ki |R)
1 − P ki |R
la cual es una expresión clave para el cálculo de relevancia en el modelo probabilístico (Baeza-Yates et al.,
1999).
Dado que inicialmente no conocemos el conjunto
y
P ki |R
R, para calcular la similitud necesitamos estimar P (ki |R)
P (ki |R)
. Existen diferentes métodos para computar estos valores, una forma es asumir que (a)
2.1.
21
INFORMATION RETRIEVAL
es constante para todos los términos del índice y (b) inferir que la mayoría de los documentos serán no
relevantes para aproximar
P ki |R
con la estadística global, lo que se traduce en:
P (ki |R) = 0, 5
ni
P ki |R =
N
donde
ni
es el número de documentos que contienen el término
ki
y
N
el número total de documentos
en el corpus.
Si bien no queremos profundizar más en el tema, cabe destacar que existen mejores métodos para estimar
P (ki |R)
y
P ki |R
así como existen otros modelos además del BIM que producen fórmulas de similitud
similares a las del modelo vectorial (Manning et al., 2008).
Las principales ventajas del modelo probabilístico son:
se basa en un marco teórico rme,
incorpora una función de relevancia continua inherente al modelo.
Las desventajas del modelo probabilístico son:
la necesidad de conjeturar la separación inicial entre documentos relevantes y no relevantes,
para el caso de utilizar el BIM, asunciones como la independencia entre términos pueden ser poco
realistas.
En la práctica ocurre que algunos modelos comienzan siendo vectoriales y luego migran al probabilístico
efectuando algunas variaciones en las fórmulas de similitud (Manning et al., 2008).
Otros Modelos de IR
Existen muchos otros modelos de IR derivados de los que hemos presentado. Algunos de ellos son: modelo booleano extendido, modelo vectorial generalizado, fuzzy sets, latent semantic indexing (LSI), redes
neuronales y bayesianas.
Estos modelos pueden reemplazar a los clásicos una vez que estamos convencidos que los primeros no
son adecuados para resolver el problema. Adelantándonos a la explicación de los modelos a elegir para la
propuesta (capítulo 3), podemos decir que los modelos clásicos proveen una buena base para construir el
motor de búsqueda sobre objetos, por lo que preferimos profundizar en técnicas de matching o priorización
y no en sosticar los modelos de IR.
Respecto de las fórmulas de similitud; Fang, Thao y Zai proponen comparar las fórmulas de relevancia
surgidas de distintos modelos de IR deniendo un conjunto de criterios funcionales a satisfacer (Fang
et al., 2004). Estos criterios o premisas son similares a las que hemos propuesto durante la exposición
del modelo vectorial al describir TF sub lineal y normalización por máximos. Las premisas se formalizan
principalmente en términos de TF, IDF y otras estadísticas para luego evaluar fórmulas de relevancia
vectoriales y probabilísticas, analizando el grado de cumplimiento de las premisas. Las conclusiones a
las que arriban los autores es que cada modelo tiene una parametrización ligada a criterios funcionales
concretos que determinan un rango de validez para sus parámetros.
2.1.5. Técnicas de Matching y Acceso a Datos
En las subsecciones previas clasicamos los sistemas de IR y presentamos sus métricas y modelos más
importantes. En esta subsección vamos a explicar las distintas técnicas que nos permiten implementar
estos sistemas y modelos de IR.
22
CAPÍTULO 2.
ESTADO DEL ARTE
Índices Invertidos
Si tuviéramos que procesar la query caesar AND brutus del ejemplo (2.1.4), la forma ingenua de hacerlo
sería recorrer línea por línea todos los documentos del corpus, quedándonos con aquellos que contienen
las dos palabras. Esta forma de solucionar el problema equivale a procesar los documentos uno a uno con
el comando grep de Unix. Otra solución similar sería utilizar consultas like de SQL sobre una tabla de un
RDBMS, cuyos campos contengan el cuerpo del documento.
Si el problema a resolver es pequeño o surge de una consulta ad-hoc que sólo necesita ejecutarse una
vez, el modelo grep o el like puede ser suciente. Ahora, si queremos recuperar información rápidamente
en colecciones de millones de documentos, efectuar consultas avanzadas (por ejemplo: Caesar a 5 palabras de distancia de Brutus) y establecer un puntaje para los resultados como en el modelo vectorial,
necesitaremos de un índice invertido (Manning et al., 2008).
Un índice invertido o archivo invertido es una estructura de datos similar a un mapa cuya clave es un
término y su valor es una lista de identicadores de documentos. Las claves del índice invertido forman
el léxico ó diccionario mientras que los valores de cada clave son las posting lists. Cada documento de la
posting list es un posting. En la gura (2.4) podemos ver la estructura del índice invertido:
Figura 2.4: Diccionario y Posting Lists. A la izquierda se ven los términos y a la derecha la lista de
documentos coincidentes.
Los índices invertidos se utilizan principalmente para recuperar y valorizar los documentos de forma
eciente. A continuación presentamos un ejemplo de construcción del índice invertido:
Ejemplo 2.1.6.
Construyamos un índice invertido para los documentos del ejemplo (2.1.4). Para con-
struir el índice necesitamos llevar a cabo estos pasos:
dj en términos ki pertenecientes al léxico K obteniendo un
Nj es el número de términos distintos en dj .
Invertir el mapa obteniendo un nuevo mapa / índice: ki → dj , dj+1 , . . . ,dj+Mj −1 donde Mj es el
número de documentos donde aparece el término ki .
1. Transformar las palabras del documento
mapa
2.
dj → ki , ki+1 , . . . ,ki+Nj −1
donde
Apliquemos estos pasos sobre los documentos del ejemplo (2.1.4):
1.
(d1 → {caesar; dictador; romano} ; d2 → {caesar; hijo; octavianus} , d3 → {brutus; conspirador; romano})
2. Invertimos las listas:
caesar →(d1 , d2 )
dictador →(d1 )
romano →(d1 , d3 )
hijo →(d2 )
octavianus →(d2 )
brutus →(d3 )
conspirador →(d3 )
2.1.
23
INFORMATION RETRIEVAL
Si ahora quisiéramos efectuar la consulta
caesar AND brutus OR conspirador
vemos que basta con inter-
sectar y unir listas del índice:
[caesar = (d1 , d2 ) ∩ brutus = (d3 )] ∪ conspirador = d3
El objetivo del índice es entonces acelerar la recuperación de documentos, permitiéndonos acceder rápidamente a la lista de documentos que contienen un término. Dependiendo cómo esté implementado el índice,
el costo de acceso a los posting lists varía desde el simple acceso a un archivo hasta técnicas complejas
que involucran compresión, front coding, árboles B, etc. A continuación presentamos algoritmos para la
construcción del índice y algunas variantes para su implementación.
Construcción del Índice
Dependiendo de modelo de IR, debemos considerar distintos componentes en la construcción del índice.
Para el modelo booleano nos alcanza con un índice como el del ejemplo (2.1.6), esto es, un mapa de
términos a documentos.
En el modelo booleano, para acelerar la intersección entre conjuntos, es conveniente construir el índice
ordenando las posting lists por identicador de documento (docID). Si la posting list está ordenada, la
intersección se puede efectuar mediante el siguiente algoritmo:
Algoritmo 2.1 Intersección de dos posting lists cuando los docID están ordenados de forma ascendente.
public
L i s t <DocID>
intersect ( String
String
results
L i s t <DocID>
listA
=
g e t P o s t i n g L i s t ( k1 ) ;
L i s t <DocID>
listB
=
g e t P o s t i n g L i s t ( k2 ) ;
int
indexA = 0 ;
int
indexB = 0 ;
while ( indexA <
= new
k1 ,
L i s t <DocID>
l i s t A . s i z e ( ) && i n d e x B <
// docID1 == docID2
docID1 =
l i s t A . get ( indexA ) ;
docID2 =
l i s t B . get ( indexB ) ;
i f ( docID1 . e q u a l s ( docID2 ) )
{
r e s u l t s . add ( docID1 . c l o n e ( ) ) ;
i n d e x A ++;
}
else
k2 )
{
A r r a y L i s t <DocID >() ;
l i s t B . size () )
{
// i d é n t i c o a h a c e r docID2 . c l o n e ( ) ;
i n d e x B ++;
i f ( docID1 . g r e a t e r T h a n ( docID2 ) )
{
i n d e x B ++;
}
else
{
i n d e x A ++;
}
}
return
results ;
}
Este algoritmo aprovecha el ordenamiento de las claves para avanzar el puntero sobre la posting list que
aún puede aportar coincidencias (si una lista se acaba con un docID menor al de la otra lista, la segunda
no necesita seguir siendo recorrida) y no requiere recorrer completamente todas las posting lists.
Sin embargo, si los documentos no están ordenados, podríamos resolver la intersección mediante un
algoritmo como el siguiente:
24
CAPÍTULO 2.
ESTADO DEL ARTE
Algoritmo 2.2 Intersección de dos posting lists cuando estas no están ordenadas por docID.
L i s t <DocID>
intersect ( String
= new
k1 ,
String
k2 )
L i s t <DocID>
results
L i s t <DocID>
listA
=
g e t P o s t i n g L i s t ( k1 ) ;
L i s t <DocID>
listB
=
g e t P o s t i n g L i s t ( k2 ) ;
L i s t <DocID> s h o r t e s t ,
largest ;
i f ( l i s t A . s i z e ( )< l i s t B . s i z e ( ) )
else
shortest
=
{
A r r a y L i s t <DocID >() ;
shortest
=
listA ;
listB ;
S e t <DocID> s m a l l S e t=new
H a s h S e t <DocID >() ;
smallSet . addAll ( shortest ) ;
while ( int
i =0;
i <l a r g e s t . s i z e ( ) ;
i ++) {
i f ( smallSet . contains ( l a r g e s t . get ( i ) ) )
r e s u l t s . add ( l a r g e s t . g e t ( i ) ) ;
}
return
results ;
}
Notemos que a diferencia del caso previo, el algoritmo (2.2) obliga a leer completamente las posting lists
pero no requiere ordenarlas (lo cual puede ser costoso).
Estos algoritmos son similares a los utilizados para resolver juntas en una base de datos. En particular,
el segundo es conocido como hash-join.
Si queremos optimizar un índice para un modelo de IR con puntajes como el vectorial, podemos almacenar
los puntajes de los documentos en el índice mismo, ordenando las posting lists por estos puntajes. Dado
que el usuario suele recibir sólo una pequeña porción de los resultados totales, podemos acelerar la
búsqueda recuperando los
N
N
documentos de mejor puntaje y trabajar con esas listas reducidas de hasta
elementos.
Otras estadística que podemos almacenar en el índice son los valores de las fórmulas TF-IDF. Para
esto podemos almacenar
insertar el valor de
tf
df
a la cabeza de cada término (notar que
df
es la longitud del posting list) e
junto a cada posting. Para usos como las búsquedas por proximidad (ver subsección
2.1.5), también es conveniente almacenar junto a cada posting el conjunto de posiciones donde ocurrió la
coincidencia. Esta última técnica también es útil para mostrar un resumen del texto con la coincidencia
resaltada (esto es conocido como Keyword In Context o KWIC ).
Para la construcción física del índice se pueden utilizar una variedad de métodos que dependen de:
el método de almacenamiento del índice (archivos, bases de datos, etc),
tipo de acceso al índice (sólo lectura vs. lectura y escritura),
la escala del sistema (corpus, hardware, nivel de concurrencia, etc).
Para resolver el problema del almacenamiento del índice, se debe implementar una estructura de datos
conocida como diccionario. El diccionario actúa como un mapa de términos a posting lists. Los candidatos
para almacenar un diccionario son mapas hash y arboles B.
Si el sistema es relativamente pequeño, el índice se puede cargar en memoria y luego refrescarlo periódicamente. Otra alternativa es utilizar una base de datos, la cual resuelve los problemas de concurrencia y
almacenamiento físico de los posting lists (normalmente utilizando árboles B). Por otro lado, cuando el
índice es de cierta envergadura, se suele almacenar en archivos ad-hoc. A continuación veremos algunas
técnicas de construcción de índices en archivos, las cuales pueden adaptarse para almacenar el índice en
un RDBMS.
Para generar el índice normalmente se utilizan variantes de dos algoritmos que presentaremos a contin-
4 (algoritmo 2.3) e Indexación por Barrido Simple en
uación: Indexación por Ordenamiento en Bloques
5
Memoria (algoritmo 2.4).
4 Traducción
5 Traducción
del inglés Block sort-based indexing.
del inglés Single-pass in-memory indexing.
2.1.
25
INFORMATION RETRIEVAL
Algoritmo 2.3 Pseudocódigo Java para el método de Indexación por Ordenamiento en Bloques.
/∗ ∗
∗
∗
∗
∗
∗
∗
I n d e x a c i ó n p o r Ordenamiento en B l o q u e s .
− R e c o r r e una ú n i c a v e z l o s d a t o s y g e n e r a p a r e s <termID , docID>
− Mantiene en memoria un mapa [ t e r m i n o −> termID ]
1 . R e c o r r e r l a l i s t a de documentos
1 . 1 T o k e n i z a r e l documento g e n e r a n d o p a r a cada t é r m i n o un termID
único
∗
1 . 2 A g r e g a r a l b l o q u e l o s p a r e s <termID , docID>
∗
∗ 2.
∗
∗
∗
∗ 3.
∗
∗/
public
S i p r o c e s a m o s más de ' m a x B l o c k S i z e ' documentos o no tenemos
más p a r a p r o c e s a r e n t o n c e s almacenamos e l b l o q u e en d i s c o
y renovamos l a s e s t r u c t u r a s de d a t o s
C o n s o l i d a r l o s b l o q u e s e s c r i t o s en e l í n d i c e f i n a l
void
i n d e x ( L i s t <Document> d o c L i s t ,
HashMap<S t r i n g ,
IndexBlock
int
maxBlockSize )
I n t e g e r > termMap = new HashMap<S t r i n g ,
i d x B l o c k = new
f o r ( I t e r a t o r <Document>
it
{
I n t e g e r >() ;
IndexBlock () ;
= docList . i t e r a t o r ()
;
i t . hasNext ( )
;
i t . h a s N e x t ( )== f a l s e )
{
){
i d x B l o c k . a d d P a i r s ( t o k e n i z e ( i t . n e x t ( ) , termMap ) ) ;
i f ( i d x B l o c k . g e t S i z e ( )>=m a x B l o c k S i z e
||
// ordenamos p o r termID y l u e g o p o r docID
// y c r e a l a s p o s t i n g l i s t s
idxBlock . sortAndInvert () ;
// grabamos e l b l o q u e a d i s c o y l o r e g e n e r a m o s
idxBlock . saveToDisk () ;
idxBlock . reset () ;
}
}
saveTermMap ( termMap ) ;
m e r g e B l o c k s I n t o I n d e x ( termMap ) ;
}
El algoritmo (2.3) es simple pero tiene el problema de que requiere almacenar en memoria principal
la correspondencia entre términos e identicadores de términos, lo cual puede ser excesivo en algunos
corpus. Para evitar esto, existe una técnica conocida como Indexación por Barrido Simple en Memoria
(ver pseudocódigo Java en algoritmo 2.4):
26
CAPÍTULO 2.
ESTADO DEL ARTE
Algoritmo 2.4 Pseudocódigo Java para el método de Indexación por Barrido Simple en Memoria.
/∗ ∗
∗ I n d e x a c i ó n de S i m p l e B a r r i d o en Memoria
∗ − R e c o r r e una ú n i c a v e z l o s d a t o s
∗ − No u t i l i z a i d e n t i f i c a d o r e s de t é r m i n o s
∗ − Genera i n d i c e s c o m p l e t o s p a r c i a l e s que l u e g o s e c o n s o l i d a n
∗
∗ 1 . R e c o r r e r l a l i s t a de documentos
∗
1 . 1 Para cada t o k e n b u s c a r su p o s t i n g l i s t s y a l m a c e n a r su docID
∗
∗ 2 . S i a l c a n z a m o s e l l í m i t e de memoria almacenamos e l í n d i c e i n v e r t i d o
∗
p a r c i a l en un a r c h i v o y renovamos l a s e s t r u c t u r a s de d a t o s
∗
∗ 3 . C o n s o l i d a r l o s í n d i c e s p a r c i a l e s en e l í n d i c e f i n a l
∗
∗/
public
void
i n d e x ( L i s t <Document> d o c L i s t ,
int
maxPostings ,
int
maxDictSize )
{
InvertedIndex
partialIndex
f o r ( I t e r a t o r <Document>
Tokenizer
it
t o k e n s = new
= new
InvertedIndex () ;
= docList . i t e r a t o r ()
;
i t . hasNext ( )
;
){
DocumentTokenizer ( i t . next ( ) ) ;
while ( tokens . hasMoreElements ( ) )
{
p a r t i a l I n d e x . a d d P o s t i n g ( t o k e n s . n extE lemen t ( ) , tok . getDocID ( ) ) ;
i f ( p a r t i a l I n d e x . g e t P o s t i n g C o u n t ( )>=m a x P o s t i n g s
||
p a r t i a l I n d e x . g e t D i c t i o n a r y S i z e ( )>=m a x D i c t S i z e )
{
p a r t i a l I n d e x . saveToDisk () ;
partialIndex . reset () ;
}
}
}
mergePartialIndexes () ;
}
Para que el algoritmo (2.4) sea eciente, se debe almacenar cada bloque con su diccionario y posting lists
ordenadas. De esta manera el paso de consolidación puede recorrer secuencialmente todos los archivos
en una única iteración (podría hacerse en etapas si fueran demasiados) y consolidar las listas término a
término. Si las posting lists están ordenadas, consolidarlas será más simple ya que sólo requerirá comparar
valores avanzando secuencialmente sobre todas al mismo tiempo.
Tal como fueron presentados, los algoritmos (2.3) y (2.4) son esencialmente centralizados porque no
balancean la carga de la indexación entre varias computadoras. Además de estos algoritmos centralizados,
en corpus extensos podemos utilizar algoritmos de indexación distribuida.
Para efectuar una indexación distribuida es posible utilizar un modelo de programación llamado MapReduce (Dean y Ghemawat, 2008). Este modelo consiste en dividir un gran trabajo en pequeñas tareas
similares, las cuales pueden llevarse a cabo en paralelo por muchas computadoras. El algoritmo (2.5)
implementa la indexación distribuida utilizando MapReduce:
2.1.
27
INFORMATION RETRIEVAL
Algoritmo 2.5 Pseudocódigo Java para indexación distribuida con MapReduce.
/∗ ∗
∗ I n d e x a c i ó n D i s t r i b u i d a u t i l i z a n d o MapReduce .
∗
∗ E l p r o c e s o comprende l o s s i g u i e n t e s p a s o s :
∗
1 . D i v i s i ó n de documentos en N mappers
∗ 2 . P r o c e s a m i e n t o d e l documento en l o s mappers
∗ 3 . D i s t r i b u c i ó n de l o s p a r e s ( t e r m i n o , docID ) h a c i a l o s r e d u c e r s
∗ 4 . Merge y s o r t de l o s p a r e s ( t e r m i n o , docID ) p o r t é r m i n o ( y
∗
o p c i o n a l m e n t e p o r docID )
∗ 5 . R e d u c c i ó n de l o s p a r e s ( t e r m i n o , docID ) a ( t e r m i n o , l i s t a de docID )
∗
∗/
/∗ ∗
∗ La f u n c i ó n Map r e c i b e documentos , l o s d i v i d e en t o k e n s y
∗ e m i t e p a r e s ( t e r m i n o , docID )
∗/
public
void
Map ( I n p u t <Document> i n p u t ,
O u t p u t<S t r i n g ,
Integer > output )
{
L i s t <S t r i n g > t o k e n s = t o k e n i z e ( i n p u t . g e t E l e m e n t ( ) ) ;
for ( String
token :
tokens )
{
o u t p u t . add ( t o k e n , doc . g e t I d ( ) ) ;
}
}
/∗ ∗
∗ E l r e d u c e r e c i b e t o d a s l a s e m i s i o n e s ( docID ) de una misma c l a v e ( t é r m i n o
)
∗ y g e n e r a e l p o s t i n g l i s t en l a forma ( t e r m i n o −> [ docId1 , . . . , docIdN ] )
∗/
public
void
R e d u c e ( I n p u t <S t r i n g ,
L i s t <I n t e g e r >> o u t p u t )
String
I t e r a t o r <I n t e g e r >> i n p u t ,
O u t p u t<S t r i n g ,
{
term = i n t p u t . getKey ( ) ;
I t e r a t o r <I n t e g e r >
L i s t <I n t e g e r >
docIdIterator
postingList
= input . getValue () ;
= new
while ( d o c I d I t e r a t o r . hasNext ( ) )
L i n k e d L i s t <I n t e g e r >() ;
{
p o s t i n g L i s t . add ( i t . n e x t ( ) ) ;
}
o u t p u t . add ( term , p o s t i n g L i s t ) ;
}
Para implementar el algoritmo anterior debemos contar con un framework que implemente MapReduce
como Hadoop (Apache, 2010). Al tratar la indexación oine de nuestra propuesta de solución, veremos
que es factible indexar un dominio en forma distribuida sin utilizar MapReduce o Hadoop.
Indexación Dinámica
Al abordar los algoritmos para la construcción del índice, no nos hemos preocupado por saber qué ocurre
cuando el sistema se encuentra en funcionamiento y ya existe un índice productivo. El problema de
indexar documentos y agregarlos a un índice existente se conoce como indexación dinámica. La indexación
dinámica es de nuestro especial interés porque al indexar objetos se debe considerar su frecuencia de
creación, actualización y eliminación.
Si el índice se mantiene en una base de datos, su actualización no presenta ningún inconveniente ya que
28
CAPÍTULO 2.
ESTADO DEL ARTE
las propiedades ACID nos permiten agregar y eliminar documentos independientemente de las consultas
que se hagan a la tabla.
Por el contrario, si decidimos almacenar el índice en archivos, debemos considerar cómo actualizarlos en
caliente de forma tal que las tareas de búsqueda no intereran con las de indexación. Veamos algunas
formas de resolver el problema de la indexación dinámica sobre archivos:
Reindexación Completa
Esta es la solución más simple y consiste en regenerar el índice completa-
mente desde cero. Esta solución tiene como principal ventaja su simplicidad pero requiere de espacio
suciente para mantener el índice actual mientras se genera la nueva versión. Además debemos considerar
que el tiempo para generar el índice puede ser prohibitivo en corpus grandes. Esta opción es viable en los
casos en los que la frecuencia de actualización del corpus es baja y el retardo en hacer visibles los nuevos
documentos es aceptable (Manning et al., 2008).
Índices Auxiliares y Generacionales
Este método consiste en mantener un índice principal que
contiene la mayor cantidad de documentos y disponer pequeños índices auxiliares para agregar los nuevos
documentos. Para manejar las eliminaciones sin requerir la actualización física del índice, se pueden
utilizar vectores de invalidación (bit vectors ), los cuales utilizan el bit
j
para indicar si el documento
dj
existe. Periódicamente se fusionan los índices principales y los auxiliares y se eliminan los documentos
marcados en el vector de invalidación. En la subsección (2.4.1) veremos que Apache Lucene aplica una
estrategia similar.
Archivos Indexados
En este caso se generan un archivo índice (posiblemente implementado con ár-
boles B) y un archivo de datos con las posting lists. Este esquema permite actualizar el índice con facilidad
pero puede ser costoso de implementar (equivale a resolver problemas típicos de un RDBMS). Un análisis
completo de esta alternativa se presenta en (Cutting y Pedersen, 1990).
Operaciones Extendidas
La búsqueda de términos en un índice invertido es una forma eciente de encontrar documentos cuando
somos efectivos en escoger los términos que producirán mayor relevancia. Sin embargo, hay casos en los
que no estamos seguros de cuáles son los mejores términos y necesitamos de cierta ayuda adicional.
A continuación presentamos algunas técnicas que exibilizan la tarea de recuperación.
Búsquedas de proximidad
Consiste en especicar la distancia máxima entre dos términos. Por ejem-
plo, la búsqueda Caesar AND/5 Brutus retornaría el documento Brutus fue hijo de Caesar pero no el
documento Caesar fue asesinado por el senado con la ayuda de Brutus. El objetivo de esta técnica es
recuperar documentos donde las palabras se encuentran en un mismo contexto.
Las búsquedas por proximidad se pueden resolver utilizando un índice posicional, el cual mantiene junto
a cada posting las posiciones donde podemos ubicar el término en el documento original.
Otra variante de implementación es efectuar una búsqueda lineal sobre los documentos recuperados,
ltrando los que no cumplan con el criterio de proximidad. Esta variante tiene peor rendimiento pero
requiere menor espacio de almacenamiento en el índice y disminuye tanto su complejidad como la del
proceso indexador.
Phrase Queries
Este tipo de búsquedas son conocidas también como búsquedas literales. Las búsquedas
literales pueden verse como un caso particular de búsquedas de proximidad en el cual se deben cumplir:
la proximidad entre los términos de la query es exactamente uno
la proximidad se aplica sólo hacia adelante (es decir, la query rosa salmón no equivale a salmón
rosa).
La implementación de las phrase queries se basa nuevamente en índices posicionales o en ltrado de
documentos.
2.1.
INFORMATION RETRIEVAL
Wildcards
29
Los wildcards son símbolos que nos ayudan a buscar todos los términos que siguen un patrón
especíco. Por ejemplo, en SQL los wildcards se especican mediante el símbolo %, mientras que en un
lesystem, es común especicarlos con el símbolo *. En un search engine los wildcards se utilizan cuando
conocemos la forma de una parte del término de búsqueda pero no podemos especicarlo completamente.
Utilizando wildcards, queries como bol* retornan documentos que contienen las palabras bolsa, bolero,
bola, etc.
Las búsquedas por wildcards se pueden resolver utilizando índices permuterm (Gareld, 1976) ó n-gram
(ver Zobel y Dart, 1995).
Edit distance
Cuando la query contiene pequeños errores de ortografía, podemos reconvertirla buscan-
do términos cercanos al original. Para lograrlo, podemos utilizar una medida de distancia entre términos
del léxico llamada edit distance ó Levenshtein distance (Levenshtein, 1965). La idea general detrás del
edit distance es calcular cuántas inserciones, modicaciones y eliminaciones hay que efectuar sobre dos
strings para que uno se convierta en el otro. El edit distance se puede calcular fácilmente utilizando
programación dinámica.
Un procedimiento para corregir una query por medio de edit distance es correr el algoritmo sobre los
términos del léxico y la query, suplantando ésta por los términos de menor edit distance. Una vez obtenidos
los candidatos de menor edit distance, se accede al índice invertido utilizando el conjunto ampliado de
términos.
Para que este procedimiento no sea excesivamente costoso existen heurísticas como asumir que las
primeras
n
letras de la query son correctas y calcular el edit distance sólo sobre los términos del léx-
ico que comienzan con esas letras.
Una variante a los edit distance es la utilización de un tesauro. Esta técnica la veremos en las próximas
subsecciones.
Range Queries
Las búsquedas por rango o range queries consisten en especicar un criterio (rango),
el cual debe cumplirse en el documento para que éste sea recuperado. Ejemplos: 2009 to 2010, Brutus to
Caesar (aquí el rango es por orden lexicográco) ó Bares en Rivadavia al 1000 to 1900.
Este tipo de búsquedas se pueden resolver de distintas formas. Una forma de resolver el problema consiste
expandir la query en forma de una conjunción dentro del rango. Esta solución tiene como principal
problema la granularidad de la expansión ya que queries como 1,1 to 1,999 ó 1 to 1.000.000 serán demasiado
costosas de implementar.
Otra opción de implementación es recorrer el léxico y ltrar los términos que cumplen con el criterio del
rango. Para léxicos moderados esta puede ser una opción factible. Una tercera vía de solución consiste
en utilizar un índice de búsqueda como los utilizados en un RDBMS.
Por supuesto también podemos resolver este problema mediante pos ltrado de resultados. Este esquema
requiere eliminar los términos de la disyunción (en caso de recuperación booleana) y luego recorrer los
resultados ltrando los documentos.
Para resolver este problema, ciertos motores no utilizan el operador to o el hasta de nuestros ejemplos
sino que el usuario debe explicitar cómo efectuar la búsqueda dando una ayuda acerca del tipo de datos
del rango y su granularidad.
Las búsquedas literales, wildcards, por rangos y edit distance son soportadas en Apache Lucene (ver casos
de estudio en subsección 2.4.1).
Búsquedas Facetadas
Tradicionalmente han coexistido dos modelos de búsqueda, el modelo de nave-
gación y el modelo de búsqueda directa (SIGIR, 2006).
En el modelo de navegación, el usuario especica iterativamente el tema de interés navegando a través
de una taxonomía (cuyas categorías no necesitan ser necesariamente excluyentes entre sí). Ejemplos
de este modelo se implementan en Google Directory (http://directory.google.com), Yahoo! Directory
(http://dir.yahoo.com) y Open Directory Project (http://www.dmoz.org/). Por otro lado, en el modelo
de búsqueda directa el usuario escribe una búsqueda en lenguaje natural para luego navegar a través
30
CAPÍTULO 2.
ESTADO DEL ARTE
de páginas de resultados o renar iterativamente los términos de búsqueda. Los ejemplos de búsqueda
directa son los buscadores de Google (http://www.google.com) y Yahoo! (http://www.yahoo.com).
La combinación de estos dos modelos de búsqueda han dado lugar a un tercer modelo, la búsqueda facetada
o faceted search.
En la búsqueda facetada el usuario comienza una búsqueda directa y el motor de búsqueda presenta los
resultados junto a una categorización por características ortogonales entre sí. En la gura (2.5) vemos un
ejemplo de búsqueda facetada en el marketplace e-Bay (http://www.ebay.com).
Figura 2.5: Búsqueda Facetada. Al buscar un ipod, podemos cortar los resultados en distintas facetas
o dimensiones. En este caso las facetas son el tipo de producto, la condición, capacidad y color. Otras
facetas podrían ser el precio, la garantía, etc.
Si bien en el caso de la gura (2.5) se efectuó una búsqueda directa, bien podríamos llegar al mismo
6 utilizando el modelo de navegación (el cual suele permitirse en los sitios de comercio electrónico
landing
como e-Bay). La diferencia principal reside en que el landing obtenido por navegación no es un modelo
mixto sino que es una navegación a través de una taxonomía.
Un desafío que se presenta al implementar las facetas es el hecho de que los resultados pueden ser muy
heterogéneos, por lo cual la elección de facetas como el color de un reproductor de música sólo podrían
aparecer cuando eliminamos otros resultados que no admiten dicha faceta (como por ejemplo una fuente
de alimentación).
La búsqueda facetada no es implementada directamente por ninguno de los casos de estudio de la sección (2.4), sin embargo, sí es implementada por una derivación del proyecto Lucene conocida como
Solr (http://lucene.apache.org/solr/). En nuestra propuesta de solución vamos a retomar este tipo de
búsquedas.
Stemming y Lematización
Si bien las operaciones extendidas nos permiten mejorar la coincidencia entre la query y los términos
del léxico, siguen operando sobre un espacio de alta dimensionalidad (según el modelo vectorial de la
6 El
término landing se utiliza para referir a la página donde se llega luego de una búsqueda.
2.1.
31
INFORMATION RETRIEVAL
subsección 2.1.4). Si reducimos las dimensiones del problema proyectando los términos sobre un espacio
de menor orden, posiblemente obtengamos un mayor recall. La proyección de un espacio sobre otro de
menores dimensiones implica necesariamente pérdida de información, la cual naturalmente se traducirá
en peor precisión.
A continuación vamos a explicar las técnicas de stemming y lematización, las cuales buscan reducir el
número de dimensiones del léxico con la menor pérdida de información posible.
Lematización
El proceso de lematización busca remover las formas exivas de las palabras para obtener
una versión canónica del término llamado lema.
La lematización es un proceso complejo que requiere llevar una palabra del diccionario a otra palabra del
diccionario, reconociendo correctamente cuál es el lema del término ingresado (normalmente esto requiere
reconocer el POS
7 del término).
Veamos un caso práctico en el siguiente ejemplo:
Ejemplo 2.1.7.
Efectuemos la canonización de algunas exiones del verbo
escribir:
lema(escribiendo)=escribir
lema(escrito)=escribir
lema(escribí)=escribir
El proceso de lematizar términos es una solución de compromiso entre recall y precisión que puede ser
más o menos útil dependiendo del lenguaje. De los casos de estudio que tomamos en la sección (2.4),
ninguno utiliza por defecto un lematizador.
Un caso práctico de lematización automática se describe en (Galceran, 2006), donde además se concluye
que la distribución de lemas en un texto promedio se encuentra muy concentrado alrededor de unos
pocos lemas. Un ejemplo de lematizador online se puede encontrar en (Grupo de Estructuras de Datos y
Lingüística Computacional, 2006).
Stemming
El proceso de stemming consiste en transformar palabras con el n de colisionar términos
de semántica similar en un mismo término (el cual no necesita ser una palabra del diccionario como
8
ocurría con el lema). El resultado de aplicar stemming a una palabra es un stem . A continuación vemos
algunos ejemplos de stemming en español.
Ejemplo 2.1.8.
Efectuamos el stemming de una lista de palabras utilizando un stemmer Snowball (ver
más adelante):
stem(tornado)=torn
stem(tornados)=torn
stem(tornar)=torn
Como podemos ver en el ejemplo anterior, el stemming logra colisionar en torn los términos de semántica
similar tornado y tornados. Un problema que debe observarse en el ejemplo anterior es que tornado (cuya
acepción principal podría referir al fenómeno meteorológico) colisiona en su stem con el de tornar (cuya
semántica reere al acto de convertir una cosa en otra). Este problema derivado de la heurística empleada
para buscar los stems es conocido como overstemming.
Implementativamente, los stemmers son un conjunto de reglas aplicadas en orden para transformar y
eliminar sujos de palabras. Al igual que el lematizador, el stemmer es una herramienta que incrementa
el recall a costa de la precisión. Normalmente, un stemmer es más agresivo que el lematizador en cuanto
a las colisiones que genera ya que se basa en heurísticas y no en un análisis morfológico de las distintas
inexiones del vocabulario.
7 El
término POS (part of speech) del inglés se utiliza para referir a la identicación de una palabra como verbo, adjetivo,
sustantivo, adverbio, etc. Por ejemplo, los POS de la frase escribiendo el libro son (verbo,artículo,sustantivo).
8 El
término stem se traduce al español como raíz, lo cual maniesta la intención del stemming de encontrar una
raíz entre palabras similares semanticamente. No se debe confundir la raíz obtenida del stemming con la verdadera raíz del
lenguaje correspondiente, ya que la segunda sí es una palabra del diccionario.
32
CAPÍTULO 2.
ESTADO DEL ARTE
Existen implementaciones de stemmers para muchos lenguajes, para el inglés la más conocida es la versión
de Porter (Porter, 1980), mientras que para lenguajes como el castellano contamos con stemmers derivados
de los algoritmos snowball que describiremos en el próximo apartado.
Es preciso notar que el stemming no es aplicable a todos los lenguajes. Un ejemplo de esto es el chino,
el cual no permite el simple truncado sus palabras. El stemming tampoco tiene la misma efectividad
en todos los lenguajes. Por ejemplo, en lenguajes como el inglés el stemming es menos efectivo que en
español o francés ya que estos últimos son más exivos (Porter, 2001).
Stemmers Snowball
Snowball es un lenguaje para la generación automática de algoritmos de stem-
ming (Porter, 2001). El propósito de snowball fue crear un lenguaje común a partir del cual poder
especicar stemmers para distintos lenguajes.
A partir de snowball han surgido stemmers para lenguajes como inglés, francés, español, portugués,
italiano, rumano, alemán y muchos otros.
Los casos de estudio que vamos a analizar en la sección (2.4) utilizan stemmers construidos mediante
snowball.
Expansión de Consultas
A pesar de nuestros esfuerzos de matching mediante stemming o lematización, el problema subyacente a
la tarea de recuperación es complejo por las distintas formas de expresión y las ambigüedades que surgen
del uso del lenguaje.
A continuación denimos y ejemplicamos estas relaciones del lenguaje, las cuales presentamos grácamente en la gura (2.6).
Sinónimo
Dicho de un vocablo o de una expresión: Que tiene una misma o muy parecida signicación
que otro. (RAE, 2006). Los sinónimos son palabras que comparten la misma semántica. Ejemplos:
después vs. luego, comencé vs. empecé, violonchelo vs. cello.
Parónimo
Se dice de cada uno de dos o más vocablos que tienen entre sí relación o semejanza, por su
etimología o solamente por su forma o sonido. (RAE, 2006). En general los parónimos son palabras
con pronunciaciones iguales o similares pero semántica distinta. Ejemplos: maya vs. malla, concejo
vs. consejo, vaya vs. valla (en particular estos parónimos además son homófonos).
Merónimo y Holónimo
Cuando en dos palabras una de ellas constituye semanticamente la parte de un
todo representado por la otra, la parte es llamada merónimo y el todo es conocido como holónimo.
Ejemplos: dedo es merónimo de su holónimo mano, país es holónimo de su merónimo provincia.
Hiperónimo e Hipónimo
Cuando en dos palabras una de ellas constituye semanticamente un concepto
particular y la otra representa un concepto general, el concepto particular es llamado hipónimo y
el concepto general es el hiperónimo.
2.1.
33
INFORMATION RETRIEVAL
automóvil
hiponimo
hiperónimo
vehículo
holónimo
merónimo
butaca
merónimo
sinónimos
asiento (sust.)
parónimos
asiento (verbo)
Figura 2.6: Relaciones entre palabras. En este ejemplo vemos la sinonimia, paronimia, meronimia e
hiperonimia.
Estas relaciones del lenguaje traen consecuencias al motor de búsquedas, quien debe poder diferenciar
entre parónimos, asociar sinónimos y contextualmente tratar de forma adecuada merónimos, holónimos,
hipónimos e hiperónimos.
Por ejemplo, si nos encontramos en el contexto de un buscador de avisos clasicados, es común que exista
una diferencia de vocabulario entre quien publica avisos y quien busca servicios. La persona que ofrece sus
servicios como mecánico de autos puede publicar el aviso reparación de Mercedez-Benz, Audi y Volkswagen,
mientras que la persona que busca puede escribir la query servicio de chapa
y
pintura de Passat (siendo
éste un modelo particular de la línea Volkswagen).
Las herramientas que poseen los motores de búsqueda para mejorar su conocimiento y capacidad de tratar
expresiones son los tesauros. Un tesauro es una colección de relaciones entre palabras y conceptos que
ayudan al sistema a encontrar la forma adecuada de mejorar la respuesta ante una query. En un tesauro
podemos modelar las relaciones entre palabras, permitiendo expandir consultas utilizando sinónimos o
formas canónicas de un concepto.
Denición 2.1.10 (Expansión de Consultas).
una query inicial
q0
en una nueva query
q1
El proceso de expansión de consultas consiste en convertir
de forma tal que el conjunto de métricas asociadas el éxito
del sistema mejoren sus indicadores.
La denición (2.1.10) es intencionalmente general porque (a) la elección de las métricas que indican el
éxito del sistema es un problema en sí mismo con distintas soluciones (ver subsección 2.1.3) y (b) la
expansión de consultas normalmente se da mediante tesauros o relevance feedback.
En la actualidad existe una herramienta que modela las relaciones entre palabras conocida como WordNet
(Princeton, 2010). Esta herramienta ha sido portada a varios lenguajes y puede ser útil para resolver los
problemas de sinonimia, meronimia, etc. En la gura (2.7) podemos ver la versión web de WordNet 3.0.
34
CAPÍTULO 2.
ESTADO DEL ARTE
Figura 2.7: Versión web de WordNet 3.0. Para la palabra soccer del inglés obtenemos conjuntos de
sinónimos y otras relaciones.
Si bien WordNet es una herramienta muy útil, el modelado de un dominio particular suele tener complicaciones adicionales como palabras en otros lenguajes o marcas comerciales como sony o notebook, lo
cual hace difícil usarlo tal como se presenta. Además, en la gura (2.7) podemos ver que si quisiéramos
expandir la consulta soccer (fútbol) utilizando relaciones como header, seguramente perjudicaríamos la
precisión debido a las relaciones de sinonimia de esta última (header se puede utilizar para referirse a
encabezados de documentos).
Caches
Dependiendo de los volúmenes de datos y niveles de concurrencia del sistema de IR, es común que la
búsqueda o la indexación de documentos utilicen intensivamente recursos como disco, red o el RDBMS.
Las respuestas a operaciones repetitivas pueden reutilizarse para ahorrar recursos y mejorar los tiempos
de respuesta del sistema. Esta reutilización se implementa por medio de memorias caché.
Existen distintos niveles de caché que responden a distintas granularidades de acceso a la información.
En la capa más externa, de grano grueso, tenemos caches de páginas de resultados. Cuando la query del
usuario es idéntica a otra query efectuada recientemente, los caches de páginas de resultados retornan una
versión servida con anterioridad. Es preciso notar que si las búsquedas se sirven de forma personalizada,
la clave del caché (típicamente la URL) debe contener información que identique al usuario, de forma
de no servir a un usuario A un listado hecho a medida de un usuario B.
Un nivel más abajo, podemos tener un caché de resultados del índice invertido, el cual responde cuando
un usuario busca un conjunto de términos que ya ha sido buscado por el resto de los usuarios. Este tipo de
caches suele ser muy efectivo ya que la entradas del índice invertido no deberían depender de un usuario
particular, permitiendo alto reuso de las entradas en caché.
Si el número de objetos a almacenar es muy grande, podemos utilizar un caché externo como Memcached
(Memcached, 2009). La utilización de un caché externo permite aumentar la tasa de aciertos (hit ratio)
en caso de que las búsquedas o procesos de indexado corran en paralelo en varios equipos.
2.1.
35
INFORMATION RETRIEVAL
Es preciso notar que el caché de grano grueso como el de páginas de resultados tiene un hit ratio menor
a uno de grano no (como el del índice invertido). Sin embargo, el caché de páginas típicamente agrega
el trabajo de confección del listado. Estos temas se discuten extensamente en (Baeza-Yates et al., 2008).
Para el caso de las búsquedas, debemos tener en cuenta que el nivel de uso concurrente del sistema suele
ser una variable difícil de controlar, por lo que debemos estar preparados para utilizar caches donde sea
necesario.
Como veremos al hablar de mapeos objeto relacionales (ORM, ver subsección 2.3.7) y al tratar Hibernate
Search y Compass (subsecciones 2.4.2 y 2.4.3), el hecho de recuperar objetos desde un índice requiere
obtener todos sus atributos mediante un proceso llamado hidratación . Si la hidratación se implementa
utilizando el ORM, es importante disponer de un caché que evite comunicación innecesaria con el RDBMS.
Para esto, los ORM de mayor envergadura suelen implementar un caché a nivel de sesión (caché L1) y
otro compartido entre sesiones (caché L2).
2.1.6. Técnicas de Puntaje y Relevancia
En esta subsección vamos a analizar técnicas para generar ordenamientos de documentos aplicables a
nuestro motor de búsqueda sobre objetos. En particular nos interesa saber cómo:
puntuar documentos más allá de la noción de relevancia dada por el modelo de IR,
indexar objetos cuya estructura excede la del texto plano,
utilizar técnicas de puntuación por relación entre documentos (PageRank y HITS),
mejorar la relevancia por medio de análisis de query logs y realimentación por parte del usuario.
Reglas de Negocio
Los modelos de IR que hemos analizado en la subsección (2.1.4) están orientados a recuperar y puntuar
los documentos desde el punto de vista de la relevancia.
Sin embargo, cuando se construye un sistema de IR se debe tener en cuenta que el éxito del mismo excede
la capacidad de asociar documentos u objetos con queries. Las fórmulas de puntajes derivadas de las reglas
del negocio son las que tienen la última palabra acerca de cómo se presentan los resultados al usuario.
Para esto se suelen utilizar variables de negocio (antigüedad del resultado, pago por posicionamiento, etc)
como fuentes adicionales de puntaje, las cuales complementan la relevancia del modelo de IR.
Las distintas fuentes de puntajes se introducen en fórmulas matemáticas que determinan el orden nal
de los resultados. Mas allá del tipo de fórmula a utilizar, nos encontraremos con dos tipos de reglas:
clusterización y posicionamiento. Las reglas de clusterización separan documentos en clases, mientras
que las reglas de posicionamiento priorizan documentos de forma interna a un cluster. Este tipo de reglas
también se las puede conocer como reglas duras y reglas blandas.
Los atributos del documento y la query se suelen utilizar como variables de entradas a algoritmos de
puntuación. Esas variables de entradas se suelen llamar proxys. A continuación presentamos un ejemplo
de este modelo de puntajes.
Ejemplo 2.1.9 (Reglas duras y blandas).
Un sistema de clasicados online prioriza sus avisos mediante
las siguientes reglas de negocio:
Los avisos se dividen en dos sectores: pagos y gratis. Los avisos pagos aparecen siempre primero
que los avisos gratis.
La relevancia dada por el modelo de IR es un valor entre 0 y 1 que se pondera como el 60 % del
puntaje nal del aviso.
La antigüedad del aviso pondera el 40 % restante del puntaje, siendo este valor igual a
Los avisos de más de 1 año de antigüedad no suman puntaje en este proxy.
1−
meses antiguedad
.
12
36
CAPÍTULO 2.
ESTADO DEL ARTE
Figura 2.8: Reglas duras y blandas. En este ejemplo generamos dos clusters: avisos pagos y gratuitos.
Dentro de cada cluster opera la formula descripta en el ejemplo.
Para cumplir con la primera restricción podemos sumar un valor unitario a los avisos pagos y luego
ponderar según la segunda y tercera restricción:
Antigüedad (dj )
Score (q, dj ) = 1 × AvisoP ago (dj ) + 0, 6 × Similitud (q, dj ) + 0, 4 × 1 −
12
Donde:
AvisoP ago (dj ) ∈ {0, 1}
según si el aviso es pago o no.
Similitud (q, dj ) ∈ [0, 1]
según denido en la implementación del modelo de IR.
Antigüedad (dj ) ∈ {0; 1; . . . ; 12}
representando los meses del año.
En el diseño de la propuesta que presentaremos en el capítulo 3 tendremos en cuenta las reglas duras y
blandas, proveyendo las herramientas necesarias para implementarlas en cada dominio.
Zonas y Campos
Usualmente, los documentos suelen contener meta datos o estructuras de mayor organización que el
simple texto libre. Un ejemplo podría ser un documento que contiene una fecha de creación, un autor y
un cuerpo principal.
Estos meta datos o estructuras son conocidos como campos y zonas (Manning et al., 2008). Manning
propone distinguir estos dos conceptos llamando campo a un breve fragmento de meta datos y zona a un
campo de texto libre de mayor tamaño.
El hecho de contar con campos y zonas genera la oportunidad de efectuar queries más precisas sobre
distintas partes de la estructura del documento. Por ejemplo, podríamos efectuar búsquedas por rangos
(ver subsección 2.1.5) sobre ciertos campos y búsquedas booleanas sobre determinadas zonas.
Ejemplo 2.1.10.
Supongamos que contamos con un sistema que almacena noticias estructurándolas en:
publish-date, autor, title
y
content.
Si contamos con la posibilidad de efectuar consultas booleanas sobre el
corpus de noticias, podemos pensar en las siguientes consultas:
autor:(Kevin*) AND publish-date:(2009) AND content:(taiwan china conict)
publish-date:(2001 to 2009) AND content:(stock market IPO)
Al introducir campos y zonas debemos replantearnos cómo valorizar los documentos en cada una de las
fórmulas de similitud de los modelos de IR.
En adelante, sin pérdida de generalidad, vamos a asumir que los campos se utilizan para ltrado y sólo
necesitamos incluir la zonas en el cálculo de similitud del modelo de IR.
2.1.
37
INFORMATION RETRIEVAL
Denición 2.1.11
.
(Similitud Booleana para Zonas)
Para el caso del modelo booleano, redenimos la
similitud como la suma ponderada de la relevancia booleana en cada zona (Manning et al., 2008):
SimilitudBool−Zones (q, dj ) =
X
gi ×SimilitudBool (q, dj [i])
(2.1.8)
∀i
donde:
gi ∈ [0, 1] es un valor que pondera la importancia de la zona i (constante para todos los documentos),
X
gi = 1,
∀i
SimilitudBool (q, dj [i]) ∈ {0; 1}
dj .
representa la similitud binaria de la query
q
en la zona
i
del docu-
mento
La denición (2.1.11) introduce valores
gi
asociados a la importancia de cada zona en un documento,
pero no explicita cómo se deben escoger dichos pesos. El problema de elegir el valor de los pesos
gi
es
crucial ya que afecta de manera determinante al éxito del sistema.
Para escoger dichos valores, podemos utilizar un método de aprendizaje supervisado en el cual etiquetamos
(qn , dj ) mediante la función de entrenamiento Φ (qn , dj ) ∈ [0, 1] , la cual da valores cercanos a 1 para
Φ (qn , dj ), podemos obtener
→
→
−
−
pesos gi ∈ G planteando una función de costo ε G , Φ (qn , dj ) para luego resolver el problema de
pares
pares relevantes entre si y cercanos a 0 para pares poco relevantes. Utilizando
los
optimización:




 X h→
X

i
−
2
arg mı́n
ε
G
,
Φ
(q
,
d
)
=
arg
mı́n
[Φ
(q
,
d
)
−
Similitud
(q
,
d
)]
n
j
n
j
Bool−Zones
n
j
→
− 
→
− 


G
G
∀n,j
∀n,j

"
#2 
X

X
Φ (qn , dj ) −
gi ×SimilitudBool (qn , dj [i])
= arg mı́n
gi 

∀n,j
Ejemplo 2.1.11.
gi para documentos de 2 zonas (i
binaria (sólo toma los valores 0 y 1).
Obtengamos el valor óptimo de los pesos
resultados de una función
Φ
(2.1.9)
∀i
= 2)
y los
Para resolver el problema es conveniente realizar algunas deniciones y simplicaciones a la notación:
establecemos
SimilitudBool (qn , dj [0]) = s0 (qn , dj ) = s0
y
SimilitudBool (qn , dj [0]) = s1 (qn , dj ) =
s1
Rij = {R00 ; R01 ; R10 ; R11 } para indicar la cantidad de casos en los que
y a su vez hubo/no hubo coincidencia booleana en la zona i y j (R00 es el caso en el que no
hubo coincidencia en ninguna zona, R10 indica que hubo coincidencia en la primera zona y no en
denimos las variables
Φ=1
la segunda, etc).
las variables
dado que
Nij = {N00 ; N01 ; N10 ; N11 }
g0 + g1 = 1,
de forma idéntica a
Rij ,
pero para el caso en el que
podemos simplicar deniendo una única variable
Φ = 0.
g/g0 = g → g1 = 1 − g
Entonces debemos reemplazar estas variables en la ecuación (2.1.9):
#2
"
X
Φ (qn , dj ) −
∀n,j
X
gi ×SimilitudBool (qn , dj [i])
=
∀i
εtotal =
X
2
{Φ − [s0 g + s1 (1 − g)]}
∀n,j
Vemos las contribuciones aportadas en cada caso:
(2.1.10)
38
CAPÍTULO 2.
ESTADO DEL ARTE
cuando s0 = 0 y s1 = 0 se produce una contribución Φ − [0 × g + 0 × (1 − g)] = Φ. Variando Φ
para sus valores 0 y 1 se obtiene una contribución de R00 (ver que estamos sumando R00 veces el
valor Φ = 1).
s0 = 0 y s1 = 1
2
N01 (g − 1)
s0 = 1
y
s1 = 0
s0 = 1
N11 .
y
s1 = 1
produce una contribución
produce una contribución
2
[Φ − (1 − g)]
2
(Φ − g)
produce una contribución
. Variando
. Variando
Φ
Φ
se expande en
2
{Φ − [g + (1 − g)]} = (Φ − 1)
2
R01 g 2 +
se expande en
2
R10 (1 − g) + N10 g 2
. Variando
Φ
se obtiene
Entonces reescribimos la ecuación (2.1.10) como:
2
2
εtotal = R01 g 2 + N01 (g − 1) + R10 (1 − g) + N10 g 2 + R00 + R11
Utilizando el hecho de que
2
2
(g − 1) = (1 − g)
:
2
εtotal = (N01 + R10 ) (1 − g) + (R01 + N10 ) g 2 + R00 + R11
Diferenciando respecto de
g
e igualando a cero obtenemos:
dεtotal
= −2 (N01 + R10 ) (1 − g) + 2 (R01 + N10 ) g = 0
dg
ĝoptimo =
De esta manera obtenemos el estimador
N01 + R10
N01 + R10 + R01 + N10
ĝoptimo
que minimiza el error cuadrático para el conjunto de
entrenamiento.
Cuando las funciones
Φ (qn , dj )
o
Similitud (qn , dj )
no asumen valores binarios, el problema del ejemplo
(2.1.11) se vuelve más difícil de resolver analíticamente. Para estos casos es necesario utilizar otros métodos
para minimizar los errores de clasicación.
Al estudiar Apache Lucene (subsección 2.4.1) veremos que la heurística que utiliza para priorizar zonas
(en la terminología de Lucene se habla de campos) es una alteración de la fórmula de puntajes muy
similar a la ecuación (2.1.8). La diferencia en el caso de Lucene es que los pesos
gi
pueden tomar valores
arbitrarios.
Un caso adicional a contemplar es cuando los documentos son heterogéneos. Este es el caso del buscador
sobre objetos que propondremos en el capítulo 3, siendo que las soluciones para dicho caso las posponemos
hasta tratar la propuesta de solución.
PageRank y HITS
Los modelos de IR que hemos tratado hasta ahora ignoraban las inter relaciones entre documentos. En
casos como la búsqueda web, es benecioso tener en cuenta la relación entre documentos como una medida
de la autoridad que tiene una página como fuente de información relevante.
En esta sección describiremos dos algoritmos muy conocidos que generan métricas de relevancia basándose
en la estructura del grafo que interconecta entes del dominio del problema, para luego en el capítulo 3
analizar la factibilidad de incluir estos algoritmos en nuestro motor de búsqueda sobre objetos.
Para ser consistentes con la bibliografía, en esta subsección vamos a dejar de hablar momentáneamente
de documentos para pasar a hablar de páginas. Mas allá de que este último término denota el uso de
estos algoritmos en los buscadores web, no hay mayores diferencias entre una página y un documento.
2.1.
39
INFORMATION RETRIEVAL
PageRank
El algoritmo PageRank descripto en (Page et al., 1998) fue utilizado como fuente de rel-
9
evancia para el algoritmo de búsqueda del buscador web Google . A continuación vamos a explicar los
conceptos detrás de PageRank así como su mecánica de cálculo basándonos en (Austin, 2006).
La idea principal detrás de PageRank es dejar que las páginas se voten entre sí mediante hipervínculos.
Para explicar este proceso es necesario introducir algunas deniciones.
Denición 2.1.12 (PageRank).
tancia
I (Pj ),
Pi y un conjunto de páginas Pj ∈ Bi
Pi , entonces denimos el valor:
Sea una página puntual
cada una con lj hipervínculos apuntando a
P ageRank (Pi ) = I (Pi ) =
X I (Pj )
lj
de impor-
(2.1.11)
Pj ∈Bi
Según la denición (2.1.12), cada
Pj
que apunte a
Pi
emite un voto proporcional a su importancia
(PageRank) e inversamente proporcional al número de enlaces presentes en ella.
Lo primero que notamos al analizar esta denición es que para calcular el PageRank de una página
debemos conocer el de todas las que enlazan a ella. Otro aspecto a notar de la denición (2.1.12) es que
los valores de
I (Pj )
tienen sentido sólo en términos relativos a las páginas que entraron en el cálculo y
no representan valores absolutos de importancia para todo el universo.
A lo largo de este apartado veremos cómo calcular simultáneamente los valores de
I (Pi ) mediante cálculo
matricial y teoría de probabilidades. Comencemos con algunas deniciones:
Denición 2.1.13
.
(Matriz de Hipervínculos)
Denimos la matriz de hipervínculos
H = [Hij ]
de la
siguiente forma:
(
Hij =
1/lj
0
Preventivamente podemos pensar en
H
Denición 2.1.14
(Vector de PageRank)
Pi
Utilicemos la matriz
tal que
es,
Pj apunta
a
Pi )
como una matriz que expresa la probabilidad de que pasemos al
Pj
PageRank de cada página
Pj ∈ Bi (esto
otro caso
azar de una página
a una
Pi .
si
.
H
para redenir
Denimos el vector
→
−
→
−
I = HI .
Esto es,
→
−
I
I (Pi ):
→
−
I = [I (Pi )]
cuyas componentes son los
es un autovector de
H
cuyo autovalor es
λ = 1.
Este tipo de autovectores cuyo autovalor es unitario se conoce como autovector estacionario.
El problema al que nos enfrentamos ahora es cómo obtener
→
−
I.
Para esto podemos utilizar un método
estándar para la obtención de autovectores conocido como el método de potencias:
−−
→
−
→
I k+1 = HI k
Para utilizar el método de potencias debemos proponer un vector inicial
→
−0
I
e iterar hasta obtener una
diferencia entre pasos menor a un valor umbral.
El siguiente problema con el que nos encontramos ahora es cómo asegurar que este procedimiento:
1. sea convergente,
2. no dependa del vector inicial
→
−0
I ,
3. nos entregue la información que estamos buscando.
9 Google
no ha dado a conocer cuál es la incidencia que hoy en día tiene PageRank en el puntaje de una página.
40
CAPÍTULO 2.
Con la formulación de
H
ESTADO DEL ARTE
que hemos hecho a priori, ninguno de estos tres requerimientos se cumple
incondicionalmente.
Estos problemas se pueden resolver replanteando el modelo en un esquema de navegación al azar (random
surfer, Page et al. 1998). Este modelo supone un usuario que sigue al azar los lj hipervínculos de
Pj ,
saltando aleatoriamente de página en página. Dado este esquema de navegación al azar, los autores
proponen considerar el PageRank como el tiempo promedio que se pasa en una página
las distintas páginas
Pi
llegando desde
Pj .
Sin embargo, la navegación al azar tiene problemas al encontrarse con un sub grafo de la web que actúa
como sumidero. Si el grafo web contiene un sumidero (totalmente factible), una vez que navegamos
dentro de él no podremos salir (de hecho los sumideros producen PageRank nulos para las páginas
fuera del sumidero). Para resolver esta situación se plantea una operación adicional de teleportación. La
teleportación se implementa jando un valor a las columnas nulas de
H,
modelando un salto aleatorio
hacia cualquier página del grafo web. Formalizando:
Denición 2.1.15.
la matriz
A ∈ Rn×n
como en la denición (2.1.13), entonces denimos:
cuyos valores serán nulos excepto para las columnas donde
A
en el que los elementos de
la matriz
H ∈ Rn×n
Sea la matriz
tomarán el valor
H
sea nula, caso
1/n.
S ∈ Rn×n /S = H + A
Vemos entonces que
A representa una probabilidad uniforme de teleportarnos desde una página sumidero
S es una nueva versión de H que
hacia cualquier otra página del grafo web. En consecuencia, la matriz
no sufre del problema de PageRank nulos debido a sumideros.
Para poder utilizar el método de las potencias con
como correcto, necesitamos que
S
S
y asegurarnos que el proceso sea tanto convergente
sea estocástica y primitiva (Austin, 2006). La condición de estocástica
se cumple fácilmente porque la matriz cuenta con entradas positivas y la suma de sus columnas es unitaria.
La condición de primitiva requiere que para algún
(esto se traduce en que luego de
m
m se cumpla que Sm
tenga todas sus entradas positivas
iteraciones podremos navegar desde una página hacia cualquier otra
con probabilidad no nula).
La solución nal al problema se da introduciendo una dualidad en el comportamiento del navegante:
Denición 2.1.16
.
(Google Matrix)
Sea la matriz estocástica
S
de la denición (2.1.15), denimos la
matriz de Google:
G = αS + (1 − α) 1
con
α ∈ [0, 1] .
La nueva matriz
G
incorpora el conocimiento de probabilidades surgido de los enlaces del grafo web más
un comportamiento aleatorio de teleportación regulado por
(1 − α).
El valor de
α
no sólo tiene que ver
en el grado de aleatoriedad introducido al modelo de navegación, sino que es responsable de la velocidad
de convergencia del método (esto se relaciona con el método de las potencias y la magnitud del segundo
autovalor de
S).
Con esta denición, dado que
G es una suma de matrices estocásticas, esta también lo es. Adicionalmente,
G también es primitiva. Estas dos propiedades nos aseguran la
como todas sus entradas son positivas,
convergencia por el método de las potencias, permitiéndonos calcular el PageRank mediante la fórmula:
−−
→
−
→
I k+1 = GI k
(2.1.12)
En conclusión, PageRank es un modelo heurístico de puntajes con sustento en una matemática rme que
nos permite obtener un puntaje estático dependiente de la estructura de enlaces del dominio (en este
caso, páginas web). Los autores han calculado el PageRank para 322 millones de links aproximando
por
−→
I 52
→
−
I
obteniendo un mínimo margen de error (Page et al., 1998).
La explicación que acabamos de dar muestra los conceptos y pasos más importantes para su cálculo.
Nuestro interés en presentar PageRank es considerar estos conceptos para dar valor a la relación entre
objetos del dominio. En el capítulo 3 se retomará este problema para analizar qué grado de soporte
podemos dar a este tipo de técnicas. Para profundizar acerca del cálculo de PageRank se puede consultar
(Austin, 2006).
2.1.
41
INFORMATION RETRIEVAL
HITS
Además del PageRank que acabamos de estudiar, existen otros métodos que tienen en cuenta
la estructura de links de las páginas web. Un método muy conocido es HITS (Hyperlink-Induced Topic
Search).
Conceptualmente, este método plantea que existen dos clases de páginas importantes: los hubs (concentradores) y
authorities
(autoridades, en el sentido de referente acerca de un tema). La propuesta es
entonces asignar a cada página un puntaje como hub y otro como authority.
Las autoridades son páginas que contienen información de calidad acerca de un tema. Por ejemplo, si buscamos el término Ingeniería, algunas autoridades podrían ser http://www..uba.ar (Facultad de Ingeniería
de la UBA), http://www.ieee.org (IEEE) o http://www.cai.org.ar (Centro Argentino de Ingenieros).
Por otro lado, existen páginas concentradoras que no son en si mismas una fuente nal de información
sino que apuntan a autoridades mediante un listado de enlaces acerca de un tema (un directorio como
los que vimos en la subsección 2.1.5 sería un caso de concentrador).
La heurística que podemos aplicar para valorizar páginas es pensar que un buen concentrador apunta a
buenas autoridades y una buena autoridad es apuntada por buenos concentradores.
A continuación mostramos brevemente el procedimiento matemático para luego obtener el algoritmo
HITS basándonos en el desarrollo propuesto en (Manning et al., 2008).
Denición 2.1.17 (Puntaje de Hub y Authority).
denimos los puntajes hub
h (v) =
X
h (v)
a (v)
y authority
Dada una página
v
que enlaza a una página
yi /v 7→ yi ,
como:
a (yi )
v7→yi
a (v) =
X
h (yi )
v7→yi
Denición 2.1.18
→
−
h = (h1 , . . . , hn )
y
(Vector de Hub y Authority)
→
−
a = (a1 , . . . , an ),
.
Para una web de
N
páginas, denimos los vectores
donde cada componente de los vectores son los puntajes respectivos
de cada página como hub y authority.
Conceptualmente, una página incrementa su puntaje como concentrador si enlaza páginas con alto puntaje
de autoridad. Recíprocamente, una página es una buena autoridad cuando esta enlazada por buenos
concentradores.
Denición 2.1.19 (Matriz de Adyacencia).
Aij
vale uno si la página
i
Denimos la matriz de adyacencia
enlaza a la página
j
A,
donde la componente
y cero en otro caso.
Ahora estamos en condiciones de reescribir las ecuaciones de la denición (2.1.17) utilizando la matriz de
adyacencia:
→
−
h
→
−
a
←
←
−
A→
a
−
T→
A h
(2.1.13)
Dado que el término izquierdo de la ecuación (2.1.13) forma parte del derecho, podemos proponer un
cálculo iterativo y reescribirla como:
→
−
h
→
−
a
Si en la ecuación (2.1.14) reemplazamos el signo
autovectores de
los autovectores
T
T
←
←
→
−
AAT h
−
AT A→
a
(2.1.14)
→
− −
← por un =, entonces h y →
a serían respectivamente los
AA y A A . Utilizando este último hallazgo reescribimos la fórmula (2.1.14) asumiendo
λh y λa :
42
CAPÍTULO 2.
→
−
h
→
−
a
ESTADO DEL ARTE
→
−
(1/λh ) AAT h
−
(1/λ ) AT A→
a
=
=
(2.1.15)
a
Las actualizaciones iterativas de la fórmula fórmula (2.1.14) pueden ser escaladas por el autovalor apropiado, lo que equivale a utilizar el método iterativo de las potencias para encontrar los autovectores de
AAT
y
valores
AT A (Manning et al., 2008). Dado que el autovector
→
− →
−
de h y a no solo son únicos sino que son estacionarios
principal de
AAT
y
AT A
es único, los
y sólo dependen la estructura de enlaces
de la web considerada (Manning et al., 2008).
Cuando estudiamos PageRank vimos que dicha medida no dependía de la query en particular
10 , sin em-
bargo, para aplicar el concepto de concentrador y autoridades, el puntaje de Hubs y Authorities debe
calcularse para un tópico en particular. Para esto existe un paso adicional que implica seleccionar un
subconjunto de la web que es potencialmente concentradora o autoridad acerca de un tópico para luego
generar vectores
→
−
h
y
→
−
a
respecto de dicho tópico. Existen distintas heurísticas para elegir este subcon-
junto, las cuales exceden el tratamiento que queremos dar a este sistema de puntajes.
Al igual que con PageRank, la combinación de los puntajes entregados por el modelo de IR, reglas de
negocio y el análisis estructural (en este caso de enlaces) depende de la aplicación particular. Para un
análisis exhaustivo acerca de HITS puede consultarse (Kleinberg, 1999).
Relevance Feedback y Query Log Mining
Las técnicas de relevance feedback (en adelante también retroalimentación o simplemente de RF ) buscan
involucrar al usuario en el proceso de recuperación para mejorar el conjunto nal de resultados (Manning
et al., 2008).
En general el ciclo de RF tiene los siguientes pasos:
1. El usuario especica una query simple
2. El sistema retorna un conjunto inicial de resultados
3. El usuario marca qué resultados son relevantes y cuáles no lo son
4. El sistema computa una nueva representación de la necesidad del usuario y corrige el conjunto
inicial efectuando una nueva recuperación que tiene en cuenta la retroalimentación.
Existen técnicas de RF que requieren que el usuario indique explícitamente qué documentos son relevantes
así como técnicas menos intrusivas que se nutren de la relevancia implícita que surge de la interacción
con el sistema. Esos dos modelos se llaman respectivamente RF explícito y RF implícito.
RF Explícito y Algoritmo de Rocchio
Dentro del modelo vectorial de IR, Rocchio propuso un
algoritmo que reformula la query del usuario buscando mejorar su efectividad (Rocchio, 1971). Este
algoritmo utiliza la clasicación explícita de relevancia provista por los usuarios y comprende los siguientes
pasos:
1. El usuario inicia la búsqueda con una query inicial
q
2. El usuario clasica los resultados explícitamente, creando dos conjuntos:
evantes y
Dnr
para los no relevantes
3. El sistema expande la query inicial
10 Existen
q
en una nueva query
qopt
ˆ
adaptaciones que calculan el PageRank para tópicos particulares.
Dr
para documentos rel-
2.1.
43
INFORMATION RETRIEVAL
Los conjuntos
Dr
y
Dnr
son una aproximación de los verdaderos conjuntos
Rq
y
Rq
(relevantes y no
relevantes). Si conociéramos estos últimos y asumiendo un modelo vectorial de IR, tendríamos que buscar
acercar la query
→
−
q
al centroide del conjunto
Rq ,
alejándolo del centroide de
Rq .
Esto equivale a resolver
el problema de optimización:
−
→
q−
Similitud (q, Rq ) − Similitud q, Rq
opt = arg máx
→
−
q
Bajo la fórmula de similitud del coseno, la separación óptima se da cuando (Manning et al., 2008):




 1 X →
X
−
→
−
1
−
−
→
dj −
dj
qopt = arg máx
→
−
Rq →

q 

 |Rq | →
−
−
(2.1.16)
dj ∈Rq
dj ∈Rq
El algoritmo propuesto por Rocchio varía la fórmula (2.1.16) para tener en cuenta que no conocemos los
conjuntos
Rq
y
Rq
sino que tenemos sus aproximaciones
Dr
y
Dnr .
Teniendo esto en cuenta llegamos a
la fórmula:
X →
−
−
β X →
γ
−
→
−
ˆ→
dj
q−
dj −
opt = α q0 +
|Dr | →
|Dnr | →
−
−
dj ∈Dr
Donde
(2.1.17)
dj ∈Dnr
−
(α, β, γ) ∈ [0, 1] × [0, 1] × [0, 1] /α + β + γ = 1, →
q0
es la query inicial del usuario y
−
ˆ→
q−
opt
es un
estimador de la query óptima de la ecuación (2.1.16). En los casos en los que un componente de
−
ˆ→
q−
opt
resulta negativo, éste se ignora (es decir, se iguala a cero).
La fórmula (2.1.17) produce una mezcla entre la query inicial
de
Dr
y
Dnr .
→
−
q0 y los términos presentes en los documentos
Es decir, si los documentos relevantes para la query glaciares suelen contener el término
antártida, entonces la versión modicada de la query dará mayor exposición a los documentos acerca de
este último término.
Hagamos algunas consideraciones prácticas acerca de este método:
En la práctica se ha encontrado que el feedback positivo (asignación de documentos a
utilidad que el feedback negativo (asignación a
Dnr )
fórmula (2.1.17) para reejar este hecho, debemos escoger
También debemos notar que normalmente
→
−
q0
Dr )
es de mayor
(Manning et al., 2008). Si queremos ajustar la
β→1
y
γ → 0.
tiene pocos términos, mientras que
P→
−
dj
agregará poten-
cialmente muchos términos a la nueva query (además de que estamos agregando varios documentos, éstos
tienden a ser más largos que las queries). Al agregar términos debemos efectuar más accesos al índice
invertido, lo cual seguramente degrade el rendimiento del sistema. Para contener este problema podemos
expandir la query solo con los
n
términos de mayor score en
RF Indirecto y Query Log Mining
P→
−
dj .
Comúnmente los usuarios buscan satisfacer sus necesidades de
información en el menor tiempo posible, siendo muy difícil que se detengan a proveer feedback explícito
sobre los resultados (sobre todo cuando los benecios de hacerlo no son claros). Para poder obtener
feedback en un contexto donde el usuario no provee una clasicación explícita, tenemos que modelar las
acciones del usuario como actos de clasicación.
Este modelado se traduce en interpretar como señales de relevancia la concentración de clics en ciertos
resultados o nodos de una taxonomía. Por otro lado, podemos interpretar hechos como abandono de la
sesión, inactividad por periodos extensos, reformulación sistemática de queries o bajo CTR (click-thru
rate o tasa de clics sobre impresiones ) como señales de ausencia de resultados relevantes.
El método de inferir la clasicación utilizando la actividad indirecta del usuario se conoce como RF
implícito o indirecto.
En la práctica, los métodos derivados del RF indirecto son menos conables que los del RF explícito,
donde el usuario juzga explícitamente los documentos (Manning et al., 2008). Esto es intuitivo ya que
siempre que haya un uso honesto de la herramienta, los juicios explícitos de relevancia tienen mayor
delidad que un modelado mediante timeouts, abandonos y CTR.
44
CAPÍTULO 2.
ESTADO DEL ARTE
Para efectuar el RF indirecto necesitamos técnicas de análisis del ujo de queries (también conocido como
query log analysis o clickstream mining ). Estos logs deben ser sucientemente expresivos para permitirnos
modelar el uso del sistema. Es decir, si vamos a trabajar con medidas de CTR, debemos poder ordenar
los registros secuencialmente por usuario, lo que requiere contar con marcas de tiempo, identicadores de
sesión y la acción que efectuó el usuario.
Los logs de queries también tienen usos que van mas allá del relevance feedback como ser la generación
de tesauros, análisis de uso del sistema, etc.
En nuestra propuesta del capítulo 3 retomaremos este tema para analizar cómo se puede incluir este tipo
de herramientas al hacer IR sobre objetos.
En este punto hemos completado la descripción necesaria del estado del arte en cuanto a Recuperación
de Información.
Sobre el nal de este capítulo (sección 2.4), retomaremos estos temas para describir y comparar las
herramientas del estado del arte, profundizando nuevamente durante los capítulos de desarrollo de la
propuesta y experimentación.
2.2.
Modelos de Dominio
Esta segunda sección trata la segunda componente de nuestro problema: el diseño de software. Dado que
estamos interesados en generar un framework de indexación y recuperación, es necesario que establezcamos
bases objetivas sobre las cuales analizar las alternativas de diseño y construir nuestra propuesta.
La subsección 2.2.1 presenta los conceptos sobre los que tratará esta sección: modelos de dominio, frame-
works, arquitecturas empresariales y patrones de arquitectura. Luego de sentar estas bases, en las subsecciones (2.2.2) y (2.2.3) profundizamos acerca de criterios de implementación.
2.2.1. Deniciones Generales
Las próximas subsecciones hablan de frameworks (qué son, tipos y características), arquitecturas em-
presariales (sobre las cuales vamos a obtener el mayor provecho del motor de búsqueda sobre objetos),
patrones de diseño y arquitectura (los que utilizaremos como conocimiento de base para discutir criterios e
implementaciones) , modelos de dominio (patrón de arquitectura utilizado para construir las aplicaciones
que vamos a indexar), inversión del control e inyección de dependencias (técnicas que diseño que tienen
consecuencias sobre los frameworks como el que queremos construir).
Frameworks y Librerías
Para proponer el framework de indexación de objetos, primero debemos preguntarnos qué es un framework.
Denición 2.2.1
(Framework)
.
Un framework es un conjunto de clases que encarnan un diseño ab-
stracto para solucionar una familia de problemas relacionados soportando reutilización en una mayor
granularidad a la de las clases (Johnson y Foote, 1988).
Un framework nace a partir de la observación y aprendizaje de un dominio de problema particular
(Roberts y Johnson, 1996). La experiencia acumulada en un dominio de problema nos permite generalizar y crear abstracciones que resolverán un problema concreto. Por ejemplo: persistencia, rendering,
remotización o indexación.
A continuación clasicamos los frameworks en dos clases principales: caja negra y caja blanca (Johnson
y Foote, 1988; Roberts y Johnson, 1996).
El tipo de framework en el que debemos proveer el comportamiento especíco de nuestra aplicación
mediante subclasicación es conocido como de framework de caja blanca. Para dar el comportamiento
propio de nuestra aplicación, debemos subclasicar clases del framework, implementando métodos en
2.2.
45
MODELOS DE DOMINIO
forma polimórca. Los frameworks de caja blanca no pueden ser utilizados directamente sino que requieren
que creemos clases especícas para utilizarlos. Este proceso de creación de clases requiere que también
sepamos cómo funciona el framework, por lo que son más difíciles de aprender (respecto de los de caja
negra que explicaremos en el próximo párrafo). Dos ejemplos de frameworks de caja blanca podrían ser
(a) servlets de Java y (b) variantes del MVC en las que aportamos nuestro comportamiento especíco
extendiendo una clase Controller.
Por otro lado, existen frameworks en los cuales la comunicación se da mediante componentes que entienden
un protocolo especíco. Estos frameworks son los de caja negra. El usuario de este tipo de framework
provee comportamiento a través de colaboración entre objetos. Los frameworks de caja negra no requieren
de la subclasicación, por lo que introducen menor acoplamiento y requieren menor conocimiento acerca
de cómo está construido el framework .
En los frameworks de caja blanca, el estado de cada instancia se encuentra implícitamente disponible a
todos los métodos del framework como si fueran variables globales, mientras que en los de caja negra,
los parámetros se deben declarar e intercambiar explícitamente. Además, los frameworks de caja blanca
implementan reglas internas la jerarquía de subclasicación, mientras que en los de caja negra las reglas
son protocolares.
La madurez de un framework está dada por una transición que comienza con frameworks de caja blanca
hasta convertirse en un diseños reusables de caja negra (Roberts y Johnson, 1996).
Es importante poder distinguir un framework de una librería. Fowler dene una librería como un conjunto
de funciones (hoy en día organizadas en clases) a las que uno puede llamar para luego retomar el ujo
de control (Fowler, 2005). Según Fowler, la diferencia principal entre una librería y un framework es la
inversión de control (ver subsección 2.2.3).
En esta sección comentaremos algunos aspectos relacionados con los frameworks, para luego retomar
el tema al analizar los casos de estudio (subsección 2.4) y el comportamiento como framework de la
propuesta de solución.
Aplicaciones Empresariales
Las aplicaciones empresariales, tipo enterprise ó enterprise applications son uno de los distintos tipos de
sistemas posibles de construir. Este tipo de aplicaciones se desarrollan para soportar la actividad de una
organización, ya sea ejecutando directamente el negocio o soportando sus procesos y ujos de trabajo.
Las aplicaciones de este tipo se denen por ciertas características:
Envergadura: suelen ser grandes, costosas de construir y mantener; requieren de un equipo de trabajo
dedicado. Poseen múltiples pantallas para interactuar con usuarios. El volumen de información
procesada y almacenada es relativamente grande.
Transversales: interconectan procesos y datos de distintas áreas y departamentos de la organización
(Ej: contable, legal, atención al cliente, etc).
Integradores: suelen interconectar sistemas heterogéneos (de múltiples vendedores) y heredados.
Concurrentes y Distribuidos ó Clusterizados: soportan el acceso de múltiples usuarios concurrentes
y pueden estar distribuidos geográcamente o clusterizados para soportar altos niveles de concurrencia.
Transaccionales: suelen funcionar realizando transacciones sobre un RDBMS.
Multicapa: requieren un diseño de múltiples capas (tier
11 ).
Multiperl: hay usuarios y roles diferenciados (Ejemplo: usuarios frontend que utilizan el sistema y
usuarios backend que lo administran).
Seguridad y Certicación: al ser sistemas abiertos y que operan con datos sensibles se diseñan con
la seguridad en mente. Dependiendo de las características de la organización, el diseño del sistema
puede estar sujeto a certicación de normas como SOX (SOX, 2002) y/o PCI (PCI, 2006).
11 La
traducción de capa es layer y no tier (cuya traducción es feta). Sin embargo, a menos que sea necesario
distinguir entre tiers y layers, utilizamos los términos de manera intercambiable.
46
CAPÍTULO 2.
ESTADO DEL ARTE
Algunos ejemplos de sistemas empresariales:
reserva online de tickets aéreos,
banca electrónica,
voto electrónico,
tienda online de productos electrónicos,
sistemas de atención al público
Ejemplos de sistemas que no son enterprise:
juegos,
compiladores,
suites de dibujo y animación,
navegador web
Estamos especialmente interesados en indexar los objetos de aplicaciones enteprise por las siguientes
razones:
El volumen de información da lugar a la necesidad de hacer búsquedas de texto libre,
tienen diversidad de entidades, favoreciendo la necesidad de hacer búsquedas horizontales sobre los
datos (multi esquema),
tienen requerimientos particulares en materia de seguridad y distribución.
Vemos entonces que, siendo nuestro framework un huésped de este tipo de aplicaciones, el motor de
búsqueda que construiremos debe funcionar en sintonía con estos requerimientos.
Patrones de Diseño y Arquitectura
Para comenzar con este apartado, recordemos la denición de patrón de diseño :
Denición 2.2.2 (Patrón de Diseño).
Son descripciones de objetos y clases comunicadas que se person-
alizan para resolver un problema general de diseño en un contexto particular (Gamma et al., 1995).
Ampliando esta denición, podemos decir que los patrones de diseño son soluciones reutilizables y probadas
para problemas recurrentes en el diseño de software.
Así como los patrones de diseño solucionan problemas de diseño comunes del desarrollo de software,
existen problemas recurrentes desde el punto de vista de la estructura o arquitectura de un sistema, los
cuales también pueden ser resueltos aplicando soluciones estándar.
Una pregunta importante que surge al analizar los patrones de arquitectura es ¾Qué es la arquitectura
de un sistema ?
Lo que caracteriza a la arquitectura de un sistema es (Fowler, 2002):
las decisiones de alto nivel acerca de cómo dividir una aplicación en partes,
las deniciones acerca de las bases de una aplicación, decisiones que uno quiere tomar lo antes
posible,
elecciones difíciles de cambiar una vez establecido el patrón de arquitectura, tienen efectos a largo
plazo en el diseño,
2.2.
MODELOS DE DOMINIO
47
deniciones que eventualmente determinan qué responsabilidades se asignan a cada parte de la
aplicación.
En base a los puntos anteriores se catalogan soluciones bien conocidas en un conjunto amplio de escenarios
llamados patrones de arquitectura.
Como vemos, no es fácil denir con precisión qué es arquitectura en una aplicación ni tener la última
palabra acerca de si un patrón cumple con los puntos enumerados anteriormente. Lo que hoy cumple con
estos criterios puede dejar de hacerlo (por ejemplo, siendo simple de modicar) y plantearnos la duda
acerca de si realmente era una decisión de arquitectura.
Los patrones que reunen mayor consenso acerca de su pertenencia a esta clase son: patrones de lógica
de dominio (domain model, table module y transaction script ), el service layer, la arquitectura en layers
y los de patrones de presentación (model view controller y front controller ). Seguramente los patrones
que tratan acerca de cómo mantener conversaciones con los medios de persistencia también pueden
considerarse patrones de arquitectura: Table Data Gateway, Row Data Gateway, Active Record y Data
Mappers.
Así como originalmente los patrones de diseño fueron clasicados en creacionales, estructurales y de
comportamiento (Gamma et al., 1995), los patrones arquitecturales pueden clasicarse según el tipo de
problema que resuelven o la capa de la aplicación donde aplican (Fowler, 2002).
En el próximo apartado se trata el patrón de arquitectura domain model, el cual constituye una parte
fundamental del problema a resolver ya que sus miembros (objetos de negocio) son los objetivos a indexar
por el motor de búsqueda.
Modelos de Dominio
En el capítulo 1 denimos la noción de modelo de dominio por Fowler y luego enunciamos una versión
algo más rigurosa. Vamos a repasar brevemente la segunda denición que representa mejor nuestras
convicciones:
Domain Model
es un diseño de objetos que representa un dominio de problema de la realidad.
Este enfoque nos permite ver a los objetos como verdaderos modelos de entes de un dominio de problema.
Para cerrar esta denición profundizamos sobre las nociones de dominio de problema y realidad :
Realidad
comprende cualquier tipo de idea que podamos concebir (un objeto concreto, el amor, el odio,
la nada, etc).
Dominio de problema
es una porción de la realidad que vamos a modelar.
Algunos ejemplos de dominios de problema podrían ser las cuentas bancarias, un lesystem o la sincronización entre procesos.
Los modelos de dominio no son un concepto nuevo en el diseño de software, sino que implementan las
premisas básicas del diseño orientado a objetos. Dentro de un dominio particular se efectúa un análisis
según el cual se modelan entes de negocio junto a sus responsabilidades, protocolo y colaboraciones. En
un domain model encontraremos objetos como productos, personas, pagos, páginas web, etc.
El contexto de arquitecturas empresariales, Fowler propone al domain model como un patrón de arqui-
tectura. Las alternativas en dicho contexto son el transaction script y el table module (Fowler, 2002). A
continuación vamos a ejemplicar construyendo un pequeño modelo de dominio:
Ejemplo 2.2.1.
En este ejemplo presentamos un modelo de dominio para una aplicación que implementa
una red de contactos. Este modelo incluye las entidades fundamentales, sus relaciones y su comportamiento no trivial (esto es, excluimos operaciones como los set y get de atributos).
48
CAPÍTULO 2.
contacto
*-content
-likesThis: List<User>
1
*
-job
-interests
Story
publicación
Profile
ESTADO DEL ARTE
*
User
1
-stories: List<Story>
List<Application>
-photoAlbums: List<PhotoAlbum>
-profile: Profile
le gusta
* -apps:
*
1
*
1
etiquetado
*
Image
-title
-taggedUsers: List<User>
*
1
*
utiliza
*
*
PhotoAlbum
MiniApplication
-imageList: List<Image>
-creation: Date
+start()
+end()
Figura 2.9: Modelo de Dominio para una red social.
En este ejemplo se modelaron entidades de interés para el negocio junto con las operaciones que son
necesarias. Veamos que la relación entre clases puede afectar la manera en la que un motor de búsqueda
presentaría los resultados de una consulta. Por ejemplo, ante la búsqueda Viaje a París podemos priorizar los objetos
Story
que han sido más votados entre los usuarios o los
PhotoAlbum
de contactos donde
aparezcan fotos de París.
Es preciso mencionar algunos aspectos relacionados al comportamiento de los objetos:
Los modelos de dominio implementan las reglas de negocio de la aplicación. Estas reglas de negocio entran
en acción al ejecutar servicios de negocio ó lógica de aplicación. Estos servicios de negocio son ejecutados
por consumidores ó clientes del negocio. Para desacoplar los consumidores del modelo mismo, es posible
utilizar el patrón Service Layer (Fowler, 2002). El service layer presenta una API bien denida hacia los
consumidores, actuando como un Façade (Gamma et al., 1995). Una variante de esta implementación de
interfaz na entre los consumidores y el modelo es cuando el service layer actúa como objeto de negocio,
orquestando la secuencialidad en la invocación de los objetos de dominio. En la gura (2.10) vemos
grácamente cómo se organizan estas capas.
2.2.
49
MODELOS DE DOMINIO
Figura 2.10: Organización de capas en una aplicación enterprise con Service Layer, Domain Model y Data
Access Layer.
Habiendo comentado acerca de los datos y comportamiento en un modelo de dominio, podemos adelantarnos al análisis del problema y decir que, a los efectos del motor de búsqueda que vamos a construir,
nos interesa el estado de los objetos (valores de los atributos persistentes de la instancia) y no su com-
portamiento (métodos de la clase).
Los modelos de dominio expresan relaciones entre objetos por medio de subclasicación y colaboraciones.
Estas dos relaciones deben ser tenidas en cuenta en la recuperación de objetos ya que introducen complejidades que no están presentes en sistemas clásicos de IR.
Tanto la subclasicación como las colaboraciones presentan situaciones ante las que debemos decidir cómo
indexar los objetos. Veamos algunas de ellas:
Consolidación de la Jerarquía: cuando se indexa un objeto cuya jerarquía dene múltiples atributos,
es necesario consolidar la lista de los atributos indexables de dicha jerarquía.
Relaciones Todo-Parte: si un objeto de tipo Persona contiene un objeto indexable de tipo Nombre,
al efectuar una búsqueda que produce una coincidencia en los atributos de la clase Nombre, posiblemente queramos recuperar el objeto de tipo Persona. Este problema se maniesta en relaciones
todo-parte, donde las partes son indexables pero el objeto recuperable es el todo.
Colecciones: si tenemos una clase Receta que contiene una lista de objetos Ingrediente, probablemente
estemos interesados en indexar los objetos de la lista pero no la lista en sí misma. Para esto es
necesario diferenciar las colecciones de las relaciones todo-parte del caso anterior.
Referencias Circulares: si distintos objetos se referencian entre sí, debemos cuidar que la indexación
no caiga en bucles innitos.
Identidad. para que un objeto indexable pueda ser hidratado
12 debe proveer una identidad. En
ocasiones esta identidad es un objeto complejo en sí mismo. Cuando esto sucede, si el motor de
búsqueda necesita interactuar con el ORM para hidratar el objeto, debe ser capaz de almacenar la
clave de forma de poder reconstruirla al momento de recuperarlo.
Para resolver las tareas que estuvimos mencionando, es necesario que el lenguaje de programación cuente
con capacidades de meta programación. En el caso del lenguaje en el cual desarrollaremos nuestro framework (Java), la meta programación está mayormente soportada mediante reection.
12 La hidratación
de un objeto del índice consiste en completar su contenido, el cual a priori sólo contiene su clave principal,
con el resto de sus atributos. Este concepto lo revisaremos apropiadamente en las próximas secciones.
50
CAPÍTULO 2.
ESTADO DEL ARTE
Estos últimos párrafos son una primera muestra de los problemas que aparecen al considerar la indexación
de objetos cuya estructura y relaciones son de mayor riqueza que la del texto plano. La respuesta acerca
de cómo resolver estos problemas se diere hasta el capítulo 3, donde haremos propuestas concretas
teniendo también en cuenta las soluciones aportadas por los casos de estudio (sección 2.4).
2.2.2. Independencia del Modelo de Dominio
A medida que evolucionan las necesidades del negocio, el modelo de dominio deberá reejar los cambios en
éste. Dichos cambios generan la necesidad de actualizarlo y probarlo aisladamente. Estas dos actividades
requieren el desacoplamiento entre el modelo de dominio y otras capas del sistema (Dahan, 2009).
Para entender mejor el problema veamos un caso concreto: en una aplicación enterprise la capa de presentación puede ser especialmente volátil. Supongamos que con el paso del tiempo cambiamos nuestro
framework de presentación, dejando de lado tecnologías de reemplazos de strings para pasar a herramientas como JSP. Si nuestro modelo de dominio es independiente, seguramente no tendremos mayores problemas en migrar. Ahora, si nuestro modelo estaba acoplado a las vistas, la tarea de migración requerirá
un esfuerzo de desacoplamiento. Para este caso, si la lógica de negocio fue mezclada con la lógica de
presentación, generar una nueva vista (por ejemplo para dispositivos móviles) requerirá migrar código
que no estuvo correctamente encapsulado.
Los conceptos subyacentes en el ejemplo anterior son la cohesión y desacoplamiento, quienes favorecen
la reutilización y
portabilidad del modelo. Para lograr estas premisas, necesitamos que los frameworks
que interactúan con el modelo permitan la independencia del modelo.
A los efectos del motor de búsqueda que plantearemos en el capítulo 3, la independencia del modelo
es un aspecto que queremos respetar para ser buenos ciudadanos en el mundo de los frameworks. A
continuación vemos distintos ejemplos donde aparecen dependencias entre el modelo y los frameworks
que componen la aplicación:
Ejemplo 2.2.2.
Tomando el modelo de dominio del ejemplo (2.2.1), presentaremos una porción de código
dependiente del esquema de persistencia y otro independiente:
public c l a s s
User {
// a t r i b u t o s , get , s e t . . .
/∗
∗
∗
∗
∗
∗/
En e s t e método s e i n t r o d u c e n d e p e n d e n c i a s e n t r e e l d o m i n i o y
e l framework de p e r s i s t e n c i a . E x i s t e un f u e r t e a c o p l a m i e n t o
e n t r e e l modelo de d o m i n i o y e l esquema de b a s e de d a t o s y l a
d e m a r c a c i ó n de t r a n s a c c i o n e s .
public void
try {
}
}
i n s e r t ()
throws
ModelPersistException {
TransactionManager . s t a r t T r a n s a c t i o n () ;
M y S q l I n s e r t i n s e r t = new M y S q l I n s e r t ( "INSERT INTO SOCIALNET . USER
VALUES ( ? , ? , ? , ? ) " ) ;
P r o f i l e prof = getProfile () ;
i f ( p r o f == n u l l )
prof . i n s e r t () ;
...
i n s e r t . execute () ;
T r a n s a c t i o n M a n a g e r . commit ( ) ;
} catch ( P e r s i s t E x c e p t i o n e ) {
TransactionManager . r o l l b a c k () ;
throw new M o d e l P e r s i s t E x c e p t i o n ( "No f u e p o s i b l e i n s e r t a r e l u s u a r i o " ,
e) ;
}
2.2.
51
MODELOS DE DOMINIO
public c l a s s U s e r S e r v i c e L a y e r {
public void addUser ( UserDTO dto ) throws B u s i n e s s E x c e p t i o n {
try {
U s e r u = new U s e r ( dto . getName ( ) , dto . g e t E m a i l ( ) , new P r o f i l e ( dto .
}
}
g e t P r o f i l e I n f o () ) ) ;
user . i n s e r t () ;
} catch ( M o d e l P e r s i s t E x c e p t i o n mpe ) {
throw new B u s i n e s s E x c e p t i o n ( " E r r o r a l i n s e r t a r e l u s u a r i o " + u + " a
p a r t i r d e l DTO " + dto , mpe ) ;
}
En el caso anterior introdujimos responsabilidades sobre el modelo de dominio que lo acoplaron respecto
del esquema y motor de bases de datos así como la demarcación de transacciones. En el siguiente ejemplo
utilizamos un motor de persistencia que se responsabiliza de estos tres factores, dejando al modelo de
dominio desacoplado de ellos:
public c l a s s
User {
// a t r i b u t o s , get , s e t
public
}
}
U s e r ( S t r i n g name , S t r i n g e m a i l , P r o f i l e p r o f i l e ) {
setEmail ( email ) ;
setName ( name ) ;
setProfile ( profile ) ;
public c l a s s U s e r S e r v i c e L a y e r
public void addUser ( UserDTO
try {
}
}
{
dto )
throws
BusinessException {
PersistenceFrameworkSession pfs = PersistenceFrameworkFactory .
getSession () ;
U s e r u = new U s e r ( dto . getName ( ) , dto . g e t E m a i l ( ) , new P r o f i l e ( dto .
g e t P r o f i l e I n f o () ) ) ;
pfs . startTransaction () ;
pfs . save (u) ;
p f s . commit ( ) ;
} catch ( M o d e l P e r s i s t E x c e p t i o n mpe ) {
pfs . rollback () ;
throw new B u s i n e s s E x c e p t i o n ( " E r r o r a l i n s e r t a r e l u s u a r i o " + u + " a
p a r t i r d e l DTO " + dto , mpe ) ;
}
La preocupación acerca de la independencia del modelo suele surgir en el contexto de modelos que
interactúan con frameworks. En los de caja blanca debemos esperar una baja independencia del modelo,
ya que estamos obligados a subclasicar. Por el contrario, en los frameworks de caja negra contamos con
un encapsulamiento que nos garantiza un mayor grado de independencia.
2.2.3. Inversión del Control e Inyección de Dependencias
La inversión de control y la inyección de dependencias son dos aspectos que han ganado gran aceptación
ya que ayudan a desacoplar servicios, generalizar implementaciones y facilitar las pruebas unitarias.
52
CAPÍTULO 2.
ESTADO DEL ARTE
La inyección de dependencias nace en el contexto del consumo de servicios por parte de un cliente. Un
problema común en el desarrollo de aplicaciones enterprise es cómo atar (también llamado wiring ) los
componentes para que cooperen entre sí (Fowler, 2004). Veamos un ejemplo:
Ejemplo 2.2.3.
En este ejemplo se presenta el problema del wiring de componentes. El siguiente código
no utiliza inyección de dependencias:
1
2
3
public c l a s s I n d e x e r {
public i n d e x ( Document
P a r s e r p a r s e r = new
document ) {
TextParser () ;
I n d e x W r i t e r w r i t e r = new S q l I n d e x W r i t e r ( ) ;
T e x t N o r m a l i z e r n o r m a l i z e r = new S p a n i s h T e x t N o r m a l i z e r ( ) ;
4
5
6
7
9
10
11
12
13
14
L i s t <S t r i n g > t o k e n s = p a r s e r . p a r s e ( document . g e t T e x t ( ) ) ;
= document . getDocumentID ( ) ;
token : tokens ) {
S t r i n g normalizedToken = n o r m a l i z e r . normalize ( token ) ;
w r i t e r . w r i t e ( docId , n o r m a l i z e d T o k e n ) ;
}
long d o c I d
for ( S t r i n g
8
}
}
En las líneas 3,4 y 5 se instancian explícitamente clases que se ocupan de interpretar, normalizar y almacenar las palabras de un documento a indexar. En esta implementación, una vez compilado el programa,
no se permite variar la implementación de los servicios
Parser, IndexWriter
y
TextNormalizer.
Para resolver los problemas respecto de cómo desacoplar el consumidor de un servicio respecto del implementador, se utilizan técnicas de inyección de dependencias. Estas técnicas consisten en utilizar un
componente externo que se ocupa de inyectar el implementador del servicio en el consumidor.
Ejemplo 2.2.4.
En este ejemplo resolvemos el problema del ejemplo (2.2.3) utilizando buenas prácticas
de inyección de dependencias:
public c l a s s I n d e x e r {
private P a r s e r p a r s e r ;
private I n d e x W r i t e r w r i t e r ;
private T e x t N o r m a l i z e r n o r m a l i z e r ;
public void
public void
public void
setParser ( Parser parser ) { this . parser = parser ;}
setIndexWriter ( IndexWriter writer ) { this . writer = writer ;}
setTextNormalizer ( TextNormalizer normalizer ) { this . normalizer
= normalizer ;}
// g e t s p a r a l o s a t r i b u t o s . . .
public void
}
}
i n d e x ( Document document ) {
L i s t <S t r i n g > t o k e n s = p a r s e r . p a r s e ( document . g e t T e x t ( ) ) ;
long d o c I d = document . getDocumentID ( ) ;
for ( S t r i n g t o k e n : t o k e n s ) {
S t r i n g normalizedToken = n o r m a l i z e r . normalize ( token ) ;
w r i t e r . w r i t e ( docId , n o r m a l i z e d T o k e n ) ;
}
public c l a s s B u s i n e s s W o r k f l o w {
public s t a t i c void main ( S t r i n g [ ]
args ) {
W i r i n g F a c t o r y wf = WiringFramework . g e t F a c t o r y ( " i n j e c t i o n . xml " ) ;
Document doc = Document . r e a d ( "mydoc . t x t " ) ;
2.2.
}
}
53
MODELOS DE DOMINIO
I n d e x e r i d x = ( I n d e x e r ) wf . g e t I n s t a n c e ( " i n d e x e r " ) ;
i d x . i n d e x ( doc ) ;
Archivo
injection.xml:
< i n j e c t i o n>
<w i r i n g i d=" i n d e x e r " c l a s s="com . mydomain . s e r v i c e s . I n d e x e r ">
<p r o p e r t y − i n j e c t i o n p r o p e r t y=" p a r s e r " i m p l="com . mydomain . p a r s e r .
S p a n i s h P a r s e r "/>
<p r o p e r t y − i n j e c t i o n p r o p e r t y=" w r i t e r " i m p l="com . mydomain . i n d e x . w r i t e r .
S q l I n d e x W r i t e r "/>
<p r o p e r t y − i n j e c t i o n p r o p e r t y=" n o r m a l i z e r " i m p l="com . mydomain . i n d e x . t e x t
. S p a n i s h N o r m a l i z e r "/>
</ w i r i n g>
</ i n j e c t i o n>
En este ejemplo utilizamos una clase
injection.xml
WiringFactory
que se ocupa de leer el archivo de conguración
para hacer el wiring de los objetos. Con este esquema desacoplamos el servicio de quien lo
implementa, facilitando la elección dinámica de la implementación y el testeo unitario.
En el ejemplo (2.2.4) utilizamos la inyección de los servicios mediante atributos de la clase. Esto también
se puede llevar a cabo inyectando las dependencias en el constructor. La elección entre un método y otro
se basa en criterios acerca de si es correcto construir un objeto que no tiene resueltas sus dependencias
(de hecho no lo es, por lo que este criterio favorece el método de inyección en constructor) versus la
posibilidad de reinyectar las dependencias sobre un objeto construido (variando su comportamiento en
tiempo de ejecución). Esta discusión aparece en (Spring, 2008).
Además de la inyección de dependencias, otra práctica que utilizaremos en la construcción del motor de
búsqueda es la inversión de control (también conocido como inversion of control ó IoC ).
La inversión del control se basa en poner el locus de control en el framework que se está utilizando y dejar
que la lógica de aplicación sea llamada cuando es necesario. Esto se conoce como el hollywood principle:
Don't call us, we'll call you (Johnson y Foote, 1988). El IoC se complementa con la inyección de
dependencias. Por ejemplo, si tenemos un framework que implementa un motor de búsqueda, podemos
inyectar plugins en forma de dependencias, lo que permitirá extender la forma en la que el motor de
búsqueda procesa los documentos. Veamos un ejemplo:
Ejemplo 2.2.5.
A continuación se utiliza inyección de dependencias e inversión del control para auditar
qué documentos se indexan.
/∗ ∗
∗ E s t a c l a s e implementa l a i n v e r s i ó n de c o n t r o l l l a m a n d o a t o d o s l o s
manejadores
∗ r e g i s t r a d o s p a r a r e c i b i r l a n o t i f i c a c i ó n de un nuevo documento i n d e x a d o
∗/
public c l a s s
NewIndexEventHandler
implements
EventHandler {
// c o n s t r u c t o r , a t r i b u t o s y métodos p a r a m a n e j a r
// l o s e v e n t o s de un nuevo documento que s e i n d e x a . .
private
L i s t <D o c u m e n t I n d e x L i s t e n e r > l i s t e n e r s =
D o c u m e n t I n d e x L i s t e n e r >() ;
new
ArrayList <
// con e s t e método s e r e g i s t r a n q u i e n e s s e r á n l l a m a d o s a l o c u r r i r un
evento
public void r e g i s t e r N e w L i s t e n e r ( D o c u m e n t I n d e x L i s t e n e r n e w L i s t e n e r ) {
t h i s . l i s t e n e r s . add ( n e w L i s t e n e r ) ;
}
54
CAPÍTULO 2.
ESTADO DEL ARTE
// d e s p a c h a l o s e v e n t o s implementando l a i n v e r s i ó n de c o n t r o l
public void h a n d l e E v e n t ( ) {
Document d = getDocument ( ) ;
EventContext ctx = getIndexContext () ;
L i s t <D o c u m e n t I n d e x L i s t e n e r > l i s t e n e r s = g e t L i s t e n e r s ( ) ;
indexer . index (d) ;
for ( D o c u m e n t I n d e x L i s t e n e r
}
}
}
dil : listeners ) {
d i l . onDocumentIndex ( d , c t x ) ;
public c l a s s
}
Indexer {
// como en e l e j e m p l o p r e v i o , s ó l o que cuando i n d e x a un documento a v i s a
a l NewIndexEventHandler
// e s t a c l a s e hace l a a u d i t o r i a de cada nuevo documento que i n g r e s a a l
índice
public c l a s s F i l e A u d i t L o g g e r implements D o c u m e n t I n d e x L i s t e n e r {
public S t r i n g onDocumentIndex ( Document doc , E v e n t C o n t e x t c t x ) {
// e s c r i b i r a un a r c h i v o l o s d a t o s i m p o r t a n t e s
}
}
En este ejemplo el control lo tiene un hilo de ejecución que recibe el evento de indexación y sabe (por
conguración externa) que debe instanciar un
NewIndexEventHandler,
programado para orquestar el resto
de los pasos. En este ejemplo se ve claramente que es fácil crear tests unitarios ya que podemos utilizar
objetos mock (Mackinnon et al., 2001) que simulan ser el
NewIndexEventHandler
o el
FileAuditLogger.
Estos breves ejemplos muestran benecios de las técnicas de inyección de dependencias e inversión de
control. Dichos benecios harán que las utilicemos en la construcción del framework de búsqueda e
indexación de objetos.
2.3.
Persistencia de Modelos de Dominio
Information Retrieval y Persistencia
En sistemas de IR como los buscadores web, los documentos están persistidos de manera que son recuperables tanto por el proceso indexador del motor de búsqueda como por el browser que utiliza el
usuario.
Cuando los documentos son objetos de una aplicación el panorama no es distinto: necesitamos accederlos para indexarlos y visualizarlos. Si calculáramos puntajes estructurales como PageRank, también
necesitaríamos acceder a los objetos para calcularlo. Adicionalmente, la eliminación de objetos se debería
reejar eventualmente en los índices del motor de búsqueda para no recuperar objetos innecesarios y
degradar su rendimiento.
Por estas razones, para llevar a cabo las actividades del motor de búsqueda, necesitaremos interactuar
con el mecanismo de persistencia que utiliza la aplicación para sincronizar el índice con el estado de la
aplicación.
La interacción entre el sistema de persistencia y el motor de búsqueda puede ser:
Manual
(estilo librería)
:
el programador indica que se debe invocar al motor de búsqueda luego
de interactuar con el ORM.
2.3.
55
PERSISTENCIA DE MODELOS DE DOMINIO
Automática
:
(estilo framework)
13 generados
el motor de búsqueda intercepta los eventos CRUD
por la aplicación y delega en el usuario si es necesario.
El modelo manual es simple de implementar ya que sólo requiere que el motor de búsqueda exponga
una API genérica hacia las aplicaciones. Esa simplicidad redunda en una solución de bajo nivel, cuyos
costos asociados serán código duplicado y los errores propios de éste. El modelo automático requiere poder
suscribirse a los eventos CRUD manejados por la herramienta de persistencia, lo que requiere proveer
conectores especícos.
Organización de esta Sección
Las siguientes subsecciones explican brevemente los distintos mecanismos de persistencia del lenguaje
Java. En las subsecciones (2.3.2) y (2.3.3) se explican los dos estilos principales de persistencia: manual
vs administrada. En las subsecciones siguientes se muestran las tecnologías a las que haremos referencia
al presentar la propuesta en el capítulo 3.
Dada la gran adopción de las bases de datos relacionales en la en la industria del software, pondremos
especial atención sobre las herramientas que mapean objetos hacia/desde los RDBMS.
Como se explicó en el Plan de Tesis (sección 1.3), el software desarrollado está construido en Java, por
lo que los desarrollos tecnológicos se harán sobre este lenguaje de programación. Sin embargo, el análisis
sigue siendo válido en otros lenguajes orientados a objetos que disponen de mecanismos de persistencia
similares.
2.3.1. Persistencia y Ciclos de Vida en Aplicaciones Enterprise
En aplicaciones enterprise los objetos del dominio tienen ciclos de vida y ubicaciones que dependen de
patrones de diseño y arquitectura propios de este tipo de aplicaciones. En estos entornos es posible que
encontremos conviviendo un conjunto de procesos, los cuales no forman parte de una misma unidad de
compilación, sino que son independientes e interactúan mediante IPC (Inter-Process Communication),
RPC (Remote Procedure Call), colas de mensajes, bases de datos, web services o archivos. Además, es
frecuente que estos procesos y componentes provengan de proveedores diferentes que utilizan distintos
medios para el almacenamiento de la información. Aún siendo todos los módulos desarrollados con las
mismas tecnologías, es muy posible encontrarse con almacenamientos mixtos, por ejemplo: archivos y
bases de datos.
En este entorno dependemos de la capacidad de interrogar e interpretar fuentes de datos de distintas
naturalezas, lo que complejiza el acceso a la información. Este acceso a la información es importante a la
hora de diseñar los métodos de indexación, recuperación y visualización de los objetos del dominio (ver
sección 2.1).
Para resolver este problema es crucial conocer los mecanismos por los cuales los objetos se vuelven
persistentes.
2.3.2. Persistencia Manual
Cuando la persistencia se administra de forma manual, es responsabilidad del programador manejar el
ciclo de vida de los objetos así como las técnicas de almacenamiento. El esquema manual otorga la mayor
exibilidad posible a cambio de un esfuerzo mayor de programación.
En este esquema el programador utiliza las APIs provistas por el lenguaje para serializar/deserializar
los objetos, acceder al medio de almacenamiento, instanciar y destruir los objetos (en el caso de Java
hablamos de desreferenciar).
Para tener un motor de búsqueda sobre objetos, es necesario sincronizar el ciclo de vida de los objetos con
el motor de búsqueda. Dado que la persistencia manual puede implementarse de muchas maneras distintas,
en este esquema de trabajo los eventos CRUD son difíciles de interceptar por cualquier herramienta
externa.
13 CRUD:
siglas en inglés para crear, leer, actualizar y eliminar objetos (create, read, update y delete).
56
CAPÍTULO 2.
ESTADO DEL ARTE
Si bien es cierto que necesitamos conocer los eventos para sincronizar la aplicación y el motor de búsqueda,
en un esquema de persistencia manual se puede modicar la aplicación para dar aviso de los eventos CRUD
programaticamente o soportar cierto grado de divergencia entre la aplicación y el motor de búsqueda.
Como ejemplo de divergencia controlada entre el motor de búsqueda y la aplicación tomemos el caso
de los buscadores web, los cuales desconocen cuándo se genera, actualiza o elimina una página web.
Sin embargo, el webmaster puede indicar por dónde comenzar la indexación y generar meta datos que
indican cómo recorrer el sitio web que está indexando, así como es posible descubrir páginas y sitios web
por los enlaces presentes en el HTML. Para el caso de un grafo de objetos, también podemos iniciar el
recorrido por nodos bien conocidos o por un lugar indicado manualmente y luego navegar las relaciones
entre objetos de forma de indexar los que vamos descubriendo.
2.3.3. Persistencia Administrada
Dada la complejidad de desarrollo de un esquema de persistencia manual, existen soluciones que administran la persistencia de objetos de manera automática. Existen muchas advertencias acerca del costo de
mapear a mano un modelo de dominio a un RDBMS (una de ellas se puede encontrar en Fowler, 2002).
Muchas de estas herramientas se presentan como frameworks que persisten objetos en bases de datos relacionales. Algunos ejemplos son Hibernate Core, iBATIS, JDO y JPA (Hibernate, 2009a ; Apache, 2009c ;
JCP, 2006b ,a ). Eventualmente también hay mecanismos de persistencia sobre archivos como BerkeleyDB
(Oracle, 2009a ).
El objetivo de estas herramientas es asistir al programador en el manejo del ciclo de vida de los objetos,
facilitando el traslado entre la memoria principal y secundaria. Estos frameworks además colaboran en
tareas más complejas como las transacciones y la optimización de consultas.
Los frameworks de persistencia administrada introducen distintos grados de dependencia entre el modelo
de dominio y el framework, así como también permiten distintos grados de transparencia respecto del
esquema de base de datos. Los ORMs (mapeadores objeto-relacionales ú object-relational mappers) de
más bajo nivel sólo proveen una API para transformar consultas SQL en objetos y vice versa, mientras
que los ORM de más alto nivel nos abstraen completamente del mecanismo de persistencia (a cambio de
que especiquemos cómo persistir los objetos con una buena cantidad de metadatos).
En las próximas subsecciones analizamos distintos mecanismos de persistencia que pueden utilizarse tanto
en un esquema de persistencia manual como administrada.
2.3.4. Binaria
El lenguaje Java incluye entre sus bibliotecas estándar la capacidad de convertir objetos en una forma
serializada, el cual es tanto transmisible por un canal como almacenable en memoria secundaria.
La persistencia binaria en Java consiste en enviar los datos necesarios hacia un ujo de salida
14 para
luego reconstruir el objeto a partir de un ujo de entrada sobre ésos datos. Java almacena a la salida el
nombre y rma de la clase así como los campos no transientes ni estáticos de la misma (Sun, 2008).
Para poder persistir un objeto, es necesario que algún ancestro en la jerarquía de clases implemente la
interfaz java.io.Serializable.
Ejemplo 2.3.1.
1
2
3
4
Una adaptación del ejemplo de persistencia binaria en Java presente en (Sun, 2008):
F i l e O u t p u t S t r e a m f o s = new F i l e O u t p u t S t r e a m ( " payment − 1258302209. d a t " ) ;
O bj ec tO u tp ut S tr ea m oos = new O bj ec tO u tp ut S tr ea m ( f o s ) ;
oos . w r i t e O b j e c t ( new C r e d i t C a r d P a y m e n t ( i t e m I n f o r m a t i o n , c r e d i t C a r d ) ) ;
oos . c l o s e ( ) ;
La línea número 2 del ejemplo (2.3.1) genera un ujo de escritura de objetos sobre el ujo denido en la
línea número 1. La línea 3 genera un objeto que contiene información de pago de una tarjeta de crédito
(asumiendo que los parámetros se generan previamente) y persiste el objeto
CreditCardPayment en el disco.
El sistema de persistencia binario de Java permite congurar otros aspectos del proceso como:
14 Hablar
de ujos de salida es más general que hablar de archivos. Los ujos de salida pueden ser valores de retorno
de una llamada a procedimiento remoto, un canal TCP, un archivo o cualquier otro almacenamiento físico ó medio de
transmisión.
2.3.
57
PERSISTENCIA DE MODELOS DE DOMINIO
protocolos propios de serialización/deserialización
control de versiones
seguridad
La información detallada del sistema de persistencia binaria se encuentra en (Sun, 2004).
En conclusión, el sistema de serialización binaria permite serializar un grafo de objetos serializables a
través de un ujo. Si bien esto es muy útil, este sistema no nos da ninguna facilidad respecto de:
transacciones
recuperación eciente de datos (índices)
inspección y manipulación fuera de linea de los objetos
generación de reportes ad-hoc / cálculo de agregaciones sobre campos
integridad referencial
control de unicidad
En las próximas subsecciones analizaremos los sistemas más populares de persistencia, los cuales solucionan algunos de estos problemas.
2.3.5. Ad-Hoc
Es posible evitar problemas de la persistencia binaria (ver subsección 2.3.4) utilizando un método de
persistencia ad-hoc.
Si bien en el extremo podríamos decir que todos los sistemas de persistencia que no son binarios son adhoc, vamos a referirnos a la persistencia ad-hoc como el método de serialización utilizado para resolver
la persistencia de un conjunto de clases de una aplicación determinada.
Algunos ejemplos de persistencia ad-hoc:
conversión a texto delimitado por comas (CSV),
serialización a texto plano con cifrado AES,
en texto plano hacia archivos indexados.
La serialización ad hoc puede ser adecuada si queremos mantener compatibilidad con futuros cambios a
los objetos persistentes.
Otro caso donde conviene generar nuestro propio serializador es cuando tenemos que persistir gran cantidad de objetos y queremos optimizar el espacio utilizado. Con un serializador propio podemos aprovechar
nuestro conocimiento acerca de los datos para obtener estadísticamente mejoras en el espacio utilizado
(comprimiendo enteros, cadenas de texto, etc.).
En Java se puede utilizar un esquema híbrido entre persistencia binaria y ad-hoc implementando la
interfaz java.io.Externalizable. Al implementar esta interfaz podemos utilizar las clases ObjectOutputStream
y ObjectInputStream para leer y escribir de los ujos, pero seremos nosotros quienes programemos el
protocolo de serialización. A continuación vemos un ejemplo en el que se utiliza este sistema.
Ejemplo 2.3.2.
Serialización Ad-Hoc implementando
java.io.Externalizable.
imprime en la pantalla la misma información que se persistió en el archivo.
Al ejecutar este código se
58
CAPÍTULO 2.
public
class
CreditCardPayment
private
ItemInformation
private
CreditCard
Externalizable
implements
ESTADO DEL ARTE
{
itemInformation ;
creditCard ;
// C o n s t r u c t o r e s , get , s e t y t o S t r i n g . .
// C a l l b a c k p a r a l a s e r i a l i z a c i ó n
public
w r i t e E x t e r n a l ( ObjectOutput
void
out )
throws
IOException
{
out . w r i t e I n t ( i t e m I n f o r m a t i o n . g e t I t e m I d ( ) ) ;
String
cipherVersion
( int
for
i
= 0;
i <
= creditCard . cipher () ;
cipherVersion . length () ;
i ++)
out . writeChar (
c i p h e r V e r s i o n . charAt ( i ) ) ;
}
// C a l l b a c k p a r a l a d e s e r i a l i z a c i ó n
public
readExternal ( ObjectInput
void
ClassNotFoundException
this . itemInformation
this . creditCard
char [ ]
i
= 0;
i <
throws
IOException ,
ItemInformation ( in . readInt () ) ;
CreditCard () ;
c i p h e r C h a r s = new
( int
for
= new
= new
in )
{
c h a r [ C r e d i t C a r d . NUMBER_LENGTH ] ;
C r e d i t C a r d . NUMBER_LENGTH ;
i ++)
cipherChars [ i ] = in .
readChar () ;
t h i s . c r e d i t C a r d . s e t C i p h e r N u m b e r ( new
String ( cipherChars ) ) ;
}
// Ejemplo de uso
public
static
main ( S t r i n g [ ]
void
ClassNotFoundException
CreditCardPayment
(10) ,
new
args )
throws
IOException ,
{
original
= new
C r e d i t C a r d P a y m e n t ( new
ItemInformation
C r e d i t C a r d (321654987456L ) ) ;
FileOutputStream
f o s = new
ObjectOutputStream
F i l e O u t p u t S t r e a m ( " payment
o o s = new
−1258302209. dat " ) ;
ObjectOutputStream ( f o s ) ;
oos . w r i t e O b j e c t ( o r i g i n a l ) ;
oos . c l o s e ( ) ;
FileInputStream
fis
= new
F i l e I n p u t S t r e a m ( " payment
= new
−1258302209. dat " ) ;
ObjectInputStream
ois
ObjectInputStream ( f i s ) ;
CreditCardPayment
r e s t a u r a d o = ( CreditCardPayment ) o i s . readObject () ;
ois . close () ;
System . out . p r i n t l n ( r e s t a u r a d o ) ;
}
}
En el ejemplo (2.3.2) se persistió una versión cifrada del número de tarjeta de crédito para no transmitir la versión original en texto plano. Si no se desea implementar java.io.Externalizable, se puede hacer
persistencia ad-hoc mediante reection de Java.
Respecto del manejo de los eventos CRUD, la serialización ad-hoc es muy similar a la binaria ya que no
hay un mecanismo estándar de noticación de estos eventos. La noticación de estos eventos queda en
manos del programador.
2.3.
59
PERSISTENCIA DE MODELOS DE DOMINIO
2.3.6. XML
La serialización XML es un tipo de serialización ad-hoc que tiene algunas ventajas:
interoperabilidad con otros lenguajes
legible y editable por un humano
herramientas que soportan lectura y escritura de XML, DTD y Schema
Estas ventajas tienen el costo de no tener la misma exibilidad de la serialización ad-hoc.
Una herramienta que se ocupa de la serialización XML es XStream (XStream, 2009). A continuación
vemos una adaptación de un ejemplo presente en la documentación de esta herramienta:
public
class
CreditCardPayment
private
ItemInformation
private
CreditCard
{
itemInformation ;
creditCard ;
// C o n s t r u c t o r y métodos . . .
}
public
static
XStream
main ( S t r i n g
void
x s t r e a m . a l i a s ( " payment " ,
xstream . a l i a s ( " item " ,
xstream . a l i a s ( " c r e d i t
CreditCardPayment
(10) ,
String
args [ ] )
x s t r e a m = new X S t r e a m ( new
new
throws
Exception
{
DomDriver ( ) ) ;
CreditCardPayment . c l a s s ) ;
ItemInformation . class ) ;
−c a r d " ,
original
CreditCard . class ) ;
= new
C r e d i t C a r d P a y m e n t ( new
ItemInformation
C r e d i t C a r d (321654987456L ) ) ;
x m l = x s t r e a m . toXML ( o r i g i n a l ) ;
}
Esto produce el XML:
<p a y m e n t>
<i t e m>
< i t e m I d>1 0</ i t e m I d>
</ i t e m>
<c r e d i t
− c a r d>
<n u m b e r>3 2 1 6 5 4 9 8 7 4 5 6</ n u m b e r>
</ c r e d i t
− c a r d>
</ p a y m e n t>
La deserialización es también muy simple:
CreditCardPayment
r e s t a u r a d o = ( C r e d i t C a r d P a y m e n t ) x s t r e a m . fromXML ( x m l ) ;
Es preciso notar que para el caso de XStream, el framework toma responsabilidades avanzadas como el
mapeo en jerarquías de herencia y agregaciones (XStream, 2009).
En muchos casos la serialización XML puede ser una mejor alternativa que los métodos ad-hoc por las
ventajas que hemos comentado, sin embargo, existen algunos aspectos a resolver: transacciones, control
de unicidad e integridad referencial, acceso eciente y generación de reportes. Este tipo de problemas
pueden abordarse mejor con el respaldo de una base de datos, por lo que en las próximas secciones vamos
a abordar las técnicas que utilizan RDBMS como parte de la persistencia de objetos.
2.3.7. Object Relational Mapper (ORM)
Los RDBMS han sido la base para miles de sistemas por más de 30 años. Asimismo, la gran masa de
esfuerzo en desarrollo de software se orienta a la programación orientada a objetos.
60
CAPÍTULO 2.
ESTADO DEL ARTE
Basándose en estos dos hechos, la industria del software ha buscado combinar tanto la programación orientada a objetos como los RDBMS. Sin embargo, a pesar del éxito de estos dos modelos existe un problema
15 . Algunos de los ítems que componen este
entre ellos llamado desajuste de impedancia (Fowler, 2002)
desajuste son:
Tipos de Datos: no existe un mapeo 1:1 entre los tipos de datos primitivos de los lenguajes de
programación y los de las bases de datos. Algunos ejemplos en Java y Oracle: String vs. Varchar e
Integer vs. Number.
Herencia y Polimorsmo: son fáciles de implementar en un lenguaje orientado a objetos, mientras
que en un RDBMS requieren elegir cuidadosamente la estrategia de implementación.
Recuperación de Datos y Junta: el mapeo entre relaciones y clases debe generar resultsets compatibles con las clases, ya que el esquema del resultset se genera dinámicamente dependiendo las
columnas que especique en la consulta. La navegación entre asociaciones en un mundo de objetos
consiste en iterar a través de colecciones, mientras que en una base de datos puede requerir una
junta, la cual no se corresponde con una clase real del modelo de objetos o bien ejecutar consultas
adicionales para obtener los objetos agregados.
Orden de Creación: en un esquema de base de datos podemos tener dos tablas cuyas relaciones se
referencian entre sí, lo cual puede ser un problema al momento de la creación de objetos, ya que un
objeto depende del otro para su creación, dando lugar a la pregunta de quién debe crearse primero.
Esquema Duplicado: estamos obligados a conservar una estructura de datos en el modelo de dominio
y otra en las tablas de la base de datos. En las consideraciones de diseño no debe perderse el balance
entre los dos esquemas y se debe tener en mente técnicas para que la navegación de objetos no genere
consultas excesivas en el RDBMS.
Identidad vs. Equivalencia: en los lenguajes de programación como Java es factible que a.equals(b)
sea cierto pero que a==b no lo sea, mientras que en una relación no pueden existir dos tuplas
diferentes con la misma clave.
Reglas de Acceso: el modelo de objetos tiene atributos públicos y privados, mientras que los RDBMS
tienen permisos.
Si bien es posible plantear otros desajustes entre los dos modelos, los expuestos dan un panorama del
problema de conciliar dos modelos exitosos por separado pero diciles de reunir.
Dado el esfuerzo que requiere persistir objetos en bases de datos relacionales, se ha desarrollado una amplia
gama de herramientas que asisten al programador en esta tarea. Estas herramientas son los ORM. Los
ORM varían desde wrappers de bajo nivel de las API del lenguaje hasta herramientas de alto nivel que
permiten abstraernos del esquema y el SQL.
A continuación presentamos pequeños casos de estudio de ORM básicos, donde el programador tiene
un rol activo en resolver los desajustes de impedancia y ORM Completos, donde el framework asiste al
programador en resolver estos desajustes.
ORM Básicos
A continuación vemos dos ejemplos de ORM básicos donde debemos resolver la mayoría de los problemas
de impedancia: JDBC y Apache iBATIS.
JDBC (Java DataBase Connectivity)
JDBC es la API de Java para estandarizar el diálogo con los
distintos RDBMS. Esta API establece las primitivas de manejo de conexiones, transacciones y ejecución
de consultas sobre el RDBMS.
En los casos en los que se quiere hacer el esfuerzo de codicar una capa propia de persistencia, normalmente
se construyen las clases del framework conectándolas con JDBC para la ejecución de consultas. En rigor
15 Si
bien Fowler efectivamente utiliza el término impedance mismatch en su obra, pueden encontrarse otras notas,
artículos o libros donde se reere al tema con la misma frase, por lo que no podemos asegurar cuál sería la cita más
adecuada.
2.3.
61
PERSISTENCIA DE MODELOS DE DOMINIO
JDBC no es un ORM en sí mismo sino que es la herramienta que éstos utilizan para dialogar con el
RDBMS.
Al utilizar JDBC se obtiene una gran exibilidad, pero queda en manos del desarrollador resolver todos
los problemas de impedancia.
Los eventos CRUD no se notican por ningún medio estándar ya que su implementación es totalmente
ad-hoc.
Ejemplo 2.3.3.
A continuación vemos un caso en el que hacemos un mapeo básico entre la base de datos
y un objeto utilizando JDBC.
public s t a t i c void
{
main ( S t r i n g a r g s [ ] )
throws
SQLException
L o c a l e . s e t D e f a u l t ( L o c a l e . ENGLISH ) ;
D r i v e r M a n a g e r . r e g i s t e r D r i v e r ( new o r a c l e . j d b c . d r i v e r . O r a c l e D r i v e r ( ) ) ;
C o n n e c t i o n conn=D r i v e r M a n a g e r . g e t C o n n e c t i o n ( " j d b c : o r a c l e : t h i n : @ l o c a l h o s t
: 1 5 2 1 : XE" , " u s u a r i o " , " p a s s w o r d " ) ;
S t a t e m e n t stmt = conn . c r e a t e S t a t e m e n t ( ) ;
}
int u s e r I d = 1 0 ;
R e s u l t S e t r s e t = stmt . e x e c u t e Q u e r y ( "SELECT f i r s t _ n a m e , last_name , l o w e r (
nickname ) FROM u s e r WHERE i d="+u s e r I d ) ;
while ( r s e t . n e x t ( ) ) {
System . out . p r i n t l n ( new com . mydomain . U s e r ( u s e r I d , r s e t . g e t S t r i n g ( 1 ) , r s e t .
getString (2) , r s e t . getString (3) ) ;
}
stmt . c l o s e ( ) ;
conn . c l o s e ( ) ;
Como se ve en este fragmento de código, el usuario se ocupa de la conexión y recuperación de datos desde
la base, mapeando el esquema de relación a los objetos del dominio en forma manual.
Apache iBATIS
Existen herramientas populares que facilitan las tareas rutinarias de persistencia con
JDBC sin obligarnos a ceder el control total de la persistencia. Una herramienta muy popular en este
sentido es Apache iBATIS (ver Apache, 2009c ).
En iBATIS 3 podemos denir las operaciones que queremos realizar sobre la base de datos mediante
interfaces. Estas interfaces reciben y devuelven objetos de dominio o en su defecto objetos de transferencia
de datos (data transfer objects ó DTO ). Las reglas acerca de cómo insertar, actualizar, eliminar o recuperar
objetos desde el RDBMS las debemos congurar explícitamente mediante archivos de conguración, los
cuales contienen el SQL necesario. A continuación vemos un ejemplo.
Ejemplo 2.3.4.
En este ejemplo vemos cómo manejar la persistencia de un objeto
cualquiera.
Clase del dominio:
public c l a s s
Person {
private long i d ;
private S t r i n g f i r s t N a m e ;
private S t r i n g lastName ;
private S t r i n g e m a i l ;
private L i s t <Person> c o n t a c t s ;
private Company company ;
Person de un dominio
62
CAPÍTULO 2.
public
}
ESTADO DEL ARTE
Person () { }
// get , s e t , . . .
Interfaz de mapeo:
public interface PersonMapper {
public P e r s o n s e l e c t P e r s o n ( int i d ) ;
public L i s t <Person> selectByCompany ( long
public void i n s e r t P e r s o n ( I n s e r t P e r s o n D t o
}
Archivo
Person.xml
id ) ;
insertDto ) ;
con el SQL de mapeo:
<? xml version =" 1 . 0 " e n c o d i n g="UTF−8" ?>
<!DOCTYPE mapper PUBLIC " −// i b a t i s . apache . o r g //DTD Mapper 3 . 0 / /EN" "
h t t p : // i b a t i s . apache . o r g / dtd / i b a t i s −3−mapper . dtd ">
<mapper namespace="com . mydomain . PersonMapper ">
<r e s u l t M a p i d=" p e r s o n R e s u l t M a p " t y p e=" P e r s o n ">
<c o n s t r u c t o r>
<i d A r g column=" i d " j a v a T y p e=" l o n g " />
<a r g column=" f i r s t _ n a m e " j a v a T y p e=" S t r i n g " />
<a r g column=" last_name " j a v a T y p e=" S t r i n g " />
<a r g column=" e m a i l " j a v a T y p e=" S t r i n g " />
</ c o n s t r u c t o r>
<i d p r o p e r t y=" i d " column=" i d " />
< r e s u l t p r o p e r t y=" f i r s t N a m e " column=" f i r s t _ n a m e " />
< r e s u l t p r o p e r t y=" lastName " column=" last_name " />
< r e s u l t p r o p e r t y=" e m a i l " column=" e m a i l " />
< a s s o c i a t i o n p r o p e r t y=" company " column=" company_id "
j a v a T y p e="Company" s e l e c t="com . mydomain . CompanyMapper .
s e l e c t C o m p a n y " />
< c o l l e c t i o n p r o p e r t y=" c o n t a c t s " j a v a T y p e=" A r r a y L i s t "
column=" i d " ofType=" P e r s o n " s e l e c t=" s e l e c t C o n t a c t s " />
</ r e s u l t M a p>
< s e l e c t i d=" s e l e c t P e r s o n " parameterType=" i n t " r e s u l t T y p e=" P e r s o n "
r e s u l t M a p=" p e r s o n R e s u l t M a p ">
select
i d , f i r s t _ n a m e , last_name , e m a i l , company_id
from
MYAPPSCHEMA. P e r s o n where i d = #{i d }
</ s e l e c t>
< s e l e c t i d=" s e l e c t C o n t a c t s " parameterType=" i n t " r e s u l t T y p e=" P e r s o n "
r e s u l t M a p=" p e r s o n R e s u l t M a p ">
select
p . i d , p . f i r s t _ n a m e , p . last_name , p . e m a i l
from
MYAPPSCHEMA. C o n t a c t c , MYAPPSCHEMA. P e r s o n p
where
c . from_id = #{i d } and
c . to_id = p . i d
</ s e l e c t>
< s e l e c t i d=" selectByCompany " parameterType=" i n t " r e s u l t T y p e=" P e r s o n "
r e s u l t M a p=" p e r s o n R e s u l t M a p ">
2.3.
PERSISTENCIA DE MODELOS DE DOMINIO
63
select
i d , f i r s t _ n a m e , last_name , e m a i l
from
MYAPPSCHEMA. P e r s o n where company_id = #{i d }
</ s e l e c t>
< i n s e r t i d=" i n s e r t P e r s o n " parameterType="com . mydomain . dto .
I n s e r t P e r s o n D t o ">
<s e l e c t K e y o r d e r="BEFORE" r e s u l t T y p e=" j a v a . l a n g . Long " k e y P r o p e r t y="
person . id " >
SELECT NEXT VALUE FOR MYAPPSCHEMA. p e r s o n _ i d FROM DUAL AS ID
</ s e l e c t K e y>
INSERT INTO MYAPPSCHEMA. PERSON
( ID , FIRST_NAME, LAST_NAME, PASSWORD, EMAIL , COMPANY_ID )
values
( #{p e r s o n . i d , jdbcType=BIGINT } ,
#{p e r s o n . f i r s t N a m e , jdbcType=VARCHAR} ,
#{p e r s o n . lastName , jdbcType=VARCHAR} ,
#{password , jdbcType=VARCHAR} ,
#{p e r s o n . e m a i l , jdbcType=VARCHAR} ,
#{p e r s o n . company . i d , jdbcType=BIGINT} )
</ i n s e r t>
</ mapper>
Finalmente vemos caso de uso de ejemplo
RegisterPerson:
public c l a s s R e g i s t e r P e r s o n {
private s t a t i c void i n s e r t P e r s o n ( Company
company , S t r i n g f i r s t N a m e ,
S t r i n g lastName , S t r i n g e m a i l , S t r i n g p a s s w o r d ) throws
BusinessException {
S t r i n g r e s o u r c e = "com/mydomain/ p e r s i s t e n c e / C o n f i g u r a t i o n . xml " ;
Reader r e a d e r = R e s o u r c e s . g e t R e s o u r c e A s R e a d e r ( r e s o u r c e ) ;
S q l S e s s i o n F a c t o r y s q l M a p p e r = new S q l S e s s i o n F a c t o r y B u i l d e r ( ) . b u i l d (
reader ) ;
S q l S e s s i o n s e s s i o n = sqlMapper . openSession () ;
{
PersonMapper personMapper = s e s s i o n . getMapper ( PersonMapper . c l a s s )
;
I n s e r t P e r s o n D t o dto = new I n s e r t P e r s o n D t o ( ) ;
try
P e r s o n p = new P e r s o n ( f i r s t N a m e , lastName , e m a i l ) ;
p . setCompany ( company ) ;
dto . s e t P e r s o n ( p ) ;
dto . s e t P a s s w o r d ( p a s s w o r d ) ;
personMapper . i n s e r t P e r s o n ( dto ) ;
}
}
}
}
s e s s i o n . commit ( ) ;
finally {
session . close () ;
En este ejemplo vimos los actores que intervienen en la persistencia con iBATIS. Para nalizar el ejemplo
vamos a enumerarlos y describir sus responsabilidades:
64
CAPÍTULO 2.
ESTADO DEL ARTE
Objetos de dominio (Person): implementan la lógica de negocio, no tienen dependencias explícitas
hacia el ORM.
Interfaz de acceso a datos (PersonMapper): dene las operaciones de persistencia sobre el objeto a
mapear. Permite proveer implementaciones alternativas al ORM.
Archivo de conguración del mapeo (Person.xml): dene la lógica de mapeo entre el esquema de base
de datos y el modelo.
Objeto de negocio (RegisterPerson): implementa la secuencialidad del caso de uso.
iBATIS evita que escribamos y dupliquemos buenas cantidades de código ad-hoc para la transformación
de datos entre el esquema de base de datos y el modelo de clases. Además ayuda a resolver desajustes de
impedancia de conversión de tipos de datos, herencia y polimorsmo.
Entre las ventajas de utilizar este ORM está la provisión de caches de objetos que evitan consultas
innecesarias a la base de datos, lo cual es conveniente para hidratar objetos recuperados.
ORM Completos
Los ORM completos buscan resolver integralmente los desajustes de impedancia entre modelos. Estos
ORM son herramientas administradas de mayor complejidad que los ORM básicos.
El primer caso de estudio es el estándar JPA (Java Persistence API, JCP, 2006a ). Este es el estándar
de Java para persistencia objeto-relacional. El segundo caso de estudio es JDO, un estándar más antiguo
que JPA pero que tiene varias implementaciones y usuarios.
El tercer caso de estudio es una herramienta de persistencia no estandarizada por un JSR
16 llamada
Hibernate (ver Hibernate, 2009a ). Este ejemplo es especialmente signicativo ya que el mismo grupo de
desarrollo provee un motor de búsqueda para el ORM llamado Hibernate Search (ver Hibernate, 2009b ),
el cual tomaremos como caso de estudio en la sección (2.4.2).
JPA (Java Persistence API)
Es un estándar de persistencia objeto-relacional introducido en (JCP,
2006a ). La intención de este estándar es simplicar el desarrollo de aplicaciones que requieren persistencia
objeto-relacional y unicando a los usuarios detrás de una sola API.
JPA impone algunos requerimientos sobre el modelo de dominio:
debemos anotar las clases con el annotation
@Entity
la clase debe tener al menos un constructor público o privado sin argumentos
ni la clase, ni sus métodos o variables persistentes de instancia pueden ser declarados
para ciertos usos, la clase debe implementar la interfaz
final
java.io.Serializable
los clientes deben acceder al estado a través de métodos de acceso (get/set) ó métodos de negocio
(calcularSaldo, etc).
A cambio de estas restricciones, JPA es capaz de mapear jerarquías de clases hacia tablas, generar claves
primarias y mapear tipos de datos entre Java y el RDBMS.
Además del SQL estándar que siempre podemos ejecutar sobre el RDBMS, JPA permite consultas sobre
objetos en un lenguaje similar a SQL (JPA-QL), lo que facilita la recuperación y mapeo de objetos.
La implementación de referencia de JPA es Oracle TopLink (Oracle, 2008). Además de esta implementación de referencia, existen otras como OpenJPA y Hibernate (a través de módulos adicionales).
Nuestro interés principal en JPA es mostrar su importancia en el ecosistema de persistencia y contarlo
como posible actor en la relación de las aplicaciones con nuestro motor de búsqueda.
16 Java
Specication Requests (JSR): son descripciones técnicas propuestas en el marco del Java Community Process
(JCP). Funcionan como propuestas de estándares hasta eventualmente incorporarse a la especicación Java.
2.4.
65
CASOS DE ESTUDIO
JDO (Java Data Objects)
La especicación de este sistema de persistencia para objetos conocido
como se publicó inicialmente en el JSR-12 (2002), previamente a JPA. Más tarde, el JSR-243 (2006)
especica la versión 2 de JDO, la cual cuenta con las siguientes características:
soporte para múltiples data stores (puede almacenar objetos fuera de un RDBMS),
no requiere modicar los objetos de dominio (Plain Old Java Objects ó POJO ),
generación automática del esquema,
auto generación de claves primarias,
no requiere escribir código relacionado a JDBC.
Técnicamente, dado que JDO persiste objetos hacia otros medios además de un RDBMS, no es un ORM
sino que es una herramienta de persistencia transparente. A pesar de esto tomamos el caso de particular
en que el data store es un RDBMS y lo tratamos como un ORM.
En JDO el proceso de compilación requiere un tratamiento de post-compilación llamado enhancer (potenciador). El enhancer le permite al framework reconocer los cambios de estado en un objeto para manejar
su persistencia.
JDO dispone de múltiples implementaciones como JPOX y DataNucleus así como una implementación
de referencia (ver JCP, 2006b ).
Así como en JPA, nuestro interés en JDO pasa por tenerlo en cuenta como un posible actor con el cual
interactuar en la construcción del motor de búsqueda.
Hibernate
El módulo base de Hibernate es el framework de persistencia no estándar más difundido
entre los ORM. Como ya hemos comentado anteriormente, Hibernate es una suite de productos construidos alrededor de Hibernate Core (módulo autónomo base del ORM) e implementa el estándar JPA
mediante módulos opcionales.
Hibernate es muy similar a JPA y JDO en el sentido de que impone algunos requerimientos básicos sobre
el modelo de dominio (constructor sin argumentos) y se ocupa de transparentar la persistencia de objetos
al RDBMS.
Este ORM tiene la particularidad de que avanzó en la solución del problema de IR sobre objetos mediante
un módulo adicional llamado Hibernate Search (Hibernate, 2009b ). En la subsección (2.4.2) ampliaremos
acerca de éste.
2.3.8. Bases de Datos Orientadas a Objetos
Tras muchos años de existencia, las Bases de Datos Orientadas a Objetos (Object Oriented DataBase
Management System ó OODBMS ) han atravesado distintos esfuerzos de estandarización sin lograr una
adopción masiva.
Si bien en ciertos campos del conocimiento pueden ser una muy buena solución (se suele citar el CAD/CAM como campo de aplicación), tienen bajo nivel de adopción en el ambiente de aplicaciones empresariales (donde planteamos mayormente el campo de aplicación del motor de búsqueda).
Por estas razones excluimos los OODBMS como caso de interacción con el motor de búsqueda, siendo
que las aplicaciones que efectivamente utilicen un OODBMS deberán comunicarse mediante una API
genérica (tal como si utilizaran persistencia manual).
2.4.
Casos de Estudio
En esta sección estudiamos tres herramientas que representan el estado del arte en materia de IR para
uso en desarrollo de software. Éstas herramientas son de especial interés a la hora de realizar un análisis
comparativo y extraer las mejores prácticas para generar la herramienta de IR que construimos.
66
CAPÍTULO 2.
ESTADO DEL ARTE
La primera herramienta es Apache Lucene (Apache, 2009b ). Lucene está orientado a la indexación y
recuperación de documentos de texto generales. La segunda y tercera herramienta son respectivamente
Hibernate Search y Compass Project. Estas dos herramientas son sosticados envoltorios de Lucene (lo
utilizan como motor de indexación y recuperación) que intentan resolver los problemas de utilizarlo
directamente como indexador de objetos (ver subsección 1.1.3).
Para la categorización de sistemas de IR que hemos hecho en la subsección (2.1.1), dentro de la categoría
de IR para desarrollo de software, Lucene entra en la sub categoría de Text Retrieval mientras que
Hibernate Search y Compass pertenecen a la sub categoría de Object Search.
2.4.1. Apache Lucene
Apache Lucene es un sistema maduro de indexación y recuperación de textos de código abierto para
Java (existen traducciones a otros lenguajes como C, C++, Perl, Python, etc). Este sistema se presenta
al programador por medio de una API extensible y permite efectuar la indexación y recuperación de
documentos en cualquier aplicación.
En los próximos apartados analizamos los conceptos básicos de Lucene y vemos ejemplos de su utilización.
Documentos y Campos
En Lucene el diseñador debe denir las entidades a indexar mediante la denición de un objeto de clase
Document. Los documentos aplican los conceptos de campos y zonas (ver subsección 2.1.6) mediante
objetos de tipo Field, los cuales se implementan como pares clave/valor sin un tipo de datos especíco
(en rigor se implementan como Strings). Lucene diferencia un campo y una zona mediante parámetros
que permiten especicar si el Field se debe almacenar y/o indexar. A continuación vemos un ejemplo de
esta estructura.
Ejemplo 2.4.1.
Denición de un documento que representa una página web.
Document doc = new Document ( ) ;
doc . add ( new F i e l d ( "URL" , " h t t p : / /www. f i . uba . a r " , S t o r e . YES , I n d e x .NOT_ANALYZED
));
doc . add ( new F i e l d ( "TITLE" , " F a c u l t a d de I n g e n i e r í a − U n i v e r s i d a d de Buenos
A i r e s " , S t o r e . YES , I n d e x . ANALYZED) ) ;
doc . add ( new F i e l d ( "BODY" , "<body><h1>B i e n v e n i d o a . . . " , S t o r e . YES , I n d e x .
ANALYZED) ) ;
Antes de almacenar el texto en el índice debemos especicar si queremos preprocesarlo o efectuar un
almacenamiento literal. Los campos que elegimos preprocesar son tomados por objetos Analyzer que efectúan tareas como: fragmentación del texto, conversión a minúsculas y eliminación de stopwords. En el
ejemplo (2.4.1) utilizamos un campo para almacenar la URL de la página web y elegimos no preprocesarlo
porque estamos interesados en usarlo sólo para búsquedas literales (de hecho como comentamos anteriormente, podríamos almacenarlo sin indexarlo, lo que haría que no haya coincidencia aunque se lo busque
literalmente). Por el contrario, para los campos TITLE y BODY sí queremos un preprocesamiento que
facilite la coincidencia con el documento, por lo que los marcamos con el parámetro Index.ANALYZED.
Como comentamos en al párrafo anterior, además de procesar los campos podemos almacenarlos en forma
literal. En el ejemplo (2.4.1) indicamos que íbamos a almacenar todos los campos al crearlos utilizando
el parámetro Store.YES. Si almacenamos los datos luego podremos recuperar la versión literal del texto
indexado (sin preprocesamientos). El almacenamiento es útil para construir un vínculo entre el pseudoesquema construido en los campos de Lucene y el esquema del documento original.
Índices, Lectores y Escritores
La indexación y la recuperación de documentos se da a través de lectores y escritores del índice (IndexSearcher
e IndexWriter). Los lectores y escritores son partes fundamentales de la API de Lucene, siendo que una
implementación básica no necesita conocer mucho que estas interfaces.
2.4.
CASOS DE ESTUDIO
67
Consultas y Analizadores
Como comentamos en apartados previos, Lucene trata las búsquedas y documentos a indexar mediante
extensiones de la clase abstracta Analyzer. El objetivo de los analizadores es almacenar los términos en
el índice utilizando las técnicas de matching y relevancia que vimos en la subsección (2.1.5).
Lucene cuenta con un lenguaje propio para especicar las búsquedas, el cual se interpreta para construir un
objeto de tipo Query. Este lenguaje permite, entre otros, denir conjunciones y disyunciones de términos,
impulsar campos (boosting), búsquedas por proximidad y edit distance. Para generar las consultas es
posible utilizar un QueryParser, el cual interpreta la búsqueda, la analiza (utilizando el analizador denido)
y construye el objeto de tipo Query.
El boosting al que referimos en al párrafo anterior es la implementación de Lucene de los conceptos de
campos y zonas presentes en la subsección (2.1.6). Cuando revisemos la fórmula de relevancia utilizada
por Lucene veremos cómo se implementa el boosting.
Directorios
En Lucene existe la posibilidad de almacenar el índice en memoria RAM, lesystems, bases de datos
y otros. Esta capacidad de variar la implementación de la capa de acceso a datos se da a través de
descendientes de la clase abstracta Directory. Esta capa de acceso a datos es extensible, permitiendo
diseñar implementaciones particulares que permitirían funcionalidades no estándar como cifrar los datos.
A continuación vemos un ejemplo sintético de indexación y recuperación en Lucene que integra los
conceptos que hemos expuesto en los apartados anteriores.
Ejemplo 2.4.2.
En este ejemplo generamos un método de indexación y uno de recuperación. Para la
indexación creamos un documento que describe personas, siendo que utilizamos un campo para su tesis y
otro campo para otros trabajos.
p r i v a t e s t a t i c void index ( ) throws IOException
{
D i r e c t o r y d i r = N I O F S D i r e c t o r y . g e t D i r e c t o r y ( new F i l e ( "/ l u c e n e / l u c e n e t e s t / " ) ) ;
I n d e x W r i t e r w r i t e r = new I n d e x W r i t e r ( d i r , new S t a n d a r d A n a l y z e r ( ) , M a x F i e l d L e n g t h .
LIMITED ) ;
}
Document doc = new Document ( ) ;
doc . add ( new F i e l d ( "AUTHOR" , " J u l i á n K l a s " , S t o r e . YES , I n d e x . ANALYZED) ) ;
doc . add ( new F i e l d ( " TESIS " , " R e c u p e r a c i ó n de I n f o r m a c i ó n s o b r e Modelos de Dominio "
, S t o r e . YES , I n d e x . ANALYZED) ) ;
doc . add ( new F i e l d ( "OTHER_WORKS" , " 38 JAIIO − R e c u p e r a c i ó n de I n f o r m a c i ó n s o b r e
Modelos de Dominio " , S t o r e . YES , I n d e x . ANALYZED) ) ;
w r i t e r . addDocument ( doc ) ;
writer . optimize () ;
writer . close () ;
p r i v a t e s t a t i c void s e a r c h ( ) throws IOException , ParseException {
D i r e c t o r y d i r = N I O F S D i r e c t o r y . g e t D i r e c t o r y ( new F i l e ( "/ l u c e n e / l u c e n e t e s t / " ) ) ;
I n d e x S e a r c h e r s e a r c h e r = new I n d e x S e a r c h e r ( d i r ) ;
Query q u e r y = new Q u e r y P a r s e r ( "OTHER_WORKS" , new S t a n d a r d A n a l y z e r ( ) ) . p a r s e ( " j a i i o
") ;
TopDocs r s = s e a r c h e r . s e a r c h ( q u e r y , n u l l , 1 0 ) ;
System . o u t . p r i n t l n ( " E n c o n t r a d o ( s ) "+r s . t o t a l H i t s+" r e s u l t a d o ( s ) " ) ;
f o r ( i n t i = 0 ; i < r s . t o t a l H i t s ; i ++) {
Document h i t = s e a r c h e r . doc ( r s . s c o r e D o c s [ i ] . doc ) ;
System . o u t . p r i n t l n ( " A u t o r : "+h i t . g e t F i e l d ( "AUTHOR" ) . s t r i n g V a l u e ( ) ) ;
System . o u t . p r i n t l n ( " T e s i s : "+h i t . g e t F i e l d ( " TESIS " ) . s t r i n g V a l u e ( ) ) ;
System . o u t . p r i n t l n ( " O t r o s : "+h i t . g e t F i e l d ( "OTHER_WORKS" ) . s t r i n g V a l u e ( ) ) ;
}
68
CAPÍTULO 2.
ESTADO DEL ARTE
}
p u b l i c s t a t i c v o i d main ( S t r i n g [ ] a r g s ) t h r o w s I O E x c e p t i o n , P a r s e E x c e p t i o n {
index () ;
search () ;
}
Este ejemplo produce el resultado:
Encontrado ( s ) 1 r e s u l t a d o ( s ) :
Autor : J u l i á n Klas
T e s i s : R e c u p e r a c i ó n de I n f o r m a c i ó n s o b r e Modelos de Dominio
O t r o s : 38 JAIIO − R e c u p e r a c i ó n de I n f o r m a c i ó n s o b r e Modelos de Dominio
Modelos de IR de Lucene
El modelo de IR (ver subsección 2.1.4) utilizado por Lucene es una combinación del modelo vectorial y
el booleano (Apache, 2009b ). En Lucene esta combinación consiste en utilizar el modelo booleano para
denir qué documentos se recuperarán y luego priorizarlos con el modelo vectorial. Tal como comentamos
al momento de introducir el modelo booleano, Lucene amplia los operadores AND, OR y NOT permitiendo
búsquedas por wildcards, edit distance, frases y rangos (ver subsección 2.1.5).
Dentro del modelo vectorial, los pesos de relevancia aplicados por Lucene son variantes de la familia
TF-IDF. Como veremos a continuación, la aplicación de TF-IDF tiene en cuenta la presencia de campos.
La fórmula de similitud de Apache Lucene
17 es (Apache, 2009b ):
Similitud (q, dj ) = coord (q, dj ) × queryN orm (q) ×
X
2
tfL (t, dj ) × idfL (t) × norm (dj ) × boost (t)
∀t∈q
(2.4.1)
Donde
t
es un termino de la query
q
y
dj
es el documento que se está evaluando. En el caso de Lucene
las funciones tf e idf que vimos en la subsección (2.1.4) están recalculadas de la siguiente manera:
tfL (t, dj ) =
q
tf (t, dj )
idfL (t) = 1 + log
N
df (t) + 1
Los demás términos tienen estos signicados:
coord (q, dj )
es una escala que depende de cuántos términos de la query aparecen en el documento
(a mayor cantidad de términos presentes, mayor peso).
queryN orm (q)
tipo de objeto
es un factor de normalización del tipo
Query
P
2
wi,q
− 21
. Los valores
wi,q
dependen del
que se utilice. Este factor de normalización no cambia el orden relativo de los
documentos (ver que no depende del documento) pero permite comparar los puntajes otorgados a
un mismo documento en dos queries distintas.
boost (t) es un valor de impulso18
para el término de la query. Si queremos impulsar dinámicamente
un término de la query lo marcamos con un circunejo y un número que Lucene traducirá a la
fórmula (2.4.1). Ej: information retrieval^4.
norm (dj )
•
es un cálculo efectuado al indexar que incluye los siguientes factores:
el boost efectuado sobre el documento antes de indexarlo
17 Este análisis se
18 Traducción del
hizo para la versión 3 de Lucene, sin embargo, la forma general de esta fórmula suele mantenerse estable.
inglés boost.
2.4.
69
CASOS DE ESTUDIO
•
el boost efectuado sobre el campo antes de agregarlo al documento
•
un valor de normalización respecto de la cantidad de términos en el documento. La clase
DefaultSimilirity lo implementa como
1
número de términos
.
Si queremos que un documento o un campo de un documento del corpus sea más relevante en forma
estática, podemos especicar un valor de impulso de forma similar a como especicamos
boost (t).
Ordenamiento y Filtrado
Comúnmente tenemos que mezclar tareas de recuperación ad hoc con operaciones típicas de un RDBMS
como el ordenamiento y el ltrado. Si suponemos una aplicación que indexa y recupera información de
un corpus de artículos cientícos, seguramente nos interese ordenar los resultados (artículos) según el
número de veces que fueron citados o ltrarlos para que la antigüedad de un artículo no exceda cierta
cantidad de años.
Lucene permite este tipo de recuperación mediante la clase Sort y las subclases de Filter. Con este
mecanismo podemos efectuar ordenamientos y ltros según el campo del documento que especiquemos.
Análisis de Lucene
En esta sección vamos a analizar Lucene desde distintos ángulos considerando su uso para la indexación
de objetos de un modelo de dominio.
Siguiendo los criterios propuestos en (Johnson y Foote, 1988), Lucene no es un framework sino que es
una librería ó biblioteca. El ujo de control lo debe mantener la aplicación huésped invocando a Lucene
al momento de indexar o buscar documentos, por lo que esta herramienta no cuenta con inversión del
control (recordemos de la subsección 2.2.1 que los enfoques más puristas agregan IoC como un requisito
para formar un framework). Dado que bajo estos criterios no es un framework, sólo podemos discutir
sus características de caja blanca y negra desde el punto de vista de la abstracción como librería. En ese
contexto presenta un comportamiento de caja negra ya que posee una API bien denida que no requiere
grandes conocimientos de su estructura interna. Para usuarios expertos, Lucene se puede extender para
alterar, por ejemplo, las fórmulas de similitud y análisis de texto.
Si bien Lucene permite indexar y recuperar documentos en cualquier aplicación Java, la herramienta está
orientada a documentos. Para indexar objetos de dominio surgen desajustes similares a los estudiados al
hablar de persistencia de objetos (ver subsección 2.3.7), los cuales deben ser resueltos por el usuario de
Lucene.
Los autores de Hibernate Search (Bernard, 2007b ) señalan tres desajustes surgidos de utilizar Apache
Lucene para indexar objetos de un dominio:
Desajuste de Sincronización (Synchronization Mismatch ): consiste en el problema de mantener
sincronizado el datastore y el índice en base a los eventos CRUD.
Desajuste Estructural (Structural Mismatch ): es el problema de efectuar un mapeo de objetos a
documentos. Este desajuste es similar al que existe cuando se mapean objetos de dominio con JDBC
según analizamos en la subsección (2.3.7).
Desajuste de Recuperación (Retrieval Mismatch ): al recuperar información Lucene devuelve instancias de la clase Document y no objetos del dominio.
Una crítica común hacia Lucene desde los autores de Hibernate Search y Compass es que es una solución
de bajo nivel. Si bien esto es especialmente subjetivo, es verdad que Lucene está lejos de ser tan simple
de utilizar como, por ejemplo, librerías de logging. Las críticas que hacen los autores de Hibernate y
Compass parece estar dirigida a que Lucene puede ser utilizado incorrectamente con facilidad y que tarde
o temprano no nos permite abstraernos de qué ocurre dentro de la herramienta.
A continuación vamos a comentar cómo se implementan en Lucene las técnicas principales de IR que
hemos visto en la sección (2.1). Es preciso notar que la mayoría de las técnicas de IR aplicadas por
Lucene son soluciones estándar bien conocidas y descriptas en la literatura.
70
CAPÍTULO 2.
ESTADO DEL ARTE
Lucene implementa la indexación dinámica mediante técnicas similares a las de índices auxiliares y
generacionales (ver subsección 2.1.5). Estos índices auxiliares son llamados segmentos. Los segmentos son
subíndices independientes sobre los que es posible agregar y eliminar documentos. Vemos entonces que
el índice no es un archivo o estructura privilegiada sino que está formado por un conjunto de segmentos
independientes. Un segmento esta formado por más de una decena de archivos, los cuales almacenan el
índice invertido, las zonas y campos, documentos eliminados y estadísticas para el cálculo de similitud
(ver análisis de ecuación 2.4.1).
Una característica de Lucene es que sus índices requieren un mantenimiento explícito mediante la opti-
mización. El proceso de optimización aplica la eliminación de documentos dados de baja en el índice así
como la fusión de segmentos según la política de fusión. El algoritmo de fusión de segmentos tiende a
buscar una solución de compromiso entre necesitar acceder a muchos archivos para las búsquedas (lo que
degrada la performance) y requerir fusiones excesivas (lo que afecta principalmente la performance de
indexación). El algoritmo de fusión es similar a la fusión logarítmica (Manning et al., 2008) y se explica en
detalle en (Cutting, 2004a ). Adicionalmente, la operación de optimización también remueve físicamente
del segmento los documentos eliminados y recupera los huecos en la secuencia de numeración introducidos
por la eliminación de documentos.
Los eventos de altas, bajas y modicaciones de documentos se implementan con ciertas sutilezas que
deben comprenderse para trabajar correctamente con Lucene
19 . En el caso de las altas, luego de que
el escritor (IndexWriter) actualiza el índice, los nuevos documentos sólo se hacen visibles a los lectores
(IndexReader) al reabrir el índice (en caso de que el índice no estuviera abierto alcanza con abrirlo por
primera vez). Las bajas de documentos se marcan en un vector de invalidaciones y se aplican físicamente
al optimizar el índice. La modicación de documentos en Lucene no se implementa estrictamente como
tal sino que es necesario efectuar una baja del documento original y un alta del documento modicado.
De hecho, estas operaciones se aplican sobre interfaces distintas (IndexReader para la baja e IndexWriter
para el alta).
Respecto de la performance de indexación y recuperación, existe un consenso acerca de las buenas marcas obtenidas por Lucene. En general se encuentra que Lucene emplea varias técnicas para mejorar su
performance (debemos tener en cuenta que la herramienta ha ido adoptando y mejorando técnicas desde
su aparición en el año 2000). Para acelerar la intersección de posting lists, Lucene utiliza skip pointers.
Debido a la necesidad de comprimir el índice, se utiliza compresión por front coding para el diccionario
de términos junto con variable bit encoding para los identicadores de documentos. Esta información se
puede obtener de la documentación de Lucene así como de (Cutting, 2004b ). Las técnicas de skip pointers,
front coding y variable bit encoding se pueden encontrar en (Manning et al., 2008).
La calidad de los resultados de Lucene (en términos de similitud) está reconocida en la industria como muy
buena. Como hemos analizado previamente, Lucene implementa el modelo vectorial mediante variantes
de TF-IDF (ver subsección 2.1.4). Las variaciones hechas a TF-IDF se explican bajo criterios heurísticos
pero la documentación no referencia justicativos analíticos de estas modicaciones. La solución utilizada
para valorizar zonas es intuitiva aunque la documentación no referencia un sustento formal como el
presentado en la subsección (2.1.6). Esto último se explica seguramente por la ausencia de un entrenador
y la variabilidad en el número de zonas en un documento.
En términos de concurrencia, Apache Lucene resuelve la indexación y búsqueda con un mecanismo similar
(pero no idéntico) al clásico problema de concurrencia de lectores y escritores. El proceso de búsqueda
(lector) puede acceder al índice en forma concurrente al mismo tiempo que un indexador (escritor). La
diferencia con la semántica de lectores y escritores es que éstos debían excluirse mutuamente mientras
que en Lucene es posible buscar mientras se indexa. La exclusión mutua entre escritores sí es necesaria
y se da mediante archivos de bloqueo (lock les), quienes previenen la escritura concurrente del índice
lanzando una excepción. La existencia de lock les provee exclusión mutua no solo entre hilos sino entre
procesos.
2.4.2. Hibernate Search
En esta sección analizamos la primera herramienta que busca solucionar el problema de indexación de
objetos de un modelo de dominio, conocida como Hibernate Search (en adelante también la llamare-
19 Estas
consideraciones seguramente cambien con futuras versiones de Lucene. Recordemos que esta descripción corre-
sponde a la versión 3 de la herramienta.
2.4.
71
CASOS DE ESTUDIO
mos simplemente HS ). A continuación vamos a analizar esta herramienta desde distintas perspectivas
relevantes al problema que queremos resolver.
Descripción General
El objetivo de HS es indexar un modelo de dominio persistido con Hibernate Core o JPA, abstrayendo al
programador de los desajustes que analizamos en las subsecciones (1.1.3) y (2.4.1). HS es una herramienta
de código abierto implementada en Java, la cual no es independiente sino que depende de Apache Lucene,
Hibernate Core y complementos de éste como Hibernate Annotations.
A diferencia del próximo caso de estudio (subsección 2.4.3), los autores de HS recomiendan utilizarlo sólo
20 .
en conjunto con Hibernate o JPA (Bernard, 2007a )
Indexación y Modelo de Dominio
Para poder indexar un dominio particular, HS requiere que introduzcamos annotations de Java que marcan las clases cuyas instancias son indexables (en adelante hablaremos simplemente de clases indexables
entendiendo que lo que se indexan son instancias).
Estas clases indexables se marcan con @Indexed y junto a ellas se debe indicar qué directorio de Lucene
alojará el índice (a priori los índices de cada clase se separan en directorios con un índice propio, aunque
es posible almacenar distintas jerarquías en un mismo índice).
El hecho de indexar una clase no sirve para mucho sin indexar sus atributos. Para que un atributo ingrese
al índice es necesario anotarlo con @Field. Esta anotación lleva parámetros que especican si el contenido
del atributo se debe almacenar, analizar, etc. Estos parámetros son idénticos a los utilizados con Lucene
para crear un Field (ver subsección 2.4.1). Al recuperar un objeto, HS permite efectuar una operación de
proyección que sólo retorna los atributos indicados (proyectados), ahorrando la carga de recuperar el
grafo de referencias de un objeto. Dado que la proyección no utiliza el RDBMS, para hacer uso de ella es
necesario almacenar en el índice el valor del atributo proyectado.
Para ser indexado y recuperado unívocamente desde el ORM, cada objeto necesita tener una identidad.
Para marcar que un atributo identica la instancia, HS utiliza las anotaciones @Id y @DocumentId.
Es posible hacer que la identicación de un objeto provenga desde otra fuente mediante la anotación
@ProvidedId.
Procesos de Indexación
Como discutiremos al abordar la propuesta de solución en la subsección (3.1.3), existen básicamente tres
modos de indexación que tendrían que cubrirse: indexación online, semi-online y oine.
HS implementa los tres modos de indexación mediante backends, los cuales son llamados de la siguiente
forma:
Lucene: utiliza directamente el índice de Lucene. Equivale al modo Online o Semi-Online dependiendo de si es sincrónico (online) o asincrónico (semi-online).
JMS: equivale al modo Oine. Se utilizan colas JMS para indicar la necesidad de indexar los objetos.
Los índices se pueden dividir siguiendo un proceso conocido como index sharding, lo cual promueve
ventajas de performance. En todos los casos, Lucene es quien nalmente accede al índice, por lo que las
políticas de bloqueos de archivos son las que maneja esa librería.
20 Si
bien han pasado algunos años desde esta armación, entendemos que el diseño de Hibernate Search no ha cambiado
sustancialmente y se sigue cumpliendo esta recomendación
72
CAPÍTULO 2.
ESTADO DEL ARTE
Jerarquías de Subclasicación y Asociaciones
HS permite la indexación automática de todos los atributos de la jerarquía de clases del objeto así como
las asociaciones que ocurren en dicha jerarquía.
Utilizando las anotaciones @IndexedEmbedded y @ContainedIn, es posible indexar grafos de objetos referenciados explícitamente o en colecciones. Esto es importante ya que un modelo rico de objetos posiblemente
requiera colaboraciones, las cuales formarán un grafo de objetos compuestos. Cuando un objeto compuesto en otro es modicado, HS actualiza el índice para que el objeto huésped se actualice con la nueva
información del objeto contenido (eso es porque HS asigna al objeto contenedor como dueño de los
postings).
Para generar el índice invertido es necesario convertir las entidades del dominio a una representación como
texto. En HS esta conversión se aplica mediante puentes (bridges). Los puentes tienen la responsabilidad
de convertir un tipo particular de objeto a String. Un hecho positivo de la implementación de HS es que no
obliga a la entidad en si misma a implementar una interfaz de conversión sino que inyecta una dependencia
al conversor mediante el annotation @FieldBridge.
Modelos de IR y Puntajes
El modelo de IR y puntajes es heredado directamente de Lucene, por lo que HS implementa internamente
un modelo vectorial. Los cálculos de TF-IDF, impulsos y demás se mantienen tal como denimos en la
subsección (2.4.1). Para manejar los valores de impulso, HS implementa un annotation @Boost, el cual
traslada este valor al índice de Lucene.
Para alterar la implementación de similitud es necesario modicar directamente las clases de Lucene como
DefaultSimilarity o Weight.
En HS existe la posibilidad de ordenar y ltrar los resultados tal como hacíamos con Lucene. Para el
caso del ordenamiento se debe usar la API de Lucene mediante las clases Sort y SortField. El ltrado se
resuelve también utilizando la API de Lucene, pero con cierta abstracción provista por HS.
Recuperación, Queries, Matching y Acceso a Datos
La recuperación de objetos en HS funciona como un wrapper de una búsqueda de Lucene. Para el usuario
de HS, la tarea de recuperación se presenta como un híbrido entre el trabajo con el ORM y con Lucene.
Como resultado de las búsquedas, HS retorna el objeto de dominio que considera relevante para la
búsqueda. La rigurosidad con la que esto se implementa garantiza la identidad, es decir, si utilizamos
el operador == entre los objeto devueltos por el ORM y HS el resultado sera true. Esto implica que
existe una única representación de los objetos de dominio (en rigor, internamente también existe la
representación como Document de Lucene).
Las operaciones extendidas (ver subsección 2.1.5) de HS se resuelven utilizando el motor de IR de Lucene,
por lo cual HS tiene la misma expresividad que éste.
En la terminología de Hibernate se suele utilizar el término hidratación para referir al proceso de convertir
objetos que sólo tienen inicializados sus identicadores (deshidratados ) en los objetos completos con todos
sus atributos (hidratados ). Esto es importante al trabajar con proyecciones (este concepto también está
presente en Hibernate Core). Una proyección permite recuperar una forma deshidratada del objeto, en la
cual sólo podemos obtener (proyectar) datos que hemos almacenado en el índice de Lucene. La proyección
puede ser útil para obtener valores del objeto sin cargarlo completamente (lo cual requiere utilizar el ORM
y eventualmente cargar un grafo de objetos). Existe una discusión respecto de si el método de proyección
es mejor que una hidratación desde el ORM ya que el segundo utiliza caches del ORM para evitar acceder
al RDBMS, lo que podría resultar en una solución aún más eciente.
A la hora de procesar los textos de los atributos, HS utiliza los analizadores de Lucene, permitiendo variar
la implementación por clase o atributo. De todas formas, la utilización de analizadores distintos dentro
de una misma clase es compleja ya que vuelve difícil efectuar las queries (porque no podemos usar un
único analizador para todos los campos).
2.4.
CASOS DE ESTUDIO
73
Una característica interesante de HS es que permite variar el analizador de forma dinámica dependiendo
del estado de la entidad que se indexa. Esto permite que si un objeto representa, por ejemplo, la entrada
de un blog en español, podremos utilizar un analizador distinto que si la entrada esta escrita en hebreo.
Existen algunas otras particularidades respecto de analizadores en HS para las cuales sugerimos consultar
la documentación.
Otra característica de HS es que permite restringir los resultados de las consultas a una entidad de negocio
particular. Esto permite que una query como Batman retorne instancias de la clase Film (película) pero no
instancias de la clase Toy (juguete). Este comportamiento además es polimórco, esto es, si B es subclase
de A y aplicamos un ltro para los objetos de tipo A, también obtendremos objetos del tipo B.
Análisis de Hibernate Search
El análisis de esta herramienta lo diferimos hasta después de describir la próxima herramienta (Compass
Project). Al nalizar la próxima sección haremos un análisis conjunto y comparativo de HS y Compass.
2.4.3. Compass
Compass Project es otra herramienta que busca resolver el problema del Domain Model Search. En los
próximos apartados analizaremos esta herramienta desde perspectivas similares a las que utilizamos para
analizar Hibernate Search.
Descripción General
El objetivo de Compass es permitirnos indexar distintas fuentes estructuradas de información. Compass
permite indexar no sólo modelos de dominio sino archivos XML, representaciones JSON y otros. Al igual
que Hibernate Search, esta herramienta está construida sobre Apache Lucene.
Compass se compone de tres módulos principales:
Core: es responsable de implementar el sistema de mapeo de entidades, las transacciones, la API
hacia el usuario y las extensiones a Lucene.
Gps: se ocupa de la integración con distintos ORM (Hibernate, JDO, JPA) y las utilidades para la
indexación de datos desde una base de datos mediante SQL.
Spring: efectúa la integración entre Compass y los módulos del framework Spring.
La mayor parte de nuestra descripción se basará en el módulo Core y en menor medida en el Gps.
Como comentamos al estudiar HS, mientras éste está dirigido a los usuarios con un modelo de dominio
persistido con Hibernate o JPA, Compass presenta una visión más generalista, admitiendo otras combinaciones de entidades y frameworks de persistencia.
Veremos que Compass tiene un comportamiento intermedio entre framework y librería. En general la
interacción con Compass requiere que instanciemos la clase Compass y dirijamos el ujo de control, lo
cual es propio de una librería. En situaciones como la inspección de los objetos y la conversión de atributos
para indexación, es la herramienta quien invoca nuestro código trabajando como un framework de caja
negra.
Indexación y Modelo de Dominio
El trabajo con nuestras entidades depende de si vamos a indexar un modelo de dominio, archivos XML,
JSON o el resultado de una consulta a una base de datos. Estos mapeos se implementan en cuatro modos
de trabajo conocidos como OSEM (Object/Search Engine Mapping), XSEM (XML to Search Engine
Mapping), JSEM (JSON Search Engine Mapping) y RSEM (Resource/Search Engine Mapping).
Compass reimplementa conceptos y objetos de Lucene que permiten que nuestro trabajo tome cierta
distancia de este. En particular las clases Document y Field de Lucene se respectivamente en Resource y
Property.
74
CAPÍTULO 2.
Indexación OSEM (Object/Search Engine Mapping)
ESTADO DEL ARTE
Este tipo de indexación se corresponde con
la provista por Hibernate Search y es la que más nos interesa ya que apunta a indexar un modelo de
dominio.
En el OSEM es necesario indicar qué clases y atributos son indexables mediante anotaciones de Java,
conguración XML o JSON. En esta descripción vamos a cubrir el método de anotaciones, siendo que
basta con saber que el resto de los métodos son equivalentes.
Para indexar las instancias de cierta clase, se debe utilizar la anotación @Searchable. Por defecto, cada
entidad se indexa en un sub índice separado. La anotación @SearchableId le indica a Compass cuál es el
identicador de la entidad. Este identicador deberá ser usado si queremos obtener la entidad real desde
el ORM. Para los casos en los que es necesario que la clave de una entidad esté representada por otra
entidad compleja, se agrega un annotation @SearchableIdComponent, la cual traslada el mapeo de clave a
la segunda entidad.
Existe un tipo distinguido de clases llamadas raíces. Las clases raíces son las que efectivamente pueden
ser recuperadas en una búsqueda. Esto implica que si tenemos un objeto Client que contiene un objeto
Name, las búsquedas que coincidan por dicho atributo retornarán el Client. Dado que el objeto recuperado
es el de la clase raíz, éstos están obligados a denir un atributo @SearchableId.
En términos de independencia del modelo de dominio (ver subsección 2.2.2), Compass introduce algunas
restricciones como:
Default Constructor: es necesario para instanciar los objetos mediante Constructor.newInstance().
Este requerimiento suele estar presente en los frameworks de persistencia como Hibernate Core.
Este constructor puede tener cualquier grado de visibilidad.
Property Identier: toda clase raíz debe poseer al menos un identicador de clave primaria.
Vemos entonces que las imposiciones sobre el modelo de dominio son bastante básicas, siendo que la
necesidad de un constructor sin argumentos es con seguridad la más discutible.
Las clases pueden denir meta datos que se indexarán de forma idéntica para todas sus instancias mediante
la anotación @SearchableConstant.
Otros métodos de indexación (XSEM , JSEM y RSEM)
Compass permite indexar fácilmente
XML y JSON así como nos da facilidades para mapear una fuente arbitraria de datos a un Resource,
el cual luego puede ser indexado. Este último mapeo es útil para indexar tablas de una base de datos
cuando no se dispone de un modelo de dominio que acompañe dicho esquema.
Si bien estos modos de mapeo son útiles para algunos casos, no forman parte de la solución al problema
principal que queremos atacar (subsección 1.1.3). Por esto en adelante hablaremos principalmente del
método de indexación OSEM. Continuando con esta línea, a continuación vemos un ejemplo básico:
Ejemplo 2.4.3.
La indexación de objetos se efectúa mediante una interfaz muy similar a la utilizada con
otros ORM como Hibernate Core. Veamos un ejemplo tomado de la documentación de Compass (Compass
Project, 2009):
1
2
3
4
5
6
7
8
9
10
11
12
13
14
CompassConfiguration conf =
new C o m p a s s C o n f i g u r a t i o n ( ) . c o n f i g u r e ( ) . a d d C l a s s ( Author . c l a s s ) ;
Compass compass = c o n f . b u i l d C o m p a s s ( ) ;
C o m p a s s S e s s i o n s e s s i o n = compass . o p e n S e s s i o n ( ) ;
CompassTransaction tx = n u l l ;
try {
tx = s e s s i o n . beginTransaction () ;
...
s e s s i o n . save ( author ) ;
CompassHits h i t s = s e s s i o n . f i n d ( " j a c k l o n d o n " ) ;
Author a = ( Author ) h i t s . d a t a ( 0 ) ;
Resource r = h i t s . resource (0) ;
...
t x . commit ( ) ;
2.4.
15
16
17
18
19
}
}
}
CASOS DE ESTUDIO
75
catch ( Compa ssExce ption ce ) {
i f ( t x != n u l l ) t x . r o l l b a c k ( ) ;
finally {
session . close () ;
Las líneas 1 a 3 generan una conguración de Compass y una fábrica de sesiones (en este caso la conguración es programática). La conguración y la generación de la fábrica de sesiones es un proceso costoso,
por lo que se suele efectuar una única vez por instancia de la aplicación. Después de la línea 4 aparece el
código que se ocupa de abrir una transacción, efectuar una query y obtener los resultados como objeto de
dominio o
Resource
(este es el código que veremos con mayor frecuencia en la aplicación).
Compass tiene un tratamiento transaccional del índice, el cual requiere una demarcación programática de
las transacciones. Los niveles de aislamiento provistos por Compass son: read_commited, lucene y async.
En los próximos apartados detallaremos la semántica de estos modos.
Como vimos en el ejemplo (2.4.3), el estilo de programación para la actualización del índice es muy similar
al de los ORM como Hibernate. Asimismo, la tarea de recuperación es similar al trabajo con Lucene.
Según los autores de Compass, estos dos hechos son una estrategia para facilitar la adopción de Compass
por parte de quienes ya trabajan con Apache Lucene y los ORM tipo Hibernate Core.
Procesos de Indexación
Compass permite la indexación programática utilizando la interfaz CompassSession así como la indexación
automática mediante el módulo Gps.
El módulo Gps permite indexar de forma transparente un dispositivo de tipo CompassGpsDevice. Dos
ejemplos de dispositivos pueden ser Hibernate o iBATIS. El Gps puede indexar todos los datos del
dispositivo (eventualmente un RDBMS) así como estar a la escucha de eventos y replicar lo que sucede
en el ORM hacia su índice.
El índice en Compass está formado por un conjunto de subíndices. Cada subíndice contiene exactamente
un índice de Lucene (el cual internamente además contiene segmentos generacionales). Para garantizar la
transaccionalidad, las operaciones de escritura bloquean los subíndices (por lo cual es útil contar con una
buena dispersión de entidades entre los subíndices). Dentro de un mismo subíndice, la exclusión mutua
sobre los segmentos se resuelve utilizando los mecanismos de bloqueos ínter e intra procesos de Apache
Lucene.
Compass permite utilizar distintos tipos de transacciones mediante transaction managers (TM). Un TM
es encargado de proveer semánticas de consistencia en la lectura y escritura del índice. Los TM más
discutidos en la documentación son: read_commited, lucene y async.
El TM read_commited garantiza que las transacciones sólo ven cambios conrmados en el índice (con excepción de los cambios que la propia transacción está llevando a cabo). Este TM bloquea el subíndice sólo
al efectuar una operación de escritura. El siguiente TM conocido como lucene, es similar al read_commited
con la diferencia de que los cambios hechos por la transacción no son visibles por ella hasta conrmarse.
Estos TM son similares al modo de indexación online que comentamos junto a HS. El TM async permite
que un hilo en paralelo tome la tarea de indexación (es decir, el método commit() retorna sin conrmar
realmente la escritura), lo que relaja la semántica de consistencia. La documentación de Compass también
habla de otros niveles transaccionales conocidos como mt, search y tc. En particular el TM tc (terracota )
funciona de manera similar al modo de indexación oine que discutimos en HS, permitiéndonos efectuar
una indexación diferida respecto de la conrmación de la sesión.
Al efectuar búsquedas, podemos trabajar fuera de una transacción pidiendo instancias de CompassDetachedHits (el cual no permite escribir en el índice).
Jerarquías de Subclasicación y Asociaciones
Dado que la subclasicación es una herramienta muy importante en los diseños de objetos, Compass
soporta la indexación de jerarquías de entidades anotadas con @Searchable. Cuando se indexa una entidad
76
CAPÍTULO 2.
ESTADO DEL ARTE
de la parte inferior de la jerarquía, todos los campos anotados de la parte superior de la jerarquía se
heredan automáticamente.
Una propiedad interesante soportada por Compass es la capacidad de indexar entidades que no fueron
anotadas pero que su jerarquía contiene en sus capas superiores clases anotadas con @Searchable. En estos
casos, la indexación sólo trabaja sobre los campos anotados en la parte superior de la jerarquía, mientras
que la búsqueda recupera y entrega objetos a partir de los atributos de toda la jerarquía.
Tal como sucedía con HS, Compass también permite indexar clases compuestas. Si queremos indexar una
asociación debemos anotarla con @SearchableProperty. Esta anotación permite indexar automáticamente
objetos de tipo String, primitivos de Java (int, oat, double, etc.), java.util.Date y java.util.Calendar.
Si tenemos una asociación entre objetos anotados como @Searchable y requerimos que la recuperación de
uno de ellos también recupere (hidrate) el otro, podemos anotar la asociación con @SearchableReference.
Este mecanismo permite utilizar lazy-loading para evitar recuperar grafos extensos desde el índice. Para
resolver las asociaciones circulares existe una anotación @SearchableParent, la cual previene a Compass
de tal situación.
Cuando estamos asociando objetos anotados con @Searchable, podemos lograr que una búsqueda que
debería retornar el objeto contenedor también recupere el objeto contenido. Para lograr este efecto
disponemos de la anotación @SearchableComponent.
En ocasiones una entidad contiene una lista o un mapa que permite implementar algo así como una clase
con atributos variables. En ciertos casos ésos elementos deben ser indexados como si fueran atributos
concretos, por lo que Compass permite resolver dinámicamente el nombre y el valor de esos atributos
dinámicos utilizando la anotación @SearchableDynamicProperty.
Modelos de IR y Puntajes
Analizando la documentación de Compass (Compass Project, 2009), el modelo de IR y puntajes ocupa
un lugar relativamente pequeño en comparación con las características de transaccionalidad. Este hecho
también se reeja en la herramienta, la cual se limita a utilizar el modelo de similitud de Lucene. Al
igual que con HS, es posible utilizar la API de Lucene para modicar las fórmulas de similitud y efectuar
ordenamientos basados en atributos especícos.
Desde el punto de vista de la conguración, Compass permite denir un valor de impulso para el algoritmo
de similitud de Lucene mediante la anotación @SearchableBoost, la cual es idéntica al annotation @Boost
de HS.
Recuperación, Queries, Matching y Acceso a Datos
Compass implementa directamente el modelo de consultas de Lucene envolviéndolo en un objeto de tipo
CompassQuery. Las consultas que se pueden efectuar tienen exactamente la misma expresividad que las
de Lucene, excepto por pequeños agregados como mejoras al sistema de consultas de rangos.
Un aspecto central que distingue la forma de trabajar de Compass es el método de hidratación de los
resultados. La hidratación depende de si activamos o no el marshalling. Cuando el marshalling está
activo, los datos necesarios para hidratar el objeto se obtienen desde los campos almacenados en el
índice invertido. Esto implica que para recuperar los objetos de dominio tenemos dos caminos: utilizar
marshalling para hidratar el objeto en base a la representación del índice o recuperar desde el índice los
identicadores para luego consultar nuestro ORM.
Es inmediato notar que el marshalling tiene impacto sobre el tamaño del índice de Lucene, el consumo
de memoria y la velocidad del sistema. Por esto también es posible escoger la solución de compromiso
más adecuada al problema efectuando un marshalling parcial del objeto. Cuando efectuamos un marshall
parcial, los atributos que no pueden recuperarse se cargan con un valor null. Eventualmente, también es
posible no hacer marshalling y trabajar directamente sobre los objetos de tipo Resource.
Como es de esperar, la expresividad de las consultas es la misma que la de Apache Lucene. Si bien
Compass posee objetos de tipo Resource, la hidratación permite obtener como resultado de una búsqueda
nuestro objeto de dominio.
2.4.
77
CASOS DE ESTUDIO
Internamente Compass utiliza analizadores compatibles con los Analyzer de Lucene. Es factible escoger
el analizador en función del atributo que se está indexando mediante la anotación @SearchableAnalyzer.
Así como vimos que podemos utilizar un objeto CompassDetachedHits, la implementación más común
utilizará el envoltorio del objeto Hits de Lucene llamado CompassHits. Esta reimplementación del objeto
de Lucene agrega el comportamiento necesario para trabajar en el entorno transaccional de Compass.
Al estudiar las técnicas de operaciones extendidas (subsección 2.1.5), vimos que puede ser util contar
con un motor de correcciones o sugerencias soportado por un tesauro. Compass provee este servicio de
sugerencias simulando el Did you mean que ofrecen buscadores como Google. El proceso de generación
de sugerencias corre en segundo plano, reconstruyendo periódicamente el índice de correcciones.
Ejemplos
En este apartado vamos a ver algunos casos concretos de trabajo con Compass (los ejemplos son mayormente adaptaciones de muestras y tests JUnit que acompañan a la herramienta).
Ejemplo 2.4.4 (Conguración e Indexación de un Objeto de Dominio).
En este ejemplo vemos (a) cómo
se construye la conguración de Compass y (b) se opera con los objetos de dominio.
1
CompassConfiguration
config
= new
CompassConfiguration () . configure (
" / o r g / compass / s a m p l e / l i b r a r y / compass . c f g . xml " ) . a d d C l a s s (
A u t h o r . c l a s s ) . a d d C l a s s ( A r t i c l e . c l a s s ) . a d d C l a s s ( Book . c l a s s ) ;
2
3
4
5
compass =
c o n f i g . buildCompass () ;
compass . g e t S e a r c h E n g i n e I n d e x M a n a g e r ( ) . d e l e t e I n d e x ( ) ;
compass . g e t S e a r c h E n g i n e I n d e x M a n a g e r ( ) . c r e a t e I n d e x ( ) ;
c o m p a s s T e m p l a t e = new
CompassTemplate ( compass ) ;
La línea número 1 genera la conguración desde un XML y agrega el mapeo de dos clases. La línea
número 2 genera el objeto compass a partir de esta conguración, dejando el sistema listo para generar
sesiones (CompassSession) o trabajar mediante un template (CompassTemplate). Las líneas 3 y 4 eliminan
el índice actual. La línea número 5 genera un objeto de tipo CompassTemplate que explicaremos en breve.
En el siguiente fragmento de código vemos la forma transaccional de operar en Compass para almacenar
dos objetos:
// generamos dos o b j e t o s de d o m i n i o
Author
j a c k L o n d o n = new
Author ( ) ;
j a c k L o n d o n . s e t I d ( new L o n g ( 1 ) ) ;
j a c k L o n d o n . s e t N a m e ( new Name ( "Mr" ,
Calendar
" Jack " ,
" London " ) ) ;
c = Calendar . getInstance () ;
c . s e t (1876 ,
0,
12) ;
jackLondon . s e t B i r t h d a t e ( c . getTime ( ) ) ;
j a c k L o n d o n . s e t K e y w o r d s ( new
String [ ]
{
" american
author "
}) ;
w h i t e F a n g = new Book ( ) ;
w h i t e F a n g . s e t I d ( new L o n g ( 1 ) ) ;
w h i t e F a n g . s e t T i t l e ( " White
c . s e t (1906 ,
0,
F a ng " ) ;
1) ;
whiteFang . s e t P u b l i s h D a t e ( c . getTime ( ) ) ;
w h i t e F a n g . s e t S u m m a r y ( " The
creature
of
the
remarkable
story
of
a
w h i t e F a n g . s e t K e y w o r d s ( new
String [ ]
" jack
london " ,
{
j a c k L o n d o n . a ddB ook ( w h i t e F a n g ) ;
// comienza l a i n t e r a c c i ó n con Compass
CompassSession
session
CompassTransaction
try
= compass . o p e n S e s s i o n ( ) ;
tx = null ;
{
tx =
fiercely
independent
wild ") ;
session . beginTransaction () ;
"call
of
the
wild "
}) ;
78
CAPÍTULO 2.
ESTADO DEL ARTE
s e s s i o n . save ( jackLondon ) ;
s e s s i o n . save ( whiteFang ) ;
t x . c o mm i t ( ) ;
}
( Exception
catch
if
( tx
null )
e)
{
{
tx . r o l l b a c k () ;
}
e;
th row
}
!=
{
finally
session . close () ;
}
Si no queremos encargarnos de abrir y cerrar la sesión, podemos utilizar una colaboración que responde
al patrón de diseño Template (Gamma et al., 1995):
compassTemplate . s a v e ( jackLondon ) ;
En este último caso Compass fue el responsable de abrir la sesión, conrmarla y cerrarla, actuando como
un framework de caja negra. Si quisiéramos efectuar una operación más compleja utilizando este patrón,
podemos hacer que colaboren CompassCallbackWithoutResult y CompassTemplate como en el siguiente
fragmento de código:
c o m p a s s T e m p l a t e . e x e c u t e ( new
protected
void
CompassCallbackWithoutResult ()
doInCompassWithoutResult ( CompassSession
CompassException
{
session )
throws
{
// cargamos un " j a c k l o n d o n "
Author
author = ( Author )
s e s s i o n . l o a d ( Author . class ,
jackLondon . g e t I d () )
;
// y su l i b r o " w h i t e f a n g "
Book
w h i t e F a n g = ( Book )
s e s s i o n . l o a d ( Book . c l a s s ,
whiteFang . g e t I d ( ) ) ;
// borramos a l a u t o r y a su l i b r o ( ú n i c a m e n t e d e l í n d i c e )
s e s s i o n . d e l e t e ( author ) ;
s e s s i o n . d e l e t e ( book ) ;
}
}) ;
A continuación efectuamos el análisis de Hibernate Search y Compass Project.
2.4.4. Análisis de Hibernate Search y Compass
En esta subsección analizamos HS y Compass buscando los siguientes objetivos:
entender las decisiones de diseño que se tomaron,
identicar sus debilidades y fortalezas,
establecer un análisis comparativo entre ellas.
Este aprendizaje permitirá que nuestro motor de búsqueda adopte sus mejores prácticas y evite sus
características no deseables. A medida que avancemos sobre cada tema vamos a formalizar la discusión
mediante proposiciones, las cuales expresan los criterios que surgen del análisis de las herramientas. Estas
proposiciones quedarán completas cuando tratemos la propuesta de solución, donde agregaremos casos y
aspectos que no han sido cubiertos por HS y Compass.
Características Generales
Tanto HS como Compass buscan resolver el problema de indexación y recuperación de información de
un modelo de dominio envolviendo Apache Lucene para que pueda indexar y recuperar objetos de un
dominio.
2.4.
CASOS DE ESTUDIO
79
La principal crítica que podemos formular hacia HS y Compass es que no aprovechan al máximo el
hecho de trabajar con objetos de dominio por sobre los documentos de Lucene. El hecho de haber sido
concebidos como un envoltorio de Lucene o un atajo para reutilizar su infraestructura ha importado vicios
del mundo de los motores de búsqueda orientados a texto plano o documentos. En los próximos apartados
veremos cómo la delegación de trabajo sobre Lucene (orientado a documentos) tiende a limitar la riqueza
de indexación y recuperación de objetos de estas dos herramientas.
Veamos algunos problemas del esfuerzo por integrarse con Lucene:
Abstracción Incompleta: a pesar de la sosticación de estos envoltorios, aún debemos conocer los
conceptos principales y funcionamiento de Lucene. Esto ocurre principalmente en HS, donde nos
encontramos con grados de abstracción propios del trabajo con Lucene. Por ejemplo, si en HS
queremos efectuar una búsqueda, debemos instanciar explícitamente un objeto Query de Lucene.
Otro ejemplo que impacta tanto a Compass como a HS es la inspección física de los índices, la cual
requiere utilizar inspectores de índices de Lucene como Luke.
Infraestructura Correctiva: en HS y Compass se construyen infraestructuras cuyo único propósito
es evitar limitaciones de Lucene. Por ejemplo, las técnicas de partición de índices en HS y Compass
tienen por objetivo mejorar aspectos negativos de la implementación del índice de Lucene (como los
bloqueos). Otro caso es la optimización de los índices, la cual propaga decisiones de conguración
hasta las capas de HS y Compass por más que son problemas exclusivos de Lucene. Es decir, estas
infraestructuras están condicionadas por forzar la reutilización a bajo nivel de Lucene.
Desaprovechamiento Estructural y Semántico: al momento de implementar las fórmulas de similitud,
ninguna de las dos herramientas toma sucientemente en cuenta las interrelaciones entre objetos
y su riqueza estructural. Más adelante veremos cómo esta riqueza estructural podría traducirse en
consultas jerárquicas como las de XML retrieval (ver Manning et al., 2008). Técnicas como PageRank y HITS (subsección 2.1.6) tampoco están aprovechadas. Esta no es una limitación necesaria de
las herramientas sino que existe por el uso directo de las fórmulas de similitud de Lucene, el cual
se limita a implementar zonas y campos (ver subsección 2.1.6).
Es preciso comentar que en ninguna de las dos herramientas plantea como objetivo primordial el ocultamiento de Lucene. Sin embargo, veremos muchos ejemplos en los cuales en vez de trabajar con un
framework de indexación de objetos terminamos haciéndolo con dos herramientas: una librería de indexación y un envoltorio de ésta. Un ejemplo de esta dualidad aparece en HS, el cual obliga a elegir una
estrategia de lectura del índice (shared, not-shared o custom). Para elegir esta estrategia es necesario comprender que los índices de Lucene mejoran su performance luego de un tiempo de precalentamiento. Es
decir, para tomar una decisión debemos conocer una herramienta adicional (Lucene) que trabaja sobre
una abstracción diferente a los objetos (documentos). En general encontraremos que, respecto de HS,
Compass cubre en objetos propios una mayor porción de la API de Lucene.
Dado que Lucene evoluciona independientemente de Compass y HS, existe una tensión propia de la
necesidad de éstos de ser compatibles con Lucene. A medida que Lucene introduce mejoras progresivas,
eventualmente se vuelve incompatible con versiones previas (de hecho la versión 3.0 muestra incompatibilidades con las anteriores). Para aprovechar dichas mejoras, los dos proyectos deberán producir
periódicamente versiones compatibles con Lucene (en el caso de HS, además estamos acoplados a ciertas
versiones de Hibernate Core, Annotations y otros).
Comportamiento como Framework
Tanto HS como Compass dan un gran paso adelante respecto de Lucene, el cual hemos visto que tiene
comportamiento de librería. En el caso de HS, la indexación se maneja automáticamente invocando
código del usuario cuando el framework lo decide (un ejemplo son los bridges que convierten tipos de
datos). En Compass, la indexación mediante un Gps también tiene el mismo estilo que el de HS. Ambos
comportamientos son típicos de un framework de caja negra, lo cual hemos visto que es un aspecto
deseable (ver subsección 2.2.1)
Sin embargo, tanto HS como Compass exhiben algunos comportamientos que no son propios de un
framework de caja negra sino de una librería. Como veremos en el próximo apartado, Compass permite
80
CAPÍTULO 2.
ESTADO DEL ARTE
indexar objetos programaticamente mediante sesiones, lo cual produce una interacción muy similar a la
que existía con Lucene (cuyo comportamiento vimos que era del tipo librería).
Las situaciones que requieren manejo explícito del ujo de control no siempre son indeseables. Existen
ocasiones en la cual es conveniente operar de forma explícita ignorando los eventos CUD que suceden en
cada transacción, como el caso de la indexación en lotes. De aquí surge la siguiente proposición:
Proposición 1.
Es deseable que el motor de búsqueda provea un comportamiento de caja negra mediante
inversión del control (subsección 2.2.1). Sin embargo, es importante que el usuario del framework pueda
hacerse dueño del ujo de control en los puntos donde la inversión de control no sea conveniente.
Indexación y Modelo de Dominio
En esta sección vamos a analizar el espíritu de trabajo de cada una de las herramientas en cuanto a
indexación de objetos. Las perspectivas desde las cuales necesitamos analizar la indexación son: soporte
transaccional, modelo de programación de actualizaciones al índice y mapeo de entidades de dominio.
A la hora de la indexación, Compass y Hibernate Search muestran losofías muy distintas en cuanto a
transaccionalidad. Compass implementa transacciones con distintos niveles de aislamiento mientras que
HS no implementa transacciones sino que simplemente utiliza los mecanismos de exclusión mutua de
Lucene. Este hecho forma parte de una gran controversia entre los dos proyectos, ya que desde Compass
ven la transaccionalidad como algo indispensable y desde HS como algo secundario cuya solución debe
venir desde Lucene.
Las transacciones de Compass incluyen la etapa de actualización del índice pero dejan fuera de su alcance
la etapa persistencia en el ORM. Esto es, las transacciones de Compass sólo garantizan niveles ACID
sobre el índice de Lucene. En el caso de Compass, dado que la hidratación se hace desde el índice, esta
semántica puede ser importante para evitar inconsistencias entre sesiones concurrentes. Fuera del caso
de Compass, esta semántica no parece brindar mayores benecios que la garantía de consistencia sobre
el índice (la cual se puede obtener mediante exclusión mutua como en HS).
Los autores de Hibernate señalan que no es deseable que una falla en la indexación produzca un rollback en
la transacción de negocio (esto es discutible caso por caso), por lo que una semántica transaccionalmente
fuerte que incluya al ORM puede no ser la más adecuada. Esto induce a una nueva proposición.
Proposición 2.
Si el motor de búsqueda implementa un comportamiento transaccional, el mismo será
ajustable, permitiendo variarlo entre los niveles:
Nivel 1: no hay comportamiento transaccional, sólo garantías de consistencia frente al acceso concurrente al recurso.
Nivel 2: ídem nivel anterior pero agrega propiedades ACID para el acceso al índice.
Nivel 3: ídem nivel anterior pero las propiedades ACID tienen en su alcance tanto la operación con
el ORM como con el índice.
La implementación del nivel 1 es condición necesaria para cualquier motor de búsqueda que quiera mantener su índice consistente. Los niveles 2 y 3 son opcionales pero la implementación debe permitir al
usuario la elección de los niveles previos ya que hay aplicaciones que no requieren esta semántica.
Compass permite trabajar sobre el índice con exibilidad mediante transacciones programáticas (en
adelante TP), Templates y Gps.
Las TP se asemejan al trabajo con una librería ya que el usuario de la herramienta debe mantener el ujo
de control, demarcar las transacciones y atrapar excepciones. El caso de las transacciones programáticas debe evitarse ya que obliga a tener código duplicado para crear/iniciar/cerrar la sesión y atrapar
excepciones.
La utilización de Templates es más razonable ya que sólo requiere que especiquemos operaciones entre
objetos de dominio y el índice, sin requerir el código relacionado con la transacción. Sin embargo, si
utilizamos el Template para eventos de escritura sobre el índice (toda operación excepto las búsquedas),
2.4.
CASOS DE ESTUDIO
81
estamos desaprovechando la inversión de control de los ORM que pueden ser programados para noticarnos de eventos de persistencia.
Este último comportamiento por eventos está dado por el Gps, quien se registra a los eventos de persistencia y se ocupa de todo el trabajo de indexación.
Como ya comentamos, HS no soporta las transacciones de la misma forma que Compass. Por esto, en HS
no encontraremos problemas de demarcación de transacciones sino que éste indexa los objetos en base a
eventos del ORM (muy similar al Gps de Compass). Sin embargo, para casos en los que las capacidades de
búsqueda se agregan después de que el sistema entra en operación, existen un mecanismo de indexación
manual. Dado que HS no soporta transacciones, este mecanismo de persistencia manual no es exactamente
comparable a las TP o a los Templates, sino que es similar a la indexación de documentos en Lucene.
Con este análisis podemos llegar a la siguiente observación para tener en cuenta en nuestro buscador:
Proposición 3.
Para evitar la repetición de código, aprovechar la inversión del control y ganar compor-
tamiento de framework de caja negra, las operaciones sobre el índice deben ocultarse al usuario del motor
de búsqueda y, cuando esto no sea posible, se deben proveer Templates que eviten la repetición de código.
Existe un caso para el cual la utilización de Templates y eventos no es posible. Si necesitamos lanzar
una excepción propia de la aplicación, no tenemos forma de lanzarla desde dentro del Template ya que
la cláusula throws debe especicar una tipo de excepción del motor de búsqueda (o un Exception, lo cual
tampoco se aconseja ya que estaríamos atrapando más casos que los que sabemos manejar).
Respecto del problema del desajuste de sincronización (synchronization mismatch, ver subsecciones 1.1.3
y 2.4.1), utilizar TP o Templates requiere que cualquier actualización de los objetos de dominio se
replique explícitamente sobre el índice, lo que tiende inevitablemente a un sistema difícil de mantener y
a la aparición de errores. Esto nos lleva a la siguiente observación:
Proposición 4.
Para evitar el problema del desajuste de sincronización, las operaciones CUD (cre-
ate, update y delete) deben ser atrapadas por el motor de búsqueda sin intervención del usuario de la
herramienta.
Existen excepciones por las cuales todavía es necesario contar con Templates o TP. Una de estas excepciones es el caso de que el ORM no dé aviso de los eventos CUD. Por esto el motor de búsqueda también
debe dar un acceso similar a las TP o Templates de Compass, siempre preriendo estos últimos. Otra
excepción a la indexación automática por eventos tiene que ver con la necesidad de indexar las entidades
manualmente fuera de los procesos de negocio transaccionales (este caso fue analizado previamente en el
apartado de Comportamiento como Framework).
Un campo en el cual las dos herramientas son muy potentes es en la resolución del structural mismatch
(ver subsección 2.4.1). Las dos implementaciones proveen mecanismos a tener en cuenta como: soporte de
subclasicación, composición de objetos, indexación de colecciones, reconocimiento de claves complejas,
ltrado de resultados por tipo de instancia y transformación de tipos de datos complejos. Como vimos
previamente, si bien las dos herramientas logran una expresividad similar en cuanto a qué pueden indexar
de un objeto y sus relaciones, la forma en la que lo hacen produce controversias acerca de cómo recuperar
esta información (ver discusión acerca de los modelos de hidratación en HS y Compass). Esto lo podemos
enunciar en la siguiente proposición:
Proposición 5.
Las capacidades de indexación y recuperación de objetos del motor de búsqueda deben
incluir soporte para:
Subclasicación
Polimorsmo
Composición
Colecciones
Claves complejas
Transformación de atributos
82
CAPÍTULO 2.
ESTADO DEL ARTE
Restricción de resultados por tipo de instancia
El tipo de soporte que debemos dar a cada característica varía dependiendo de si estamos en un contexto
de indexación o recuperación. Como ejemplo, podemos adelantar que reconocer colecciones durante la
indexación nos permite indexar los objetos contenidos en una lista y no la lista en sí misma, mientras
que al recuperar objetos podemos reconocer las listas para generar proxys que utilicen lazy loading.
Estos casos se abordarán en detalle el tratar la propuesta de solución, donde también incluiremos otras
dimensiones como la seguridad.
Al comenzar este apartado dijimos que una de las perspectivas desde la cual analizaríamos el problema
es el mapeo de los objetos de dominio. El término mapeo debe ser utilizado con mucho cuidado ya que
es propenso a confusiones. En el ámbito de los ORM hablamos de mapeos entre objetos y tablas de
un RDBMS. En un motor de búsqueda sobre objetos, este concepto debe tomarse con pinzas ya que
hay una diferencia importante entre introducir un objeto y sus atributos en el índice vs. mapearlos a
tablas. Justamente, en un motor de búsqueda sobre objetos no necesitamos garantizar que el objeto es
representado elmente por el índice, sino que sólo debemos garantizar que éste permitirá eventualmente
obtener su identidad.
Durante el mapeo de objetos en Compass, éste utiliza tres representaciones de los objetos: la entidad
misma, el Resource de Compass y el Document de Lucene. Esto también ocurre para los atributos de una
entidad, los cuales existen como objetos del dominio, objetos Property de Compass y Field de Lucene. Si
bien Compass agrega dos representaciones adicionales de la entidad, a los ojos del usuario sólo se expone
la entidad de dominio y los envoltorios de Compass (Resource y Property).
Por el lado de Hibernate Search, sólo existen las representaciones como entidad del dominio y sus contrapartes de Lucene (Document y Field). Sin embargo, HS facilita el acceso a las estructuras de Lucene,
dando acceso a un trabajo puramente orientado a documentos.
Estas representaciones intermedias entre el dominio y el índice sólo tienen sentido en el contexto de
herramientas que necesitan interactuar con Lucene o indexar fuente como archivos XML o JSON (casos
en los que no estamos interesados). En un motor de búsqueda sobre objetos no hay necesidad de que el
usuario maneje una abstracción adicional a la entidad misma. Establezcamos esto en una proposición:
Proposición 6.
El proceso de indexación y recuperación debe minimizar la presencia de representaciones
no polimórcas de los objetos de dominio.
Procesos de Indexación
Tanto HS como Compass dan soporte a procesos de indexación online, semi-online y oine (ver sección
3.3.4).
En el caso de HS la indexación oine se implementa a través de colas de mensajes JMS, lo cual es una
buena alternativa pero requiere que el contexto de ejecución de la transacción de negocio utilice JMS.
Proposición 7.
El proceso de indexación de objetos debe permitir elegir entre una indexación sincrónica
con la transacción de negocio y una indexación asincrónica. Esta elección es una solución de compromiso
que deberá pesar las eventuales demoras en completar una transacción a causa de la indexación vs. el
retardo de sincronización entre el índice y el RDBMS.
Recuperación, Queries, Matching y Acceso a Datos
Excepto por optimizaciones puntuales como la implementación de subíndices o la reutilización de lectores, HS y Compass delegan la lectura del índice en Lucene. Las optimizaciones son agregados de cada
herramienta y sólo tienen sentido en un contexto de falta de abstracción en cuanto a índices de Lucene.
Idealmente, cualquiera de estas optimizaciones deberían ser parte de cómo el motor de búsqueda resuelve
el almacenamiento de sus índices y no un agregado para resolver deciencias de una librería subyacente.
A la hora de especicar búsquedas, vimos las dos herramientas agregan a Lucene la mínima expresividad
necesaria para referenciar los atributos de los objetos. Las queries se especican en texto plano siguiendo
una nomenclatura híbrida que permite tanto utilizar las operaciones extendidas de Lucene (wildcards,
phrase queries, etc) como referenciar los atributos mapeados a campos de Lucene.
2.4.
83
CASOS DE ESTUDIO
Existen casos en los cuales tanto el documento como la query son ricos en su estructura. Uno de estos
casos es la recuperación de XML (ver
Manning et al., 2008). Para el caso de objetos, la presencia de
estructuras y relaciones es aún mucho más evidente. Teniendo esto en cuenta, una característica que no
está presente en ninguna de las dos herramientas es la capacidad de especicar información de relaciones
entre objetos. Veamos un ejemplo de este caso:
Ejemplo 2.4.5 (Grafo de Objetos como Query).
En este ejemplo especicamos una query en base a un
grafo de personas relacionadas en una red social.
// u s u a r i o s de n u e s t r a r e d de c o n t a c t o s
P e r s o n a U s e r = new P e r s o n ( " Pablo M. López " ) ;
P e r s o n a n o t h e r U s e r = new P e r s o n ( " Pablo S a l a d i l l o " ) ;
P e r s o n a T h i r d U s e r = new P e r s o n ( " J o s é " ) ;
// Creamos e s t e g r a f o de r e l a c i o n e s :
// ( Pablo M. López ) <−> ( Pablo S a l a d i l l o ) <−> ( J o s e )
a U s e r . ad dC on t ac t ( a n o t h e r U s e r ) ;
a n o t h e r U s e r . a d dC on ta ct ( a T h i r d U s e r ) ;
// s a l v a m o s l o s o b j e t o s en e l ORM
s e s s i o n . save ( . . . ) ;
// en o t r a p a r t e creamos una q u e r y en forma de g r a f o p a r a b u s c a r p e r s o n a s a
d i s t a n c i a 2 de un " J o s é "
P e r s o n q u e r y = new P e r s o n ( " j o s e " ) ;
// cramos un f i l t r o que s ó l o p e r m i t e r e t o r n a r o b j e t o s a e x a c t a m e n t e 2
g r a d o s de d i s t a n c i a de l a q u e r y
F i l t e r f i l t e r = new S e c o n d D e g r e e F i l t e r ( ) ;
// hacemos una búsqueda que d e b e r í a r e t o r n a r a " Pablo M. López " y NO a "
Pablo S a l a d i l l o "
C o l l e c t i o n <Person> s e a r c h R e s u l t s = S e a r c h . f i n d ( q u e r y , f i l t e r ) ;
Como vimos al estudiar el modelo vectorial (subsección 2.1.4), éste nos permite recuperar documentos
similares entre sí. Esta característica es conocida como More like this (en adelante MLT). En Compass
se provee un MLT a través de la implementación de Lucene, la cual naturalmente no conoce nada acerca
de objetos por lo que se limita a utilizar el modelo vectorial sobre la unión de todos los campos indexados.
A diferencia de Compass, HS no dispone actualmente
21 de un MLT.
Así como quisiéramos contar con la capacidad de efectuar búsquedas a partir de un objeto teniendo en
cuenta sus relaciones, también deberíamos tener una implementación del MLT que reciba un objeto y
entregue los objetos similares. Consolidamos este análisis en la siguiente proposición:
Proposición 8.
Un motor de búsqueda orientado a objetos debería permitir búsquedas a partir de objetos.
En caso de implementar una funcionalidad del tipo MLT, la misma también debería aceptar objetos.
La implementación de MLT en Compass no puede cumplir con esta proposición porque está basada en
una extensión a Lucene, la cual sólo conoce acerca de documentos.
Como hemos comentado previamente, un aspecto que genera gran controversia entre los dos proyectos es
la hidratación de los objetos recuperados. En Compass el proceso de hidratación se hace desde el índice,
lo que requiere almacenar todos los datos necesarios en éste. Esta solución evita acceder a la base de
datos para obtener los resultados a cambio (a) un índice más grande, (b) asignar al motor de búsqueda
la responsabilidad de resolver la persistencia de los objetos y (c) exponerse al sincronization mismatch si
las tablas se modican mediante SQL. Por el lado de Hibernate Search, la hidratación se hace desde el
ORM, con la penalidad teórica de requerir acceso al RDBMS para resolver las búsquedas.
21 Al
momento de escribir este trabajo, el MLT está en la lista de funcionalidades a implementar en la versión 3.2 de la
herramienta.
84
CAPÍTULO 2.
ESTADO DEL ARTE
Entre las dos posiciones, la hidratación desde el RDBMS es más sensata en cuanto que evita tanto el sincronization mismatch como la reimplementación de la persistencia en el motor de búsqueda. Además, tal
como dependen los autores de HS, la presencia de caches en el ORM puede evitar la necesidad de utilizar
el RDBMS. Otro aspecto que se gana manteniendo la hidratación desde el ORM es la identidad (recordar
que HS garantiza que el operador == retorna true cuando se compara un mismo objeto recuperado desde
el motor de búsqueda y el ORM). En base a esto enunciamos la siguiente proposición:
Proposición 9.
Mientras sea posible, la hidratación de los objetos se debe delegar en el ORM para evitar
el sincronization mismach, aprovechar sus caches y mantener la identidad de los objetos.
Cabe comentar que existen casos donde podemos almacenar datos en el índice para resolver problemas
puntuales como la previsualización del contexto donde hubo coincidencia con la query. Este tipo de
técnicas no deben ser consideradas una hidratación ya que tienen un propósito muy especíco que no
incluye sustituir polimorcamente al objeto indexado.
Modelos de IR y Puntajes
En lo que respecta al modelo de IR y la valoración de los resultados, vimos que tanto HS como Compass
utilizan las fórmulas estándar de Lucene. Al igual que cuando estamos utilizando Lucene, podemos variar
la implementación de similitud. En HS es posible indicar la implementación mediante una anotación a
nivel de clase. Para el caso de Compass, la documentación establece la posibilidad de ajustar las clases
de similitud por separado para la indexación y la recuperación en forma global y no por tipo de instancia
como en HS.
En ninguna de las dos herramientas se resuelve el problema de decidir qué tipo entidad es la que mejor
responde a una necesidad de información sino que se utilizan las fórmulas de similitud y eventualmente
se ltra por tipo de instancia.
Por otro lado, como vimos en la subsección (2.1.6), en ocasiones es necesario poder valorizar los resultados
no solo en base a la relevancia sino a reglas de negocio. Para implementar estas reglas en HS o Compass,
debemos modicar la fórmula de similitud o reordenar los resultados de búsqueda. En cualquier de los
dos casos, necesitamos conocer ciertos detalles avanzados de Lucene, lo que nos induce a la siguiente
proposición:
Proposición 10.
El motor de búsqueda debe permitir incorporar reglas de puntajes y ordenamientos que
excedan la noción de similitud del modelo de IR, dando la exibilidad de ajustar parámetros de negocio
por conguración.
Respecto de técnicas de valoración estructural global como HITS y PageRank, ninguna de las dos herramientas analiza cómo incorporarlas. Dado que estas herramientas son principalmente envoltorios de
Lucene y éste no implementa técnicas como HITS y PageRank, éstas exceden el alcance de HS y Compass. Una posible implementación bajo HS o Compass seguramente requeriría de un cálculo externo a la
herramienta y la consecuente aplicación de impulsos a entidades particulares. Es probable ver la solución
a este tipo de problemas desde Lucene más que desde HS y Compass, los cuales no dedican sus principales
esfuerzos a adaptar la similitud provista por Lucene a la recuperación de objetos.
Analizando la documentación de las herramientas podemos observar que la relevancia no toma un papel
central en ninguna de ellas, quizás asumiendo que Lucene es el encargado de resolver estos problemas. En
caso de que tal suposición exista, entendemos que es errónea ya que Lucene no cuenta con nociones de
recuperación de objetos, por lo que difícilmente ayude a resolver los problemas que excedan la abstracción
de documentos.
Capítulo 3
Desarrollo de la Propuesta de Solución
El objetivo de este capítulo es exponer el diseño de nuestra solución al problema de IR sobre modelos de
dominio, basándonos en el análisis del estado del arte y en las diferentes soluciones posibles al problema.
La sección 3.1 analiza el problema que queremos resolver planteando las distintas variantes de solución,
teniendo en cuenta lo que hemos estudiado en el capítulo anterior acerca del estado del arte. Como es
esperable de una etapa de análisis, el objetivo de esta sección es comprender qué queremos construir.
En la sección 3.2 explicamos la estrategia por la cual el framework de IR puede conocer el modelo de
dominio, de forma de poder indexarlo y recuperar información sobre éste. Esta sección funciona también
como transición del análisis al diseño.
En la sección 3.3 describimos el diseño interno del framework, explicando su arquitectura y la integración
con los distintos ORM y motores de acceso a índices invertidos. Al nalizar esta sección, quedará explicito
cómo construimos este framework.
3.1.
Análisis General del Problema
Luego de haber analizado el estado del arte, estamos en mejor posición de rearmar que la recuperación
de información no es una tarea trivial y que no puede ser resuelta íntegramente a base de consultas a una
base de datos, por lo que necesitamos un software especíco que se ocupe de esto. Esto es, un framework
reusable.
También vimos que las librerías de IR como Lucene efectúan un muy buen trabajo pero que no son
adecuadas para trabajar transparentemente sobre modelos de objetos.
Por último, sabemos que existen muy buenas herramientas como Compass y Hibernate Search, las cuales
llevan años de maduración pero, al estar montados sobre Lucene, exponen el hecho de que no son un
framework único e independiente sino que son capa adaptadora de Lucene. Esta adaptación tampoco es
transparente, ya que vemos expuestos conceptos propios de Lucene y no de un framework de indexación de
objetos. Aún aceptando su modelo mixto, al contrastar las herramientas (subsección 2.4.4) encontramos
que proponen diferentes soluciones a cada problema, lo que nos llevó a enunciar una serie de proposiciones
acerca de cómo debería resolverse el problema de IR sobre objetos.
En las siguientes subsecciones vamos a analizar distintos aspectos del problema y escoger la mejor solución
para nuestro framework.
3.1.1. Modelos de IR
En la subsección (2.1.4) estudiamos tres modelos clásicos de IR: booleano, vectorial y probabilístico.
Como vimos, los distintos modelos de IR son determinantes en cuanto a qué resultados entrega el motor
de búsqueda y cómo los prioriza de cara al usuario.
Vimos que el modelo booleano es muy simple pero todavía sigue siendo aceptado en sistemas donde
el usuario es un experto creador de consultas. Por otro lado, sabemos que el modelo vectorial y el
probabilístico parten de orígenes distintos pero llegan a fórmulas de relevancia similares.
85
86
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Lucene implementa un híbrido entre el modelo booleano y el vectorial (ver subsección 2.4.1), teniendo en
cuenta una noción de zonas y campos (ver subsección 2.1.6).
Para nuestro framework de IR tomamos las siguientes decisiones:
Proposición 11
(Modelo de IR)
.
En esta versión del framework implementaremos tanto el modelo
booleano como el modelo vectorial.
La decisión de implementar el modelo booleano se basa en que (a) su simplicidad nos permite diseñar el
framework comenzando por un problema más simple que si tuviéramos que comenzar por otro modelo
y (b) a pesar de su simpleza es muy utilizado, aún en escenarios reales.
A su vez, la decisión de implementar el modelo vectorial se basa en que (a) la literatura lo respalda como
un avance sobre el modelo booleano y (b) tanto los frameworks que estudiamos en la sección (2.4) como
el resto de la industria lo han adoptado logrando muy buenos resultados.
Dentro de la elección del modelo vectorial, decidimos comenzar con la versión más simple de las fórmulas
TF-IDF e ir complejizándolas a medida que sea necesario.
Es preciso tener en cuenta que respecto del modelo booleano, la implementación del modelo vectorial
supone un esfuerzo extra ya que nos obliga a almacenar y mantener los valores TF-IDF para cada
término y posting.
Si bien decidimos comenzar con una implementación de los modelos booleanos y vectorial, bien podríamos
querer incorporar el modelo probabilístico. En consecuencia, una de las ventajas que queremos incorporar
en nuestro framework es la facilidad de variar entre modelos de IR con un mínimo impacto en la aplicación
huésped (en lo posible, quisiéramos variar el modelo por conguración). Esto es especialmente importante
porque nos permite escoger el modelo más apto para nuestra aplicación en base a los resultados que
produce cada uno.
Esto último nos induce a la siguiente proposición:
Proposición 12.
Permitiremos variar el modelo sin afectar al usuario del framework.
En la próxima subsección continuamos el análisis abordando las técnicas de acceso a datos y la inteligencia
para lograr coincidencia entre términos.
3.1.2. Técnicas de Matching y Acceso a Datos
Registro Maestro e Índices Invertidos
Como sabemos de la subsección (2.1.5) y de los casos de estudio de la sección (2.4), tanto la literatura
como los frameworks actuales señalan que los índices invertidos son la mejor solución disponible para la
tarea de recuperación de información en base a términos.
Una característica innovadora que queremos incorporar en el framework es un registro maestro de objetos
indexados.
El objetivo de este registro es:
evitar reprocesar el texto del objeto para conocer sus postings al momento de actualizar/eliminar
objetos,
no requerir acceso al objeto persistido para que el proceso de reindexación pueda saber qué objetos
están indexados,
conocer en todo momento qué objetos deberían estar en el índice y bajo qué términos a los nes
de resolver eventuales inconsistencias. Esto es, evitar postings fantasma que existen en el índice
pero no en la base de datos (este problema ocurre si se actualiza el objeto persistido mediante una
consulta SQL).
3.1.
ANÁLISIS GENERAL DEL PROBLEMA
87
Respecto de implementar sólo un índice invertido, incorporar un registro maestro trae aparejado un
incremento del espacio necesario en disco o memoria para alojar este registro así como un incremento
en la complejidad de la solución. Haciendo un balance de lo anterior, decidimos que es benecioso hacer
el esfuerzo de incorporar este índice ya que brinda un grado de robustez necesario para aplicaciones del
mundo real.
Además de este registro maestro vamos a construir un índice invertido tradicional, el cual debe estar
sincronizado con el anterior.
La pareja registro maestro-índice invertido debe presentar una interfaz que soporte las siguientes operaciones:
Operaciones sincronizadas entre Índice Invertido y Registro Maestro:
•
creación de nuevos Postings
•
eliminación de Postings
Operaciones del Índice Invertido:
•
leer una Posting List
•
obtener el valor de DF por término (para soportar el cálculo TF-IDF)
•
obtener el tamaño del diccionario de términos (para soportar el cálculo TF-IDF)
Operaciones del Registro Maestro
•
obtener iterador sobre la lista de objetos indexados
•
obtener el número total de objetos indexados (para soportar el cálculo TF-IDF)
Especicando este conjunto de operaciones en una interfaz, podemos componentizar el par registro-índice
en una capa de acceso a datos intercambiable, permitiendo variar la implementación por conguración.
La mayoría de las operaciones anteriores son necesarias para intersectar posting lists (ver subsección
2.1.5) y luego indicarle al usuario o al ORM qué objeto es necesario hidratar.
Respecto de esto surgen dos preguntas:
¾La hidratación debe ser responsabilidad del framework?
¾Cuál es la forma correcta referenciar el objeto indexado de forma de poder hidratarlo?
La primera pregunta la vamos a responder parcialmente en la siguiente proposición para luego completarla
al presentar los plugins de Hibernate e iBATIS (ver subsecciónes 3.3.4 y 3.3.4).
Proposición 13.
La hidratación no es responsabilidad del framework de búsqueda sino que es respons-
abilidad del framework de persistencia. Por lo tanto, nuestro framework sólo mantendrá la información
necesaria para que el framework de persistencia pueda hidratar los objetos luego de ser recuperados.
Esta proposición nos da el pié para responder a la segunda de las preguntas que nos hicimos:
Proposición 14.
Tanto el índice invertido como el registro maestro de objetos mantendrán referencia
de los objetos indexados mediante la combinación de:
una versión serializada del identicador del objeto a indexar y
el nombre completo de la clase a la que pertenece dicho objeto.
A esta combinación la llamaremos clave del objeto.
Esta proposición se justica por las siguientes razones:
la hidratación en los ORM siempre requiere el identicador del objeto y, de una u otra forma, la
clase del objeto a recuperar.
88
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
dado que tanto el identicador original del objeto como el nombre de su clase son serializables, la
clave del objeto es serializable, lo que permite almacenar el índice en un medio persistente.
es independiente del espacio de memoria donde se ejecuta el motor de búsqueda, esto es, los objetos
indexados en una máquina virtual pueden recuperarse en otra.
si el identicador del objeto implementa el método equals, permite aplicar álgebra de Boole sobre
las postings lists (necesario para los modelos de IR que queremos soportar).
En la próxima subsección veremos cómo construir físicamente el índice y el registro para soportar estas
operaciones.
Construcción del Índice
Dado que vamos a soportar el modelo booleano y el vectorial, necesitamos que los índices invertidos
soporten las operaciones básicas de cada modelo:
Modelo Booleano
•
•
obtención de postings lists a partir de un término
operaciones AND, OR y NOT sobre los postings
Modelo Vectorial con TF-IDF (ver subsección 2.1.4)
•
•
•
•
obtención de postings lists a partir de un término
operación OR sobre postings
obtención del tamaño del diccionario de términos
obtención de la cantidad de objetos asociados a un término
Para soportar el modelo booleano nos alcanza con un generar un índice invertido simple, capaz de asociar
términos y claves de objetos. Este índice se visualiza en la gura (3.1).
(A;1)
(B;1) (A;1)
(B;1) (A;2)
Figura 3.1: Índice Invertido para recuperación booleana. En este ejemplo A y B son nombres de clases
y los números son el identicador serializado del objeto.
El modelo vectorial requiere llevar cuenta de la cantidad de objetos indexados, la cantidad de términos
en el diccionario y la cantidad de veces que aparece un término en un objeto. El primer valor se puede
conseguir en el registro maestro de objetos indexados, mientras que el segundo y el tercero se suelen
ubicar en el índice invertido según se muestra en la gura (3.2).
1
(A;1) tf=1
2
(B;1) tf=2 (A;1) tf=1
2
(B;1) tf=1 (A;2) tf=3
tamaño = 3
con TF por posting
Figura 3.2: Índice Invertido para recuperación vectorial. Respecto del caso booleano, agregamos el tamaño
del diccionario, el número de postings por término y la cantidad de apariciones del término en el objeto.
3.1.
89
ANÁLISIS GENERAL DEL PROBLEMA
Si bien este último modelo de índice es suciente a los efectos de la recuperación pura, al explicar
ordenamiento y ltrado veremos que normalmente necesitamos conocer qué atributo del objeto es el que
produjo determinado posting así como será necesario almacenar valores en el índice.
Como vimos en la subsección (2.1.6), en ocasiones tenemos la necesidad de ltrar los resultados y/o
priorizarlos por un criterio de negocio. Para poder hacer esto dentro de los pocos milisegundos que se
espera que tome una consulta, es necesario almacenar el valor de dichos atributos fuera del ORM.
Este último requerimiento nos obliga a retroceder en cuanto a sólo mantener una referencia al objeto
indexado y no superponer nuestras responsabilidades con el ORM. Sin embargo, a falta de una mejor
alternativa debemos aceptar el requerimiento como tal e implementarlo de la mejor forma posible (es
preciso notar que los frameworks que estudiamos en la sección 2.4 resuelven este problema de la misma
forma).
Entonces tenemos que agregar al índice de la gura (3.2) la capacidad de:
indicar TF para cada atributo que contenga el término de la posting list
almacenar los campos que intervengan en ltrado y ordenamiento
Estos cambios se ven aplicados en la gura (3.3):
1
A;1
fieldX.tf=1
fieldY.value=5
2
B;1
fieldA.tf=2
A;1
fieldX.tf=1
fieldY.value=5
2
B;1
fieldA.tf=2
A;2
fieldX.tf=3
fieldY.value=3
tamaño = 3
con TF y Stored Fields por posting
Figura 3.3: Índice Invertido con almacenamiento y TF diferenciado por atributo. Este índice soporta tanto
el modelo booleano como el vectorial, así como los conceptos de zonas y campos y ordenamiento/ltrado
por reglas de negocio.
Si bien es verdad que el almacenamiento de campos superpone roles del ORM con los del framework
de IR, si somos conservadores acerca de almacenar únicamente atributos inmutables, evitamos la desincronización con la base de datos, lo cual reduce el desajuste de sincronización (ver subsección 2.4.1).
Por el lado de las optimizaciones, este último índice invertido puede utilizar todas las técnicas disponibles
en la literatura como front coding, skip pointers y merge generacional ( ver Manning et al., 2008).
Además de las optimizaciones clásicas que podemos encontrar en la literatura, podríamos enviar los atributos almacenados al registro maestro y así tener una única versión de dichos valores, lo cual ahorra espacio
en el índice invertido y reduce la probabilidad de anomalías de actualización. Esta última optimización
puede ser contraproducente en las búsquedas, ya que no podemos resolver la búsqueda con un único
acceso al índice invertido.
Para esta versión del framework decidimos utilizar la opción más simple de alojar los atributos almacenados en los postings. Si en el futuro esto probara ser una oportunidad de mejora, gracias a la componentización de los índices, podríamos pasar estos valores al registro maestro sin mayor impacto.
Procesamiento de Textos
El procesamiento de textos comprende toda la inteligencia para lograr coincidencia entre la consulta del
usuario (query) y los textos presentes en los objetos. Los procesadores de textos deben ser simétricos en
cuanto a la indexación y la recuperación. Es decir, necesitamos que al indexar un objeto se produzcan
los mismos términos que se producirán al analizar la query del usuario.
Tanto la indexación como la interpretación de la query requieren dos pasos: normalización del texto y
delimitación en términos.
90
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Las normalización del texto consiste en conversión a mayúsculas/minúsculas, quitar símbolos, aplicar
stemming ó lematización, corregir ortografía, quitar espacios redundantes, etc.
La estrategia que elegimos para delimitar términos es separar mediante espacios en blanco, comas, puntos
y demás símbolos ortográcos. Esta estrategia es muy efectiva en lenguajes como el español, pero puede
ser inefectiva en otros como el alemán o el chino. También pueden existir problemas en dominios donde
algunas palabras pierdan signicado al ser tratadas por separado. Por ejemplo, la query X-MEN tendría
menor efectividad separando los términos por el guión ya que el primer termino (X) se tomaría como un
stopword y el término restante (MEN) tendría alto recall y baja precisión. Por este tipo de problemas
locales a cada lenguaje y dominio, incluimos la posibilidad de variar el comportamiento de los procesadores
de texto en forma dinámica según el lenguaje y modicar la implementación para soportar excepciones
especícas del dominio.
En la gura (3.4) presentamos un ejemplo simple de normalización y delimitación:
Figura 3.4: El primer paso de normalización convierte las mayúsculas a minúsculas y los caracteres
acentuados a su versión sin tildes. Luego se convierten signos ortográcos a espacios en blanco, eliminando
los espacios redundantes. Por último, se delimita el texto en base a los espacios blancos, en este caso se
obtiene el conjunto
{brutus, conspiro, contra, caesar, su, padre}.
Nuestro framework proveerá un procesador de textos por defecto que aplica eliminación de stop words,
normalización de textos y stemming. El procesador es un componente especialmente sensible al lenguaje,
por lo que nuestra implementación debe ser variable según el lenguaje del texto.
Adicionalmente a la normalización y la delimitación, la interpretación de la query del usuario requiere la
construcción de un árbol de consulta, el cual explicaremos en el próximo apartado.
Consultas Booleanas y Vectoriales
En el apartado previo vimos que la resolución de consultas y la indexación requieren de normalización y
delimitación de términos. Estas dos operaciones son agnósticas respecto del modelo de IR utilizado.
Por otro lado, el procesamiento de consultas sí depende del modelo de IR, ya que en un modelo como
1
el booleano debemos poder utilizar conectores lógicos AND, OR y NOT . Esta característica constituye
diferencia la indexación de texto de la interpretación de consultas, la cual requiere la construcción de un
árbol de consulta.
El árbol de consulta determina el orden jerárquico de recuperación de los términos en el índice así como
las operaciones booleanas a aplicar entre posting lists. A continuación vemos un ejemplo:
Ejemplo 3.1.1 (Árbol de Consulta).
1 Si
Veamos el árbol de consulta para una query en el modelo booleano:
bien adoptamos la convención de uso del término NOT, el término debería ser MINUS ya que estamos substrayendo
elementos entre conjuntos (operación binaria) y no complementándolos (operación unaria).
3.1.
91
ANÁLISIS GENERAL DEL PROBLEMA
coca cola light +OR pepsi diet +NOT zero
NOT
zero
OR
AND
AND
coca cola light
pepsi diet
En este ejemplo los rectángulos simbolizan las posting lists resultantes de cada operación y los operadores
lógicos se simbolizan con un
+
por delante del conector.
En primer lugar tenemos una intersección entre los postings de las palabras
lugar entre las de
pepsi diet.
coca cola light
y en segundo
Luego de estas dos intersecciones, se efectúa una unión entre los resultados
de cada sub rama del árbol y por último se eliminan todos los elementos presentes en la posting lists de
zero.
En el ejemplo anterior se evidenció que existen algunos estándares de facto respecto de cómo interpretar
lógicamente una consulta:
la consulta se interpreta según el sentido de lectura del idioma del usuario. Esto es, de izquierda a
derecha para inglés o español y de derecha a izquierda para hebreo o árabe,
en el modelo booleano, ante la ausencia de un conector lógico entre términos se asume un AND
booleano,
en el modelo vectorial los términos están conectados implícitamente por un OR booleano.
Además de los estándares de cada modelo, el árbol de consulta varía al momento de su ejecución en dos
aspectos:
el tipo de resultados que entrega cada modelo puede diferir ya que el modelo booleano sólo retorna
claves de objeto, mientras que el vectorial además retorna un valor de similitud para cada resultado,
en el caso vectorial necesitamos calcular valores de TF-IDF sobre los objetos recuperados.
Resumiendo lo que vimos en este apartado:
el motor de búsqueda interpreta las consultas según convenciones adecuadas de cada modelo de IR,
la interpretación produce como resultado un árbol de consulta.
En el próximo apartado utilizaremos estos conceptos para generar un método de consulta a partir de
objetos.
92
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Consultas de Objetos
Una característica original incluida en nuestro framework es la posibilidad de especicar consultas a partir
de objetos.
Al contar con un objeto como elemento de consulta, podemos aprovechar la inteligencia de mapeos (ver
sección 3.2) para generar una consulta simétrica al objeto indexado. Esta característica propone salir de
un esquema clásico en el cual la consulta es una frase para pasar a un esquema donde la query y el corpus
son de un mismo tipo (objetos). Veamos esto aplicado a un ejemplo:
Ejemplo 3.1.2.
Supongamos un sitio web en el cual queremos crear un buscador de opiniones acerca de
autos.
El siguiente fragmento de código muestra una clase que representa una opinión particular sobre un automóvil:
public c l a s s
CarOpinion {
// e s t o s a t r i b u t o s s e n o r m a l i z a n p e r o no s e l e s a p l i c a n stemming n i
e l i m i n a c i ó n de s t o p words
S t r i n g brand , model ;
// s o b r e e s t o s a t r i b u t o s a p l i c a m o s n o r m a l i z a c i ó n , e l i m i n a c i ó n de s t o p
words y stemming según e l l e n g u a j e de l a a p l i c a c i ó n
String subject , opinion ;
// s e almacena p a r a p e r m i t i r f i l t r a r l o s r e s u l t a d o s
int s t a r s ;
}
// i d e n t i f i c a d o r d e l o b j e t o
long o p i n i o n I d ;
Entonces el usuario ingresa algunos valores a partir de los cuales quiere buscar opiniones:
Car Reviews
Ópticas Delanteras
Buscar
Quiero refinar mi búsqueda:
Marca
fiat
Modelo
brava
Sólo opiniones con calificación mínima:
Tema
En base a estos valores, podemos generar un objeto de tipo
CarOpinion
y proveerlo al motor de búsqueda
para que retorne opiniones como las que buscamos.
Veamos que si utilizamos el mismo tipo de objetos en la búsqueda e indexación, el motor de búsqueda puede
Ópticas Delanteras en óptica delantera sólo
model, el cual resultaría en un overstemming entre
marca Fiat), empeorando la precisión de la respuesta.
aplicar selectivamente el procesamiento de textos y normalizar
para un atributo, evitando el stemming del atributo
los modelos
brava
y
bravo
(modelos existentes de la
La capacidad de proveer objetos como consulta nos abre las puertas a una implementación sencilla de
la función More Like This que discutimos en la subsección (2.4.4). Para implementar el MLT, sólo
3.1.
ANÁLISIS GENERAL DEL PROBLEMA
93
necesitamos proveer el objeto resultante de una búsqueda, y el motor de búsqueda buscará los objetos de
mayor similitud en términos de IR.
La recuperación a partir de objetos no es un modelo completo en sí mismo sino que se monta sobre el
modelo vectorial. Esto es, la similitud entre objetos se realiza en base a fórmulas TF-IDF.
3.1.3. Procesos de Indexación
Las tareas de recuperación que estamos describiendo requieren la capacidad de indexar objetos en el
índice invertido. En los apartados previos discutimos la estategia para llevar adelante esta indexación,
siendo que nos resta analizar la dinámica con la cual esto se llevará a cabo.
En esta subsección veremos tres dimensiones en las que una aplicación puede variar sus procesos de
indexación, explicando cómo vamos a dar soporte desde el framework a cada caso de uso.
Tipos de Indexación
En cuanto al ujo de objetos hacia el indexador, podemos trabajar en dos formas distintas:
Indexación Automática o por Eventos: consiste en utilizar un plugin del framework de IR (ver
último apartado) que interconecta los eventos CUD del ORM con el proceso indexador. Este tipo de
indexación es la recomendada ya que transparenta el uso del framework y nos garantiza consistencia
entre el almacén de datos y los índices.
Indexación Proactiva: esta es una opción de más bajo nivel en la cual el usuario se ocupa de inyectar
los objetos al indexador para que este aplique los cambios sobre los índices. Esta variante es más
versátil que la anterior ya que ya que puede utilizarse en ausencia de un ORM y puede combinarse
con la indexación automática a efectos de reindexar manualmente un conjunto de objetos.
Nuestro framework permite tanto la indexación por eventos como la proactiva. Para el caso de la indexación automática, proveemos plugins que reejan los eventos de los ORM Hibernate e iBATIS. En el
último apartado retomaremos la explicación acerca de la funcionalidad de plugins.
La segunda dimensión en la que podemos variar nuestro proceso indexador tiene que ver con el grado de
sincronismo de la indexación:
Indexación Online: se ejecuta sincronicamente con la transacción de negocio, no deja que ésta
concluya hasta haber terminado la indexación del grupo de objetos implicados en la transacción.
Indexación Semi-Online: ocurre en la misma máquina virtual que desarrolla las transacciones de
negocio, pero en forma asincrónica a ésta. A su vez puede ser mono o multi hilo.
Indexación Oine: el proceso indexador se ejecuta en forma asincronica en una máquina virtual
distinta a la transacción de negocio.
En los próximos apartados damos algunos detalles más de estos tres modos de indexación.
La tercera y última dimensión involucra la distribución del indexador:
Indexación Centralizada: la indexación se efectúa en un único nodo.
Indexación Distribuida: la indexación se efectúa en distintos nodos al mismo tiempo.
En los siguientes apartados tratamos cada tipo de indexación desde el punto de vista del sincronismo y
explicamos cómo se ven afectadas por el resto de las dimensiones.
94
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Indexación Online
En esta modalidad el framework de IR aplica el procesamiento de objetos y los cambios sobre índices
desde el mismo hilo que invoca la indexación, ya sea por eventos o proactivo (ver gura 3.5).
: objetos a indexar
Figura 3.5: Indexación Online. La aplicación efectúa operaciones transaccionales que se ejecutan sincrónicamente con la indexación de objetos.
Es preciso notar que la indexación online no excluye la posibilidad de que varios hilos indexen un mismo
objeto concurrentemente, por lo que debemos resguardar el índice de operaciones inconsistentes. En un
apartado posterior profundizaremos acerca de la concurrencia y distribución.
La indexación online es la modalidad más simple, sobre todo cuando la indexación es centralizada. Esta
modalidad está dirigida a aplicaciones que pueden esperar a que se indexe un objeto antes de completar
la transacción de negocio.
Indexación Semi-Online
En este caso el framework encola las operaciones de indexación y las asigna a hilos trabajadores o
workers. Estos workers se comportan como indexadores online paralelos que se ejecutan fuera del entorno
de la transacción de negocio (ver gura 3.6).
3.1.
95
ANÁLISIS GENERAL DEL PROBLEMA
: objetos a indexar
: cola de mensajes
Figura 3.6: Indexación Semi-Online. Los hilos de ejecución de la aplicación y el ORM están desacoplados
de
En un escenario de tolerancia a fallas, este método de indexación se complejiza porque el sistema puede
fallar luego de que la transacción de negocio ha sido conrmada pero antes que el indexador complete su
trabajo. Para resolver este problema, el indexador debería mantener un log de transacciones similar al de
un RDBMS, de forma tal de poder recuperarse ante un fallo inesperado.
Indexación Oine
La indexación oine consiste en encolar mensajes de indexación persistentes en una máquina virtual
y aplicarlos a los índices en forma asincrónica en otra máquina virtual. El o los procesos indexadores
deberán desencolar los mensajes, procesar los objetos y aplicar los cambios en los índices. Este esquema
se presenta en la gura (3.7):
96
CAPÍTULO 3.
: objetos a indexar
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
: cola de mensajes
Figura 3.7: Indexación Oine. La máquina virtual en la que ejecuta la aplicación persiste los datos en su
almacén de datos y encola mensajes de indexación. En forma asincrónica, la máquina virtual indexadora
desencola los mensajes, procesa los objetos y aplica los cambios sobre el índice.
Esta indexación oine requiere la utilización de un sistema de mensajería, el cual debe ser asincrónico
y puede o no ser persistente. Es importante que el sistema de mensajería preserve el orden en el que se
producen los eventos de indexación, de otro modo nos arriesgamos a que una ejecución fuera de orden
produzca inconsistencias en los índices. Por otro lado, los mensajes de indexación se pueden generar tanto
por eventos como en forma proactiva.
Los problemas como el ruteo, concurrencia y orden de mensajes son especialmente complejos en un
entorno distribuido, por lo delegaremos la implementación a un framework o librería que implemente el
estándar de mensajería JMS de Java. La indexación oine distribuida se trata en el siguiente apartado.
Indexadores Concurrentes y Distribuidos
Tanto las indexaciones concurrente como distribuida presentan algunas sutilezas que debemos mencionar.
La indexación concurrente y la distribuida pueden generar la ejecución de los eventos de indexación en
un orden distinto al que se da en el ORM. Por ejemplo, si las operaciones de indexación y desindexación
de un objeto invierten su orden, al nalizar la ejecución los datos no habrán sido eliminados del índice.
En este escenario, podemos decir que el framework de IR es responsable de aplicar los cambios en el
mismo orden que le son indicados, asumiendo que el ORM o la aplicación proveen este orden en forma
3.1.
97
ANÁLISIS GENERAL DEL PROBLEMA
correcta.
La combinación de un indexador concurrente y uno distribuido nos dan cuatro escenarios donde indexar
objetos:
Indexador Concurrente
Indexador Distribuido
Problemas de Aislamiento
Problemas de Orden
no
no
no
no
no
si
no
si
si
no
si
si
si
si
si
si
Los problemas de aislamiento ocurren cuando un objeto se indexa en simultaneo desde más de un hilo,
de forma tal que los cambios producidos por uno de ellos se hace visible al otro antes que el primero
complete su ejecución. Esto da lugar a problemas clásicos de los RDBMS como las lecturas fantasma
(este es el caso en el que un hilo ve como válido un dato temporal que sólo existió durante el transcurso
de otra transacción).
La resolución de los problemas de aislamiento los delegamos en la capa de acceso a datos. Es decir, es
responsabilidad de quien provee el sistema de almacenamiento de índices que una escritura no se haga
visible hasta tanto se conrme la operación.
Los problemas de orden se dan cuando la aplicación ejecuta las transacciones
pero nuestro framework aplica los cambios en el orden
cuando el hilo
H1
T2 → T1 .
se demora procesando los objetos de la transacción
procesa e indexa los objetos de la transacción
T2
antes que
H1
T1
y
T2
en orden
T1 →T2
En el caso centralizado, esto ocurre
T1 ,
de forma tal que el hilo
H2
termine. En el caso distribuido, además
puede ocurrir que nuestro framework demore el encolado en un nodo, de forma que el indexador recibe
las operaciones en un orden distinto al especicado por el ORM.
Este último problema es especialmente complejo, pero tiene algunas soluciones posibles:
la solución más simple es asegurar aplicativamente que un mismo objeto no es operado desde nodos
distintos, o en términos más generales, que las operaciones de indexación son conmutativas entre
nodos (no así dentro de un mismo nodo, donde debemos dar garantías FIFO). Si podemos generar
este escenario, se eliminan los problemas de ordenamiento.
para el caso centralizado-concurrente, podemos establecer una cola FIFO que nos asegure un desencolado en el orden especicado por el ORM, mas un sistema de bloqueos que prevenga a un hilo
de indexar objetos en conicto con otro. Por ejemplo, si el hilo
comenzar, cuando
H2
H1
intenta operar sobre un objeto compartido con
bloquea los objetos antes de
H1
deberá esperar a que éste
nalice su trabajo, evitando la ejecución fuera de orden.
para los casos distribuidos, debemos implementar algoritmos distribuidos que nos permitan simular
(a) la cola FIFO del caso anterior y (b) el bloqueo de objetos.
Los detalles de implementación de este escenario son problemas complejos típicos de sistemas distribuidos, por lo que exceden el análisis que queremos dar al caso. Sin embargo, es preciso notar que
podemos evitar algunos problemas si contamos con una herramienta centralizada como un RDBMS,
el cual nos puede facilitar la generación de números secuenciales y el bloqueo de objetos mediante
tablas de bloqueos.
En aplicaciones en las cuales los objetos son relativamente simples, podemos paralelizar la indexación
simplemente partiendo el espacio de claves del objeto en particiones disjuntas e indexando cada lote por
separado. En estos casos, los dos problemas anteriores desaparecen.
En general los escenarios donde tenemos que indexar objetos suelen ser más benignos y no es necesario
considerar la indexación de objetos cruzados entre nodos o al menos no al mismo tiempo.
Reindexación
Si los objetos de la aplicación se actualizan por un circuito externo al aplicativo (por ejemplo, mediante
consultas SQL), el índice tenderá a divergir respecto del contenido real de los objetos.
Este problema se puede resolver utilizando un proceso reindexador.
98
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Para reindexar un objeto es necesario (a) eliminar los postings del objeto previo y (b) indexar los nuevos
postings.
Para eliminar los postings del objeto anterior entra en juego el registro maestro de objetos, el cual conoce
qué términos generó el objeto originalmente indexado. Utilizando el registro maestro podemos ubicar los
términos viejos y eliminarlos.
Por otro lado, el reindexador necesita saber qué objetos hay que reindexar, lo cual debe quedar a cargo
de la aplicación o quien parametrice dicho proceso.
En el capítulo 4 veremos que no es necesario proveer un reindexador genérico ya que el código ligado al
motor de búsqueda es trivial.
Plugins
Como explicamos al principio de esta subsección, tenemos la intención de componentizar algunas partes
del framework, de forma de poder cambiar su implementación por conguración sin impacto en el código
de la aplicación huésped. Estos componentes serán llamados plugins.
En cuanto al acceso a datos y la indexación, tenemos tres tipos de plugins:
Plugins de Indexación por Eventos desde ORM: se conecta con el mecanismo de eventos del ORM
y adapta su interfaz para reejar los eventos CUD en los índices.
Plugins de Backend de Acceso a Índices: implementan el acceso físico a los índices invertidos y al
registro maestro.
Plugins de Indexación Oine: conecta el framework de IR con el proveedor del servicio de colas de
mensajes.
Los plugins se pueden congurar y asocian a las clases que los requieren por medio de inyección de
dependencias. En el capítulo 4 retomaremos este tema mostrando cómo inyectamos los plugins en una
aplicación editando archivos de conguración.
3.1.4. Técnicas de Puntaje y Relevancia
Hasta aquí hemos analizado qué modelos de IR se adecúan al problema que estamos resolviendo, qué
índices debemos construir y qué procesamiento debemos efectuar sobre los textos. Dado que hemos
sentado las bases para indexar y recuperar objetos, en esta subsección vamos a explicar cómo priorizar y
ltrar estos objetos recuperados por una query.
Las técnicas de puntajes las abordaremos desde el punto de vista de la similitud de cada modelo de IR y
desde el ordenamiento por reglas de negocio, dependientes de cada aplicación.
Además de priorizar los resultados, también necesitaremos aplicarles clasicación y ltrado.
La clasicación consiste en un proceso opcional de separación de los resultados en clases, lo cual abre las
puertas a la búsqueda facetada (ver subsección 2.1.5).
El ltrado se utiliza para eliminar de la respuesta los resultados que no cumplen con una condición dada
al momento de la búsqueda. Esta funcionalidad se puede utilizar para tareas de seguridad (ltrar objetos
a los que una persona no tiene acceso), para implementar reglas de negocio (por ejemplo eliminando de
la respuesta objetos de cierta antigüedad) o para implementar la búsqueda facetada.
Notemos que tanto la clasicación como el ltrado responden a necesidades particulares de negocio. Es
decir, en general no son herramientas portables entre dominios como sí lo pueden ser los modelos de IR.
En los próximos apartados analizamos cómo vamos a incorporar las nociones de similitud, clasicación y
ltrado.
3.1.
99
ANÁLISIS GENERAL DEL PROBLEMA
Similitud
Para soportar el modelo vectorial vamos a implementar una fórmula de similitud del tipo
TF-IDF. Las fórmulas TF-IDF que aplicaremos tendrán la forma general:
!
Similitud (q, o) =
X
idft ×
∀t∈q
X
tft,a
(3.1.1)
∀a∈o
donde:
q
es una query compuesta de términos
t
o
es el objeto que está siendo evaluado y
a
son sus atributos
Si bien esta fórmula es relativamente simple, es buen punto para comenzar y luego ajustarla o reemplazarla
según el problema que estemos resolviendo.
Es preciso notar que esta sencilla fórmula requiere bastante información:
idft :
necesita el número de postings para
tft,a :
t
y la cantidad total de objetos indexados
requiere saber cuántas ocurrencias se dieron para cada término en cada atributo
Notemos que la ecuación de similitud diere de la fórmula de Lucene (ecuación 2.4.1) principalmente en
factores de normalización y en el valor de impulso que Lucene aplica a cada campo.
Nuestra propuesta de implementar esta fórmula simple se debe a que queremos evitar una sobre ingeniería
en la fórmula de similitud desde el comienzo para todos los dominios. Dado que la similitud tiene menor
prelación que los ordenamientos por reglas duras y blandas, una sobre ingeniería puede perjudicar el
rendimiento y quedaría oculta detrás de reglas de negocio de mayor prioridad. Sin embargo, vamos a
aseguremos que el diseño del framework permita extender esta fórmula para el caso en el que muestre ser
inefectiva.
En el capítulo 4 ejemplicaremos el uso de esta fórmula sobre tres dominios distintos, donde analizaremos
en detalle su desempeño.
Ordenamiento y Reglas de Negocio
Como vimos en la subsección (2.1.6), el éxito de un sistema
de IR excede a la relevancia provista por el modelo de IR.
Mientras el modelo de IR se ocupa de dar una solución global al problema de la relevancia, nosotros
debemos permitir implementar una solución local a cada aplicación mediante reglas duras y blandas (ver
subsección 2.1.6).
Nuestro framework soporta la inclusión de ordenamiento por reglas de negocio previo al retorno de los
objetos recuperados.
El hecho de permitir un ordenamiento previo a la entrega de los resultados paginados es distintivo del
framework y es importante ya que nos permite aplicar ordenamiento antes de hidratar los objetos (lo cual
puede ser prohibitivo si una búsqueda retorna cientos de miles de objetos).
Filtrado
El ltrado se utiliza cuando queremos implementar algún tipo de búsqueda facetada o sim-
plemente eliminar objetos que no cumplan con ciertas condiciones (de seguridad, de antigüedad, etc.)
El framework provee una implementación de algunos ltros básicos como el ltrado por tipo de objeto
(clase) y antigüedad de un atributo. Mas allá de estos casos puntuales, la actividad de ltrado responde
siempre a un requerimiento de negocio, por lo que es responsabilidad del usuario del framework proveer
los ltros.
La etapa de ltrado recorre todos los objetos retornados desde los índices, permitiendo la clasicación
de los resultados. Esta clasicación bien puede contar los objetos que cumplen con ciertos criterios y así
implementar búsquedas facetadas (ver subsección 2.1.5).
La actividad de ltrado necesita recorrer completamente la lista de resultados, por lo que se ejecuta en
tiempo O (n). Dado que
O (n log n), para reducir
el ordenamiento de los resultados se ejecuta en el mejor de los casos en tiempo
el tiempo total de ejecución, el ltrado debe preceder al ordenamiento.
100
CAPÍTULO 3.
3.2.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Mapeo de Clases
3.2.1. Introducción
En la sección (3.1) recorrimos los problemas que componen un framework como el que queremos desarrollar, investigamos las variantes de implementación y efectuamos propuestas concretas acerca de cómo
resolver el problema.
En esta sección haremos una transición del análisis al diseño abordando los siguientes problemas:
cómo indicar al framework de IR las decisiones que tomamos en cuanto a cómo queremos indexar
y recuperar los objetos,
cómo procesar los objetos de forma que éstos se conviertan desde y hacia las posting lists.
Comencemos a resolver estos problemas con una analogía:
La mayoría de los frameworks de persistencia automática requieren que el programador indique cómo debe
ser mapeo entre el modelo de objetos y al RDBMS. Mas allá de cuan inteligente sea el framework de
persistencia, el framework desconoce la semántica de cada atributo (por ejemplo, para saber qué campos
deben ser persistentes o claves) y existen muchas formas de mapear un conjunto de clases.
Análogamente, un framework de IR sobre objetos tampoco puede deducir qué clases deben indexarse o
qué atributos deben utilizarse para la recuperación.
Para resolver este problema, es necesario idear un sistema de mapeos de clases
2 que produzca directivas
3
de indexación tal que el framework pueda obtener información semántica del dominio.
Este mapeo de clases debe especicar qué tipos de objetos deben ingresar al framework de IR para ser
indexados y recuperados. Estos tipos son llamados clases indexables.
Además, para poder recuperar los objetos desde los índices invertidos e hidratar los resultados, necesitamos conocer qué atributo identica unívocamente al objeto. Estos son los identicadores de objeto.
Por último, es necesario especicar qué parte del objeto contiene la información a indexar. Estos son los
atributos indexables.
Si bien veremos que hay otras directivas más complejas, las directivas básicas deben determinar (a) cuáles
son las clases indexables, (b) los identicadores de objeto y (c) los atributos indexables.
A continuación explicamos cómo se conguran los mapeos en el framework de IR para luego explicar
mapeos más avanzados.
3.2.2. Conguración y Mapeo
El conjunto de directivas de indexación de todas las clases indexables constituyen lo que llamamos la
conguración del framework.
Enumeremos algunas propiedades de la conguración:
1. Para efectuar cualquier acción de indexación o recuperación debe existir una conguración activa.
2. En todo momento existe a lo sumo una conguración activa (puede no haber ninguna).
3. Las conguraciones son modicables durante el ciclo de vida de la aplicación sin requerir reinicios
de la misma.
4. Se pueden generar programaticamente o a partir de anotaciones Java.
La conguración es un punto centralizado del framework, por lo cual lo implementamos en la clase
SearchConguration siguiendo el patrón Singleton (Gamma et al., 1995).
2 En
rigor, el término mapeo no debería ser usado ya que suele referir a un isomorsmo entre un modelo de objetos y un
modelo distinto como el relacional, lo cual veremos que no sucederá en este caso.
3 Si
bien estas directivas se suelen utilizar simétricamente en la indexación y la recuperación, por simplicidad hablamos
sólo de directivas de indexación implicando también la recuperabilidad de los objetos.
3.2.
101
MAPEO DE CLASES
Mapeo Programático
Como explicamos anteriormente, el motor de búsqueda se puede congurar programaticamente o mediante
anotaciones. Para el caso programático, el programador dene los mapeos invocando los métodos de la
clase SearchConguration.
Ejemplo 3.2.1
.
(Conguración y Mapeo Programático)
El siguiente fragmento de código muestra el
mapeo programático de una clase y un atributo mediante un test JUnit.
1
2
3
4
5
6
private c l a s s D u m m y E n t i t y W i t h A t t r i b u t e
private O b j e c t a t t r i b u t e ;
}
@Test
public void
testMappedAttributeIsMapped () {
// creamos una nueva c o n f i g u r a c i o n y obtenemos una r e f e r e n c i a a e l l a
SearchConfiguration c o n f i g u r a t i o n = SearchEngine . getInstance () .
newConfiguration () ;
7
8
9
// l a e n t i d a d a mapear
D u m m y E n t i t y W i t h A t t r i b u t e dummyWithAttributes =
DummyEntityWithAttribute ( ) ;
10
11
12
new
// g e t A t t r i b u t e F i e l d e x t r a e e l campo ' a t t r i b u t e ' de una c l a s e c u a l q u i e r a
F i e l d a t t r i b u t e F i e l d = g e t A t t r i b u t e F i e l d ( dummyWithAttributes ) ;
13
14
15
// agregamos un mapeo p a r a l a c l a s e y e l a t r i b u t o
c o n f i g u r a t i o n . addEmptyMapping ( dummyWithAttributes . g e t C l a s s ( ) ) . put (
a t t r i b u t e F i e l d , new M a p p e d F i e l d D e s c r i p t o r ( a t t r i b u t e F i e l d ) ) ;
16
17
18
19
20
21
{
}
// v e r i f i c a m o s que e l a t r i b u t o ha s i d o mapeado
A s s e r t . a s s e r t T r u e ( c o n f i g u r a t i o n . isMapped ( dummyWithAttributes . g e t C l a s s ( ) ,
attributeField )) ;
Analicemos las líneas más importantes de este fragmento de código:
El primer paso para iniciar el trabajo se da en la línea 8 con la generación de una nueva conguración.
En las líneas 11 y 14 extraemos el atributo
attribute
de la clase
DummyEntityWithAttributes,
el cual se
mapea programaticamente en la línea 17.
Por último, la línea 20 verica que el mapeo se haya efectuado.
En el ejemplo anterior podemos intuir que la conguración programática es una solución relativamente
bajo nivel. En la próxima subsección veremos una conguración de más alto nivel a partir de anotaciones.
Mapeo desde Anotaciones Java
Para congurar el framework mediante anotaciones contamos con la clase AnnotationCongurationMapper.
Esta clase inspecciona la estructura de la clase en busca de anotaciones propias del motor de búsqueda e
internamente utiliza la interfaz programática para generar el mapeo. Veamos un ejemplo de la conguración por anotaciones:
Ejemplo 3.2.2.
Veriquemos que una clase anotada como indexable fue mapeada como tal utilizando
un test JUnit.
1
2
3
4
@Indexable
private c l a s s
@Test
DummyIndexable {}
102
5
t e s t I n d e x a b l e O b j e c t I s M a p p e d ( ) throws
SearchEngineMappingException {
// e l i m i n a m o s c u a l q u i e r c o n f i g u r a c i ó n p r e v i a y generamos una nueva
SearchEngine . getInstance () . r e s e t () ;
7
8
// e l o b j e t o a i n d e x a r , cuya c l a s e queremos mapear
DummyIndexable dummyIndexable = new DummyIndexable ( ) ;
9
10
11
// creamos una nueva c o n f i g u r a c i ó n y obtenemos una r e f e r e n c i a a e l l a
SearchConfiguration c u r r e n t C o n f i g u r a t i o n = SearchEngine . getInstance () .
newConfiguration () ;
12
13
14
// creamos e l mapeador de a n o t a c i o n e s y l e pedimos que
A n n o t a t i o n C o n f i g u r a t i o n M a p p e r mapper = new A n n o t a t i o n C o n f i g u r a t i o n M a p p e r
() ;
15
16
17
// e s t e método e f e c t ú a t o d o s l o s mapeos n e c e s a r i o s
mapper . map( o b j e c t ) ;
18
19
20
// v e r i f i c a m o s que l a c l a s e f u e mapeada y que e s i n d e x a b l e ( r e c o r d a r que
una c o s a no i m p l i c a l a o t r a ! )
A s s e r t . a s s e r t T r u e ( c u r r e n t C o n f i g u r a t i o n . isMapped ( dummyIndexable . g e t C l a s s ( )
));
A s s e r t . a s s e r t T r u e ( c u r r e n t C o n f i g u r a t i o n . getMapping ( dummyIndexable . g e t C l a s s
() ) . isIndexable () ) ;
21
22
23
25
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
public void
6
24
CAPÍTULO 3.
}
Nuevamente hagamos un análisis de las líneas más importantes:
La línea 1 comienza con la anotación
@Indexable,
la cual le indica al framework que los objetos de este
tipo deben ser considerados en la indexación.
La línea 7 renueva la conguración, de forma de eliminar mapeos previos, siendo que la línea 13 obtiene
una instancia de la conguración.
En la línea 16 creamos un
AnnotationCongurationMapper, quien será el encargado de leer las anotaciones
de los objetos e internamente generar la conguración programática. Esto se materializa en la línea 19
con la ejecución del método
map.
Por último, las líneas 22 y 23 verican que efectivamente hayamos mapeado la clase como indexable.
Como explicamos antes, dado que el mapeo desde anotaciones utiliza internamente la conguración programática, el poder de expresividad de las anotaciones es equivalente al anterior (aunque mucho más
sencillo).
Si bien ya hemos visto cómo indicar que una clase es indexable, para constituir la clave del objeto necesitamos indicar qué atributo lo identica dentro de la jerarquía de clases. Este atributo se especica
anotándolo con @SearchId. Utilizando en conjunto las anotaciones @Indexable y @SearchId podemos construir la clave del objeto, la cual será necesaria para identicar unívocamente el objeto recuperado.
La última anotación básica que nos resta presentar es la que nos indica qué atributos deben utilizarse
para indexar el objeto. Esta anotación es @SearchField. Veamos un ejemplo en el que combinamos estas
tres anotaciones para efectuar un mapeo básico:
Ejemplo 3.2.3.
@SearchField
1
2
3
4
En el siguiente fragmento de código aplicamos las anotaciones
para indexar una clase de dominio:
@Indexable
private c l a s s
@SearchId
private long
Article {
i d = 1L ;
@Indexable, @SearchId
y
3.2.
5
6
7
8
}
103
MAPEO DE CLASES
@SearchField
String
private
abstract
= " In recent years , advances in the f i e l d of . . . " ;
En las próximas secciones veremos capacidades avanzadas de nuestro framework que nos permitirán
trabajar con objetos y jerarquías complejas.
3.2.3. Mapeos Avanzados
Los usos más simples del framework requieren mapear una clase como indexable, su identicador y los
campos desde los que extraer información. Sin embargo, en los próximos apartados consideraremos mapeos
avanzados que nos permitirán trabajar con jerarquías de herencia, asociaciones de objetos, indexar objetos
en colecciones y muchos otros casos de uso.
Jerarquías de Herencia
En dominios que utilizan subclasicación debemos considerar los siguientes casos:
un objeto debe poder ser indexable por pertenencia a una jerarquía de objetos indexables,
los atributos de un objeto indexable deben poder repartirse a lo largo de toda la jerarquía de
herencia sin requerir que todas las clases en dicha jerarquía sean indexables,
en una jerarquía indexable, ciertos miembros de la jerarquía pueden querer ser excluidos de la árbol
jerarquía de clases indexable.
Ejemplo 3.2.4
.
(Mapeo en Cascada y Herencia de Atributos)
ChildWithCascadeParent
En este fragmento de código, la clase
hereda de su superclase la capacidad de ser indexado, así como sus atributos de
indexación.
@ I n d e x a b l e ( m a k e S u b c l a s s e s I n d e x a b l e = true )
private c l a s s SuperWithCascade {
@ S e a r c h I d public int i d ;
@ S e a r c h F i e l d public S t r i n g someValue ;
}
private c l a s s
Ejemplo 3.2.5.
ChildWithCascadeParent
extends
SuperWithCascade { }
En el siguiente fragmento de código, un objeto hereda atributos de su superclase, por
más que esta última no es indexable:
private c l a s s N o t I n d e x a b l e W i t h I d {
@ S e a r c h I d public int i d = 1 0 0 0 ;
}
@Indexable ( climbingTarget = NotIndexableWithId . class )
private c l a s s I n d e x a b l e W i t h I n h e r i t e d I d extends N o t I n d e x a b l e W i t h I d {
public S t r i n g a t t r i b u t e = " s o m e t h i n g " ;
}
Ejemplo 3.2.6.
Este fragmento de código muestra el caso en el cual una clase propaga su condición de
indexable sólo en una rama de la jerarquía de herencia:
@Indexable ( makeSubclassesIndexable =
private c l a s s SuperWithCascade {
@ S e a r c h I d public int i d ;
}
true )
104
CAPÍTULO 3.
@NotIndexable
private c l a s s
NotIndexableLeaf
private c l a s s
IndexableLeaf
}
}
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
extends
extends
SuperWithCascade {
SuperWithCascade {
Si bien el mapeo de jerarquías es inherentemente complejo, esta herramienta nos da el poder de expresividad necesario para controlar la indexación de dominios con uso extensivo de subclasicación.
En el próximo apartado tratamos el caso de objetos referenciados entre sí.
Asociación de Objetos
Existen una serie de casos en los cuales el motor de búsqueda debe conocer las relaciones entre objetos,
de forma de indexar indirectamente un objeto a partir de sus asociaciones.
Las asociaciones entre objetos pueden requerir indexación en los siguientes casos:
los objetos de una colección deben poder ser indexados a partir del objeto que tiene la referencia a
la colección. Ejemplo: relación entre un objeto autor y una lista de objetos libro correspondiente
a sus obras.
si un objeto (indexable o no) mantiene una referencia a un objeto indexable, debemos poder indexar
este segundo objeto. Ejemplo: un objeto producto referenciado por un objeto orden de compra.
en los casos previos debemos permitir que el objeto indexado referencie a su contenedor, a él mismo
o ambos.
Ejemplo 3.2.7
.
(Mapeo de Asociaciones)
Veamos cómo se indican algunas relaciones de asociación
mediante anotaciones:
@IndexableContainer
DummyContainter {
@ S e a r c h C o l l e c t i o n ( r e f e r e n c e=I n d e x R e f e r e n c e . SELF ) L i s t <DummyContained>
o b j e c t L i s t = new A r r a y L i s t <DummyContained >() ;
}
public c l a s s
En el fragmento de código anterior, la anotación
@IndexableContainer
indica al motor de búsqueda que si
bien la entidad no es indexable, debe ser inspeccionada porque contiene elementos indexables.
@SearchCollection indica que los objetos de la colección deben ser analizados para indexarlos
reference indica que cualquier posting resultante de este análisis debe hacer referencia al
tipo DummyContained y no a DummyContainer. El parámetro reference se puede variar para que
La anotación
y el parámetro
objeto de
las referencias de los postings producidos referencien al objeto contenedor o a ambos.
Selección de Índices
En ocasiones necesitamos segregar objetos de distinta naturaleza en diferentes índices, tal de permitir la
indexación y recuperación de unos y otros en forma independiente. Por ejemplo, en la subsección (4.2.3)
utilizaremos la selección de índices para separar la búsqueda e indexación de productos comerciales de la
de anuncios publicitarios.
La selección del índice se puede hacer:
a nivel de clase, utilizando un mismo índice para toda instancia de la clase,
en forma dinámica por objeto según el valor de un atributo o el valor retornado desde un método.
Ejemplo 3.2.8.
Veamos algunos ejemplos de selección de índices con annotations.
3.2.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
105
MAPEO DE CLASES
@ I n d e a x a b l e ( indexName=" a d v e r t i s i n g " )
public c l a s s A d v e r t i s i n g {
@ S e a r c h I d private Long i d ;
@ S e a r c h F i e l d private S t r i n g t i t l e ;
public S t r i n g h y p e r l i n k ;
...
}
@ I n d e x a b l e ( indexName=" p r o d u c t s " )
public c l a s s P r o d u c t {
@SearchId
private Long i d ;
@SearchField
private S t r i n g i t e m T i t l e ;
}
En este ejemplo, las líneas 1 y 9 seleccionan de forma estática los índices de cada entidad.
Veamos otro caso:
1
2
@Indexable
private c l a s s
3
@SearchId
private I n t e g e r i d ;
4
5
6
@IndexSelector
String indexSelector ;
7
private
8
9
10
11
12
Something {
}
@SearchField
String value ;
private
En este nuevo caso, la selección se da en forma dinámica según el valor del atributo
indexSelector
de la
línea 8.
La separación de objetos en distintos índices también permite a la capa de acceso a datos separar los
objetos indexados en diferentes tablas, archivos o estructuras de datos, lo cual puede mejorar la eciencia
de dicha capa.
Identicador y Selector de Lenguaje
El identicador de lenguaje le indica al framework que los objetos de una clase poseen textos en un
lenguaje determinado. Esto permite que las clases que procesan el texto de los objetos adecúen su lista
de stopwords, stemmer y demás elementos al lenguaje particular del objeto que tratan.
Esta es una característica original de nuestro framework que no suele estar presente en otros similares.
Esto es muy util en casos como el desarrollo web, donde una misma clase produce objetos para sitios
de países distintos. Con esta directiva, escribimos un único código y en tiempo de ejecución elegimos la
implementación correcta del procesador de textos.
La selección del lenguaje se puede dar en forma general para todos los objetos de una clase o bien ser
elegida objeto por objeto en base al valor de un atributo. Cuando la selección del lenguaje se da por un
atributo, tenemos un selector de lenguaje. A continuación vemos un ejemplo:
Ejemplo 3.2.9
(Indicador y Selector de Lenguaje )
.
mediante anotaciones:
@ I n d e x a b l e @LangId ( v a l u e="es_AR" )
private c l a s s DummyIndexableWithLangId {
Veamos cómo indicar el lenguaje de un objeto
106
}
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
@ S e a r c h I d private long i d = 0 ;
@ S e a r c h F i e l d private S t r i n g t e x t = " yo u t i l i z o p a l a b r a s l o c a l e s como ' che
'" ;
@Indexable
private c l a s s D u m m y I n d e x a b l e W i t h L a n g S e l e c t o r {
@ S e a r c h I d private long i d = 0 ;
@ L a n g S e l e c t o r private S t r i n g l a n g = "en_US" ;
@ S e a r c h F i e l d private S t r i n g t e x t = " t h i s t e x t
English . " ;
}
En el código anterior, la clase
DummyIndexableWithLangId
s h o u l d be i n d e x e d i n
está anotada de forma que todos los objetos
de ése tipo serán procesados según la lista de stopwords, stemmers y correcciones del español localizado
en Argentina.
Para el segundo caso,
DummyIndexableWithLangSelector
especica un atributo que por defecto indica la
indexación de texto en inglés localizado en los Estados Unidos, pero que puede variar objeto a objeto.
Filtrado y Ordenamiento
Cuando analizamos las técnicas de puntaje y relevancia (ver subsección 3.1.4), vimos que debemos dar
soporte al modelo de similitud del modelo de IR, los ordenamientos por reglas duras y blandas y al ltrado
y clasicación de objetos. Para brindar este soporte, debemos indicarle al framework qué campos van a
participar en estos procesos.
Dado que la recuperación booleana no incorpora una noción de similitud intrínseca al modelo, no necesitamos almacenar ninguna información adicional a la clave del objeto. Sin embargo, para el modelo vectorial
sí debemos almacenar valores adicionales para el cálculo de TF-IDF. Para esto, nuestro framework no
requiere una directiva de mapeo explícito sino que automáticamente ubica los valores necesarios junto a
los postings y posting lists.
Tratemos ahora el caso de ltrado y ordenamiento:
Como explicamos en la subsección (3.1.4), tanto el ltrado como el ordenamiento ocurren antes de la
hidratación de los resultados. Por lo tanto, el motor de búsqueda es responsable de proveer los atributos
requeridos en esa etapa. Para identicar estos valores, incluimos una directiva de mapeo para atributos
de ltrado y ordenamiento. Veamos un ejemplo:
Ejemplo 3.2.10 (Mapeo de Atributos para Filtrado).
En este extracto de código mostramos cómo indicar
que cierto atributo debe utilizarse para ltrado utilizando anotaciones:
1
2
3
@Indexable
private c l a s s E n t i t y F o r F i l t e r i n g
@ S e a r c h I d public int i d ;
4
@SearchField
5
6
public
9
10
11
12
13
14
15
16
17
18
String attribute ;
@ S e a r c h F i l t e r ( a c c e s s B y G e t=true )
7
8
19
public
}
}
{
public
Date d a t e O f B i r t h ;
Date g e t D a t e O f B i r t h ( ) {
Calendar birthCalendar = Calendar . getInstance () ;
b i r t h C a l e n d a r . s e t ( C a l e n d a r . YEAR, 1 9 8 3 ) ;
b i r t h C a l e n d a r . s e t ( C a l e n d a r .MONTH, 0 3 ) ;
b i r t h C a l e n d a r . s e t ( C a l e n d a r .DAY_OF_MONTH, 3 0 ) ;
b i r t h C a l e n d a r . s e t ( C a l e n d a r . MINUTE, 0 ) ;
b i r t h C a l e n d a r . s e t ( C a l e n d a r . SECOND, 0 ) ;
b i r t h C a l e n d a r . s e t ( C a l e n d a r . MILLISECOND , 0 ) ;
return b i r t h C a l e n d a r . getTime ( ) ;
3.2.
107
MAPEO DE CLASES
La clave de este ejemplo está en la línea 7. En esta línea especicamos que el campo
dateOfBirth (fecha de
nacimiento) debe utilizarse en tareas de ltrado y por lo tanto debe almacenarse. Notemos que el acceso
al atributo puede hacerse mediante la convención JavaBeans, llamando al método
getDateOfBirth.
En este ejemplo también podemos ver que los objetos que utilizamos para el ltrado pueden ser objetos
complejos Java, con el simple requisito de ser serializables.
Para soportar el ordenamiento por reglas el panorama es muy similar. Ya sea ltrado u ordenamiento,
el framework siempre debe proveer los atributos intervinientes. A la hora de especicar dichos atributos,
debemos utilizar nuevamente las directivas de mapeo. Veamos un ejemplo:
Ejemplo 3.2.11
(Mapeo de Atributos para Ordenamiento por Reglas)
.
En este extracto fragmento de
código especicamos que un atributo participa del ordenamiento por reglas utilizando anotaciones:
1
2
3
@Indexable
private c l a s s S o r t E n t i t y {
@ S e a r c h I d private int i d ;
4
6
@SearchField
7
8
11
12
14
private
public S o r t E n t i t y ( int
this . id = id ;
this . t i t l e = t i t l e ;
this . price = price ;
9
10
13
private f l o a t
@SearchSort
5
}
price ;
String t i t l e ;
float
id , S t r i n g t i t l e ,
price ) {
}
En este caso la clave está en la línea 5, donde indicamos que el atributo
price
debe ser almacenado para
utilizarse al aplicar el ordenamiento por reglas.
Con estos ejemplos estamos en condiciones de resumir los puntos claves del mapeo de atributos para
ltrado y ordenamiento:
los atributos deben almacenarse en el motor de búsqueda por utilizarse en forma previa a la hidratación,
existen dos directivas de indexación que indican si un atributo participa del ltrado y/o del ordenamiento, las cuales son accesibles programaticamente o mediante las anotaciones @SearchSort y
@SearchField.
Más adelante retomaremos este tema reriéndonos a cómo diseñar el framework para que ltrar, clasicar
y ordenar resultados sea una tarea sencilla y rápida (subsección 3.3.5).
Procesamiento de Textos
Cuando estudiamos las técnicas de matching y acceso a datos (subsección 3.1.2), propusimos implementar
cierta inteligencia que normalice y delimite los textos presentes en los objetos indexables.
En nuestro framework esta inteligencia se puede variar a partir de directivas de indexación, las cuales
indican qué clase es la encargada de extraer el texto de los atributos, normalizarlos y generar términos.
Veamos un ejemplo:
Ejemplo 3.2.12
(Elección del Procesador de Textos)
.
En el siguiente fragmento de código, elegimos el
procesador de textos para la clase utilizando anotaciones:
1
2
3
4
5
6
@Indexable
@TextProcessor ( MyTextProcessor . c l a s s )
private c l a s s E n t i t y W i t h T e x t P r o c e s s o r {
@ S e a r c h I d int i d = 0 ;
@ S e a r c h F i e l d public S t r i n g a t t r i b u t e = "some t e x t t o be t r e a t e d " ;
}
108
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
La línea clave de este ejemplo es la número 2, donde indicamos al framework que debe utilizar la clase
MyTextProcessor
para la extracción y procesamiento del texto some
text to be treated.
Si no indicamos explícitamente un procesador de textos, el framework utiliza uno por defecto que aplica
eliminación de símbolos y stop words, normalización a mayúsculas, stemming y delimitación de términos
por espacios.
Notas Adicionales
Cuando utilizamos un mapeo por anotaciones, todos los mapeos avanzados/directivas de indexación se
heredan en la jerarquía de objetos. Es decir, si aplicamos un procesador de objetos o un selector de
lenguajes a una clase y ésta utiliza el parámetro makeSubclassesIndexable = true, las clases descendientes
aplicarán el mismo procesador de objetos o selector de lenguajes. Para evitar la herencia de directivas de
indexación podemos redenir la directiva en el nivel de la jerarquía donde queremos evitar que se aplique
la directiva conictiva.
Hasta aquí explicamos qué problemas debemos resolver para generar una herramienta completa de IR
sobre objetos, así como expusimos la expresividad del framework en términos de mapeos o directivas de
indexación.
En la siguiente sección completamos la propuesta exponiendo el diseño interno del framework y los
algoritmos que dan solución a cada uno de los elementos que analizamos.
3.3.
Diseño del Framework de IR sobre ob jetos
3.3.1. Introducción
Como explicamos al comenzar este capítulo, en esta sección completamos la transición del análisis al
diseño, explicando cómo hemos construido el motor de búsqueda sobre objetos.
El objetivo de esta sección es ajustar el grado de detalle y denir el diseño del framework que presentaremos en funcionamiento en el capítulo 4.
3.3.2. Arquitectura del Framework
Diseño de Capas: Indexación
El framework se estructura en una arquitectura de capas intercambiables por conguración.
En la gura (3.8) vemos las capas que vamos a utilizar para la indexación.
3.3.
109
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
Application
Indexer Service
Indexing
Pipeline
Index
Writer
Memory, etc...
Figura 3.8: Arquitectura de Capas de Indexación. La primera capa siempre es la aplicativa, luego aparecen
las capas del framework y por último una sección física de almacenamiento de índices.
Analicemos cada una de estas capas:
Application: produce los eventos de indexación, delegando la indexación en el Indexer Service.
Indexer Service: es el encargado de secuenciar el procesamiento de los objetos y asegurarse que se
escriban en los índices. El framework provee las siguientes variantes:
•
OnlineIndexer y SemiOnlineIndexer: cumplen las funciones que explicamos en la subsección
(3.1.3). Luego de resolver la secuenciación y sincronización de procesos, delegan la indexación
en DefaultIndexerService. Más adelante veremos que existe un servicio de indexación oine a
través de JMS, el cual es provisto por un plugin.
•
DefaultIndexerService: es la terminal de todos los indexadores. Se ocupa de orquestar la conversión de objetos a postings a través del IndexingPipeline y la escritura en los índices a través de
los IndexWriter. Mantiene un pool de escritores abiertos para minimizar las aperturas y cierres
de índices.
Indexing Pipeline: su responsabilidad es procesar una entidad y generar un semi índice con los
postings que deben escribirse en el índice invertido y el registro maestro. El pipeline se utiliza sólo
en la creación y actualización de objetos, no en la eliminación. Este proceso lo tratamos en detalle
más adelante.
Index Writer: es el encargado del acceso de escritura al índice invertido y al registro maestro.
Para visualizar mejor la solución podemos explicitar estas interfaces en el siguiente listado de código:
public
interface
IndexerService
public
void
c r e a t e ( Object
public
void
b u l k C r e a t e ( L i s t <?>
throws
entity )
public
void
d e l e t e ( Object
public
void
b u l k D e l e t e ( L i s t <?>
public
void
update ( Object
public
void
b u l k U p d a t e ( L i s t <?>
public
void
createOrUpdate ( Object
public
void
b u l k C r e a t e O r U p d a t e ( L i s t <?>
IndexObjectException ;
}
{
entity )
entity )
throws
entities )
entities )
IndexObjectException ;
throws
IndexObjectException ;
entity )
IndexObjectException ;
throws
throws
entities )
IndexObjectException ;
IndexObjectException ;
IndexObjectException ;
throws
throws
entity )
IndexObjectException ;
throws
110
CAPÍTULO 3.
public
interface
public
IndexingPipeline
SemiIndex
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
{
processObject ( Object
entity )
throws
IndexObjectException
;
}
public
interface
IndexWriter
{
public
void
open ( ) ;
public
void
open ( I n d e x I d
public
void
close () ;
public
void
w r i t e ( Term
public
void
d e l e t e ( IndexObjectDto
indexId ) ;
term ,
ObjectKey
key ,
PostingMetadata
metadata ) ;
indexObjectDto ) ;
public
void
openDeleteAndClose ( IndexObjectDto
public
void
o p e n W r i t e A n d C l o s e ( Term
term ,
indexObjectDto ) ;
ObjectKey
key ,
PostingMetadata
metadata ) ;
public
,
void
openWriteAndClose ( I n d e x I d
PostingMetadata
indexName ,
Term
term ,
ObjectKey
key
metadata ) ;
}
Como comentamos al principio de este apartado, estas capas están bien denidas por interfaces, por lo
que podemos variar su implementación con facilidad.
Diseño de Capas: Recuperación
Así como la etapa de indexación cuenta con una arquitectura de capas, la etapa de búsqueda también
cuenta con una arquitectura de este tipo.
Veamos un diagrama de las capas del motor de recuperación:
Application
Query Parser
Core Engine
Result Windowing
Sorting Rules
Filtering Rules
IR Model Similarity (Vector Model only)
Index Reader
Memory, etc...
Figura 3.9: Arquitectura de Capas de Búsqueda. La capa aplicativa colabora con el intérprete de consultas
y el motor de búsquedas. Éste se compone de sub capas según el modelo de IR correspondiente, accediendo
a los índices mediante la capa de acceso a datos (IndexReader).
3.3.
111
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
Analicemos en detalle las capas de la gura (3.9):
Application: provee al QueryParser el texto u objeto de búsqueda para que éste lo interprete y envía
la consulta al motor de búsqueda (Core Engine). Luego de la ejecución de la búsqueda, la aplicación
recibe una lista de objetos deshidratados.
Query Parser: interpreta la consulta que envía la aplicación y colabora con los procesadores de texto
para normalizar y delimitar la consulta. El Query Parser es responsable de construir un árbol de
consultas adecuado al modelo de IR. Existe una implementación del Query Parser para el modelo
booleano y otra para el modelo vectorial, estas son BooleanQueryParser y VectorQueryParser.
Core Engine: esta capa implementa el núcleo de la recuperación de información. Existen dos implementaciones, una para el modelo booleano y otra para el vectorial, estas son: BooleanSearch y
VectorSearch.
Según el modelo de IR, se recibe un objeto BooleanQuery o VectorQuery y se procede a ejecutar una
serie de operaciones internas (en orden inverso):
•
IR Model Similarity (sólo modelo vectorial): VectorSearch delega en un objeto VectorRanker la
evaluación de los puntajes correspondientes al modelo. La implementación por defecto efectúa
el cálculo de la fórmula (3.1.1).
•
Filtering Rules: aplica los ltros que se especiquen. Los ltros se deben agregar a un objeto
de tipo FilterChain, el cual los recorre uno a uno, eliminando los resultados que no cumplan
la condición de algún ltro. En esta etapa podemos también clasicar los resultados para
construir una búsqueda facetada.
•
Sorting Rules: aquí se aplican los ordenamientos por reglas duras y blandas. Existen dos formas
combinables de aplicar reglas: proveyendo una implementación de la interfaz PreSort o una
implementación de la interfaz Comparator. En el primero caso, la implementación de PreSort es
responsable de ordenar los resultados, mientras que en el segundo caso se utiliza el algoritmo
de ordenamiento de la clase Collections de Java.
•
Result Windowing: nalmente los resultados son segmentados en páginas según lo especique
la query.
Index Reader: recibe los pedidos de lectura de términos, accede a los índices y retorna las postings
lists.
Ejemplo 3.3.1.
Veamos un ejemplo de código que muestra la recuperación en el caso booleano y aso-
ciemoslo con las capas de la gura (3.9).
1
2
3
4
5
6
B o o l e a n Q u e r y P a r s e r p a r s e r = new B o o l e a n Q u e r y P a r s e r ( " b o o l e a n r e t r i e v a l " ) ;
BooleanQuery q u e r y = p a r s e r . g e t Q u e r y ( ) ;
query . setPage (1) ;
query . setPageSize (30) ;
B o o l e a n S e a r c h b o o l e a n S e a r c h = new B o o l e a n S e a r c h ( query , MemoryIndexFactory .
getInstance () ) ;
Set<O b j e c t K e y R e s u l t > r e s u l t = b o o l e a n S e a r c h . s e a r c h ( ) ;
Este código que escribimos pertenece a la capa
Application.
Core Engine).
La línea 5 accede a la capa
debe utilizar para la capa
Index Reader.
Core Engine
Query Parsing,
Result Windowing de
La línea 1 accede a la capa de
la cual nos entrega una query sobre la cual especicamos el paginado (ver subcapa
indicándole al motor booleano qué implementación
Por último, la línea 6 ejecuta la búsqueda y obtiene un conjunto
de resultados deshidratados (claves de objeto u
ObjectKeyResult).
Al igual que la etapa de indexación, podemos intercambiar las capas y cambiar de modelo fácilmente.
Veamos una variante en las que modicamos el motor de búsqueda y el backend de acceso a índices.
Ejemplo 3.3.2.
Modiquemos el ejemplo (3.3.1) para permitir la recuperación vectorial y utilizar un
lector de índices en disco.
112
1
2
3
4
5
6
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
V e c t o r Q u e r y P a r s e r p a r s e r = new V e c t o r Q u e r y P a r s e r ( " v e c t o r r e t r i e v a l " ) ;
VectorQuery query = p a r s e r . getQuery () ;
query . setPage (1) ;
query . setPageSize (30) ;
V e c t o r S e a r c h v e c t o r S e a r c h = new V e c t o r S e a r c h ( query ,
BerkeleyIndexReaderFactory . getInstance () ) ;
L i s t <V e c t o r R a n k e d R e s u l t > r e s u l t = v e c t o r S e a r c h . s e a r c h ( ) ;
A primera vista, las diferencias principales de este código con el del ejemplo (3.3.1) es el reemplazo de la
palabra Boolean por Vector. Sin embargo, hay algunos detalles que tenemos que notar:
El tipo de datos de retorno en la línea 6 cambia tanto en el tipo de colección como en su contenido. Esto
sucede porque la búsqueda booleana simple no aplica un modelo de similitud entre objetos, por lo tanto
devuelve un conjunto de objetos (recordemos que un conjunto no tiene noción de orden). El cambio de
tipo de datos en la colección obedece a que en el caso vectorial los resultados contienen información de
valoración, cosa que no ocurre en el caso booleano.
Por otro lado, notamos que la línea 5 cambia el uso de índices en memoria por un índice en disco Berkeley.
Tanto el lector de índices en memoria del ejemplo anterior como este lector de índices Berkeley cumplen
con la interfaz
IndexReader,
de forma tal que
VectorSearch
hará su trabajo independientemente de cómo
se almacenen los postings. Seguramente este es uno de los ejemplos más fuertes acerca de la simplicidad
con la que podemos intercambiar capas en este diseño.
Plugins
La arquitectura de capas que mostramos en las subsecciones anteriores nos permiten generar componentes
intercambiables o plugins.
Cuando hablamos de plugins nos estamos reriendo a componentes heterogéneos que pueden afectar
distintas partes del framework: desde intérpretes de queries hasta acceso a índices, el comportamiento del
framework es fácilmente variable por estos plugins.
Actualmente el framework dispone de cuatro plugins: índice BerkeleyDB (subsección 3.3.3), indexador
oine JMS (subsección 3.3.4) y los indexadores por eventos para Hibernate (subsección 3.3.4) e iBATIS
(subsección 3.3.4).
3.3.3. Técnicas de Matching y Acceso a Datos
Arquitectura de Índices, Posting Lists y Postings
La arquitectura de índices se organiza en base al conjunto de interfaces de la gura (3.10):
3.3.
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
113
Figura 3.10: Arquitectura de Índices. El diseño se desacopla en índices y lectores. Los índices deben implementar MasterAndInvertedIndex, mientras que los lectores hacen lo propio con las interfaces de escritura
y lectura.
Veamos el rol de cada interfaz:
MasterAndInvertedIndexReader y MasterAndInvertedIndexWriter: especican el conjunto de operaciones que se esperan de quien es responsable de abrir, leer/escribir y cerrar índices.
InvertedIndex y MasterIndex: representan el índice invertido y el registro maestro. En nuestro framework no implementamos directamente estas interfaces sino que lo hacemos a través de MasterAndInvertedIndex.
MasterAndInvertedIndex: representa la combinación de un registro maestro y un índice invertido, tal
como los hemos presentado al hacer el análisis del problema. En nuestro framework proveemos una
implementación a través del MemoryIndex y otra en el BerkeleyIndex (ver más adelante el apartado
acerca de BerkeleyDB).
IndexReaderFactory e IndexWriterFactory: las implementaciones de estas dos interfaces son el punto
de interacción entre la aplicación y la capa de acceso a datos. Esta es una implementación similar
al patrón de diseño AbstractFactory (Gamma et al., 1995).
El hecho de contar con este juego de interfaces nos permite extender el backend de acceso a datos a partir
de plugins, tal que implementándolos podemos crear índices sobre otras tecnologías como bases de datos
relacionales o archivos.
114
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Además de este conjunto de interfaces, el diseño de los índices cuenta con otras entidades concretas. A
continuación explicamos las entidades que aparecen como parámetros en los métodos de la gura (3.10):
Object Key: representa la clave del objeto tal como la denimos en la subsección (3.1.2).
Posting List: contiene los postings asociados a un término, representándolos por pares (ObjectKey ;
PostingMetadata), los cuales se almacenan en un mapa. El concepto representado por esta clase es
el mismo que presentamos en la subsección (2.1.5).
Posting Metadata: este objeto almacena para cada posting los datos necesarios para el ltrado,
ordenamiento y valoración por el modelo de IR. En el caso del modelo vectorial, también lleva
cuenta del valor de TF para cada atributo del objeto.
Term: es la representación de un término normalizado y delimitado. Luego del procesamiento de
objetos, es la única representación del texto original del objeto operable por el motor de búsqueda.
Cualquier índice que implemente las interfaces de la gura (3.10) debe representar la información en
términos de estos objetos desde y hacia el motor de búsqueda (por más que internamente utilice otra
representación de datos).
En las próximas subsecciones presentamos dos índices concretos sobre las interfaces y objetos estudiados.
Índice en Memoria
El índice en memoria es una implementación de la interfaz MasterAndInvertedIndex que mantiene el registro
maestro y el índice invertido en memoria RAM.
Esta implementación es útil para pruebas de concepto y el desarrollo de tests unitarios (como comentaremos en el capítulo 4, esto es especialmente importante para nosotros ya que hicimos un desarrollo basado
en pruebas).
De los casos de estudio de la sección (2.4), podemos recordar que Apache Lucene también implementa
un índice en memoria, el cual también es heredado por sus frameworks descendientes (Hibernate Search
y Compass).
Este índice también puede ser utilizado como una representación intermedia o buer, por ejemplo, para
un almacenamiento asincrónico en un medio persistente.
Índice Berkeley DB
BerkeleyDB (Oracle, 2009a ) es una implementación gratuita de una base de datos embebida, la cual tiene
una implementación Java que incluye soporte transaccional.
Este plugin experimental se construyó para dar soporte a los índices persistentes de las aplicaciones del
capítulo 4, proveyendo versiones en disco del registro maestro y el índice invertido.
Como programador del plugin, BerkeleyDB nos provee una implementación persistente de la clase HashMap
de Java, la cual es una muy buena opción para representar el mapeo entre términos y posting lists.
El diagrama de clases de este plugin es el siguiente:
3.3.
115
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
Figura 3.11: Diagrama de clases del plugin BerkeleyDB.
Para utilizar el plugin basta con agregarlo como dependencia en el proyecto que se está desarrollando, y
proveerlo al motor de búsqueda tanto al indexar como al recuperar objetos.
Procesador de Textos
Los procesadores de textos forman un componente esencial en cualquier motor de búsqueda ya que
determinan en gran medida el recall y precisión del sistema.
En nuestro caso, éstos intervienen en la indexación de objetos y en la interpretación de consultas de
usuarios.
La interfaz que debe cumplir un procesador de textos para la indexación es la siguiente:
public
interface
public
abstract
ObjectTextProcessor
L i s t <Term>
MappedFieldDescriptor
{
processField ( String
extractedText ,
fieldDescriptor ) ;
}
y para el caso de la interpretación de consultas:
public
interface
public
QueryTextProcessor
{
L i s t <Term> p r o c e s s T e x t ( S t r i n g
text ,
Language
}
Analicemos cada interfaz para entender sus similitudes y diferencias:
language ) ;
116
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Al momento de indexar un objeto necesitamos conocer información acerca de cómo fue mapeado (por
ejemplo, para saber qué tipo de stemming aplicar). Dentro de la información de mapeo se encuentran
los detalles acerca del lenguaje del objeto o del atributo, el tipo de stemming a aplicar, cómo extraer los
textos de los atributos, etc.
Por otro lado, al momento de interpretar una query, la interfaz sólo requiere el texto de la consulta y su
lenguaje. Otras características como el stemming o la eliminación de stop words no deben pertenecer a la
interfaz de estos objetos porque no son una parte esencial de un procesador de textos sino implementaciones particulares que deben congurarse por separado de la interfaz denida.
Comentemos brevemente respecto de la inclusión explícita de lenguajes:
Una característica original de este framework es que incorpora explícitamente la noción de lenguaje a lo
largo de todas las clases de mapeos y procesamiento de textos.
El hecho de conocer el lenguaje de los objetos en tiempo de ejecución podría haber sido opcional, sin
embargo, decidimos incluirlo en todo el sistema porque permite extender naturalmente el framework
para efectuar operaciones como la corrección de ortografía (lo cual de otra forma requeriría reconocer
el lenguaje del texto original, lo cual no es trivial). Si no tuviéramos una fuerte presencia de objetos
que representan lenguajes, seguramente tendríamos que resolver este problema por nuestra cuenta y
dicultaríamos las extensiones del framework, lo cual no es deseable.
En esta versión del framework incluimos un procesador de textos por defecto para la indexación ( DefaultObjectTextProcessor ) y otro para la interpretación de consultas ( DefaultQueryTextProcessor ). Estos
procesadores aplican normalización a mayúsculas, eliminación de caracteres no alfabéticos, delimitación
de términos por espacios, eliminación de stop words y, en caso de que corresponda, stemming snowball.
Intérprete de Consultas
En la subsección (3.1.2) explicamos que la interpretación de consultas comprende tanto la normalización
y delimitación de textos, a cargo del procesador de textos, así como la construcción del árbol de consultas.
Este apartado expone el diseño de las clases encargadas de la construcción del árbol de consultas.
Cada modelo de IR cuenta con su propio intérprete de consultas: en el caso booleano tenemos el BooleanQueryParser mientras que en el vectorial tenemos el VectorQueryParser. Estos intérpretes se diferencian
en los algoritmos que aplican para generar el árbol de consulta, de forma de soportar sus operadores y
estándares de facto (ver subsección 3.1.2).
Para soportar estas dos variantes, cada modelo complementa su árbol de consulta con un extractor de
posting lists. El trabajo del extractor es:
1. leer las postings lists y generar resultados según el tipo de objeto esperado por cada modelo,
2. recolectar desde el índice los datos necesarios para el modelo correspondiente de IR. En el caso del
modelo vectorial, buscamos el valor de DF sobre los postings recuperados.
Mas allá de qué algoritmo se utilice, cada interprete lee la consulta del usuario y la traduce en una
composición de operadores lógicos AND, OR, NOT mas un operador adicional no booleano llamado
RETRIEVE ó simplemente R. Este último operador tiene la responsabilidad de leer la posting list desde
el índice y colaborar con el extractor de resultados para generar un resultado parcial de la búsqueda (ver
subsección 3.1.2).
La jerarquía de clases que representa estos operadores se presenta en la gura (3.12).
3.3.
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
117
Figura 3.12: Jerarquía de Operadores. Los operadores constituyen el árbol de consulta de una query. El
nodo raíz produce la ejecución en cascada del resto de los operadores, terminando siempre en nodos hoja
de tipo RETRIEVE.
La jerarquía de la gura (3.12) se organiza de la siguiente forma:
Operator: clase abstracta que dene las responsabilidades de cualquier operador. Estas responsabilidades son: entregar el término que los representa en una query (getOperatorTerm), efectuar su
operación (work) y redenir los métodos hashCode e equals de la clase Object.
BinaryOperator: esta clase abstracta representa un operador que a su vez contiene un operador
izquierdo y derecho. Esta clase es la base de la composición de operadores, lo cual permite la
construcción del árbol de consulta. La mayoría de los operadores extienden esta clase.
BinarySimetricOperator: esta clase abstracta agrega a BinaryOperator la propiedad de conmutatividad. Redene los métodos equals y hashCode para adecuarlos a su semántica.
MinusOperator, OrOperator, AndOperator: estos son operadores concretos cuyos métodos work efectúan operaciones de substracción, conjunción y disyunción de conjuntos. Cada uno de ellos implementa el método getOperatorTerm para indicar cómo se los debe reconocer en una query. El
MinusOperator es un operador simétrico no conmutativo, por lo que extiende a BinaryOperator. En
cambio, OrOperator y AndOperator sí son conmutativos, de forma que extienden a BinarySimetricOperator.
RetrieveOperator: esta clase concreta implementa el único operador unario de la jerarquía. El objetivo
de esta clase es obtener y extraer los postings a partir del lector del índice. El RetrieveOperator es
el único operador hoja de esta jerarquía.
Desde el punto de vista procedural, cuando el intérprete de queries le entrega el árbol de consulta al motor
de búsqueda, éste ejecuta el método work del operador raíz, produciendo la ejecución en cascada de los
operadores del árbol. Por ejemplo, si la raíz es un operador binario, éste pedirá a su término izquierdo
los resultados de su trabajo, luego hará lo mismo con el derecho y por último operará sobre los conjuntos
obtenidos por sus operadores izquierdo y derecho.
118
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
3.3.4. Procesos de Indexación
En esta subsección vamos a tratar el diseño de la parte del framework que desglosa objetos de dominio
en postings del índice invertido.
El diseño del framework separa los procesos de indexación en tres subcapas con responsabilidades bien
diferenciadas:
Dinámica de Indexación: maneja el nivel de asincronismo entre las transacciones de negocio y la
actualización de los índices. Aquí tenemos los indexadores online, semi-online u oine.
Secuenciamiento de la Indexación y Acceso a Índices: orquesta el procesamiento de objetos y la
interacción con la capa de escritura de índices. Aquí tenemos al DefaultIndexerService.
Procesamiento de Objetos / Pipeline de Indexación: es responsable de generar postings a partir de
un objeto y su descripción de mapeo.
Pipeline de Indexación
El pipeline de indexación es el encargado de implementar la lógica de generación de postings a partir de
un objeto de dominio.
Este proceso debe tener en cuenta los mapeos que expusimos en la sección (3.2), de forma tal de producir
los resultados esperados por el usuario del framework.
El pipeline de indexación es denido de la siguiente forma:
public
interface
public
IndexingPipeline
SemiIndex
throws
{
processObject ( Object
entity )
IndexObjectException ;
}
La implementación por defecto de este interfaz está dada por la clase DefaultIndexingPipeline. Al igual
que otras capas del framework, el pipeline de indexación se puede reemplazar fácilmente proveyendo otra
implementación de IndexingPipeline.
En la gura (3.13) presentamos un diagrama de actividades simplicado para la implementación del
método processObject de DefaultIndexingPipeline.
3.3.
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
119
Figura 3.13: Diagrama de actividades para el método DefaultIndexingPipeline.processObject(Object entity).
En este diagrama simplicado vemos las variantes de generación del semi índice en función de cómo fue
mapeada la entidad.
Analizando el diagrama de actividades de la gura (3.13) vemos que la indexación de un objeto depende
en gran parte de si es indexable por sí mismo y/o si es un contenedor de objetos indexables.
El primer rombo de decisión del diagrama (3.13) insinúa la capacidad de este algoritmo de indexar
jerarquías de objetos anidados. Esta capacidad se implementa mediante recursión. Para controlar que
esta recursividad sea nita, cada llamado verica que el objeto actual no haya sido procesado en el árbol
de llamadas del hilo actual, retornando en caso de que ello suceda.
El sistema de mapeos permite además que los objetos anidados generen postings con referencias a sus
contenedores, y a su vez estos postings pueden almacenarse en un índice distinto al del objeto contenedor.
Para soportar estas características, el resultado del método processObject no puede ser simplemente un
conjunto de postings a aplicar en el índice sino que debe ser una estructura más compleja llamada semi
índice (SemiIndex).
El semi índice representa un mapeo a postings de los objetos que están cursando su proceso de indexación.
Además de sus características como estructura de datos, el semi índice permite la operación de fusión
(merge) con otro semi índice, tal de permitir acumular postings a medida que procesamos recursivamente
una jerarquía de objetos anidados.
Indexador Online, Semi-Online & Oine
En este apartado retomamos la discusión de la subsección (3.1.3), diseñando las tres dinámicas de indexación de objetos: indexación online, semi-online y oine.
Para crear un indexador que reeje los eventos CUD en el motor de búsqueda, debemos implementar la
interfaz IndexerService. Los indexadores que vamos a presentar se ocupan de la relación entre la transacción
120
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
y la indexación. La generación de postings no es implementada por estas clases sino que se delega en otra
implementación de IndexerService.
El primer indexador que vamos a comentar es el OnlineIndexer, el cual naturalmente se corresponde con
el indexador online que presentamos en la sección de análisis. Este indexador hace algunas vericaciones
simples y delega el procesamiento de los objetos en una implementación de IndexerService (normalmente
en DefaultIndexerService).
El segundo tipo de indexador es el semi-online y está implementado por la clase SemiOnlineIndexer. En
este modo de indexación debemos trabajar en forma asincrónica a la transacción de negocio, encolando
los pedidos de indexación y retornando inmediatamente el control a la aplicación. El desencolado de estos
pedidos de indexación se realiza en forma concurrente por un grupo de hilos, los cuales se comportan
como indexadores online en paralelo.
Por último tenemos al indexador oine. Éste es similar al indexador semi-online, excepto por la diferencia
de que la cola de pedidos se implementa en forma persistente y el proceso que desencola los pedidos debe
ejecutarse en una máquina virtual distinta al que los encola. El indexador oine requiere de un plugin
que provea la cola de indexación persistente, siendo que nuestra implementación está basada en JMS (ver
subsección 3.3.4).
Al discutir los casos de experimentación del capítulo 4 veremos cómo variar la dinámica de indexación
con una simple conguración de inyección de dependencias.
Plugin de Indexación Oine JMS
Este plugin implementa la indexación oine utilizando una implementación del estándar de mensajería
?
de Java, la tecnología JMS ( ). La implementación particular de JMS que utilizamos para este plugin es
Apache ActiveMQ (Apache, 2009a ).
Nuestra implementación consiste en un plugin inyectable como dependencia en la capa de interacción con
el ORM. Al igual que cualquier indexador, es necesario implementar la interfaz IndexerService, lo cual
hicimos en la clase JmsOineIndexer.
La migración hacia/desde el indexador oine JMS es transparente a la aplicación ya que la implementación de IndexerService es semánticamente equivalente al indexador online o el semi-online.
Plugin Hibernate
El objetivo de este plugin es reejar los eventos CUD del ORM en el motor de búsqueda.
Para usar este plugin, debemos registrarlo como un escucha de los eventos que se producen en el ORM
mediante conguración de Hibernate. El plugin consiste en una implementación de las interfaces de
escucha de eventos de Hibernate, tal de reejar los cambios de estado de los objetos en nuestro motor de
búsqueda.
El principio de funcionamiento del plugin es simple: cuando un objeto se crea, actualiza o elimina un
objeto, Hibernate envía a los escuchas registrados la información relativa a dicho evento. Esta información
es recibida por nuestro framework, quien la reeja en los índices.
Ejemplo 3.3.3
.
(Conguración Programática del Plugin para Hibernate)
Veamos cómo congurar Hi-
bernate para que utilice el plugin de indexación por eventos.
1
2
3
4
5
6
7
8
9
H i b e r n a t e E v e n t I n t e r c e p t o r l i s t e n e r=
new H i b e r n a t e E v e n t I n t e r c e p t o r (
new S e a r c h I n t e r c e p t o r (
new D e f a u l t I n d e x e r S e r v i c e (
new D e f a u l t I n d e x i n g P i p e l i n e ( ) ,
MemoryIndexWriterFactory . g e t I n s t a n c e () ) ) ) ;
C o n f i g u r a t i o n c o n f i g u r a t i o n = new C o n f i g u r a t i o n ( ) . c o n f i g u r e ( ) ;
c o n f i g u r a t i o n . g e t E v e n t L i s t e n e r s ( ) . s e t P r e I n s e r t E v e n t L i s t e n e r s ( new
P r e I n s e r t E v e n t L i s t e n e r [ ] { l i s t e n e r }) ;
3.3.
10
11
12
13
121
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
c o n f i g u r a t i o n . g e t E v e n t L i s t e n e r s ( ) . s e t P r e U p d a t e E v e n t L i s t e n e r s ( new
P r e U p d a t e E v e n t L i s t e n e r [ ] { l i s t e n e r }) ;
c o n f i g u r a t i o n . g e t E v e n t L i s t e n e r s ( ) . s e t P r e D e l e t e E v e n t L i s t e n e r s ( new
P r e D e l e t e E v e n t L i s t e n e r [ ] { l i s t e n e r }) ;
sessionFactory = configuration . buildSessionFactory () ;
Las líneas 1 a 6 conguran las capas de indexación del framework (ver gura 3.8). La clase
teEventInterceptor
Hiberna-
que referenciamos en la línea 1 será la encargada de escuchar los eventos producidos
por Hibernate y enviarlos a
SearchInterceptor.
La línea 8 le indica a Hibernate que ya puede congurarse,
mientras que las líneas 9 a 11 son especialmente importantes porque le indican a Hibernate que debe
comunicar los eventos CUD al framework de indexación.
Plugin iBATIS
Al igual que el plugin Hibernate, el plugin para iBATIS conecta el ORM con el motor de búsqueda para
que los eventos CUD generados en la aplicación se reejen en nuestro framework. Veamos un ejemplo.
Ejemplo 3.3.4
.
(Conguración de Plugin Interceptor de iBATIS)
Veamos cómo se congura un inter-
ceptor iBATIS a partir de XML y un fragmento de código.
El primer paso consiste en editar el archivo XML de conguración iBATIS para indicarle que utilice
nuestro plugin:
<p l u g i n s>
<p l u g i n i n t e r c e p t o r="com . j k l a s . s e a r c h . i n t e r c e p t o r s . i b a t i s .
I b a t i s 3 I n t e r c e p t o r "/>
</ p l u g i n s>
La clase
Ibatis3Interceptor
que denimos en esta conguración implementa la interfaz
Interceptor
de
iBATIS, lo que le permite recibir los eventos CUD.
Por último, las capas de indexación se interconectan como hicimos con el plugin de Hibernate:
new I b a t i s 3 I n t e r c e p t o r ( ) . s e t S e a r c h I n t e r c e p t o r (
new S e a r c h I n t e r c e p t o r (
new D e f a u l t I n d e x e r S e r v i c e (
new D e f a u l t I n d e x i n g P i p e l i n e ( ) ,
MemoryIndexWriterFactory . g e t I n s t a n c e () ) ) ) ;
Luego de seguir estos pasos, el framework de IR reejará en sus índices operaciones como las siguientes:
@Test
public void
P e r s o n I s I n d e x e d ( ) throws I O E x c e p t i o n {
Company c = new Company ( ) ;
c . s etI d (1) ;
P e r s o n p = new P e r s o n ( " J u l i á n " , " K l a s " , " j k l a s @ f i . uba . a r " ) ;
p . setCompany ( c ) ;
new
}
PersonDao ( s e s s i o n ) . i n s e r t P e r s o n ( p , " 123456 " ) ;
A s s e r t . a s s e r t E q u a l s ( 1 , MemoryIndex . g e t D e f a u l t I n d e x ( ) . g e t O b j e c t C o u n t ( ) ) ;
Hasta aquí hemos descripto el diseño de las funcionalidades de indexación. En la próxima subsección
expondremos el diseño del framework en términos priorizar los objetos recuperados según el modelo de
IR y las reglas de negocio.
122
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
3.3.5. Técnicas de Puntaje y Relevancia
En esta subsección vamos a presentar nuestra implementación de los modelos de similitud vectorial, el
ordenamiento por reglas y el ltrado de objetos.
Puntaje en el Modelo Vectorial: VectorRanker
Para implementar una fórmula de similitud en el modelo vectorial (recordemos que el modelo booleano
no tiene noción de similitud), debemos implementar la interfaz VectorRanker.
Presentemos dicha interfaz:
public
abstract
L i s t <V e c t o r R a n k e d R e s u l t > r a n k ( V e c t o r Q u e r y
vectorQuery ,
S e t <S i n g l e T e r m O b j e c t R e s u l t > u n s o r t e d R e s u l t s ,
MasterAndInvertedIndexReader
reader ) ;
Las implementaciones de esta interfaz tienen la responsabilidad de retornar una lista priorizada de resultados según la relevancia del modelo.
Analicemos b¯evemente los parámetros del método rank:
VectorQuery: representa la consulta del usuario en términos del modelo vectorial. Ésta es necesaria
para implementar fórmulas que tengan en cuenta los términos de la query.
Set<SingleTermObjectResult>: es el conjunto de postings obtenidos desde los índices. La implementación de hashCode e equals permite que convivan en el conjunto postings que referencian a un
mismo objeto desde diferentes postings lists (es decir, para distintos términos). Este conjunto es
vital para efectuar el cálculo de similitud.
MasterAndInvertedIndexReader: el lector de índices es necesario para conocer datos que intervienen
en la fórmula de similitud y que no corresponden a la query ni al conjunto puntual de resultados
(por ejemplo, el número global de objetos indexados).
Tal como describimos en la subsección (3.1.4), nuestro framework provee una implementación por defecto
de la fórmula de similitud vectorial a través de la clase DefaultVectorRanker.
En el capítulo 4 presentaremos resultados experimentales en los que analizaremos los resultados de DefaultVectorRanker en aplicaciones reales.
Ordenamiento por Reglas
Tanto al estudiar el estado del arte como al analizar esta propuesta de solución vimos que es necesario
tener un mecanismo de ordenamiento previo a la hidratación de objetos, interno a motor de búsqueda y
que respete las variables de negocio de la aplicación huésped. Para cumplir con estas premisas, nuestro
framework incorpora una capa de ordenamiento inmediatamente posterior a la similitud vectorial (en el
caso booleano se implementa inmediatamente después de la etapa de ltrado). Para enfatizar que esta
capa ejecuta previamente a la hidratación, también la llamaremos capa de pre -ordenamiento.
Si bien podríamos aplicar las reglas de negocio fuera del motor de búsqueda, esto requeriría conocer los
atributos del objeto para cada resultado de la recuperación, lo que se puede traducir en miles de consultas
al ORM (y potencialmente al RDBMS).
Para solucionar el problema de requerir el valor de los atributos desde el ORM, las capacidades de mapeo
de nuestro framework permiten almacenar atributos en el índice invertido tal que disponer de ellos al
momento de recuperar los objetos.
A diferencia de la similitud vectorial, las reglas de pre-ordenamiento dependen de cada aplicación, por lo
que no podemos proveer una regla por defecto. Sin embargo, cualquier implementación debe cumplir con
la siguiente interfaz:
3.3.
123
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
public
PreSort
interface
L i s t <?
public
{
O b j e c t R e s u l t > w o r k ( C o l l e c t i o n <?
extends
extends
ObjectResult > currentObjects ) ;
}
A continuación ejemplicamos la implementación de esta interfaz con un extracto de los tests del framework:
@Indexable
public
HardAndSoftRuleEntity
class
@SearchId
public
final
int
@SearchSort
public
final
int
@SearchSort
public
final
float
@SearchSort
public
final
String
@SearchField
public
public
final
{
id ;
proxy1 ;
proxy2 ;
String
HardAndSoftRuleEntity ( int
int
proxy3 ;
attribute ;
id ,
String
proxy1 ,
attribute ,
proxy2 ,
float
String
proxy3 )
{
this . id
= id ;
this . attribute
=
attribute ;
this . proxy1
= proxy1 ;
this . proxy2
= proxy2 ;
this . proxy3
= proxy3 ;
}
}
private
class
private
HardAndSoftRule
implements
PreSort
ValueHolder
implements
C o m p a r a b l e <V a l u e H o l d e r > {
class
public
ObjectResult
public
float
public
ValueHolder ( ObjectResult
this . okr
{
okr ;
score ;
= okr ;
this . score
okr ,
float
score )
{
=s c o r e ;
}
@Override
public
compareTo ( V a l u e H o l d e r
int
F l o a t . compare ( s c o r e ,
return
o)
{
o . score ) ;
}
}
private
public
final
Field
proxy1Field ,
HardAndSoftRule ( )
throws
proxy2Field
;
SecurityException ,
NoSuchFieldException
{
this . proxy1Field
= HardAndSoftRuleEntity . class . getDeclaredField ("
proxy1 " ) ;
this . proxy2Field
= HardAndSoftRuleEntity . class . getDeclaredField ("
proxy2 " ) ;
}
protected
final
return
() ) ;
}
@Override
boolean
objectAccepted ( ObjectResult
object )
{
HardAndSoftRuleEntity . c l a s s . e q u a l s ( o b j e c t . getKey ( ) . g e t C l a z z
124
CAPÍTULO 3.
public
L i s t <?
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
ObjectResult >
extends
w o r k ( C o l l e c t i o n <?
extends
ObjectResult > currentObjects )
{
i f ( c u r r e n t O b j e c t s== n u l l )
th row
new
I l l e g a l A r g u m e n t E x c e p t i o n ( " Can ' t
work
on
a
null
result
set ") ;
L i s t <V a l u e H o l d e r >
int
treated
= new
A r r a y L i s t <V a l u e H o l d e r >() ;
proxy1Max = 0 ;
( I t e r a t o r <?
for
i t e r a t o r () ;
extends
ObjectResult >
iterator
= currentObjects .
i t e r a t o r . hasNext ( ) ; )
{
ObjectResult
okr = ( ObjectResult )
i f ( ! objectAccepted ( okr ) )
i t e r a t o r . next () ;
continue ;
i t e r a t o r . remove ( ) ;
t r e a t e d . a d d ( new
V a l u e H o l d e r ( okr ,
0f)) ;
proxy1Value = ( I n t e g e r ) okr . g e t S t o r e d F i e l d s () . get (
int
proxy1Field ) ;
i f ( p r o x y 1 V a l u e >p r o x y 1 M a x )
proxy1Max = p r o x y 1 V a l u e ;
}
( ValueHolder
for
valueHolder
:
treated )
{
proxy2Value =
float
( f l o a t ) valueHolder . okr . g e t S t o r e d F i e l d s () . get ( proxy2Field )
( float )
proxy1Max ;
valueHolder . score = proxy2Value ;
}
Collections . sort ( treated ) ;
L i s t <O b j e c t R e s u l t >
result
=
A r r a y L i s t <O b j e c t R e s u l t >( t r e a t e d . s i z e ( ) + c u r r e n t O b j e c t s .
new
size () ) ;
( ValueHolder
for
valueHolder
:
treated )
{
r e s u l t . add ( v a l u e H o l d e r . o k r ) ;
}
r e s u l t . addAll ( currentObjects ) ;
return
result ;
}
}
@Test
public
BooleanRetrievalMaxRule ()
void
throws
SecurityException ,
NoSuchFieldException
{
HardAndSoftRuleEntity
,
=
to
be
retrieved" ,
1
,
50.0 f
"A" ) ;
HardAndSoftRuleEntity
new
entity1
HardAndSoftRuleEntity (0 , " Something
new
entity2
=
HardAndSoftRuleEntity (1 , " Another
20.0 f
,
"A" ) ;
HardAndSoftRuleEntity
entity3
=
thing
to
be
retrieved" ,
5
,
/
3.3.
125
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
HardAndSoftRuleEntity (2 , " I
new
f
,
should
U t i l s . setupSampleMemoryIndex ( e n t i t y 1 ,
BooleanSearch
s e a r c h = new
r e t r i e v e d " ) . getQuery () ,
L i s t <?
be
retrieved
too ! " ,
10
,
10.0
"A" ) ;
extends
entity2 ,
BooleanSearch (
entity3 ) ;
BooleanQueryParser ("
new
MemoryIndexReaderFactory . g e t I n s t a n c e ()
ObjectResult >
results
) ;
= s e a r c h . s e a r c h ( new
HardAndSoftRule ( ) ) ;
Assert . assertEquals (2 ,
r e s u l t s . get ( 0 ) . getKey ( ) . g e t I d ( ) ) ;
Assert . assertEquals (1 ,
r e s u l t s . get ( 1 ) . getKey ( ) . g e t I d ( ) ) ;
Assert . assertEquals (0 ,
r e s u l t s . get ( 2 ) . getKey ( ) . g e t I d ( ) ) ;
}
@Test
public
VectorRetrievalMaxRule ()
void
throws
SecurityException ,
NoSuchFieldException
{
HardAndSoftRuleEntity
entity1
=
HardAndSoftRuleEntity (0 , " Something
new
,
HardAndSoftRuleEntity
entity2
20.0 f
,
entity3
1
,
50.0 f
,
thing
to
be
retrieved" ,
5
,
,
10.0
=
HardAndSoftRuleEntity (2 , " I
f
retrieved" ,
"A" ) ;
HardAndSoftRuleEntity
new
be
=
HardAndSoftRuleEntity (1 , " Another
new
to
"A" ) ;
should
be
retrieved
too ! " ,
10
"A" ) ;
U t i l s . setupSampleMemoryIndex ( e n t i t y 1 ,
VectorSearch
s e a r c h = new
entity2 ,
entity3 ) ;
VectorSearch (
new
VectorQueryParser ( " r e t r i e v e d " ) . getQuery () ,
new
MemoryIndexReader ( )
) ;
L i s t <?
extends
ObjectResult >
results
= s e a r c h . s e a r c h ( new
HardAndSoftRule ( ) ) ;
Assert . assertEquals (2 ,
r e s u l t s . get ( 0 ) . getKey ( ) . g e t I d ( ) ) ;
Assert . assertEquals (1 ,
r e s u l t s . get ( 1 ) . getKey ( ) . g e t I d ( ) ) ;
Assert . assertEquals (0 ,
r e s u l t s . get ( 2 ) . getKey ( ) . g e t I d ( ) ) ;
}
Analicemos este extracto de código:
Los métodos BooleanRetrievalMaxRule y VectorRetrievalMaxRule muestran cómo una aplicación utilizaría las capacidades de pre-ordenamiento de nuestro framework. Estos métodos generan entidades,
las indexan y luego las recuperan indicando su implementación de PreSort.
La clase HardAndSoft rule implementa dos criterios de negocio. El primero es una regla dura que
indica que las entidades de tipo HardAndSoftEntity deben ubicarse primero que el resto. El segundo
criterio es una regla blanda que busca el máximo global de un atributo de HardAndSoftEntity y
establece el puntaje como la división del otro atributo de la entidad sobre el máximo global.
Veamos que el usuario de las reglas de pre-ordenamiento sólo conoce el nombre de la clase que
implementa las reglas, siendo que no es necesario conocer nada mas acerca de cada regla. Esto es
importante porque separa al usuario experto que implementa una regla del usuario que tiene un
conocimiento mas básico del framework.
126
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Por último, es importante notar que el mismo conjunto de reglas fue aplicado en una búsqueda
vectorial y en una booleana, sin alterar en ninguna forma dichas reglas. Este es un claro ejemplo
de la independencia entre capas.
En el próximo apartado analizamos la operación de ltrado, la cual completa el circulo de requerimientos
básicos para un uso real.
Filtrado
Al igual que permitimos un pre-ordenamiento de objetos recuperados, también debemos permitir la
eliminación de objetos que no cumplan con requisitos de negocio. Esta eliminación o ltrado es llamada
pre-ltrado y quienes realizan esta acción son los pre-ltros.
Como ya hemos comentado en capítulos y secciones previas, el hecho de eliminar los objetos previamente
a su valoración es especialmente importante en corpus cuyas consultas recuperan muchos objetos. Esto
se debe a que la operación de ltrado es de orden
O (n)
mientras que la de ordenamiento es
O (n log n).
Es decir, si ltramos antes de ordenar estamos mejorando el tiempo total de recuperación.
El ltrado de objetos involucra las siguientes clases e interfaces:
FilterChain: interfaz cuyas implementaciones mantienen la lista de ltros a aplicar y los ejecutan en
el orden especicado.
ResultFilter: interfaz que implementan los pre-ltros.
Presentemos la interfaz FilterChain:
public
interface
public
abstract
FilterChain
void
{
a p p l y F i l t e r s ( C o l l e c t i o n <?
extends
ObjectResult >
unfilteredResults ) ;
}
y ResultFilter:
public
interface
public
boolean
ResultFilter
{
i s F i l t e r e d ( ObjectResult
filtrable ) ;
}
A continuación vemos un ejemplo en el que implementamos esta interfaz para generar un pre-ltro.
Ejemplo 3.3.5.
El siguiente es ejemplo de un ltro que elimina del conjunto de resultados las clases que
no pertenecen a cierta jerarquía:
public c l a s s
ClassFilter
implements
ResultFilter {
private f i n a l C l a s s <?> f i l t e r C l a z z ;
private f i n a l boolean a l l o w S u b c l a s s e s ;
public C l a s s F i l t e r ( C l a s s <?> f i l t e r C l a z z , boolean a l l o w S u b c l a s s e s )
i f ( f i l t e r C l a z z==n u l l )
throw new I l l e g a l A r g u m e n t E x c e p t i o n ( "The c l a z z t o be u s ed f o r
f i l t e r i n g can ' t be n u l l " ) ;
}
this . f i l t e r C l a z z = f i l t e r C l a z z ;
this . allowSubclasses = allowSubclasses ;
public C l a s s F i l t e r ( C l a s s <?>
i f ( f i l t e r C l a z z==n u l l )
filterClazz ) {
{
3.3.
127
DISEÑO DEL FRAMEWORK DE IR SOBRE OBJETOS
throw new
I l l e g a l A r g u m e n t E x c e p t i o n ( "The c l a z z t o be u s ed f o r
f i l t e r i n g can ' t be n u l l " ) ;
}
this . f i l t e r C l a z z = f i l t e r C l a z z ;
t h i s . a l l o w S u b c l a s s e s = true ;
public boolean i s F i l t e r e d ( O b j e c t R e s u l t f i l t r a b l e ) {
i f ( allowSubclasses ) {
return ! f i l t e r C l a z z . i s A s s i g n a b l e F r o m ( f i l t r a b l e . getKey ( ) . g e t C l a z z ( ) ) ;
} else {
return ! f i l t e r C l a z z . e q u a l s ( f i l t r a b l e . getKey ( ) . g e t C l a z z ( ) ) ;
}
}
}
En este extracto de código el ltro recibe el nombre de la clase que admite y un ag para determinar si
admite la jerarquía completa o sólo la clase especicada.
Como ejercicio para entender mejor el diseño, supongamos que necesitamos implementar un ltro de
seguridad que elimina del conjunto de resultados los objetos a los que el operador actual no tiene acceso.
Para esto podríamos implementar un ltro similar al del ejemplo anterior, controlando que la clave del
objeto esté en la lista de objetos que ése operador puede leer.
Otro n que se le puede dar a los ltros es la implementación de la búsqueda facetada. Normalmente, para
implementar la búsqueda facetada necesitamos contar cuántos objetos cumplen con el criterio de cada
4
faceta. Tanto esta operación como la de ltrado se pueden realizar en simultaneo , siendo que el ltrado
tendrá efecto sobre el framework de IR y el recuento deberá ser consultado por el usuario del ltro.
Para soportar tanto la búsqueda facetada como la normal, tenemos dos implementaciones de FilterChain:
ImmediateRemoveFilterChain: esta implementación elimina de la lista de resultados los objetos que
no pasan por un ltro, evitando que pasen por el resto de ellos.
LateRemoveFilterChain: si queremos que el framework de IR pase todos los objetos por todos los
ltros y luego los ltre debemos agregar los ltros a esta implementación. Debemos tener en cuenta
que este tipo de FilterChain genera mayor trabajo sobre los últimos ltros de la cadena.
Por último, queremos resaltar que al igual que ocurrió con los pre-ordenamientos, los pre-ltros no dependen del modelo de IR en el que trabajemos.
En este capítulo cumplimos el objetivo de analizar y diseñar una solución al problema de IR sobre objetos
de un modelo de dominio, basándonos en los conceptos de recuperación de información, diseño de software
y persistencia de objetos que estudiamos en el capítulo 2.
En el próximo capítulo ponemos a prueba nuestra solución implementando tres aplicaciones con necesidades de information retrieval, las cuales se deberán satisfacer a través de nuestra herramienta.
4 Teniendo
el cuidado de indicarle al framework de IR que no elimine los objetos hasta pasarlos por todos los ltros, ya
que de otra forma los últimos ltros contarían menos objetos de los que realmente hay.
128
CAPÍTULO 3.
DESARROLLO DE LA PROPUESTA DE SOLUCIÓN
Capítulo 4
Experimentación
El objetivo de este capítulo es mostrar cómo probamos la adecuación de la solución propuesta en el
capítulo 3 al problema de IR sobre objetos.
La organización de este capítulo es la siguiente:
La sección 4.1 explica qué tipos de pruebas se diseñaron para vericar la validez de la propuesta.
La sección 4.2 muestra las tres aplicaciones que utilizamos para validar nuestro framework y analiza
distintos aspectos de la integración con estas aplicaciones.
Por último, la sección 4.3 desarrolla las pruebas comparativas cuantitativas y cualitativas.
4.1.
Tipo de Pruebas Efectuadas
En esta sección explicaremos los distintos tipos de pruebas que se hicieron para vericar la validez de la
solución propuesta en el capítulo 3.
Recordemos que el framework construido no es un sistema completo por sí mismo sino que da servicio
a aplicaciones que necesitan capacidades de indexación y búsqueda. Esto requiere que veriquemos la
capacidad del framework de adaptarse a distintos dominios y aplicaciones, característica que llamaremos
portabilidad .
Para vericar la portabilidad decidimos aplicar el primero de los patrones que propone Johnsonn para
el desarrollo de un framework: comenzar implementando tres aplicaciones de referencia sobre las cuales
luego probar la adecuación del framework (Roberts y Johnson, 1996). Estas aplicaciones pueden ser
prototipos, pero es importante que hagan algo real. Las tres aplicaciones construidas
1 son PetClinic,
Klink y KStore.
PetClinic es una aplicación articial creada por los desarrolladores del framework Spring para mostrar
el funcionamiento de Spring Framework y es un estándar de facto con el que distintos frameworks como
Compass, iBATIS, Hibernate y otros han mostrado su funcionamiento.
KStore y KLink son aplicaciones articiales creadas especícamente para este trabajo. La primera simula
una tienda online de productos generales y la segunda una red de contactos entre personas.
Dentro de estas pruebas con aplicaciones reales vericamos el funcionamiento del framework con ORMs
como Hibernate e iBATIS.
Además de estas pruebas con aplicaciones de referencia, hicimos pruebas de calidad. Las pruebas de
calidad buscan reejar que el sistema que se construyó es correcto. Para probar este punto, ejecutamos
una serie de tests unitarios que nos aseguran el correcto comportamiento de la herramienta ante los casos
probados y calculamos métricas de cobertura de código.
1A
diferencia de Klink y KStore, PetClinic no fue construida para este trabajo sino que fue mayormente tomada de la
implementación que acompaña a Compass. En adelante, para facilitar la redacción, evitaremos marcar esta excepción en
forma explícita.
129
130
CAPÍTULO 4.
EXPERIMENTACIÓN
Las pruebas comparativas de rendimiento buscan comprobar si existe un comportamiento anómalo en el
rendimiento del framework y comprender qué grado de competitividad tiene respecto de las herramientas
del estado del arte.
Por último, las pruebas comparativas cualitativas buscan encontrar las diferencias en la integración de los
distintos frameworks con las aplicaciones.
A continuación presentamos las pruebas con aplicaciones de referencia.
4.2.
Pruebas con Aplicaciones de Referencia
4.2.1. PetClinic
Descripción General
PetClinic es una aplicación web que implementa el negocio de una veterinaria y es una aplicación de
referencia con la que frameworks como Spring muestran su funcionamiento.
Las entidades más importantes en este dominio son las mascotas (Pet), dueños (Owner), visitas (Visit),
veterinarios (Vet) y la clínica misma (Clinic).
Para nuestro objetivo de probar la integración y adecuación del framework de IR sobre PetClinic, mantuvimos las tecnologías y casos de uso originales, agregando el mínimo conjunto de funcionalidades necesarias
para dar a la aplicación capacidades de IR.
PetClinic es una aplicación de tres capas (presentación, negocio y acceso a datos) construida con el patrón
model-view-controller (MVC) a través de Spring MVC.
La capa de presentación de PetClinic produce vistas HTML utilizando tecnologías como Java Server
Pages (JSP) , Java Standard Tag Libraries (JSTL) y Spring Web.
La capa de negocio se basa en objetos simples Java que colaboran entre si y aprovechan facilidades del
framework Spring como inyección de dependencias.
La capa de acceso a datos está implementada sobre el ORM Hibernate, por lo que la integración con
PetClinic sirvió para hacer una prueba real de integración con este ORM.
Casos de Uso y Modelo de Dominio
Los casos de uso de PetClinic son:
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
131
Figura 4.1: Casos de Uso en PetClinic.
Vamos a describir brevemente el propósito de cada caso de uso:
See Search Stats: permite conocer el número de objetos indexados por índice.
Search: representa la búsqueda de entidades utilizando el motor de IR a partir de una expresión de
lenguaje natural.
Find Owner: este caso de uso sirve para encontrar un dueño utilizando Hibernate, es decir, generando
una consulta a la base de datos sin la inteligencia del motor de búsqueda.
Manage Pet: permite agregar y modicar la información de una mascota. La información ingresada
se indexa en el motor de IR.
Add Visit: agrega una visita de una mascota a la clínica. La información ingresada se indexa en el
motor de IR.
Show Vets & Specialties: visualiza los veterinarios y sus especialidades.
Manage Pet Owners: permite agregar dueños así como modicar sus datos. La información ingresada
se indexa en el motor de IR.
Veamos un diagrama de clases de las entidades de dominio:
132
CAPÍTULO 4.
EXPERIMENTACIÓN
Figura 4.2: Entidades de dominio en PetClinic.
Para esta prueba seleccionamos un subconjunto de las clases de dominio y las anotamos para volverlas
indexables. Respecto del diagrama de clases de la gura (4.2), en la gura (4.3) agregamos estereotipos
para indicar cómo anotamos las clases para su indexación.
Figura 4.3: Entidades de dominio en PetClinic con información de indexación.
El diseño de clases de la gura (4.2) es el original de PetClinic. Esto es, no se adaptó de ninguna forma
para utilizar nuestro framework. El hecho de no requerir ningún tipo de modicación en el dominio marca
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
133
el éxito de uno de los criterios de diseño de un buen framework: independencia del modelo de dominio
(ver subsección 2.2.2).
Dada la extensiva utilización de la subclasicación en el diseño de PetClinic, fue necesario utilizar y
extender el framework de IR para cubrir casos complejos. Veamos algunos de ellos:
Person: junto a los objetos de tipo Pet, los objetos de esta clase son los principales objetivos de
indexación en la aplicación. El identicador de esta clase se encuentra en la superclase (concretamente en la clase Entity) pero los objetos concretos a indexar serán principalmente de las subclases
Owner y Vet. Por otro lado, los atributos indexables se encuentran principalmente en la clase Person.
Para soportar este mapeo, la anotación @Indexable de la clase Person indica la propiedad climbingTarget=Entity.class, lo cual obliga a recorrer la jerarquía de clases en busca de atributos e identicadores.
A su vez, la anotación @Indexable fue necesario indicar la propiedad makeSubclassesIndexable=true
tal que las clases Owner y Vet sean indexadas automáticamente.
Owner: el principal desafío en mapear esta clase consiste en que casi todos sus atributos se ubican
en superclases, con excepción de una colección de mascotas, la cual también se indexa.
Pet: los objetos de esta clase no sólo son indexables por si mismos sino que contienen dos atributos
indexables por separado: un Owner y un PetType. Particularmente, estos dos objetos referencian a
la mascota, de forma tal que cuando se indexa una mascota, una posterior búsqueda de su dueño
retornará entre los resultados a la mascota.
El mapeo de todas las entidades se hizo sobre un mismo índice, es decir, no hubo necesidad de particionar
las entidades en índices distintos. Tampoco fue necesario utilizar características avanzadas del framework
como ltrado de objetos y ordenamientos ad hoc.
Funciones de IR en PetClinic
Sumado a los casos de uso originales de la aplicación (administración de dueños, mascotas y visitas), se
agregó un caso de uso de búsqueda de entidades.
En términos visuales, se implementó un cuadro de texto donde ingresar la expresión de búsqueda (query)
y una vista donde se presentan las entidades que coincidieron con la query (gura 4.4).
Figura 4.4: Cuadro de búsqueda (arriba a la izquierda) y resultados de búsqueda (centro).
La búsqueda de entidades se hace mediante un modelo vectorial simple utilizando la familia de fórmulas
TF-IDF. Dado que nuestra estrategia de indexación permite recuperar objetos de tipos heterogéneos,
agregamos una pequeña capa con la inteligencia de dirigir los clics de cada tipo de entidad a su pantalla
correspondiente (las mascotas tienen una pantalla de edición distinta a los dueños).
Pruebas de Relevancia
A continuación describimos una prueba de relevancia para un pequeño conjunto de datos generados,
analizando los resultados de ejecutar distintas queries sobre el sistema.
El conjunto de datos que se generó utilizando la interfaz web de PetClinic es el siguiente:
134
CAPÍTULO 4.
EXPERIMENTACIÓN
3 (tres) dueños: Julián Klas, Pedro Klas y Pedro de Mendoza.
2 (dos) mascotas: Fido (mascota de Pedro Klas) y Cuky (mascota de Julián Klas)
3 (tres) tipos de mascotas: Cat, Dog y Other
3 (dos) visitas de mascotas, una de Fido y dos de Cuky
Para analizar mejor los resultados del sistema de IR, hicimos una modicación a la interfaz de la gura
(4.4) que incluye el puntaje que obtuvo cada elemento recuperado (gura 4.5).
Ejemplo 4.2.1.
Ante la query
Julián,
los resultados son:
Figura 4.5: Resultados para la query Julián sobre el conjunto de datos de prueba.
Para analizar los puntajes debemos tener en cuenta:
se utiliza TF-IDF sobre los términos coincidentes entre la query y el ítem,
hay un total de
N = 11
objetos indexados,
al indicarle al framework de IR que debe indexar las mascotas junto a su dueño, cualquier match
en el dueño también aplica a la mascota. Entonces
df (Julian) = 2
porque se indexó tanto para el
dueño como la mascota.
el único atributo coincidente entre la query y el corpus es el atributo
para el dueño
Julián Klas.
Analicemos entonces el puntaje entre la query
Julián
rstName
de la clase
Owner
y el objeto coincidente:
SimilitudT F −IDF (query, owner) = tf (Julian) × idf (Julian)
= tf (Julian) × log10
N
11 ∼
= 1 × log10
= 0,7404
df (Julián)
2
Para el caso del objeto la mascota el cálculo es idéntico sólo que la referencia a la identidad recuperada
apunta a la mascota.
Ejemplo 4.2.2.
Veamos la respuesta del buscador y su análisis ante la query
Pedro Klas:
Figura 4.6: Resultados para la query Pedro Klas sobre el mismo corpus del ejemplo previo.
4.2.
135
PRUEBAS CON APLICACIONES DE REFERENCIA
A continuación analizamos el resultado posición por posición, teniendo en cuenta:
dfP EDRO = 3
Fido.
porque se referencia desde los dueños
dfKLAS = 4 porque se referencia desde los dueños
mascotas Fido y Cuky.
Pedro de Mendoza, Pedro Klas
y su mascota
Pedro Klas y Julián Klas, mas sus correspondientes
Si analizamos el resultado posición por posición:
Resultado 1: es el elemento del corpus de mayor coincidencia y su ubicación se explica conceptualmente porque es el único que contiene todos los términos de la query. Haciendo el cálculo formal
de similitud:
SimilitudT F −IDF (query, owner) = tf (Pedro) × idf (Pedro) + tf (Klas) × idf (Klas)
= 1 × log10
11
11
+ 1 × log10
= 1, 003604
3
4
Resultado 2: es idéntico al anterior porque el mapeo que hicimos de la clase
Pedro Klas
Owner.
dueño
el
referenciando a la mascota
Fido.
Pet
hizo indexar a su
Es decir, la similitud es igual a la anterior sobre
Resultado 3: naturalmente este resultado aparece por la coincidencia parcial en el término
Pedro pero
vemos que tiene un puntaje sensiblemente inferior a los primeros dos resultados por la coincidencia
en un único término. Calculando la similitud:
SimilitudT F −IDF (query, owner) = tf (Pedro) × idf (Pedro) = 1 × log10
11
= 0, 564271
3
Resultado 4: nuevamente tenemos coincidencia parcial, pero ahora en el término
Klas.
Aquí vemos
cómo la familia de formulas TF-IDF da menor prioridad a los términos de poca selectividad de documentos. A diferencia del término
Pedro, el
Klas
cual se asociaba a 3 objetos, el término
a 4 objetos. Esto hace que el término
tenga menor relevancia que
Pedro,
Klas
referencia
reejándose en la
similitud:
SimilitudT F −IDF (query, owner) = tf (Klas) × idf (Klas) = 1 × log10
Resultado 5: al igual que con el segundo resultado, la mascota
su dueño
Julian Klas
Cuky
11
= 0, 439333
4
obtiene la misma similitud que
de la posición anterior.
Otras características de la solución
La interconexión entre componentes como el ORM, el motor de IR y PetClinic se hizo utilizando la object
factory del framework Spring.
Para reejar los eventos CUD sobre el índice invertido utilizamos nuestro plugin de Hibernate, el cual
se conectó al sistema de eventos de Hibernate Core mediante Spring. El interceptor de eventos fue
congurado para recibir los eventos luego de que éstos se ejecutan en Hibernate.
Como adelantamos al inicio de este apartado, tanto el pipeline de indexación como el tipo de índice
(memoria, Berkeley u otro) son congurables mediante Spring. Veamos un fragmento del XML que
usamos para congurar el uso del índice en memoria y el pipeline de indexación por defecto:
136
CAPÍTULO 4.
<b e a n
i d=" O b j e c t S e a r c h H i b e r n a t e L i s t e n e r "
EXPERIMENTACIÓN
c l a s s ="com . j k l a s . s e a r c h .
i n t e r c e p t o r s . h i b e r n a t e . H i b e r n a t e E v e n t I n t e r c e p t o r ">
<c o n s t r u c t o r
−a r g
r e f =" S e a r c h I n t e r c e p t o r " />
</ b e a n>
<b e a n
i d=" S e a r c h I n t e r c e p t o r "
c l a s s ="com . j k l a s . s e a r c h . i n t e r c e p t o r s .
S e a r c h I n t e r c e p t o r ">
<c o n s t r u c t o r
−a r g
r e f =" O n l i n e I n d e x e r " />
</ b e a n>
<b e a n
i d=" O n l i n e I n d e x e r "
c l a s s ="com . j k l a s . s e a r c h . i n d e x e r . o n l i n e .
O n l i n e I n d e x e r ">
<c o n s t r u c t o r
−a r g
r e f =" D e f a u l t I n d e x e r S e r v i c e " />
</ b e a n>
<b e a n
i d=" D e f a u l t I n d e x e r S e r v i c e "
c l a s s ="com . j k l a s . s e a r c h . i n d e x e r .
D e f a u l t I n d e x e r S e r v i c e ">
<c o n s t r u c t o r
<c o n s t r u c t o r
−a r g
−a r g
r e f =" D e f a u l t I n d e x i n g P i p e l i n e " />
r e f =" I n d e x W r i t e r F a c t o r y " />
</ b e a n>
<b e a n
i d=" D e f a u l t I n d e x i n g P i p e l i n e "
c l a s s ="com . j k l a s . s e a r c h . i n d e x e r .
p i p e l i n e . D e f a u l t I n d e x i n g P i p e l i n e " />
<b e a n
i d=" I n d e x W r i t e r F a c t o r y "
c l a s s ="com . j k l a s . s e a r c h . i n d e x . memory .
M e m o r y I n d e x W r i t e r F a c t o r y " />
Comentemos brevemente la conguración de estos beans
2:
IndexWriterFactory: selecciona el backend/plugin con el que se escribe en el índice invertido. En
este caso se eligió el índice en memoria que viene incluido dentro del framework. Veamos que la
inyección de dependencias permite variar la implementación del backend de índices invertidos de
forma transparente a PetClinic, sin modicar el código de negocio.
DefaultIndexerService: se ocupa de orquestar el procesamiento de los objetos para su indexación y
la posterior escritura en el índice. Es preciso notar que estamos inyectando dos dependencias en
su constructor: el backend de escritura en el índice y el pipeline de procesamiento de objetos. Esta
inyección de dependencias nos permite variar sustancialmente el comportamiento del framework sin
cambiar una sola línea de código en el framework o PetClinic.
ObjectSearchHibernateListener: este bean es inyectado en otro bean de Hibernate y congura la
interceptación de eventos por parte del framework de IR.
OnlineIndexer: este bean congura el indexador online que analizamos y diseñamos en las secciones
(3.1.3) y (3.3.4). Editando este bean podemos hacer que nuestro framework utilice indexación online,
semi-online u oine sin tener otra consideración de código sobre la aplicación.
Como acabamos de ver, el medio de almacenamiento del índice invertido se puede variar por conguración
utilizando la object factory de Spring. De la misma forma podemos intercambiar el tipo de indexación
online por el semi-online o el oine.
Dado que esta aplicación fue la primera con la que probamos la integración del framework de IR, se
buscó reducir la complejidad inicial para centrarse en las funcionalidades de IR y en eventuales ajustes al
framework. Siguiendo este criterio, las pruebas las desarrollamos utilizando indexación online sobre un
índice en memoria. Si bien el índice en memoria es volatil y sólo se utiliza para pruebas, en las próximas
aplicaciones de ejemplo sí utilizaremos un índice persistente.
2 En
este contexto, un bean es un objeto Java que instanciamos utilizando la Object Factory de Spring y el cual se utiliza
en un contexto de inyección de dependencias.
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
137
4.2.2. Klink
Descripción General
Klink es una aplicación web articial creada para esta tesis con el objetivo de probar la adecuación del
framework a distintos escenarios, siguiendo el patrón propuesto por Johnson (ver sección 4.1).
Klink implementa una red social básica en la que las personas pueden agregar contactos libremente. Los
conceptos más importantes en Klink son: el usuario como miembro del sitio (Person), los contactos (también de tipo Person), el perl (atributos de la clase Person), la compañía para la que trabaja (Company)
y el país en el que esta reside (Country).
Al igual que hicimos con PetClinic, esta aplicación se construye utilizando Spring MVC como framework
estructural y JSP/JSTL como framework de presentación. Se utilizó el framework Spring Core para
inyectar dependencias en los objetos y congurar la indexación.
La capa de negocio es elemental y se ocupa principalmente de la comunicación con la capa de acceso a
datos implementada con Apache iBATIS.
El hecho de utilizar iBATIS para la capa de acceso a datos no es accidental sino que fue elegida para probar
la adecuación del framework a distintos entornos de persistencia (recordemos que PetClinic utilizaba
Hibernate).
Casos de Uso y Modelo de Dominio
Las funcionalidades de Klink se pueden ver fácilmente en este diagrama de casos de uso:
Figura 4.7: Casos de uso en Klink.
Expliquemos brevemente este diagrama:
Unregistered User: es un usuario no registrado en el sistema y por tanto sólo puede ejecutar el caso
3
de uso Signup .
3 En
rigor, también puede ejecutar el caso de uso Login, el cual no le permitirá acceder al sistema por no estar registrado.
Preferimos no mostrar esta relación en el diagrama porque se presta a confusión.
138
CAPÍTULO 4.
•
EXPERIMENTACIÓN
Signup: este caso de uso permite al usuario formar parte de la red de contactos y requiere el
ingreso de ciertos datos en el sistema. Luego de ejecutarse el caso de uso, el usuario deja de
estar representado por el actor unregistered user para verse representado por el registered user.
Registered User: son los usuarios que ejecutaron con éxito el caso de de uso Signup.
•
Login: mediante una contraseña y una dirección de correo electrónico, el caso de uso valida
que el usuario que lo ejecuta sea un registered user. En caso de que el usuario demuestre serlo,
el sistema le permite ejecutar los casos de uso Search Friends, Add Friends, View Home Page y
Logout. En caso de no vericarse que el usuario sea un registered user, se indica al usuario que
revise los datos ingresados o que ejecute el caso de uso Signup.
•
Search Friends: este caso de uso permite al usuario encontrar personas a partir de una expresión
de lenguaje natural, utilizando el motor de búsqueda. Entre los resultados de la búsqueda
pueden haber tanto personas relacionadas con el usuario como personas no relacionadas. En
base al listado resultante de este caso de uso, el usuario puede iniciar el caso de uso Add Friend
sobre las personas no relacionadas con él.
•
Add Friend: este caso de uso agrega una relación entre el usuario y la persona seleccionada en
el caso de uso Search Friends.
•
View Home Page: el usuario inicia este caso de uso para visualizar sus datos propios. Este caso
de uso permite no sólo ver la página de inicio (Home) sino una pantalla de perl (Prole).
•
Logout: se ejecuta cuando el usuario desea dejar de usar el sistema. Luego de ejecutar este
caso de uso, no será posible ejecutar otro caso de uso que no sea el de Login (luego del cual se
volverá a permitir ejecutar el resto de los casos de uso).
El diagrama de clases de Klink es el siguiente:
Figura 4.8: Diagrama de clases en Klink.
Marcando con estereotipos las anotaciones sobre las clases:
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
139
Figura 4.9: Diagrama de clases y anotaciones en Klink.
Hagamos una breve descripción acerca de estas entidades:
Person: representan a los usuarios del sistema como entes sociales que se interconectan entre sí.
Estos objetos se indexan por sus datos de contacto: email, rstName y lastName.
Los elementos de la colección contacts se indexan referenciando tanto al objeto que contiene la
colección (contenedor) como al objeto que está dentro de la colección (contenido). Con este mapeo,
una persona se indexa referenciando a todos sus contactos y ellos a su vez referencian a éste.
Por último, las personas están asociadas a exactamente un Company, la cual se indexa a referenciando a su contenedor (objetos de tipo Person). Esto permite buscar una compañía y recuperar
sus empleados.
Company: esta entidad representa una empresa donde trabajan personas y, como vimos, se indexa
de forma tal de referenciar a todas las personas que trabajan en esa compañía.
Country: representa la información de nacionalidad en el sistema. Es un atributo de Company y no
se indexa.
A continuación presentamos algunas capturas de pantalla que muestran partes de los ujos correspondientes a los casos de uso de la gura (4.7).
Comencemos con Login y Signup:
140
CAPÍTULO 4.
EXPERIMENTACIÓN
Figura 4.10: Login. El primer paso en el sistema es la pantalla de login, desde donde podemos identicarnos
o agregarnos como usuarios registrados.
Figura 4.11: Signup. Creamos un usuario en el sistema, el cual se indexará por sus datos de contacto y
por la compañía a la que pertenece.
Veamos ahora los ujos del caso de uso View Home Page:
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
141
Figura 4.12: Sección Home. Esta sección nos muestra un mensaje de bienvenida y, eventualmente, mensajes
de otros usuarios o noticaciones (correo nuevo, invitaciones, etc).
Figura 4.13: Sección Prole. Vemos la información pública hacia el resto de la comunidad.
Ahora podemos buscar a un Juan que recordamos que trabaja en FIUBA utilizando el caso de uso Search
Friends:
142
CAPÍTULO 4.
EXPERIMENTACIÓN
Figura 4.14: Search Friends. Ingresamos la query juan uba y obtenemos los resultados priorizados por el
modelo vectorial.
utilizando los resultados de la gura anterior, hacemos clic en el contacto Juan Ale y seguimos el ujo de
Add Friend:
Figura 4.15: Add Friend y Search. Utilizando los resultados de la búsqueda anterior agregamos a Juan Ale
como contacto de Julian Klas.
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
143
Figura 4.16: Add Friend y Search. Repetimos la búsqueda original y visualizamos que Juan Ale ya es
contacto nuestro.
En el próximo apartado describimos en mayor detalle las funciones de IR de esta aplicación.
Funciones de IR en Klink
En esta aplicación utilizamos el framework de IR para implementar el caso de uso Search Friends. Este
caso de uso es central ya que es el punto de acceso para la construcción de la red de contactos (caso de
uso Add Friend).
A diferencia de PetClinic, el modelo de Klink es muy simple. Las clases indexables en el dominio de esta
aplicación son las personas y compañías (instancias respectivas de Person y Company).
El mapeo que hicimos de la entidad Person nos permite recuperarlas según su correo electrónico, nombre,
apellido y contactos. En el caso de las compañías, la recuperación se da por el nombre de ésta.
En todos los casos es necesario establecer un identicador de las entidades ya que la hidratación de los
objetos se efectúa con posterioridad a la tarea de IR.
Los puntos clave que marcan el éxito de este ejemplo fueron:
variación del modelo de dominio sin impactar en el framework (portabilidad)
utilización del plugin para iBATIS
utilización del plugin de indexación Berkeley
En el próximo apartado hacemos algunos comentarios tecnológicos sobre este ejemplo/caso de estudio.
Otras características de la solución
Al igual que en el caso anterior (PetClinic), utilizamos Spring y su object factory para la conguración
e interconexión de componentes.
144
CAPÍTULO 4.
EXPERIMENTACIÓN
Los eventos CUD se reejaron sobre el índice mediante el plugin para iBATIS provisto por nuestro
framework, el cual interceptó los eventos de persistencia de manera similar al plugin de Hibernate que
utilizamos en PetClinic. La base de datos utilizada para almacenamiento de datos de aplicación fue
HSQLDB (HyperSQL, 2008).
Al presentar PetClinic mencionamos que utilizamos el índice en memoria para acotar la complejidad a
la indexación y recuperación. En Klink movimos el foco de la complejidad para abarcar otros ángulos
tecnológicos del problema. Este movimiento se materializó en la utilización del índice invertido en disco
Berkeley.
Respecto de PetClinic, la utilización del backend en disco Berkeley sólo requirió cambios de conguración en Spring, lo cual probó la transparencia con la que el framework permite elegir el backend de
almacenamiento del índice invertido.
Para generar el conjunto de datos de prueba se obtuvieron tablas de nombres de personas del repositorio
público Freebase (Metaweb Techonologies, 2010). Estos datos se importaron mediante un proceso index-
ador oine. El indexador lee los datos desde un archivo de texto plano y genera objetos de tipo Person,
los cuales se persistieron con iBATIS y se indexaron automáticamente por el plugin de iBATIS. Dada la
simplicidad del proceso indexador, concluimos que no tiene sentido proveer un indexador genérico desde
el framework.
Además, tenemos un proceso reindexador que lee todos los registros de la base de datos y los indexa en
nuestro framework. Veamos el código fuente de este proceso:
public
class
public
FullReindex
static
void
{
main ( S t r i n g [ ]
args )
SearchEngineMappingException ,
throws
IOException ,
IndexObjectException
{
S e a r c h L i b r a r y . configureAndMap ( Person . c l a s s ) ;
new
B e r k e l e y G l o b a l P r o p e r t y E d i t o r ( ) . s e t B a s e D i r ( " i d x /" ) ;
BerkeleyIndex . renewAllIndexes () ;
Log
l o g = LogFactory . getLog ( F u l l R e i n d e x . c l a s s ) ;
log . info (" Starting
String
Person . c l a s s
reindex . . . ") ;
r e s o u r c e = "com/ k l i n k / C o n f i g u r a t i o n . x m l " ;
Resources . setDefaultClassLoader ( IBatisHelper . class . getClassLoader () ) ;
Reader
reader = Resources . getResourceAsReader ( resource ) ;
PersonDao
p e r s o n D a o = new
P e r s o n D a o ( new
SqlSessionFactoryBuilder () .
build ( reader ) . openSession () ) ;
L i s t <P e r s o n >
new
personList
= personDao . r e t r i e v e A l l ( ) ;
D e f a u l t I n d e x e r S e r v i c e ( new
DefaultIndexingPipeline () ,
BerkeleyIndexWriterFactory . getInstance () ) . bulkCreate ( personList ) ;
log . i n f o ( " Person . c l a s s
reindex
finished . . . ") ;
}
}
Notemos del anterior fragmento de código que sólo 4 líneas están relacionadas al motor de búsqueda.
Esto muestra el objetivo de generar procesos simples de indexación y reindexación.
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
145
4.2.3. KStore
Descripción General
La última aplicación con la que probamos la adecuación del framework se llama KStore.
KStore es una tienda virtual cuyo caso de uso principal es la búsqueda de productos. Es decir, en esta
aplicación las tareas de IR están en el centro del problema.
Las entidades principales son los productos a la venta (Item), las categorías bajo las que se agrupan los
ítems (Category) y los sitios en los que opera KStore (Site).
Una característica particular de KStore es la indexación de anuncios publicitarios (entidad Advertising),
la cual permite mostrar anuncios contextuales según la búsqueda que el usuario está llevando adelante.
Los frameworks que colaboran para implementar la arquitectura MVC, las vistas HTML e inversión del
control son principalmente: Spring, JSP y las tecnologías que hemos comentado en los ejemplos previos
(ver subsecciones 4.2.1 y 4.2.3).
Casos de Uso y Modelo de Dominio
Veamos los casos de uso de KStore y una breve descripción de éstos:
Figura 4.17: Casos de uso en KStore.
Buscar Productos: mediante una expresión de texto libre, el usuario indica el/los productos de interés
y el sistema presenta un listado paginado de resultados. Este caso de uso puede comenzarse desde
cualquier pantalla del sitio utilizando la barra de búsqueda y es particularmente importante por ser
el punto de entrada desde el cual se inician el resto de los casos de uso.
Agregar/Quitar Producto en Carrito de Compras: consiste en permitir al usuario administrar los
productos que buscó en el transcurso del caso de uso Buscar Productos, llevando cuenta de cuáles
son de su interés. Para utilizar el caso de uso Comprar Productos en Carrito es necesario haber
iniciado este caso de uso.
Comprar Productos en Carrito: este caso de uso consiste en adición de los productos agregados al
carrito y la compra mediante una tarjeta de crédito simulada. Este paso requiere que el usuario
demuestre ser un humano y no un robot mediante un CAPTCHA (Ahn et al., 2004).
146
CAPÍTULO 4.
EXPERIMENTACIÓN
Ver Enlace Patrocinado: este caso se inicia cuando el cliente selecciona uno de los enlaces patrocinados. Comúnmente el resultado de este caso es la aparición de una pantalla promocional o la salida
de este sistema hacia otro.
El diagrama de clases de KStore es el siguiente:
Figura 4.18: Diagrama de clases de KStore.
nuevamente utilizamos estereotipos para mostrar las propiedades de indexación:
Figura 4.19: Clases de KStore con estereotipos de indexación.
Hagamos una descripción breve de estas entidades y sus relaciones:
Item: representa los productos pasibles de ser comprados por los clientes. En este ejemplo indexamos
sólo el campo title, el cual nos da suciente información para la mayoría de las búsquedas. A efectos
de mantener la simplicidad del modelo, omitimos almacenar atributos como el stock, el cual no es
requerido en nuestros casos de uso pero bien podría usarse en un sistema real.
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
147
Category: esta entidad representa un concepto de dominio bajo el cual se agrupan items. Algunos
objetos que podrían ser instancias de esta clase: Electrónica, GPS, etc. En este ejemplo nos
limitamos a indexar los títulos de las categorías, lo cual da la posibilidad de construir un índice de
items por categorías.
Advertising: es una publicidad que se presenta en el contexto del caso de uso Buscar Productos. Estos
objetos son recuperados por el motor de búsqueda utilizando un proceso similar al de búsqueda de
items con el n de proveer al cliente una alternativa relevante a los resultados de búsqueda que se le
presentan. Los objetos advertising son indexados en índices separados de los items para evitar que
participen del proceso de recuperación y valoración de los items.
Site: representa una instancia particular del sistema KStore para una zona determinada. Normalmente un site corresponde a un dominio web y todos los objetos que discutimos previamente se
agruparán dentro de un sitio particular. El hecho de contar con un objeto site nos permite formalizar conocimiento como moneda local, dominios web, tasa de cambio respecto de monedas de
referencia, etc.
Veamos algunas pantallas que muestran el ujo de compra en KStore:
Figura 4.20: Pantalla principal de búsqueda de KStore.
148
CAPÍTULO 4.
EXPERIMENTACIÓN
Figura 4.21: Resultados para la query apple ipod nano blue. En la columna central tenemos los resultados
de búsqueda, a la izquierda los anuncios contextuales, a la derecha el carrito de compras y en la parte
inferior el paginador.
Figura 4.22: Los resultados se agregan dinámicamente al carrito de compras, el cual luego pasa al proceso
de pago (checkout).
4.2.
PRUEBAS CON APLICACIONES DE REFERENCIA
149
Figura 4.23: Conrmación de la compra. El formulario de pago requiere que ingresemos los datos de la
tarjeta de crédito y un código vericador para evitar usos fraudulentos. En la parte inferior se encuentra
la lista de productos que estamos comprando junto al monto total a pagar.
Figura 4.24: Pago completado. El sistema conrma la operación y presenta un botón para continuar
utilizando el sistema desde la página de inicio.
150
CAPÍTULO 4.
EXPERIMENTACIÓN
Funciones de IR en KStore
Las tareas de recuperación de información son centrales a KStore ya que soportan el modelo de negocio
de la tienda en línea tanto en cuanto a compra de productos como respecto de la publicidad.
El mapeo de entidades no presentó nuevos desafíos ya que es relativamente simple. Sin embargo, fue
conveniente separar los anuncios y los items en índices diferentes, tal de aislar la recuperación de unos y
otros. Esto no solo permitió un código más simple, sino que facilita la paralelización de la búsqueda de
anuncios e items en forma concurrente.
Para este ejemplo utilizamos el plugin de indexación para Hibernate y el de índices en disco Berkeley. El
primero de estos plugins fue necesario ya que el modelo de dominio se indexaba utilizando Hibernate y
fue natural replicar los eventos CUD utilizando este plugin. El plugin Berkeley fue utilizado para contar
con índices persistentes. A su vez, nuestro indexador oine requiere un índice persistente para que la
aplicación pueda consultar el índice invertido.
Otras características de la solución
En KStore tuvimos la intención de generar un corpus similar al de un sitio web real de tamaño pequeñomediano. Para esto jamos como objetivo generar corpus de volumen no menor a los 5.000 productos
(items).
Para generar este corpus se construyó un software ad-hoc (robot) cuya tarea fue extraer información real
de sitios comerciales y utilizar dicha información en la generación del corpus. El robot construido efectuó
pedidos REST (Fielding, 2000) a una API pública del sitio web Shopping.com, desde el cual se extrajeron
items con sus correspondientes categorías.
El robot recibió un archivo de texto plano con 100 palabras clave ingresadas por el autor y, por cada una
de ellas, efectuó un llamado a la API REST de búsqueda por palabra de Shopping.com.
Este proceso produjo como resultado un archivo de texto plano con títulos, descripciones, categorías y
precio en dólares estadounidenses para 6149 items. A partir de dichos items se construyó el corpus de
categorías, el cual se compone de 93 categorías distintas.
Por último, se escribió un proceso que lee el archivo producido por el robot y almacena dichas entidades
en Hibernate. Los objetos almacenados en Hibernate se indexaron transparentemente por medio de los
plugins para Hibernate y BerkeleyDB provistos en nuestro framework (y congurados mediante inyección
de dependencias desde Spring).
En resumen, el proceso de generación del corpus comprendió:
1. Confección de lista de keywords de búsqueda para Shopping.com
2. Ejecución del robot
3. Ejecución del proceso indexador
Con posterioridad a la construcción de la búsqueda de items surgió la propuesta de agregar como aditamento funcional la indexación y recuperación de anuncios publicitarios. El hecho de agregar los anuncios
publicitarios con el sistema en funcionamiento sólo requirió replicar el proceso indexador variando la
lectura del archivo de texto del cual provenían los items para que se lean los títulos, subtítulos y enlaces
correspondientes a los anuncios.
La posibilidad de particionar los índices por entidad permitió separar la indexación de anuncios en un
índice independiente, de forma tal que la búsqueda de estas entidades no interrió con la búsqueda de
items.
A diferencia del corpus de items, el de anuncios se construyó en forma totalmente manual por lo que
cuenta pocos anuncios en relación a la cantidad de items.
4.3.
PRUEBAS DE CALIDAD Y RENDIMIENTO
151
Notas acerca de Experimentación y Refactorización
La experimentación en situaciones de uso real produjeron una iteración sobre el software construido,
aportando una mejora signicativa al software entregado con esta tesis. En este apartado hacemos algunos
comentarios puramente experimentales acerca del aprendizaje adquirido en el desarrollo de estas tres
aplicaciones de ejemplo.
La primera aproximación a integrar las aplicaciones con el framework de IR y sus dependencias fue la de
ubicar manualmente todos los archivos Java (JAR) en una ubicación donde el cargador de clases Java
pueda encontrarlos. Este mecanismo de administración de dependencias es efectivo pero poco mantenible
ya que se producen conictos de versiones, redundancias y esfuerzos de mantenimiento innecesarios.
Para mejorar la administración de estos proyectos se adoptó un software robusto para administración de
proyectos: Apache Maven (Apache Foundation, 2010).
La adopción de Maven permitió:
que las tres aplicaciones puedan incluir el framework de IR, sus plugins y dependencias automáticamente con unas pocas líneas de conguración XML,
delegar en Maven la resolución de dependencias de cada módulo,
generar lanzamientos (releases) con automatización de casos de prueba,
simplicar la administración de los proyectos.
La adopción de Maven abrió la posibilidad de desacoplar partes del framework, cambiando una estructura
monolítica en la cual un único módulo hace todo por una arquitectura de plugins.
Este cambio de estructura consistió en partir el framework originalmente monolítico en:
un módulo básico de IR (core),
plugin de interacción con Hibernate,
plugin de interacción con iBATIS,
plugin para backend de índices en BerkeleyDB,
plugin de indexación distribuida oine con JMS.
Esta descripción acerca de la iteración que hicimos sobre la administración del proyecto es ejemplo del
acierto en construir distintas aplicaciones reales donde probar el framework.
4.3.
Pruebas de Calidad y Rendimiento
En esta sección vamos a explicar cómo se probó que el software construido es correcto y cuál es su
rendimiento de indexación y recuperación de objetos, estableciendo algunas comparaciones con frameworks similares.
La subsección 4.3.1 desarrolla las pruebas de calidad. La subsección 4.3.2 muestra las pruebas de rendimiento y las compara con lo obtenido en frameworks similares.
4.3.1. Pruebas de Calidad
El framework de IR se construyó siguiendo la técnica de programación conocida como Test Driven De-
velopment (Beck, 2002). Esta técnica tiene como metodología de trabajo construir el sistema completo
a partir de pruebas unitarias. La metodología de TDD produce naturalmente un diseño desacoplado y,
dado que sólo se desarrolla lo que se está probando, los tests resultantes cubren un alto porcentaje del
código.
152
CAPÍTULO 4.
EXPERIMENTACIÓN
Las pruebas unitarias tienen por objetivo probar el funcionamiento aislado de una funcionalidad. En esta
subsección explicamos algunos reportes y métricas que nos indican el grado de certeza que podemos tener
acerca del correcto funcionamiento del framework.
La distribución de pruebas por módulo del framework es la siguiente (los valores que siguen corresponden
4
al release 1.0 del framework ):
Core Search Engine: 192 tests
Plugins Berkeley, Hibernate, iBATIS y JMS: 27 tests
Sumando estos números, el release pasa con éxito 219 tests.
Existen varias métricas para evaluar cuán representativos son los tests. Una de las métricas básicas más
utilizadas en la evaluación de los tests es la cobertura de código (o simplemente cobertura ). La cobertura
mide qué cantidad/porcentaje de líneas de código son ejecutadas por los tests.
Cuando analizamos la cobertura, debemos tener cuidado para no llegar a falsas conclusiones. Por ejemplo,
el siguiente par test/programa tiene una cobertura del 100 % pero sin embargo falla en todos los casos
excepto el probado:
public
BadMathTest ( )
class
{
// t e s t que c u b r e e l 100 % d e l c ó d i g o c u b r i e n d o s ó l o e l c a s o p a r t i c u l a r
que p a s a e l t e s t
@Test
public
exponentialFunctionTest ()
void
Assert . assertEquals (1 ,
{
BadMath ( ) . e x p o n e n t i a l ( 0 ) ) ;
new
}
}
// mal a j u s t e de f ( x )=e^x p o r g ( x )=x+1
public
BadMath ( )
class
public
return
{
exponential ( int
int
x)
{
x +1;
}
}
por otro lado, las pruebas de este par test/progama son mejores que las del caso anterior, aunque logra
menor cobertura:
public
GoodMathTest ( )
class
{
// un t e s t m e j o r p e r o que c u b r e poco c ó d i g o
@Test
public
exponentialFunctionTest ()
void
− 1.0 f
Assert . assertEquals (
Assert . assertEquals (
0.0 f ,
Assert . assertEquals (
1.0 f
Assert . assertEquals (
10.0F
,
new
,
GoodMath ( ) . l o g ( 1 . 0 d ) ) ;
new
,
{
GoodMath ( ) . l o g ( 0 . 1 d ) ) ;
new
new
GoodMath ( ) . l o g ( 1 0 . 0 d ) ) ;
GoodMath ( ) . l o g ( 1 0 0 . 0 d ) ) ;
}
}
// buen a j u s t e p o r t a y l o r , p e r o con c h e q u e o s que b a j a n l a c o b e r t u r a
public
class
public
GoodMath ( )
float
i f ( x== n u l l )
th row
new
{
l o g ( Double
x)
{
{
I l l e g a l A r g u m e n t E x c e p t i o n ( " Can ' t
calculate
log
}
4 Los
releases se etiquetan en el repositorio de código para poder ser reconstruidos con facilidad
of
null ") ;
4.3.
153
PRUEBAS DE CALIDAD Y RENDIMIENTO
if (
x <= 0 . 0 d
th row
new
)
{
I l l e g a l A r g u m e n t E x c e p t i o n ( " Can ' t
calculate
log
of
negative
numbers " ) ;
}
// d e s a r r o l l o en s e r i e de t a y l o r
return
taylorSeriesForLog (x) ;
}
}
La caída en la cobertura se da porque las excepciones no son cubiertas por los tests. Esto no quiere decir
que los tests no deban provocar excepciones, sino que el sólo hecho de tener una buena cobertura no
es suciente para tener buenos tests. Habiendo explicado esto, podemos continuar con la cobertura de
nuestro framework.
Para el cálculo de cobertura utilizamos un plugin de Maven llamado Cobertura (Doliner, 2006) y analizamos los reportes para el módulo core del framework.
La ejecución inicial de Cobertura nos indica una métrica del 54 % de cobertura (1925/3559 líneas de
código).
Dado que inicialmente este número pareció bajo, analizamos los reportes y encontramos:
Muchísimas líneas de código que no son cubiertas por los tests se encuentran en los stemmers. Dado
que éstos se asumen correctos por construcción (son código auto generado utilizando Snowball),
deberíamos excluirlos de nuestros cálculos.
Existen muchas excepciones no lanzadas durante los tests que corresponden a parámetros nulos,
erróneos o excepciones lanzadas por la JVM como producto de operaciones, por ejemplo, de reexión.
En muchas clases se auto generaron los métodos hashCode() e equals() mediante el IDE Eclipse.
Estos métodos generalmente no son alcanzados en su totalidad por los tests, ya que su estructura
suele hacer que retornen rápidamente sin ejecutar todas sus líneas. Además, asumiendo que Eclipse
fue probado correctamente, no es necesario que los test alcancen estos métodos ya que se suponen
correctos.
Los métodos toString() suelen ser triviales pero no suelen ser alcanzados por los tests.
En algunos casos se presentan versiones sobrecargadas de un método simplemente para adoptar un
valor por defecto (haciendo que un método invoque al otro). Este tipo de prácticas tampoco suelen
probarse por su trivialidad.
Si eliminamos el aporte de los stemmers, la cobertura asciende a 65 % (1641/2515 líneas de código).
Ahora, si asumiéramos que los métodos hashCode(), equals(), excepciones y toString de las 140 clases
intervinientes aportan un 15 % de código adicional, la cobertura se ubicaría alrededor del 80 %, lo cual
parece ser una buena marca para un prototipo como el que hemos construido.
Además de la cobertura, existen muchas otras métricas asociadas a la complejidad del código, las cuales
exceden el tratamiento que le queremos dar al tema.
4.3.2. Pruebas de Rendimiento
En esta subsección vamos a exponer los resultados de algunas pruebas de rendimiento que efectuamos
sobre nuestro software.
Si bien el foco de este trabajo no está en que la versión actual del framework tenga un rendimiento superior
a las herramientas del estado del arte, queremos vericar que no existen comportamientos anómalos. Estos
comportamientos anómalos podrían ser, por ejemplo, que el crecimiento lineal en el número de objetos a
indexar incremente el tiempo de indexación o bien el uso de memoria en forma no lineal.
154
CAPÍTULO 4.
EXPERIMENTACIÓN
Además de detectar comportamientos anómalos, haremos un estudio acerca de dónde se encuentran los
cuellos de botella en la indexación y recuperación, a efectos de saber dónde tenemos oportunidades de
optimización.
Descripción del Laboratorio de Pruebas
Todas las pruebas se realizaron en un equipo con la siguiente conguración:
procesador de doble núcleo Intel Core 2 Duo de 1.83 GHz
2 GB RAM
Ubuntu 10.04 LTS, kernel 2.6.32-23-generic
Sun Java HotSpot(TM) Server VM (versión 1.6.0_21)
Prueba de Indexación en Memoria
El objetivo de esta prueba es comprobar experimentalmente la estabilidad y linealidad del tiempo de
indexación, para lo cual vamos a utilizar el corpus de items de KStore.
En esta prueba medimos los tiempos de indexación para los 6619 items, indexándolos en lotes de a
100. Las capas de indexación se conguraron para utilizar DefaultIndexerService, DefaultIndexerPipeline y
MemoryIndexWriterFactory.
En los siguientes grácos vemos los tiempos de indexación acumulativos y promedio para los 66 lotes:
Figura 4.25: Tiempos de Indexación Acumulativos. El gráco muestra en trazos continuos de color cuatro
ensayos más dos trazos discontinuos que representan un desvío a partir del promedio de los cuatro ensayos.
4.3.
PRUEBAS DE CALIDAD Y RENDIMIENTO
155
Figura 4.26: Tiempo de Indexación Promedio. Este gráco muestra en forma de columnas el tiempo
promedio de indexación de cada lote para los cuatro ensayos de la gura anterior.
El loteo de items tiene que ver con la utilización del método IndexerService.bulkCreate(), el cual recibe los
items en listas. Variando el número de lotes desde 1 hasta 6619 items por lote no encontramos variaciones
signicativas en los tiempos de indexación. Esto nos induce a pensar que el loteo no es necesario cuando
se trabaja en memoria.
Si repetimos la prueba anterior sin utilizar el stemmer snowball del inglés para tratar los títulos de los
items, los tiempos de indexación se reducen signicativamente:
156
CAPÍTULO 4.
EXPERIMENTACIÓN
Figura 4.27: Tiempos de indexación acumulativos comparativos con y sin stemming para el corpus KStore,
índice en memoria.
Figura 4.28: Tiempos de indexación promedio por lote comparativos con y sin stemming para el corpus
KStore, índice en memoria.
Los datos de las pruebas anteriores corresponden a muestras aleatorias tomadas entre varias indexaciones,
excluyendo la primera corrida. Esta precaución debe tomarse debido a que la primera indexación incurre
4.3.
PRUEBAS DE CALIDAD Y RENDIMIENTO
157
en una penalidad de carga de clases y caches que en un sistema en funcionamiento ya encontraríamos en
memoria.
Veamos cómo varían los valores de la misma estadística tomando sólo las primeras corridas del indexador:
Figura 4.29: Tiempos de Indexación Acumulativos para Primeras Corridas. En trazos continuos de color
tenemos los cuatro ensayos más dos trazos discontinuos que representan un desvío a partir del promedio.
Figura 4.30: Tiempo de Indexación Promedio para Primeras Corridas del Indexador. La línea de tendencia
muestra que el costo en los primeros lotes es superior al de los últimos lotes, donde los tiempos se
estabilizan.
158
CAPÍTULO 4.
EXPERIMENTACIÓN
Como se puede apreciar en la gura anterior, respecto de las indexaciones normales, la primera corrida
incrementa los tiempos de ejecución en un orden de magnitud.
Pruebas Comparativas de Indexación en Memoria
En este apartado repetimos las pruebas del apartado previo, indexando el corpus KStore sobre Apache
Lucene.
La indexación en Lucene requiere la creación explícita de un Document. En nuestro caso, el documento
se denió de la siguiente forma:
Item
item = itemsToBeIndexed . get ( i ) ;
d o c . a d d ( new
Field (" id " ,
( ( L o n g ) i t e m . g e t I d ( ) ) . t o S t r i n g ( ) , F i e l d . S t o r e . YES ,
F i e l d . I n d e x . NOT_ANALYZED) ) ;
d o c . a d d ( new
Field (" t i t l e " ,
i t e m . g e t T i t l e ( ) , F i e l d . S t o r e . NO, F i e l d . I n d e x .
ANALYZED) ) ;
Es decir, indexamos los ítems por su título, almacenando su identicador en el índice invertido, tal de
poder hidratar los objetos luego de la recuperación.
Si comparamos la indexación en memoria del corpus KStore sobre nuestro framework versus la indexación
en memoria con Lucene, tenemos el siguiente gráco comparativo:
Figura 4.31: Tiempos de Indexación Comparativos. En la prueba con el corpus KStore, nuestro framework
fue más rápido que Lucene tanto en los casos en los que procesamos el texto con stemming como en los
que no lo hicimos.
Analizando la gura (4.31), vemos que los dos frameworks sufren una degradación al aplicar análisis sobre
5
los textos .
Un aspecto curioso de la implementación de Lucene es que el método close() de su índice en memoria
toma un tiempo similar al de toda la indexación junta. Este tiempo puede visualizarse en el gráco (4.31)
como una rampa pronunciada sobre su límite derecho.
5 En
rigor, en nuestro framework sólo eliminamos el stemming, manteniendo tanto la delimitación como la normalización.
4.3.
PRUEBAS DE CALIDAD Y RENDIMIENTO
159
Aún quitando el tiempo que toma el método close(), nuestro framework es hasta un 60 % más veloz
cuando no se utiliza análisis ni stemming y hasta un 15 % más veloz cuando se utiliza análisis y stemming.
Incluyendo el tiempo que insume el método close(), nuestro framework llega a ser un 73 % más rápido sin
análisis ni stemming y un 43 % más veloz cuando los incluimos.
Pruebas de Indexación en BerkeleyDB
Así como en el apartado previo hicimos pruebas de indexación sobre memoria RAM, en este apartado
vamos a estudiar el rendimiento utilizando nuestro plugin BerkeleyDB (ver subsección 3.3.3).
Las pruebas realizadas sobre este plugin son las mismas que efectuamos en el apartado anterior, es decir,
indexamos el corpus KStore en lotes de 100 items.
Antes de presentar los resultados, es preciso comentar que el plugin BerkeleyDB permite trabajar tanto
en forma transaccional como no transaccional. Desde el punto de vista del rendimiento, trabajar en forma
transaccional degrada la performance en forma muy fuerte. Sin embargo, es sensato trabajar en modo
transaccional si el corpus es pequeño, si disponemos de hardware de altas prestaciones o si no tenemos
otra opción para resguardar la integridad de nuestro proceso.
Conociendo conceptualmente el impacto que tiene el modo transaccional, estamos en condiciones de
presentar los resultados experimentales:
Figura 4.32: Indexación No Transaccional en BerkeleyDB. La línea superior (verde) corresponde a una
primera corrida, la cual hemos visto que suele demorar más. El resto de las líneas se corresponde con
un muestreo de corridas intermedias, las cuales no sufren el efecto anterior.
160
CAPÍTULO 4.
EXPERIMENTACIÓN
Figura 4.33: Tiempos de indexación promedio por lote para la indexación no transaccional en BerkeleyDB.
A diferencia del caso en memoria, el índice se degrada notablemente a medida que se agregan lotes. La
línea roja marca la tendencia lineal para la evolución del tiempo promedio por lote.
Figura 4.34: Indexación Transaccional en BerkeleyDB. La versión transaccional toma hasta 30 veces más
tiempo en indexar los lotes y diluye el efecto de primeras corridas.
Vemos entonces que la indexación transaccional toma aproximadamente 30 veces más que la versión no
transaccional. En el caso transaccional, también vemos que el retardo en la primera corrida se diluye
debido a que el tiempo total de indexación es muy largo.
4.3.
161
PRUEBAS DE CALIDAD Y RENDIMIENTO
Sin embargo, el índice no transaccional sí logra buenas marcas. La línea de tendencia gracada en color
rojo en la gura (4.33) tiene fórmula:
t (Li ) = 0, 01966i + 0, 27646
[s]
donde:
t (Li )
es el tiempo de indexación en segundos del lote
cada lote
Li
i
tiene 100 items
Asumiendo que la degradación de la performance sigue esta tendencia lineal, podemos calcular cuánto
tiempo nos tomaría indexar un corpus de
100
X
1000
lotes (100000 items):
t (Li ) = 10116, 29
s.
= 168, 60
min
∼
= 2, 8
hs
i=1
Lo cual es una muy buena marca para esta versión no optimizada del índice Berkeley.
Pruebas de Recuperación en Memoria
En esta subsección vamos a estudiar el rendimiento de nuestro framework en cuanto a recuperación de
objetos.
Dado que los tiempos invertidos en recuperación se exponen directamente a los usuarios nales de la
aplicación, la tarea de recuperación es aún más importante que la de indexación.
A continuación explicamos el diseño de las pruebas de recuperación en memoria junto con sus resultados.
El diseño de la prueba comprendió:
índice en memoria,
corpus KStore,
un conjunto 4024 queries distintas, tomadas de las primeras y segundas palabras de los items del
corpus KStore,
un conjunto de 50 ensayos sin efectos de retardo por primeras corridas.
La variable que nos interesa medir en este caso es el
µq = tiempo
promedio de ejecución por consulta.
La medición del tiempo promedio por consulta incluye las operaciones:
Procesamiento de Textos: normalización y construcción del árbol de consulta,
Recuperación: acceso a índices, operaciones sobre posting lists y valoración según modelo vectorial.
Bajo este diseño, obtuvimos los resultados:
µq = 4, 4376 × 10−5 [s/query] = 0, 044376 [ms/query]
σq = 0, 7127 × 10−5 [s/query] = 0, 007127 [ms/query]
Lo cual nuevamente es una muy buena marca.
Si extrapolamos esta estadística destinando un segundo completo a resolver consultas que toman un
tiempo
µq
, podríamos recuperar:
#queries en 1 segundo
=
1000 ms
s
= 22534 [queries/s]
0, 044376 ms
q
162
CAPÍTULO 4.
EXPERIMENTACIÓN
Esto es, podríamos resolver más de 22.000 consultas por segundo.
Es muy importante conocer si estos resultados son comparables a los de otras alternativas como Apache
Lucene. Para esto repetimos las pruebas anteriores utilizando el RAMDirectory de Lucene y efectuamos
consultas sobre el atributo title de la clase Item.
Ejecutando las pruebas sobre Lucene, obtuvimos los resultados:
µq−Lucene = 6, 6634 × 10−5 [s] = 0, 066634 [ms]
σq−Lucene = 0, 6365 × 10−5 [s] = 0, 006365 [ms]
Este resultado de Lucene también es muy bueno, sin embargo, si extrapolamos nuevamente la estadística,
ahora podríamos efectuar:
#queries en 1 segundo para Lucene
=
1000 ms
s
= 15007 [queries/s]
0, 066634 ms
q
En resumen, para un mismo intervalo de tiempo de un segundo, nuestro framework permite ejecutar un
50 % más de consultas sobre el corpus KStore que lo que permite Lucene.
Pruebas de Recuperación en BerkeleyDB
Las pruebas de recuperación sobre el índice BerkeleyDB son prácticamente iguales a las que hicimos sobre
el índice en memoria, con la única excepción de que indexamos y recuperamos los objetos hacia/desde el
índice Berkeley en modo no transaccional.
La recuperación en Berkeley tuvo el siguiente resultado:
µq−Berkeley = 2, 2748 [ms]
σq−Berkeley = 0, 1811 [ms]
si repetimos la búsqueda para el caso transaccional, encontramos que el rendimiento es casi idéntico:
µq−BerkeleyT rans = 2, 2907 [ms]
σq−BerkeleyT rans = 0, 0386 [ms]
Vemos entonces que la transaccionalidad afecta el rendimiento de la indexación, mientras que la recuperación no sufre de penalización alguna.
Con el índice Berkeley no transaccional y un equipo como el descripto al principio de esta sección,
calculemos cuántas búsquedas por segundo podemos servir sobre el corpus KStore:
#queries en 1 segundo para Berkeley DB (no transaccional)
=
1000 ms
s
= 439 [q/s]
2, 2748 ms
q
Al igual que observamos con la indexación, el rendimiento en memoria es muy superior al obtenido en
disco con BerkeleyDB. Sin embargo, en el caso de la indexación la operatoria en memoria mejoraba los
tiempos aproximadamente por un factor de
factor de
50.
400,
mientras que en el caso de la recuperación tenemos un
Esto nos permite obtener las siguientes conclusiones:
la transaccionalidad degrada el rendimiento muchísimo más en la indexación que en la recuperación,
BerkeleyDB mejora su rendimiento en escenarios de lecturas intensivas (recuperación) por sobre el
escenario de escrituras intensivas (indexación).
Con esto terminamos las pruebas cuantitativas de rendimiento. En la próxima subsección analizaremos
cualitativamente la integración de nuestro framework con PetClinic y haremos una comparativa general
de características con el resto de las herramientas.
4.3.
PRUEBAS DE CALIDAD Y RENDIMIENTO
163
4.3.3. Análisis Comparativo Cualitativo
En esta sección vamos a comparar nuestra solución respecto de alternativas similares en términos cualitativos. Primero vamos a hacer una breve reseña de qué similitudes y diferencias encontramos en la
implementación de PetClinic por Compass y luego vamos a escoger una serie de items sobre las que
comparar las herramientas.
PetClinic sobre Compass
En este apartado seleccionamos algunos puntos clave sobre los que comparar nuestra implementación de
PetClinic con la provista por Compass:
Mapeo XML: en vez de utilizar un mapeo por medio de anotaciones (también soportado por Compass), se utilizan un archivo XML llamado petclinic.cpm.xml.
Semántica de Mapeos: si comparamos el archivo petclinic.cpm.xml con nuestro mapeo de la gura
(4.2), encontramos que el mapeo es muy similar. Los dos frameworks mapean la jerarquía de clases,
incluyendo Entity, NamedEntity, Person, Vet y Pet. En el caso de Compass también se mapea la
clase Specialty.
ORM: en el ejemplo de Compass se soportan simultáneamente JDBC, Hibernate y OJB mediante
el intercambio de archivos de conguración de Spring y la utilización del patrón de diseño DAO
(Data Access Objects). En nuestro framework, si bien el ORM nos es transparente a través de los
plugins, la implementación que hicimos de PetClinic sólo trabaja con Hibernate.
Indexación: en los dos ejemplos se utilizó la indexación por eventos. La conguración acerca de cómo
hacerlo varía signicativamente entre frameworks, pero en los dos casos el objetivo es el mismo.
Recuperación: en el caso de Compass, la recuperación se efectúa a través de una clase interna del
framework que delega la visualización a una vista congurada mediante Spring. En nuestro ejemplo
de PetClinic no utilizamos un controlador provisto por nuestro framework sino que hicimos un
controlador propio que efectúa explícitamente la búsqueda.
Dado que nuestra prueba sobre PetClinic está basada en la versión que acompaña a Compass, es natural
que no hayan existido grandes diferencias entre una y otra versión.
Los puntos anteriores corresponden a una instancia particular de la utilización de los dos frameworks. En
el próximo apartado seleccionamos características generales sobre las que efectuar una comparativa.
Comparación General de Características
En este apartado seleccionamos algunos puntos sobre los que nos interesa evaluar los distintos frameworks
estudiados. Esta lista coincide mayormente con los puntos elegidos para analizar Compass y Hibernate
Search en la subsección (2.4.4).
Naturalmente, los trabajos más maduros han incorporado a lo largo del tiempo mayor cobertura de casos
de uso satelitales (índices sobre RDBMS, resaltadores de resultados, etc). Sin embargo, queremos guiarnos
por la adecuación de la herramienta al problema que queremos resolver y no por los características que
pueden incorporarse iterativamente. Por esto, elegimos las características que son las que consideramos
fundamentales al problema de indexación de objetos, dejando un último apartado para características
satelitales e integración con otros frameworks u ORMs.
Características Generales
Al analizar las características generales de HS y Compass (ver subsección
2.4.4), planteamos tres problemas: abstracción incompleta, infraestructura correctiva y desaprovechamien-
to estructural y semántico.
La abstracción incompleta tiene que ver con la presencia en HS y Compass de objetos de Lucene y
conceptos de orientación a documentos. Estas características no están presentes en nuestro framework,
164
CAPÍTULO 4.
EXPERIMENTACIÓN
donde las consultas, ltros y ordenamiento se efectúan en términos de los objetos originales y no de
documentos.
La infraestructura correctiva de HS y Compass eran un conjunto de características implementadas para
reutilizar Lucene y ocultar sus limitaciones (bloqueos, optimización de índices, precalentamiento de lectores, etc). En la infraestructura de nuestro framework encontraremos que este tipo de problemas no
aplican.
El desaprovechamiento estructural tiene que ver con la delegación en Lucene de los problemas de recuperación, evitando consultas a partir de objetos y valoraciones estructurales del tipo PageRank y HITS.
En este campo, nuestro framework crea la capacidad de efectuar consultas a partir de objetos. En cuanto
a análisis de enlaces entre objetos, esta característica no fue implementada aún en nuestro framework. Sin
embargo, los índices fueron diseñados de forma de ser adecuados para un cálculo de este tipo utilizando
el registro maestro.
Además explicamos que el hecho de que Lucene sea el motor de IR de base para HS y Compass provoca
dos problemas. El primer problema es que la evolución de estas herramientas depende de la evolución
de Lucene. La segunda es que para aprovechar las mejoras en Lucene, los usuarios del framework deben
esperar a que HS y Compass generen una versión compatible con la nueva versión de Lucene.
Comportamiento como Framework
Este punto tiene que ver con lo que establecimos en la proposi-
ción 1 respecto de tomar un comportamiento de framework de caja negra.
En este sentido, los tres frameworks de indexación de objetos obtienen un adecuado comportamiento de
caja negra. El único rezagado es Lucene, quien tiene comportamiento de librería.
Indexación y Modelo de Dominio
Cuando analizamos este punto en la subsección (2.4.4), efectu-
amos cinco proposiciones que vamos a retomar para discutir este ítem.
La proposición 2 proponía distintos niveles de soporte transaccional. En este sentido, Compass es el único
que da garantías transaccionales, siendo que el resto de las herramientas sólo garantiza consistencia básica
del índice.
Muy relacionada a la proposición anterior, la proposición 3 plantea que las operaciones sobre los índices
se deben hacer evitando repetir código asociado a transacciones o a lecturas y escrituras de índices. Este
es un problema que está mayormente resuelto en todos los frameworks.
La siguiente es la proposición 4, la cual hablaba de la necesidad de proveer indexación automática de
eventos CUD en el ORM. En este sentido, los tres frameworks de indexación de objetos soportan este
sistema, siendo que nuestro framework y Compass son los únicos que permiten trabajar con distintos
ORMs. HS sólo indexa automáticamente los eventos CUD del ORM Hibernate Core, mientras que Lucene
no soporta indexación automática.
La anteúltima proposición plantea requerimientos mínimos en cuanto a la expresividad del sistema de
mapeos. En este punto, los tres frameworks de IR sobre objetos presentan una expresividad prácticamente
equivalente. Mas allá de pequeñas diferencias de forma, no hay grandes diferencias de características entre
estos tres frameworks. Sin embargo, Lucene sí queda relegado frente al resto por no soportar el mapeo
de entidades.
Por último, la proposición 6 nos dice que la indexación y la recuperación deben utilizar representaciones
polimórcas de los objetos de dominio. Hemos visto que HS y Compass exponen una serie de objetos
no polimórcos como Document, Resource y Property. Sin embargo, también permiten trabajar con entidades polimórcas o idénticas a las de dominio. En nuestro framework dimos un paso adelante, eliminando
cualquier objeto espejado de la entidad de dominio, referenciando los objetos únicamente mediante meta
programación, utilizando atributos de tipo Class y Field. Sin embargo, introdujimos objetos que representan el concepto de clave de objeto sin proveerlas como un reemplazo de la entidad indexada sino como
un mecanismo para poder hidratarla desde el ORM.
Procesos de Indexación
La proposición 7 nos requería permitir una indexación sincrónica con la
transacción de negocio y otra asincrónica.
En este sentido, todos los frameworks de indexación de objetos implementan en la indexación online,
semi-online y oine, por lo que las diferencias tienen que ver con detalles implementativos.
4.3.
165
PRUEBAS DE CALIDAD Y RENDIMIENTO
Recuperación, Queries, Matching y Acceso a Datos
La principal diferencia conceptual en materia
de acceso a datos es que nuestro framework incorpora un registro maestro de objetos que permite conocer
qué objetos están indexados y bajo qué términos. Creemos que la utilidad de este registro maestro justica
el espacio extra que pueda requerir, por lo que es una ventaja comparativa entre frameworks.
Dado el carácter experimental de nuestros índices en BerkeleyDB, creemos es demasiado pronto para
comparar la performance de los índices en disco. Sin embargo, es preciso comentar que Lucene implementa
técnicas como front coding o skip pointers, los cuales sin duda colaboran en un excelente rendimiento.
En cuanto a recuperación, Lucene ha implementado una serie de características satelitales que nuestro
framework todavía no soporta: consultas por comodines, literales, edit distance, etc.
El ltrado por atributos se efectúa en forma muy distinta entre frameworks. En el caso de Lucene y sus
derivados, el campo por el que se quiere ltrar se debe especicar en la consulta. En nuestro framework,
el ltrado de campos se efectúa como un pos ltro dentro de una cadena de ltrado. El inconveniente
que tiene la versión de Lucene es que asume que el valor por el que se ltra es texto plano, cuando bien
podría ser un otro objeto completo sobre el que aplicaríamos el método equals(). En este sentido, creemos
que nuestra solución se adecúa mejor al problema que queremos resolver.
En el ejemplo (2.4.5) propusimos un dar un paso adelante en cuanto a recuperación, proponiendo la
recuperación a partir de objetos. Este tipo de recuperación trata al objeto recibido como al indexado,
generando los términos adecuados al procesador de texto, selector de índices y lenguajes. Este tipo de
recuperación sólo está presente en nuestro framework y seguramente es la propuesta más innovadora que
incluimos en esta versión.
La implementación de un More Like This es soportada por Compass y HS, pero sólo nuestro framework
admite una consulta polimórca con el objeto para el cual se quiere buscar otros similares.
Por último, existe una pequeña controversia en cuanto a la hidratación de objetos. La proposición 9
nos indica que la hidratación es responsabilidad del ORM, lo cual tanto HS como nuestro framework
respetan a rajatabla. Por otro lado, Compass está orientado a recuperar los objetos desde el índice,
lo cual explicamos que no es la mejor solución. Sin embargo, si bien por defecto Compass efectúa la
hidratación desde los índices, también permite obtener la clave del objeto para una hidratación desde el
ORM.
Modelos de Information Retrieval
Tanto Hibernate Search como Compass dejan en manos de
Lucene la implementación de los modelos de IR, por lo que nos corresponde analizar a éste último.
Lucene implementa una versión híbrida del modelo booleano y vectorial, utilizando el modelo booleano
para ltrar documentos y el vectorial para priorizarlos. Si se desactiva la priorización, se obtiene un
modelo booleano.
Si bien nuestro framework implementa tanto el modelo booleano como el vectorial, a diferencia de Lucene,
los implementa por separado generando implementaciones distintas de la interfaz Search.
Si bien creemos que nuestro diseño es más desacoplado y separa mejor los objetos que hacen sentido en
un modelo de los del otro, no existe una razón objetiva para descartar una u otra propuesta.
En cuanto a similitud, las fórmulas de Lucene son algo más complejas que las que implementamos en esta
versión de nuestro framework. Sin embargo, tuvimos especial cuidado en diseñar los índices de forma tal
que eventualmente permitan implementar una fórmula como la de Lucene.
En términos del modelo de IR, la mayor diferencia entre frameworks tiene que ver con el hecho de que
nuestro framework adopta un modelo vectorial puro. En base a la explicación que hicimos al introducir el
modelo vectorial, donde presentamos las limitaciones del modelo booleano de conjunciones y disyunciones,
creemos que nuestra elección es la adecuada.
Otra diferencia importante entre Lucene y nuestro framework tiene que ver con los factores de impulso
(boost). En nuestro framework no incluimos un factor de impulso para atributos o clases. La razón por
la que no incluimos los factores de impulso es porque no son completamente necesarios y no queda claro
que sea la forma correcta de lograr relevancia. Dado que en nuestro framework las fórmulas de similitud
son parametrizables, podemos lograr los mismos o mejores resultados sin impulsos.
Por último, la implementación de reglas duras y blandas con HS, Compass y Lucene es considerablemente
más compleja de implementar en dichos frameworks. Nuestro framework está diseñado para permitir
166
CAPÍTULO 4.
EXPERIMENTACIÓN
incorporar este tipo de reglas con facilidad, accediendo a los atributos originales de los objetos indexados
y no a una representación como texto plano.
Funcionalidades Satelitales e Interacción con otros Frameworks
Este punto de comparación
reere a cómo se ha expandido la herramienta para cubrir distintos casos de uso e integrarse en el
ecosistema de herramientas que componen las aplicaciones. En este punto comparativo debemos excluir a
nuestra herramienta, ya que el alcance de esta primera versión sólo incluyó las funcionalidades esenciales
de la recuperación de objetos.
Entre las herramientas Lucene, Compass y Hibernate, encontramos que Lucene tiene gran cantidad de
adicionales en cuanto a técnicas de matching y acceso a datos, mientras que Compass es quien mejor se
integra con otros frameworks y ORMs.
Lucene posee una variedad muy amplia de analizadores (procesadores de texto), backends de almacenamiento de índices invertidos, fórmulas de similitud, etc. Además, a lo largo del tiempo se han ido
desarrollando herramientas como inspectores de índices invertidos. Es decir, esta librería tiene un vasto
repositorio de utilidades, siempre en el campo de IR sobre documentos.
Por el lado de Compass encontramos una amplia variedad de adicionales, entre los que se encuentra el
soporte para Spring MVC y otros ORM como TopLink o JDBC.
Aquí concluye el capítulo de experimentación. Para nalizar este trabajo, el próximo capítulo expondrá las
conclusiones haciendo un repaso de los puntos más importantes de cada capítulo, poniendo en contraste
los avances obtenidos y agregando una sección de trabajos futuros.
Capítulo 5
Conclusiones
En este capitulo nal presentamos nuestras conclusiones respecto de qué resultados obtuvimos a lo largo
de esta tesis
La primera sección presenta las conclusiones propiamente dichas, haciendo una retrospectiva desde que
comenzamos a introducir el problema en los capítulos introductorios, hasta que terminamos de validar
nuestra propuesta a través de comparaciones.
La segunda y última sección consiste en proponer características adicionales que quedaron fuera del
alcance de este trabajo y que conrmarían este software como una alternativa de primer nivel.
5.1.
Conclusiones
Recordando los comienzos del capítulo introductorio, vimos que existen aplicaciones cuyos objetivos de
negocio parecen ser ortogonales a la recuperación de información, hasta que ésta se vuelve un medio
para lograr sus objetivos de negocio. Cuando esto ocurre, necesitamos un framework de IR que se integre
transparentemente a la aplicación y se adecúe al paradigma de programación orientada a objetos y al
ecosistema de frameworks que componen una aplicación (por ejemplo, los ORM, los frameworks web,
etc).
En base a estos requerimientos hemos delimitado un problema de IR sobre objetos, el cual llamamos el
problema del Domain Model Search.
Al estudiar el estado del arte encontramos herramientas que buscan resolver este problema reutilizando
librerías de IR sobre documentos. Analizando estas herramientas, hemos desarrollado una serie de proposiciones que nos guíaron en cómo resolver el problema en base al aprendizaje que adquirimos estudiando
el estado del arte.
En base al estudio del campo de IR, del diseño de software y las herramientas anteriores, se analizó
y diseñó un framework que se adecúa al problema del Domain Model Search. Con el análisis de qué
solución debíamos construir conseguimos uno de los primeros objetivos de la tesis: delimitar el problema
en distintas actividades de IR (recuperación, indexación, etc.) encontrando las distintas variantes de
solución y seleccionando las mejores alternativas.
Una vez que se diseñó e implementó un prototipo del framework fue necesario vericarlo. Para esto
seguimos un patrón de diseño de frameworks que propone escribir tres aplicaciones distintas sobre las
que probar nuestra herramienta, las cuales seleccionamos eligiendo: una estándar (PetClinic) y dos aplicaciones nuevas creadas para esta tesis (KStore y Klink), las cuales reejan negocios reales en la web. La
experimentación con estas aplicaciones nos permitió iterar sobre el análisis y diseño del framework, hasta
que llegamos al alcance necesario para observar los frutos en términos comparativos.
La primera comparación que hicimos permite vericar que el rendimiento de nuestro framework se
mantiene en el mismo orden de magnitud que el resto de las opciones en cuanto a tiempos de indexación
y recuperación. En estas pruebas encontramos que nuestra solución no solo se mantuvo en el mismo orden
de magnitud sino que mejora las marcas de rendimiento de herramientas maduras del estado del arte.
167
168
CAPÍTULO 5.
CONCLUSIONES
El segundo conjunto de comparaciones lo hicimos en términos cualitativos. Estos términos cualitativos
fueron escogidos con el mismo criterio con el que analizamos las herramientas del estado del arte, contrastando cada herramienta con las proposiciones que efectuamos. Esta comparativa cualitativa mostró (a)
que nuestro framework incorporó las mejores prácticas para la resolución del problema y (b) una serie de
características a implementar como continuación de este trabajo.
En terminos de la motivación que describimos en el capítulo introductorio, podemos decir que cumplimos
con el objetivo de aplicar las técnicas de IR para encontrar información almacenada en los objetos de los
modelos de dominio, determinando las actividades, componentes y alternativas de un motor de búsqueda
sobre objetos.
Finalmente, esta la tesis logró contribuir con una solución concreta al problema de IR sobre objetos,
presentando un problema de ingeniería de software, planteando sus alternativas de solución, desarrollando
un alternativa precisa y contrastando los resultados obtenidos respecto del estado del arte.
5.2.
Trabajos Futuros
En esta sección enumeramos las características que podríamos mejorar o incluir en nuestro framework
para conrmar este software como una alternativa de primer nivel.
Índices
En la capa de acceso a datos proponemos agregar características y efectuar algunas mejoras sobre la
implementación actual:
La primera característica a mejorar es el plugin de índices en disco. El plugin actual se podría rediseñar
utilizando un motor de acceso a archivos ad-hoc que implemente front coding, skip pointers, merge
generacional y demás técnicas del estado del arte.
En muchas aplicaciones es conveniente contar con una implementación del índice sobre un RDBMS tal
de poder inspeccionar y mantener el índice utilizando herramientas estándar. Para esto proponemos
desarrollar un plugin que permita almacenar el índice en un esquema relacional.
Por último, dado que en este momento la inspección de los índices en disco requiere un desarrollo ad-hoc,
deberíamos proveer una herramienta genérica de inspección que muestre cómo se indexaron los objetos.
Information Retrieval
Como indicamos oportunamente, las fórmulas de relevancia que implementamos son algo básicas, por lo
que uno de los puntos donde deberíamos trabajar es en validar su ecacia sobre un corpus estándar y
eventualmente iterar sobre ellas.
También relacionado con las técnicas de valoración, creemos que una futura versión debe mejorar el
soporte para el puntaje estructural del tipo HITS y PageRank, facilitando almacenar puntajes por objeto
en el registro maestro.
Características Generales
Existen algunos puntos donde nuestro framework todavía es poco amigable y podemos mejorarlo con
facilidad:
Para mejorar la usabilidad del framework deberíamos implementar una forma simple de obtener información agregada de los resultados como el número total de coincidencias e información de paginado.
La conguración de los mapeos se debería externalizar para permitir variaciones en caliente e indexación
de dominios sobre los que no se cuenta con el código fuente original (recordar que las anotaciones requieren modicar el código fuente). Hacer esta conguración desde archivos XML o JSON es muy simple,
pero requiere desarrollar una clase que lea el XML y efectúe un trabajo similar al AnnotationCongurationMapper.
5.2.
TRABAJOS FUTUROS
169
Una característica satelital soportada por algunos frameworks y que podríamos incoporar tiene que ver con
soportar el resaltado de las coincidencias en el contexto original del término. Esto requiere implementar
un resaltador (highlighter) y el llamado Keyword In Context ó KWIC (ver subsección 2.1.5).
Por último, creemos que una futura versión debería mejorar el soporte transaccional y el logging de
eventos internos al framework.
Integración con Otros Frameworks
Dentro de los planes de desarrollo de este framework, quisiéramos agregar plugins para soportar la indexación automática desde otros ORMs como JDO, JPA, etc.
Por último, últimamente han emergido lenguajes dinámicos compilados a bytecode Java como Groovy /
Grails, a los cuales podríamos darles soporte beneciarse de un framework de IR como el que construimos.
Aún fuera de lenguajes compilados a bytecode Java, se podría portar la herramienta para trabajar en
otros lenguajes similares.
170
CAPÍTULO 5.
CONCLUSIONES
Bibliografía
Ahn, L. et al.
`Telling humans and computers apart automatically'. Commun. ACM, tomo 47, 2, págs.
5660, 2004. ISSN 0001-0782. doi:http://doi.acm.org/10.1145/966389.966390.
Apache.
URL
`Apache ActiveMQ', 2009a .
http://activemq.apache.org/
Apache.
URL
`Apache Lucene', 2009b .
http://lucene.apache.org/
Apache.
URL
`iBATIS Persistence Framework for Java', 2009c .
http://ibatis.apache.org/
Apache.
URL
`Apache Hadoop', 2010.
http://hadoop.apache.org/
Apache Foundation.
tool.
URL
`How Google Finds Your Needle in the Web's Haystack', 2006.
http://www.ams.org/featurecolumn/archive/pagerank.html
Baeza-Yates, R. et al.
URL
Maven project management & comprehension
http://maven.apache.org/
Austin, D.
URL
`Apache Maven Project', 2010.
Modern Information Retrieval. ACM Press - Addison Wesley, 1999.
http://people.ischool.berkeley.edu/~hearst/irbook/
Baeza-Yates, R. et al. `Design trade-os for search engine caching'. ACM Trans. Web, tomo 2, 4, págs.
128, 2008. ISSN 1559-1131. doi:http://doi.acm.org/10.1145/1409220.1409223.
Beck, K.
Test Driven Development: By Example. Addison Wesley, 2002.
Bernard, E.
URL
html
`Hibernate Search - cool, but is it the right approach? Year baby!' Blog, 2007a .
http://blog.emmanuelbernard.com/2007/06/hibernate-search-cool-but-is-it-right.
Bernard, E.
`Hibernate Search: Googling Your Java Technology-Based Persistent Domain Model'. En
JavaOne Conference. 2007b .
Compass Project.
URL
`Compass Search Engine', 2009.
http://www.compass-project.org/
Cutting, D. et al.
`Optimization for dynamic inverted index maintenance'. En SIGIR '90: Proceedings
of the 13th annual international ACM SIGIR conference on Research and development in information
retrieval, (págs. 405411). ACM, New York, NY, USA, 1990. ISBN 0-89791-408-2. doi:http://doi.acm.
org/10.1145/96749.98245.
Cutting, D.
URL
Cutting, D.
URL
`Lucene Talk at University of Pisa', 2004b .
http://lucene.sourceforge.net/talks/pisa/
Dahan, U.
URL
`Dynamization and Lucene', 2004a .
http://cutting.wordpress.com/2004/11/
`Employing the Domain Model Pattern', 2009.
http://msdn.microsoft.com/en-us/magazine/ee236415.aspx#id0400005
171
172
BIBLIOGRAFÍA
Dean, J.
et al. `MapReduce: Simplied Data Processing on Large Clusters'. Communications of the
ACM, tomo 51, págs. 107113, 2008.
Doliner, M.
URL
`Cobertura Code Coverage Tool', 2006.
http://cobertura.sourceforge.net/
Fang, H.
et al.
`A formal study of information retrieval heuristics'.
En SIGIR '04: Proceedings of
the 27th annual international ACM SIGIR conference on Research and development in information
retrieval, (págs. 4956). ACM, New York, NY, USA, 2004. ISBN 1-58113-881-4. doi:http://doi.acm.
org/10.1145/1008992.1009004.
Fielding, R.T.
Architectural Styles and the Design of Network-based Software Architectures.
Tesis
Doctoral, University of California, 2000.
Fowler, M.
URL
Fowler, M.
URL
`Inversion of Control Containers and the Dependency Injection pattern', 2004.
http://martinfowler.com/articles/injection.html
Fowler, M.
URL
Patterns of Enterprise Application Architecture. Addison Wesley, 2002.
http://www.martinfowler.com/books.html
`Inversion of Control', 2005.
http://martinfowler.com/bliki/InversionOfControl.html
Galceran, C.L. `Lematización automática y diccionarios electrónicos'. En C.I.M. y Sara Gómez Seibane,
ed., Lingüística Vasco-Románica, n
o
21 en Oihenart. Cuadernos de Lengua y Literatura, (págs. 331
343). Eusko Ikaskuntza, Donostia-San Sebastián, 2006.
URL
http://hedatuz.euskomedia.org/4082/
Gamma, E.
et al.
Design patterns: elements of reusable object-oriented software.
Addison-Wesley
Professional, 1995.
Gareld, E.
`The Permuterm Subject Index: An Autobiographical Review'. Journal of the American
Society for Information Science, tomo 27, págs. 288291, 1976.
Google.
URL
Google.
URL
`Google Search Appliance', 2009a .
http://www.google.com/enterprise/gsa/
`Google Search Engine', 2009b .
http://www.google.com
Grupo de Estructuras de Datos y Lingüística Computacional.
`Flexionador y lematizador de
palabras del español', 2006. Accedido: Enero de 2010.
URL
http://www.gedlc.ulpgc.es/investigacion/scogeme02/lematiza.htm
Hibernate.
URL
Hibernate.
URL
JCP.
`HyperSQL Database Engine', 2008.
http://hsqldb.org/
`Enterprise JavaBeans 3.0', 2006a .
URL
JCP.
`Hibernate Search', 2009b .
http://search.hibernate.org/
HyperSQL.
URL
`Hibernate - Relational Persistence for Idiomatic Java', 2009a .
http://core.hibernate.org/
http://jcp.org/en/jsr/detail?id=220
`JavaTM Data Objects 2.0 - An Extension to the JDO specication', 2006b .
URL
http://www.jcp.org/en/jsr/detail?id=243
Johnson, R.E. et al.
`Designing Reusable Classes'. Journal of Object-Oriented Programming, tomo 1,
págs. 2235, 1988.
Klas, J.
URL
`Recuperación de Información sobre Modelos de Dominio'. 38 JAIIO AST, 2009.
http://www.scribd.com/doc/19217222/Recuperacion-de-Informacion-sobre-Modelos-de-Dominio
173
BIBLIOGRAFÍA
Kleinberg, J.M.
`Authoritative Sources in a Hyperlinked Environment', 1999.
Levenshtein, V.I.
`Binary codes capable of correcting spurious insertions and deletions of ones'. Prob-
lems of Information Transmission, tomo 1, págs. 817, 1965.
Lexis Nexis Research.
URL
Mackinnon, T. et al.
Manning, C. et al.
URL
`Endo-testing: unit testing with mock objects'. (págs. 287301), 2001.
Introduction to Information Retrieval. Cambridge University Press, 2008.
http://www-csli.stanford.edu/~hinrich/information-retrieval-book.html
Memcached.
URL
`LexisNexis', 2009.
http://www.lexisnexis.com/research/
`Memcached', 2009.
http://memcached.org/
Metaweb Techonologies.
URL
Oracle.
URL
`Oracle Berkeley DB Java Edition', 2009a .
http://www.oracle.com/technology/products/berkeley-db/je/index.html
Oracle.
URL
`Oracle TopLink', 2008.
http://www.oracle.com/technology/products/ias/toplink/index.html
Oracle.
URL
`Oracle Secure Enterprise Search', 2009b .
http://www.oracle.com/technology/products/oses/index.html
Page, L. et al.
PCI.
`Freebase', 2010.
http://www.freebase.com/
`The PageRank Citation Ranking: Bringing Order to the Web', 1998.
`PCI Security Standards Council 2006', 2006.
URL
https://www.pcisecuritystandards.org/
Porter, M.F.
`Snowball: A language for stemming algorithms'.
Published online, 2001.
Accessed
11.03.2008, 15.00h.
URL
http://snowball.tartarus.org/texts/introduction.html
Porter, M.
`An algorithm for sux stripping'. Program, tomo 14, 3, págs. 130137, 1980.
Princeton.
`WordNet 3.0', 2010.
URL
RAE.
http://wordnet.princeton.edu/
Diccionario de la lengua española. Real Academia Española, 2006.
Roberts, D. et al.
`Evolving Frameworks: A Pattern Language for Developing Object-Oriented Frame-
works', 1996.
URL
http://st-www.cs.illinois.edu/users/droberts/evolve.html
Robertson, S.E.
`The Probability Ranking Principle in IR'. Journal of Documentation, tomo 33, págs.
294304, 1977.
Robertson, S.E.
et al.
`Relevance weighting of search terms'.
Journal of the American Society for
Information Science, tomo 27, págs. 129146, 1976.
Rocchio, J.J.
The SMART Retrieval SystemExperiments in Automatic Document Processing, (págs.
313323). Prentice-Hall, Inc., Upper Saddle River, NJ, USA, 1971.
Salton, G. et
al. `A vector space model for automatic indexing'. Commun. ACM, tomo 18, 11, págs.
613620, 1975. ISSN 0001-0782. doi:http://doi.acm.org/10.1145/361219.361220.
SIGIR.
URL
SOX.
`SIGIR'2006 Workshop on Faceted Search - Call for Participation', 2006.
http://sites.google.com/site/facetedsearch/
`Sarbanes-Oxley Act of 2002', 2002.
URL
http://www.gpo.gov/fdsys/pkg/PLAW-107publ204/content-detail.html
174
BIBLIOGRAFÍA
Spring.
URL
Sun.
`Java Object Serialization Specication', 2004.
URL
Sun.
`The Spring Framework 2.5 - Reference Documentation', 2008.
http://static.springframework.org/spring/docs/2.5.x/reference/index.html
http://java.sun.com/j2se/1.5.0/docs/guide/serialization/spec/serialTOC.html
`Java Platform Standard Ed. 6 Documentation', 2008.
URL
http://java.sun.com/javase/6/docs/api/java/io/ObjectOutputStream.html
XStream.
URL
Yahoo.
URL
`XStream Serialization Framework', 2009.
http://xstream.codehaus.org/
`Yahoo Search Engine', 2009.
http://www.yahoo.com
Zobel, J. et al.
`Finding approximate matches in large lexicons'. Softw. Pract. Exper., tomo 25, 3, págs.
331345, 1995. ISSN 0038-0644. doi:http://dx.doi.org/10.1002/spe.4380250307.
Apéndice A
Instalación del Software y el Código
Fuente
A.1.
Instalación del Software de Pruebas
Esta tesis está acompañada de un software que permite reproducir los ensayos del capítulo 4 así como
ejecutar PetClinic, Klink y KStore sobre nuestro sistema de IR.
Para ejecutar las pruebas y demostraciones en Windows o Linux se deben seguir los siguientes pasos:
1. Desde el DVD que acompaña este trabajo se debe instalar VirtualBox (se incluye una versión para
Windows y otra para Linux, en caso de no ser compatible con su sistema operativo, VirtualBox se
puede descargar desde http://www.virtualbox.org/wiki/Downloads).
2. Una vez instalado VirtualBox, se debe copiar y descomprimir el archivo que contiene la imagen de
la máquina virtual (vm-tesis.rar) en una carpeta cualquiera. Esto requiere aproximadamente 6 GB
de espacio libre en disco.
3. Iniciar VirtualBox y agregar la imagen de disco siguiendo la secuencia:
a ) elegir el menú File
b ) dentro del menú, seleccionar la primera opción: Virtual Media Manager
c ) en el diálogo que se abre, seleccionar la tableta Hard Disks
d ) en la parte superior del diálogo, presionar el botón Add
e ) elegir la imagen de disco descomprimida en el paso 2 (archivo con extensión .vdi dentro de la
subcarpeta HardDisks) y presionar el botón Ok.
4. Crear una máquina virtual siguiendo la secuencia:
a ) en la pantalla inicial de VirtualBox presionamos el botón superior New
b ) ignorar la primera pantalla y presionar Next
c ) ingresamos el nombre para la VM, elegimos el SO Linux, la versión Ubuntu y presionamos Next
d ) en la pantalla de selección de memoria, elegir 512 MB RAM y presionar Next
e ) elegir la opción Use existing hard disk, seleccionar el disco agregado en el paso 3 y presionar
Next
f ) ignorar esta última pantalla y presionar Finish
5. Por último, en la pantalla principal iniciamos la máquina virtual presionando el botón Start .
6. Al iniciar la VM, Ubuntu le pedirá el password de inicio de sesión. Tanto el nombre de usuario
como la contraseña se corresponden con la palabra tesis. Una vez que se inició la sesión, abrir el
archivo de instrucciones situado en el escritorio y seguir los pasos indicados.
175
176
APÉNDICE A.
A.2.
INSTALACIÓN DEL SOFTWARE Y EL CÓDIGO FUENTE
Código Fuente y Sitio Web del Proyecto
El código fuente del framework puede descargarse libremente de SourceForge y GitHub:
http://modelsearch.git.sourceforge.net/git/gitweb-index.cgi
http://github.com/jklas/ObjectSearch
Actualmente también existe una página web desde donde se puede seguir el proyecto:
https://sites.google.com/site/objectsearchframework/
Índice alfabético
índice invertido, 22
corpus, 8
documentos, 8
algoritmo de Rocchio, véase relevance feedback
léxico, 9
Apache Lucene, 4
modelo booleano, 13
aplicaciones empresariales, 44
modelo probabilístico, 19
modelo vectorial, 14
búsqueda facetada, 29
modelos, 12
búsquedas literales, 28
precision, véase precision
bases de datos orientadas a objetos, 64
recall, véase Recall
boolean model, véase modelo booleano
inversión del control, 50
inyección de dependencias, 50
caches, 34
campos, 36
Klink, 129
cobertura de código, 129
KStore, 129
desajustes
léxico, 22
de impedancia, 59
lematización, 31
de recuperación, 68
de sincronización, 68
mapeos avanzados, 103
estructural, 68
merónimo, 32
diccionario, 22
modelos de dominio, 2, 46
distancia de Levenshtein, véase edit distance
ordenamiento, 106
edit distance, 29
enterprise applications, véase aplicaciones empresariales
overstemming, 31
PageRank, 38
parónimo, 32
F-measure, 11
patrones
faceted search, véase búsqueda facetada
de arquitectura, 45
ltrado, 99, 106, 126, 165
de diseño, 45
persistencia de modelos de dominio, 53
hidratación, 34
hipónimo, 32
Ad-Hoc, 56
hiperónimo, 32
Administrada, 55
HITS, 40
Binaria, 55
holónimo, 32
Manual, 54
hubs, véase HITS
Object Oriented DBMS, véase Bases de Datos
impedance mismatch, véase desajuste de impedan-
ORM, 58
Orientadas a Objetos
básico, 59
cia
completo, 63
indexación, 9
algoritmos distribuidos, 26
Hibernate, 64
concurrente y distribuida, 96
JDO, 64
JPA, 63
dinámica, 27
XML, 58
oine, 95
online, 94
PetClinic, 129
por Barrido Simple en Memoria, 24
plugin, 98
por Ordenamiento en Bloques, 24
portabilidad, 129
semi-online, 94
posting lists, 22
information retrieval, 1
accuracy, 11
precision, 9
probabilistic model, véase modelo probabilístico
177
178
ÍNDICE ALFABÉTICO
pruebas
comparativas cualitativas, 130
comparativas de rendimiento, 130
de calidad, 129
query log mining, véase relevance feedback
range queries, 29
ranking, 11
recall, 9
reindexación, 97
relevance feedback, 41
algoritmo de Rocchio, 42
explícito, 42
implícito, 42
query log mining, 42
similitud, 99
booleana, 13
probabilística, 20
vectorial, 15
sinónimo, 32
snowball, 32
stemming, 31
tesauro, 29, 33
TF-IDF, 17
vector space model, véase modelo vectorial
zonas, 36
Descargar