diff --git a/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/Entity.java b/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/Entity.java
index da0c3c18e..ad9aacbef 100644
--- a/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/Entity.java
+++ b/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/Entity.java
@@ -6,6 +6,8 @@
import com.contentgrid.appserver.application.model.attributes.ContentAttribute;
import com.contentgrid.appserver.application.model.attributes.SimpleAttribute;
import com.contentgrid.appserver.application.model.attributes.SimpleAttribute.Type;
+import com.contentgrid.appserver.application.model.attributes.flags.SyntheticAttributeFlag;
+import com.contentgrid.appserver.application.model.attributes.flags.IgnoredFlag;
import com.contentgrid.appserver.application.model.attributes.flags.ReadOnlyFlag;
import com.contentgrid.appserver.application.model.exceptions.AttributeNotFoundException;
import com.contentgrid.appserver.application.model.exceptions.DuplicateElementException;
@@ -17,6 +19,7 @@
import com.contentgrid.appserver.application.model.i18n.TranslatableImpl;
import com.contentgrid.appserver.application.model.i18n.TranslationBuilderSupport;
import com.contentgrid.appserver.application.model.i18n.UnconfigurableTranslatable;
+import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchContentAttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.SearchFilter;
import com.contentgrid.appserver.application.model.sortable.SortableField;
import com.contentgrid.appserver.application.model.values.AttributeName;
@@ -197,6 +200,24 @@ public static class ConfigurableEntityTranslations implements EntityTranslations
}
);
this.attributes.remove(this.primaryKey.getName());
+
+ // Look for any FullTextSearchContentAttributeSearchFilter defined on a content attribute of this entity.
+ // If these exist, create an attribute on the entity that will be used to store the content text.
+ searchFilters.forEach(
+ searchFilter -> {
+ if (searchFilter instanceof FullTextSearchContentAttributeSearchFilter ftsContentFilter) {
+ // Create a hidden attribute on this entity that will be used to store the extracted text.
+ String attributeName = ftsContentFilter.getHiddenTextAttributeFormattedName();
+ Attribute hiddenTextAttribute = SimpleAttribute.builder()
+ .name(AttributeName.of(attributeName))
+ .column(ColumnName.of(attributeName))
+ .type(Type.TEXT)
+ .flag(SyntheticAttributeFlag.INSTANCE)
+ .build();
+ this.attributes.put(AttributeName.of(attributeName), hiddenTextAttribute);
+ }
+ }
+ );
}
/**
diff --git a/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/attributes/flags/SyntheticAttributeFlag.java b/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/attributes/flags/SyntheticAttributeFlag.java
new file mode 100644
index 000000000..d8eb20749
--- /dev/null
+++ b/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/attributes/flags/SyntheticAttributeFlag.java
@@ -0,0 +1,10 @@
+package com.contentgrid.appserver.application.model.attributes.flags;
+
+/**
+ * Marks an attribute as synthetic; generated internally for the benefit of the application itself
+ *
+ * Synthetic attributes are not part of the application model, and are always hidden as well
+ */
+public interface SyntheticAttributeFlag extends IgnoredFlag {
+ SyntheticAttributeFlag INSTANCE = attribute -> {};
+}
diff --git a/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/searchfilters/FullTextSearchContentAttributeSearchFilter.java b/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/searchfilters/FullTextSearchContentAttributeSearchFilter.java
new file mode 100644
index 000000000..0ea3f64b0
--- /dev/null
+++ b/contentgrid-appserver-application-model/src/main/java/com/contentgrid/appserver/application/model/searchfilters/FullTextSearchContentAttributeSearchFilter.java
@@ -0,0 +1,77 @@
+package com.contentgrid.appserver.application.model.searchfilters;
+
+import com.contentgrid.appserver.application.model.attributes.ContentAttribute;
+import com.contentgrid.appserver.application.model.i18n.ConfigurableTranslatable;
+import com.contentgrid.appserver.application.model.i18n.TranslatableImpl;
+import com.contentgrid.appserver.application.model.i18n.TranslationBuilderSupport;
+import com.contentgrid.appserver.application.model.searchfilters.flags.SearchFilterFlag;
+import com.contentgrid.appserver.application.model.values.AttributeName;
+import com.contentgrid.appserver.application.model.values.FilterName;
+import com.contentgrid.appserver.application.model.values.PropertyPath;
+import lombok.Builder;
+import lombok.Getter;
+import lombok.NonNull;
+
+import java.util.List;
+import java.util.Locale;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * FullTextSearchContentAttributeSearchFilter is a search filter that performs full-text search against
+ * a specific content attribute.
+ *
+ * It needs to be different from the {@link FullTextSearchAttributeSearchFilter} because it uses
+ * custom logic to resolve the content attribute to the correct database table with the extracted
+ * text when querying.
+ */
+@Getter
+public class FullTextSearchContentAttributeSearchFilter extends BaseAttributeSearchFilter implements LocaleAwareSearchFilter {
+ public static final String HIDDEN_EXTRACTED_TEXT_ATTRIBUTE_FORMAT = "%s__cg_text";
+
+ @NonNull private final Locale locale;
+
+ @Builder
+ public FullTextSearchContentAttributeSearchFilter(
+ @NonNull FilterName name,
+ @NonNull ConfigurableTranslatable translations,
+ @NonNull PropertyPath attributePath,
+ @NonNull Set flags,
+ @NonNull Locale locale) {
+ super(name, translations, attributePath, flags);
+
+ this.locale = locale;
+ }
+
+ public static @NonNull FullTextSearchContentAttributeSearchFilterBuilder builder() {
+ return new FullTextSearchContentAttributeSearchFilterBuilder()
+ .translations(new TranslatableImpl<>(ConfigurableSearchFilterTranslations::new))
+ .flags(Set.of());
+ }
+
+ public String getHiddenTextAttributeFormattedName() {
+ List path = this.getAttributePath().toList();
+ // Format the paths from x.y.z to x_y_z so they can be used to construct
+ // the attribute name. However, if a path is formatted like
+ // some.path_to.attribute, and another attribute is formatted like
+ // some.path.to.attribute, both will end up with the same generated name for
+ // the extracted text attribute. To get around this, we can replace all underscores in each
+ // path element's name with two underscores. This way, the paths above become
+ // some_path__to_attribute and some_path_to_attribute.
+ String formattedPath = path.stream()
+ .map(element -> element.replace("_", "__"))
+ .collect(Collectors.joining("_"));
+ return HIDDEN_EXTRACTED_TEXT_ATTRIBUTE_FORMAT.formatted(formattedPath);
+ }
+
+ public static class FullTextSearchContentAttributeSearchFilterBuilder extends TranslationBuilderSupport {
+ {
+ getTranslations = () -> translations;
+ }
+
+ public FullTextSearchContentAttributeSearchFilterBuilder attribute(@NonNull ContentAttribute attribute) {
+ this.attributePath = PropertyPath.of(attribute.getName());
+ return this;
+ }
+ }
+}
diff --git a/contentgrid-appserver-application-model/src/test/java/com/contentgrid/appserver/application/model/EntityTest.java b/contentgrid-appserver-application-model/src/test/java/com/contentgrid/appserver/application/model/EntityTest.java
index 9a52bca83..1151a4453 100644
--- a/contentgrid-appserver-application-model/src/test/java/com/contentgrid/appserver/application/model/EntityTest.java
+++ b/contentgrid-appserver-application-model/src/test/java/com/contentgrid/appserver/application/model/EntityTest.java
@@ -2,6 +2,7 @@
import static org.junit.jupiter.api.Assertions.*;
+import com.contentgrid.appserver.application.model.attributes.Attribute;
import com.contentgrid.appserver.application.model.attributes.CompositeAttribute;
import com.contentgrid.appserver.application.model.attributes.CompositeAttributeImpl;
import com.contentgrid.appserver.application.model.attributes.ContentAttribute;
@@ -16,6 +17,7 @@
import com.contentgrid.appserver.application.model.i18n.UserLocales;
import com.contentgrid.appserver.application.model.searchfilters.AttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.AttributeSearchFilter.Operation;
+import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchContentAttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.SearchFilter;
import com.contentgrid.appserver.application.model.sortable.SortableField;
import com.contentgrid.appserver.application.model.values.ApplicationName;
@@ -30,6 +32,8 @@
import com.contentgrid.appserver.application.model.values.TableName;
import java.util.List;
import java.util.Locale;
+import java.util.Optional;
+
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.ValueSource;
@@ -495,4 +499,26 @@ void entity_translations() {
assertEquals("Couleur", entity.getTranslations(Locale.FRENCH).getSingularName());
assertNull(entity.getTranslations(Locale.FRENCH).getPluralName());
}
+
+ @Test
+ void entity_createsHiddenTextAttribute() {
+ FullTextSearchContentAttributeSearchFilter filter = FullTextSearchContentAttributeSearchFilter
+ .builder()
+ .name(FilterName.of("file-fts-content"))
+ .locale(Locale.ENGLISH)
+ .attribute(CONTENT1)
+ .build();
+ var entity = Entity.builder()
+ .name(EntityName.of("file"))
+ .pathSegment(PathSegmentName.of("files"))
+ .linkName(LinkName.of("file"))
+ .table(TableName.of("file"))
+ .attribute(CONTENT1)
+ .searchFilter(filter)
+ .build();
+ String attrName = filter.getHiddenTextAttributeFormattedName();
+ Optional attribute = entity.getAttributeByName(AttributeName.of(attrName));
+ assertTrue(attribute.isPresent());
+ assertTrue(attribute.get().isIgnored());
+ }
}
diff --git a/contentgrid-appserver-domain/src/main/java/com/contentgrid/appserver/domain/ThunkExpressionGenerator.java b/contentgrid-appserver-domain/src/main/java/com/contentgrid/appserver/domain/ThunkExpressionGenerator.java
index cc6143bbb..319a2ced2 100644
--- a/contentgrid-appserver-domain/src/main/java/com/contentgrid/appserver/domain/ThunkExpressionGenerator.java
+++ b/contentgrid-appserver-domain/src/main/java/com/contentgrid/appserver/domain/ThunkExpressionGenerator.java
@@ -8,7 +8,9 @@
import com.contentgrid.appserver.application.model.searchfilters.AttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.BaseAttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchAttributeSearchFilter;
+import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchContentAttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.SearchFilter;
+import com.contentgrid.appserver.application.model.values.AttributeName;
import com.contentgrid.appserver.application.model.values.AttributePath;
import com.contentgrid.appserver.application.model.values.FilterName;
import com.contentgrid.appserver.application.model.values.PropertyPath;
@@ -47,13 +49,17 @@ static ThunkExpression from(Application application, Entity entity, Map
SearchFilter searchFilter = maybeSearchFilter.get();
- // currently only handle attribute search filters
if (searchFilter instanceof BaseAttributeSearchFilter attributeSearchFilter) {
- var attribute = application.resolvePropertyPath(entity, attributeSearchFilter.getAttributePath());
+ PropertyPath propertyPath = attributeSearchFilter.getAttributePath();
+ // FTS content search filter targets a different attribute than is specified in its filter spec
+ if (searchFilter instanceof FullTextSearchContentAttributeSearchFilter ftsSearchFilter) {
+ propertyPath = PropertyPath.of(AttributeName.of(ftsSearchFilter.getHiddenTextAttributeFormattedName()));
+ }
+ var attribute = application.resolvePropertyPath(entity, propertyPath);
List pathElements;
try {
- pathElements = convertPath(application, entity, attributeSearchFilter.getAttributePath());
+ pathElements = convertPath(application, entity, propertyPath);
} catch (IllegalArgumentException e) {
throw new InvalidParameterException(entity.getName().getValue(), entry.getKey(),
attribute.getType(), entry.getValue().toString(), e);
@@ -121,6 +127,7 @@ private static ThunkExpression createExpression(BaseAttributeSearchFilt
SymbolicReference attr = SymbolicReference.of(Variable.named("entity"), pathElements);
if (filter instanceof FullTextSearchAttributeSearchFilter ftsSearchFilter) return createExpression(ftsSearchFilter, attr, value);
+ if (filter instanceof FullTextSearchContentAttributeSearchFilter ftsContentSearchFilter) return createExpression(ftsContentSearchFilter, attr, value);
if (filter instanceof AttributeSearchFilter attrSearchFilter) return createExpression(attrSearchFilter, attr, value);
throw new IllegalArgumentException("Received unknown filter type (%s).".formatted(filter.getClass().getName()));
@@ -143,6 +150,12 @@ private static ThunkExpression createExpression(FullTextSearchAttribute
return StringComparison.contentGridFullTextSearchMatch(attr, value.assertResultType(String.class), filter.getLocale());
}
+ private static ThunkExpression createExpression(FullTextSearchContentAttributeSearchFilter filter,
+ SymbolicReference attr,
+ Scalar> value) {
+ return StringComparison.contentGridFullTextSearchMatch(attr, value.assertResultType(String.class), filter.getLocale());
+ }
+
private static List convertPath(Application application, Entity entity, PropertyPath path) {
List pathElements = new ArrayList<>();
Entity currentEntity = entity;
diff --git a/contentgrid-appserver-domain/src/test/java/com/contentgrid/appserver/domain/ThunkExpressionGeneratorTest.java b/contentgrid-appserver-domain/src/test/java/com/contentgrid/appserver/domain/ThunkExpressionGeneratorTest.java
index 27e07cfaa..12f3069a5 100644
--- a/contentgrid-appserver-domain/src/test/java/com/contentgrid/appserver/domain/ThunkExpressionGeneratorTest.java
+++ b/contentgrid-appserver-domain/src/test/java/com/contentgrid/appserver/domain/ThunkExpressionGeneratorTest.java
@@ -8,6 +8,7 @@
import com.contentgrid.appserver.application.model.Entity;
import com.contentgrid.appserver.application.model.attributes.CompositeAttribute;
import com.contentgrid.appserver.application.model.attributes.CompositeAttributeImpl;
+import com.contentgrid.appserver.application.model.attributes.ContentAttribute;
import com.contentgrid.appserver.application.model.attributes.SimpleAttribute;
import com.contentgrid.appserver.application.model.attributes.SimpleAttribute.Type;
import com.contentgrid.appserver.application.model.attributes.flags.ReadOnlyFlag;
@@ -19,6 +20,7 @@
import com.contentgrid.appserver.application.model.relations.SourceOneToOneRelation;
import com.contentgrid.appserver.application.model.searchfilters.AttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.AttributeSearchFilter.Operation;
+import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchContentAttributeSearchFilter;
import com.contentgrid.appserver.application.model.values.ApplicationName;
import com.contentgrid.appserver.application.model.values.AttributeName;
import com.contentgrid.appserver.application.model.values.ColumnName;
@@ -30,6 +32,7 @@
import com.contentgrid.appserver.application.model.values.RelationName;
import com.contentgrid.appserver.application.model.values.TableName;
import com.contentgrid.appserver.exception.InvalidParameterException;
+import com.contentgrid.appserver.query.engine.api.thunx.expression.StringComparison;
import com.contentgrid.thunx.predicates.model.Comparison;
import com.contentgrid.thunx.predicates.model.FunctionExpression.Operator;
import com.contentgrid.thunx.predicates.model.LogicalOperation;
@@ -41,6 +44,7 @@
import java.time.Instant;
import java.util.HashMap;
import java.util.List;
+import java.util.Locale;
import java.util.Map;
import java.util.UUID;
import org.junit.jupiter.api.Test;
@@ -110,6 +114,16 @@ class ThunkExpressionGeneratorTest {
.build())
.build();
+ private static final ContentAttribute CONTENT_ATTR = ContentAttribute.builder()
+ .name(AttributeName.of("content"))
+ .pathSegment(PathSegmentName.of("content"))
+ .linkName(LinkName.of("content"))
+ .idColumn(ColumnName.of("content__id"))
+ .filenameColumn(ColumnName.of("content__filename"))
+ .mimetypeColumn(ColumnName.of("content__mimetype"))
+ .lengthColumn(ColumnName.of("content__length"))
+ .build();
+
private static final Entity testEntity = Entity.builder()
.name(EntityName.of("testEntity"))
.table(TableName.of("test_entity"))
@@ -122,6 +136,7 @@ class ThunkExpressionGeneratorTest {
.attribute(TEXT_ATTR)
.attribute(DATETIME_ATTR)
.attribute(COMP_ATTR)
+ .attribute(CONTENT_ATTR)
.searchFilter(AttributeSearchFilter.builder()
.operation(Operation.EXACT)
.name(FilterName.of("count"))
@@ -227,6 +242,11 @@ class ThunkExpressionGeneratorTest {
.name(FilterName.of("shipment.destination"))
.attributePath(PropertyPath.of(RelationName.of("shipment"), AttributeName.of("destination")))
.build())
+ .searchFilter(FullTextSearchContentAttributeSearchFilter.builder()
+ .name(FilterName.of("content~fts"))
+ .attribute(CONTENT_ATTR)
+ .locale(Locale.ENGLISH)
+ .build())
.build();
private static final Entity shipmentEntity = Entity.builder()
@@ -764,4 +784,27 @@ void acrossManyToManyRelationAttributeIsValid() {
);
assertEquals(Scalar.of("A unicorn"), comparison.getRightTerm());
}
+
+ @Test
+ void contentFullTextSearch() {
+ Map> params = Map.of("content~fts", List.of("I am your father"));
+ var entity = testApplication.getEntityByName(EntityName.of("testEntity")).orElseThrow();
+ ThunkExpression result = ThunkExpressionGenerator.from(testApplication, entity, params);
+
+ assertInstanceOf(StringComparison.ContentGridFullTextSearch.class, result);
+ StringComparison.ContentGridFullTextSearch comparison = (StringComparison.ContentGridFullTextSearch) result;
+
+ assertEquals(Locale.ENGLISH, comparison.getLocale());
+ // Check that left term of comparison is the correct reference to the hidden text attribute
+ String textAttrName = FullTextSearchContentAttributeSearchFilter.HIDDEN_EXTRACTED_TEXT_ATTRIBUTE_FORMAT.formatted(CONTENT_ATTR.getName());
+ assertEquals(
+ SymbolicReference.of(
+ Variable.named("entity"),
+ SymbolicReference.path(textAttrName)
+ ),
+ comparison.getLeftTerm()
+ );
+ // Right term should be the search term
+ assertEquals(Scalar.of("I am your father"), comparison.getRightTerm());
+ }
}
\ No newline at end of file
diff --git a/contentgrid-appserver-json-schema/src/main/java/com/contentgrid/appserver/json/DefaultApplicationSchemaConverter.java b/contentgrid-appserver-json-schema/src/main/java/com/contentgrid/appserver/json/DefaultApplicationSchemaConverter.java
index 32b334d7d..1d12e9887 100644
--- a/contentgrid-appserver-json-schema/src/main/java/com/contentgrid/appserver/json/DefaultApplicationSchemaConverter.java
+++ b/contentgrid-appserver-json-schema/src/main/java/com/contentgrid/appserver/json/DefaultApplicationSchemaConverter.java
@@ -11,6 +11,7 @@
import com.contentgrid.appserver.application.model.attributes.flags.AttributeFlag;
import com.contentgrid.appserver.application.model.attributes.flags.CreatedDateFlag;
import com.contentgrid.appserver.application.model.attributes.flags.CreatorFlag;
+import com.contentgrid.appserver.application.model.attributes.flags.SyntheticAttributeFlag;
import com.contentgrid.appserver.application.model.attributes.flags.ETagFlag;
import com.contentgrid.appserver.application.model.attributes.flags.IgnoredFlag;
import com.contentgrid.appserver.application.model.attributes.flags.ModifiedDateFlag;
@@ -30,6 +31,7 @@
import com.contentgrid.appserver.application.model.searchfilters.AttributeSearchFilter.Operation;
import com.contentgrid.appserver.application.model.searchfilters.BaseAttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchAttributeSearchFilter;
+import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchContentAttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.SearchFilter.ConfigurableSearchFilterTranslations;
import com.contentgrid.appserver.application.model.searchfilters.SearchFilter.SearchFilterTranslations;
import com.contentgrid.appserver.application.model.searchfilters.flags.HiddenSearchFilterFlag;
@@ -74,7 +76,6 @@
import com.contentgrid.appserver.json.model.UserAttribute;
import com.contentgrid.appserver.json.validation.ApplicationSchemaValidator;
import com.fasterxml.jackson.databind.ObjectMapper;
-import lombok.NonNull;
import java.io.IOException;
import java.io.InputStream;
@@ -96,6 +97,7 @@
public class DefaultApplicationSchemaConverter implements ApplicationSchemaConverter {
private static final String FTS_TYPE = "full-text";
+ private static final String CONTENT_FTS_TYPE = "full-text-content";
private final ObjectMapper mapper = ApplicationSchemaObjectMapperFactory.createObjectMapper();
private final ApplicationSchemaValidator validator = new ApplicationSchemaValidator();
@@ -316,8 +318,9 @@ private com.contentgrid.appserver.application.model.searchfilters.SearchFilter f
var propertyPath = PropertyPath.of(attrPath);
var filterName = FilterName.of(jsonFilter.getName());
- return type.equals(FTS_TYPE) ? fromJsonFullTextSearchFilter(jsonFilter, propertyPath, filterName)
- : fromJsonAttributeSearchFilter(jsonFilter, type, propertyPath, filterName);
+ if (type.equals(FTS_TYPE)) return fromJsonFullTextSearchFilter(jsonFilter, propertyPath, filterName);
+ else if (type.equals(CONTENT_FTS_TYPE)) return fromJsonContentFullTextSearchFilter(jsonFilter, propertyPath, filterName);
+ else return fromJsonAttributeSearchFilter(jsonFilter, type, propertyPath, filterName);
}
private com.contentgrid.appserver.application.model.searchfilters.FullTextSearchAttributeSearchFilter fromJsonFullTextSearchFilter(
@@ -332,6 +335,18 @@ private com.contentgrid.appserver.application.model.searchfilters.FullTextSearch
.build();
}
+ private com.contentgrid.appserver.application.model.searchfilters.FullTextSearchContentAttributeSearchFilter fromJsonContentFullTextSearchFilter(
+ SearchFilter jsonFilter, PropertyPath propertyPath, FilterName filterName) throws InvalidJsonException {
+ Locale locale = Objects.requireNonNull(jsonFilter.getLocale(), "Full-text search filters require a locale to be set.");
+
+ return SEARCH_FILTER_TRANSLATIONS.mapInto(jsonFilter, FullTextSearchContentAttributeSearchFilter.builder())
+ .name(filterName)
+ .attributePath(propertyPath)
+ .flags(fromJsonSearchFilterFlags(jsonFilter.getFlags()))
+ .locale(locale)
+ .build();
+ }
+
private com.contentgrid.appserver.application.model.searchfilters.SearchFilter fromJsonAttributeSearchFilter(
SearchFilter jsonFilter, String type, PropertyPath propertyPath, FilterName filterName) throws InvalidJsonException {
var operation = switch (type) {
@@ -478,7 +493,12 @@ private Entity toJsonEntity(com.contentgrid.appserver.application.model.Entity e
jsonEntity.setDescription(toJsonTranslations(entity, EntityTranslations::getDescription));
jsonEntity.setTable(entity.getTable().getValue());
jsonEntity.setPrimaryKey((SimpleAttribute) toJsonAttribute(entity.getPrimaryKey()));
- jsonEntity.setAttributes(entity.getAttributes().stream().map(this::toJsonAttribute).toList());
+ jsonEntity.setAttributes(entity
+ .getAttributes()
+ .stream()
+ .filter(attribute -> !attribute.hasFlag(SyntheticAttributeFlag.class))
+ .map(this::toJsonAttribute)
+ .toList());
jsonEntity.setSearchFilters(entity.getSearchFilters().stream()
.filter(searchfilter -> !searchfilter.hasFlag(SyntheticSearchFilterFlag.class))
.map(this::toJsonSearchFilter).toList());
@@ -593,6 +613,10 @@ private SearchFilter toJsonSearchFilter(
type = FTS_TYPE;
jsonFilter.setLocale(fullTextSearchAttributeSearchFilter.getLocale());
}
+ case FullTextSearchContentAttributeSearchFilter fullTextSearchContentAttributeSearchFilter -> {
+ type = CONTENT_FTS_TYPE;
+ jsonFilter.setLocale(fullTextSearchContentAttributeSearchFilter.getLocale());
+ }
default -> throw new IllegalStateException("Unexpected value: " + filter);
}
jsonFilter.setType(type);
diff --git a/contentgrid-appserver-json-schema/src/main/resources/schemas/application-schema.json b/contentgrid-appserver-json-schema/src/main/resources/schemas/application-schema.json
index 3b50d071b..785328951 100644
--- a/contentgrid-appserver-json-schema/src/main/resources/schemas/application-schema.json
+++ b/contentgrid-appserver-json-schema/src/main/resources/schemas/application-schema.json
@@ -677,7 +677,7 @@
"type": {
"type": "string",
"description": "The type of filter.",
- "const": "full-text"
+ "enum": ["full-text", "full-text-content"]
},
"locale": {
"type": "string",
diff --git a/contentgrid-appserver-json-schema/src/test/resources/sample-application.json b/contentgrid-appserver-json-schema/src/test/resources/sample-application.json
index 94c34e41c..abf888a40 100644
--- a/contentgrid-appserver-json-schema/src/test/resources/sample-application.json
+++ b/contentgrid-appserver-json-schema/src/test/resources/sample-application.json
@@ -481,6 +481,12 @@
"attributePath": [{"name": "description", "type": "attr"}],
"type": "full-text",
"locale": "en"
+ },
+ {
+ "name": "contentFilter",
+ "attributePath": [{"name": "file", "type": "attr"}],
+ "type": "full-text-content",
+ "locale": "en"
}
],
"sortableFields": [
diff --git a/contentgrid-appserver-rest/src/main/java/com/contentgrid/appserver/rest/assembler/profile/hal/ProfileSearchParamRepresentationModel.java b/contentgrid-appserver-rest/src/main/java/com/contentgrid/appserver/rest/assembler/profile/hal/ProfileSearchParamRepresentationModel.java
index 537cd8097..29ea4bca5 100644
--- a/contentgrid-appserver-rest/src/main/java/com/contentgrid/appserver/rest/assembler/profile/hal/ProfileSearchParamRepresentationModel.java
+++ b/contentgrid-appserver-rest/src/main/java/com/contentgrid/appserver/rest/assembler/profile/hal/ProfileSearchParamRepresentationModel.java
@@ -3,6 +3,7 @@
import com.contentgrid.appserver.application.model.searchfilters.AttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.BaseAttributeSearchFilter;
import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchAttributeSearchFilter;
+import com.contentgrid.appserver.application.model.searchfilters.FullTextSearchContentAttributeSearchFilter;
import com.contentgrid.appserver.rest.assembler.profile.BlueprintLinkRelations;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonInclude;
@@ -58,6 +59,7 @@ public String toString() {
public static ProfileSearchParamType from(BaseAttributeSearchFilter filter) {
// Locales are not relevant for the profile (at the moment?), so we treat both filter types the same way here.
if (filter instanceof FullTextSearchAttributeSearchFilter) return FULL_TEXT;
+ if (filter instanceof FullTextSearchContentAttributeSearchFilter) return FULL_TEXT;
AttributeSearchFilter attributeSearchFilter = (AttributeSearchFilter) filter;
return switch (attributeSearchFilter.getOperation()) {