Skip to content

Commit

Permalink
Add DisjunctiveFaceting
Browse files Browse the repository at this point in the history
  • Loading branch information
Xavier Grand committed Dec 4, 2014
1 parent da11911 commit ca1a0d8
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 0 deletions.
138 changes: 138 additions & 0 deletions src/main/java/com/algolia/search/saas/Index.java
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,17 @@
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.algolia.search.saas.APIClient.IndexQuery;


/*
* Copyright (c) 2013 Algolia
Expand Down Expand Up @@ -644,4 +649,137 @@ public JSONObject updateUserKey(String key, List<String> acls, int validity, int
}
return client.putRequest("/1/indexes/" + encodedIndexName + "/keys/" + key, jsonObject.toString());
}

/**
* Perform a search with disjunctive facets generating as many queries as number of disjunctive facets
* @param query the query
* @param disjunctiveFacets the array of disjunctive facets
* @param refinements Map<String, List<String>> representing the current refinements
* ex: { "my_facet1" => ["my_value1", "my_value2"], "my_disjunctive_facet1" => ["my_value1", "my_value2"] }
* @throws AlgoliaException
*/
public JSONObject disjunctiveFaceting(Query query, List<String> disjunctiveFacets, Map<String, List<String>> refinements) throws AlgoliaException {
if (refinements == null) {
refinements = new HashMap<String, List<String>>();
}
HashMap<String, List<String>> disjunctiveRefinements = new HashMap<String, List<String>>();
for (Map.Entry<String, List<String>> elt : refinements.entrySet()) {
if (disjunctiveFacets.contains(elt.getKey())) {
disjunctiveRefinements.put(elt.getKey(), elt.getValue());
}
}

// build queries
List<IndexQuery> queries = new ArrayList<IndexQuery>();
// hits + regular facets query
StringBuilder filters = new StringBuilder();
boolean first_global = true;
for (Map.Entry<String, List<String>> elt : refinements.entrySet()) {
StringBuilder or = new StringBuilder();
or.append("(");
boolean first = true;
for (String val : elt.getValue()) {
if (disjunctiveRefinements.containsKey(elt.getKey())) {
// disjunctive refinements are ORed
if (!first) {
or.append(',');
}
first = false;
or.append(String.format("%s:%s", elt.getKey(), val));
} else {
if (!first_global) {
filters.append(',');
}
first_global = false;
filters.append(String.format("%s:%s", elt.getKey(), val));
}
}
// Add or
if (disjunctiveRefinements.containsKey(elt.getKey())) {
or.append(')');
if (!first_global) {
filters.append(',');
}
first_global = false;
filters.append(or.toString());
}
}

queries.add(new IndexQuery(this.indexName, new Query(query).setFacetFilters(filters.toString())));
// one query per disjunctive facet (use all refinements but the current one + hitsPerPage=1 + single facet
for (String disjunctiveFacet : disjunctiveFacets) {
filters = new StringBuilder();
first_global = true;
for (Map.Entry<String, List<String>> elt : refinements.entrySet()) {
if (disjunctiveFacet.equals(elt.getKey())) {
continue;
}
StringBuilder or = new StringBuilder();
or.append("(");
boolean first = true;
for (String val : elt.getValue()) {
if (disjunctiveRefinements.containsKey(elt.getKey())) {
// disjunctive refinements are ORed
if (!first) {
or.append(',');
}
first = false;
or.append(String.format("%s:%s", elt.getKey(), val));
} else {
if (!first_global) {
filters.append(',');
}
first_global = false;
filters.append(String.format("%s:%s", elt.getKey(), val));
}
}
// Add or
if (disjunctiveRefinements.containsKey(elt.getKey())) {
or.append(')');
if (!first_global) {
filters.append(',');
}
first_global = false;
filters.append(or.toString());
}
}
List<String> facets = new ArrayList<String>();
facets.add(disjunctiveFacet);
queries.add(new IndexQuery(this.indexName, new Query(query).setHitsPerPage(1).setAttributesToRetrieve(new ArrayList<String>()).setAttributesToHighlight(new ArrayList<String>()).setAttributesToSnippet(new ArrayList<String>()).setFacets(facets).setFacetFilters(filters.toString())));
}
JSONObject answers = this.client.multipleQueries(queries);

// aggregate answers
// first answer stores the hits + regular facets
try {
JSONArray results = answers.getJSONArray("results");
JSONObject aggregatedAnswer = results.getJSONObject(0);
JSONObject disjunctiveFacetsJSON = new JSONObject();
for (int i = 1; i < results.length(); ++i) {
JSONObject facets = results.getJSONObject(i).getJSONObject("facets");
Iterator<String> keys = facets.keys();
while(keys.hasNext()) {
String key = keys.next();
// Add the facet to the disjunctive facet hash
disjunctiveFacetsJSON.put(key, facets.getJSONObject(key));
// concatenate missing refinements
if (!disjunctiveRefinements.containsKey(key)) {
continue;
}
for (String refine : disjunctiveRefinements.get(key)) {
if (!disjunctiveFacetsJSON.getJSONObject(key).has(refine)) {
disjunctiveFacetsJSON.getJSONObject(key).put(refine, 0);
}
}
}
}
aggregatedAnswer.put("disjunctiveFacets", disjunctiveFacetsJSON);
return aggregatedAnswer;
} catch (JSONException e) {
throw new Error(e);
}
}
public JSONObject disjunctiveFaceting(Query query, List<String> disjunctiveFacets) throws AlgoliaException {
return disjunctiveFaceting(query, disjunctiveFacets, null);
}
}
39 changes: 39 additions & 0 deletions src/main/java/com/algolia/search/saas/Query.java
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.List;

