diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test.yml index 6475f30..ae2b1a7 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test.yml @@ -17,6 +17,8 @@ jobs: format-check: name: Check Code Formatting runs-on: ubuntu-latest + env: + AKKA_REPO_URL: ${{ secrets.AKKA_REPO_URL }} steps: - uses: actions/checkout@v4 @@ -39,6 +41,8 @@ jobs: runs-on: ubuntu-latest permissions: contents: read + env: + AKKA_REPO_URL: ${{ secrets.AKKA_REPO_URL }} steps: - uses: actions/checkout@v4 @@ -53,5 +57,8 @@ jobs: - name: Setup Gradle uses: gradle/actions/setup-gradle@af1da67850ed9a4cedd57bfd976089dd991e2582 # v4.0.0 + - name: Build API with Gradle Wrapper + run: ./gradlew :cqp-api:build :cqp-api:publishToMavenLocal + - name: Build with Gradle Wrapper - run: ./gradlew build + run: ./gradlew build -x :cqp-api:build diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 28fa5d6..7ecdc58 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -91,7 +91,7 @@ The Chemical Query Platform (CQP) is an open-source framework for indexing and s ### cqp-api (Core API Module) -**Version**: 0.0.16 +**Version**: 0.0.17 **Purpose**: Defines interfaces, models, and abstractions for chemical structure storage and search. @@ -132,7 +132,7 @@ com.quantori.cqp.api/ ### cqp-storage-elasticsearch (Elasticsearch Implementation) -**Version**: 0.0.14 +**Version**: 0.0.17 **Purpose**: Elasticsearch implementation of CQP storage interfaces. @@ -155,7 +155,7 @@ com.quantori.cqp.storage.elasticsearch/ └── ReactionMapper.java # Domain ↔ Elasticsearch ``` -**Dependencies**: cqp-api (0.0.16), Elasticsearch Java Client (8.6.2) +**Dependencies**: cqp-api (0.0.17), Elasticsearch Java Client (8.6.2) ### cqp-core (Akka Actors Module - Currently Missing) diff --git a/CONTEXT.md b/CONTEXT.md index 9de2653..2b4b326 100644 --- a/CONTEXT.md +++ b/CONTEXT.md @@ -6,6 +6,20 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co CQP is an open-source framework for indexing and searching within cheminformatics applications (molecules & reactions). It's built on the Akka Actors framework for scalability and supports multiple storage engines including PostgreSQL, Elasticsearch, and Apache Solr. +## Package Naming Strategy + +**CQP Internal Code**: All code in this repository uses the `com.quantori.cqp.*` package hierarchy: +- `com.quantori.cqp.api.model.*` - Core data models (Property, PropertyType, PropertyValue, etc.) +- `com.quantori.cqp.core.*` - Akka actors and core framework +- `com.quantori.cqp.storage.elasticsearch.*` - Elasticsearch storage implementation +- `com.quantori.cqp.build.*` - Gradle build plugins + +**External Indigo Library**: The external Indigo Toolkit library uses `com.epam.indigo.*`: +- `com.epam.indigo.Indigo` - Main Indigo class +- `com.epam.indigo.IndigoObject` - Indigo molecule/reaction objects +- `com.epam.indigo.IndigoException` - Indigo exceptions +- **Do not change** these package references - they belong to the external library + ## Project Structure This is a multi-module Gradle project with the following key modules: diff --git a/Devnotes.md b/Devnotes.md index 6fefa8b..dfa7d0a 100644 --- a/Devnotes.md +++ b/Devnotes.md @@ -1,4 +1,15 @@ ## Build instructions * Download the repository * Let your IDE import necessary dependencies automatically or fetch them manually -* For local repository publication Run `gradle build` task and then run `gradle publishToMavenLocal` tasks +* For local repository publication run the following tasks + * `gradlew :cqp-api:build :cqp-api:publishToMavenLocal` + * `gradle build -x :cqp-api:build` + * `gradle publishToMavenLocal -x :cqp-api:publishToMavenLocal` + +## Repository configuration + +* The shared plugin reads the Akka repository URL in this order and stops at the first match: + 1. Gradle property `AKKA_REPO_URL` (set via `gradle.properties` or `-PAKKA_REPO_URL=...`) + 2. Gradle property `akkaRepoUrl` (legacy camelCase fallback) + 3. Environment variable `AKKA_REPO_URL` +* If none of the above is supplied, the build fails immediately. Obtain the secure URL/token from https://account.akka.io/ and set one of the properties/variables before running Gradle. diff --git a/cqp-api/build.gradle.kts b/cqp-api/build.gradle.kts index 6e6c921..e6ae25e 100644 --- a/cqp-api/build.gradle.kts +++ b/cqp-api/build.gradle.kts @@ -6,7 +6,7 @@ plugins { group = "com.quantori" description = "Chem query platform. Storage API" -version = "0.0.16" +version = "0.0.17" dependencies { implementation("commons-codec:commons-codec:1.15") @@ -34,4 +34,4 @@ dependencies { testImplementation(libs.jackson) testImplementation(libs.lombok) testAnnotationProcessor(libs.lombok) -} \ No newline at end of file +} diff --git a/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java b/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java index 24ebd74..0f1314a 100644 --- a/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java +++ b/cqp-api/src/main/java/com/quantori/cqp/api/model/Property.java @@ -21,7 +21,48 @@ public Property(String name, PropertyType type) { this.type = type; } + /** + * Defines the supported property kinds in Chem Query Platform. Existing values must remain + * unchanged to keep backward compatibility with previously persisted data. + */ public enum PropertyType { - STRING, DECIMAL, DATE + STRING, + DECIMAL, + DATE, + /** + * Binary payload for storing images, PDFs or other small files. Intended size limit is 10 MB per + * value and the binary content is transferred as byte arrays. + */ + BINARY, + /** + * Timestamp value with timezone information preserved. Values are serialized as ISO-8601 + * instants (e.g. {@code 2025-01-15T10:30:00Z}). + */ + DATE_TIME, + /** + * Ordered collection of string values. This allows preserving the order in which the user + * provided the values (e.g. {@code ["first", "second"]}). + */ + LIST, + /** + * Hyperlink that stores HTTP/HTTPS (and similar) URL references. Validation is applied on + * backend layers while the enum only marks the data type. + */ + HYPERLINK, + /** + * Chemical structure serialized as a SMILES string. Consumers rely on Indigo toolkit for + * validation. + */ + CHEMICAL_STRUCTURE, + /** + * Three dimensional molecular structure stored as MOL text block. This is typically used for 3D + * renderings and cannot be exported to legacy SDF files. + */ + STRUCTURE_3D, + /** + * Sanitized HTML fragment used for rich text property rendering. Scripts and unsafe tags are + * expected to be removed before persisting. + */ + HTML } } diff --git a/cqp-api/src/main/java/com/quantori/cqp/api/model/PropertyValue.java b/cqp-api/src/main/java/com/quantori/cqp/api/model/PropertyValue.java new file mode 100644 index 0000000..5319d93 --- /dev/null +++ b/cqp-api/src/main/java/com/quantori/cqp/api/model/PropertyValue.java @@ -0,0 +1,53 @@ +package com.quantori.cqp.api.model; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.List; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +/** + * Container for the value of a {@link Property}. Each concrete field maps to a {@link + * Property.PropertyType}. Only one field is expected to be non-null for a particular value. + */ +@Data +@NoArgsConstructor +@AllArgsConstructor +@Builder +public class PropertyValue { + + /** String payload for {@link Property.PropertyType#STRING} values. */ + private String stringValue; + + /** Decimal payload for {@link Property.PropertyType#DECIMAL} values. */ + private Double decimalValue; + + /** Date payload for {@link Property.PropertyType#DATE} values. */ + private LocalDate dateValue; + + /** + * Binary content for {@link Property.PropertyType#BINARY} values. Typical use cases include + * storing images or PDF attachments (up to 10 MB). + */ + private byte[] binaryValue; + + /** Timestamp payload for {@link Property.PropertyType#DATE_TIME} values. */ + private Instant dateTimeValue; + + /** Ordered collection of strings for {@link Property.PropertyType#LIST} values. */ + private List listValue; + + /** URL string for {@link Property.PropertyType#HYPERLINK} values. */ + private String hyperlinkValue; + + /** SMILES payload for {@link Property.PropertyType#CHEMICAL_STRUCTURE} values. */ + private String chemicalStructureValue; + + /** MOL block payload for {@link Property.PropertyType#STRUCTURE_3D} values. */ + private String structure3DValue; + + /** Sanitized HTML fragment for {@link Property.PropertyType#HTML} values. */ + private String htmlValue; +} diff --git a/cqp-api/src/test/java/com/quantori/cqp/api/model/PropertyTypeTest.java b/cqp-api/src/test/java/com/quantori/cqp/api/model/PropertyTypeTest.java new file mode 100644 index 0000000..fbced70 --- /dev/null +++ b/cqp-api/src/test/java/com/quantori/cqp/api/model/PropertyTypeTest.java @@ -0,0 +1,60 @@ +package com.quantori.cqp.api.model; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.time.Instant; +import java.time.LocalDate; +import java.util.EnumSet; +import java.util.List; +import org.junit.jupiter.api.Test; + +class PropertyTypeTest { + + @Test + void shouldContainExtendedTypes() { + EnumSet types = EnumSet.allOf(Property.PropertyType.class); + + assertThat(types) + .contains( + Property.PropertyType.BINARY, + Property.PropertyType.DATE_TIME, + Property.PropertyType.LIST, + Property.PropertyType.HYPERLINK, + Property.PropertyType.CHEMICAL_STRUCTURE, + Property.PropertyType.STRUCTURE_3D, + Property.PropertyType.HTML); + } + + @Test + void shouldAllowPropertyValueAccessors() { + byte[] binary = new byte[] {1, 2, 3}; + Instant timestamp = Instant.parse("2024-01-01T10:15:30Z"); + List orderedList = List.of("first", "second"); + LocalDate date = LocalDate.of(2024, 2, 29); + + PropertyValue value = + PropertyValue.builder() + .stringValue("test") + .decimalValue(12.34d) + .dateValue(date) + .binaryValue(binary) + .dateTimeValue(timestamp) + .listValue(orderedList) + .hyperlinkValue("https://example.com") + .chemicalStructureValue("CCO") + .structure3DValue("3D-MOL-DATA") + .htmlValue("

