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

Updating index mapping automatically #2465

Closed
anton-johansson opened this issue Feb 17, 2023 · 5 comments · Fixed by #2708
Closed

Updating index mapping automatically #2465

anton-johansson opened this issue Feb 17, 2023 · 5 comments · Fixed by #2708
Labels
type: enhancement A general enhancement

Comments

@anton-johansson
Copy link

Creating indexes works perfectly, a very nice feature IMO. But whenever I make changes to my document classes, for example adding a new field, I need to update the index mapping. It would be awesome if I could tell Spring Boot Elasticsearch to perform this update automatically, just like index creation works.

Thoughts? Maybe this feature already exists, but I just can't seem to find it?

@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Feb 17, 2023
@anton-johansson
Copy link
Author

I added my own initializer for this:

/**
 * Initializes Elasticsearch by updating index mappings.
 */
@Component
class MappingInitializer
{
    private static final Logger LOG = LogManager.getFormatterLogger(MappingInitializer.class);
    private final ElasticsearchOperations operations;

    @Autowired
    MappingInitializer(ElasticsearchOperations operations)
    {
        this.operations = requireNonNull(operations);
    }

    /**
     * Initializes the Elasticsearch features.
     */
    @PostConstruct
    public void initialize()
    {
        IndexOperations indexOperations = operations.indexOps(MyDocumentType.class);
        boolean result = indexOperations.putMapping();

        LOG.info("Mapping update result: " + result);
    }
}

But it feels like it could be a nice thing to have natively in Spring Data Elasticsearch. Like @Document(updateMapping = true)?

@sothawo sothawo added type: enhancement A general enhancement and removed status: waiting-for-triage An issue we've not yet triaged labels Feb 18, 2023
@anton-johansson
Copy link
Author

Related side note: I can't find any way of managing custom analyzers (or other index settings). Could be nice to have that possibility, on IndexOperations perhaps? I realize that modifying certain settings (such as custom analyzers) require the index to be closed, so I guess it's not something that should happen by default.

I tried messing around with IndexOperations#getSettings(). If I manually add my analyzer, and then call IndexOperations, I get an exception:

2023-02-19 19:22:43.224 ERROR org.springframework.boot.SpringApplication:820 - Application run failed
 org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'mappingInitializer': Invocation of init method failed
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:195) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.applyBeanPostProcessorsBeforeInitialization(AbstractAutowireCapableBeanFactory.java:420) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1743) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:599) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:521) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.AbstractBeanFactory.lambda$doGetBean$0(AbstractBeanFactory.java:326) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:234) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:324) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:200) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:961) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:915) ~[spring-context-6.0.4.jar:6.0.4]
	at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:584) ~[spring-context-6.0.4.jar:6.0.4]
	at org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:146) ~[spring-boot-3.0.2.jar:3.0.2]
	at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:730) ~[spring-boot-3.0.2.jar:3.0.2]
	at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:432) ~[spring-boot-3.0.2.jar:3.0.2]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:308) ~[spring-boot-3.0.2.jar:3.0.2]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1302) ~[spring-boot-3.0.2.jar:3.0.2]
	at org.springframework.boot.SpringApplication.run(SpringApplication.java:1291) ~[spring-boot-3.0.2.jar:3.0.2]
	at mypackage.Application.main(Application.java:29) ~[classes/:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[?:?]
	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:568) ~[?:?]
	at org.springframework.boot.devtools.restart.RestartLauncher.run(RestartLauncher.java:49) ~[spring-boot-devtools-3.0.2.jar:3.0.2]
