diff --git a/solrdf/src/main/java/org/gazzax/labs/solrdf/Strings.java b/solrdf/src/main/java/org/gazzax/labs/solrdf/Strings.java index 02e2927..a67e84e 100644 --- a/solrdf/src/main/java/org/gazzax/labs/solrdf/Strings.java +++ b/solrdf/src/main/java/org/gazzax/labs/solrdf/Strings.java @@ -29,6 +29,16 @@ public static boolean isNullOrEmpty(final String value) { public static String round(final String numericStringValue) { final int indexOfDot = numericStringValue.indexOf("."); - return indexOfDot != -1 ? numericStringValue.substring(0, indexOfDot) : numericStringValue; + if (indexOfDot == -1) { + return numericStringValue; + } + + final String d = numericStringValue.substring(indexOfDot + 1); + for (int index = 0; index < d.length(); index++) { + if (d.charAt(index) != '0') { + return numericStringValue; + } + } + return numericStringValue.substring(0, indexOfDot); } } \ No newline at end of file diff --git a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/FacetQuery.java b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/FacetQuery.java index 8d0d57f..3c84f72 100644 --- a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/FacetQuery.java +++ b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/FacetQuery.java @@ -18,6 +18,10 @@ * @see https://cwiki.apache.org/confluence/display/solr/Faceting#Faceting-RangeFaceting */ public abstract class FacetQuery { + public final static String STRING_HINT = "str"; + public final static String BOOLEAN_HINT = "bool"; + public final static String NUMERIC_HINT = "num"; + public final static String DATE_HINT = "date"; protected final int index; diff --git a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/NumericFacets.java b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/NumericFacets.java index bc0930a..18dfc3f 100644 --- a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/NumericFacets.java +++ b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/NumericFacets.java @@ -1,16 +1,14 @@ package org.gazzax.labs.solrdf.handler.search.faceting; +import static org.gazzax.labs.solrdf.Strings.round; + import java.io.IOException; -import java.util.ArrayDeque; import java.util.Collections; -import java.util.Deque; import java.util.HashMap; -import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; -import java.util.Set; -import static org.gazzax.labs.solrdf.Strings.*; + import org.apache.lucene.document.FieldType.NumericType; import org.apache.lucene.index.AtomicReaderContext; import org.apache.lucene.index.ReaderUtil; @@ -45,9 +43,6 @@ */ final class NumericFacets { - NumericFacets() { - } - static class HashTable { static final float LOAD_FACTOR = 0.7f; @@ -132,11 +127,8 @@ public static NamedList getCounts( final int mincount, final boolean missing, final String sort) throws IOException { - final boolean zeros = mincount <= 0; -// mincount = Math.max(mincount, 1); - - final SchemaField sf = searcher.getSchema().getField(fieldName); - final FieldType ft = sf.getType(); + final SchemaField schemaField = searcher.getSchema().getField(fieldName); + final FieldType ft = schemaField.getType(); final NumericType numericType = ft.getNumericType(); if (numericType == null) { throw new IllegalStateException(); @@ -162,24 +154,6 @@ public static NamedList getCounts( case LONG: longs = FieldCache.DEFAULT.getLongs(ctx.reader(), fieldName, true); break; - case INT: - final FieldCache.Ints ints = FieldCache.DEFAULT.getInts(ctx.reader(), fieldName, true); - longs = new FieldCache.Longs() { - @Override - public long get(int docID) { - return ints.get(docID); - } - }; - break; - case FLOAT: - final FieldCache.Floats floats = FieldCache.DEFAULT.getFloats(ctx.reader(), fieldName, true); - longs = new FieldCache.Longs() { - @Override - public long get(int docID) { - return NumericUtils.floatToSortableInt(floats.get(docID)); - } - }; - break; case DOUBLE: final FieldCache.Doubles doubles = FieldCache.DEFAULT.getDoubles(ctx.reader(), fieldName, true); longs = new FieldCache.Longs() { @@ -238,136 +212,48 @@ protected boolean lessThan(Entry a, Entry b) { } // 4. build the NamedList - final ValueSource vs = ft.getValueSource(sf, null); + final ValueSource vs = ft.getValueSource(schemaField, null); final NamedList result = new NamedList<>(); - - // This stuff is complicated because if facet.mincount=0, the counts - // needs - // to be merged with terms from the terms dict - if (!zeros || FacetParams.FACET_SORT_COUNT.equals(sort) || FacetParams.FACET_SORT_COUNT_LEGACY.equals(sort)) { - // Only keep items we're interested in - final Deque counts = new ArrayDeque<>(); - while (pq.size() > offset) { - counts.addFirst(pq.pop()); - } - - // Entries from the PQ first, then using the terms dictionary - for (Entry entry : counts) { - final int readerIdx = ReaderUtil.subIndex(entry.docID, leaves); - final FunctionValues values = vs.getValues(Collections.emptyMap(), leaves.get(readerIdx)); - result.add(round(values.strVal(entry.docID - leaves.get(readerIdx).docBase)), entry.count); - } - - if (zeros && (limit < 0 || result.size() < limit)) { // need to merge with the term dict - if (!sf.indexed()) { - throw new IllegalStateException("Cannot use " + FacetParams.FACET_MINCOUNT + "=0 on field " - + sf.getName() + " which is not indexed"); - } - // Add zeros until there are limit results - final Set alreadySeen = new HashSet<>(); - while (pq.size() > 0) { - Entry entry = pq.pop(); - final int readerIdx = ReaderUtil.subIndex(entry.docID, leaves); - final FunctionValues values = vs.getValues(Collections.emptyMap(), leaves.get(readerIdx)); - alreadySeen.add(values.strVal(entry.docID - leaves.get(readerIdx).docBase)); - } - - for (int i = 0; i < result.size(); ++i) { - alreadySeen.add(result.getName(i)); - } - - final Terms terms = searcher.getAtomicReader().terms(fieldName); - if (terms != null) { - final String prefixStr = TrieField.getMainValuePrefix(ft); - final BytesRef prefix; - if (prefixStr != null) { - prefix = new BytesRef(prefixStr); - } else { - prefix = new BytesRef(); - } - final TermsEnum termsEnum = terms.iterator(null); - BytesRef term; - switch (termsEnum.seekCeil(prefix)) { - case FOUND: - case NOT_FOUND: - term = termsEnum.term(); - break; - case END: - term = null; - break; - default: - throw new AssertionError(); - } - - final CharsRef spare = new CharsRef(); - for (int skipped = hashTable.size; skipped < offset && term != null - && StringHelper.startsWith(term, prefix);) { - ft.indexedToReadable(term, spare); - final String termStr = spare.toString(); - if (!alreadySeen.contains(termStr)) { - ++skipped; - } - term = termsEnum.next(); - } - for (; term != null && StringHelper.startsWith(term, prefix) - && (limit < 0 || result.size() < limit); term = termsEnum.next()) { - ft.indexedToReadable(term, spare); - final String termStr = round(spare.toString()); - if (!alreadySeen.contains(termStr)) { - - result.add(termStr, 0); - } - } - } + final Map counts = new HashMap<>(); + + while (pq.size() > 0) { + final Entry entry = pq.pop(); + final int readerIdx = ReaderUtil.subIndex(entry.docID, leaves); + final FunctionValues values = vs.getValues(Collections.emptyMap(), leaves.get(readerIdx)); + counts.put(values.strVal(entry.docID - leaves.get(readerIdx).docBase), entry.count); + } + + final Terms terms = searcher.getAtomicReader().terms(fieldName); + if (terms != null) { + final String prefixStr = TrieField.getMainValuePrefix(ft); + final BytesRef prefix; + if (prefixStr != null) { + prefix = new BytesRef(prefixStr); + } else { + prefix = new BytesRef(); } - } else { - // sort=index, mincount=0 and we have less than limit items - // => Merge the PQ and the terms dictionary on the fly - if (!sf.indexed()) { - throw new IllegalStateException("Cannot use " + FacetParams.FACET_SORT + "=" - + FacetParams.FACET_SORT_INDEX + " on a field which is not indexed"); + final TermsEnum termsEnum = terms.iterator(null); + BytesRef term; + switch (termsEnum.seekCeil(prefix)) { + case FOUND: + case NOT_FOUND: + term = termsEnum.term(); + break; + case END: + term = null; + break; + default: + throw new AssertionError(); } - final Map counts = new HashMap<>(); - while (pq.size() > 0) { - final Entry entry = pq.pop(); - final int readerIdx = ReaderUtil.subIndex(entry.docID, leaves); - final FunctionValues values = vs.getValues(Collections.emptyMap(), leaves.get(readerIdx)); - counts.put(values.strVal(entry.docID - leaves.get(readerIdx).docBase), entry.count); + final CharsRef spare = new CharsRef(); + for (int i = 0; i < offset && term != null && StringHelper.startsWith(term, prefix); ++i) { + term = termsEnum.next(); } - final Terms terms = searcher.getAtomicReader().terms(fieldName); - if (terms != null) { - final String prefixStr = TrieField.getMainValuePrefix(ft); - final BytesRef prefix; - if (prefixStr != null) { - prefix = new BytesRef(prefixStr); - } else { - prefix = new BytesRef(); - } - final TermsEnum termsEnum = terms.iterator(null); - BytesRef term; - switch (termsEnum.seekCeil(prefix)) { - case FOUND: - case NOT_FOUND: - term = termsEnum.term(); - break; - case END: - term = null; - break; - default: - throw new AssertionError(); - } - final CharsRef spare = new CharsRef(); - for (int i = 0; i < offset && term != null && StringHelper.startsWith(term, prefix); ++i) { - term = termsEnum.next(); - } - for (; term != null && StringHelper.startsWith(term, prefix) && (limit < 0 || result.size() < limit); term = termsEnum - .next()) { - ft.indexedToReadable(term, spare); - final String termStr = spare.toString(); - Integer count = counts.get(termStr); - if (count == null) { - count = 0; - } + for (; term != null && StringHelper.startsWith(term, prefix) && (limit < 0 || result.size() < limit); term = termsEnum.next()) { + ft.indexedToReadable(term, spare); + final String termStr = spare.toString(); + final Integer count = counts.get(termStr); + if (count != null && count > 0) { result.add(round(termStr), count); } } diff --git a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/PerSegmentSingleValuedFaceting.java b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/PerSegmentSingleValuedFaceting.java index 87b3f12..52d48d0 100644 --- a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/PerSegmentSingleValuedFaceting.java +++ b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/PerSegmentSingleValuedFaceting.java @@ -1,7 +1,13 @@ package org.gazzax.labs.solrdf.handler.search.faceting; import java.io.IOException; -import java.util.*; -import java.util.concurrent.*; +import java.util.LinkedList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.CompletionService; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Executor; +import java.util.concurrent.ExecutorCompletionService; +import java.util.concurrent.Future; import org.apache.lucene.index.AtomicReaderContext; import org.apache.lucene.index.SortedDocValues; @@ -12,11 +18,9 @@ import org.apache.lucene.search.Filter; import org.apache.lucene.util.BytesRef; import org.apache.lucene.util.BytesRefBuilder; -import org.apache.lucene.util.CharsRef; import org.apache.lucene.util.CharsRefBuilder; import org.apache.lucene.util.PriorityQueue; import org.apache.lucene.util.UnicodeUtil; -import org.apache.lucene.util.packed.PackedInts; import org.apache.solr.common.SolrException; import org.apache.solr.common.params.FacetParams; import org.apache.solr.common.util.NamedList; @@ -26,6 +30,14 @@ import org.apache.solr.search.SolrIndexSearcher; import org.apache.solr.util.BoundedTreeSet; +/** + * A class that generates facet information for a given request. Note that it + * extends the already existing {@link SimpleFacets} in order to reuse that + * logic as much as possible. + * + * @author Andrea Gazzarini + * @since 1.0 + */ // FIXME this depends on SimpleFacets (several static method calls) public class PerSegmentSingleValuedFaceting { diff --git a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/RDFacets.java b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/RDFacets.java index 724d8ca..4e72ef0 100644 --- a/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/RDFacets.java +++ b/solrdf/src/main/java/org/gazzax/labs/solrdf/handler/search/faceting/RDFacets.java @@ -175,14 +175,14 @@ public NamedList call() throws Exception { final NamedList result = new SimpleOrderedMap<>(); if (termList != null) { result.add( - foq.alias(), + foq.key(), getListedTermCounts( workerFacetValue, collector.getDocSet(), StrUtils.splitSmart(termList, ",", true))); } else { result.add( - foq.alias(), + foq.key(), getTermCounts(foq, collector.getDocSet())); } return result; @@ -446,7 +446,7 @@ public NamedList getFacetDateCounts() { * @param base the values constraint for this specific count computation. */ public NamedList getTermCounts(final FacetObjectQuery query, final DocSet base) throws IOException { - final int mincount = query.optionalInt(FacetParams.FACET_MINCOUNT, 0); + final int mincount = Math.max(query.optionalInt(FacetParams.FACET_MINCOUNT, 1), 1); return getTermCounts(query, mincount, base); } diff --git a/solrdf/src/test/java/org/gazzax/labs/solrdf/integration/IntegrationTestSupertypeLayer.java b/solrdf/src/test/java/org/gazzax/labs/solrdf/integration/IntegrationTestSupertypeLayer.java new file mode 100644 index 0000000..271ab95 --- /dev/null +++ b/solrdf/src/test/java/org/gazzax/labs/solrdf/integration/IntegrationTestSupertypeLayer.java @@ -0,0 +1,146 @@ +package org.gazzax.labs.solrdf.integration; + +import java.io.IOException; + +import javax.xml.stream.XMLStreamConstants; +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamReader; + +import org.apache.solr.client.solrj.SolrServerException; +import org.apache.solr.client.solrj.impl.HttpSolrServer; +import org.apache.solr.client.solrj.impl.XMLResponseParser; +import org.apache.solr.common.SolrDocumentList; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.SimpleOrderedMap; +import org.gazzax.labs.solrdf.log.Log; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.slf4j.LoggerFactory; + +/** + * Supertype layer for all integration tests. + * + * @author Andrea Gazzarini + * @since 1.0 + */ +public class IntegrationTestSupertypeLayer { + protected static final String SOLR_URI = "http://127.0.0.1:8080/solr/store"; + protected static final String SPARQL_ENDPOINT_URI = SOLR_URI + "/sparql"; + protected static final String GRAPH_STORE_ENDPOINT_URI = SOLR_URI + "/rdf-graph-store"; + + protected final Log log = new Log(LoggerFactory.getLogger(getClass())); + + protected static HttpSolrServer solr; + + /** + * Initilisation procedure for this test case. + * + * @throws Exception hopefully never. + */ + @BeforeClass + public static void initClient() { + solr = new HttpSolrServer(SOLR_URI); + resetSolRDFXmlResponseParser(); + } + + /** + * Shutdown procedure for this test case. + * + * @throws Exception hopefully never. + */ + @AfterClass + public static void shutdownClient() throws Exception { + clearData(); + solr.shutdown(); + } + + /** + * Cleans all data previously indexed on SolRDF. + * + * @throws Exception hopefully never. + */ + protected static void clearData() throws Exception { + solr.setParser(new XMLResponseParser()); + solr.deleteByQuery("*:*"); + commitChanges(); + resetSolRDFXmlResponseParser(); } + + /** + * Commits changes on Solr. + * + * @throws SolrServerException in case of a Solr failure. + * @throws IOException in case of I/O failure. + */ + protected static void commitChanges() throws SolrServerException, IOException { + solr.setParser(new XMLResponseParser()); + solr.commit(); + resetSolRDFXmlResponseParser(); + } + + protected static void resetSolRDFXmlResponseParser() { + solr.setParser(new XMLResponseParser() { + @Override + public String getContentType() { + return "text/xml"; + } + + @Override + protected SolrDocumentList readDocuments(final XMLStreamReader parser) throws XMLStreamException { + return new SolrDocumentList(); + } + + protected NamedList readNamedList(final XMLStreamReader parser) throws XMLStreamException { + if( XMLStreamConstants.START_ELEMENT != parser.getEventType()) { + throw new RuntimeException("must be start element, not: " + parser.getEventType()); + } + + final StringBuilder builder = new StringBuilder(); + final NamedList nl = new SimpleOrderedMap<>(); + KnownType type = null; + String name = null; + + int depth = 0; + while( true ) { + switch (parser.next()) { + case XMLStreamConstants.START_ELEMENT: + depth++; + builder.setLength( 0 ); + type = KnownType.get( parser.getLocalName() ); + if( type == null ) { + continue; + } + + name = null; + int cnt = parser.getAttributeCount(); + for( int i=0; i expectedPublishersOccurrences = new HashMap(); + { + expectedPublishersOccurrences.put("ABCD Publishing", 2); + expectedPublishersOccurrences.put("Acme Publishing", 2); + expectedPublishersOccurrences.put("Packt Publishing", 1); + expectedPublishersOccurrences.put("CDEF Publishing", 1); + } + + private Map expectedPublishersWithAtLeastTwoOccurrences = new HashMap(); + { + expectedPublishersWithAtLeastTwoOccurrences.put("ABCD Publishing", 2); + expectedPublishersWithAtLeastTwoOccurrences.put("Acme Publishing", 2); + } + + private Map expectedReviewedOccurrences = new HashMap(); + { + expectedReviewedOccurrences.put("true", 3); + expectedReviewedOccurrences.put("false", 1); + } + + private Map expectedDownloadsOccurrences = new HashMap(); + { + expectedDownloadsOccurrences.put("192", 2); + expectedDownloadsOccurrences.put("442", 1); + expectedDownloadsOccurrences.put("99", 1); + expectedDownloadsOccurrences.put("199", 1); + } + + private Map expectedPricesOccurrences = new HashMap(); + { + expectedPricesOccurrences.put("22.1", 1); + expectedPricesOccurrences.put("23.95", 1); + expectedPricesOccurrences.put("22.4", 1); + expectedPricesOccurrences.put("20", 1); + expectedPricesOccurrences.put("11", 1); + expectedPricesOccurrences.put("22", 1); + } + + private Map expectedDatesOccurrences = new HashMap(); + { + expectedDatesOccurrences.put("2000-12-30T23:00:00Z", 1); + expectedDatesOccurrences.put("2010-06-22T22:00:00Z", 1); + expectedDatesOccurrences.put("2010-10-09T22:00:00Z", 1); + expectedDatesOccurrences.put("2010-12-31T23:00:00Z", 1); + expectedDatesOccurrences.put("2011-09-06T22:00:00Z", 1); + expectedDatesOccurrences.put("2015-08-22T22:00:00Z", 1); + } + + /** + * Loads all triples found in the datafile associated with the given name. + * @throws IOException + * @throws SolrServerException + * + * @throws Exception hopefully never, otherwise the test fails. + */ + @BeforeClass + public final static void loadSampleData() throws SolrServerException, IOException { + final Model memoryModel = ModelFactory.createDefaultModel(); + memoryModel.read(TEST_DATA_URI, DUMMY_BASE_URI, "N-TRIPLES"); + + DATASET = DatasetAccessorFactory.createHTTP(GRAPH_STORE_ENDPOINT_URI); + DATASET.add(memoryModel); + + commitChanges(); + + final Model model = DATASET.getModel(); + + assertFalse(model.isEmpty()); + assertTrue(model.isIsomorphicWith(memoryModel)); + } + + /** + * Setup fixture for this test. + * + * @throws Exception hopefully never, otherwise the test fails. + */ + @Before + public final void setUp() throws Exception { + query = new SolrQuery("SELECT * WHERE { ?s ?p ?o }"); + query.setRows(0); + query.setFacet(true); + query.setRequestHandler("/sparql"); + } + + /** + * In case the given hint is unknown, then "str" will be used. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void strHintAsDefault() throws Exception { + assertOneFacetQuery( + publisherQuery, + randomString(), + null, + expectedPublishersOccurrences); + + assertOneFacetQuery( + publisherQuery, + randomString(), + randomString(), + expectedPublishersOccurrences); + } + + /** + * Facet mincount must be greater than 0. If not so, then 1 will be used as default value. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void mincountMustBeAtLeastOne() throws Exception { + query.set("facet.mincount", -1); + oneStringFacetWithAlias(); + + query.set("facet.mincount", 0); + oneStringFacetWithAlias(); + } + + /** + * A single string facet object query without alias and a mincount equals to 2. + * The facet is keyed with its query. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void atLeastTwoOccurrences() throws Exception { + query.set("facet.mincount", 2); + assertOneFacetQuery( + publisherQuery, + FacetQuery.STRING_HINT, + null, + expectedPublishersWithAtLeastTwoOccurrences); + } + + /** + * A single string facet object query without alias. + * The facet is keyed with its query. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneStringFacetWithoutAlias() throws Exception { + assertOneFacetQuery( + publisherQuery, + FacetQuery.STRING_HINT, + null, + expectedPublishersOccurrences); + } + + /** + * A single string facet object query with alias. + * The facet is keyed with the provided alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneStringFacetWithAlias() throws Exception { + assertOneFacetQuery( + publisherQuery, + FacetQuery.STRING_HINT, + randomString(), + expectedPublishersOccurrences); + } + + /** + * A single boolean facet object query without alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneBooleanFacetWithoutAlias() throws Exception { + assertOneFacetQuery( + reviewedQuery, + FacetQuery.BOOLEAN_HINT, + null, + expectedReviewedOccurrences); + } + + /** + * A single boolean facet object query with alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneBooleanFacetWithAlias() throws Exception { + assertOneFacetQuery( + reviewedQuery, + FacetQuery.BOOLEAN_HINT, + randomString(), + expectedReviewedOccurrences); + } + + /** + * A single numeric (integer) facet object query without alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneIntegerFacetWithoutAlias() throws Exception { + assertOneFacetQuery( + downloadsQuery, + FacetQuery.NUMERIC_HINT, + null, + expectedDownloadsOccurrences); + } + + /** + * A single numeric (integer) facet object query with alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneIntegerFacetWithAlias() throws Exception { + assertOneFacetQuery( + downloadsQuery, + FacetQuery.NUMERIC_HINT, + randomString(), + expectedDownloadsOccurrences); + } + + /** + * A single numeric (decimal) facet object query without alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneDecimalFacetWithoutAlias() throws Exception { + assertOneFacetQuery( + priceQuery, + FacetQuery.NUMERIC_HINT, + null, + expectedPricesOccurrences); + } + + /** + * A single numeric (integer) facet object query with alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneDecimalFacetWithAlias() throws Exception { + assertOneFacetQuery( + priceQuery, + FacetQuery.NUMERIC_HINT, + randomString(), + expectedPricesOccurrences); + } + + /** + * A single date facet object query without alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneDateFacetWithoutAlias() throws Exception { + assertOneFacetQuery( + dateQuery, + FacetQuery.DATE_HINT, + null, + expectedDatesOccurrences); + } + + /** + * A single date facet object query with alias. + * + * @throws Exception hopefully never otherwise the test fails. + */ + @Test + public void oneDateFacetWithAlias() throws Exception { + assertOneFacetQuery( + dateQuery, + FacetQuery.DATE_HINT, + randomString(), + expectedDatesOccurrences); + } + + /** + * Executes the current {@link SolrQuery} and returns back the {@link NamedList} containing the collected facet object queries. + * + * @return the collected facet object queries. + * @throws SolrServerException in case of Solr request processing failure. + */ + private NamedList executeQueryAndGetFacetObjectQueries() throws SolrServerException { + final QueryResponse queryResponse = solr.query(query); + final NamedList response = queryResponse.getResponse(); + assertNotNull(response); + + final NamedList facetCounts = (NamedList) response.get("facet_counts"); + assertNotNull(facetCounts); + final NamedList facetObjectQueries = (NamedList) facetCounts.get("facet_object_queries"); + assertNotNull(facetObjectQueries); + return facetObjectQueries; + } + + /** + * Asserts a given NamedList against an expected map of results. + * + * @param expected the map containing expected (facet) results. + * @param actual the actual {@link NamedList} returned from Solr. + */ + private void assertFacetResults(final Map expected, final NamedList actual) { + assertNotNull(actual); + assertEquals(expected.size(), actual.size()); + + for (final Entry expectedCount : expected.entrySet()) { + assertEquals(expectedCount.getValue(), actual.remove(expectedCount.getKey())); + } + assertEquals(0, actual.size()); + } + + /** + * A compose method for avoid duplication in "oneFacet" test methods. + * + * @param facetQuery the facet query. + * @param hint the facet query hint. + * @param alias the facet query alias. + * @param expectedResults the expected results. + * + * @throws Exception hopefully never otherwise the corresponding test fails. + */ + private void assertOneFacetQuery( + final String facetQuery, + final String hint, + final String alias, + final Map expectedResults) throws Exception { + + if (alias != null) { + query.set("facet.obj.q.alias", alias); + } + + query.set("facet.obj.q.hint", hint); + query.set("facet.obj.q", facetQuery); + + final NamedList facetObjectQueries = executeQueryAndGetFacetObjectQueries(); + assertEquals(1, facetObjectQueries.size()); + + if (alias != null) { + assertNull(facetObjectQueries.get(facetQuery)); + assertFacetResults(expectedResults, (NamedList) facetObjectQueries.get(alias)); + } else { + assertNull(facetObjectQueries.get(alias)); + assertFacetResults(expectedResults, (NamedList) facetObjectQueries.get(facetQuery)); + } + } +} \ No newline at end of file diff --git a/solrdf/src/test/java/org/gazzax/labs/solrdf/integration/LearningSparql_ITCase.java b/solrdf/src/test/java/org/gazzax/labs/solrdf/integration/sparql/LearningSparql_ITCase.java similarity index 82% rename from solrdf/src/test/java/org/gazzax/labs/solrdf/integration/LearningSparql_ITCase.java rename to solrdf/src/test/java/org/gazzax/labs/solrdf/integration/sparql/LearningSparql_ITCase.java index 5ee7890..fdac98f 100644 --- a/solrdf/src/test/java/org/gazzax/labs/solrdf/integration/LearningSparql_ITCase.java +++ b/solrdf/src/test/java/org/gazzax/labs/solrdf/integration/sparql/LearningSparql_ITCase.java @@ -12,7 +12,7 @@ * We warmly appreciate and thank the author and O'Reilly for such permission. * */ -package org.gazzax.labs.solrdf.integration; +package org.gazzax.labs.solrdf.integration.sparql; import static org.gazzax.labs.solrdf.MisteryGuest.misteryGuest; import static org.gazzax.labs.solrdf.TestUtility.DUMMY_BASE_URI; @@ -28,16 +28,12 @@ import java.util.Arrays; import java.util.List; -import org.apache.solr.client.solrj.SolrServer; -import org.apache.solr.client.solrj.SolrServerException; -import org.apache.solr.client.solrj.impl.HttpSolrServer; import org.gazzax.labs.solrdf.MisteryGuest; -import org.gazzax.labs.solrdf.log.Log; +import org.gazzax.labs.solrdf.integration.IntegrationTestSupertypeLayer; import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Test; -import org.slf4j.LoggerFactory; import com.hp.hpl.jena.query.Dataset; import com.hp.hpl.jena.query.DatasetAccessor; @@ -52,25 +48,17 @@ import com.hp.hpl.jena.sparql.resultset.ResultSetCompare; /** - * SPARQL integration test. + * Facet Object Queries integration test. * * @author Andrea Gazzarini * @since 1.0 */ -public class LearningSparql_ITCase { +public class LearningSparql_ITCase extends IntegrationTestSupertypeLayer { + protected final static String LEARNING_SPARQL_EXAMPLES_DIR = "src/test/resources/LearningSPARQLExamples"; - protected static final String SOLR_URI = "http://127.0.0.1:8080/solr/store"; - protected static final String SPARQL_ENDPOINT = SOLR_URI + "/sparql"; - protected static final String GRAPH_STORE_ENDPOINT = SOLR_URI + "/rdf-graph-store"; - - protected final static String EXAMPLES_DIR = "src/test/resources/LearningSPARQLExamples"; - - protected final Log log = new Log(LoggerFactory.getLogger(LearningSparql_ITCase.class)); - protected Dataset memoryDataset; protected DatasetAccessor dataset; - - static SolrServer solr; + static final List DATA = new ArrayList(); /** @@ -78,8 +66,6 @@ public class LearningSparql_ITCase { */ @BeforeClass public static void init() { - solr = new HttpSolrServer(SOLR_URI); - DATA.add(misteryGuest("ex003.rq", "Query with prefixes", "ex002.ttl")); DATA.add(misteryGuest("ex006.rq", "Query without prefixes", "ex002.ttl")); DATA.add(misteryGuest("ex007.rq", "FROM keyword", "ex002.ttl")); @@ -120,17 +106,16 @@ public static void init() { @Before public final void setUp() { memoryDataset = DatasetFactory.createMem(); - dataset = DatasetAccessorFactory.createHTTP(GRAPH_STORE_ENDPOINT); + dataset = DatasetAccessorFactory.createHTTP(GRAPH_STORE_ENDPOINT_URI); } - /** + /** * Shutdown procedure for this test. * - * @throws SolrServerException in case of a Solr failure. - * @throws IOException in case of I/O failure. + * @throws Exception hopefully never. */ @After - public void tearDown() throws SolrServerException, IOException { + public void tearDown() throws Exception { clearDatasets(); } @@ -149,13 +134,13 @@ public void select() throws Exception { assertTrue( Arrays.toString(data.datasets) + ", " + data.query, ResultSetCompare.isomorphic( - (execution = QueryExecutionFactory.sparqlService(SPARQL_ENDPOINT, query)).execSelect(), + (execution = QueryExecutionFactory.sparqlService(SPARQL_ENDPOINT_URI, query)).execSelect(), (inMemoryExecution = QueryExecutionFactory.create(query, memoryDataset)).execSelect())); } catch (final Exception error) { error.printStackTrace(); QueryExecution debugExecution = null; log.debug("JNS\n" + ResultSetFormatter.asText( - (debugExecution = QueryExecutionFactory.sparqlService(SPARQL_ENDPOINT, query)).execSelect())); + (debugExecution = QueryExecutionFactory.sparqlService(SPARQL_ENDPOINT_URI, query)).execSelect())); debugExecution.close(); log.debug("MEM\n" + ResultSetFormatter.asText( @@ -224,28 +209,16 @@ protected void load(final MisteryGuest data) throws Exception { * @return the URI (as string) of a given filename. */ URI source(final String filename) { - return new File(EXAMPLES_DIR, filename).toURI(); + return new File(LEARNING_SPARQL_EXAMPLES_DIR, filename).toURI(); } /** * Removes all data created by this test. * - * @throws SolrServerException in case of a Solr failure. - * @throws IOException in case of I/O failure. + * @throws Exception hopefully never. */ - private void clearDatasets() throws SolrServerException, IOException { - solr.deleteByQuery("*:*"); - commitChanges(); + private void clearDatasets() throws Exception { + clearData(); memoryDataset.getDefaultModel().removeAll(); } - - /** - * Commits changes on Solr. - * - * @throws SolrServerException in case of a Solr failure. - * @throws IOException in case of I/O failure. - */ - private void commitChanges() throws SolrServerException, IOException { - solr.commit(); - } } \ No newline at end of file diff --git a/solrdf/src/test/resources/sample-data.nt b/solrdf/src/test/resources/sample-data.nt deleted file mode 100644 index 04ecd71..0000000 --- a/solrdf/src/test/resources/sample-data.nt +++ /dev/null @@ -1,6 +0,0 @@ - "42" . - "David Copperfield" . - "Edmund Wells" . - "14.3" . - "Andrea Gazzarini" . - "Apache Solr Essentials" . \ No newline at end of file diff --git a/solrdf/src/test/resources/sample-data/bsbm-generated-dataset.nt b/solrdf/src/test/resources/sample_data/bsbm-generated-dataset.nt similarity index 100% rename from solrdf/src/test/resources/sample-data/bsbm-generated-dataset.nt rename to solrdf/src/test/resources/sample_data/bsbm-generated-dataset.nt diff --git a/solrdf/src/test/resources/sample_data/faceting_test_dataset.nt b/solrdf/src/test/resources/sample_data/faceting_test_dataset.nt new file mode 100644 index 0000000..46ae8dc --- /dev/null +++ b/solrdf/src/test/resources/sample_data/faceting_test_dataset.nt @@ -0,0 +1,45 @@ + "Bla bla bla" . + "Edmund Wells" . + "2000-12-31"^^ . + "ABCD Publishing" . + "22.10"^^ . + "192"^^ . + "true"^^ . + + "Andrea Gazzarini" . + "Apache Solr Essentials" . + "2015-08-23"^^ . + "Packt Publishing" . + "23.95"^^ . + "442"^^ . + "true"^^ . + + "John Aber" . + "This is a title" . + "2010-06-23"^^ . + "CDEF Publishing" . + "22.40"^^ . + "192"^^ . + "true"^^ . + + "Malcolm Louise" . + "J.F. Loiny" . + "This is a title" . + "2010-10-10"^^ . + "ABCD Publishing" . + "20.00"^^ . + "99"^^ . + "false"^^ . + + "Marie Yumm" . + "Yet another title" . + "2011-01-01"^^ . + "Acme Publishing" . + "11.00"^^ . + "199"^^ . + + "Yphos Cathe" . + "Lady Kalea" . + "2011-09-07"^^ . + "Acme Publishing" . + "22.00"^^ . \ No newline at end of file