Mittwoch Dez 03, 2008

Compass - oder - Suchen einfach gemacht

Eine der wirklich wichtigen Dinge in einer modernen Webapplikation ist eine gut funktionierende Suche. Um eine solche zu implementieren gibt es in Java-Land natürlich das fantastische Lucene - allerdings ist Lucene ziemlich low-level und es erfordert einiges an Arbeit und Lines of code um sein Domänen- Modell in Lucene Dokumente zu überführen, einen Such Controller zu basteln und dann aus den gefundenen Dokumenten wieder die ursprünglichen Domänen-Modell Objekte zu machen - nicht zu vergessen die Mühen die es bereitet den Index dann up-to-date zu halten wenn die Modell-Objekte geändert werden. Doch ich bin natürlich nicht der einzige, der dieses Problem kennt und so hat sich Shay Banon sehr kundig dieses Problems angenommen und das Compass Framework entwickelt. Was bietet das Compass Framework? Compass lässt sich wohl grob in zwei Hauptbereiche aufteilen Einen deklarativen Mechanismus um seine Domänen-Modell-Objekte (DMO) auf Lucene Dokumente zu mappen - entweder über Annotations oder über XML Mapping Dateien. Eine Mechanismus, der sich in den verwendeten DataAccess Layer einklinkt und automatisch den Lucene Index up-to-date hält. Dazu ein (nicht 100% vollständiges) Beispiel. Es handelt sich bei meiner Webapplikation im übrigen um eine Spring/SpringMVC Applikation, bei der Hibernate als Persistenz/ORM Schicht zur Verwendung kommt. Zunächst ein simple DMO


import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;


import org.compass.annotations.Searchable;
import org.compass.annotations.SearchableComponent;
import org.compass.annotations.SearchableId;
import org.compass.annotations.SearchableProperty;

@Entity
@Table
@SequenceGenerator(name = "newsSequence", sequenceName = "news_seq")
@Searchable()
public class News  {

	private Long id;
	private String title;
	private String summary;
	
	@Id
	@GeneratedValue(generator = "newsSequence")
	@SearchableId
	public Long getId() {
		return id;
	}
	public void setId(Long id) {
		this.id = id;
	}
	
	@Column
	@SearchableProperty
	public String getTitle() {
		return title;
	}
	public void setTitle(String title) {
		this.title = title;
	}
	@Column(length = 500)
	@SearchableProperty
	public String getSummary() {
		return summary;
	}
	public void setSummary(String summary) {
		this.summary = summary;
	}
}

Es handelt sich hier um ein POJO News Objekt, welches mit Hilfe von JPA/Hibernate persistiert wird. Die @Searchable Annotation auf Klassenebene zeigt an, daß dieses Objekt indiziert werden soll. @SearchableId teilt Compass mit, über welche Eigenschaft eine Instanz dieses Objekts eindeutig identifiert werden kann und die @SearchableProperty Annotationen mappen die zu indizierenden Eigenschaften. So weit so einfach. Jetzt muß Compass konfiguriert werden. Dazu fügt man einer XML basierten Spring Applikationskontext-Konfiguration folgendes hinzu:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:compass="http://www.compass-project.org/schema/spring-core-config"
	xsi:schemaLocation=" 
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd 
		http://www.compass-project.org/schema/spring-core-config http://www.compass-project.org/schema/spring-compass-core-config-2.1.xsd">
	
	<compass:compass name="compass" txManager="transactionManager">
		
        <compass:connection>
            <compass:file path="${compass.index.path}" /><!-- A system property or environment variable of that name needs to be specified at runtime, defaults to '/[temp_dir]/compass-index' -->
        </compass:connection>
        <compass:transaction factory="org.compass.spring.transaction.SpringSyncTransactionFactory" />
        
        <compass:mappings>
        	<compass:class name="de.woerd.aktionsbund.model.News"/>
        </compass:mappings>
    </compass:compass>
	
	<bean id="compassGps" class="org.compass.gps.impl.SingleCompassGps" init-method="start" destroy-method="stop">
		<property name="compass" ref="compass" />
		<property name="gpsDevices">
			<list>
				<bean class="org.compass.spring.device.SpringSyncTransactionGpsDeviceWrapper">
					<property name="gpsDevice">
						<bean  class="org.compass.gps.device.hibernate.HibernateGpsDevice">
							<property name="sessionFactory" ref="sessionFactory" />
							<property name="nativeExtractor"><bean class="org.compass.spring.device.hibernate.SpringNativeHibernateExtractor" /></property>
						</bean>
					</property>
					<property name="name" value="hibernateGpsDevice"/>
				</bean>
			</list>
		</property>
	</bean>
	