Caused by: co.elastic.clients.json.JsonpMappingException: Error deserializing co.elastic.clients.elasticsearch._types.analysis.Analyzer: Property 'type' not found (JSON path: conversations.settings.index.analysis.analyzer.html_content) (line no=1, co
lumn no=240, offset=-1)
	at co.elastic.clients.json.JsonpUtils.lookAheadFieldValue(JsonpUtils.java:171) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:201) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:148) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.BuildFunctionDeserializer.deserialize(BuildFunctionDeserializer.java:47) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:43) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializerBase$StringMapDeserializer.deserialize(JsonpDeserializerBase.java:349) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializerBase$StringMapDeserializer.deserialize(JsonpDeserializerBase.java:333) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer$FieldObjectDeserializer.deserialize(ObjectDeserializer.java:78) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:192) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:148) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:79) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:43) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer$FieldObjectDeserializer.deserialize(ObjectDeserializer.java:78) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:192) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:148) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:79) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:43) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer$FieldObjectDeserializer.deserialize(ObjectDeserializer.java:78) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:192) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:148) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:79) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:43) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer$FieldObjectDeserializer.deserialize(ObjectDeserializer.java:78) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:192) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectDeserializer.deserialize(ObjectDeserializer.java:148) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.ObjectBuilderDeserializer.deserialize(ObjectBuilderDeserializer.java:79) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.DelegatingDeserializer$SameType.deserialize(DelegatingDeserializer.java:43) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializerBase$StringMapDeserializer.deserialize(JsonpDeserializerBase.java:349) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializerBase$StringMapDeserializer.deserialize(JsonpDeserializerBase.java:333) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.elasticsearch.indices.GetIndicesSettingsResponse.lambda$createGetIndicesSettingsResponseDeserializer$0(GetIndicesSettingsResponse.java:175) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer$3.deserialize(JsonpDeserializer.java:127) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.json.JsonpDeserializer.deserialize(JsonpDeserializer.java:76) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.transport.rest_client.RestClientTransport.decodeResponse(RestClientTransport.java:325) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.transport.rest_client.RestClientTransport.getHighLevelResponse(RestClientTransport.java:295) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.transport.rest_client.RestClientTransport.performRequest(RestClientTransport.java:148) ~[elasticsearch-java-8.5.3.jar:?]
	at co.elastic.clients.elasticsearch.indices.ElasticsearchIndicesClient.getSettings(ElasticsearchIndicesClient.java:1139) ~[elasticsearch-java-8.5.3.jar:?]
	at org.springframework.data.elasticsearch.client.elc.IndicesTemplate.lambda$getSettings$6(IndicesTemplate.java:265) ~[spring-data-elasticsearch-5.0.1.jar:5.0.1]
	at org.springframework.data.elasticsearch.client.elc.ChildTemplate.execute(ChildTemplate.java:71) ~[spring-data-elasticsearch-5.0.1.jar:5.0.1]
	at org.springframework.data.elasticsearch.client.elc.IndicesTemplate.getSettings(IndicesTemplate.java:264) ~[spring-data-elasticsearch-5.0.1.jar:5.0.1]
	at org.springframework.data.elasticsearch.client.elc.IndicesTemplate.getSettings(IndicesTemplate.java:256) ~[spring-data-elasticsearch-5.0.1.jar:5.0.1]
	at mypackage.MappingInitializer.updateMapping(MappingInitializer.java:45) ~[classes/:?]
	at mypackage.MappingInitializer.initialize(MappingInitializer.java:38) ~[classes/:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[?:?]
	at jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77) ~[?:?]
	at jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[?:?]
	at java.lang.reflect.Method.invoke(Method.java:568) ~[?:?]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleElement.invoke(InitDestroyAnnotationBeanPostProcessor.java:424) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor$LifecycleMetadata.invokeInitMethods(InitDestroyAnnotationBeanPostProcessor.java:368) ~[spring-beans-6.0.4.jar:6.0.4]
	at org.springframework.beans.factory.annotation.InitDestroyAnnotationBeanPostProcessor.postProcessBeforeInitialization(InitDestroyAnnotationBeanPostProcessor.java:192) ~[spring-beans-6.0.4.jar:6.0.4]
	... 23 more

@anton-johansson
Copy link
Author

PS: Sorry for the spam!

Okay, so the exception above is not related to Spring really, as can be seen here: elastic/elasticsearch-java#392.

To make sure the index settings and mappings is up to date, I've written a custom initializer component:

/**
 * Creates and updates the Elasticsearch indices, their settings and their mappings.
 */
@Component
class IndexInitializer
{
    private static final Logger LOG = LogManager.getLogger(IndexInitializer.class);
    private final ElasticsearchOperations operations;

    @Autowired
    IndexInitializer(ElasticsearchOperations operations)
    {
        this.operations = requireNonNull(operations);
    }

    /**
     * Initializes the Elasticsearch features.
     */
    @PostConstruct
    public void initialize()
    {
        initializeIndex(MyDocumentClass.class);
    }

