Ich habe bereits im Mai etwas über Matt Chaput’s Whoosh-Projekt geschrieben. Den Beitrag könnt ihr hier nachlesen. Seit der damals aktuellen Version 0.3.15 sind nicht einmal 4 Monate vergangen und nun ist das Projekt schon fast bei der finalen Version 1.0.0 angekommen. Ein großes Lob ist aber nicht nur für die schnelle Weiterentwicklung angebracht, sondern vor allem für die hervorragende Dokumentation. Im Mai versprach ich euch außerdem, dass ich mich etwas mehr mit Whoosh auseinander setze und deshalb möchte ich nun ein kleines Script vorstellen, das den RSS-Feed dieses Blogs in einen Whoosh-Index wirft und abfragbar macht.
Im Grunde geht es mir darum zu zeigen wie man mit sehr wenig Zeitaufwand einen Prototypen für eine individuelle Suchlösung erstellen kann, ohne gleich einen dicken Search Server installieren zu müssen, der verhältnismäßig aufwendig an die eigenen Bedürfnisse angepasst werden muss. Außerdem bietet dieser Ansatz durch die leichte Erweiterbarkeit die Möglichkeit schnell und iterativ verschiedene Ranking- und Scoring-Strategien auszuprobieren.
Später lassen sich die gewonnen Erkenntnisse natürlich auch auf eine weniger leichtgewichtige, dafür aber hoch skalierbarere Produktivumgebung übertragen. Der Vorteil dabei: Die wichtigsten Erkenntnisse konnten schon im Vorfeld zeitsparend auf Basis des Prototypen gewonnen werden.
Im Grunde reichen drei Komponenten aus, um einen simplen Client zur Abfrage des Indexes, den Indexer selbst und dessen Schema zu scripten:
- schema.py
- writer.py
- searcher.py
Das Schema wird in schema.py definiert und sieht so aus:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
"""
Ein einfaches Schema um schnell & einfach das
Wordpress-RSS in einen Whoosh! Index zu werfen
"""
from whoosh.fields import Schema, TEXT, KEYWORD, DATETIME
from whoosh.analysis import StemmingAnalyzer
u"""
Felder:
link_txt - Verweis auf das Dokument (wird im Index gespeichert)
title_txt - Der Titel des Dokuments. Sehr wichtig, deshalb doppelter Boost.
body_txt - Der Inhalt des Dokuments. Wird nicht gespeichert um Platz zu sparen.
Kann aber gefahrlos gespeichert werden, wenn ohnehin nur Teaser im RSS stehen.
author_txt - Der Autor. Sehr hoher Boost-Faktor.
published_on - Das Datum an dem der Artikel publiziert bzw. das letzte mal geändert wurde
indexed_on - Der Zeitpunkt der Indexierung (wichtig um die Freshness zu bestimmen).
keywords - Die Schlagworte. Sie werden gespeichert um sie ggfs. innerhalb der SERP anzuzeigen.
Sehr hoher Boost-Faktor (setzt qualitative Verschlagwortung voraus).
"""
blogschema = Schema(link_txt=TEXT(stored=True),
title_txt=TEXT(stored=True, field_boost=2.0),
body_txt=TEXT(stored=False),
author_txt=TEXT(stored=True, field_boost=3.0),
published_on=DATETIME(),
indexed_on=DATETIME(),
keywords=KEYWORD(stored=True, commas=True, field_boost=3.0)) # Comma-Separierung um z. B. "Web Crawler" als Keyword-Term zu erlauben
Ich habe die Felder inline kommentiert. Das sollte euch eine Idee geben, wie das Schema angelegt ist und welche Möglichkeiten ihr später bei der Darstellung der Suchergebnisse habt, und welche nicht. Die Boost-Werte sind dabei ziemlich willkürlich gesetzt und eigentlich nur deshalb im Quellcode enthalten, um kein absolut triviales Schema zu implementieren.
Kommen wir zum writer.py Script, mit dem wir sowohl den RSS-Feed dieses Blogs abholen, als auch den Index befüllen. Das Script ist nicht besonders robust und überprüft weder die Datenqualität des gelieferten RSS-Feeds, noch sind try/except Konstrukte enthalten. Natürlich werden professionelle Entwickler die Writer-Komponente viel modularer, performanter, robuster und wundertoller bauen. Aber das passt dann nicht mehr in einen kurzen Blog-Artikel mit Starthilfe-Charakter
#!/usr/bin/python
# -*- coding: UTF-8 -*-
"""
Der Index Writer
"""
import os.path, datetime, time
import feedparser
from schema import blogschema
from whoosh.index import create_in
if not os.path.exists("index"):
os.mkdir("index")
ix = create_in("index", blogschema)
writer = ix.writer()
rss = feedparser.parse("http://www.suchkultur.de/blog/feed/")
article_count = len(rss['entries'])
iteration = 0
for article in rss.entries:
# Keyword-Terme zum Indexieren in einfachen String verwandeln
kw_terms = u" "
for item in rss.entries[iteration].tags:
kw_terms = kw_terms + " " + item.term
author = unicode(rss.entries[iteration].author)
title = unicode(rss.entries[iteration].title)
body = unicode(rss.entries[iteration].content)
link = unicode(rss.entries[iteration].link)
# Converting python time.struct_time to datetime object
# ('twas tricky, Danke an: http://stackoverflow.com/questions/1697815/how-do-you-convert-a-python-time-struct-time-object-into-a-datetime-object)
published_or_updated = datetime.datetime(*rss.entries[iteration].updated_parsed[:6])
print "\n### Document-Ingestion von WP-Document-ID %s" % (str(rss.entries[iteration].id))
writer.add_document(link_txt=link,
title_txt=title,
author_txt=author,
body_txt=body,
keywords=kw_terms,
published_on=published_or_updated, # dieses datetime objekt wird so nicht angenommen(?)
indexed_on=datetime.datetime.now(),
)
print "Autor: " + author
print "Link: " + link
print "Dokument " + rss.entries[iteration].title + u" hinzugefügt."
#print "Content: " + str(rss.entries[iteration].content) - too much noise
print u"Publiziert oder geändert am : " + str(published_or_updated) + " (Python datetime-Tupel)"
print "Tags: " + kw_terms
print "Indexiert: " + str(datetime.datetime.now())
iteration = iteration +1
writer.commit()
Ich stand übrigens vor dem Problem, dass ich zunächst gar nicht wußte wie ich den RSS-Feed geparst bekomme, ohne einen XML-Reader zu implementieren. Zum Glück hat der in der Python-Welt durch seine Bücher bekannte, Ex-IBMler und jetzt Google-Mitarbeiter Mark Pilgrim mit seiner Library feedparser mein Problem schon im Jahre 2005 gelöst. Das feedparser Modul macht das Parsing sehr einfach und bequem. iLike.
Da der Quellcode diesmal nicht inline kommentiert wurde, liefere ich euch die Anmerkungen diesmal stichpunktweise für die wichtigsten Stellen im Code:
- In Zeile 13-15 erstellen wir den Index in einem Unterverzeichnis namens “index” unterhalb unseres aktuellen Arbeitsverzeichnisses
- In Zeile 17 wird ein Writer-Objekt erstellt, dass auf den Index schreibend zugreift
- In Zeile 18 lasse ich mir von Mark Pilgrims feedparser-Modul die ganze Arbeit abnehmen und parse den RSS-Feed dieses Blogs
- Ab Zeile 23 schreiben wir programmatisch in den Index. Wichtig ist, dass in einem Whoosh-Index alles in Unicode gespeichert wird. Deshalb nutze ich regelmäßig unicode() Typecasts und Strings mit u”" Prefix.
- In Zeile 26-27 baue ich die einzelnen Keywords in einen einzigen String zusammen, um sie anschließend im Index-Feld “keywords” zu platzieren (vgl. schema.py)
- Von 29-35 lese ich das geparste RSS-File aus. Ich hatte zunächst Probleme mit dem time.struct_time, dass ich vom RSS-Parser zurück bekam. Dank der Code-Community stackoverflow.com konnte mein Problem aber schnell gelöst werden.
- Ab Zeile 39 schreibe ich mit der add_document() Methode in den Index, bis keine RSS-Elemente mehr vorhanden sind.
- Zeile 57: writer.commit() nicht vergessen!
Der Output dieses Programms sieht nun folgendermaßen aus:
### Document-Ingestion von WP-Document-ID http://www.suchkultur.de/blog/?p=108
Autor: stefan
Link: http://www.suchkultur.de/blog/suchmaschinen/solr/apache-solr-und-lucene-finden-zueinander/
Dokument Apache Solr und Lucene finden zueinander hinzugefügt.
Publiziert oder geändert am : 2010-08-29 20:40:29 (Python datetime-Tupel)
Tags: Lucene Solr
Indexiert: 2010-09-11 14:09:56.956753
### Document-Ingestion von WP-Document-ID http://www.suchkultur.de/blog/?p=98
Autor: stefan
Link: http://www.suchkultur.de/blog/social-web/i-robot-yelp-com-fordert-die-einhaltung-von-isaac-asimovs-robotergesetzen-ein/
Dokument I, Robot – Yelp.com fordert die Einhaltung von Isaac Asimov’s Robotergesetzen ein hinzugefügt.
Publiziert oder geändert am : 2010-08-26 19:45:55 (Python datetime-Tupel)
Tags: Crawler Social Web Suchmaschinen
Indexiert: 2010-09-11 14:09:56.962235
### Document-Ingestion von WP-Document-ID http://www.suchkultur.de/blog/?p=76
Autor: stefan
Link: http://www.suchkultur.de/blog/systemadministration/stressfreies-monitoring-mit-dem-watch-kommando/
Dokument Stressfreies Monitoring mit dem watch-Kommando hinzugefügt.
Publiziert oder geändert am : 2010-08-13 15:49:01 (Python datetime-Tupel)
Tags: Systemadministration
Indexiert: 2010-09-11 14:09:56.974080
Natürlich gehören diese Ausgaben eigentlich in ein sauber formatiertes Logfile.
Wenn nichts schief gelaufen ist, haben wir nun den Index auf unsere Festplatte geschrieben und können all die schönen Dinge damit tun, für die Suchmaschinen bekannt sind wie z. B. eine Blogsuche aufsetzen oder einen Feed-Aggregator schreiben oder ein Search-driven Portal aufbauen und vieles andere mehr. Im Rahmen dieses Artikels möchte ich mich darauf beschränken euch zu zeigen, wie man einen ganz simplen Client schreibt und die Informationen auf der Kommandozeile wieder aus dem Index auslesen kann. Der Weg zur Suchbox auf einer HTML-Seite oder einem kleinen HTTP-Server, der als Search-Server agiert, ist aber von hier aus auch nicht mehr weit.
Der Searcher:
#!/usr/bin/python
# -*- coding: UTF-8 -*-
"""
Der Index Searcher
Whoosh default Query-Language:
http://packages.python.org/Whoosh/querylang.html
Beispiel zur Anwendung:
./searcher.py 'author_txt:stefan AND title_txt:"Logfile-Monitoring mit GNU less"'
"""
import os.path, sys
from whoosh import index, reading
from whoosh.searching import Searcher
from whoosh.qparser import QueryParser, MultifieldParser
if os.path.exists("index"):
ix = index.open_dir("index" )
parser = MultifieldParser(['link_txt', 'title_txt', 'body_txt', 'author_txt', 'keywords'], schema=ix.schema)
myquery = parser.parse(unicode(sys.argv[1]))
searcher = ix.searcher()
results = searcher.search(myquery)
print u"\nErgebnis für Query %s\n" % (results.query)
for document in results:
for key, value in document.items():
print key + ": " + value
Im Grunde steht im Kommentar am Kopf des Scripts schon alles drin, was ihr wissen müsst um ein wenig mit unserem Index zu spielen. Whoosh implementiert eine einfache Query-Language, die man dank der modularen Architektur entweder erweitern oder gegen eine eigene Abfragesprache austauschen kann. Die Beispielanfrage auf den Index liefert zur Zeit folgendes Ergebnis:
./searcher.py 'author_txt:stefan AND title_txt:"Logfile-Monitoring mit GNU less"'
Ergebnis für Query (author_txt:stefan AND title_txt:"logfile monitoring mit gnu less")
keywords: Systemadministration
title_txt: Logfile-Monitoring mit GNU less oder: when less is more than tail -f
link_txt: http://www.suchkultur.de/blog/systemadministration/logfile-monitoring-mit-gnu-less-oder-when-less-is-more-than-tail-f/
author_txt: stefan
Wie man sieht funktioniert unser Index. Das ist nun der Punkt an dem ich auch schon zum Ende kommen möchte, damit ihr eurem eigenen Spieltrieb nachgehen könnt. Whoosh ist für den Python-Kenner innerhalb weniger Minuten installiert und die Index-Erstellung ist ein Klacks. Es gibt mittlerweile auch einige Applikationen, die Whoosh als Backend für die Suche nutzen. Die bekannteste dürfte Haystack sein, die dem Django Web Framework eine Index-basierte Suchmaschine bereitstellt. Unterstützt werden dabei neben Whoosh auch das Java-basierte Lucene und das in C++ implementierte Rennpferd Xapian.
Nun wünsche ich euch viel Spass beim Rapid Prototyping mit Whoosh!