wagtail 7.1.2__py3-none-any.whl → 7.2rc1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- wagtail/__init__.py +1 -1
- wagtail/actions/copy_page.py +1 -1
- wagtail/actions/create_alias.py +1 -1
- wagtail/actions/delete_page.py +1 -1
- wagtail/actions/publish_page_revision.py +1 -1
- wagtail/actions/publish_revision.py +1 -1
- wagtail/actions/revert_to_page_revision.py +1 -1
- wagtail/actions/unpublish.py +1 -1
- wagtail/actions/unpublish_page.py +1 -1
- wagtail/admin/auth.py +3 -1
- wagtail/admin/checks.py +2 -2
- wagtail/admin/filters.py +28 -1
- wagtail/admin/forms/collections.py +1 -1
- wagtail/admin/forms/comments.py +1 -1
- wagtail/admin/forms/models.py +1 -1
- wagtail/admin/forms/pages.py +1 -1
- wagtail/admin/forms/tags.py +1 -1
- wagtail/admin/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/cs/LC_MESSAGES/django.po +25 -1
- wagtail/admin/locale/en/LC_MESSAGES/django.po +278 -192
- wagtail/admin/locale/en/LC_MESSAGES/djangojs.po +29 -15
- wagtail/admin/locale/it/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/it/LC_MESSAGES/django.po +3 -2
- wagtail/admin/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/django.po +57 -3
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.mo +0 -0
- wagtail/admin/locale/nl/LC_MESSAGES/djangojs.po +8 -2
- wagtail/admin/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/ru/LC_MESSAGES/django.po +58 -1
- wagtail/admin/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/admin/locale/tr/LC_MESSAGES/django.po +3 -2
- wagtail/admin/static/wagtailadmin/css/core.css +1 -1
- wagtail/admin/static/wagtailadmin/js/bulk-actions.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/chooser-widget.js +1 -1
- wagtail/admin/static/wagtailadmin/js/comments.js +1 -1
- wagtail/admin/static/wagtailadmin/js/core.js +1 -1
- wagtail/admin/static/wagtailadmin/js/core.js.LICENSE.txt +2 -2
- wagtail/admin/static/wagtailadmin/js/date-time-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/draftail.js +1 -1
- wagtail/admin/static/wagtailadmin/js/filtered-select.js +1 -1
- wagtail/admin/static/wagtailadmin/js/icons.js +1 -1
- wagtail/admin/static/wagtailadmin/js/modal-workflow.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser-telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/page-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/privacy-switch.js +1 -1
- wagtail/admin/static/wagtailadmin/js/sidebar.js +1 -1
- wagtail/admin/static/wagtailadmin/js/task-chooser-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/task-chooser.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/blocks.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/telepath.js +1 -1
- wagtail/admin/static/wagtailadmin/js/telepath/widgets.js +1 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js +1 -1
- wagtail/admin/static/wagtailadmin/js/userbar.js.LICENSE.txt +2 -2
- wagtail/admin/static/wagtailadmin/js/vendor/bootstrap-modal.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/bootstrap-transition.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery-3.6.0.min.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery-ui-1.13.2.min.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.datetimepicker.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.fileupload-process.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.fileupload.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/jquery.iframe-transport.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor/tag-it.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js +1 -1
- wagtail/admin/static/wagtailadmin/js/vendor.js.LICENSE.txt +1 -1
- wagtail/admin/static/wagtailadmin/js/wagtailadmin.js +1 -1
- wagtail/admin/static/wagtailadmin/js/workflow-action.js +1 -1
- wagtail/admin/templates/wagtailadmin/account/account.html +2 -0
- wagtail/admin/templates/wagtailadmin/base.html +14 -0
- wagtail/admin/templates/wagtailadmin/generic/chooser/chooser.html +2 -1
- wagtail/admin/templates/wagtailadmin/generic/chooser/creation_form.html +2 -1
- wagtail/admin/templates/wagtailadmin/panels/multi_field_panel_child.html +1 -1
- wagtail/admin/templates/wagtailadmin/panels/object_list.html +1 -1
- wagtail/admin/templates/wagtailadmin/panels/tabbed_interface.html +3 -2
- wagtail/admin/templates/wagtailadmin/shared/formatted_field.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/forms/single_checkbox.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/keyboard_shortcuts_dialog.html +19 -0
- wagtail/admin/templates/wagtailadmin/shared/panel.html +1 -1
- wagtail/admin/templates/wagtailadmin/shared/set_privacy.html +15 -0
- wagtail/admin/templates/wagtailadmin/shared/side_panels/checks.html +28 -1
- wagtail/admin/templates/wagtailadmin/shared/workflow_history/detail.html +2 -2
- wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_header.html → tables/ordering_header.html} +2 -2
- wagtail/admin/templates/wagtailadmin/tables/title_cell.html +1 -1
- wagtail/admin/templates/wagtailadmin/widgets/{daterange_input.html → range_input.html} +1 -1
- wagtail/admin/templates/wagtailadmin/workflows/task_chooser/chooser.html +4 -2
- wagtail/admin/templatetags/wagtailadmin_tags.py +41 -22
- wagtail/admin/tests/api/test_pages.py +7 -7
- wagtail/admin/tests/api/test_renderer_classes.py +16 -0
- wagtail/admin/tests/pages/test_create_page.py +34 -2
- wagtail/admin/tests/pages/test_edit_page.py +128 -14
- wagtail/admin/tests/pages/test_explorer_view.py +34 -7
- wagtail/admin/tests/pages/test_reorder_page.py +11 -0
- wagtail/admin/tests/test_collections_views.py +12 -0
- wagtail/admin/tests/test_edit_handlers.py +3 -3
- wagtail/admin/tests/test_filters.py +2 -2
- wagtail/admin/tests/test_keyboard_shortcuts.py +52 -2
- wagtail/admin/tests/test_menu.py +0 -2
- wagtail/admin/tests/test_privacy.py +16 -16
- wagtail/admin/tests/test_templatetags.py +137 -0
- wagtail/admin/tests/test_workflows.py +34 -0
- wagtail/admin/tests/viewsets/test_model_viewset.py +322 -0
- wagtail/admin/ui/tables/orderable.py +73 -0
- wagtail/admin/ui/tables/pages.py +3 -13
- wagtail/admin/views/collection_privacy.py +6 -2
- wagtail/admin/views/generic/__init__.py +1 -0
- wagtail/admin/views/generic/mixins.py +20 -2
- wagtail/admin/views/generic/models.py +67 -1
- wagtail/admin/views/generic/ordering.py +79 -0
- wagtail/admin/views/home.py +3 -3
- wagtail/admin/views/page_privacy.py +5 -2
- wagtail/admin/views/pages/create.py +1 -1
- wagtail/admin/views/pages/edit.py +2 -2
- wagtail/admin/views/pages/listing.py +7 -42
- wagtail/admin/views/pages/move.py +1 -1
- wagtail/admin/views/pages/ordering.py +1 -1
- wagtail/admin/viewsets/base.py +1 -1
- wagtail/admin/viewsets/model.py +49 -1
- wagtail/admin/wagtail_hooks.py +2 -1
- wagtail/admin/widgets/slug.py +10 -10
- wagtail/api/v2/serializers.py +1 -1
- wagtail/api/v2/tests/test_renderer_classes.py +32 -0
- wagtail/apps.py +2 -0
- wagtail/bin/wagtail.py +1 -1
- wagtail/contrib/forms/locale/en/LC_MESSAGES/django.po +14 -14
- wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/nl/LC_MESSAGES/django.po +19 -2
- wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/forms/locale/ru/LC_MESSAGES/django.po +18 -1
- wagtail/contrib/frontend_cache/tests.py +4 -2
- wagtail/contrib/redirects/locale/en/LC_MESSAGES/django.po +4 -4
- wagtail/contrib/redirects/tests/test_tmp_storages.py +20 -0
- wagtail/contrib/redirects/tmp_storages.py +1 -1
- wagtail/contrib/redirects/views.py +3 -3
- wagtail/contrib/search_promotions/locale/en/LC_MESSAGES/django.po +3 -3
- wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/search_promotions/locale/tr/LC_MESSAGES/django.po +43 -3
- wagtail/contrib/search_promotions/static/wagtailsearchpromotions/js/query-chooser-modal.js +1 -1
- wagtail/contrib/search_promotions/views/settings.py +2 -2
- wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/cs/LC_MESSAGES/django.po +6 -1
- wagtail/contrib/settings/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/nl/LC_MESSAGES/django.po +6 -2
- wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/contrib/settings/locale/ru/LC_MESSAGES/django.po +6 -1
- wagtail/contrib/settings/tests/site_specific/test_admin.py +40 -6
- wagtail/contrib/simple_translation/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/styleguide/templates/wagtailstyleguide/base.html +5 -5
- wagtail/contrib/table_block/blocks.py +1 -0
- wagtail/contrib/table_block/locale/en/LC_MESSAGES/django.po +5 -1
- wagtail/contrib/table_block/static/table_block/js/table.js +1 -1
- wagtail/contrib/typed_table_block/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/contrib/typed_table_block/static/typed_table_block/js/typed_table_block.js +1 -1
- wagtail/coreutils.py +5 -5
- wagtail/documents/forms.py +18 -1
- wagtail/documents/locale/en/LC_MESSAGES/django.po +10 -10
- wagtail/documents/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/nl/LC_MESSAGES/django.po +9 -0
- wagtail/documents/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/documents/locale/ru/LC_MESSAGES/django.po +9 -0
- wagtail/documents/models.py +1 -1
- wagtail/documents/static/wagtaildocs/js/add-multiple.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser-modal.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser-telepath.js +1 -1
- wagtail/documents/static/wagtaildocs/js/document-chooser.js +1 -1
- wagtail/documents/templates/wagtaildocs/documents/add.html +0 -34
- wagtail/documents/tests/test_admin_views.py +132 -26
- wagtail/documents/tests/test_collection_privacy.py +18 -4
- wagtail/documents/tests/test_form_overrides.py +1 -1
- wagtail/documents/tests/test_search.py +21 -8
- wagtail/documents/views/documents.py +1 -1
- wagtail/embeds/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/embeds/static/wagtailembeds/js/embed-chooser-modal.js +1 -1
- wagtail/images/forms.py +16 -1
- wagtail/images/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/cs/LC_MESSAGES/django.po +12 -1
- wagtail/images/locale/en/LC_MESSAGES/django.po +57 -46
- wagtail/images/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/nl/LC_MESSAGES/django.po +37 -14
- wagtail/images/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/images/locale/ru/LC_MESSAGES/django.po +20 -1
- wagtail/images/models.py +1 -1
- wagtail/images/static/wagtailimages/js/add-multiple.js +1 -1
- wagtail/images/static/wagtailimages/js/focal-point-chooser.js +1 -1
- wagtail/images/static/wagtailimages/js/image-block.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser-modal.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser-telepath.js +1 -1
- wagtail/images/static/wagtailimages/js/image-chooser.js +1 -1
- wagtail/images/static/wagtailimages/js/image-url-generator.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/jquery.Jcrop.min.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-image.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/jquery.fileupload-validate.js +1 -1
- wagtail/images/static/wagtailimages/js/vendor/load-image.min.js +1 -1
- wagtail/images/templates/wagtailimages/chooser/chooser.html +22 -13
- wagtail/images/templates/wagtailimages/chooser/image_preview_column_cell.html +10 -0
- wagtail/images/templates/wagtailimages/chooser/results.html +24 -20
- wagtail/images/templates/wagtailimages/chooser/title_column_cell.html +15 -0
- wagtail/images/templates/wagtailimages/images/add.html +0 -34
- wagtail/images/templates/wagtailimages/images/index.html +3 -3
- wagtail/images/templates/wagtailimages/images/index_results.html +1 -1
- wagtail/images/templates/wagtailimages/images/layout_toggle_button.html +8 -7
- wagtail/images/templatetags/wagtailimages_tags.py +2 -2
- wagtail/images/tests/test_admin_views.py +87 -0
- wagtail/images/tests/test_form_overrides.py +1 -1
- wagtail/images/tests/test_models.py +48 -9
- wagtail/images/views/chooser.py +66 -2
- wagtail/locale/en/LC_MESSAGES/django.po +55 -55
- wagtail/locale/is_IS/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/is_IS/LC_MESSAGES/django.po +3 -3
- wagtail/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/nl/LC_MESSAGES/django.po +11 -2
- wagtail/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/locale/ru/LC_MESSAGES/django.po +11 -1
- wagtail/locales/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/locales/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/nl/LC_MESSAGES/django.po +12 -1
- wagtail/locales/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/locales/locale/ru/LC_MESSAGES/django.po +10 -1
- wagtail/locales/views.py +2 -2
- wagtail/models/orderable.py +10 -0
- wagtail/models/pages.py +9 -11
- wagtail/models/sites.py +1 -1
- wagtail/models/workflows.py +8 -5
- wagtail/project_template/home/tests.py +6 -7
- wagtail/project_template/project_name/settings/base.py +9 -9
- wagtail/project_template/requirements.txt +1 -1
- wagtail/query.py +7 -2
- wagtail/rich_text/rewriters.py +1 -1
- wagtail/search/apps.py +4 -49
- wagtail/search/backends/__init__.py +1 -113
- wagtail/search/backends/base.py +1 -547
- wagtail/search/backends/database/__init__.py +1 -50
- wagtail/search/backends/database/fallback.py +1 -253
- wagtail/search/backends/database/mysql/mysql.py +1 -700
- wagtail/search/backends/database/mysql/query.py +1 -258
- wagtail/search/backends/database/postgres/postgres.py +1 -749
- wagtail/search/backends/database/postgres/query.py +1 -83
- wagtail/search/backends/database/postgres/weights.py +1 -63
- wagtail/search/backends/database/sqlite/query.py +1 -294
- wagtail/search/backends/database/sqlite/sqlite.py +1 -719
- wagtail/search/backends/database/sqlite/utils.py +1 -35
- wagtail/search/backends/deprecation.py +45 -0
- wagtail/search/backends/elasticsearch7.py +18 -1260
- wagtail/search/backends/elasticsearch8.py +21 -96
- wagtail/search/backends/elasticsearch9.py +35 -0
- wagtail/search/backends/opensearch2.py +35 -0
- wagtail/search/backends/opensearch3.py +35 -0
- wagtail/search/index.py +1 -358
- wagtail/search/locale/en/LC_MESSAGES/django.po +2 -10
- wagtail/search/management/commands/update_index.py +1 -205
- wagtail/search/management/commands/wagtail_update_index.py +1 -4
- wagtail/search/models.py +32 -158
- wagtail/search/query.py +1 -114
- wagtail/search/queryset.py +1 -43
- wagtail/search/signal_handlers.py +1 -24
- wagtail/search/tasks.py +1 -10
- wagtail/search/tests/test_elasticsearch.py +22 -0
- wagtail/search/utils.py +1 -206
- wagtail/sites/locale/en/LC_MESSAGES/django.po +1 -1
- wagtail/snippets/locale/en/LC_MESSAGES/django.po +3 -3
- wagtail/snippets/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/ru/LC_MESSAGES/django.po +8 -1
- wagtail/snippets/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/snippets/locale/tr/LC_MESSAGES/django.po +8 -1
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser-telepath.js +1 -1
- wagtail/snippets/static/wagtailsnippets/js/snippet-chooser.js +1 -1
- wagtail/snippets/tests/test_reordering.py +319 -0
- wagtail/snippets/tests/test_snippets.py +65 -12
- wagtail/snippets/views/snippets.py +16 -0
- wagtail/test/numberformat.py +30 -0
- wagtail/test/settings.py +35 -12
- wagtail/test/testapp/fields.py +12 -0
- wagtail/test/testapp/migrations/0056_commentablejsonpage.py +50 -0
- wagtail/test/testapp/migrations/0057_featurecompletetoy_sort_order.py +23 -0
- wagtail/test/testapp/migrations/0058_customlocktask.py +31 -0
- wagtail/test/testapp/models.py +27 -0
- wagtail/test/testapp/views.py +3 -1
- wagtail/test/utils/page_tests.py +17 -17
- wagtail/test/utils/template_tests.py +4 -6
- wagtail/test/utils/wagtail_tests.py +1 -2
- wagtail/tests/test_page_model.py +15 -0
- wagtail/{search/tests → tests}/test_page_search.py +29 -2
- wagtail/tests/test_search_fields.py +69 -0
- wagtail/tests/test_tests.py +62 -6
- wagtail/tests/test_workflow.py +25 -1
- wagtail/users/locale/cs/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/cs/LC_MESSAGES/django.po +3 -0
- wagtail/users/locale/en/LC_MESSAGES/django.po +2 -2
- wagtail/users/locale/nl/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/nl/LC_MESSAGES/django.po +6 -3
- wagtail/users/locale/ru/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/ru/LC_MESSAGES/django.po +5 -1
- wagtail/users/locale/tr/LC_MESSAGES/django.mo +0 -0
- wagtail/users/locale/tr/LC_MESSAGES/django.po +78 -4
- wagtail/users/templates/wagtailusers/users/create.html +2 -0
- wagtail/users/templates/wagtailusers/users/edit.html +2 -0
- wagtail/users/tests/test_admin_views.py +4 -0
- wagtail/users/views/users.py +1 -1
- {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/METADATA +7 -6
- {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/RECORD +309 -315
- wagtail/admin/templates/wagtailadmin/collection_privacy/set_privacy.html +0 -13
- wagtail/admin/templates/wagtailadmin/page_privacy/set_privacy.html +0 -13
- wagtail/search/tests/__init__.py +0 -0
- wagtail/search/tests/elasticsearch_common_tests.py +0 -251
- wagtail/search/tests/test_backends.py +0 -1215
- wagtail/search/tests/test_db_backend.py +0 -62
- wagtail/search/tests/test_elasticsearch7_backend.py +0 -1452
- wagtail/search/tests/test_elasticsearch8_backend.py +0 -15
- wagtail/search/tests/test_index_functions.py +0 -256
- wagtail/search/tests/test_indexed_class.py +0 -157
- wagtail/search/tests/test_mysql_backend.py +0 -192
- wagtail/search/tests/test_postgres_backend.py +0 -210
- wagtail/search/tests/test_queries.py +0 -332
- wagtail/search/tests/test_related_fields.py +0 -102
- wagtail/search/tests/test_sqlite_backend.py +0 -52
- wagtail/test/search/__init__.py +0 -0
- wagtail/test/search/apps.py +0 -9
- wagtail/test/search/fixtures/search.json +0 -545
- wagtail/test/search/migrations/0001_initial.py +0 -146
- wagtail/test/search/migrations/0002_bookunindexed.py +0 -43
- wagtail/test/search/migrations/0003_book_summary.py +0 -18
- wagtail/test/search/migrations/__init__.py +0 -0
- wagtail/test/search/models.py +0 -137
- /wagtail/admin/templates/wagtailadmin/{pages/listing/_ordering_cell.html → tables/ordering_cell.html} +0 -0
- /wagtail/{search/checks.py → checks.py} +0 -0
- {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/WHEEL +0 -0
- {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/entry_points.txt +0 -0
- {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/licenses/LICENSE +0 -0
- {wagtail-7.1.2.dist-info → wagtail-7.2rc1.dist-info}/top_level.txt +0 -0
|
@@ -1,1277 +1,35 @@
|
|
|
1
|
-
import
|
|
2
|
-
from
|
|
3
|
-
|
|
4
|
-
from urllib.parse import urlparse
|
|
5
|
-
|
|
6
|
-
from django.db import DEFAULT_DB_ALIAS, models
|
|
7
|
-
from django.db.models import Subquery
|
|
8
|
-
from django.db.models.sql import Query
|
|
9
|
-
from django.db.models.sql.constants import MULTI, SINGLE
|
|
10
|
-
from django.utils.crypto import get_random_string
|
|
11
|
-
from elasticsearch import VERSION as ELASTICSEARCH_VERSION
|
|
12
|
-
from elasticsearch import Elasticsearch, NotFoundError
|
|
13
|
-
from elasticsearch.helpers import bulk
|
|
14
|
-
|
|
15
|
-
from wagtail.search.backends.base import (
|
|
16
|
-
BaseSearchBackend,
|
|
17
|
-
BaseSearchQueryCompiler,
|
|
18
|
-
BaseSearchResults,
|
|
19
|
-
FilterFieldError,
|
|
20
|
-
get_model_root,
|
|
1
|
+
from wagtailmodelsearch.backends.elasticsearch7 import * # noqa: F403
|
|
2
|
+
from wagtailmodelsearch.backends.elasticsearch7 import (
|
|
3
|
+
Elasticsearch7AutocompleteQueryCompiler as _Elasticsearch7AutocompleteQueryCompiler,
|
|
21
4
|
)
|
|
22
|
-
from
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
SearchField,
|
|
28
|
-
class_is_indexed,
|
|
29
|
-
get_indexed_models,
|
|
5
|
+
from wagtailmodelsearch.backends.elasticsearch7 import (
|
|
6
|
+
Elasticsearch7SearchBackend as _Elasticsearch7SearchBackend,
|
|
7
|
+
)
|
|
8
|
+
from wagtailmodelsearch.backends.elasticsearch7 import (
|
|
9
|
+
Elasticsearch7SearchQueryCompiler as _Elasticsearch7SearchQueryCompiler,
|
|
30
10
|
)
|
|
31
|
-
from wagtail.search.query import And, Boost, Fuzzy, MatchAll, Not, Or, Phrase, PlainText
|
|
32
|
-
from wagtail.utils.utils import deep_update
|
|
33
|
-
|
|
34
|
-
use_new_elasticsearch_api = ELASTICSEARCH_VERSION >= (7, 15)
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
class Field:
|
|
38
|
-
def __init__(self, field_name, boost=1):
|
|
39
|
-
self.field_name = field_name
|
|
40
|
-
self.boost = boost
|
|
41
|
-
|
|
42
|
-
@property
|
|
43
|
-
def field_name_with_boost(self):
|
|
44
|
-
if self.boost == 1:
|
|
45
|
-
return self.field_name
|
|
46
|
-
else:
|
|
47
|
-
return f"{self.field_name}^{self.boost}"
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
class Elasticsearch7Mapping:
|
|
51
|
-
all_field_name = "_all_text"
|
|
52
|
-
edgengrams_field_name = "_edgengrams"
|
|
53
|
-
|
|
54
|
-
type_map = {
|
|
55
|
-
"AutoField": "integer",
|
|
56
|
-
"SmallAutoField": "integer",
|
|
57
|
-
"BigAutoField": "long",
|
|
58
|
-
"BinaryField": "binary",
|
|
59
|
-
"BooleanField": "boolean",
|
|
60
|
-
"CharField": "string",
|
|
61
|
-
"CommaSeparatedIntegerField": "string",
|
|
62
|
-
"DateField": "date",
|
|
63
|
-
"DateTimeField": "date",
|
|
64
|
-
"DecimalField": "double",
|
|
65
|
-
"FileField": "string",
|
|
66
|
-
"FilePathField": "string",
|
|
67
|
-
"FloatField": "double",
|
|
68
|
-
"IntegerField": "integer",
|
|
69
|
-
"BigIntegerField": "long",
|
|
70
|
-
"IPAddressField": "string",
|
|
71
|
-
"GenericIPAddressField": "string",
|
|
72
|
-
"NullBooleanField": "boolean",
|
|
73
|
-
"PositiveIntegerField": "integer",
|
|
74
|
-
"PositiveSmallIntegerField": "integer",
|
|
75
|
-
"PositiveBigIntegerField": "long",
|
|
76
|
-
"SlugField": "string",
|
|
77
|
-
"SmallIntegerField": "integer",
|
|
78
|
-
"TextField": "string",
|
|
79
|
-
"TimeField": "date",
|
|
80
|
-
"URLField": "string",
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
keyword_type = "keyword"
|
|
84
|
-
text_type = "text"
|
|
85
|
-
edgengram_analyzer_config = {
|
|
86
|
-
"analyzer": "edgengram_analyzer",
|
|
87
|
-
"search_analyzer": "standard",
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
def __init__(self, model):
|
|
91
|
-
self.model = model
|
|
92
|
-
|
|
93
|
-
def get_parent(self):
|
|
94
|
-
for base in self.model.__bases__:
|
|
95
|
-
if issubclass(base, Indexed) and issubclass(base, models.Model):
|
|
96
|
-
return type(self)(base)
|
|
97
|
-
|
|
98
|
-
def get_document_type(self):
|
|
99
|
-
return "doc"
|
|
100
|
-
|
|
101
|
-
def get_field_column_name(self, field):
|
|
102
|
-
# Fields in derived models get prefixed with their model name, fields
|
|
103
|
-
# in the root model don't get prefixed at all
|
|
104
|
-
# This is to prevent mapping clashes in cases where two page types have
|
|
105
|
-
# a field with the same name but a different type.
|
|
106
|
-
root_model = get_model_root(self.model)
|
|
107
|
-
definition_model = field.get_definition_model(self.model)
|
|
108
|
-
|
|
109
|
-
if definition_model != root_model:
|
|
110
|
-
prefix = (
|
|
111
|
-
definition_model._meta.app_label.lower()
|
|
112
|
-
+ "_"
|
|
113
|
-
+ definition_model.__name__.lower()
|
|
114
|
-
+ "__"
|
|
115
|
-
)
|
|
116
|
-
else:
|
|
117
|
-
prefix = ""
|
|
118
|
-
|
|
119
|
-
if isinstance(field, FilterField):
|
|
120
|
-
return prefix + field.get_attname(self.model) + "_filter"
|
|
121
|
-
elif isinstance(field, AutocompleteField):
|
|
122
|
-
return prefix + field.get_attname(self.model) + "_edgengrams"
|
|
123
|
-
elif isinstance(field, SearchField):
|
|
124
|
-
return prefix + field.get_attname(self.model)
|
|
125
|
-
elif isinstance(field, RelatedFields):
|
|
126
|
-
return prefix + field.field_name
|
|
127
|
-
|
|
128
|
-
def get_boost_field_name(self, boost):
|
|
129
|
-
# replace . with _ to avoid issues with . in field names
|
|
130
|
-
boost = str(float(boost)).replace(".", "_")
|
|
131
|
-
return f"{self.all_field_name}_boost_{boost}"
|
|
132
|
-
|
|
133
|
-
def get_content_type(self):
|
|
134
|
-
"""
|
|
135
|
-
Returns the content type as a string for the model.
|
|
136
|
-
|
|
137
|
-
For example: "wagtailcore.Page"
|
|
138
|
-
"myapp.MyModel"
|
|
139
|
-
"""
|
|
140
|
-
return self.model._meta.app_label + "." + self.model.__name__
|
|
141
|
-
|
|
142
|
-
def get_all_content_types(self):
|
|
143
|
-
"""
|
|
144
|
-
Returns all the content type strings that apply to this model.
|
|
145
|
-
This includes the models' content type and all concrete ancestor
|
|
146
|
-
models that inherit from Indexed.
|
|
147
|
-
|
|
148
|
-
For example: ["myapp.MyPageModel", "wagtailcore.Page"]
|
|
149
|
-
["myapp.MyModel"]
|
|
150
|
-
"""
|
|
151
|
-
# Add our content type
|
|
152
|
-
content_types = [self.get_content_type()]
|
|
153
|
-
|
|
154
|
-
# Add all ancestor classes content types as well
|
|
155
|
-
ancestor = self.get_parent()
|
|
156
|
-
while ancestor:
|
|
157
|
-
content_types.append(ancestor.get_content_type())
|
|
158
|
-
ancestor = ancestor.get_parent()
|
|
159
|
-
|
|
160
|
-
return content_types
|
|
161
|
-
|
|
162
|
-
def get_field_mapping(self, field):
|
|
163
|
-
if isinstance(field, RelatedFields):
|
|
164
|
-
mapping = {"type": "nested", "properties": {}}
|
|
165
|
-
nested_model = field.get_field(self.model).related_model
|
|
166
|
-
nested_mapping = type(self)(nested_model)
|
|
167
|
-
|
|
168
|
-
for sub_field in field.fields:
|
|
169
|
-
sub_field_name, sub_field_mapping = nested_mapping.get_field_mapping(
|
|
170
|
-
sub_field
|
|
171
|
-
)
|
|
172
|
-
mapping["properties"][sub_field_name] = sub_field_mapping
|
|
173
|
-
|
|
174
|
-
return self.get_field_column_name(field), mapping
|
|
175
|
-
else:
|
|
176
|
-
mapping = {"type": self.type_map.get(field.get_type(self.model), "string")}
|
|
177
|
-
|
|
178
|
-
if isinstance(field, SearchField):
|
|
179
|
-
if mapping["type"] == "string":
|
|
180
|
-
mapping["type"] = self.text_type
|
|
181
|
-
|
|
182
|
-
if field.boost:
|
|
183
|
-
mapping["boost"] = field.boost
|
|
184
|
-
|
|
185
|
-
mapping["include_in_all"] = True
|
|
186
|
-
|
|
187
|
-
if isinstance(field, AutocompleteField):
|
|
188
|
-
mapping["type"] = self.text_type
|
|
189
|
-
mapping.update(self.edgengram_analyzer_config)
|
|
190
|
-
|
|
191
|
-
elif isinstance(field, FilterField):
|
|
192
|
-
if mapping["type"] == "string":
|
|
193
|
-
mapping["type"] = self.keyword_type
|
|
194
|
-
|
|
195
|
-
if "es_extra" in field.kwargs:
|
|
196
|
-
for key, value in field.kwargs["es_extra"].items():
|
|
197
|
-
mapping[key] = value
|
|
198
|
-
|
|
199
|
-
return self.get_field_column_name(field), mapping
|
|
200
|
-
|
|
201
|
-
def get_mapping(self):
|
|
202
|
-
# Make field list
|
|
203
|
-
fields = {
|
|
204
|
-
"pk": {"type": self.keyword_type, "store": True},
|
|
205
|
-
"content_type": {"type": self.keyword_type},
|
|
206
|
-
self.edgengrams_field_name: {"type": self.text_type},
|
|
207
|
-
}
|
|
208
|
-
fields[self.edgengrams_field_name].update(self.edgengram_analyzer_config)
|
|
209
|
-
|
|
210
|
-
for field in self.model.get_search_fields():
|
|
211
|
-
key, val = self.get_field_mapping(field)
|
|
212
|
-
fields[key] = val
|
|
213
|
-
|
|
214
|
-
# Add _all_text field
|
|
215
|
-
fields[self.all_field_name] = {"type": "text"}
|
|
216
|
-
|
|
217
|
-
unique_boosts = set()
|
|
218
|
-
|
|
219
|
-
# Replace {"include_in_all": true} with {"copy_to": ["_all_text", "_all_text_boost_2"]}
|
|
220
|
-
def replace_include_in_all(properties):
|
|
221
|
-
for field_mapping in properties.values():
|
|
222
|
-
if "include_in_all" in field_mapping:
|
|
223
|
-
if field_mapping["include_in_all"]:
|
|
224
|
-
field_mapping["copy_to"] = self.all_field_name
|
|
225
|
-
|
|
226
|
-
if "boost" in field_mapping:
|
|
227
|
-
# added to unique_boosts to avoid duplicate fields, or cases like 2.0 and 2
|
|
228
|
-
unique_boosts.add(field_mapping["boost"])
|
|
229
|
-
field_mapping["copy_to"] = [
|
|
230
|
-
field_mapping["copy_to"],
|
|
231
|
-
self.get_boost_field_name(field_mapping["boost"]),
|
|
232
|
-
]
|
|
233
|
-
del field_mapping["boost"]
|
|
234
|
-
|
|
235
|
-
del field_mapping["include_in_all"]
|
|
236
|
-
|
|
237
|
-
if field_mapping["type"] == "nested":
|
|
238
|
-
replace_include_in_all(field_mapping["properties"])
|
|
239
|
-
|
|
240
|
-
replace_include_in_all(fields)
|
|
241
|
-
for boost in unique_boosts:
|
|
242
|
-
fields[self.get_boost_field_name(boost)] = {"type": "text"}
|
|
243
|
-
|
|
244
|
-
return {
|
|
245
|
-
"properties": fields,
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
def get_document_id(self, obj):
|
|
249
|
-
return str(obj.pk)
|
|
250
|
-
|
|
251
|
-
def _get_nested_document(self, fields, obj):
|
|
252
|
-
doc = {}
|
|
253
|
-
edgengrams = []
|
|
254
|
-
model = type(obj)
|
|
255
|
-
mapping = type(self)(model)
|
|
256
|
-
|
|
257
|
-
for field in fields:
|
|
258
|
-
value = field.get_value(obj)
|
|
259
|
-
doc[mapping.get_field_column_name(field)] = value
|
|
260
|
-
|
|
261
|
-
# Check if this field should be added into _edgengrams
|
|
262
|
-
if isinstance(field, AutocompleteField):
|
|
263
|
-
edgengrams.append(value)
|
|
264
|
-
|
|
265
|
-
return doc, edgengrams
|
|
266
|
-
|
|
267
|
-
def get_document(self, obj):
|
|
268
|
-
# Build document
|
|
269
|
-
doc = {"pk": str(obj.pk), "content_type": self.get_all_content_types()}
|
|
270
|
-
edgengrams = []
|
|
271
|
-
for field in self.model.get_search_fields():
|
|
272
|
-
value = field.get_value(obj)
|
|
273
|
-
|
|
274
|
-
if isinstance(field, RelatedFields):
|
|
275
|
-
if isinstance(value, (models.Manager, models.QuerySet)):
|
|
276
|
-
nested_docs = []
|
|
277
|
-
|
|
278
|
-
for nested_obj in value.all():
|
|
279
|
-
nested_doc, extra_edgengrams = self._get_nested_document(
|
|
280
|
-
field.fields, nested_obj
|
|
281
|
-
)
|
|
282
|
-
nested_docs.append(nested_doc)
|
|
283
|
-
edgengrams.extend(extra_edgengrams)
|
|
284
|
-
|
|
285
|
-
value = nested_docs
|
|
286
|
-
elif isinstance(value, models.Model):
|
|
287
|
-
value, extra_edgengrams = self._get_nested_document(
|
|
288
|
-
field.fields, value
|
|
289
|
-
)
|
|
290
|
-
edgengrams.extend(extra_edgengrams)
|
|
291
|
-
elif isinstance(field, FilterField):
|
|
292
|
-
if isinstance(value, (models.Manager, models.QuerySet)):
|
|
293
|
-
value = list(value.values_list("pk", flat=True))
|
|
294
|
-
elif isinstance(value, models.Model):
|
|
295
|
-
value = value.pk
|
|
296
|
-
elif isinstance(value, (list, tuple)):
|
|
297
|
-
value = [
|
|
298
|
-
item.pk if isinstance(item, models.Model) else item
|
|
299
|
-
for item in value
|
|
300
|
-
]
|
|
301
|
-
|
|
302
|
-
doc[self.get_field_column_name(field)] = value
|
|
303
|
-
|
|
304
|
-
# Check if this field should be added into _edgengrams
|
|
305
|
-
if isinstance(field, AutocompleteField):
|
|
306
|
-
edgengrams.append(value)
|
|
307
|
-
|
|
308
|
-
# Add partials to document
|
|
309
|
-
doc[self.edgengrams_field_name] = edgengrams
|
|
310
|
-
|
|
311
|
-
return doc
|
|
312
|
-
|
|
313
|
-
def __repr__(self):
|
|
314
|
-
return f"<ElasticsearchMapping: {self.model.__name__}>"
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
class Elasticsearch7Index:
|
|
318
|
-
def __init__(self, backend, name):
|
|
319
|
-
self.backend = backend
|
|
320
|
-
self.es = backend.es
|
|
321
|
-
self.mapping_class = backend.mapping_class
|
|
322
|
-
self.name = name
|
|
323
|
-
|
|
324
|
-
if use_new_elasticsearch_api:
|
|
325
|
-
|
|
326
|
-
def put(self):
|
|
327
|
-
self.es.indices.create(index=self.name, **self.backend.settings)
|
|
328
|
-
|
|
329
|
-
def delete(self):
|
|
330
|
-
try:
|
|
331
|
-
self.es.indices.delete(index=self.name)
|
|
332
|
-
except NotFoundError:
|
|
333
|
-
pass
|
|
334
|
-
|
|
335
|
-
def refresh(self):
|
|
336
|
-
self.es.indices.refresh(index=self.name)
|
|
337
|
-
|
|
338
|
-
else:
|
|
339
|
-
|
|
340
|
-
def put(self):
|
|
341
|
-
self.es.indices.create(self.name, self.backend.settings)
|
|
342
|
-
|
|
343
|
-
def delete(self):
|
|
344
|
-
try:
|
|
345
|
-
self.es.indices.delete(self.name)
|
|
346
|
-
except NotFoundError:
|
|
347
|
-
pass
|
|
348
|
-
|
|
349
|
-
def refresh(self):
|
|
350
|
-
self.es.indices.refresh(self.name)
|
|
351
|
-
|
|
352
|
-
def exists(self):
|
|
353
|
-
return self.es.indices.exists(self.name)
|
|
354
|
-
|
|
355
|
-
def is_alias(self):
|
|
356
|
-
return self.es.indices.exists_alias(name=self.name)
|
|
357
|
-
|
|
358
|
-
def aliased_indices(self):
|
|
359
|
-
"""
|
|
360
|
-
If this index object represents an alias (which appear the same in the
|
|
361
|
-
Elasticsearch API), this method can be used to fetch the list of indices
|
|
362
|
-
the alias points to.
|
|
363
|
-
|
|
364
|
-
Use the is_alias method if you need to find out if this an alias. This
|
|
365
|
-
returns an empty list if called on an index.
|
|
366
|
-
"""
|
|
367
|
-
return [
|
|
368
|
-
self.backend.index_class(self.backend, index_name)
|
|
369
|
-
for index_name in self.es.indices.get_alias(name=self.name).keys()
|
|
370
|
-
]
|
|
371
|
-
|
|
372
|
-
def put_alias(self, name):
|
|
373
|
-
"""
|
|
374
|
-
Creates a new alias to this index. If the alias already exists it will
|
|
375
|
-
be repointed to this index.
|
|
376
|
-
"""
|
|
377
|
-
self.es.indices.put_alias(name=name, index=self.name)
|
|
378
|
-
|
|
379
|
-
def add_model(self, model):
|
|
380
|
-
# Get mapping
|
|
381
|
-
mapping = self.mapping_class(model)
|
|
382
|
-
|
|
383
|
-
# Put mapping
|
|
384
|
-
self.es.indices.put_mapping(index=self.name, body=mapping.get_mapping())
|
|
385
|
-
|
|
386
|
-
if use_new_elasticsearch_api:
|
|
387
|
-
|
|
388
|
-
def add_item(self, item):
|
|
389
|
-
# Make sure the object can be indexed
|
|
390
|
-
if not class_is_indexed(item.__class__):
|
|
391
|
-
return
|
|
392
|
-
|
|
393
|
-
# Get mapping
|
|
394
|
-
mapping = self.mapping_class(item.__class__)
|
|
395
|
-
|
|
396
|
-
# Add document to index
|
|
397
|
-
self.es.index(
|
|
398
|
-
index=self.name,
|
|
399
|
-
document=mapping.get_document(item),
|
|
400
|
-
id=mapping.get_document_id(item),
|
|
401
|
-
)
|
|
402
|
-
|
|
403
|
-
else:
|
|
404
|
-
|
|
405
|
-
def add_item(self, item):
|
|
406
|
-
# Make sure the object can be indexed
|
|
407
|
-
if not class_is_indexed(item.__class__):
|
|
408
|
-
return
|
|
409
|
-
# Get mapping
|
|
410
|
-
mapping = self.mapping_class(item.__class__)
|
|
411
|
-
|
|
412
|
-
# Add document to index
|
|
413
|
-
self.es.index(
|
|
414
|
-
self.name, mapping.get_document(item), id=mapping.get_document_id(item)
|
|
415
|
-
)
|
|
416
|
-
|
|
417
|
-
def add_items(self, model, items):
|
|
418
|
-
if not class_is_indexed(model):
|
|
419
|
-
return
|
|
420
|
-
|
|
421
|
-
# Get mapping
|
|
422
|
-
mapping = self.mapping_class(model)
|
|
423
|
-
|
|
424
|
-
# Create list of actions
|
|
425
|
-
actions = []
|
|
426
|
-
for item in items:
|
|
427
|
-
# Create the action
|
|
428
|
-
action = {"_id": mapping.get_document_id(item)}
|
|
429
|
-
action.update(mapping.get_document(item))
|
|
430
|
-
actions.append(action)
|
|
431
|
-
|
|
432
|
-
# Run the actions
|
|
433
|
-
bulk(self.es, actions, index=self.name)
|
|
434
|
-
|
|
435
|
-
def delete_item(self, item):
|
|
436
|
-
# Make sure the object can be indexed
|
|
437
|
-
if not class_is_indexed(item.__class__):
|
|
438
|
-
return
|
|
439
|
-
|
|
440
|
-
# Get mapping
|
|
441
|
-
mapping = self.mapping_class(item.__class__)
|
|
442
|
-
|
|
443
|
-
# Delete document
|
|
444
|
-
try:
|
|
445
|
-
self.es.delete(index=self.name, id=mapping.get_document_id(item))
|
|
446
|
-
except NotFoundError:
|
|
447
|
-
pass # Document doesn't exist, ignore this exception
|
|
448
|
-
|
|
449
|
-
def reset(self):
|
|
450
|
-
# Delete old index
|
|
451
|
-
self.delete()
|
|
452
|
-
|
|
453
|
-
# Create new index
|
|
454
|
-
self.put()
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
class Elasticsearch7SearchQueryCompiler(BaseSearchQueryCompiler):
|
|
458
|
-
mapping_class = Elasticsearch7Mapping
|
|
459
|
-
DEFAULT_OPERATOR = "or"
|
|
460
|
-
|
|
461
|
-
def __init__(self, *args, **kwargs):
|
|
462
|
-
super().__init__(*args, **kwargs)
|
|
463
|
-
self.mapping = self.mapping_class(self.queryset.model)
|
|
464
|
-
self.remapped_fields = self._remap_fields(self.fields)
|
|
465
|
-
|
|
466
|
-
def _remap_fields(self, fields):
|
|
467
|
-
"""Convert field names into index column names and add boosts."""
|
|
468
|
-
|
|
469
|
-
remapped_fields = []
|
|
470
|
-
if fields:
|
|
471
|
-
searchable_fields = {f.field_name: f for f in self.get_searchable_fields()}
|
|
472
|
-
for field_name in fields:
|
|
473
|
-
field = searchable_fields.get(field_name)
|
|
474
|
-
if field:
|
|
475
|
-
field_name = self.mapping.get_field_column_name(field)
|
|
476
|
-
remapped_fields.append(Field(field_name, field.boost or 1))
|
|
477
|
-
else:
|
|
478
|
-
remapped_fields.append(Field(self.mapping.all_field_name))
|
|
479
|
-
|
|
480
|
-
models = get_indexed_models()
|
|
481
|
-
unique_boosts = set()
|
|
482
|
-
for model in models:
|
|
483
|
-
if not issubclass(model, self.queryset.model):
|
|
484
|
-
continue
|
|
485
|
-
for field in model.get_searchable_search_fields():
|
|
486
|
-
if field.boost:
|
|
487
|
-
unique_boosts.add(float(field.boost))
|
|
488
|
-
|
|
489
|
-
remapped_fields.extend(
|
|
490
|
-
[
|
|
491
|
-
Field(self.mapping.get_boost_field_name(boost), boost)
|
|
492
|
-
for boost in unique_boosts
|
|
493
|
-
]
|
|
494
|
-
)
|
|
495
|
-
|
|
496
|
-
return remapped_fields
|
|
497
|
-
|
|
498
|
-
def _process_lookup(self, field, lookup, value):
|
|
499
|
-
column_name = self.mapping.get_field_column_name(field)
|
|
500
|
-
|
|
501
|
-
if lookup == "exact":
|
|
502
|
-
if value is None:
|
|
503
|
-
return {
|
|
504
|
-
"missing": {
|
|
505
|
-
"field": column_name,
|
|
506
|
-
}
|
|
507
|
-
}
|
|
508
|
-
else:
|
|
509
|
-
if isinstance(value, (Query, Subquery)):
|
|
510
|
-
db_alias = self.queryset._db or DEFAULT_DB_ALIAS
|
|
511
|
-
query = value.query if isinstance(value, Subquery) else value
|
|
512
|
-
value = query.get_compiler(db_alias).execute_sql(result_type=SINGLE)
|
|
513
|
-
# The result is either a tuple with one element or None
|
|
514
|
-
if value:
|
|
515
|
-
value = value[0]
|
|
516
|
-
|
|
517
|
-
return {
|
|
518
|
-
"term": {
|
|
519
|
-
column_name: value,
|
|
520
|
-
}
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
if lookup == "isnull":
|
|
524
|
-
query = {
|
|
525
|
-
"exists": {
|
|
526
|
-
"field": column_name,
|
|
527
|
-
}
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
if value:
|
|
531
|
-
query = {"bool": {"mustNot": query}}
|
|
532
|
-
|
|
533
|
-
return query
|
|
534
|
-
|
|
535
|
-
if lookup in ["startswith", "prefix"]:
|
|
536
|
-
return {
|
|
537
|
-
"prefix": {
|
|
538
|
-
column_name: value,
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
|
|
542
|
-
if lookup in ["gt", "gte", "lt", "lte"]:
|
|
543
|
-
return {
|
|
544
|
-
"range": {
|
|
545
|
-
column_name: {
|
|
546
|
-
lookup: value,
|
|
547
|
-
}
|
|
548
|
-
}
|
|
549
|
-
}
|
|
550
|
-
|
|
551
|
-
if lookup == "range":
|
|
552
|
-
lower, upper = value
|
|
553
|
-
|
|
554
|
-
return {
|
|
555
|
-
"range": {
|
|
556
|
-
column_name: {
|
|
557
|
-
"gte": lower,
|
|
558
|
-
"lte": upper,
|
|
559
|
-
}
|
|
560
|
-
}
|
|
561
|
-
}
|
|
562
|
-
|
|
563
|
-
if lookup == "in":
|
|
564
|
-
if isinstance(value, (Query, Subquery)):
|
|
565
|
-
db_alias = self.queryset._db or DEFAULT_DB_ALIAS
|
|
566
|
-
query = value.query if isinstance(value, Subquery) else value
|
|
567
|
-
resultset = query.get_compiler(db_alias).execute_sql(result_type=MULTI)
|
|
568
|
-
value = [row[0] for chunk in resultset for row in chunk]
|
|
569
|
-
|
|
570
|
-
elif not isinstance(value, list):
|
|
571
|
-
value = list(value)
|
|
572
|
-
return {
|
|
573
|
-
"terms": {
|
|
574
|
-
column_name: value,
|
|
575
|
-
}
|
|
576
|
-
}
|
|
577
|
-
|
|
578
|
-
def _process_match_none(self):
|
|
579
|
-
return {"bool": {"mustNot": {"match_all": {}}}}
|
|
580
|
-
|
|
581
|
-
def _connect_filters(self, filters, connector, negated):
|
|
582
|
-
if filters:
|
|
583
|
-
if len(filters) == 1:
|
|
584
|
-
filter_out = filters[0]
|
|
585
|
-
elif connector == "AND":
|
|
586
|
-
filter_out = {
|
|
587
|
-
"bool": {"must": [fil for fil in filters if fil is not None]}
|
|
588
|
-
}
|
|
589
|
-
elif connector == "OR":
|
|
590
|
-
filter_out = {
|
|
591
|
-
"bool": {"should": [fil for fil in filters if fil is not None]}
|
|
592
|
-
}
|
|
593
|
-
|
|
594
|
-
if negated:
|
|
595
|
-
filter_out = {"bool": {"mustNot": filter_out}}
|
|
596
|
-
|
|
597
|
-
return filter_out
|
|
598
|
-
|
|
599
|
-
def _compile_plaintext_query(self, query, fields, boost=1.0):
|
|
600
|
-
match_query = {"query": query.query_string}
|
|
601
|
-
|
|
602
|
-
if query.operator != "or":
|
|
603
|
-
match_query["operator"] = query.operator
|
|
604
|
-
|
|
605
|
-
if len(fields) == 1:
|
|
606
|
-
if boost != 1.0 or fields[0].boost != 1.0:
|
|
607
|
-
match_query["boost"] = boost * fields[0].boost
|
|
608
|
-
return {"match": {fields[0].field_name: match_query}}
|
|
609
|
-
else:
|
|
610
|
-
if boost != 1.0:
|
|
611
|
-
match_query["boost"] = boost
|
|
612
|
-
match_query["fields"] = [field.field_name_with_boost for field in fields]
|
|
613
|
-
|
|
614
|
-
return {"multi_match": match_query}
|
|
615
|
-
|
|
616
|
-
def _compile_fuzzy_query(self, query, fields):
|
|
617
|
-
match_query = {
|
|
618
|
-
"query": query.query_string,
|
|
619
|
-
"fuzziness": "AUTO",
|
|
620
|
-
}
|
|
621
|
-
|
|
622
|
-
if query.operator != "or":
|
|
623
|
-
match_query["operator"] = query.operator
|
|
624
|
-
|
|
625
|
-
if len(fields) == 1:
|
|
626
|
-
if fields[0].boost != 1.0:
|
|
627
|
-
match_query["boost"] = fields[0].boost
|
|
628
|
-
return {"match": {fields[0].field_name: match_query}}
|
|
629
|
-
else:
|
|
630
|
-
match_query["fields"] = [field.field_name_with_boost for field in fields]
|
|
631
|
-
return {"multi_match": match_query}
|
|
632
|
-
|
|
633
|
-
def _compile_phrase_query(self, query, fields):
|
|
634
|
-
if len(fields) == 1:
|
|
635
|
-
if fields[0].boost != 1.0:
|
|
636
|
-
return {
|
|
637
|
-
"match_phrase": {
|
|
638
|
-
fields[0].field_name: {
|
|
639
|
-
"query": query.query_string,
|
|
640
|
-
"boost": fields[0].boost,
|
|
641
|
-
}
|
|
642
|
-
}
|
|
643
|
-
}
|
|
644
|
-
else:
|
|
645
|
-
return {"match_phrase": {fields[0].field_name: query.query_string}}
|
|
646
|
-
else:
|
|
647
|
-
return {
|
|
648
|
-
"multi_match": {
|
|
649
|
-
"query": query.query_string,
|
|
650
|
-
"fields": [field.field_name_with_boost for field in fields],
|
|
651
|
-
"type": "phrase",
|
|
652
|
-
}
|
|
653
|
-
}
|
|
654
|
-
|
|
655
|
-
def _compile_query(self, query, field, boost=1.0):
|
|
656
|
-
if isinstance(query, MatchAll):
|
|
657
|
-
match_all_query = {}
|
|
658
|
-
|
|
659
|
-
if boost != 1.0:
|
|
660
|
-
match_all_query["boost"] = boost
|
|
661
|
-
|
|
662
|
-
return {"match_all": match_all_query}
|
|
663
|
-
|
|
664
|
-
elif isinstance(query, And):
|
|
665
|
-
return {
|
|
666
|
-
"bool": {
|
|
667
|
-
"must": [
|
|
668
|
-
self._compile_query(child_query, field, boost)
|
|
669
|
-
for child_query in query.subqueries
|
|
670
|
-
]
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
|
-
|
|
674
|
-
elif isinstance(query, Or):
|
|
675
|
-
return {
|
|
676
|
-
"bool": {
|
|
677
|
-
"should": [
|
|
678
|
-
self._compile_query(child_query, field, boost)
|
|
679
|
-
for child_query in query.subqueries
|
|
680
|
-
]
|
|
681
|
-
}
|
|
682
|
-
}
|
|
683
|
-
|
|
684
|
-
elif isinstance(query, Not):
|
|
685
|
-
return {
|
|
686
|
-
"bool": {"mustNot": self._compile_query(query.subquery, field, boost)}
|
|
687
|
-
}
|
|
688
|
-
|
|
689
|
-
elif isinstance(query, PlainText):
|
|
690
|
-
return self._compile_plaintext_query(query, [field], boost)
|
|
691
|
-
|
|
692
|
-
elif isinstance(query, Fuzzy):
|
|
693
|
-
return self._compile_fuzzy_query(query, [field])
|
|
694
|
-
|
|
695
|
-
elif isinstance(query, Phrase):
|
|
696
|
-
return self._compile_phrase_query(query, [field])
|
|
697
|
-
|
|
698
|
-
elif isinstance(query, Boost):
|
|
699
|
-
return self._compile_query(query.subquery, field, boost * query.boost)
|
|
700
|
-
|
|
701
|
-
else:
|
|
702
|
-
raise NotImplementedError(
|
|
703
|
-
"`%s` is not supported by the Elasticsearch search backend."
|
|
704
|
-
% query.__class__.__name__
|
|
705
|
-
)
|
|
706
|
-
|
|
707
|
-
def get_inner_query(self):
|
|
708
|
-
if self.remapped_fields:
|
|
709
|
-
fields = self.remapped_fields
|
|
710
|
-
else:
|
|
711
|
-
fields = [self.mapping.all_field_name]
|
|
712
|
-
|
|
713
|
-
if len(fields) == 0:
|
|
714
|
-
# No fields. Return a query that'll match nothing
|
|
715
|
-
return {"bool": {"mustNot": {"match_all": {}}}}
|
|
716
|
-
|
|
717
|
-
# Handle MatchAll and PlainText separately as they were supported
|
|
718
|
-
# before "search query classes" was implemented and we'd like to
|
|
719
|
-
# keep the query the same as before
|
|
720
|
-
if isinstance(self.query, MatchAll):
|
|
721
|
-
return {"match_all": {}}
|
|
722
|
-
|
|
723
|
-
elif isinstance(self.query, PlainText):
|
|
724
|
-
return self._compile_plaintext_query(self.query, fields)
|
|
725
|
-
|
|
726
|
-
elif isinstance(self.query, Phrase):
|
|
727
|
-
return self._compile_phrase_query(self.query, fields)
|
|
728
|
-
|
|
729
|
-
elif isinstance(self.query, Fuzzy):
|
|
730
|
-
return self._compile_fuzzy_query(self.query, fields)
|
|
731
|
-
|
|
732
|
-
elif isinstance(self.query, Not):
|
|
733
|
-
return {
|
|
734
|
-
"bool": {
|
|
735
|
-
"mustNot": [
|
|
736
|
-
self._compile_query(self.query.subquery, field)
|
|
737
|
-
for field in fields
|
|
738
|
-
]
|
|
739
|
-
}
|
|
740
|
-
}
|
|
741
|
-
|
|
742
|
-
else:
|
|
743
|
-
return self._join_and_compile_queries(self.query, fields)
|
|
744
|
-
|
|
745
|
-
def _join_and_compile_queries(self, query, fields, boost=1.0):
|
|
746
|
-
if len(fields) == 1:
|
|
747
|
-
return self._compile_query(query, fields[0], boost)
|
|
748
|
-
else:
|
|
749
|
-
# Compile a query for each field then combine with disjunction
|
|
750
|
-
# max (or operator which takes the max score out of each of the
|
|
751
|
-
# field queries)
|
|
752
|
-
field_queries = []
|
|
753
|
-
for field in fields:
|
|
754
|
-
field_queries.append(self._compile_query(query, field, boost))
|
|
755
|
-
|
|
756
|
-
return {"dis_max": {"queries": field_queries}}
|
|
757
|
-
|
|
758
|
-
def get_content_type_filter(self):
|
|
759
|
-
# Query content_type using a "match" query. See comment in
|
|
760
|
-
# Elasticsearch7Mapping.get_document for more details
|
|
761
|
-
content_type = self.mapping_class(self.queryset.model).get_content_type()
|
|
762
|
-
|
|
763
|
-
return {"match": {"content_type": content_type}}
|
|
764
|
-
|
|
765
|
-
def get_filters(self):
|
|
766
|
-
# Filter by content type
|
|
767
|
-
filters = [self.get_content_type_filter()]
|
|
768
|
-
|
|
769
|
-
# Apply filters from queryset
|
|
770
|
-
queryset_filters = self._get_filters_from_queryset()
|
|
771
|
-
if queryset_filters:
|
|
772
|
-
filters.append(queryset_filters)
|
|
773
|
-
|
|
774
|
-
return filters
|
|
775
|
-
|
|
776
|
-
def get_query(self):
|
|
777
|
-
inner_query = self.get_inner_query()
|
|
778
|
-
filters = self.get_filters()
|
|
779
|
-
|
|
780
|
-
if len(filters) == 1:
|
|
781
|
-
return {
|
|
782
|
-
"bool": {
|
|
783
|
-
"must": inner_query,
|
|
784
|
-
"filter": filters[0],
|
|
785
|
-
}
|
|
786
|
-
}
|
|
787
|
-
elif len(filters) > 1:
|
|
788
|
-
return {
|
|
789
|
-
"bool": {
|
|
790
|
-
"must": inner_query,
|
|
791
|
-
"filter": filters,
|
|
792
|
-
}
|
|
793
|
-
}
|
|
794
|
-
else:
|
|
795
|
-
return inner_query
|
|
796
|
-
|
|
797
|
-
def get_searchable_fields(self):
|
|
798
|
-
return self.queryset.model.get_searchable_search_fields()
|
|
799
|
-
|
|
800
|
-
def get_sort(self):
|
|
801
|
-
# Ordering by relevance is the default in Elasticsearch
|
|
802
|
-
if self.order_by_relevance:
|
|
803
|
-
return
|
|
804
|
-
|
|
805
|
-
# Get queryset and make sure its ordered
|
|
806
|
-
if self.queryset.ordered:
|
|
807
|
-
sort = []
|
|
808
|
-
|
|
809
|
-
for reverse, field in self._get_order_by():
|
|
810
|
-
column_name = self.mapping.get_field_column_name(field)
|
|
811
|
-
|
|
812
|
-
sort.append({column_name: "desc" if reverse else "asc"})
|
|
813
|
-
|
|
814
|
-
return sort
|
|
815
|
-
|
|
816
|
-
else:
|
|
817
|
-
# Order by pk field
|
|
818
|
-
return ["pk"]
|
|
819
|
-
|
|
820
|
-
def __repr__(self):
|
|
821
|
-
return json.dumps(self.get_query())
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
class Elasticsearch7SearchResults(BaseSearchResults):
|
|
825
|
-
fields_param_name = "stored_fields"
|
|
826
|
-
supports_facet = True
|
|
827
|
-
|
|
828
|
-
def facet(self, field_name):
|
|
829
|
-
# Get field
|
|
830
|
-
field = self.query_compiler._get_filterable_field(field_name)
|
|
831
|
-
if field is None:
|
|
832
|
-
raise FilterFieldError(
|
|
833
|
-
'Cannot facet search results with field "'
|
|
834
|
-
+ field_name
|
|
835
|
-
+ "\". Please add index.FilterField('"
|
|
836
|
-
+ field_name
|
|
837
|
-
+ "') to "
|
|
838
|
-
+ self.query_compiler.queryset.model.__name__
|
|
839
|
-
+ ".search_fields.",
|
|
840
|
-
field_name=field_name,
|
|
841
|
-
)
|
|
842
|
-
|
|
843
|
-
# Build body
|
|
844
|
-
body = self._get_es_body()
|
|
845
|
-
column_name = self.query_compiler.mapping.get_field_column_name(field)
|
|
846
|
-
|
|
847
|
-
body["aggregations"] = {
|
|
848
|
-
field_name: {
|
|
849
|
-
"terms": {
|
|
850
|
-
"field": column_name,
|
|
851
|
-
"missing": 0,
|
|
852
|
-
}
|
|
853
|
-
}
|
|
854
|
-
}
|
|
855
|
-
|
|
856
|
-
# Send to Elasticsearch
|
|
857
|
-
response = self._backend_do_search(
|
|
858
|
-
body,
|
|
859
|
-
index=self.backend.get_index_for_model(
|
|
860
|
-
self.query_compiler.queryset.model
|
|
861
|
-
).name,
|
|
862
|
-
size=0,
|
|
863
|
-
)
|
|
864
|
-
|
|
865
|
-
return OrderedDict(
|
|
866
|
-
[
|
|
867
|
-
(bucket["key"] if bucket["key"] != 0 else None, bucket["doc_count"])
|
|
868
|
-
for bucket in response["aggregations"][field_name]["buckets"]
|
|
869
|
-
]
|
|
870
|
-
)
|
|
871
|
-
|
|
872
|
-
def _get_es_body(self, for_count=False):
|
|
873
|
-
body = {"query": self.query_compiler.get_query()}
|
|
874
|
-
|
|
875
|
-
if not for_count:
|
|
876
|
-
sort = self.query_compiler.get_sort()
|
|
877
|
-
|
|
878
|
-
if sort is not None:
|
|
879
|
-
body["sort"] = sort
|
|
880
|
-
|
|
881
|
-
return body
|
|
882
|
-
|
|
883
|
-
def _get_results_from_hits(self, hits):
|
|
884
|
-
"""
|
|
885
|
-
Yields Django model instances from a page of hits returned by Elasticsearch
|
|
886
|
-
"""
|
|
887
|
-
# Get pks from results
|
|
888
|
-
pks = [hit["fields"]["pk"][0] for hit in hits]
|
|
889
|
-
scores = {str(hit["fields"]["pk"][0]): hit["_score"] for hit in hits}
|
|
890
|
-
|
|
891
|
-
# Initialise results dictionary
|
|
892
|
-
results = {str(pk): None for pk in pks}
|
|
893
|
-
|
|
894
|
-
# Find objects in database and add them to dict
|
|
895
|
-
for obj in self.query_compiler.queryset.filter(pk__in=pks):
|
|
896
|
-
results[str(obj.pk)] = obj
|
|
897
|
-
|
|
898
|
-
if self._score_field:
|
|
899
|
-
setattr(obj, self._score_field, scores.get(str(obj.pk)))
|
|
900
|
-
|
|
901
|
-
# Yield results in order given by Elasticsearch
|
|
902
|
-
for pk in pks:
|
|
903
|
-
result = results[str(pk)]
|
|
904
|
-
if result:
|
|
905
|
-
yield result
|
|
906
|
-
|
|
907
|
-
if use_new_elasticsearch_api:
|
|
908
|
-
|
|
909
|
-
def _backend_do_search(self, body, **kwargs):
|
|
910
|
-
# As of Elasticsearch 7.15, the 'body' parameter is deprecated; instead, the top-level
|
|
911
|
-
# keys of the body dict are now kwargs in their own right
|
|
912
|
-
return self.backend.es.search(**body, **kwargs)
|
|
913
|
-
|
|
914
|
-
else:
|
|
915
|
-
|
|
916
|
-
def _backend_do_search(self, body, **kwargs):
|
|
917
|
-
# Send the search query to the backend.
|
|
918
|
-
return self.backend.es.search(body=body, **kwargs)
|
|
919
|
-
|
|
920
|
-
def _do_search(self):
|
|
921
|
-
PAGE_SIZE = 100
|
|
922
|
-
|
|
923
|
-
if self.stop is not None:
|
|
924
|
-
limit = self.stop - self.start
|
|
925
|
-
else:
|
|
926
|
-
limit = None
|
|
927
|
-
|
|
928
|
-
use_scroll = limit is None or limit > PAGE_SIZE
|
|
929
|
-
|
|
930
|
-
body = self._get_es_body()
|
|
931
|
-
params = {
|
|
932
|
-
"index": self.backend.get_index_for_model(
|
|
933
|
-
self.query_compiler.queryset.model
|
|
934
|
-
).name,
|
|
935
|
-
"_source": False,
|
|
936
|
-
self.fields_param_name: "pk",
|
|
937
|
-
}
|
|
938
|
-
|
|
939
|
-
if use_scroll:
|
|
940
|
-
params.update(
|
|
941
|
-
{
|
|
942
|
-
"scroll": "2m",
|
|
943
|
-
"size": PAGE_SIZE,
|
|
944
|
-
}
|
|
945
|
-
)
|
|
946
|
-
|
|
947
|
-
# The scroll API doesn't support offset, manually skip the first results
|
|
948
|
-
skip = self.start
|
|
949
|
-
|
|
950
|
-
# Send to Elasticsearch
|
|
951
|
-
page = self._backend_do_search(body, **params)
|
|
952
|
-
|
|
953
|
-
while True:
|
|
954
|
-
hits = page["hits"]["hits"]
|
|
955
|
-
|
|
956
|
-
if len(hits) == 0:
|
|
957
|
-
break
|
|
958
|
-
|
|
959
|
-
# Get results
|
|
960
|
-
if skip < len(hits):
|
|
961
|
-
for result in self._get_results_from_hits(hits):
|
|
962
|
-
if limit is not None and limit == 0:
|
|
963
|
-
break
|
|
964
|
-
|
|
965
|
-
if skip == 0:
|
|
966
|
-
yield result
|
|
967
|
-
|
|
968
|
-
if limit is not None:
|
|
969
|
-
limit -= 1
|
|
970
|
-
else:
|
|
971
|
-
skip -= 1
|
|
972
|
-
|
|
973
|
-
if limit is not None and limit == 0:
|
|
974
|
-
break
|
|
975
|
-
else:
|
|
976
|
-
# Skip whole page
|
|
977
|
-
skip -= len(hits)
|
|
978
|
-
|
|
979
|
-
# Fetch next page of results
|
|
980
|
-
if "_scroll_id" not in page:
|
|
981
|
-
break
|
|
982
|
-
|
|
983
|
-
page = self.backend.es.scroll(scroll_id=page["_scroll_id"], scroll="2m")
|
|
984
|
-
|
|
985
|
-
# Clear the scroll
|
|
986
|
-
if "_scroll_id" in page:
|
|
987
|
-
self.backend.es.clear_scroll(scroll_id=page["_scroll_id"])
|
|
988
|
-
else:
|
|
989
|
-
params.update(
|
|
990
|
-
{
|
|
991
|
-
"from_": self.start,
|
|
992
|
-
"size": limit or PAGE_SIZE,
|
|
993
|
-
}
|
|
994
|
-
)
|
|
995
|
-
|
|
996
|
-
# Send to Elasticsearch
|
|
997
|
-
hits = self._backend_do_search(body, **params)["hits"]["hits"]
|
|
998
|
-
|
|
999
|
-
# Get results
|
|
1000
|
-
for result in self._get_results_from_hits(hits):
|
|
1001
|
-
yield result
|
|
1002
|
-
|
|
1003
|
-
def _do_count(self):
|
|
1004
|
-
# Get count
|
|
1005
|
-
hit_count = self.backend.es.count(
|
|
1006
|
-
index=self.backend.get_index_for_model(
|
|
1007
|
-
self.query_compiler.queryset.model
|
|
1008
|
-
).name,
|
|
1009
|
-
body=self._get_es_body(for_count=True),
|
|
1010
|
-
)["count"]
|
|
1011
|
-
|
|
1012
|
-
# Add limits
|
|
1013
|
-
hit_count -= self.start
|
|
1014
|
-
if self.stop is not None:
|
|
1015
|
-
hit_count = min(hit_count, self.stop - self.start)
|
|
1016
|
-
|
|
1017
|
-
return max(hit_count, 0)
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
class ElasticsearchAutocompleteQueryCompilerImpl:
|
|
1021
|
-
def __init__(self, *args, **kwargs):
|
|
1022
|
-
super().__init__(*args, **kwargs)
|
|
1023
|
-
|
|
1024
|
-
# Convert field names into index column names
|
|
1025
|
-
# Note: this overrides Elasticsearch7SearchQueryCompiler by using autocomplete fields instead of searchable fields
|
|
1026
|
-
if self.fields:
|
|
1027
|
-
fields = []
|
|
1028
|
-
autocomplete_fields = {
|
|
1029
|
-
f.field_name: f
|
|
1030
|
-
for f in self.queryset.model.get_autocomplete_search_fields()
|
|
1031
|
-
}
|
|
1032
|
-
for field_name in self.fields:
|
|
1033
|
-
if field_name in autocomplete_fields:
|
|
1034
|
-
field_name = self.mapping.get_field_column_name(
|
|
1035
|
-
autocomplete_fields[field_name]
|
|
1036
|
-
)
|
|
1037
11
|
|
|
1038
|
-
|
|
12
|
+
from wagtail.search.backends.deprecation import (
|
|
13
|
+
IndexOptionMixin,
|
|
14
|
+
LegacyContentTypeMatchMixin,
|
|
15
|
+
)
|
|
1039
16
|
|
|
1040
|
-
self.remapped_fields = fields
|
|
1041
|
-
else:
|
|
1042
|
-
self.remapped_fields = None
|
|
1043
17
|
|
|
1044
|
-
|
|
1045
|
-
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
# No fields. Return a query that'll match nothing
|
|
1049
|
-
return {"bool": {"mustNot": {"match_all": {}}}}
|
|
1050
|
-
elif isinstance(self.query, PlainText):
|
|
1051
|
-
return self._compile_plaintext_query(self.query, fields)
|
|
1052
|
-
elif isinstance(self.query, MatchAll):
|
|
1053
|
-
return {"match_all": {}}
|
|
1054
|
-
else:
|
|
1055
|
-
raise NotImplementedError(
|
|
1056
|
-
"`%s` is not supported for autocomplete queries."
|
|
1057
|
-
% self.query.__class__.__name__
|
|
1058
|
-
)
|
|
18
|
+
class Elasticsearch7SearchQueryCompiler(
|
|
19
|
+
LegacyContentTypeMatchMixin, _Elasticsearch7SearchQueryCompiler
|
|
20
|
+
):
|
|
21
|
+
pass
|
|
1059
22
|
|
|
1060
23
|
|
|
1061
24
|
class Elasticsearch7AutocompleteQueryCompiler(
|
|
1062
|
-
|
|
25
|
+
LegacyContentTypeMatchMixin, _Elasticsearch7AutocompleteQueryCompiler
|
|
1063
26
|
):
|
|
1064
27
|
pass
|
|
1065
28
|
|
|
1066
29
|
|
|
1067
|
-
class
|
|
1068
|
-
def __init__(self, index):
|
|
1069
|
-
self.index = index
|
|
1070
|
-
|
|
1071
|
-
def reset_index(self):
|
|
1072
|
-
self.index.reset()
|
|
1073
|
-
|
|
1074
|
-
def start(self):
|
|
1075
|
-
# Reset the index
|
|
1076
|
-
self.reset_index()
|
|
1077
|
-
|
|
1078
|
-
return self.index
|
|
1079
|
-
|
|
1080
|
-
def finish(self):
|
|
1081
|
-
self.index.refresh()
|
|
1082
|
-
|
|
1083
|
-
|
|
1084
|
-
class ElasticsearchAtomicIndexRebuilder(ElasticsearchIndexRebuilder):
|
|
1085
|
-
def __init__(self, index):
|
|
1086
|
-
self.alias = index
|
|
1087
|
-
self.index = index.backend.index_class(
|
|
1088
|
-
index.backend, self.alias.name + "_" + get_random_string(7).lower()
|
|
1089
|
-
)
|
|
1090
|
-
|
|
1091
|
-
def reset_index(self):
|
|
1092
|
-
# Delete old index using the alias
|
|
1093
|
-
# This should delete both the alias and the index
|
|
1094
|
-
self.alias.delete()
|
|
1095
|
-
|
|
1096
|
-
# Create new index
|
|
1097
|
-
self.index.put()
|
|
1098
|
-
|
|
1099
|
-
# Create a new alias
|
|
1100
|
-
self.index.put_alias(self.alias.name)
|
|
1101
|
-
|
|
1102
|
-
def start(self):
|
|
1103
|
-
# Create the new index
|
|
1104
|
-
self.index.put()
|
|
1105
|
-
|
|
1106
|
-
return self.index
|
|
1107
|
-
|
|
1108
|
-
def finish(self):
|
|
1109
|
-
self.index.refresh()
|
|
1110
|
-
|
|
1111
|
-
if self.alias.is_alias():
|
|
1112
|
-
# Update existing alias, then delete the old index
|
|
1113
|
-
|
|
1114
|
-
# Find index that alias currently points to, we'll delete it after
|
|
1115
|
-
# updating the alias
|
|
1116
|
-
old_index = self.alias.aliased_indices()
|
|
1117
|
-
|
|
1118
|
-
# Update alias to point to new index
|
|
1119
|
-
self.index.put_alias(self.alias.name)
|
|
1120
|
-
|
|
1121
|
-
# Delete old index
|
|
1122
|
-
# aliased_indices() can return multiple indices. Delete them all
|
|
1123
|
-
for index in old_index:
|
|
1124
|
-
if index.name != self.index.name:
|
|
1125
|
-
index.delete()
|
|
1126
|
-
|
|
1127
|
-
else:
|
|
1128
|
-
# self.alias doesn't currently refer to an alias in Elasticsearch.
|
|
1129
|
-
# This means that either nothing exists in ES with that name or
|
|
1130
|
-
# there is currently an index with the that name
|
|
1131
|
-
|
|
1132
|
-
# Run delete on the alias, just in case it is currently an index.
|
|
1133
|
-
# This happens on the first rebuild after switching ATOMIC_REBUILD on
|
|
1134
|
-
self.alias.delete()
|
|
1135
|
-
|
|
1136
|
-
# Create the alias
|
|
1137
|
-
self.index.put_alias(self.alias.name)
|
|
1138
|
-
|
|
1139
|
-
|
|
1140
|
-
class Elasticsearch7SearchBackend(BaseSearchBackend):
|
|
1141
|
-
mapping_class = Elasticsearch7Mapping
|
|
1142
|
-
index_class = Elasticsearch7Index
|
|
30
|
+
class Elasticsearch7SearchBackend(IndexOptionMixin, _Elasticsearch7SearchBackend):
|
|
1143
31
|
query_compiler_class = Elasticsearch7SearchQueryCompiler
|
|
1144
32
|
autocomplete_query_compiler_class = Elasticsearch7AutocompleteQueryCompiler
|
|
1145
|
-
results_class = Elasticsearch7SearchResults
|
|
1146
|
-
basic_rebuilder_class = ElasticsearchIndexRebuilder
|
|
1147
|
-
atomic_rebuilder_class = ElasticsearchAtomicIndexRebuilder
|
|
1148
|
-
catch_indexing_errors = True
|
|
1149
|
-
timeout_kwarg_name = "timeout"
|
|
1150
|
-
|
|
1151
|
-
settings = {
|
|
1152
|
-
"settings": {
|
|
1153
|
-
"analysis": {
|
|
1154
|
-
"analyzer": {
|
|
1155
|
-
"ngram_analyzer": {
|
|
1156
|
-
"type": "custom",
|
|
1157
|
-
"tokenizer": "standard",
|
|
1158
|
-
"filter": ["asciifolding", "lowercase", "ngram"],
|
|
1159
|
-
},
|
|
1160
|
-
"edgengram_analyzer": {
|
|
1161
|
-
"type": "custom",
|
|
1162
|
-
"tokenizer": "standard",
|
|
1163
|
-
"filter": ["asciifolding", "lowercase", "edgengram"],
|
|
1164
|
-
},
|
|
1165
|
-
},
|
|
1166
|
-
"tokenizer": {
|
|
1167
|
-
"ngram_tokenizer": {
|
|
1168
|
-
"type": "ngram",
|
|
1169
|
-
"min_gram": 3,
|
|
1170
|
-
"max_gram": 15,
|
|
1171
|
-
},
|
|
1172
|
-
"edgengram_tokenizer": {
|
|
1173
|
-
"type": "edge_ngram",
|
|
1174
|
-
"min_gram": 2,
|
|
1175
|
-
"max_gram": 15,
|
|
1176
|
-
"side": "front",
|
|
1177
|
-
},
|
|
1178
|
-
},
|
|
1179
|
-
"filter": {
|
|
1180
|
-
"ngram": {"type": "ngram", "min_gram": 3, "max_gram": 15},
|
|
1181
|
-
"edgengram": {"type": "edge_ngram", "min_gram": 1, "max_gram": 15},
|
|
1182
|
-
},
|
|
1183
|
-
},
|
|
1184
|
-
"index": {
|
|
1185
|
-
"max_ngram_diff": 12,
|
|
1186
|
-
},
|
|
1187
|
-
}
|
|
1188
|
-
}
|
|
1189
|
-
|
|
1190
|
-
def _get_host_config_from_url(self, url):
|
|
1191
|
-
"""Given a parsed URL, return the host configuration to be added to self.hosts"""
|
|
1192
|
-
use_ssl = url.scheme == "https"
|
|
1193
|
-
port = url.port or (443 if use_ssl else 80)
|
|
1194
|
-
|
|
1195
|
-
http_auth = None
|
|
1196
|
-
if url.username is not None and url.password is not None:
|
|
1197
|
-
http_auth = (url.username, url.password)
|
|
1198
|
-
|
|
1199
|
-
return {
|
|
1200
|
-
"host": url.hostname,
|
|
1201
|
-
"port": port,
|
|
1202
|
-
"url_prefix": url.path,
|
|
1203
|
-
"use_ssl": use_ssl,
|
|
1204
|
-
"verify_certs": use_ssl,
|
|
1205
|
-
"http_auth": http_auth,
|
|
1206
|
-
}
|
|
1207
|
-
|
|
1208
|
-
def _get_options_from_host_urls(self, urls):
|
|
1209
|
-
"""Given a list of parsed URLs, return a dict of additional options to be passed into the
|
|
1210
|
-
Elasticsearch constructor; necessary for options that aren't valid as part of the 'hosts' config
|
|
1211
|
-
"""
|
|
1212
|
-
return {}
|
|
1213
|
-
|
|
1214
|
-
def __init__(self, params):
|
|
1215
|
-
super().__init__(params)
|
|
1216
|
-
|
|
1217
|
-
# Get settings
|
|
1218
|
-
self.hosts = params.pop("HOSTS", None)
|
|
1219
|
-
self.index_name = params.pop("INDEX", "wagtail")
|
|
1220
|
-
self.timeout = params.pop("TIMEOUT", 10)
|
|
1221
|
-
|
|
1222
|
-
if params.pop("ATOMIC_REBUILD", False):
|
|
1223
|
-
self.rebuilder_class = self.atomic_rebuilder_class
|
|
1224
|
-
else:
|
|
1225
|
-
self.rebuilder_class = self.basic_rebuilder_class
|
|
1226
|
-
|
|
1227
|
-
self.settings = deepcopy(
|
|
1228
|
-
self.settings
|
|
1229
|
-
) # Make the class settings attribute as instance settings attribute
|
|
1230
|
-
self.settings = deep_update(self.settings, params.pop("INDEX_SETTINGS", {}))
|
|
1231
|
-
|
|
1232
|
-
# Get Elasticsearch interface
|
|
1233
|
-
# Any remaining params are passed into the Elasticsearch constructor
|
|
1234
|
-
options = params.pop("OPTIONS", {})
|
|
1235
|
-
|
|
1236
|
-
# If HOSTS is not set, convert URLS setting to HOSTS
|
|
1237
|
-
if self.hosts is None:
|
|
1238
|
-
es_urls = params.pop("URLS", ["http://localhost:9200"])
|
|
1239
|
-
# if es_urls is not a list, convert it to a list
|
|
1240
|
-
if isinstance(es_urls, str):
|
|
1241
|
-
es_urls = [es_urls]
|
|
1242
|
-
|
|
1243
|
-
parsed_urls = [urlparse(url) for url in es_urls]
|
|
1244
|
-
|
|
1245
|
-
self.hosts = [self._get_host_config_from_url(url) for url in parsed_urls]
|
|
1246
|
-
options.update(self._get_options_from_host_urls(parsed_urls))
|
|
1247
|
-
|
|
1248
|
-
options[self.timeout_kwarg_name] = self.timeout
|
|
1249
|
-
|
|
1250
|
-
self.es = Elasticsearch(hosts=self.hosts, **options)
|
|
1251
|
-
|
|
1252
|
-
def get_index_for_model(self, model):
|
|
1253
|
-
# Split models up into separate indices based on their root model.
|
|
1254
|
-
# For example, all page-derived models get put together in one index,
|
|
1255
|
-
# while images and documents each have their own index.
|
|
1256
|
-
root_model = get_model_root(model)
|
|
1257
|
-
index_suffix = (
|
|
1258
|
-
"__"
|
|
1259
|
-
+ root_model._meta.app_label.lower()
|
|
1260
|
-
+ "_"
|
|
1261
|
-
+ root_model.__name__.lower()
|
|
1262
|
-
)
|
|
1263
|
-
|
|
1264
|
-
return self.index_class(self, self.index_name + index_suffix)
|
|
1265
|
-
|
|
1266
|
-
def get_index(self):
|
|
1267
|
-
return self.index_class(self, self.index_name)
|
|
1268
|
-
|
|
1269
|
-
def get_rebuilder(self):
|
|
1270
|
-
return self.rebuilder_class(self.get_index())
|
|
1271
|
-
|
|
1272
|
-
def reset_index(self):
|
|
1273
|
-
# Use the rebuilder to reset the index
|
|
1274
|
-
self.get_rebuilder().reset_index()
|
|
1275
33
|
|
|
1276
34
|
|
|
1277
35
|
SearchBackend = Elasticsearch7SearchBackend
|