38 lines
1.8 KiB
Python
38 lines
1.8 KiB
Python
"""Full-text search with a narrow fallback for stop-word abbreviations."""
|
|
|
|
import re
|
|
|
|
from django.db import connections
|
|
from watson.backends import PostgresSearchBackend
|
|
from watson.models import SearchEntry
|
|
from watson.search import search as watson_search
|
|
|
|
|
|
class AbbreviationPostgresSearchBackend(PostgresSearchBackend):
|
|
def do_search(self, engine_slug, queryset, search_text):
|
|
# English stemming drops short names such as IT as stop words. Only
|
|
# empty, short single-token tsqueries need a literal title fallback.
|
|
# Normal queries retain Watson's GIN-backed full-text search unchanged.
|
|
if 2 <= len(search_text) <= 8 and search_text.isalnum():
|
|
with connections[queryset.db].cursor() as cursor:
|
|
cursor.execute(
|
|
"SELECT numnode(to_tsquery(%s::regconfig, %s)) = 0",
|
|
(self.search_config, self.escape_postgres_query(search_text)),
|
|
)
|
|
is_stop_word = cursor.fetchone()[0]
|
|
if is_stop_word:
|
|
# Match an entire token, not 'it' inside 'Security'. Do not scan
|
|
# descriptions or extracted document contents for stop words.
|
|
pattern = rf"(^|[^[:alnum:]_]){re.escape(search_text)}([^[:alnum:]_]|$)"
|
|
return queryset.filter(title__iregex=pattern)
|
|
return super().do_search(engine_slug, queryset, search_text)
|
|
|
|
|
|
def search_with_abbreviations(search_text):
|
|
backend_name = None
|
|
if connections[SearchEntry.objects.db].vendor == "postgresql":
|
|
backend_name = "search.backends.AbbreviationPostgresSearchBackend"
|
|
# Watson still applies its engine/model/live-queryset filters. Candidates
|
|
# also go through the API's normal object ACL, counting and pagination.
|
|
return watson_search(search_text, backend_name=backend_name)
|