Skip to content

Add backwards-compatible serialization for filtration #840

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

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
5 changes: 4 additions & 1 deletion .code-samples.meilisearch.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -227,7 +227,10 @@ get_filterable_attributes_1: |-
client.index("movies").getFilterableAttributesSettings();
update_filterable_attributes_1: |-
Settings settings = new Settings();
settings.setFilterableAttributes(new String[] {"genres", "director"});
Map<String, Boolean> filtersTypes = new HashMap<>();
filtersTypes.put("comparison",true);
filtersTypes.put("equality",true);
settings.setFilterableAttributes(new FilterableAttribute[] {new FilterableAttribute("genres"), new FilterableAttribute(new String[]{"director"}, true, filters)});
client.index("movies").updateSettings(settings);
reset_filterable_attributes_1: |-
client.index("movies").resetFilterableAttributesSettings();
Expand Down
60 changes: 52 additions & 8 deletions src/main/java/com/meilisearch/sdk/Index.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.meilisearch.sdk.model.*;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import lombok.Getter;
Expand Down Expand Up @@ -444,7 +445,7 @@ public Searchable search(SearchRequest searchRequest) throws MeilisearchExceptio
* href="https://www.meilisearch.com/docs/reference/api/facet_search#perform-a-facet-search">API
* specification</a>
* @see Index#getFilterableAttributesSettings() getFilterableAttributesSettings
* @see Index#updateFilterableAttributesSettings(String[]) updateFilterableAttributesSettings
* @see Index#updateFilterableAttributesSettings(Object[]) updateFilterableAttributesSettings
* @since 1.3
*/
public FacetSearchable facetSearch(FacetSearchRequest facetSearchRequest)
Expand Down Expand Up @@ -747,28 +748,71 @@ public TaskInfo resetLocalizedAttributesSettings() throws MeilisearchException {
* specification</a>
*/
public String[] getFilterableAttributesSettings() throws MeilisearchException {
FilterableAttribute[] attributes =
this.settingsHandler.getFilterableAttributesSettings(this.uid);
return Arrays.stream(this.settingsHandler.getFilterableAttributesSettings(this.uid))
.reduce(
new ArrayList<String>(),
(list, next) -> {
list.addAll(Arrays.asList(next.getPatterns()));
return list;
},
(a, b) -> {
a.addAll(b);
return a;
})
.toArray(new String[0]);
}

/**
* Gets the filterable attributes of the index, along with its filtration metadata.
*
* @return filterable attributes of a given uid as FilterableAttribute
* @throws MeilisearchException if an error occurs
* @see <a
* href="https://meilisearch.notion.site/API-usage-Settings-to-opt-out-indexing-features-filterableAttributes-1764b06b651f80aba8bdf359b2df3ca8?pvs=74">API
* Specification</a>
*/
public FilterableAttribute[] getFullFilterableAttributesSettings() throws MeilisearchException {
return this.settingsHandler.getFilterableAttributesSettings(this.uid);
}

/**
* Updates the filterable attributes of the index. This will re-index all documents in the index
* Generic getFilterableAttributesSettings. Updates the filterable attributes of the index. This
* will re-index all documents in the index
*
* @param filterableAttributes An array of strings containing the attributes that can be used as
* filters at query time.
* @param filterableAttributes An array of FilterableAttributes or Strings containing the
* attributes that can be used as filters at query time.
* @return TaskInfo instance
* @throws MeilisearchException if an error occurs
* @throws MeilisearchException if an error occurs in the que
* @see <a
* href="https://www.meilisearch.com/docs/reference/api/settings#update-filterable-attributes">API
* specification</a>
*/
public TaskInfo updateFilterableAttributesSettings(String[] filterableAttributes)
public <O> TaskInfo updateFilterableAttributesSettings(O[] filterableAttributes)
throws MeilisearchException {
if (filterableAttributes == null)
return this.settingsHandler.updateFilterableAttributesSettings(this.uid, null);
else if (filterableAttributes.getClass().getComponentType() == FilterableAttribute.class)
return this.settingsHandler.updateFilterableAttributesSettings(
this.uid, (FilterableAttribute[]) filterableAttributes);
else if (filterableAttributes.getClass().getComponentType() == String.class)
return this.updateFilterableAttributeSettingsLegacy((String[]) filterableAttributes);
else
throw new MeilisearchException(
"filterableAttributes must be of type String or FilterableAttribute");
}

private TaskInfo updateFilterableAttributeSettingsLegacy(String[] filterableAttributes) {
return this.settingsHandler.updateFilterableAttributesSettings(
this.uid, filterableAttributes);
this.uid,
Arrays.stream(filterableAttributes)
.map(FilterableAttribute::new)
.toArray(FilterableAttribute[]::new));
}

