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 14 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 8 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
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)});
Comment on lines +230 to +233
Copy link
Contributor

@coderabbitai coderabbitai bot May 28, 2025

Choose a reason for hiding this comment

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

⚠️ Potential issue

Fix variable name typo in code sample.

The map is declared as filtersTypes but the third argument passes filters, which doesn’t exist and will cause a compilation error. It should use filtersTypes. Consider also renaming the variable to filterTypes for clarity.

Apply this diff:

- settings.setFilterableAttributes(new FilterableAttribute[] {
-   new FilterableAttribute("genres"),
-   new FilterableAttribute(new String[]{"director"}, true, filters)
- });
+ settings.setFilterableAttributes(new FilterableAttribute[] {
+   new FilterableAttribute("genres"),
+   new FilterableAttribute(new String[]{"director"}, true, filtersTypes)
+ });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
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)});
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, filtersTypes)
});
🤖 Prompt for AI Agents
In .code-samples.meilisearch.yaml around lines 230 to 233, the variable name
used in the third argument of the FilterableAttribute constructor is incorrect;
it uses 'filters' which is undefined. Rename the variable 'filtersTypes' to
'filterTypes' for clarity and update the constructor argument to use
'filterTypes' instead of 'filters' to fix the compilation error.

Copy link
Member

Choose a reason for hiding this comment

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

Can you apply this change as the @coderabbitai suggested?

Copy link
Contributor

Choose a reason for hiding this comment

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

@brunoocasali I'll apply the suggested fix for the compilation error in the code sample.

Task: Fix compilation error in filterable attributes code sample

  • Fix the undefined variable filters reference to use the correct variable name
  • Optionally improve variable naming for clarity

⏳ Preparing changes...

Copy link
Contributor

Choose a reason for hiding this comment

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

A summary of the edits CodeRabbit can apply:

Fix compilation error in .code-samples.meilisearch.yaml by correcting the variable name reference and improving naming clarity

A plan of each step that CodeRabbit will take in order to execute the edits for each file (with inline rough draft snippets):

✏️ .code-samples.meilisearch.yaml

Replace the variable declaration on line 230 from Map<String, Boolean> filtersTypes = new HashMap<>(); to Map<String, Boolean> filterTypes = new HashMap<>(); to improve naming clarity by removing the redundant 's' and making it more readable.


Update line 231 to use the new variable name by changing filtersTypes.put("comparison",true); to filterTypes.put("comparison", true); - also adding proper spacing around the comma for consistency.


Update line 232 to use the new variable name by changing filtersTypes.put("equality",true); to filterTypes.put("equality", true); - also adding proper spacing around the comma for consistency.


Fix the compilation error on line 233 by replacing the undefined variable filters with the correct variable name filterTypes. Change the constructor call from new FilterableAttribute(new String[]{"director"}, true, filters) to new FilterableAttribute(new String[]{"director"}, true, filterTypes).

A summary of the context that CodeRabbit is considering across the codebase:

Let me check the exact structure and context around the compilation error to understand the FilterableAttribute constructor usage.

  • ✅ Create PR with these edits
  • 📌 Create commit in current branch
  • 📋 Get copyable edits

client.index("movies").updateSettings(settings);
reset_filterable_attributes_1: |-
client.index("movies").resetFilterableAttributesSettings();
Expand Down
58 changes: 51 additions & 7 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,24 +748,67 @@ public TaskInfo resetLocalizedAttributesSettings() throws MeilisearchException {
* specification</a>
*/
public String[] getFilterableAttributesSettings() throws MeilisearchException {
Copy link
Member

Choose a reason for hiding this comment

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

note for myself: Ensure this will be able to handle both old and new styles

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
Copy link
Member

Choose a reason for hiding this comment

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

"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)
Copy link
Member

Choose a reason for hiding this comment

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

It wouldn't be possible to define two methods with the same name but with just different params, so we would have an overloaded version with Strings[] and another with an FilterableAttribute[]?

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));
}

/**
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(
Copy link
Member

Choose a reason for hiding this comment

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

Like I suggested before, do you think it would be possible to have an overload here as well?

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
Contributor

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
Copy link
Member

Choose a reason for hiding this comment

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

Can we call this GsonFilterableAttributeTypeAdapter so it will match the GsonKeyTypeAdapter

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