import org.json.JSONArray;
Expand Down Expand Up @@ -110,6 +111,44 @@ public Query() {
analytics = synonyms = replaceSynonyms = typoTolerance = allowTyposOnNumericTokens = true;
removeWordsIfNoResult = RemoveWordsType.REMOVE_NONE;
}

public Query(Query other) {
if (other.attributesToHighlight != null) {
attributesToHighlight = new ArrayList<String>(other.attributesToHighlight);
}
if (other.attributes != null) {
attributes = new ArrayList<String>(other.attributes);
}
if (other.attributesToSnippet != null) {
attributesToSnippet = new ArrayList<String>(other.attributesToSnippet);
}
minWordSizeForApprox1 = other.minWordSizeForApprox1;
minWordSizeForApprox2 = other.minWordSizeForApprox2;
getRankingInfo = other.getRankingInfo;
ignorePlural = other.ignorePlural;
distinct = other.distinct;
advancedSyntax = other.advancedSyntax;
page = other.page;
hitsPerPage = other.hitsPerPage;
restrictSearchableAttributes = other.restrictSearchableAttributes;
tags = other.tags;
numerics = other.numerics;
insideBoundingBox = other.insideBoundingBox;
aroundLatLong = other.aroundLatLong;
aroundLatLongViaIP = other.aroundLatLongViaIP;
query = other.query;
queryType = other.queryType;
optionalWords = other.optionalWords;
facets = other.facets;
facetsFilter = other.facetsFilter;
maxNumberOfFacets = other.maxNumberOfFacets;
analytics = other.analytics;
synonyms = other.synonyms;
replaceSynonyms = other.replaceSynonyms;
typoTolerance = other.typoTolerance;
allowTyposOnNumericTokens = other.allowTyposOnNumericTokens;
removeWordsIfNoResult = other.removeWordsIfNoResult;
}

