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

[Backport 8.x] support composable templates #1952

Merged
merged 1 commit into from
Dec 12, 2024
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
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ venv

# sample code for GitHub issues
issues
.direnv
.envrc
11 changes: 10 additions & 1 deletion elasticsearch_dsl/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,14 @@
construct_field,
)
from .function import SF
from .index import AsyncIndex, AsyncIndexTemplate, Index, IndexTemplate
from .index import (
AsyncComposableIndexTemplate,
AsyncIndex,
AsyncIndexTemplate,
ComposableIndexTemplate,
Index,
IndexTemplate,
)
from .mapping import AsyncMapping, Mapping
from .query import Q, Query
from .response import AggResponse, Response, UpdateByQueryResponse
Expand All @@ -102,6 +109,7 @@
"A",
"Agg",
"AggResponse",
"AsyncComposableIndexTemplate",
"AsyncDocument",
"AsyncEmptySearch",
"AsyncFacetedSearch",
Expand All @@ -117,6 +125,7 @@
"Boolean",
"Byte",
"Completion",
"ComposableIndexTemplate",
"ConstantKeyword",
"CustomField",
"Date",
Expand Down
54 changes: 51 additions & 3 deletions elasticsearch_dsl/_async/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,47 @@ async def save(
)


class AsyncComposableIndexTemplate:
def __init__(
self,
name: str,
template: str,
index: Optional["AsyncIndex"] = None,
priority: Optional[int] = None,
**kwargs: Any,
):
if index is None:
self._index = AsyncIndex(template, **kwargs)
else:
if kwargs:
raise ValueError(
"You cannot specify options for Index when"
" passing an Index instance."
)
self._index = index.clone()
self._index._name = template
self._template_name = name
self.priority = priority

def __getattr__(self, attr_name: str) -> Any:
return getattr(self._index, attr_name)

def to_dict(self) -> Dict[str, Any]:
d: Dict[str, Any] = {"template": self._index.to_dict()}
d["index_patterns"] = [self._index._name]
if self.priority is not None:
d["priority"] = self.priority
return d

async def save(
self, using: Optional[AsyncUsingType] = None
) -> "ObjectApiResponse[Any]":
es = get_connection(using or self._index._using)
return await es.indices.put_index_template(
name=self._template_name, **self.to_dict()
)


class AsyncIndex(IndexBase):
_using: AsyncUsingType

Expand Down Expand Up @@ -102,13 +143,20 @@ def as_template(
pattern: Optional[str] = None,
order: Optional[int] = None,
) -> AsyncIndexTemplate:
# TODO: should we allow pattern to be a top-level arg?
# or maybe have an IndexPattern that allows for it and have
# Document._index be that?
return AsyncIndexTemplate(
template_name, pattern or self._name, index=self, order=order
)

def as_composable_template(
self,
template_name: str,
pattern: Optional[str] = None,
priority: Optional[int] = None,
) -> AsyncComposableIndexTemplate:
return AsyncComposableIndexTemplate(
template_name, pattern or self._name, index=self, priority=priority
)

