Integrar un portlet propio con la búsqueda de Liferay
El índice de Liferay
Liferay tiene un índice que utilizan todos sus portlets. Usando el índice podemos:
- Introducir una cadena de texto libre y obtener resultados de todos los portlets que estén integrados con el índice.
- Crear un portlet propio de búsqueda que utilice las capacidades del índice.
En éste artículo vamos a ver cómo crear un portlet que indexe sus contenidos en el índice para que los resultados aparezcan cuando se utilice el portlet genérico de búsqueda de Liferay igual que aparecen los artículos del journal, posts del blog, etc.
El motor de índices por defecto es Lucene pero se pueden utilizar otros como por ejemplo, Solr. De todos modos, la implementación del portlet será la misma independientemente del motor utilizado puesto que nosotros utilizaremos las librerías de Liferay, no las propias de cada índice.
Pasos a seguir
A grandes rasgos, los pasos a seguir para realizar la integración son:
-
Crear una clase indexer que extienda BaseIndexer e implemente todos los métodos necesarios.
Ésta será la encargada de indexar y realizar la búsqueda concreta para nuestro portlet.
-
Crear una clase que extienda HitsOpenSearchImpl e implemente todos los métodos necesarios.
Ésta será la clase que utilizará el buscador de Liferay para acceder a la funcionalidad de búsqueda de nuestro portlet.
-
Registrar las dos clases en el nuevo portlet.
Para los ejemplos de código, vamos a suponer que estamos creando un portlet para gestionar videos. Cuando se añada o modifique un video, debemos actualizar el índice y cuando se busque a través del buscador de Liferay, debemos buscar también en los videos y mostrar por pantalla los resultados con la url para poder acceder directamente a él.
Implementación de la clase Indexer
La clase indexer del portlet será la encargada de añadir, modificar, eliminar y buscar en el índice las entidades del portlet. En nuestro caso, los videos. El indexer debe implementar la interfaz “Indexer” de Liferay pero como en muchos casos hay muchas partes comunes, en lugar de implementar la interfaz, podemos extender “BaseIndexer”. BaseIndexer implementa la funcionalidad básica que en muchos casos ya nos servirá.
Los métodos declarados abstractos en BaseIndexer y que tenemos que implementar són:
protected String getPortletId(SearchContext sc)
Devuelve el identificador del portlet.
public String[] getClassNames()
Devuelve el nombre de las clases de las entidades que utiliza.
protected void doDelete(Object o)
Elimina la entidad del índice.
protected Document doGetDocument(Object o)
Obtiene un objeto Document a partir de la entidad. El objeto Document es el que pasamos al índice.
protected void doReindex(Object o)
Se llama cuando sea necesario reindexar una entidad. Se le pasa la entidad.
protected void doReindex(String className, long classPK)
Se llama cuando sea necesario reindexar una entidad. Se le pasa el nombre de clase y el id.
protected void doReindex(String[] ids)
Se llama cuando sea necesario reindexar todas las entidades relacionadas con una o más organizaciones concretas. Se le pasa un array con los ids de las organizaciones.
public Summary getSummary(Document document, String snippet, PortletURL portletURL)
Devuelve un objeto Summary que es el que utilizará el buscador para obtener la información que muestra por pantalla
Otro método de la interfaz pero que ya implementa BaseIndexer es:
- public Hits search(SearchContext searchContext)
que es el que realiza la búsqueda en el índice. De todos modos, la implementación de BaseIndexer utiliza unos campos concretos para buscar: portlet_id, title, description,… y en nuestro caso, por ejemplo, quizá sería interesante indexar más campos tales como autor (del video) y poder buscar por ellos. En tal caso, la implementación proporcionada por BaseIndexer no serviría y necesitaríamos implementar una propia para poder utilizar los campos que queramos.
Como veremos enseguida, para añadir al índice, modificar, etc. utilizaremos la clase de Liferay SearchEngineUtil y se le tiene que pasar la entidad como un “Document”. Para poder hacer la conversión, tenemos que implementar la función doGetDocument:
@Override
protected Document doGetDocument(Object o) throws Exception {
Video video = (Video)o;
Document doc = new DocumentImpl();
doc.addUID(PORTLET_ID, video.getId());
doc.addModifiedDate();
doc.addKeyword(Field.ENTRY_CLASS_PK, video.getId());
doc.addKeyword(Field.COMPANY_ID, video.getCompanyId());
doc.addKeyword(Field.PORTLET_ID, PORTLET_ID);
doc.addKeyword(Field.GROUP_ID, video.getGroupId());
doc.addText("title", video.getTitle());
doc.addText("description", video.getDescription());
doc.addText("author", video.getAuthor());
doc.addDate("date", video.getDate());
return doc;
}
A destacar del código anterior:
- addKeyword añade el texto que se le pasa por parámetro sin modificaciones
- addText trata el texto que se le pasa por parámetro para eliminar caracteres que no son relevantes como artículos, signos de puntuación, etc.
Luego, podríamos añadir una función para llamar cuando tengamos que añadir un video al índice:
public void addToIndex(Video v, long companyId) {
Document doc = doGetDocument(v);
SearchEngineUtil.addDocument(companyId, doc);
}
Otro ejemplo, para eliminar:
@Override
protected void doDelete(Object o) throws Exception {
Video v = (Video) o;
Document document = new DocumentImpl();
document.addUID(PORTLET_ID, v.getId());
SearchEngineUtil.deleteDocument(v.getCompanyId(), document.get(Field.UID));
}
Otra función importante es getSummary. Cuando se muestran los resultados, se llama a getSummary para cada uno para obtener la información que se tiene que mostrar al usuario:
public Summary getSummary(Document document, String snippet, PortletURL portletURL) {
String title = document.get(Field.TITLE);
String content = snippet;
if (Validator.isNull(snippet)) {
content = StringUtil.shorten(document.get(Field.CONTENT), 200);
}
String entryId = document.get(Field.ENTRY_CLASS_PK);
portletURL.setParameter("struts_action", "/reproductor/reproducir");
portletURL.setParameter("entryId", entryId);
return new Summary(title, content, portletURL);
}
La función debe devolver un objeto Summary. El objeto contiene el título, descripción y url a mostrar al usuario. Utilizando la clase PortletURL, se debe construir la url a la que redirigir si el usuario selecciona un resultado de la búsqueda. En el caso del ejemplo, a la página para reproducir el video indicado.
La función getPortletId, tiene que devolver el id del portlet. Dependiendo de si el portlet está en el núcleo de Liferay, si se ha creado con el EXT SDK, si es un plugin,… el tipo de identificador será diferente. Uno de los casos más comunes es que el portlet se añada como un plugin empaquetado en un war. En éste caso, el identificador del portlet es “nombreportlet_WAR_nombrewar”. Por ejemplo, en el caso de un portlet llamado Reproductor en un war llamado CanalTV, seria: “reproductor_WAR_canaltv”.
El caso de la función getClassNames() es más fácil puesto que simplemente debemos devolver el nombre de las clases y se pueden obtener dinámicamente. Si por ejemplo, tenemos sólo una entidad que se llama Video:
public String[] getClassNames() {
return {Video.class.getName()};
}
Y así sucesivamente hasta implementar todas las funciones abstractas de BaseIndexer.
Implementación de la clase OpenSearch
La clase OpenSearch será la que utilizará el buscador de Liferay para consultar los datos de nuestro portlet. Open Search es un formato de publicación de resultados de búsqueda estandarizado que utilizan muchos sitios web y buscadores. Como en el caso del Indexer, la clase tiene que implementar una interfaz de Liferay, en éste caso la OpenSearch. De todos modos e igual que en el caso anterior, Liferay ya tiene una serie de implementaciones que podemos utilizar según las necesidades de nuestro portlet: BaseOpenSearchImpl, PortalOpenSearchImpl, HitsOpenSearchImpl,…
Una caso muy habitual es tener que hacer una búsqueda por texto libre que devuelva una serie de resultados ordenados por calidad, dónde los más “probables” aparezcan primero. En éste caso, podemos extender la clase HitsOpenSearchImpl puesto que es justamente lo que hace.
Las funciones abstractas que debemos implementar son:
public String getPortletId()
Devuevle el id del portlet (el mismo que se utiliza en el Indexer)
public String getSearchPath()
Devuelve el path de la búsqueda. Habitualmente es un string del tipo /c/nombreportlet/open_search. Ej: /c/video/open_search
public String getTitle(String keywords)
Devuelve el título de la búsqueda. Por ejemplo: “Búsqueda de videos: ” + keywords. Aparecerá en los resultados.
Registrar las dos clases en el nuevo portlet
Una vez creadas las dos clases, debemos indicar a Liferay que una es la clase Indexer y la otra la Open Search. Para hacerlo, se debe añadir a la configuración del portlet en liferay-portlet.xml. Por ejemplo:
<portlet>
<portlet-name>videos</portlet-name>
<indexer-class>com.agilogy.test.VideoIndexer</indexer-class>
<open-search-class>com.agilogy.test.OpenSearch</open-search-class>
<instanceable>false</instanceable>
</portlet>
Una vez hecho, nuestro portlet indexará su contenido en el índice de Liferay y podrá ser buscado a través del portlet de búsqueda o cualquier otro que utilice el índice de Liferay.