</beans>
Dabei sind transactionManager und sessionFactory die entsprechend für Hibernate konfigurierten Beans. Von nun an lauscht Compass an allen Transaktion und aktualisiert den Index entsprechend. Nun zur Suche. Man kann den Lucene Index direkt mit Lucene durchsuchen aber Compass stellt noch ein paar kleine Helfer dazu zur Verfügung. Ich habe eine dieser Helfer nach einem Tip im Compass Forum noch etwas erweitert, da ich die Suche auf bestimmte DMO begrenzen können wollte.

import org.compass.core.Compass;
import org.compass.core.CompassQuery;
import org.compass.core.CompassQueryBuilder;
import org.compass.core.CompassSession;
import org.compass.core.support.search.CompassSearchCommand;
import org.compass.core.support.search.CompassSearchHelper;

public class CompassAliasSearchHelper extends CompassSearchHelper {
	
	private String[] aliases = {};
	
	public CompassAliasSearchHelper(Compass compass, Integer pageSize) {
		super(compass, pageSize);
	}

	public CompassAliasSearchHelper(Compass compass) {
		super(compass);
	}
	
	public CompassAliasSearchHelper(Compass compass, Integer pageSize, String[] aliases)
	{
		this(compass, pageSize);
		this.aliases = aliases;
	}

	@Override
	protected CompassQuery buildQuery(final CompassSearchCommand searchCommand,	final CompassSession session) {
		
		CompassQueryBuilder queryBuilder = session.queryBuilder();

		CompassQuery compassQuery = queryBuilder.queryString(searchCommand.getQuery()).toQuery().setAliases(this.aliases);
		return compassQuery;
	}
	
	

}
Dann kann man sehr schön mit Code ähnlich dem folgenden Fragment suchen

	import org.compass.core.Compass;
	import org.compass.core.support.search.CompassSearchCommand;
	import org.compass.core.support.search.CompassSearchResults;
	
	...
	
	int PAGE_SIZE=10;
	int PAGE_NO = 1;
	String query = "eine such eingabe";
	CompassAliasSearchHelper searchHelper = new CompassAliasSearchHelper(compass, PAGE_SIZE, new String[] {"News"});
		
	CompassSearchResults result = searchHelper.search(new CompassSearchCommand(query, PAGE_NO));
Dabei ist 'compass' eine Instanz von org.compass.core.Compass, die gewöhnlich von Spring in die suchende Klasse injiziert wird. Es hat mich etwas Zeit gekostet, mich in Compass einzuarbeiten. Jetzt möchte ich es nicht mehr missen. Compass ist einfach da im Hintergrund und verrichtet zuverlässig seinen Dienst. Zudem lässt sich jedes Problem relativ schnell über einen Post an das Compass Forum lösen, in dem der Master himself sehr viele Fragen schnell und freundlich beantwortet. Der nächste Schritt ist jetzt den Index über mehrere Applikationsserver zu clustern, evtl. mit Terracotta - wünscht mir Glück! P.S.: Noch ein Wort der Warnung: Wenn man mit den Suchergebnissen arbeitet, hat man Zugriff auf 'echte' Instanzen der DMOs - allerdings sind in jenen natürlich nur die Eigenschaften mit Werten befüllt, die Compass auch indiziert hat, d.h. man sollte ich gut überlegen, was man alles indiziert und wie man mit den Ergebnissen arbeitet - die Objekte verhalten sich einfach nicht ganz genau so wie jene, die aus einer Hibernate Session kommen.

Kommentare:

Senden Sie einen Kommentar:
  • HTML Syntax: Eingeschaltet