/**
* Resets the filterable attributes of the index
* Resets the filterable attributes of te index
*
* @return TaskInfo instance
* @throws MeilisearchException if an error occurs
Expand Down
20 changes: 7 additions & 13 deletions src/main/java/com/meilisearch/sdk/SettingsHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,7 @@

import com.meilisearch.sdk.exceptions.MeilisearchException;
import com.meilisearch.sdk.http.URLBuilder;
import com.meilisearch.sdk.model.Faceting;
import com.meilisearch.sdk.model.LocalizedAttribute;
import com.meilisearch.sdk.model.Pagination;
import com.meilisearch.sdk.model.Settings;
import com.meilisearch.sdk.model.TaskInfo;
import com.meilisearch.sdk.model.TypoTolerance;
import com.meilisearch.sdk.model.*;
import java.util.Map;

/**
Expand Down Expand Up @@ -318,9 +313,10 @@ TaskInfo resetLocalizedAttributesSettings(String uid) throws MeilisearchExceptio
* @return an array of strings that contains the filterable attributes settings
* @throws MeilisearchException if an error occurs
*/
String[] getFilterableAttributesSettings(String uid) throws MeilisearchException {
FilterableAttribute[] getFilterableAttributesSettings(String uid) throws MeilisearchException {
return httpClient.get(
settingsPath(uid).addSubroute("filterable-attributes").getURL(), String[].class);
settingsPath(uid).addSubroute("filterable-attributes").getURL(),
FilterableAttribute[].class);
}