    private void initializeIndex(Class<?> type)
    {
        IndicesTemplate indexOperations = (IndicesTemplate) operations.indexOps(type);
        String indexName = indexOperations.getIndexCoordinates().getIndexName();

        if (!indexOperations.exists())
        {
            LOG.info("Creating index '{}'", indexName);
            assertAcknowledged(indexOperations.execute(client -> client.create(new CreateIndexRequest.Builder()
                    .index(indexName)
                    .settings(indexSettings(true))
                    .build())));
        }
        else
        {
            LOG.info("Getting index settings for '{}' to check if we need to update them", indexName);
            IndexSettings settings = indexOperations.execute(client -> client.getSettings()).get(indexName).settings();
            if (areSettingsEqual(settings, indexSettings(false)))
            {
                LOG.info("No need to update index settings for index '{}'", indexName);
            }
            else
            {
                LOG.warn("Closing index '{}' in order to update its settings", indexName);
                assertAcknowledged(indexOperations.execute(client -> client.close(new CloseIndexRequest.Builder().index(indexName).build())));

                LOG.warn("Updating settings of index '{}'", indexName);
                assertAcknowledged(indexOperations.execute(client -> client.putSettings(new PutIndicesSettingsRequest.Builder().settings(indexSettings(false)).build())));

                LOG.warn("Re-opening index '{}'", indexName);
                assertAcknowledged(indexOperations.execute(client -> client.open(new OpenRequest.Builder().index(indexName).build())));
            }
        }

        LOG.info("Updating the mapping of index '{}'", indexName);
        if (indexOperations.putMapping())
        {
            LOG.info("Successfully updated the mapping of index '{}'", indexName);
        }
        else
        {
            LOG.warn("Could not update the mapping for index '{}'", indexName);
        }
    }

    private IndexSettings indexSettings(boolean create)
    {
        Builder settings = new IndexSettings.Builder();
        if (create)
        {
            settings.numberOfShards("1");
        }

        settings.numberOfReplicas("0");
        settings.analysis(new IndexSettingsAnalysis.Builder()
                .analyzer("html-content", new Analyzer.Builder()
                        .custom(new CustomAnalyzer.Builder()
                                .tokenizer("standard")
                                .charFilter("html_strip")
                                .build())
                        .build())
                .build());

        return new IndexSettings.Builder()
                .index(settings.build())
                .build();
    }

    // TODO: Make this check smarter... Elastics objects do not implement #equals properly, so it's a bit tricky...
    private boolean areSettingsEqual(IndexSettings left, IndexSettings right)
    {
        IndexSettings l = left.index();
        IndexSettings r = right.index();
        return l.numberOfReplicas().equals(r.numberOfReplicas())
            && l.analysis().analyzer().keySet().equals(r.analysis().analyzer().keySet());
    }

    private void assertAcknowledged(AcknowledgedResponse response)
    {
        if (!response.acknowledged())
        {
            throw new IllegalStateException("Response from Elasticsearch was not acknowledged");
        }
    }
}

Some thoughts though:

  • I have to cast the IndexOperations to IndicesTemplate to be able to access the execute method. Maybe expose it via the interface instead?
  • The areSettingsEqual would need to be improved, obviously
  • It would be great if I could specify analyzers via annotations, so I could utilize indexOperations#getSettings and work with that instead of "manually" defining the settings (#indexSettings(boolean create)). Maybe something like this:
@Setting(shards = 2, analyzers = {
    @Analyzer(name = "my-analyzer", tokenizer = "standard", charFilters = "html_strip"),
    @Analyzer(name = "my-other-analyzer", tokenizer = "standard", charFilters = "some_other_filter"),
})
@Document(createIndex = false, dynamic = STRICT, indexName = "mydocument", writeTypeHint = FALSE)
public class MyDocumentClass
{
}

@sothawo
Copy link
Collaborator

sothawo commented Feb 20, 2023

  1. ElasticsearchTemplate.execute This cannot be pulled into the interface, because every implementing class has its own callback class for its client. Currently there are implementations using the RestHighlevelClient and the ElasticsearchClient in this project and the Opensearch project (https://github.com/opensearch-project/spring-data-opensearch) implements the interface as well. Besides that, exposing this method would introduce client specific classes into the interface.
  2. The possibility to configure index settings by annotation should go to a separate issue.

@anton-johansson
Copy link
Author

  1. Aha, that makes sense... Any ideas? I can of course keep casting to IndicesTemplate like I do today, but it doesn't feel safe or future proof.
  2. Yep, agreed! I created Allow more settings (analyzers) when creating the index #2475.

sothawo added a commit to sothawo/spring-data-elasticsearch that referenced this issue Sep 23, 2023
sothawo added a commit that referenced this issue Sep 23, 2023
@sothawo sothawo added this to the 5.2 RC1 (2023.1.0) milestone Sep 23, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
type: enhancement A general enhancement
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants