From cc223daa170f8070b7b44c6e0ec2a2db3a9c85ab Mon Sep 17 00:00:00 2001 From: Matt Westcott Date: Mon, 17 Apr 2023 15:16:01 +0100 Subject: [PATCH] Fix autocompletion on sqlite FTS backend * Fix incorrect signatures for `SQLiteAutocompleteQueryCompiler` * Syntax for an autocomplete search should have the `*` outside of quotes - `"foo"*` rather than `"foo*"` * Query compiler was hardcoded to search on the 'title' and 'body' fields of the FTS table, rather than 'autocomplete' Full support for searching on specified fields is still lacking - the 'title' and 'body' fields are special cased, and other column names will return a SQL error. --- .../search/backends/database/sqlite/query.py | 12 ++++-------- .../search/backends/database/sqlite/sqlite.py | 19 +++++++++---------- wagtail/search/tests/test_sqlite_backend.py | 19 +------------------ 3 files changed, 14 insertions(+), 36 deletions(-) diff --git a/wagtail/search/backends/database/sqlite/query.py b/wagtail/search/backends/database/sqlite/query.py index fa9920b484e..c930b919c77 100644 --- a/wagtail/search/backends/database/sqlite/query.py +++ b/wagtail/search/backends/database/sqlite/query.py @@ -71,16 +71,12 @@ def __init__(self, value, output_field=None, *, prefix=False, weight=None): super().__init__(value, output_field=output_field) def as_sql(self, compiler, connection): - param = "%s" % self.value.replace("'", "''").replace("\\", "\\\\") + param = self.value.replace("'", "''").replace("\\", "\\\\") - template = '"%s"' - - label = "" if self.prefix: - label += "*" - - if label: - param = "{}{}".format(param, label) + template = '"%s"*' + else: + template = '"%s"' return template, [param] diff --git a/wagtail/search/backends/database/sqlite/sqlite.py b/wagtail/search/backends/database/sqlite/sqlite.py index f4ac9e09dc3..c130d8cdc94 100644 --- a/wagtail/search/backends/database/sqlite/sqlite.py +++ b/wagtail/search/backends/database/sqlite/sqlite.py @@ -316,6 +316,7 @@ class SQLiteSearchQueryCompiler(BaseSearchQueryCompiler): DEFAULT_OPERATOR = "AND" LAST_TERM_IS_PREFIX = False TARGET_SEARCH_FIELD_TYPE = SearchField + FTS_TABLE_FIELDS = ["title", "body"] def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -516,7 +517,7 @@ def search(self, config, start, stop, score_field=None): ) # We add the subsequent vectors to the combined vector. # Build the FTS match expression. - expr = MatchExpression(self.fields or ["title", "body"], search_query) + expr = MatchExpression(self.fields or self.FTS_TABLE_FIELDS, search_query) # Perform the FTS search. We'll get entries in the SQLiteFTSIndexEntry model. objs = ( SQLiteFTSIndexEntry.objects.filter(expr) @@ -589,6 +590,7 @@ def _connect_filters(self, filters, connector, negated): class SQLiteAutocompleteQueryCompiler(SQLiteSearchQueryCompiler): LAST_TERM_IS_PREFIX = True TARGET_SEARCH_FIELD_TYPE = AutocompleteField + FTS_TABLE_FIELDS = ["autocomplete"] def get_config(self, backend): return backend.autocomplete_config @@ -596,12 +598,9 @@ def get_config(self, backend): def get_search_fields_for_model(self): return self.queryset.model.get_autocomplete_search_fields() - def get_index_vectors(self, search_query): + def get_index_vectors(self): return [(F("index_entries__autocomplete"), 1.0)] - def get_fields_vectors(self, search_query): - raise NotImplementedError() - class SQLiteSearchResults(BaseSearchResults): def get_queryset(self, for_count=False): @@ -656,10 +655,7 @@ def facet(self, field_name): class SQLiteSearchBackend(BaseSearchBackend): query_compiler_class = SQLiteSearchQueryCompiler - - # FIXME: the implementation of SQLiteAutocompleteQueryCompiler is incomplete - - # leave this undefined so that we get a clean NotImplementedError from BaseSearchBackend - # autocomplete_query_compiler_class = SQLiteAutocompleteQueryCompiler + autocomplete_query_compiler_class = SQLiteAutocompleteQueryCompiler results_class = SQLiteSearchResults rebuilder_class = SQLiteSearchRebuilder @@ -668,7 +664,10 @@ class SQLiteSearchBackend(BaseSearchBackend): def __init__(self, params): super().__init__(params) self.index_name = params.get("INDEX", "default") - self.config = params.get("SEARCH_CONFIG") + + # SQLite backend currently has no config options + self.config = None + self.autocomplete_config = None if params.get("ATOMIC_REBUILD", False): self.rebuilder_class = self.atomic_rebuilder_class diff --git a/wagtail/search/tests/test_sqlite_backend.py b/wagtail/search/tests/test_sqlite_backend.py index cb91216eb59..b8e767ecedc 100644 --- a/wagtail/search/tests/test_sqlite_backend.py +++ b/wagtail/search/tests/test_sqlite_backend.py @@ -8,7 +8,6 @@ from wagtail.search.backends.database.sqlite.utils import fts5_available from wagtail.search.tests.test_backends import BackendTests -from wagtail.test.search import models @unittest.skipUnless( @@ -44,23 +43,7 @@ def test_annotate_score(self): def test_annotate_score_with_slice(self): return super().test_annotate_score_with_slice() - def test_autocomplete_raises_not_implemented_error(self): - with self.assertRaises(NotImplementedError): - self.backend.autocomplete("Py", models.Book) - - @skip("The SQLite backend doesn't support autocomplete.") - def test_autocomplete(self): - return super().test_autocomplete() - - @skip("The SQLite backend doesn't support autocomplete.") - def test_autocomplete_not_affected_by_stemming(self): - return super().test_autocomplete_not_affected_by_stemming() - - @skip("The SQLite backend doesn't support autocomplete.") - def test_autocomplete_uses_autocompletefield(self): - return super().test_autocomplete_uses_autocompletefield() - - @skip("The SQLite backend doesn't support autocomplete.") + @skip("The SQLite backend doesn't support searching on specified fields.") def test_autocomplete_with_fields_arg(self): return super().test_autocomplete_with_fields_arg()