html

") + .build(); + + assertThat(value.getStringValue()).isEqualTo("test"); + assertThat(value.getDecimalValue()).isEqualTo(12.34d); + assertThat(value.getDateValue()).isEqualTo(date); + assertThat(value.getBinaryValue()).containsExactly(binary); + assertThat(value.getDateTimeValue()).isEqualTo(timestamp); + assertThat(value.getListValue()).containsExactlyElementsOf(orderedList); + assertThat(value.getHyperlinkValue()).isEqualTo("https://example.com"); + assertThat(value.getChemicalStructureValue()).isEqualTo("CCO"); + assertThat(value.getStructure3DValue()).isEqualTo("3D-MOL-DATA"); + assertThat(value.getHtmlValue()).isEqualTo("

html

"); + } +} diff --git a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt index dd6fbc7..6e6cf99 100644 --- a/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt +++ b/cqp-build/src/main/kotlin/com/quantori/cqp/build/CqpJavaLibraryPlugin.kt @@ -1,5 +1,6 @@ package com.quantori.cqp.build +import org.gradle.api.GradleException import org.gradle.api.JavaVersion import org.gradle.api.Plugin import org.gradle.api.Project @@ -28,10 +29,18 @@ class CqpJavaLibraryPlugin : Plugin { project.repositories { mavenLocal() - mavenCentral () + mavenCentral() + + val akkaRepoUrl = + (project.findProperty("AKKA_REPO_URL") as String?) + ?: (project.findProperty("akkaRepoUrl") as String?) + ?: System.getenv("AKKA_REPO_URL").takeIf { !it.isNullOrBlank() } + ?: throw GradleException( + "AKKA_REPO_URL is not configured. Configure the secret/property to resolve Akka artifacts.") + maven { name = "Akka" - url = project.uri("https://repo.akka.io/maven") + url = project.uri(akkaRepoUrl) content { includeGroup("com.typesafe.akka") includeGroupByRegex("com\\.lightbend\\..*") @@ -200,4 +209,4 @@ class CqpJavaLibraryPlugin : Plugin { } } } -} \ No newline at end of file +} diff --git a/cqp-core/build.gradle.kts b/cqp-core/build.gradle.kts index 053d408..08cb0a6 100644 --- a/cqp-core/build.gradle.kts +++ b/cqp-core/build.gradle.kts @@ -5,13 +5,13 @@ plugins { } description = "Chem query platform. Compound quick search" -version = "0.0.15" +version = "0.0.17" val akkaVersion: String = "2.9.0" val lightbendVersion: String = "1.5.0" dependencies { - implementation("com.quantori:cqp-api:0.0.16") + implementation("com.quantori:cqp-api:0.0.17") implementation("com.typesafe:config:1.4.2") diff --git a/cqp-storage-elasticsearch/build.gradle.kts b/cqp-storage-elasticsearch/build.gradle.kts index 7ef5cc2..87d75b7 100644 --- a/cqp-storage-elasticsearch/build.gradle.kts +++ b/cqp-storage-elasticsearch/build.gradle.kts @@ -6,7 +6,7 @@ plugins { group = "com.quantori" description = "Chem query platform. Storage Elasticsearch" -version = "0.0.14" +version = "0.0.17" tasks.named("javadoc") { exclude( @@ -16,7 +16,7 @@ tasks.named("javadoc") { } dependencies { - api("com.quantori:cqp-api:0.0.16") + api("com.quantori:cqp-api:0.0.17") implementation("co.elastic.clients:elasticsearch-java:8.6.2") implementation(libs.jackson) implementation(libs.jackson.jsr310) diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java index bf8eb3d..5bdcd90 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticIndexMappingsFactory.java @@ -4,7 +4,12 @@ import com.quantori.cqp.storage.elasticsearch.model.MoleculeDocument; import lombok.experimental.UtilityClass; +import java.util.Collections; +import java.util.EnumMap; import java.util.Map; +import java.util.stream.Collectors; + +import static java.util.Map.entry; @UtilityClass class ElasticIndexMappingsFactory { @@ -19,19 +24,38 @@ class ElasticIndexMappingsFactory { public static final String ROLE = "role"; public static final String REACTION_ID = "reactionId"; - public static final Map TYPES_MAP = Map.of( - Property.PropertyType.STRING, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Wildcard.jsonValue(), - Property.PropertyType.DATE, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Date.jsonValue(), - Property.PropertyType.DECIMAL, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Double.jsonValue() - ); + public static final String PROPERTY_TYPE_META_KEY = "propertyType"; - public static final - Map KINDS_MAP = Map.of( - co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Wildcard, Property.PropertyType.STRING, - co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Date, Property.PropertyType.DATE, - co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Double, Property.PropertyType.DECIMAL + private static final Map KIND_BY_PROPERTY_TYPE = Map.ofEntries( + entry(Property.PropertyType.STRING, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Wildcard), + entry(Property.PropertyType.DATE, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Date), + entry(Property.PropertyType.DATE_TIME, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.DateNanos), + entry(Property.PropertyType.DECIMAL, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Double), + entry(Property.PropertyType.BINARY, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Binary), + entry(Property.PropertyType.LIST, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Keyword), + entry(Property.PropertyType.HYPERLINK, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.MatchOnlyText), + entry(Property.PropertyType.CHEMICAL_STRUCTURE, + co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Text), + entry(Property.PropertyType.STRUCTURE_3D, + co.elastic.clients.elasticsearch._types.mapping.Property.Kind.SearchAsYouType), + entry(Property.PropertyType.HTML, co.elastic.clients.elasticsearch._types.mapping.Property.Kind.Text) ); + public static final Map TYPES_MAP = + KIND_BY_PROPERTY_TYPE.entrySet().stream() + .collect(Collectors.toUnmodifiableMap(Map.Entry::getKey, entry -> entry.getValue().jsonValue())); + + public static final Map + KINDS_MAP; + + static { + EnumMap map = + new EnumMap<>(co.elastic.clients.elasticsearch._types.mapping.Property.Kind.class); + KIND_BY_PROPERTY_TYPE.forEach((propertyType, kind) -> map.putIfAbsent(kind, propertyType)); + KINDS_MAP = Collections.unmodifiableMap(map); + } + public static final String MOLECULES_MAPPING = String.format(""" { "settings": { @@ -228,20 +252,30 @@ class ElasticIndexMappingsFactory { "yyyy-MM-dd HH:mm:ss", "MM/dd/yyyy HH:mm:ss a", "dd.MM.yyyy", - "dd/MM-yyyy" + "dd/MM-yyyy", + "strict_date_optional_time", + "strict_date_optional_time_nanos" }; - public static final String DATE_PROPERTY_MAPPING = String.format(""" - "%%s": { - "type": "%%s", + public static final String DATE_FORMAT_PATTERN = String.join("||", DATE_FORMATS); + + public static final String DATE_PROPERTY_MAPPING = """ + "%s": { + "type": "%s", "ignore_malformed": true, - "format": "%s" + "format": "%s", + "meta": { + "%s": "%s" + } } - """, String.join("||", DATE_FORMATS)); + """; public static final String PROPERTY_MAPPING = """ "%s": { - "type": "%s" + "type": "%s", + "meta": { + "%s": "%s" + } } """; } diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java index 07c62a7..02451e0 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchMoleculesResourceAllocator.java @@ -73,13 +73,25 @@ private String getPropertiesMapping(Map propertiesMapping) { String propertyKey = entry.getKey(); Property property = entry.getValue(); Property.PropertyType type = Objects.requireNonNullElse(property.getType(), Property.PropertyType.STRING); - if (Property.PropertyType.DATE == type) { - return String.format(ElasticIndexMappingsFactory.DATE_PROPERTY_MAPPING, propertyKey, - ElasticIndexMappingsFactory.TYPES_MAP.get(type)); - } else { - return String.format(ElasticIndexMappingsFactory.PROPERTY_MAPPING, propertyKey, - ElasticIndexMappingsFactory.TYPES_MAP.get(type)); + String elasticType = ElasticIndexMappingsFactory.TYPES_MAP.get(type); + String typeName = type.name(); + if (Property.PropertyType.DATE == type || Property.PropertyType.DATE_TIME == type) { + return String.format( + ElasticIndexMappingsFactory.DATE_PROPERTY_MAPPING, + propertyKey, + elasticType, + ElasticIndexMappingsFactory.DATE_FORMAT_PATTERN, + ElasticIndexMappingsFactory.PROPERTY_TYPE_META_KEY, + typeName + ); } + return String.format( + ElasticIndexMappingsFactory.PROPERTY_MAPPING, + propertyKey, + elasticType, + ElasticIndexMappingsFactory.PROPERTY_TYPE_META_KEY, + typeName + ); }) .collect(Collectors.joining(",\n", ",\"" + ElasticIndexMappingsFactory.PROPERTIES + "\": {\n", "}\n")); } diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java index fc95cde..7b5344a 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStorageMolecules.java @@ -441,10 +441,11 @@ private String escape(String value) { private Object convert(String value, Property.PropertyType type) { return switch (type) { - case DATE -> { + case DATE, DATE_TIME -> { // TODO limited date support, needs to add a check for formats in future if (StringUtils.isBlank(value)) { - throw new ElasticsearchStorageException(String.format("A value \"%s\" cannot be cast to date", value)); + throw new ElasticsearchStorageException( + String.format("A value \"%s\" cannot be cast to %s", value, type.name().toLowerCase())); } else { yield value; } @@ -457,6 +458,12 @@ private Object convert(String value, Property.PropertyType type) { throw new ElasticsearchStorageException(String.format("A value \"%s\" cannot be cast to decimal", value)); } } + case BINARY, + LIST, + HYPERLINK, + CHEMICAL_STRUCTURE, + STRUCTURE_3D, + HTML -> value; }; } } diff --git a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java index d057246..57bad51 100644 --- a/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java +++ b/cqp-storage-elasticsearch/src/main/java/com/quantori/cqp/storage/elasticsearch/ElasticsearchStoragePropertiesMapping.java @@ -3,6 +3,7 @@ import co.elastic.clients.elasticsearch.ElasticsearchClient; import co.elastic.clients.elasticsearch._types.ElasticsearchException; import co.elastic.clients.elasticsearch.indices.GetMappingResponse; +import co.elastic.clients.elasticsearch._types.mapping.PropertyBase; import co.elastic.clients.transport.ElasticsearchTransport; import com.quantori.cqp.api.model.Library; import com.quantori.cqp.api.model.Property; @@ -188,7 +189,7 @@ private Map convertElasticPropertiesToApplicationProperties( return indexedProperties.entrySet().stream() .collect(Collectors.toMap(Map.Entry::getKey, entry -> { Property property = libraryDocument.getPropertiesMapping().getOrDefault(entry.getKey(), null); - Property.PropertyType type = ElasticIndexMappingsFactory.KINDS_MAP.get(entry.getValue()._kind()); + Property.PropertyType type = resolvePropertyType(property, entry.getValue()); if (property == null) { return new Property(entry.getKey(), type); } else { @@ -225,6 +226,28 @@ private OperationStatus createAndUpdatePropertiesMapping(String libraryId, Libra } } + private Property.PropertyType resolvePropertyType( + Property storedProperty, + co.elastic.clients.elasticsearch._types.mapping.Property elasticProperty + ) { + if (storedProperty != null && storedProperty.getType() != null) { + return storedProperty.getType(); + } + return Optional.ofNullable(elasticProperty._get()) + .filter(PropertyBase.class::isInstance) + .map(PropertyBase.class::cast) + .map(PropertyBase::meta) + .map(meta -> meta.get(ElasticIndexMappingsFactory.PROPERTY_TYPE_META_KEY)) + .map(value -> { + try { + return Property.PropertyType.valueOf(value); + } catch (IllegalArgumentException ignored) { + return null; + } + }) + .orElse(ElasticIndexMappingsFactory.KINDS_MAP.get(elasticProperty._kind())); + } + /** * Get a property name in an elastic index *