/**
Expand All @@ -332,13 +328,11 @@ String[] getFilterableAttributesSettings(String uid) throws MeilisearchException
* @return TaskInfo instance
* @throws MeilisearchException if an error occurs
*/
TaskInfo updateFilterableAttributesSettings(String uid, String[] filterableAttributes)
throws MeilisearchException {
TaskInfo updateFilterableAttributesSettings(
String uid, FilterableAttribute[] filterableAttributes) throws MeilisearchException {
return httpClient.put(
settingsPath(uid).addSubroute("filterable-attributes").getURL(),
filterableAttributes == null
? httpClient.jsonHandler.encode(filterableAttributes)
: filterableAttributes,
httpClient.jsonHandler.encode(filterableAttributes),
TaskInfo.class);
Comment on lines +331 to 336
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

Updates update method parameter type and simplifies encoding

Changed the parameter type from String[] to FilterableAttribute[] and simplified the JSON encoding logic. The removal of the null check is safe because encode() already handles null values.

Let's verify how the Update method is called:


🏁 Script executed:

#!/bin/bash
# Find calls to updateFilterableAttributesSettings
rg "updateFilterableAttributesSettings" --type java

Length of output: 2055


Breaking Change: UpdateFilterableAttributesSettings Signature Now Requires FilterableAttribute[]

The parameter type was changed from String[] to FilterableAttribute[] and the null‐check was removed (since encode() already handles null). This is a breaking change: all existing call sites passing String[] or relying on the generic overload will fail to compile.

Please update the following locations:

  • src/main/java/com/meilisearch/sdk/Index.java
    The generic method

    public <O> TaskInfo updateFilterableAttributesSettings(O[] filterableAttributes)

    now passes O[] directly to SettingsHandler#updateFilterableAttributesSettings(String, FilterableAttribute[]). You must either:

    • Add an overload taking String... filterableAttributes that converts each String to a FilterableAttribute; or
    • Change all call sites to use FilterableAttribute enums directly.
  • src/test/java/com/meilisearch/integration/SettingsTest.java

  • src/test/java/com/meilisearch/integration/DocumentsTest.java
    Update calls like

    index.updateFilterableAttributesSettings(new String[] {"genres"});
    index.updateFilterableAttributesSettings(null);

    to use FilterableAttribute[] (or your new String... overload).

No change needed in SettingsHandler.java beyond the already applied signature update.

}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
package com.meilisearch.sdk.json;

import com.google.gson.*;
import com.meilisearch.sdk.model.FilterableAttribute;
import java.lang.reflect.Type;
import java.util.*;

/** JSON serializer/deserializer for {@link FilterableAttribute} objects. */
public class GsonFilterableAttributeSerializer
implements JsonSerializer<FilterableAttribute>, JsonDeserializer<FilterableAttribute> {

@Override
public JsonElement serialize(
FilterableAttribute attributes,
Type type,
JsonSerializationContext jsonSerializationContext) {
// when possible, limit size of data sent by using legacy string format
return (canBeString(attributes))
? new JsonPrimitive(attributes.getPatterns()[0])
: serializeAttribute(attributes);
}

private boolean canBeString(FilterableAttribute attribute) {
if (attribute == null) return false;
Map<String, Boolean> filters = attribute.getFilter();
if (filters == null) filters = new HashMap<>();

boolean equalityAllowed = !filters.containsKey("equality") || filters.get("equality");
boolean comparisonAllowed = filters.getOrDefault("comparison", false);

return attribute.getPatterns() != null
&& attribute.getPatterns().length == 1
&& (attribute.getFacetSearch() == null || !attribute.getFacetSearch())
&& equalityAllowed
&& !comparisonAllowed;
}

private JsonElement serializeAttribute(FilterableAttribute attribute) {
if (attribute == null) return null;
List<Exception> exceptions = new ArrayList<>();
JsonArray patternArray = new JsonArray();
if (attribute.getPatterns() != null && attribute.getPatterns().length > 0)
try {
// Collect values from POJO
patternArray =
Arrays.stream(attribute.getPatterns())
.map(JsonPrimitive::new)
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll);
} catch (Exception e) {
exceptions.add(e);
}
else exceptions.add(new JsonParseException("Patterns to filter for were not specified!"));

JsonObject filters = new JsonObject();
if (attribute.getFilter() != null) {
try {
filters =
attribute.getFilter().entrySet().stream()
.collect(
JsonObject::new,
(jsonObject, kv) ->
jsonObject.addProperty(kv.getKey(), kv.getValue()),
this::combineJsonObjects);
} catch (Exception e) {
exceptions.add(e);
}
} else {
filters.addProperty("comparison", false);
filters.addProperty("equality", true);
}

if (!exceptions.isEmpty()) {
throw new JsonParseException(String.join("\n", Arrays.toString(exceptions.toArray())));
}

// Create JSON object
JsonObject jsonObject = new JsonObject();
JsonObject features = new JsonObject();
if (attribute.getFacetSearch() != null)
features.addProperty("facetSearch", attribute.getFacetSearch());
else features.addProperty("facetSearch", false);
features.add("filter", filters);
jsonObject.add("attributePatterns", patternArray);
jsonObject.add("features", features);
return jsonObject;
}

private void combineJsonObjects(JsonObject a, JsonObject b) {
for (Map.Entry<String, JsonElement> kv : b.entrySet()) a.add(kv.getKey(), kv.getValue());
}

@Override
public FilterableAttribute deserialize(
JsonElement jsonElement,
Type type,
JsonDeserializationContext jsonDeserializationContext)
throws JsonParseException {
try {
// legacy check
if (jsonElement.isJsonPrimitive() && jsonElement.getAsJsonPrimitive().isString())
return new FilterableAttribute(jsonElement.getAsString());

JsonObject object = jsonElement.getAsJsonObject();
JsonObject features =
object.has("features") ? object.getAsJsonObject("features") : null;
// default values for instance lacking `features`
boolean facetSearch = false;
Map<String, Boolean> filters = new HashMap<>();
filters.put("equality", true);
filters.put("comparison", false);

List<Exception> exceptions = new ArrayList<>();
// pull values from features.
if (features != null && features.has("facetSearch")) {
try {
JsonPrimitive facetSearchPrimitive = features.getAsJsonPrimitive("facetSearch");
facetSearch =
facetSearchPrimitive != null && facetSearchPrimitive.getAsBoolean();
} catch (ClassCastException | IllegalStateException e) {
exceptions.add(e);
}
}
if (features != null && features.has("filter"))
try {
filters =
features.getAsJsonObject("filter").entrySet().stream()
.collect(
HashMap::new,
(m, kv) ->
m.put(
kv.getKey(),
kv.getValue().getAsBoolean()),
HashMap::putAll);
} catch (ClassCastException | IllegalStateException e) {
exceptions.add(e);
}
String[] patterns = new String[0];
try {
patterns =
object.has("attributePatterns")
? object.getAsJsonArray("attributePatterns").asList().stream()
.map(JsonElement::getAsString)
.toArray(String[]::new)
: new String[0];
} catch (ClassCastException | IllegalStateException e) {
exceptions.add(e);
}

if (!exceptions.isEmpty())
throw new JsonParseException(
String.join("\n", Arrays.toString(exceptions.toArray())));

if (filters.entrySet().stream().noneMatch(Map.Entry::getValue))
exceptions.add(
new JsonParseException(
"No filtration methods were allowed! Must have at least one type <comparison, equality> allowed.\n"
+ Arrays.toString(filters.entrySet().toArray())));
if (patterns.length == 0)
exceptions.add(
new JsonParseException(
"Patterns to filter for were not specified! Invalid Attribute."));

if (!exceptions.isEmpty())
throw new JsonParseException(
String.join("\n", Arrays.toString(exceptions.toArray())));

return new FilterableAttribute(patterns, facetSearch, filters);
} catch (Exception e) {
throw new JsonParseException("Failed to deserialize FilterableAttribute", e);
}
}
}
3 changes: 3 additions & 0 deletions src/main/java/com/meilisearch/sdk/json/GsonJsonHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import com.meilisearch.sdk.exceptions.JsonDecodingException;
import com.meilisearch.sdk.exceptions.JsonEncodingException;
import com.meilisearch.sdk.exceptions.MeilisearchException;
import com.meilisearch.sdk.model.FilterableAttribute;
import com.meilisearch.sdk.model.Key;

public class GsonJsonHandler implements JsonHandler {
Expand All @@ -16,6 +17,8 @@ public class GsonJsonHandler implements JsonHandler {
public GsonJsonHandler() {
GsonBuilder builder = new GsonBuilder();
builder.registerTypeAdapter(Key.class, new GsonKeyTypeAdapter());
builder.registerTypeAdapter(
FilterableAttribute.class, new GsonFilterableAttributeSerializer());
this.gson = builder.create();
}

Expand Down
Loading