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 5 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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)});
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 {
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));
}

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

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
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<>();
return (attribute.getPatterns() != null
&& attribute.getPatterns().length == 1
&& (attribute.getFacetSearch() == null || !attribute.getFacetSearch())
&& (filters.containsKey("equality") && filters.get("equality"))
&& (!filters.containsKey("comparison") || !filters.get("comparison")));
}

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