Serveur d'indexation pour Django avec XapianDans mon post précédent, je parlais de l'indexation avec Xapian. Le fonctionnement du paquet a du être revu pour fonctionner convenablement avec un site Django (ou tout autre site web). En effet un site internet peut manipuler le code par le biais de plusieurs threads et processus, et Xapian ne peut avoir qu'un seul accès en écriture à sa base de données. Il est donc nécessaire de découpler l'indexation des requêtes de recherche.
Une des solutions est de maintenir sur le filesystem un fichier de lock pour s'assurer que la base n'est pas ouverte en écriture par deux threads au même moment, mais cette solution fonctionne mal en cas de crash de l'application. De plus, les données en cours d'indexation peuvent être perdues. La solution optimale est d'opter pour un pattern producers-consumer avec une pile persistente des données de travail, indépendante du site web, qui ne devient qu'un client parmi d'autres. Voici les modifications apportées au paquet pour mettre en place ce fonctionnement :
Cette architecture offre en outre un fonctionnement rapide : lorsque des documents sont ajoutés ou modifiés dans le site, leur indexation est fait de manière asynchrone. Le système des signals est utilisé pour déclencher une indexation à chaque modification d'un document basé sur un model. Ce mécanisme, basé sur le framework d'events de Django, est expliqué ici: http://www.mercurytide.com/whitepapers/django-signals/. Au final, pour utiliser le paquet il faut:
Voici le doctest complet pour bien comprendre le fonctionnement :
=======
indexer
=======
The indexer provides:
- client-side modules : API for client to ask for indexations and query the
Xapian database. When an indexation is asked, it is stored in a sql
database;
- server-side application: a standalone thread that indexes what has been
asked by reading the sql database.
Let's import the modules used by the client-side::
>>> import indexer
>>> import searcher
Let's reset the SQL DB first::
>>> indexer.reset()
Let's also reset the Xapian DB::
>>> from xapindexer import force_reset
>>> force_reset()
The Xapian DB should be empty now::
>>> searcher.corpus_size()
0
Indexation
==========
Each indexable content has a unique id, and a text to index::
>>> uid = '1'
>>> text = 'my taylor is not rich anymore'
Let's index it::
>>> indexer.index_document(uid, text)
Another one::
>>> indexer.index_document('2', 'pluto is a dog')
Let's start the worker that is in charge of asynchronous indexation::
>>> from xapindexer import start_server
>>> start_server()
Let's wait a bit so the worker has the time to read the SQL Database
and do the work::
>>> import time
>>> while indexer.is_working():
... time.sleep(0.2)
`is_working` looks in the SQL DB if there is some work left.
The Xapian DB has two documents now::
>>> searcher.corpus_size()
2
Searching
=========
Let's search now, with `searcher`. Operator is AND by default::
>>> res = searcher.search('rich')
>>> list(res)
['1']
>>> res = searcher.search('pluto')
>>> list(res)
['2']
>>> res = searcher.search('dog')
>>> list(res)
['2']
>>> res = searcher.search('rich dog')
>>> list(res)
[]
Or operator::
>>> res = searcher.search('rich dog', or_=True)
>>> res = list(res)
>>> res.sort()
>>> res
['1', '2']
We have an API to detect if a document is present::
>>> searcher.document_exists('2')
True
>>> searcher.document_exists('ttt')
False
And another one to retrieve indexed terms::
>>> list(searcher.document_terms('2'))
['dog', 'is', 'pluto']
Reindexation
============
The document can also be reindexed::
>>> indexer.index_document('2', 'pluto is a cat')
>>> indexer.work_in_process()
([2], [])
Let's wait a bit::
>>> while indexer.is_working():
... time.sleep(0.2)
Let's make sure the document has been reindexed::
>>> list(searcher.document_terms('2'))
['cat', 'is', 'pluto']
Then check the indexation has changed::
>>> res = searcher.search('rich dog', or_=True)
>>> list(res)
['1']
Or deleted::
>>> res = searcher.search('pluto')
>>> list(res)
['2']
>>> indexer.delete_document('2')
>>> while indexer.is_working():
... time.sleep(0.2)
>>> res = searcher.search('pluto')
>>> list(res)
[]
Sur la maquette que j'ai monté pour tester l'outil, j'ai été surpris de la rapidité de la solution. Le couple Xapian+Django permet de mettre en place un moteur de recherche très efficace. Lorsque le site en cours de conception sera terminé, je posterais un lien sur mon blog pour démo. Le code peut être récupéré dans xap ici: http://hg.programmation-python.org/repositories/public/ Prochains billets possibles sur ce sujet (à vous de choisir en commentant ce billet):
|
A propos
|