From e624e608b410bcb1f95741ad80c31bb82fb00c11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jeremy=20Lain=C3=A9?= Date: Sat, 26 Apr 2025 15:50:12 +0200 Subject: [PATCH] Switch from elasticsearch-py to opensearch-py Starting with version 7.14.0, the Elasticsearch Python client library checks that the server is an "official" Elasticsearch, i.e. that it is not OpenSearch. On the other hand, the OpenSearch client library will happily work against both Elasticsearch and OpenSearch. --- Makefile | 2 +- README.rst | 2 +- docs/index.rst | 8 ++++---- docs/indexers.rst | 8 ++++---- docs/tasks.rst | 6 +++--- pyproject.toml | 4 ++-- src/rest_search/__init__.py | 6 +++--- src/rest_search/decorators.py | 2 +- src/rest_search/forms.py | 6 +++--- src/rest_search/indexers.py | 12 ++++++------ src/rest_search/middleware.py | 2 +- src/rest_search/tasks.py | 20 ++++++++++---------- src/rest_search/views.py | 4 ++-- tests/test_connection.py | 20 ++++++++++---------- tests/test_indexers.py | 2 +- tests/test_tasks.py | 10 +++++----- tests/test_views.py | 6 +++--- 17 files changed, 60 insertions(+), 60 deletions(-) diff --git a/Makefile b/Makefile index 3bec112..8adeab3 100644 --- a/Makefile +++ b/Makefile @@ -11,6 +11,6 @@ lint: test: coverage erase - coverage run `which django-admin` test + coverage run `which django-admin` test -v 1 coverage report coverage xml diff --git a/README.rst b/README.rst index 453ec36..48e608c 100644 --- a/README.rst +++ b/README.rst @@ -16,7 +16,7 @@ Django REST Search :target: https://pypi.python.org/pypi/djangorestsearch Django REST Search provides a set of classes to facilitate the integration of -ElasticSearch into applications powered by the Django REST Framework. +OpenSearch into applications powered by the Django REST Framework. Full documentation for the project is available at http://django-rest-search.readthedocs.io/. diff --git a/docs/index.rst b/docs/index.rst index 8d572ce..4e44824 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -2,7 +2,7 @@ Django REST Search ================== Django REST Search provides a set of classes to facilitate the integration of -ElasticSearch [1]_ into applications powered by the Django REST Framework [2]_. +OpenSearch [1]_ into applications powered by the Django REST Framework [2]_. Installation ------------ @@ -22,7 +22,7 @@ Add `'rest_search'` to your `INSTALLED_APPS` setting. 'rest_search', ] -Add the middleware to flush ElasticSearch updates to a celery task. +Add the middleware to flush OpenSearch updates to a celery task. .. code-block:: python @@ -31,7 +31,7 @@ Add the middleware to flush ElasticSearch updates to a celery task. 'rest_search.middleware.FlushUpdatesMiddleware', ] -Configure your ElasticSearch connection. +Configure your OpenSearch connection. .. code-block:: python @@ -68,6 +68,6 @@ Contents indexers tasks -.. [1] https://www.elastic.co/products/elasticsearch +.. [1] https://opensearch.org/ .. [2] http://www.django-rest-framework.org/ diff --git a/docs/indexers.rst b/docs/indexers.rst index 067d250..74d1618 100644 --- a/docs/indexers.rst +++ b/docs/indexers.rst @@ -2,11 +2,11 @@ Indexers ======== Indexers provide the binding between the data in the ORM and the data which -is indexed in ElasticSearch. +is indexed in OpenSearch. To perform data serialization, indexers build upon the REST framework's serializers, which allow great flexibility in how you map the data in the ORM -and the JSON representation which is sent to ElasticSearch. +and the JSON representation which is sent to OpenSearch. Declaring indexers ------------------ @@ -45,13 +45,13 @@ Options When declaring an indexer, you can specify some additional properties: -- `index`: the name of the ElasticSearch index, defaults to the model name in lowercase +- `index`: the name of the OpenSearch index, defaults to the model name in lowercase Index updates ------------- When you register an indexer, it will install signal handlers for save and -delete events and queue updates to the ElasticSearch index. +delete events and queue updates to the OpenSearch index. For these updates to actually be performed, either install the ```rest_search.middleware.FlushUpdatesMiddleware``` middleware or wrap your diff --git a/docs/tasks.rst b/docs/tasks.rst index 2342516..9585a25 100644 --- a/docs/tasks.rst +++ b/docs/tasks.rst @@ -2,17 +2,17 @@ Celery Tasks ============ Django REST Search relies on the Celery [1]_ distributed task queue to -perform updates to the ElasticSearch index. +perform updates to the OpenSearch index. There are two tasks: - ```rest_search.tasks.update_index``` performs a full update of the - ElasticSearch index by iterating over all the items for which an + OpenSearch index by iterating over all the items for which an indexer has been defined. You will need to perform this at least once when you start using Django REST Search or whenever you add an indexer. - ```rest_search.tasks.patch_index``` performs a partial update of the - ElasticSearch index for specific items. You usually do not need to invoke + OpenSearch index for specific items. You usually do not need to invoke this yourself, as the indexer sets the required signal handlers when it is registered. diff --git a/pyproject.toml b/pyproject.toml index f17ae44..9e70682 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "djangorestsearch" -description = "ElasticSearch integration for Django" +description = "OpenSearch integration for Django" readme = "README.rst" requires-python = ">=3.9" license = "BSD-2-Clause" @@ -25,7 +25,7 @@ classifiers = [ ] dependencies = [ "aws-requests-auth>=0.3.0", - "elasticsearch>=7.0.0,<8.0.0", + "opensearch-py>=2.0.0,<3.0.0", ] dynamic = ["version"] diff --git a/src/rest_search/__init__.py b/src/rest_search/__init__.py index b1cf5a9..6284931 100644 --- a/src/rest_search/__init__.py +++ b/src/rest_search/__init__.py @@ -4,7 +4,7 @@ from aws_requests_auth.aws_auth import AWSRequestsAuth from django.conf import settings -from elasticsearch import Elasticsearch, RequestsHttpConnection +from opensearchpy import OpenSearch, RequestsHttpConnection __version__ = "0.12.0" @@ -57,10 +57,10 @@ def __create_connection(self, config): aws_service="es", ) - return Elasticsearch(**kwargs) + return OpenSearch(**kwargs) -def get_elasticsearch(indexer): +def get_opensearch(indexer): return connections["default"] diff --git a/src/rest_search/decorators.py b/src/rest_search/decorators.py index 7e9dea7..8d7687d 100644 --- a/src/rest_search/decorators.py +++ b/src/rest_search/decorators.py @@ -7,7 +7,7 @@ def flush_updates(function): """ - Decorator that flushes ElasticSearch updates. + Decorator that flushes OpenSearch updates. """ @functools.wraps(function) diff --git a/src/rest_search/forms.py b/src/rest_search/forms.py index 2a6aef1..e00e203 100644 --- a/src/rest_search/forms.py +++ b/src/rest_search/forms.py @@ -5,12 +5,12 @@ class SearchMixin(object): """ - Mixin to help build ElasticSearch queries. + Mixin to help build OpenSearch queries. """ def get_query(self): """ - Returns the query to be executed by ElasticSearch. + Returns the query to be executed by OpenSearch. """ return self._score_query(self._build_query()) @@ -45,7 +45,7 @@ def _score_query(self, query): class SearchForm(SearchMixin, forms.Form): """ - Base form for building ElasticSearch queries. + Base form for building OpenSearch queries. """ def __init__(self, data, context={}): diff --git a/src/rest_search/indexers.py b/src/rest_search/indexers.py index f360045..9f8c543 100644 --- a/src/rest_search/indexers.py +++ b/src/rest_search/indexers.py @@ -4,9 +4,9 @@ from django.db import models from django.db.models.signals import post_delete, post_save -from elasticsearch.helpers import scan +from opensearchpy.helpers import scan -from rest_search import get_elasticsearch +from rest_search import get_opensearch _REGISTERED_CLASSES = [] @@ -27,7 +27,7 @@ def __init__(self): # Make a note of the field name. self.pk_name = primary_key.name - # Determine how an `_id` from ElasticSearch is parsed to + # Determine how an `_id` from OpenSearch is parsed to # a primary key value. if isinstance(primary_key, models.UUIDField): self.pk_from_string = uuid.UUID @@ -50,11 +50,11 @@ def map_result_item(x): return map(map_result_item, results) def scan(self, **kwargs): - es = get_elasticsearch(self) + es = get_opensearch(self) return scan(es, index=self.index, **kwargs) def search(self, **kwargs): - es = get_elasticsearch(self) + es = get_opensearch(self) return es.search(index=self.index, **kwargs) @@ -67,7 +67,7 @@ def _get_registered(): def _instance_changed(sender, instance, **kwargs): """ - Queues an update to the ElasticSearch index. + Queues an update to the OpenSearch index. """ from rest_search import queue_add diff --git a/src/rest_search/middleware.py b/src/rest_search/middleware.py index a254ca1..43a0a15 100644 --- a/src/rest_search/middleware.py +++ b/src/rest_search/middleware.py @@ -7,7 +7,7 @@ class FlushUpdatesMiddleware(MiddlewareMixin): """ - Middleware that flushes ElasticSearch updates. + Middleware that flushes OpenSearch updates. """ def process_response(self, request, response): diff --git a/src/rest_search/tasks.py b/src/rest_search/tasks.py index 45d5d21..e1aa949 100644 --- a/src/rest_search/tasks.py +++ b/src/rest_search/tasks.py @@ -4,9 +4,9 @@ from celery import shared_task from django.conf import settings -from elasticsearch.helpers import bulk +from opensearchpy.helpers import bulk -from rest_search import DEFAULT_INDEX_SETTINGS, get_elasticsearch +from rest_search import DEFAULT_INDEX_SETTINGS, get_opensearch from rest_search.indexers import _get_registered logger = logging.getLogger("rest_search") @@ -14,7 +14,7 @@ def create_index(): """ - Creates the ElasticSearch indices if they do not exist. + Creates the OpenSearch indices if they do not exist. """ for indexer in _get_registered(): _create_index(indexer) @@ -22,17 +22,17 @@ def create_index(): def delete_index(): """ - Deletes the ElasticSearch indices. + Deletes the OpenSearch indices. """ for indexer in _get_registered(): - es = get_elasticsearch(indexer) + es = get_opensearch(indexer) es.indices.delete(index=indexer.index, ignore=404) @shared_task def patch_index(updates): """ - Performs a partial update of the ElasticSearch indices. + Performs a partial update of the OpenSearch indices. Primary keys are received as strings. """ @@ -51,7 +51,7 @@ def patch_index(updates): @shared_task def update_index(remove=True): """ - Performs a full update of the ElasticSearch indices. + Performs a full update of the OpenSearch indices. """ logger.info("Updating indices") @@ -71,14 +71,14 @@ def _create_index(indexer): if indexer.mappings is not None: body["mappings"] = indexer.mappings - es = get_elasticsearch(indexer) + es = get_opensearch(indexer) if not es.indices.exists(indexer.index): logger.info("Creating index %s" % indexer.index) es.indices.create(index=indexer.index, body=body) def _delete_items(indexer, pks): - es = get_elasticsearch(indexer) + es = get_opensearch(indexer) def mapper(pk): return { @@ -94,7 +94,7 @@ def _index_items(indexer, pks): """ Primary keys are manipulated in their native type (int, UUID). """ - es = get_elasticsearch(indexer) + es = get_opensearch(indexer) seen_pks = set() def bulk_mapper(block_size=1000): diff --git a/src/rest_search/views.py b/src/rest_search/views.py index f7721f8..4ac33d3 100644 --- a/src/rest_search/views.py +++ b/src/rest_search/views.py @@ -39,7 +39,7 @@ def get(self, request, *args, **kwargs): if sort: body["sort"] = sort - # execute elasticsearch query + # execute opensearch query indexer = self.get_indexer() res = indexer.search(body=body) @@ -53,7 +53,7 @@ def get_indexer(self): def get_query(self): """ - Returns the 'query' element of the ElasticSearch request body. + Returns the 'query' element of the OpenSearch request body. """ form = self.form_class(self.request.GET, context={"request": self.request}) if not form.is_valid(): diff --git a/tests/test_connection.py b/tests/test_connection.py index feaa288..ea0c0d7 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -21,25 +21,25 @@ def tearDown(self): } } ) - @patch("rest_search.Elasticsearch") - def test_aws_auth(self, mock_elasticsearch): + @patch("rest_search.OpenSearch") + def test_aws_auth(self, mock_opensearch): es = connections["default"] self.assertIsNotNone(es) - self.assertEqual(mock_elasticsearch.call_count, 1) - self.assertEqual(mock_elasticsearch.call_args[0], ()) + self.assertEqual(mock_opensearch.call_count, 1) + self.assertEqual(mock_opensearch.call_args[0], ()) self.assertEqual( - sorted(mock_elasticsearch.call_args[1].keys()), + sorted(mock_opensearch.call_args[1].keys()), ["connection_class", "host", "http_auth", "port", "use_ssl"], ) - @patch("rest_search.Elasticsearch") - def test_no_auth(self, mock_elasticsearch): + @patch("rest_search.OpenSearch") + def test_no_auth(self, mock_opensearch): es = connections["default"] self.assertIsNotNone(es) - self.assertEqual(mock_elasticsearch.call_count, 1) - self.assertEqual(mock_elasticsearch.call_args[0], ()) + self.assertEqual(mock_opensearch.call_count, 1) + self.assertEqual(mock_opensearch.call_args[0], ()) self.assertEqual( - sorted(mock_elasticsearch.call_args[1].keys()), ["host", "port", "use_ssl"] + sorted(mock_opensearch.call_args[1].keys()), ["host", "port", "use_ssl"] ) diff --git a/tests/test_indexers.py b/tests/test_indexers.py index a56d39f..2809631 100644 --- a/tests/test_indexers.py +++ b/tests/test_indexers.py @@ -57,7 +57,7 @@ def test_scan(self, mock_scan): indexer = BookIndexer() indexer.scan(body={"query": {"match_all": {}}}) - @patch("elasticsearch.client.Elasticsearch.search") + @patch("opensearchpy.client.OpenSearch.search") def test_search(self, mock_search): mock_search.return_value = { "_shards": {"failed": 0, "successful": 5, "total": 5}, diff --git a/tests/test_tasks.py b/tests/test_tasks.py index fcb0d69..9c78b8b 100644 --- a/tests/test_tasks.py +++ b/tests/test_tasks.py @@ -31,8 +31,8 @@ def create_books(self): book = Book.objects.create(id=1, title="Some book") book.tags.set([Tag.objects.create(slug=slug) for slug in ["foo", "bar"]]) - @patch("elasticsearch.client.indices.IndicesClient.exists") - @patch("elasticsearch.client.indices.IndicesClient.create") + @patch("opensearchpy.client.indices.IndicesClient.exists") + @patch("opensearchpy.client.indices.IndicesClient.create") def test_create_index(self, mock_create, mock_exists): mock_exists.return_value = False @@ -83,8 +83,8 @@ def test_create_index(self, mock_create, mock_exists): ], ) - @patch("elasticsearch.client.indices.IndicesClient.exists") - @patch("elasticsearch.client.indices.IndicesClient.create") + @patch("opensearchpy.client.indices.IndicesClient.exists") + @patch("opensearchpy.client.indices.IndicesClient.create") def test_create_index_exists(self, mock_create, mock_exists): mock_exists.return_value = True @@ -99,7 +99,7 @@ def test_create_index_exists(self, mock_create, mock_exists): ) mock_create.assert_not_called() - @patch("elasticsearch.client.indices.IndicesClient.delete") + @patch("opensearchpy.client.indices.IndicesClient.delete") def test_delete_index(self, mock_delete): delete_index() diff --git a/tests/test_views.py b/tests/test_views.py index 6a21910..128669f 100644 --- a/tests/test_views.py +++ b/tests/test_views.py @@ -11,7 +11,7 @@ def test_create(self, mock_delay): response = self.client.post("/books", {"title": "New book"}) self.assertEqual(response.status_code, 201) - @patch("elasticsearch.client.Elasticsearch.search") + @patch("opensearchpy.client.OpenSearch.search") def test_search(self, mock_search): mock_search.return_value = { "_shards": {"failed": 0, "successful": 5, "total": 5}, @@ -62,7 +62,7 @@ def test_search_invalid(self): self.assertEqual(response.status_code, 400) self.assertEqual(response.data, {"id": ["Enter a whole number."]}) - @patch("elasticsearch.client.Elasticsearch.search") + @patch("opensearchpy.client.OpenSearch.search") def test_search_pagination(self, mock_search): mock_search.return_value = { "_shards": {"failed": 0, "successful": 5, "total": 5}, @@ -99,7 +99,7 @@ def test_search_pagination(self, mock_search): index="book", ) - @patch("elasticsearch.client.Elasticsearch.search") + @patch("opensearchpy.client.OpenSearch.search") def test_search_sorted(self, mock_search): mock_search.return_value = { "_shards": {"failed": 0, "successful": 5, "total": 5},