2011-08-20 17:08:00 +00:00
|
|
|
"""Search backends used by django-watson."""
|
|
|
|
|
|
2011-08-24 10:22:51 +00:00
|
|
|
import re
|
|
|
|
|
|
2011-08-20 17:08:00 +00:00
|
|
|
from django.conf import settings
|
|
|
|
|
from django.core.exceptions import ImproperlyConfigured
|
2011-08-21 16:58:41 +00:00
|
|
|
from django.contrib.contenttypes.models import ContentType
|
2011-08-21 14:52:24 +00:00
|
|
|
from django.db import models, connection
|
2011-08-21 16:58:41 +00:00
|
|
|
from django.db.models import Q
|
2011-08-20 17:27:57 +00:00
|
|
|
|
2011-08-21 17:14:43 +00:00
|
|
|
from watson.models import SearchEntry, has_int_pk
|
2011-08-20 17:08:00 +00:00
|
|
|
|
|
|
|
|
|
2011-08-23 17:14:38 +00:00
|
|
|
def regex_from_search_text(search_text):
|
|
|
|
|
"""Generates a regext from the given search text"""
|
|
|
|
|
words = search_text.split()
|
|
|
|
|
return u"|".join(
|
|
|
|
|
u"(\s{word}\s)|(^{word}\s)|(\s{word}$)|(^{word}$)".format(
|
2011-08-24 10:22:51 +00:00
|
|
|
word = re.escape(word),
|
2011-08-23 17:14:38 +00:00
|
|
|
)
|
|
|
|
|
for word in words
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
2011-08-20 17:08:00 +00:00
|
|
|
class SearchBackend(object):
|
|
|
|
|
|
|
|
|
|
"""Base class for all search backends."""
|
|
|
|
|
|
2011-08-20 17:27:57 +00:00
|
|
|
def do_install(self):
|
2011-08-20 17:08:00 +00:00
|
|
|
"""Generates the SQL needed to install django-watson."""
|
2011-08-23 16:12:35 +00:00
|
|
|
pass
|
2011-08-20 17:08:00 +00:00
|
|
|
|
2011-08-21 15:55:37 +00:00
|
|
|
def do_search(self, queryset, search_text):
|
2011-08-20 17:27:57 +00:00
|
|
|
"""Filters the given queryset according the the search logic for this backend."""
|
2011-08-23 17:14:38 +00:00
|
|
|
regex = regex_from_search_text(search_text)
|
2011-08-23 16:12:35 +00:00
|
|
|
return queryset.filter(
|
|
|
|
|
Q(title__iregex=regex) | Q(content__iregex=regex) | Q(content__iregex=regex),
|
|
|
|
|
)
|
2011-08-23 17:14:38 +00:00
|
|
|
|
|
|
|
|
def do_filter(self, queryset, search_text):
|
|
|
|
|
"""Filters the given queryset according the the search logic for this backend."""
|
|
|
|
|
regex = regex_from_search_text(search_text)
|
|
|
|
|
return queryset.filter(
|
|
|
|
|
Q(searchentry_set__title__iregex=regex) | Q(searchentry_set__content__iregex=regex) | Q(searchentry_set__content__iregex=regex),
|
|
|
|
|
)
|
2011-08-21 14:52:24 +00:00
|
|
|
|
2011-08-23 16:12:35 +00:00
|
|
|
def save_search_entry(self, search_entry, obj, adapter):
|
2011-08-21 14:52:24 +00:00
|
|
|
"""Saves the given search entry in the database."""
|
2011-08-23 16:12:35 +00:00
|
|
|
search_entry.save()
|
2011-08-20 17:50:35 +00:00
|
|
|
|
2011-08-20 17:08:00 +00:00
|
|
|
|
|
|
|
|
class PostgresSearchBackend(SearchBackend):
|
|
|
|
|
|
|
|
|
|
"""A search backend that uses native PostgreSQL full text indices."""
|
|
|
|
|
|
2011-08-20 17:27:57 +00:00
|
|
|
def do_install(self):
|
2011-08-20 17:08:00 +00:00
|
|
|
"""Generates the PostgreSQL specific SQL code to install django-watson."""
|
2011-08-21 16:38:04 +00:00
|
|
|
connection.cursor().execute("""
|
2011-08-23 16:19:03 +00:00
|
|
|
-- Ensure that plpgsql is installed.
|
|
|
|
|
CREATE OR REPLACE FUNCTION make_plpgsql() RETURNS VOID LANGUAGE SQL AS
|
|
|
|
|
$$
|
|
|
|
|
CREATE LANGUAGE plpgsql;
|
|
|
|
|
$$;
|
|
|
|
|
SELECT
|
|
|
|
|
CASE
|
|
|
|
|
WHEN EXISTS(
|
|
|
|
|
SELECT 1
|
|
|
|
|
FROM pg_catalog.pg_language
|
|
|
|
|
WHERE lanname='plpgsql'
|
|
|
|
|
)
|
|
|
|
|
THEN NULL
|
|
|
|
|
ELSE make_plpgsql() END;
|
|
|
|
|
DROP FUNCTION make_plpgsql();
|
2011-08-21 16:38:04 +00:00
|
|
|
|
2011-08-23 16:19:03 +00:00
|
|
|
-- Create the search index.
|
|
|
|
|
ALTER TABLE watson_searchentry ADD COLUMN search_tsv tsvector NOT NULL;
|
|
|
|
|
CREATE INDEX watson_searchentry_search_tsv ON watson_searchentry USING gin(search_tsv);
|
|
|
|
|
|
|
|
|
|
-- Create the trigger function.
|
|
|
|
|
CREATE FUNCTION watson_searchentry_trigger_handler() RETURNS trigger AS $$
|
|
|
|
|
begin
|
|
|
|
|
new.search_tsv :=
|
|
|
|
|
setweight(to_tsvector('pg_catalog.english', coalesce(new.title, '')), 'A') ||
|
|
|
|
|
setweight(to_tsvector('pg_catalog.english', coalesce(new.description, '')), 'C') ||
|
|
|
|
|
setweight(to_tsvector('pg_catalog.english', coalesce(new.content, '')), 'D');
|
|
|
|
|
return new;
|
|
|
|
|
end
|
|
|
|
|
$$ LANGUAGE plpgsql;
|
|
|
|
|
CREATE TRIGGER watson_searchitem_trigger BEFORE INSERT OR UPDATE
|
|
|
|
|
ON watson_searchentry FOR EACH ROW EXECUTE PROCEDURE watson_searchentry_trigger_handler();
|
2011-08-21 16:38:04 +00:00
|
|
|
""")
|
|
|
|
|
|
|
|
|
|
def do_search(self, queryset, search_text):
|
|
|
|
|
"""Performs the full text search."""
|
|
|
|
|
return queryset.extra(
|
|
|
|
|
select = {
|
2011-08-23 17:22:17 +00:00
|
|
|
"rank": "ts_rank_cd(search_tsv, plainto_tsquery(%s))",
|
2011-08-21 16:38:04 +00:00
|
|
|
},
|
|
|
|
|
select_params = (search_text,),
|
2011-08-23 17:22:17 +00:00
|
|
|
where = ("search_tsv @@ plainto_tsquery(%s)",),
|
|
|
|
|
params = (search_text,),
|
|
|
|
|
order_by = ("-rank",),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
def do_filter(self, queryset, search_text):
|
|
|
|
|
"""Performs the full text filter."""
|
|
|
|
|
model = queryset.model
|
|
|
|
|
if has_int_pk(model):
|
|
|
|
|
ref_name = "object_id_int"
|
|
|
|
|
else:
|
|
|
|
|
ref_name = "object_id"
|
|
|
|
|
return queryset.extra(
|
|
|
|
|
select = {
|
|
|
|
|
"rank": "ts_rank_cd(watson_searchentry.search_tsv, plainto_tsquery(%s))",
|
|
|
|
|
},
|
|
|
|
|
select_params = (search_text,),
|
|
|
|
|
tables = ("watson_searchentry",),
|
|
|
|
|
where = (
|
|
|
|
|
"watson_searchentry.search_tsv @@ plainto_tsquery(%s)",
|
|
|
|
|
"watson_searchentry.{ref_name} = {table_name}.{pk_name}".format(
|
|
|
|
|
ref_name = ref_name,
|
|
|
|
|
table_name = connection.ops.quote_name(model._meta.db_table),
|
|
|
|
|
pk_name = connection.ops.quote_name(model._meta.pk.name),
|
|
|
|
|
),
|
|
|
|
|
),
|
2011-08-21 16:38:04 +00:00
|
|
|
params = (search_text,),
|
|
|
|
|
order_by = ("-rank",),
|
|
|
|
|
)
|
2011-08-20 17:08:00 +00:00
|
|
|
|
|
|
|
|
|
|
|
|
|
class AdaptiveSearchBackend(SearchBackend):
|
|
|
|
|
|
|
|
|
|
"""
|
|
|
|
|
A search backend that guesses the correct search backend based on the
|
|
|
|
|
DATABASES["default"] settings.
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
def __new__(cls):
|
|
|
|
|
"""Guess the correct search backend and initialize it."""
|
|
|
|
|
database_engine = settings.DATABASES["default"]["ENGINE"]
|
|
|
|
|
if database_engine.endswith("postgresql_psycopg2") or database_engine.endswith("postgresql"):
|
|
|
|
|
return PostgresSearchBackend()
|
|
|
|
|
else:
|
2011-08-23 16:12:35 +00:00
|
|
|
return SearchBackend()
|