async def load_mappings(self, using: Optional[AsyncUsingType] = None) -> None:
await self.get_or_create_mapping().update_from_es(
self._name, using=using or self._using
Expand Down
50 changes: 47 additions & 3 deletions elasticsearch_dsl/_sync/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,43 @@ def save(self, using: Optional[UsingType] = None) -> "ObjectApiResponse[Any]":
return es.indices.put_template(name=self._template_name, body=self.to_dict())


class ComposableIndexTemplate:
def __init__(
self,
name: str,
template: str,
index: Optional["Index"] = None,
priority: Optional[int] = None,
**kwargs: Any,
):
if index is None:
self._index = Index(template, **kwargs)
else:
if kwargs:
raise ValueError(
"You cannot specify options for Index when"
" passing an Index instance."
)
self._index = index.clone()
self._index._name = template
self._template_name = name
self.priority = priority

def __getattr__(self, attr_name: str) -> Any:
return getattr(self._index, attr_name)

def to_dict(self) -> Dict[str, Any]:
d: Dict[str, Any] = {"template": self._index.to_dict()}
d["index_patterns"] = [self._index._name]
if self.priority is not None:
d["priority"] = self.priority
return d

def save(self, using: Optional[UsingType] = None) -> "ObjectApiResponse[Any]":
es = get_connection(using or self._index._using)
return es.indices.put_index_template(name=self._template_name, **self.to_dict())


class Index(IndexBase):
_using: UsingType

Expand Down Expand Up @@ -96,13 +133,20 @@ def as_template(
pattern: Optional[str] = None,
order: Optional[int] = None,
) -> IndexTemplate:
# TODO: should we allow pattern to be a top-level arg?
# or maybe have an IndexPattern that allows for it and have
# Document._index be that?
return IndexTemplate(
template_name, pattern or self._name, index=self, order=order
)

def as_composable_template(
self,
template_name: str,
pattern: Optional[str] = None,
priority: Optional[int] = None,
) -> ComposableIndexTemplate:
return ComposableIndexTemplate(
template_name, pattern or self._name, index=self, priority=priority
)

def load_mappings(self, using: Optional[UsingType] = None) -> None:
self.get_or_create_mapping().update_from_es(
self._name, using=using or self._using
Expand Down
8 changes: 6 additions & 2 deletions elasticsearch_dsl/index.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,9 @@
# specific language governing permissions and limitations
# under the License.

from ._async.index import AsyncIndex, AsyncIndexTemplate # noqa: F401
from ._sync.index import Index, IndexTemplate # noqa: F401
from ._async.index import ( # noqa: F401
AsyncComposableIndexTemplate,
AsyncIndex,
AsyncIndexTemplate,
)
from ._sync.index import ComposableIndexTemplate, Index, IndexTemplate # noqa: F401
5 changes: 4 additions & 1 deletion examples/alias_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@

ALIAS = "test-blog"
PATTERN = ALIAS + "-*"
PRIORITY = 100


class BlogPost(Document):
Expand Down Expand Up @@ -81,7 +82,9 @@ def setup() -> None:
deploy.
"""
# create an index template
index_template = BlogPost._index.as_template(ALIAS, PATTERN)
index_template = BlogPost._index.as_composable_template(
ALIAS, PATTERN, priority=PRIORITY
)
# upload the template into elasticsearch
# potentially overriding the one already there
index_template.save()
Expand Down
5 changes: 4 additions & 1 deletion examples/async/alias_migration.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@

ALIAS = "test-blog"
PATTERN = ALIAS + "-*"
PRIORITY = 100


class BlogPost(AsyncDocument):
Expand Down Expand Up @@ -82,7 +83,9 @@ async def setup() -> None:
deploy.
"""
# create an index template
index_template = BlogPost._index.as_template(ALIAS, PATTERN)
index_template = BlogPost._index.as_composable_template(
ALIAS, PATTERN, priority=PRIORITY
)
# upload the template into elasticsearch
# potentially overriding the one already there
await index_template.save()
Expand Down
2 changes: 1 addition & 1 deletion examples/async/parent_child.py
Original file line number Diff line number Diff line change
Expand Up @@ -226,7 +226,7 @@ async def save(self, **kwargs: Any) -> None: # type: ignore[override]

async def setup() -> None:
"""Create an IndexTemplate and save it into elasticsearch."""
index_template = Post._index.as_template("base")
index_template = Post._index.as_composable_template("base", priority=100)
await index_template.save()


Expand Down
2 changes: 1 addition & 1 deletion examples/parent_child.py
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ def save(self, **kwargs: Any) -> None: # type: ignore[override]

def setup() -> None:
"""Create an IndexTemplate and save it into elasticsearch."""
index_template = Post._index.as_template("base")
index_template = Post._index.as_composable_template("base", priority=100)
index_template.save()


Expand Down
1 change: 1 addition & 0 deletions setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,6 @@ filterwarnings =
error
ignore:Legacy index templates are deprecated in favor of composable templates.:elasticsearch.exceptions.ElasticsearchWarning
ignore:datetime.datetime.utcfromtimestamp\(\) is deprecated and scheduled for removal in a future version..*:DeprecationWarning
default:enable_cleanup_closed ignored.*:DeprecationWarning
markers =
sync: mark a test as performing I/O without asyncio.
4 changes: 4 additions & 0 deletions tests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ def teardown_method(self, _: Any) -> None:
)
self.client.indices.delete(index="*", expand_wildcards=expand_wildcards)
self.client.indices.delete_template(name="*")
self.client.indices.delete_index_template(name="*")

def es_version(self) -> Tuple[int, ...]:
if not hasattr(self, "_es_version"):
Expand Down Expand Up @@ -172,6 +173,9 @@ def write_client(client: Elasticsearch) -> Generator[Elasticsearch, None, None]:
for index_name in client.indices.get(index="test-*", expand_wildcards="all"):
client.indices.delete(index=index_name)
client.options(ignore_status=404).indices.delete_template(name="test-template")
client.options(ignore_status=404).indices.delete_index_template(
name="test-template"
)


@pytest_asyncio.fixture
Expand Down
27 changes: 26 additions & 1 deletion tests/test_integration/_async/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from elasticsearch import AsyncElasticsearch

from elasticsearch_dsl import (
AsyncComposableIndexTemplate,
AsyncDocument,
AsyncIndex,
AsyncIndexTemplate,
Expand All @@ -35,7 +36,31 @@ class Post(AsyncDocument):

@pytest.mark.asyncio
async def test_index_template_works(async_write_client: AsyncElasticsearch) -> None:
it = AsyncIndexTemplate("test-template", "test-*")
it = AsyncIndexTemplate("test-template", "test-legacy-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
await it.save()

i = AsyncIndex("test-legacy-blog")
await i.create()

assert {
"test-legacy-blog": {
"mappings": {
"properties": {
"title": {"type": "text", "analyzer": "my_analyzer"},
"published_from": {"type": "date"},
}
}
}
} == await async_write_client.indices.get_mapping(index="test-legacy-blog")


@pytest.mark.asyncio
async def test_composable_index_template_works(
async_write_client: AsyncElasticsearch,
) -> None:
it = AsyncComposableIndexTemplate("test-template", "test-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
await it.save()
Expand Down
36 changes: 34 additions & 2 deletions tests/test_integration/_sync/test_index.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,15 @@
import pytest
from elasticsearch import Elasticsearch

from elasticsearch_dsl import Date, Document, Index, IndexTemplate, Text, analysis
from elasticsearch_dsl import (
ComposableIndexTemplate,
Date,
Document,
Index,
IndexTemplate,
Text,
analysis,
)


class Post(Document):
Expand All @@ -28,7 +36,31 @@ class Post(Document):

@pytest.mark.sync
def test_index_template_works(write_client: Elasticsearch) -> None:
it = IndexTemplate("test-template", "test-*")
it = IndexTemplate("test-template", "test-legacy-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
it.save()

i = Index("test-legacy-blog")
i.create()

assert {
"test-legacy-blog": {
"mappings": {
"properties": {
"title": {"type": "text", "analyzer": "my_analyzer"},
"published_from": {"type": "date"},
}
}
}
} == write_client.indices.get_mapping(index="test-legacy-blog")


@pytest.mark.sync
def test_composable_index_template_works(
write_client: Elasticsearch,
) -> None:
it = ComposableIndexTemplate("test-template", "test-*")
it.document(Post)
it.settings(number_of_replicas=0, number_of_shards=1)
it.save()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ async def test_alias_migration(async_write_client: AsyncElasticsearch) -> None:
await alias_migration.setup()

# verify that template, index, and alias has been set up
assert await async_write_client.indices.exists_template(name=ALIAS)
assert await async_write_client.indices.exists_index_template(name=ALIAS)
assert await async_write_client.indices.exists(index=PATTERN)
assert await async_write_client.indices.exists_alias(name=ALIAS)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@
@pytest_asyncio.fixture
async def question(async_write_client: AsyncElasticsearch) -> Question:
await setup()
assert await async_write_client.indices.exists_template(name="base")
assert await async_write_client.indices.exists_index_template(name="base")

# create a question object
q = Question(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ def test_alias_migration(write_client: Elasticsearch) -> None:
alias_migration.setup()

# verify that template, index, and alias has been set up
assert write_client.indices.exists_template(name=ALIAS)
assert write_client.indices.exists_index_template(name=ALIAS)
assert write_client.indices.exists(index=PATTERN)
assert write_client.indices.exists_alias(name=ALIAS)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
@pytest.fixture
def question(write_client: Elasticsearch) -> Question:
setup()
assert write_client.indices.exists_template(name="base")
assert write_client.indices.exists_index_template(name="base")

# create a question object
q = Question(
Expand Down
Loading
Loading