Montag, 4. März 2013

Play Framework und Nginx: Durchreichen von Medien

Der Verbund von Play! und einem Reverse Proxy wie Nginx funktioniert problemlos für HTML-Dateien. Sobald aber andere Dateien wie Bilder oder XML-Dokumente zum Client durchgereicht werden sollen, werden sie von Nginx blockiert. Der Grund dafür ist, dass Nginx nicht mit gechunkten Antworten auskommt. In diesem Fix wurde das Problem bei Play! behoben, allerdings nur für Dateien im Assets-Verzeichnis (CSS, Logo, etc). Wie können nun aber Dateien, die nicht in den Assets liegen, erfolgreich geladen werden?
Als Beispiel nehmen wir die Sitemap, ein zentraler Baustein vom SEO, welcher auf jeder Homepage verfügbar sein sollte. Da die Sitemap oft wechselt, ist es sinnlos, diese statisch in Play! einzubinden (zusammen mit den CSS oder Javascripts im Assets-Vezeichnis). Die Sitemap ist als XML-Datei irgendwo auf dem Server gespeichert und soll beim Aufruf von beispielsweise http://mydomain.com/sitemap.xml angezeigt werden. Nachdem in Play! eine Route und der dazugehörige Controller erstellt wurde, können wir die Methode in der Controller-Klasse implementieren.
Route
 GET          /sitemap.xml                    controllers.SEO.getSitemap()  
Controller: SEO.java
 public static Result getSitemap() {  
      final File file = new File("/location/of/sitemap.xml");  
      if (!file.canRead()) {  
           return notFound();  
      }  
        
      return ok(file);  
 }  

Das File wird vom Dateisystem gelesen und an den Client weitergeleitet. Ohne Reverse Proxy funktioniert der obige Code tadellos. Ein Reverse Proxy vor dem Server erfordert aber ein paar zusätzliche Zeilen:
Controller: SEO.java
 public static Result getSitemap() {  
      final File file = new File("/location/of/sitemap.xml");  
      if (!file.canRead()) {  
           return notFound();  
      }  
   
      FileInputStream stream;
      ByteArrayOutputStream bos;
      try {  
           // convert the file to a byte-array  
           stream = new FileInputStream(file);  
           bos = new ByteArrayOutputStream();  
           byte[] buffer = new byte[1024];  
           for (int readNum; (readNum = stream.read(buffer)) != -1;) {  
                bos.write(buffer, 0, readNum);  
           } 
   
           // get the mime type. For a XML-document it is "application/xml"  
           String mimeType = MimeTypes.forFileName(file.getName()).get();  
           return ok(bos.toByteArray()).as(mimeType);  
      } catch (IOException e) {  
           // return an error to indicate the exception  
           return internalServerError();  
      } finally {
           if(stream != null) { stream.close(); }
      }
 }  

Die XML-Datei muss als Byte-Array durch Nginx durchgeschleust werden, damit sie nicht blockiert wird. Die selbe Prozedur kann bei Bildern oder anderen Dateitypen angewendet werden. Wichtig dabei ist, dass der Mime-Type entsprechend angepasst wird.

Montag, 7. Januar 2013

Defaultsettings für JVM Optionen


Die Java Virtual Machine (JVM) unterstützt bekanntlich sehr viele Optionen, um deren Laufzeitverhalten zu kontrollieren und zu optimieren. Siehe dazu
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html.

Leider ist es jedoch schwieriger herauszufinden, was nun der Defaultwert für eine bestimmte Option ist, d.h., ist die gewünschte Option bereits aktiv oder muss ich diese via Argument einschalten? Um dies einfach herauszufinden zu können, gibt es in der Java-Distribution das Tool jinfo. Wenn man zum Beispiel herausfinden möchte, ob die Option AggressiveOpts eingeschaltet ist kann man folgenden Befehl ausführen:

jinfo -flag AggressiveOpts <Prozess-ID der JVM>
was bei mir (mit JRE 1.6.0_30-b12 unter RHEL 5.8) zu folgender Ausgabe führt:
-XX:-AggressiveOpts
d.h., diese Option ist standardmässig nicht eingeschaltet.

