Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore: add ELASTIC_SEARCH_INDEX_PREFIX setting to prefix indices #130

Merged
merged 1 commit into from
Apr 10, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,15 @@ where

3. `search` - the operation to find matching documents within the index. `doc_type` is supported as an optional keyword parameter to return results only with a certain doc_type

## Configuring for multi-tenancy

The modules exposes a setting `ELASTIC_SEARCH_INDEX_PREFIX` to enable so that the indices for multiple clients do not collide.

```python
SearchEngine(index_name="test")
```

When invoked, this line will create an index named `test` on Elastic Search. Setting `ELASTIC_SEARCH_INDEX_PREFIX="client1_"` will instead create the index `client1_test`.

## Index documents
Index documents are passed to the search application as python dictionaries, along with a `doc_type` document type, which is also optionally supported as a way to return only certain document types from a search.
Expand Down
2 changes: 1 addition & 1 deletion edxsearch/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
""" Container module for testing / demoing search """

__version__ = '3.4.0'
__version__ = '3.5.0'
40 changes: 26 additions & 14 deletions search/elastic.py
Original file line number Diff line number Diff line change
Expand Up @@ -304,16 +304,16 @@ def mappings(self):
we'll load them again from Elasticsearch
"""
# Try loading the mapping from the cache.
mapping = ElasticSearchEngine.get_mappings(self.index_name)
mapping = ElasticSearchEngine.get_mappings(self._prefixed_index_name)

# Fall back to Elasticsearch
if not mapping:
mapping = self._es.indices.get_mapping(
index=self.index_name
).get(self.index_name, {}).get("mappings", {})
index=self._prefixed_index_name
).get(self._prefixed_index_name, {}).get("mappings", {})
# Cache the mapping, if one was retrieved
if mapping:
ElasticSearchEngine.set_mappings(self.index_name, mapping)
ElasticSearchEngine.set_mappings(self._prefixed_index_name, mapping)

return mapping

Expand All @@ -323,14 +323,27 @@ def _clear_mapping(self):

Next time ES mappings is are requested.
"""
ElasticSearchEngine.set_mappings(self.index_name, {})
ElasticSearchEngine.set_mappings(self._prefixed_index_name, {})

def __init__(self, index=None):
super().__init__(index)
es_config = getattr(settings, "ELASTIC_SEARCH_CONFIG", [{}])
self._es = getattr(settings, "ELASTIC_SEARCH_IMPL", Elasticsearch)(es_config)
if not self._es.indices.exists(index=self.index_name):
self._es.indices.create(index=self.index_name)
params = None

if not self._es.indices.exists(index=self._prefixed_index_name):
self._es.indices.create(index=self._prefixed_index_name, params=params)

@property
def _prefixed_index_name(self):
"""
Property that returns the defined index_name with the configured
prefix.

To be used anywhere the index_name is required.
"""
prefix = getattr(settings, "ELASTIC_SEARCH_INDEX_PREFIX", "")
return prefix + self.index_name

def _check_mappings(self, body):
"""
Expand Down Expand Up @@ -396,9 +409,10 @@ def field_property(field_name, field_value):
}

if new_properties:

self._es.indices.put_mapping(
index=self.index_name,
body={"properties": new_properties}
index=self._prefixed_index_name,
body={"properties": new_properties},
)
self._clear_mapping()

Expand All @@ -417,7 +431,7 @@ def index(self, sources, **kwargs):
id_ = source.get("id")
log.debug("indexing object with id %s", id_)
action = {
"_index": self.index_name,
"_index": self._prefixed_index_name,
"_id": id_,
"_source": source
}
Expand All @@ -437,14 +451,13 @@ def remove(self, doc_ids, **kwargs):
"""
Implements call to remove the documents from the index
"""

try:
actions = []
for doc_id in doc_ids:
log.debug("Removing document with id %s", doc_id)
action = {
"_op_type": "delete",
"_index": self.index_name,
"_index": self._prefixed_index_name,
"_id": doc_id
}
actions.append(action)
Expand Down Expand Up @@ -568,7 +581,6 @@ def search(self,
"""

log.debug("searching index with %s", query_string)

elastic_queries = []
elastic_filters = []

Expand Down Expand Up @@ -642,7 +654,7 @@ def search(self,
body["aggs"] = _process_aggregation_terms(aggregation_terms)

try:
es_response = self._es.search(index=self.index_name, body=body, **kwargs)
es_response = self._es.search(index=self._prefixed_index_name, body=body, **kwargs)
except exceptions.ElasticsearchException as ex:
log.exception("error while searching index - %r", ex)
raise
Expand Down
27 changes: 22 additions & 5 deletions search/tests/test_engines.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,35 @@
import json
import os
from datetime import datetime

from unittest.mock import patch

from django.test import TestCase
from django.test.utils import override_settings
from elasticsearch import exceptions
from elasticsearch.helpers import BulkIndexError

from search.api import perform_search, NoSearchEngineError
from search.api import NoSearchEngineError, perform_search
from search.elastic import RESERVED_CHARACTERS
from search.tests.mock_search_engine import MockSearchEngine, json_date_to_datetime
from search.tests.mock_search_engine import (MockSearchEngine,
json_date_to_datetime)
from search.tests.tests import MockSearchTests
from search.tests.utils import ErroringElasticImpl, SearcherMixin
from search.tests.utils import (TEST_INDEX_NAME, ErroringElasticImpl,
SearcherMixin)


@override_settings(ELASTIC_SEARCH_INDEX_PREFIX='prefixed_')
@override_settings(SEARCH_ENGINE="search.tests.utils.ForceRefreshElasticSearchEngine")
class ElasticSearchPrefixTests(MockSearchTests):
"""
Override that runs the same tests for ElasticSearchTests,
but with a prefixed index name.
"""

@property
def index_name(self):
"""
The search index name to be used for this test.
"""
return f"prefixed_{TEST_INDEX_NAME}"


@override_settings(SEARCH_ENGINE="search.tests.utils.ForceRefreshElasticSearchEngine")
Expand Down
14 changes: 11 additions & 3 deletions search/tests/tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,14 @@
@override_settings(MOCK_SEARCH_BACKING_FILE=None)
class MockSearchTests(TestCase, SearcherMixin):
""" Test operation of search activities """

@property
def index_name(self):
"""
The search index name to be used for this test.
"""
return TEST_INDEX_NAME

@property
def _is_elastic(self):
""" check search engine implementation, to manage cleanup differently """
Expand All @@ -39,11 +47,11 @@ def setUp(self):
if self._is_elastic:
_elasticsearch = Elasticsearch()
# Make sure that we are fresh
_elasticsearch.indices.delete(index=TEST_INDEX_NAME, ignore=[400, 404])
_elasticsearch.indices.delete(index=self.index_name, ignore=[400, 404])

config_body = {}
# ignore unexpected-keyword-arg; ES python client documents that it can be used
_elasticsearch.indices.create(index=TEST_INDEX_NAME, ignore=400, body=config_body)
_elasticsearch.indices.create(index=self.index_name, ignore=400, body=config_body)
else:
MockSearchEngine.destroy()
self._searcher = None
Expand All @@ -55,7 +63,7 @@ def tearDown(self):
if self._is_elastic:
_elasticsearch = Elasticsearch()
# ignore unexpected-keyword-arg; ES python client documents that it can be used
_elasticsearch.indices.delete(index=TEST_INDEX_NAME, ignore=[400, 404])
_elasticsearch.indices.delete(index=self.index_name, ignore=[400, 404])
else:
MockSearchEngine.destroy()

Expand Down