Pełnotekstowe wyszukiwanie w SQLite

Od SQLite w wersji 3.5.9 dostępny jest moduł, mechanizm pełnotekstowego wyszukiwania FTS3 (starsze wersje mogą mieć tylko FTS1, FTS2). Dzięki temu modułowi bardzo łatwo i szybko możemy dodać opcję pełnotekstowego wyszukiwania. W przypadku Pythona to pod Pythonem 2.5 pysqlite nie obsługuje FTS3 (może rekompilacja z -DSQLITE_ENABLE_FTS3=1 by temu zaradziła), lecz Python 2.6 już powinien obsługiwać FTS3 bez problemów (przy problemach przekompiluj sqlite z podaną wcześniej flagą).

Tworzenie wirtualnej tabeli

Oto przykładowa wirtualna tabela:

CREATE VIRTUAL TABLE my_search using FTS3(slug, body);
Gdzie FTS3(kolumny...) określa strukturę wirtualnej tabeli (w rzeczywistości będzie ich kilka). Jeżeli SQLite nie obsługuje FTS3 to nie pozwoli stworzyć tabeli i wyświetli stosowny komunikat. Wszystkie kolumny w FTS3 są polami typu Text. W tym przykładzie "slug" identyfikuje wiersz, a "body" zawiera tekst względem którego będę przeszukiwał pełnotekstowo. Jeżeli chcemy oferować wyszukiwanie po jednym z wybranych pól to odpowiednio rozbudowujemy strukturę tabeli, czy też tworzymy kilka tabel dla kilku różnych źródeł danych.

Obsługa FTS1 i FTS2 wygląda tak samo jak FTS3. Więcej dowiemy się na sqlite.org.

Dodawanie danych do tabeli wirtualnej

Można zacząć od zaimportowania danych z istniejącej tabeli, np. taki skrypt odpalony z katalogu projektu Django zaimportuje dane:

# -*- coding: utf-8 -*-
import sys
import urllib2
from os import environ

environ['DJANGO_SETTINGS_MODULE'] = 'settings'

from settings import *
from django.contrib.sessions.models import *
from django.db import connection, transaction

from MYAPP.models import *


cursor = connection.cursor()

j = MY_SOME_MODEL.objects.all()
iterr = 1
for i in j:
	print iterr
	txt = i.some_txt + ' ' + i.more_txt + ' ' + i.city_of_something
	# txt should be stripped from HTML, stop words etc. to get smaller size of the database
	cursor.execute("INSERT INTO my_search (slug, body) VALUES (%s, %s)", (i.slug, txt))
	iterr += 1

transaction.commit_unless_managed()

Indeksowanie nowych wpisów można w Django zrealizować np. za pomocą sygnałów. Do np. models.py danej aplikacji można dodać:

from django.db.models import signals

#....

def update_index(sender, instance, created, **kwargs):
		cursor = connection.cursor()
		
		txt = instance.some_txt + ' ' + instance.more_txt + ' ' + instance.city_of_something
		# txt should be stripped from HTML, stop words etc. to get smaller size of the database
		txt = clean_to_search(txt)
		if created:
			# add if object is created, not updated
			cursor.execute("INSERT INTO my_search (slug, body) VALUES (%s, %s)", (instance.slug, txt))
			transaction.commit_unless_managed()
signals.post_save.connect(update_index, sender=MY_SOME_MODEL)

Wyszukiwanie pełnotekstowe

Zapytanie ma postać typu:

SELECT slug FROM my_search WHERE body MATCH 'wartość';
W Django można wykonać zapytanie tego typu tak:
cursor = connection.cursor()
cursor.execute("SELECT slug FROM offers_search WHERE body MATCH %s", (query,))
results = cursor.fetchall()
Mając slugi, czy też IDki identyfikujące rekordy można wyświetlić wyniki pobierając pasujące rekordy z macierzystej tabeli (można też w wirtualnej tabeli przetrzymywać wszystkie informacje potrzebne do wyświetlenia wyników wyszukiwania).

Testując pełnotekstowe wyszukiwanie w SQLite na megiteam.pl, gdzie stosowany jest Nginx+FastCGI nie stwierdziłem jak na razie żadnych problemów, nadmiernego zużycia RAMu itp. przez takie operacje. Jako że nie trzeba dodatkowych aplikacji (whoosh, sphinx itd.) jest to rozwiązanie prostsze i łatwiejsze w implementacji.

RkBlog

Django, 21 August 2009

Comment article
Comment article RkBlog main page Search RSS Contact