jinfo bietet weiter die Möglichkeit die Optionen zur Laufzeit zu verändern oder alle Optionen und System-Properties aufzulisten. Ein nettes Tool, welches ich bis jetzt übersehen habe...

Dienstag, 13. November 2012

Mahout: Recommender kombinieren und mit JUnit verbessern

Wie weiss ein Computer, was der Benutzer eventuell mag? Amazon ist der Vorreiter bei der Anwendung dieser Algorithmen. Wenn ein Artikel angeschaut wird, erscheint die Sektion „Andere Benutzer haben auch gekauft...“.

In jedem Buch, das erklärt, wie solche Recommender funktionieren, wird klargemacht, dass es nicht einen „besten“ Algorithmus für die Erzeugung von Empfehlungen gibt. Welcher Algorithmus der beste ist, muss jeweils empirisch an realen Daten ermittelt werden. Wieso nicht eine Kombination verschiedener Recommender? Skalierbarkeit und Performance leiden stark darunter – hier trotzdem ein Versuch...

Kombination verschiedener Algorithmen
Um das beste Resultat zu erhalten, habe ich eine eigene Implementierung vom Interface Recommender.java, Teil der Apache Mahout Library, erstellt, welche mit variabler Gewichtung Empfehlungen aus drei bestehenden Algorithmen generiert:
  • User-Similarity: Für jeden Nutzer wird eine „Nachbarschaft“ von den ähnlichsten anderen Nutzern generiert (z.B. aus den meisten ähnlichen Bewertungen). Empfohlen sind alle Events, welche die Nachbarschaft bewertet hat, der Nutzer selbst aber noch nicht kennt.
  • Item-Similarity: Ähnlich wie Tanimoto-Koeffizient: Die Ähnlichkeit zwischen Benutzern wird aus der Anzahl der gemeinsamen Events berechnet. Die Mathematik dahinter könnte alleine ein ganzes Buch füllen. Für die Distanz zwischen den Items wird mit der Log-Likelihood-Funktion berechnet.
  • Slope-One: Empfehlungen werden berechnet, indem der durchschnittliche Unterschied zwischen den Bewertungen der Events berücksichtigt wird. Beispielsweise wird Zirkus Knie immer um 1 besser bewertet als der Zirkus Monti. Und zudem wird Zirkus Knie durchschnittlich gleich bewertet wie Zirkus Royal. Ein Nutzer bewertet nun Monti mit 2, und Royal mit 4. Wie wäre nun die geschätzte Wertung für Knie? Wenn wir von Monti ausgehen würden, wäre dies 2 + 1 = 3. Ausgegangen von Royal wäre es 4 + 0 = 4 (immer gleich wie Knie). Als Schätzung gibt der Computer nun eine durchschnittliche Bewertung von 3.5 an.

 public class MyRecommenderBuilder implements RecommenderBuilder {  
      @Override  
      public Recommender buildRecommender(DataModel dataModel) throws TasteException {  
           // this map holds all recommenders  
           Map<Recommender, Integer> recommenders = new HashMap<Recommender, Integer>();  
   
           if (userSimilarityFactor > 0) recommenders.put(getUserSimilarityRecommender(dataModel), userSimilarityFactor);  
           if (logLikelihoodFactor > 0)recommenders.put(getLogLikelihoodRecommender(dataModel), logLikelihoodFactor);  
           if (slopeOneFactor > 0) recommenders.put(getSlopeOneRecommender(dataModel), slopeOneFactor);  
   
               // create the recommender  
           return new CombinatedRecommender(recommenders);  
      }  
 }  
 public class CombinatedRecommender implements Recommender {  
      private final Map<Recommender, Integer> recommenders; // holds all recommenders  
      private int sumWeight = 0; // total weight of the recommenders  
   
      public CombinatedRecommender(Map<Recommender, Integer> recommenders) {  
           this.recommenders = recommenders;  
           for (Map.Entry<Recommender, Integer> recommender : recommenders.entrySet()) {  
                sumWeight += recommender.getValue();  
      }  
   
      @Override  
      public List<RecommendedItem> recommend(long userID, int howMany) throws TasteException {  
           // Map holding all recommendations and the according score (weighted)  
           Map<Long, Float> allRecommendations = new Hashtable<Long, Float>();  
   
           // get recommendations of all recommenders  
           for (Map.Entry<Recommender, Integer> recommender : recommenders.entrySet()) {  
                // iterate through all recommendations of this recommender  
                for (RecommendedItem thisRec : recommender.getKey().recommend(userID, howMany)) {  
                     long itemID = thisRec.getItemID();  
   
                     // to weight the recomendations, multiply the accuracy of the recommendation with the  
                     // weight of the recommender  
                     float thisScore = thisRec.getValue() * recommender.getValue();  
   
                     // adds this recommendation to the map of all recommended items. If this recommendation has already
                     // been done by another recommender, increase the score of the recommendation  
                     addOrInsert(allRecommendations, itemID, thisScore);  
                }  
           }  
   
           // ... next steps ...  
           // 1. sort the result descending (highest score first) and limit the size to ‘howMany’  
           // 2. return this list  
      }  
 }  
Code 1: MyRecommenderBuilder.java erstellt drei Recommender und gewichtet diese (in einer Map abgelegt). CombinatedRecommender.java erhält diese Map im Konstruktor . Der Aufruf von recommend(...) iteriert durch alle vorhandenden Recommender und lässt, unabhängig von den anderen Recommendern, Empfehlungen generieren. Die Resultate werden in allRecommendations gespeichert. Wenn mehrere Recommender dieselbe Empfehlung machen, erhält diese Empfehlung mehr Gewicht.

Mit JUnit die Faktoren verbessern
Eine flexible Gewichtung ermöglicht die einfache Anpassung an reale Daten. In einer Test-Klasse werden alle möglichen Kombinationen von Gewichtungen ausprobiert und die Deckung (0-100%) berechnet. Die Kombination mit der grössten Deckung wird dann übernommen. Der nächste Code-Ausschnitt zeigt einen Teil aus dem parametrierten JUnit-Test, der die beste Faktorenkombination berechnet. Diese Faktoren können dann im Code 1 produktiv eingesetzt werden.


 private final int userSimFactor;  
 private final int logLikelihoodFactor;  
 private final int slopeOneFactor;  
   
 private static int bestFactor1;  
 private static int bestFactor2;  
 private static int bestFactor3;  
 private static double bestScore = Double.NaN;  
   
 @Test  
 public void testFactors() throws TasteException {  
      RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();  
      MyRecommenderBuilder recommenderBuilder = new MyRecommenderBuilder(userSimFactor,  
                                     logLikelihoodFactor, slopeOneFactor);  
   
      // how accurate are those factors?  
      double score = evaluator.evaluate(recommenderBuilder, null, dataModel, 0.9, 1.0);  
      if (score < bestScore) {  
           // new improved score --> Save these factors  
           bestFactor1 = userSimFactor;  
           bestFactor2 = logLikelihoodFactor;  
           bestFactor3 = slopeOneFactor;  
           bestScore = score;  
      }  
 }  
Code 2: Test-Methode berechnet mittels RecommenderEvaluator.evaluate(...) den Deckungsgrad für beliebige Faktoren und ermittelt die beste Kombination.

Weitere Verbesserungen konnten innerhalb der bereits bestehenden Recommendern vorgenommen werden. Beispielsweise wird für die User-Similarity eine Nachbarschaft mit einer vordefinierten Anzahl von nächsten Usern berechet. Je nach Test-Daten ist es möglich, dass eine Nachbarschaft von nur zwei Benutzern das beste Resultat liefert. Bei anderen Test-Daten ist ein Optimum bei einer Nachbarschaft von über 100 weiteren Benutzern durchaus normal. Code 3 zeigt einen weiteren JUnit-Test, der, analog zu testFactors() die beste Nachbarschaftsgrösse ermittelt.


 private final int neighborhoodSize;  
 private static int bestNeighborhoodSize;  
 private static double bestScore = Double.NaN;  
   
 @Test  
 public void testNeighborhoodSize() {  
      RecommenderEvaluator evaluator = new AverageAbsoluteDifferenceRecommenderEvaluator();  
      RecommenderBuilder recommenderBuilder = new RecommenderBuilder() {  
           @Override  
           public Recommender buildRecommender(DataModel dataModel) throws TasteException {  
                // use the Euclidian distance. Test with oder Similarities can be done  
                UserSimilarity userSimilarity = new EuclideanDistanceSimilarity(dataModel, Weighting.WEIGHTED);  
                // calculate neighborhood of the users with the parameterized size  
                UserNeighborhood neighborhood = new NearestNUserNeighborhood(neighborhoodSize, userSimilarity, dataModel);  
                // get recommendations  
                return new GenericUserBasedRecommender(dataModel, neighborhood, userSimilarity);  
           }  
      };  
             
      // how accurate is this neighborhood size  
      double score = evaluator.evaluate(recommenderBuilder, null, dataModel, 0.9, 1.0);  
      if (score < bestScore) {  
           // found a better neighborhood than before --> save better  
           bestNeighborhoodSize = neighborhoodSize;  
           bestScore = score;  
          }  
 }  
Code 3: Wie bei ‚Code 1’ berechnet diese Test-Methode den Deckungsgrad, dieses Mal aber mit variierender Nachbarschaftsgrösse.


Performance – was noch zu retten ist
Somit wäre noch das Thema Performance anzusprechen. Solche Empfehlungsgeneratoren skalieren sehr schnell (oft quadratisch mit der Anzahl der User). Wenn Real-Time-Empfehlungen berechnet werden sollen, müssen Resultate in genügend schneller Zeit zurückgeliefert werden. Ein grosser Nachteil der obigen Implementierung ist, dass gleich drei Recommender parallel laufen. Wenn nun beispielsweise 25 Empfehlungen gefragt sind, müssen alle drei Recommender je 25 Empfehlungen erstellen. Danach werden diese gewichtet und zusammengemischt. Der Speicherbedarf der Empfehlungs-Engine ist von deren Konfiguration abhängig.
Eine Performance-Verbesserung für Live-Empfehlungen wäre eine gleichzeitige Berechnung der Empfehlungen durch mehrere Recommender. Mehrere Threads erlauben eine optimale Speicher- und Prozessorauslastung.

Samstag, 20. Oktober 2012

Eclipse Finance Day - Résumé

Diese Woche fand bei der UBS in Zürich der Eclipse Finance Day statt. Es wurden interessante Einblicke gewährt wie Eclipse Technologie im Bankenumfeld und darüber hinaus eingesetzt wird.
Der Fokus lag dabei auf Modellierung (EMF, Xtext, ...) - es wurden aber auch andere Themen wie Modularisierung (OSGi), RCP-Entwicklung, ALM, etc. präsentiert.
Alles Technologien welche auch wir bereits seit Jahren in Projekten erfolgreich einsetzen.

Insgesamt ein gelungener Event mit knapp 100 Besuchern. Zu einigen Vorträgen sind die Folien bereits online verfügbar.

Samstag, 15. September 2012

Team-Event in Filzbach (Kerenzerberg)

Auch dieses Jahr haben wir wieder einen Team-Event organisiert. Dieser führte uns nach Filzbach auf den "Erlebnisberg". Hier haben wir verschiedene Aktivitäten unternommen. Vor den Aktivitäten mussten wir uns natürlich zuerst mit dem Älpler-Grill stärken:
Danach ging es los mit der ersten Aktivität. Hier ging es darum, mittels Kompass möglichst genau westwärts zu gehen und Objekte zu finden. Wie bei unserer alltäglichen Arbeit war Genauigkeit und Teamarbeit gefragt:
Im unwegsamen Gelände war dies gar nicht so einfach:
Danach ging es weiter mit dem "Flying Fox". Die Schlucht, welche wir zu überwinden hatten, war echt tief. Für mich galt einfach "nicht runterschauen". Anderen machte dies weniger aus:
Spass machte es auf jeden Fall:
Die nächste Aktivität war dann Abseilen über eine 25 hohe Steilwand:
Danach ging es weiter mit Bogenschiessen, was natürlich viel schwieriger ist, als es aussieht. Ich war jeweils froh die Zielscheibe zu treffen:
Michael konnte man am ehesten noch als "Robin Hood" bezeichnen. Meine Haltungsnote für ihn "10.0":
Nach all diesen Aktivitäten darf natürlich der gemütliche Teil auch nicht fehlen:
Die verschiedenen Aktivitäten haben mir viel Spass gemacht und die Organisation durch erlebnisberg.ch war tadellos.

Mittwoch, 12. September 2012

EclipseRAP Webapps für das Google Crawling vorbereiten

RAP Webapplikationen erzeugen die einzelnen Seiten durch interne AJAX-Calls. Ein schön lesbarer HTML-Code, welcher von Suchmaschinen durchsucht werden könnte, ist also nicht vorhanden. Der Grund dafür ist: Der Browser kann die Calls ausführen, um die Seiten dynamisch zu erzeugen, die Suchmaschinenroboter können dies jedoch nicht.

Natürlich will man aber, dass auch der Inhalt in der Webapplikation (welcher normalerweise dynamisch kreiert und angezeigt wird) in den Suchergebnissen der Suchmaschinenanbieter auftaucht. Um dies zu berwerkstelligen, hat Google den Google Ajax Crawling Guide für Entwickler von AJAX-basierten Webapplikation bereitgestellt. 

Schritt 1: Ajax crawling scheme

Vorbereitend muss man sicherstellen, dass die Webapplikation das "AJAX crawling scheme" unterstützt. Dieses Schema besagt folgendes: Alle URL Parameter (=hash fragments) dürfen erst nach der Zeichenfolge #! auftauchen. Das heisst für eine URL:

www.example.com/myapp#parameter 

wird zu 

www.example.com/myapp#!parameter

Standardmässig unterstützt eine EclipseRAP-Anwendung keine Links mit URL-Parametern.
Dies kann jedoch mit Hilfe der RWT BrowserHistory sehr einfach eingebaut werden,indem man einen Call ähnlich zu folgendem aufruft:  


RWT.getBrowserHistory().createEntry( "!mystate", "Example" ); 

Dieser Aufruf fügt einen neuen Eintrag in die History des Browsers ein. Der Eintrag wird die folgende URL haben: 
(Achtung das #-Zeichen wird automatisch eingefügt. Das ist auch richtig so.)

example.com/myapp#!mystate


Um für Links dieser Form auch den richtigen (dynamisch erzeugten) Inhalt zu liefern, muss noch ein BrowserHistoryListener registriert werden, welcher Änderungen in  der URL abfängt und entsprechend behandelt, indem er den gewünschten Inhalt dynamisch erzeugt und im RAP Webapp anzeigt. Der Listener sollte so ähnlich aussehen:



RWT.getBrowserHistory().addBrowserHistoryListener( new BrowserHistoryListener() {
  public void navigated( BrowserHistoryEvent event ) {
    // show state represented by event.entryId
  }
} ); 



In der navigated()-Methode des Listeners kann man dann herausfinden,  welche Parameter in der URL angegeben wurden, und den diesen Parametern entsprechenden Inhalt in der Webapplikation anzeigen. Auf diese Weise kann man auch "Deep links" für das Webapp zur Verfügung stellen. Ein User der eine URL der Webapp direkt mit einem Parameter aufruft, bekommt direkt die Ansicht, die er möchte.


Schritt 2: Inhalt als lesbaren HTML-Code darstellen

Nun muss man eine Version der Webapp-Seiten generieren, welche in HTML-Code dargestellt sind, damit der Crawling Bot sie lesen und nach Inhalt durchsuchen kann. Diese HTML-Dokumente nennt man auch "HTML-Snapshots", da sie im Grunde eine Momentaufnahme der Webapplikation in Form von HTML darstellen.
Im Crawling Guide von Google wird hier die Verwendung eines "headless HTML-Browser" für Java vorgeschlagen (siehe: hier). 

Wem die Verwendung von dieser Methode nicht zusagt, kann das Erzeugen der HTML-Snapshots selbst in die Hand nehmen. Dabei erzeugt man für jeden möglichen Inhalt der Webapplikation (welcher dynamisch ändern kann) eine eigene HTML-Seite z.B. direkt aus Java heraus und legt Sie im Filesystem des Webservers ab.


Schritt 3: Crawler erkennen und mit den HTML-Dokumenten füttern.

Die Strategie ist wie folgt: Unser System muss die HTTP-Anfragen abfangen, und erkennen, ob es sich bei dem "Besucher" um einen realen User handelt oder um einen Google Crawling Roboter. Die wichtige Frage ist: Woran erkenne ich eine Abfrage des Google Crawling Roboter? Die hier gennante Antwort findet man im Google AJAX Crawling Guide. Man erkennt den Crawling Bot daran, dass in der URL des HTTP-Requests das Fragment  #! durch das Fragment  _escaped_fragment_=  ersetzt wird. Bsp:

Normale Anfrage:      example.com/myapp#!mystate
Crawling Bot Anfrage:   example.com/myapp_escaped_fragment_=mystate


Um den Google-Bot abzufangen sollte man einen  Servlet-Filter schreiben, welches alle Anfragen auf die URL der Webapplikation abfängt und nach dem escaped-fragment des Bots sucht. Das Servlet sollte auf dem produktiven Webserver laufen, auf dem auch die Webapplikation läuft (man kann das Servlet auch direkt in das selbe WAR-File exportieren wie die Webapplikation). Im web.xml File der Webapplikation muss man auch den Servlet-Filter angeben und auf die gewünschte URL ansetzen.

Der Servlet-Filter kann folgendermassen aussehen (siehe hier auch unter Punkt 3.):




public final class CrawlServlet implements Filter {

public void doFilter(ServletRequest request, ServletResponse response,
      FilterChain chain) throws IOException {
      ...
      if ((queryString != null) && (queryString.contains("_escaped_fragment_"))) 
 {
       ...

/* find the HTML-Snapshot for the desired part of the   
*  webapp(depending on the parameters in the querystring)
*  in the filesystem (if you crated the HTML-Snapshots by 
*  yourself. 
*  OR: use HTMLUnit as a Headless Browser to generate 
*  the HTML-Snapshot here..
*/

//feed the HTML-Snapshot to the crawling bot

HttpServletResponse res = (HttpServletResponse) response;
res.setContentType("text/html;charset=UTF-8");
PrintWriter out = res.getWriter();

out.println(*** your HTML-Snapshot here ***);
out.close();

     } else {
      try {
        // not an _escaped_fragment_ URL, so move up the chain of servlets
        chain.doFilter(request, response);
      } catch (ServletException e) {
        System.err.println("Servlet exception caught: " + e);
        e.printStackTrace();
      }
    }
    ...
  }
}

Schritt 4:  Optimieren & Testen

Um das Ganze bezüglich der Google Indexierung noch zu optimieren, sollte man eine XML-Sitemap für die Crawling-Bots zur Verfügung stellen. Die Links in der Sitemap sollten auf die verschiedenen dynamischen Inhalte eurer Seite verlinken (mithilfe der #!-URL's). Um die Generierung der Sitemap zu automatisieren helfen Java-Libraries wie diese . 
Die Sitemap kann  man dann im Google Webmasters Tool registrieren und überprüfen lassen.

Auch zum Testen des Crawling-Mechanismus eignet sich Google Webmaster Tools hervorrangend. Unter "Abruf wie durch Google" kann man eine Anfrage des Google Crawling-Roboter simulieren, und sieht anschliessend, was zurückgegeben wird. So kann man kontrollieren, ob der gewünschte HTML-Snapshot bei Google ankommt.

Links:

Google Webmaster Tools
StackOverFlow Anleitung zum Google Crawling für RAP
Google AJAX Crawling Guide
Offizielle Java Servlet Spezifikation



Donnerstag, 30. August 2012

Eclipse Finance Day 2012

Am 16. Oktober 2012 findet in Zürich der Eclipse Finance Day 2012 statt. Mehr Infos dazu finden sich hier.