Author: tommaso
Date: Tue Dec 15 10:03:54 2015
New Revision: 1720108
URL: http://svn.apache.org/viewvc?rev=1720108&view=rev
Log:
OAK-2510 - support for facets in solr index
Added:
jackrabbit/oak/trunk/oak-solr-core/src/test/java/org/apache/jackrabbit/oak/jcr/query/FacetTest.java
(with props)
Modified:
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java
jackrabbit/oak/trunk/oak-solr-core/src/main/resources/solr/oak/conf/schema.xml
jackrabbit/oak/trunk/oak-solr-core/src/test/resources/solr/oak/conf/schema.xml
Modified:
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java?rev=1720108&r1=1720107&r2=1720108&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java
(original)
+++
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/FilterQueryParser.java
Tue Dec 15 10:03:54 2015
@@ -19,6 +19,7 @@ package org.apache.jackrabbit.oak.plugin
import java.util.Collection;
import java.util.List;
+import org.apache.jackrabbit.oak.api.Type;
import
org.apache.jackrabbit.oak.plugins.index.solr.configuration.OakSolrConfiguration;
import org.apache.jackrabbit.oak.query.QueryImpl;
import org.apache.jackrabbit.oak.query.fulltext.FullTextAnd;
@@ -63,6 +64,17 @@ class FilterQueryParser {
}
}
+ // facet enable
+ List<Filter.PropertyRestriction> facetRestriction =
filter.getPropertyRestrictions(QueryImpl.REP_FACET);
+ if (facetRestriction != null && facetRestriction.size() > 0) {
+ solrQuery.setFacetMinCount(1);
+ solrQuery.setFacet(true);
+ for (Filter.PropertyRestriction pr : facetRestriction) {
+ String value = pr.first.getValue(Type.STRING);
+
solrQuery.addFacetField(value.substring(QueryImpl.REP_FACET.length() + 1,
value.length() - 1)+"_facet");
+ }
+ }
+
if (sortOrder != null) {
for (QueryIndex.OrderEntry orderEntry : sortOrder) {
SolrQuery.ORDER order;
Modified:
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java?rev=1720108&r1=1720107&r2=1720108&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java
(original)
+++
jackrabbit/oak/trunk/oak-solr-core/src/main/java/org/apache/jackrabbit/oak/plugins/index/solr/query/SolrQueryIndex.java
Tue Dec 15 10:03:54 2015
@@ -23,6 +23,7 @@ import java.util.Collections;
import java.util.Deque;
import java.util.HashSet;
import java.util.Iterator;
+import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -53,6 +54,7 @@ import org.apache.solr.client.solrj.Solr
import org.apache.solr.client.solrj.SolrServer;
import org.apache.solr.client.solrj.SolrServerException;
import org.apache.solr.client.solrj.embedded.EmbeddedSolrServer;
+import org.apache.solr.client.solrj.response.FacetField;
import org.apache.solr.client.solrj.response.QueryResponse;
import org.apache.solr.client.solrj.response.SpellCheckResponse;
import org.apache.solr.common.SolrDocument;
@@ -237,6 +239,7 @@ public class SolrQueryIndex implements F
private AbstractIterator<SolrResultRow> getIterator(final Filter filter,
final List<OrderEntry> sortOrder, final String parent, final int parentDepth) {
return new AbstractIterator<SolrResultRow>() {
+ public Collection<FacetField> facetFields = new
LinkedList<FacetField>();
private final Set<String> seenPaths = Sets.newHashSet();
private final Deque<SolrResultRow> queue = Queues.newArrayDeque();
private int offset = 0;
@@ -270,7 +273,7 @@ public class SolrQueryIndex implements F
if (scoreObj != null) {
score = (Float) scoreObj;
}
- return new SolrResultRow(path, score, doc);
+ return new SolrResultRow(path, score, doc, facetFields);
}
@@ -343,6 +346,25 @@ public class SolrQueryIndex implements F
}
}
+ // get facets
+ List<FacetField> returnedFieldFacet =
queryResponse.getFacetFields();
+ if (returnedFieldFacet != null) {
+ facetFields.addAll(returnedFieldFacet);
+ }
+
+ // filter facets on doc paths
+ if (!facetFields.isEmpty() && docs != null) {
+ for (SolrDocument doc : docs) {
+ String path =
String.valueOf(doc.getFieldValue(configuration.getPathField()));
+ // if facet path doesn't exist in the node state,
filter the facets
+ for (FacetField ff : facetFields) {
+ if (!filter.isAccessible(path + "/" +
ff.getName())) {
+ filterFacet(doc, ff);
+ }
+ }
+ }
+ }
+
// handle spellcheck
SpellCheckResponse spellCheckResponse =
queryResponse.getSpellCheckResponse();
if (spellCheckResponse != null &&
spellCheckResponse.getSuggestions() != null &&
@@ -374,6 +396,36 @@ public class SolrQueryIndex implements F
};
}
+ private void filterFacet(SolrDocument doc, FacetField facetField) {
+ // facet filtering by value requires that the facet values match the
stored values
+ // a *_facet field must exist, storing docValues to be used for
faceting and at filtering time
+ if (doc.getFieldNames().contains(facetField.getName())) {
+ // decrease facet value
+ Collection<Object> docFieldValues =
doc.getFieldValues(facetField.getName());
+ if (docFieldValues != null) {
+ for (Object docFieldValue : docFieldValues) {
+ String valueString = String.valueOf(docFieldValue);
+ List<FacetField.Count> toRemove = new
LinkedList<FacetField.Count>();
+ for (FacetField.Count count : facetField.getValues()) {
+ long existingCount = count.getCount();
+ if (valueString.equals(count.getName())) {
+ if (existingCount > 1) {
+ // decrease the count
+ count.setCount(existingCount - 1);
+ } else {
+ // remove the entire entry
+ toRemove.add(count);
+ }
+ }
+ }
+ for (FacetField.Count f : toRemove) {
+ assert facetField.getValues().remove(f);
+ }
+ }
+ }
+ }
+ }
+
private void putSpellChecks(SpellCheckResponse spellCheckResponse,
final Deque<SolrResultRow> queue,
Filter filter) throws
SolrServerException {
@@ -450,7 +502,9 @@ public class SolrQueryIndex implements F
(!configuration.useForPropertyRestrictions() // Solr index not
used for properties
|| (configuration.getUsedProperties().size() > 0 &&
!configuration.getUsedProperties().contains(propertyName)) // not explicitly
contained in the used properties
|| propertyName.contains("/") // no child-level
property restrictions
- || "rep:excerpt".equals(propertyName) // rep:excerpt
is not handled at the property level
+ || QueryImpl.REP_EXCERPT.equals(propertyName) //
rep:excerpt is not handled at the property level
+ ||
QueryImpl.OAK_SCORE_EXPLANATION.equals(propertyName) // score explain is not
handled at the property level
+ || QueryImpl.REP_FACET.equals(propertyName) //
rep:facet is not handled at the property level
||
QueryConstants.RESTRICTION_LOCAL_NAME.equals(propertyName)
||
configuration.getIgnoredProperties().contains(propertyName));
}
@@ -498,25 +552,31 @@ public class SolrQueryIndex implements F
final String path;
final double score;
final SolrDocument doc;
+ final Collection<FacetField> facetFields;
final String suggestion;
- private SolrResultRow(String path, double score, SolrDocument doc,
String suggestion) {
+ private SolrResultRow(String path, double score, SolrDocument doc,
String suggestion, Collection<FacetField> facetFields) {
this.path = path;
this.score = score;
this.doc = doc;
this.suggestion = suggestion;
+ this.facetFields = facetFields;
}
SolrResultRow(String path, double score, SolrDocument doc) {
- this (path, score, doc, null);
+ this (path, score, doc, null, null);
}
SolrResultRow(String suggestion, double score) {
- this ("/", score, null, suggestion);
+ this ("/", score, null, suggestion, null);
}
SolrResultRow(String suggestion) {
- this ("/", 1.0, null, suggestion);
+ this ("/", 1.0, null, suggestion, null);
+ }
+
+ SolrResultRow(String path, float score, SolrDocument doc,
Collection<FacetField> facetFields) {
+ this(path, score, doc, null, facetFields);
}
@Override
@@ -593,6 +653,21 @@ public class SolrQueryIndex implements F
if (QueryImpl.JCR_SCORE.equals(columnName)) {
return PropertyValues.newDouble(currentRow.score);
}
+ if (columnName.startsWith(QueryImpl.REP_FACET)) {
+ String facetFieldName =
columnName.substring(QueryImpl.REP_FACET.length() + 1, columnName.length() - 1);
+ FacetField facetField = null;
+ for (FacetField ff : currentRow.facetFields) {
+ if (ff.getName().equals(facetFieldName +
"_facet")) {
+ facetField = ff;
+ break;
+ }
+ }
+ if (facetField != null) {
+ return PropertyValues.newString(facetFieldName +
":" + facetField.getValues().toString());
+ } else {
+ return null;
+ }
+ }
if (QueryImpl.REP_SPELLCHECK.equals(columnName) ||
QueryImpl.REP_SUGGEST.equals(columnName)) {
return PropertyValues.newString(currentRow.suggestion);
}
Modified:
jackrabbit/oak/trunk/oak-solr-core/src/main/resources/solr/oak/conf/schema.xml
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-solr-core/src/main/resources/solr/oak/conf/schema.xml?rev=1720108&r1=1720107&r2=1720108&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-solr-core/src/main/resources/solr/oak/conf/schema.xml
(original)
+++
jackrabbit/oak/trunk/oak-solr-core/src/main/resources/solr/oak/conf/schema.xml
Tue Dec 15 10:03:54 2015
@@ -135,6 +135,9 @@
<field name="path_depth" type="tint" indexed="true" stored="false"/>
<field name="_version_" type="long" indexed="true" stored="true"/>
+ <!-- facet fields -->
+ <dynamicField name="*_facet" type="string" indexed="false"
stored="false" docValues="true" multiValued="true"/>
+
<!-- sorting dynamic fields -->
<dynamicField name="*_double_sort" type="tdouble" indexed="false"
stored="false" multiValued="false" docValues="true"/>
<dynamicField name="*_string_sort" type="string" indexed="false"
stored="false" multiValued="false" docValues="true"/>
@@ -149,6 +152,7 @@
<copyField source="path_exact" dest="path_child"/>
<copyField source="path_exact" dest=":path"/>
<copyField source="*" dest="catch_all"/>
+ <copyField source="*" dest="*_facet"/>
<copyField source="jcr:title" dest=":spellcheck"/>
<copyField source="jcr:description" dest=":spellcheck"/>
<copyField source="jcr:title" dest=":suggest"/>
Added:
jackrabbit/oak/trunk/oak-solr-core/src/test/java/org/apache/jackrabbit/oak/jcr/query/FacetTest.java
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-solr-core/src/test/java/org/apache/jackrabbit/oak/jcr/query/FacetTest.java?rev=1720108&view=auto
==============================================================================
---
jackrabbit/oak/trunk/oak-solr-core/src/test/java/org/apache/jackrabbit/oak/jcr/query/FacetTest.java
(added)
+++
jackrabbit/oak/trunk/oak-solr-core/src/test/java/org/apache/jackrabbit/oak/jcr/query/FacetTest.java
Tue Dec 15 10:03:54 2015
@@ -0,0 +1,168 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.jackrabbit.oak.jcr.query;
+
+import org.apache.jackrabbit.core.query.AbstractQueryTest;
+import org.apache.jackrabbit.oak.api.Type;
+
+import javax.jcr.Node;
+import javax.jcr.RepositoryException;
+import javax.jcr.Session;
+import javax.jcr.Value;
+import javax.jcr.query.Query;
+import javax.jcr.query.QueryManager;
+import javax.jcr.query.QueryResult;
+import javax.jcr.query.Row;
+import javax.jcr.query.RowIterator;
+
+/**
+ * Test for faceting capabilities via JCR API
+ */
+public class FacetTest extends AbstractQueryTest {
+
+ public void testFacetRetrieval() throws Exception {
+ Session session = superuser;
+ QueryManager qm = session.getWorkspace().getQueryManager();
+ Node n1 = testRootNode.addNode("node1");
+ n1.setProperty("text", "hello");
+ Node n2 = testRootNode.addNode("node2");
+ n2.setProperty("text", "hallo");
+ Node n3 = testRootNode.addNode("node3");
+ n3.setProperty("text", "oh hallo");
+ session.save();
+
+ String sql2 = "select [jcr:path], [rep:facet(text)] from [nt:base] " +
+ "where contains([text], 'hello OR hallo') order by [jcr:path]";
+ Query q = qm.createQuery(sql2, Query.JCR_SQL2);
+ QueryResult result = q.execute();
+ String facetResult = "text:[hallo (1), hello (1), oh hallo (1)]";
+ assertEquals(facetResult + ", " + facetResult + ", " + facetResult,
getResult(result, "rep:facet(text)"));
+ }
+
+ public void testFacetRetrievalMV() throws Exception {
+ Session session = superuser;
+ QueryManager qm = session.getWorkspace().getQueryManager();
+ Node n1 = testRootNode.addNode("node1");
+ n1.setProperty("jcr:title", "apache jackrabbit oak");
+ n1.setProperty("tags", new String[]{"software", "repository",
"apache"});
+ Node n2 = testRootNode.addNode("node2");
+ n2.setProperty("jcr:title", "oak furniture");
+ n2.setProperty("tags", "furniture");
+ Node n3 = testRootNode.addNode("node3");
+ n3.setProperty("jcr:title", "oak cosmetics");
+ n3.setProperty("tags", "cosmetics");
+ Node n4 = testRootNode.addNode("node4");
+ n4.setProperty("jcr:title", "oak and aem");
+ n4.setProperty("tags", new String[]{"software", "repository", "aem"});
+ session.save();
+
+ String sql2 = "select [jcr:path], [rep:facet(tags)] from [nt:base] " +
+ "where contains([jcr:title], 'oak') order by [jcr:path]";
+ Query q = qm.createQuery(sql2, Query.JCR_SQL2);
+ QueryResult result = q.execute();
+ String facetResult = "tags:[repository (2), software (2), aem (1),
apache (1), cosmetics (1), furniture (1)], tags:[repository (2), software (2),
aem (1), apache (1), cosmetics (1), furniture (1)], tags:[repository (2),
software (2), aem (1), apache (1), cosmetics (1), furniture (1)],
tags:[repository (2), software (2), aem (1), apache (1), cosmetics (1),
furniture (1)]";
+ assertEquals(facetResult, getResult(result, "rep:facet(tags)"));
+ }
+
+ public void testFacetRetrievalWithAnonymousUser() throws Exception {
+ Session session = superuser;
+
+ Node n1 = testRootNode.addNode("node1");
+ n1.setProperty("text", "hello");
+ Node n2 = testRootNode.addNode("node2");
+ n2.setProperty("text", "hallo");
+ Node n3 = testRootNode.addNode("node3");
+ n3.setProperty("text", "oh hallo");
+ session.save();
+
+ session = getHelper().getReadOnlySession();
+ QueryManager qm = session.getWorkspace().getQueryManager();
+
+ String sql2 = "select [jcr:path], [rep:facet(text)] from [nt:base] " +
+ "where contains([text], 'hello OR hallo') order by [jcr:path]";
+ Query q = qm.createQuery(sql2, Query.JCR_SQL2);
+ QueryResult result = q.execute();
+ String facetResult = "text:[hallo (1), hello (1), oh hallo (1)]";
+ assertEquals(facetResult + ", " + facetResult + ", " + facetResult,
getResult(result, "rep:facet(text)"));
+ }
+
+ public void testFacetRetrieval2() throws Exception {
+ Session session = superuser;
+ QueryManager qm = session.getWorkspace().getQueryManager();
+ Node n1 = testRootNode.addNode("node1");
+ String pn = "jcr:title";
+ n1.setProperty(pn, "hello");
+ Node n2 = testRootNode.addNode("node2");
+ n2.setProperty(pn, "hallo");
+ Node n3 = testRootNode.addNode("node3");
+ n3.setProperty(pn, "oh hallo");
+ session.save();
+
+ String sql2 = "select [jcr:path], [rep:facet(" + pn + ")] from
[nt:base] " +
+ "where contains([" + pn + "], 'hallo') order by [jcr:path]";
+ Query q = qm.createQuery(sql2, Query.JCR_SQL2);
+ QueryResult result = q.execute();
+ String facetResult = pn + ":[hallo (1), oh hallo (1)]";
+ assertEquals(facetResult + ", " + facetResult, getResult(result,
"rep:facet(" + pn + ")"));
+ }
+
+ public void testMultipleFacetsRetrieval() throws Exception {
+ Session session = superuser;
+ QueryManager qm = session.getWorkspace().getQueryManager();
+ Node n1 = testRootNode.addNode("node1");
+ String pn = "jcr:title";
+ String pn2 = "jcr:description";
+ n1.setProperty(pn, "hello");
+ n1.setProperty(pn2, "a");
+ Node n2 = testRootNode.addNode("node2");
+ n2.setProperty(pn, "hallo");
+ n2.setProperty(pn2, "b");
+ Node n3 = testRootNode.addNode("node3");
+ n3.setProperty(pn, "oh hallo");
+ n3.setProperty(pn2, "a");
+ session.save();
+
+ String sql2 = "select [jcr:path], [rep:facet(" + pn + ")],
[rep:facet(" + pn2 + ")] from [nt:base] " +
+ "where contains([" + pn + "], 'hallo') order by [jcr:path]";
+ Query q = qm.createQuery(sql2, Query.JCR_SQL2);
+ QueryResult result = q.execute();
+ String facetResult = pn + ":[hallo (1), oh hallo (1)], " + pn2 + ":[a
(1), b (1)], " + pn + ":[hallo (1), oh hallo (1)], " + pn2 + ":[a (1), b (1)]";
+ assertEquals(facetResult, getResult(result, "rep:facet(" + pn + ")",
"rep:facet(" + pn2 + ")"));
+ }
+
+ static String getResult(QueryResult result, String... propertyNames)
throws RepositoryException {
+ StringBuilder buff = new StringBuilder();
+ RowIterator it = result.getRows();
+ while (it.hasNext()) {
+
+ Row row = it.nextRow();
+ for (String propertyName : propertyNames) {
+ Value value = row.getValue(propertyName);
+ if (value != null) {
+ if (buff.length() > 0) {
+ buff.append(", ");
+ }
+ buff.append(value.getString());
+ }
+ }
+ }
+ return buff.toString();
+ }
+
+}
\ No newline at end of file
Propchange:
jackrabbit/oak/trunk/oak-solr-core/src/test/java/org/apache/jackrabbit/oak/jcr/query/FacetTest.java
------------------------------------------------------------------------------
svn:eol-style = native
Modified:
jackrabbit/oak/trunk/oak-solr-core/src/test/resources/solr/oak/conf/schema.xml
URL:
http://svn.apache.org/viewvc/jackrabbit/oak/trunk/oak-solr-core/src/test/resources/solr/oak/conf/schema.xml?rev=1720108&r1=1720107&r2=1720108&view=diff
==============================================================================
---
jackrabbit/oak/trunk/oak-solr-core/src/test/resources/solr/oak/conf/schema.xml
(original)
+++
jackrabbit/oak/trunk/oak-solr-core/src/test/resources/solr/oak/conf/schema.xml
Tue Dec 15 10:03:54 2015
@@ -147,6 +147,9 @@
<field name="path_depth" type="tint" indexed="true" stored="false"/>
<field name="_version_" type="long" indexed="true" stored="true"/>
+ <!-- facet fields -->
+ <dynamicField name="*_facet" type="string" indexed="false"
stored="false" docValues="true" multiValued="true"/>
+
<!-- sorting dynamic fields -->
<dynamicField name="*_double_sort" type="tdouble" indexed="false"
stored="false" multiValued="false" docValues="true"/>
<dynamicField name="*_string_sort" type="string" indexed="false"
stored="false" multiValued="false" docValues="true"/>
@@ -160,6 +163,7 @@
<copyField source="path_exact" dest="path_child"/>
<copyField source="path_exact" dest=":path"/>
<copyField source="*" dest="catch_all"/>
+ <copyField source="*" dest="*_facet"/>
<copyField source="jcr:title" dest=":spellcheck"/>
<copyField source="jcr:description" dest=":spellcheck"/>
<copyField source="jcr:title" dest=":suggest"/>