"""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)