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