/**
* Select the strategy to adopt when a query does not return any result.
Expand Down
55 changes: 55 additions & 0 deletions src/test/java/com/algolia/search/saas/SimpleTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;

import org.json.JSONArray;
Expand Down Expand Up @@ -600,5 +601,59 @@ public void test36_deleteByQuery() throws JSONException, AlgoliaException {
JSONObject res = index.search(new Query(""));
assertEquals(1, res.getInt("nbHits"));
}

@Test
public void test37_disjunctiveFaceting() throws AlgoliaException, JSONException {
index.setSettings(new JSONObject("{\"attributesForFaceting\":[\"city\", \"stars\", \"facilities\"]}"));
JSONObject task = index.addObjects(new JSONArray()
.put(new JSONObject("{\"name\":\"Hotel A\", \"stars\":\"*\", \"facilities\":[\"wifi\", \"bath\", \"spa\"], \"city\":\"Paris\"}"))
.put(new JSONObject("{\"name\":\"Hotel B\", \"stars\":\"*\", \"facilities\":[\"wifi\"], \"city\":\"Paris\"}"))
.put(new JSONObject("{\"name\":\"Hotel C\", \"stars\":\"**\", \"facilities\":[\"bath\"], \"city\":\"San Fancisco\"}"))
.put(new JSONObject("{\"name\":\"Hotel D\", \"stars\":\"****\", \"facilities\":[\"spa\"], \"city\":\"Paris\"}"))
.put(new JSONObject("{\"name\":\"Hotel E\", \"stars\":\"****\", \"facilities\":[\"spa\"], \"city\":\"New York\"}")));
index.waitTask(task.getString("taskID"));
HashMap<String, List<String>> refinements = new HashMap<String, List<String>>();
List<String> disjunctiveFacets = new ArrayList<String>();
List<String> facets = new ArrayList<String>();
facets.add("city");
disjunctiveFacets.add("stars");
disjunctiveFacets.add("facilities");
JSONObject answer = index.disjunctiveFaceting(new Query("h").setFacets(facets), disjunctiveFacets);
assertEquals(5, answer.getInt("nbHits"));
assertEquals(1, answer.getJSONObject("facets").length());
assertEquals(2, answer.getJSONObject("disjunctiveFacets").length());

ArrayList<String> refineValue = new ArrayList<String>();
refineValue.add("*");
refinements.put("stars", refineValue);
answer = index.disjunctiveFaceting(new Query("h").setFacets(facets), disjunctiveFacets, refinements);
assertEquals(2, answer.getInt("nbHits"));
assertEquals(1, answer.getJSONObject("facets").length());
assertEquals(2, answer.getJSONObject("disjunctiveFacets").length());
assertEquals(2, answer.getJSONObject("disjunctiveFacets").getJSONObject("stars").getInt("*"));
assertEquals(1, answer.getJSONObject("disjunctiveFacets").getJSONObject("stars").getInt("**"));
assertEquals(2, answer.getJSONObject("disjunctiveFacets").getJSONObject("stars").getInt("****"));

refineValue = new ArrayList<String>();
refineValue.add("Paris");
refinements.put("city", refineValue);
answer = index.disjunctiveFaceting(new Query("h").setFacets(facets), disjunctiveFacets, refinements);
assertEquals(2, answer.getInt("nbHits"));
assertEquals(1, answer.getJSONObject("facets").length());
assertEquals(2, answer.getJSONObject("disjunctiveFacets").length());
assertEquals(2, answer.getJSONObject("disjunctiveFacets").getJSONObject("stars").getInt("*"));
assertEquals(1, answer.getJSONObject("disjunctiveFacets").getJSONObject("stars").getInt("****"));

refineValue = new ArrayList<String>();
refineValue.add("*");
refineValue.add("****");
refinements.put("stars", refineValue);
answer = index.disjunctiveFaceting(new Query("h").setFacets(facets), disjunctiveFacets, refinements);
assertEquals(3, answer.getInt("nbHits"));
assertEquals(1, answer.getJSONObject("facets").length());
assertEquals(2, answer.getJSONObject("disjunctiveFacets").length());
assertEquals(2, answer.getJSONObject("disjunctiveFacets").getJSONObject("stars").getInt("*"));
assertEquals(1, answer.getJSONObject("disjunctiveFacets").getJSONObject("stars").getInt("****"));
}

}

0 comments on commit ca1a0d8

Please sign in to comment.