Xapian w Pythonie
14 July 2008
Comments
Xapian to jedna z dostępnych w pythonie wyszukiwarek pełnotekstowych. Jak wszystkie ma słabą dokumentację (praktycznie samo API) i nie jest przez to łatwa w użyciu. W odróżnieniu od Lucene nie wymaga GCJ (Java) i dostępna jest w wielu językach, również dla PHP czy Ruby. Obecnie Xapian nie obsługuje prawdopodobieństw polskich wyrazów na bazie ich rdzeni (steming), ale i tak jest w miarę użyteczny. Istnieją również pomocnicze projekty jak Xapwrap czy PyXapian, ale nimi nie będę się na razie zajmował. Jeżeli korzystasz z MySQL to lepiej skorzystać z możliwości pełnotekstowego wyszukiwania, jaką daje ta baza danych.Instalacja
Pakiet "xapian" i "xapian-bindings" powinny być w repozytoriach większości dystrybucji. Niektóre mogą zamiast "xapian-bindings" mieć "xapian-bindings-python" czy "xapian-python". Sam xapian nie posiada modułu pythona, dopiero "xapian-bindings" go zapewnia. Jeżeli nie możesz znaleźć pakietów dla swojej dystrybucji sprawdź stronę projektu.Wprowadzenie
Oto indekser bazujący na przykładzie rozprowadzanym z xapian-bindings:import xapian
import string
MAX_PROB_TERM_LENGTH = 64
def p_alnum(c):
return (c in string.ascii_letters or c in string.digits)
def p_notalnum(c):
return not p_alnum(c)
def p_notplusminus(c):
return c != '+' and c != '-'
def find_p(string, start, predicate):
while start<len(string) and not predicate(string[start]):
start += 1
return start
database = xapian.WritableDatabase('test/', xapian.DB_CREATE_OR_OPEN)
stemmer = xapian.Stem("english")
para = '''this is a testing'''
doc = xapian.Document()
doc.set_data(para)
pos = 0
i = 0
while i < len(para):
i = find_p(para, i, p_alnum)
j = find_p(para, i, p_notalnum)
k = find_p(para, j, p_notplusminus)
if k == len(para) or not p_alnum(para[k]):
j = k
if (j - i) <= MAX_PROB_TERM_LENGTH and j > i:
term = stemmer.stem_word(string.lower(para[i:j]))
doc.add_posting(term, pos)
pos += 1
i = j
database.add_document(doc)
para = '''this is a test'''
I ponownie wykonaj skrypt. Teraz wyszukiwarka:
import sys
import xapian
try:
database = xapian.Database('test/')
enquire = xapian.Enquire(database)
stemmer = xapian.Stem("english")
terms = []
for term in sys.argv[1:]:
terms.append(stemmer.stem_word(term.lower()))
query = xapian.Query(xapian.Query.OP_OR, terms)
print "Performing query `%s'" % query.get_description()
enquire.set_query(query)
matches = enquire.get_mset(0, 10)
print "%i results found" % matches.get_matches_estimated()
for match in matches:
print "ID %i %i%% [%s]" % (match[xapian.MSET_DID], match[xapian.MSET_PERCENT], match[xapian.MSET_DOCUMENT].get_data())
except Exception, e:
print >> sys.stderr, "Exception: %s" % str(e)
sys.exit(1)
python szukaj.py test
W wynikach pojawią się obie frazy, ta z "testing" będzie miała mniejsze prawdopodobieństwo. Po zindeksowaniu kilku tekstów o podobnej tematyce zdolności wyszukiwania pełnotekstowego będą jeszcze bardziej wyraźne. W powyższych skryptach pojawia się:
stemmer = xapian.Stem("english")
Co określa język tekstu - uwzględnia to indekser umożliwiając "logiczne" pełnotekstowe wyszukiwanie. Wspierane są: none, danish (da), dutch (nl), english (en), finnish (fi), french (fr), german (de), italian (it), norwegian (no), portuguese (pt), russian (ru), spanish (es), swedish (sv).Xapian i indeksowanie danych z bazy danych
W przypadku indeksowania danych z bazy danych wpis w indeksie musi zawierać numer ID wpisu oraz zazwyczaj dodatkowe dane takie jak tytuł i opis wpisu, które wykorzystamy przy wyświetlaniu wyników. Xapian umożliwia dodawanie pól do indeksu. Oto modyfikacja indeksera:import xapian
import string
MAX_PROB_TERM_LENGTH = 64
def p_alnum(c):
return (c in string.ascii_letters or c in string.digits)
def p_notalnum(c):
return not p_alnum(c)
def p_notplusminus(c):
return c != '+' and c != '-'
def find_p(string, start, predicate):
while start<len(string) and not predicate(string[start]):
start += 1
return start
NEWS_ID = 0
NEWS_TITLE = 1
NEWS_DESC = 2
database = xapian.WritableDatabase('test/', xapian.DB_CREATE_OR_OPEN)
stemmer = xapian.Stem("english")
para = '''this is a testing'''
doc = xapian.Document()
doc.set_data(para)
pos = 0
i = 0
while i < len(para):
i = find_p(para, i, p_alnum)
j = find_p(para, i, p_notalnum)
k = find_p(para, j, p_notplusminus)
if k == len(para) or not p_alnum(para[k]):
j = k
if (j - i) <= MAX_PROB_TERM_LENGTH and j > i:
term = stemmer.stem_word(string.lower(para[i:j]))
doc.add_posting(term, pos)
pos += 1
i = j
doc.add_value(NEWS_ID, str(323))
doc.add_value(NEWS_TITLE, 'Tytul newsa')
doc.add_value(NEWS_DESC, 'bla bla bla')
database.add_document(doc)
import sys
import xapian
NEWS_ID = 0
NEWS_TITLE = 1
NEWS_DESC = 2
try:
database = xapian.Database('test/')
enquire = xapian.Enquire(database)
stemmer = xapian.Stem("english")
terms = []
for term in sys.argv[1:]:
terms.append(stemmer.stem_word(term.lower()))
query = xapian.Query(xapian.Query.OP_OR, terms)
print "Performing query `%s'" % query.get_description()
enquire.set_query(query)
matches = enquire.get_mset(0, 10)
print "%i results found" % matches.get_matches_estimated()
for match in matches:
print "ID %i %i%% [%s]" % (match[xapian.MSET_DID], match[xapian.MSET_PERCENT], match[xapian.MSET_DOCUMENT].get_data())
print match[xapian.MSET_DOCUMENT].get_value(NEWS_TITLE)
except Exception, e:
print >> sys.stderr, "Exception: %s" % str(e)
sys.exit(1)
Przykładowo dla Django, aplikacji newsów o takim modelu:
class News(models.Model):
title = models.CharField(maxlength=255, verbose_name=_('Title'))
slug = models.SlugField(maxlength=255, unique=True, prepopulate_from=("title", ), verbose_name=_('Slug'))
text = models.TextField(verbose_name=_('Text'))
text_more = models.TextField(verbose_name=_(' More Text'), blank=True)
is_more = models.BooleanField(blank=True, default=False, verbose_name=_('Is More Text'))
is_external = models.BooleanField(blank=True, default=False, verbose_name=_('Is External News'))
category = models.ForeignKey(Category)
date = models.DateField(auto_now = True)
jakilinux_field = models.CharField(maxlength=255, verbose_name='Jakilinux', blank=True)
wykop_field = models.CharField(maxlength=255, verbose_name='Wykop', blank=True)
infoneo_field = models.CharField(maxlength=255, verbose_name='Infoneo', blank=True)
digg_field = models.CharField(maxlength=255, verbose_name='Digg', blank=True)
reddit_field = models.CharField(maxlength=255, verbose_name='Reddit', blank=True)
class Meta:
verbose_name = _('News')
verbose_name_plural = _('News')
db_table = 'rk_news' + str(settings.SITE_ID)
class Admin:
list_display = ('title', 'date')
list_filter = ['date']
search_fields = ['title', 'text']
date_hierarchy = 'date'
def get_absolute_url(self):
return '/news/more/' + str(self.slug) + '/'
def __str__(self):
return self.title
from os import environ
environ['DJANGO_SETTINGS_MODULE'] = 'settings'
from settings import *
from news.models import *
import xapian
import string
MAX_PROB_TERM_LENGTH = 64
def p_alnum(c):
return (c in string.ascii_letters or c in string.digits)
def p_notalnum(c):
return not p_alnum(c)
def p_notplusminus(c):
return c != '+' and c != '-'
def find_p(string, start, predicate):
while start<len(string) and not predicate(string[start]):
start += 1
return start
news = News.objects.all()
NEWS_ID = 0
NEWS_TITLE = 1
NEWS_DESC = 2
database = xapian.WritableDatabase('test/', xapian.DB_CREATE_OR_OPEN)
stemmer = xapian.Stem("english")
for new in news:
para = new.text + str(new.text_more)
doc = xapian.Document()
doc.set_data(para)
pos = 0
i = 0
while i < len(para):
i = find_p(para, i, p_alnum)
j = find_p(para, i, p_notalnum)
k = find_p(para, j, p_notplusminus)
if k == len(para) or not p_alnum(para[k]):
j = k
if (j - i) <= MAX_PROB_TERM_LENGTH and j > i:
term = stemmer.stem_word(string.lower(para[i:j]))
doc.add_posting(term, pos)
pos += 1
i = j
doc.add_value(NEWS_ID, str(new.id))
doc.add_value(NEWS_TITLE, new.title)
doc.add_value(NEWS_DESC, new.text)
database.add_document(doc)
[piotr@localhost biblioteka]$ python simplesearch.py django Performing query `Xapian::Query(django)' 8 results found How to Beat Rails - 100% More crazy changes for Django 1.0 ? - 98% Big Django project ;) - 87% Django 0.96 released - 86% polib - gettext translation manager - 71% Diamanda 2006.12 Stable Released - 65% More on Django 1.0 changes - 65% Djangoish Gettext Translator - 57%
Pumping Up Your Applications with Xapian Full-Text Search - Bardziej zaawansowany przykład wykorzystujący XML-RPC i Twisted
RkBlog
Comment article