| /* |
| * 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.solr.search.join; |
| |
| import java.util.ArrayList; |
| import java.util.Arrays; |
| import java.util.Collections; |
| import java.util.HashSet; |
| import java.util.LinkedList; |
| import java.util.List; |
| import java.util.Set; |
| |
| import org.apache.solr.SolrTestCaseJ4; |
| import org.apache.solr.common.SolrException; |
| import org.junit.AfterClass; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| |
| public class BlockJoinFacetRandomTest extends SolrTestCaseJ4 { |
| private static String handler; |
| private static final int NUMBER_OF_PARENTS = 10; |
| private static final int NUMBER_OF_VALUES = 5; |
| private static final int NUMBER_OF_CHILDREN = 5; |
| private static final String[] facetFields = {"brand", "category", "color", "size", "type"}; |
| private static final String[] otherValues = {"x_", "y_", "z_"}; |
| public static final String PARENT_VALUE_PREFIX = "prn_"; |
| public static final String CHILD_VALUE_PREFIX = "chd_"; |
| |
| |
| private static Facet[] facets; |
| |
| @BeforeClass |
| public static void beforeClass() throws Exception { |
| initCore("solrconfig-blockjoinfacetcomponent.xml", "schema-blockjoinfacetcomponent.xml"); |
| handler = random().nextBoolean() ? "/blockJoinDocSetFacetRH":"/blockJoinFacetRH"; |
| facets = createFacets(); |
| createIndex(); |
| } |
| |
| public static void createIndex() throws Exception { |
| int i = 0; |
| List<List<List<String>>> blocks = createBlocks(); |
| for (List<List<String>> block : blocks) { |
| List<XmlDoc> updBlock = new ArrayList<>(); |
| for (List<String> blockFields : block) { |
| blockFields.add("id"); |
| blockFields.add(Integer.toString(i)); |
| updBlock.add(doc(blockFields.toArray(new String[blockFields.size()]))); |
| i++; |
| } |
| //got xmls for every doc. now nest all into the last one |
| XmlDoc parentDoc = updBlock.get(updBlock.size() - 1); |
| parentDoc.xml = parentDoc.xml.replace("</doc>", |
| updBlock.subList(0, updBlock.size() - 1).toString().replaceAll("[\\[\\]]", "") + "</doc>"); |
| assertU(add(parentDoc)); |
| |
| if (random().nextBoolean()) { |
| assertU(commit()); |
| // force empty segment (actually, this will no longer create an empty segment, only a new segments_n) |
| if (random().nextBoolean()) { |
| assertU(commit()); |
| } |
| } |
| } |
| assertU(commit()); |
| assertQ(req("q", "*:*"), "//*[@numFound='" + i + "']"); |
| } |
| |
| private static List<List<List<String>>> createBlocks() { |
| List<List<List<String>>> blocks = new ArrayList<>(); |
| for (int i = 0; i < NUMBER_OF_PARENTS; i++) { |
| List<List<String>> block = createChildrenBlock(i, facets); |
| List<String> fieldsList = new LinkedList<>(); |
| fieldsList.add("parent_s"); |
| fieldsList.add(parent(i)); |
| for (Facet facet : facets) { |
| for (RandomFacetValue facetValue : facet.facetValues) { |
| RandomParentPosting posting = facetValue.postings[i]; |
| if (posting.parentHasOwnValue) { |
| fieldsList.add(facet.getFieldNameForIndex()); |
| fieldsList.add(facetValue.facetValue); |
| } else if (facet.multiValued && random().nextBoolean()) { |
| fieldsList.add(facet.getFieldNameForIndex()); |
| fieldsList.add(someOtherValue(facet.fieldType)); |
| } |
| } |
| if (facet.additionalValueIsAllowedForParent(i)&&random().nextBoolean()) { |
| fieldsList.add(facet.getFieldNameForIndex()); |
| fieldsList.add(someOtherValue(facet.fieldType)); |
| } |
| } |
| block.add(fieldsList); |
| blocks.add(block); |
| } |
| Collections.shuffle(blocks, random()); |
| return blocks; |
| } |
| |
| private static List<List<String>> createChildrenBlock(int parentIndex, Facet[] facets) { |
| List<List<String>> block = new ArrayList<>(); |
| for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { |
| List<String> fieldsList = new LinkedList<>(); |
| |
| fieldsList.add("child_s"); |
| fieldsList.add(child(i)); |
| fieldsList.add("parentchild_s"); |
| fieldsList.add(parentChild(parentIndex, i)); |
| for (Facet facet : facets) { |
| for (RandomFacetValue facetValue : facet.facetValues) { |
| RandomParentPosting posting = facetValue.postings[parentIndex]; |
| if (posting.childrenHaveValue[i]) { |
| fieldsList.add(facet.getFieldNameForIndex()); |
| fieldsList.add(facetValue.facetValue); |
| } else if (facet.multiValued && random().nextBoolean()) { |
| fieldsList.add(facet.getFieldNameForIndex()); |
| fieldsList.add(someOtherValue(facet.fieldType)); |
| } |
| } |
| if (facet.additionalValueIsAllowedForChild(parentIndex,i)&&random().nextBoolean()) { |
| fieldsList.add(facet.getFieldNameForIndex()); |
| fieldsList.add(someOtherValue(facet.fieldType)); |
| } |
| } |
| block.add(fieldsList); |
| } |
| Collections.shuffle(block, random()); |
| return block; |
| } |
| |
| private static String parent(int docNumber) { |
| return fieldValue(PARENT_VALUE_PREFIX, docNumber); |
| } |
| |
| private static String child(int docNumber) { |
| return fieldValue(CHILD_VALUE_PREFIX, docNumber); |
| } |
| |
| private static String someOtherValue(FieldType fieldType) { |
| int randomValue = random().nextInt(NUMBER_OF_VALUES) + NUMBER_OF_VALUES; |
| switch (fieldType) { |
| case String : |
| int index = random().nextInt(otherValues.length); |
| return otherValues[index]+randomValue; |
| case Float: |
| return createFloatValue(randomValue); |
| default: |
| return String.valueOf(randomValue); |
| |
| } |
| |
| } |
| |
| private static String createFloatValue(int intValue) { |
| return intValue + ".01"; |
| } |
| |
| private static String fieldValue(String valuePrefix, int docNumber) { |
| return valuePrefix + docNumber; |
| } |
| |
| private static String parentChild(int parentIndex, int childIndex) { |
| return parent(parentIndex) + "_" + child(childIndex); |
| } |
| |
| @AfterClass |
| public static void cleanUp() throws Exception { |
| if (null != h) { |
| assertU(delQ("*:*")); |
| optimize(); |
| assertU((commit())); |
| } |
| } |
| |
| @Test |
| public void testValidation() throws Exception { |
| assertQ("Component is ignored", |
| req("q", "+parent_s:(prn_1 prn_2)", "qt", handler) |
| , "//*[@numFound='2']" |
| , "//doc/str[@name=\"parent_s\"]='prn_1'" |
| , "//doc/str[@name=\"parent_s\"]='prn_2'" |
| ); |
| |
| assertQEx("Validation exception is expected because query is not ToParentBlockJoinQuery", |
| BlockJoinFacetComponent.NO_TO_PARENT_BJQ_MESSAGE, |
| req( |
| "q", "t", |
| "df", "name", |
| "qt", handler, |
| BlockJoinFacetComponent.CHILD_FACET_FIELD_PARAMETER, facetFields[0] |
| ), |
| SolrException.ErrorCode.BAD_REQUEST |
| ); |
| |
| assertQEx("Validation exception is expected because facet field is not defined in schema", |
| req( |
| "q", "{!parent which=\"parent_s:[* TO *]\"}child_s:chd_1", |
| "qt", handler, |
| BlockJoinFacetComponent.CHILD_FACET_FIELD_PARAMETER, "undefinedField" |
| ), |
| SolrException.ErrorCode.BAD_REQUEST |
| ); |
| } |
| |
| @Test |
| public void testAllDocs() throws Exception { |
| int[] randomFacets = getRandomArray(facets.length); |
| assertQ("Random facets for all docs should be calculated", |
| req(randomFacetsRequest(null, null, null, null, null, randomFacets)), |
| expectedResponse(null, null, randomFacets)); |
| } |
| |
| @Test |
| public void testRandomParentsAllChildren() throws Exception { |
| int[] randomParents = getRandomArray(NUMBER_OF_PARENTS); |
| int[] randomFacets = getRandomArray(facets.length); |
| assertQ("Random facets for random parents should be calculated", |
| req(randomFacetsRequest(randomParents, null, null, null, null, randomFacets)), |
| expectedResponse(randomParents, null, randomFacets)); |
| } |
| |
| @Test |
| public void testRandomChildrenAllParents() throws Exception { |
| int[] randomChildren = getRandomArray(NUMBER_OF_CHILDREN); |
| int[] randomFacets = getRandomArray(facets.length); |
| assertQ("Random facets for all parent docs should be calculated", |
| req(randomFacetsRequest(null, randomChildren, null, null, null, randomFacets)), |
| expectedResponse(null, randomChildren, randomFacets)); |
| } |
| |
| @Test |
| public void testRandomChildrenRandomParents() throws Exception { |
| int[] randomParents = getRandomArray(NUMBER_OF_PARENTS); |
| int[] randomChildren = getRandomArray(NUMBER_OF_CHILDREN); |
| int[] randomFacets = getRandomArray(facets.length); |
| assertQ("Random facets for all parent docs should be calculated", |
| req(randomFacetsRequest(randomParents, randomChildren, null, null, null, randomFacets)), |
| expectedResponse(randomParents, randomChildren, randomFacets)); |
| } |
| |
| @Test |
| public void testRandomChildrenRandomParentsRandomRelations() throws Exception { |
| int[] randomParents = getRandomArray(NUMBER_OF_PARENTS); |
| int[] randomChildren = getRandomArray(NUMBER_OF_CHILDREN); |
| int[] parentRelations = getRandomArray(NUMBER_OF_PARENTS); |
| int[] childRelations = getRandomArray(NUMBER_OF_CHILDREN); |
| int[] randomFacets = getRandomArray(facets.length); |
| assertQ("Random facets for all parent docs should be calculated", |
| req(randomFacetsRequest(randomParents, randomChildren, parentRelations, childRelations, null, randomFacets)), |
| expectedResponse(intersection(randomParents, parentRelations), |
| intersection(randomChildren, childRelations), randomFacets)); |
| } |
| |
| @Test |
| public void testRandomFilters() throws Exception { |
| int[] randomParents = getRandomArray(NUMBER_OF_PARENTS); |
| int[] randomChildren = getRandomArray(NUMBER_OF_CHILDREN); |
| int[] parentRelations = getRandomArray(NUMBER_OF_PARENTS); |
| int[] childRelations = getRandomArray(NUMBER_OF_CHILDREN); |
| int[] randomParentFilters = getRandomArray(NUMBER_OF_PARENTS); |
| int[] randomFacets = getRandomArray(facets.length); |
| assertQ("Random facets for all parent docs should be calculated", |
| req(randomFacetsRequest(randomParents, randomChildren, parentRelations, childRelations, randomParentFilters, randomFacets)), |
| expectedResponse(intersection(intersection(randomParents, parentRelations), randomParentFilters), |
| intersection(randomChildren, childRelations), randomFacets)); |
| } |
| |
| private int[] intersection(int[] firstArray, int[] secondArray) { |
| Set<Integer> firstSet = new HashSet<>(); |
| for (int i : firstArray) { |
| firstSet.add(i); |
| } |
| Set<Integer> secondSet = new HashSet<>(); |
| for (int i : secondArray) { |
| secondSet.add(i); |
| } |
| firstSet.retainAll(secondSet); |
| int[] result = new int[firstSet.size()]; |
| int i = 0; |
| for (Integer integer : firstSet) { |
| result[i++] = integer; |
| } |
| return result; |
| } |
| |
| private String[] randomFacetsRequest(int[] parents, int[] children, |
| int[] parentRelations, int[] childRelations, |
| int[] parentFilters, int[] facetNumbers) { |
| List<String> params = new ArrayList<>(Arrays.asList( |
| "q", parentsQuery(parents), |
| "qt",handler, |
| "pq","parent_s:[* TO *]", |
| "chq", childrenQuery(children, parentRelations, childRelations), |
| "fq", flatQuery(parentFilters, "parent_s", PARENT_VALUE_PREFIX) |
| )); |
| for (int facetNumber : facetNumbers) { |
| params .add(BlockJoinFacetComponent.CHILD_FACET_FIELD_PARAMETER); |
| params .add(facets[facetNumber].getFieldNameForIndex()); |
| } |
| return params.toArray(new String[params.size()]); |
| } |
| |
| private String parentsQuery(int[] parents) { |
| String result; |
| if (parents == null) { |
| result = "{!parent which=$pq v=$chq}"; |
| } else { |
| result = flatQuery(parents, "parent_s", PARENT_VALUE_PREFIX) + " +_query_:\"{!parent which=$pq v=$chq}\""; |
| } |
| return result; |
| } |
| |
| private String flatQuery(int[] docNumbers, final String fieldName, String fieldValuePrefix) { |
| String result; |
| if (docNumbers == null) { |
| result = "+" + fieldName + ":[* TO *]"; |
| } else { |
| StringBuilder builder = new StringBuilder("+" + fieldName +":("); |
| if (docNumbers.length == 0) { |
| builder.append("match_nothing_value"); |
| } else { |
| for (int docNumber : docNumbers) { |
| builder.append(fieldValue(fieldValuePrefix, docNumber)); |
| builder.append(" "); |
| } |
| builder.deleteCharAt(builder.length() - 1); |
| } |
| builder.append(")"); |
| result = builder.toString(); |
| } |
| return result; |
| } |
| |
| private String childrenQuery(int[] children, int[] parentRelations, int[] childRelations) { |
| StringBuilder builder = new StringBuilder(); |
| builder.append(flatQuery(children, "child_s", CHILD_VALUE_PREFIX)); |
| if (parentRelations == null) { |
| if (childRelations == null) { |
| builder.append(" +parentchild_s:[* TO *]"); |
| } else { |
| builder.append(" +parentchild_s:("); |
| if (childRelations.length == 0) { |
| builder.append("match_nothing_value"); |
| } else { |
| for (int childRelation : childRelations) { |
| for (int i = 0; i < NUMBER_OF_PARENTS; i++) { |
| builder.append(parentChild(i, childRelation)); |
| builder.append(" "); |
| } |
| } |
| builder.deleteCharAt(builder.length() - 1); |
| } |
| builder.append(")"); |
| } |
| } else { |
| builder.append(" +parentchild_s:("); |
| if (parentRelations.length == 0) { |
| builder.append("match_nothing_value"); |
| } else { |
| if (childRelations == null) { |
| for (int parentRelation : parentRelations) { |
| for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { |
| builder.append(parentChild(parentRelation, i)); |
| builder.append(" "); |
| } |
| } |
| } else if (childRelations.length == 0) { |
| builder.append("match_nothing_value"); |
| } else { |
| for (int parentRelation : parentRelations) { |
| |
| for (int childRelation : childRelations) { |
| builder.append(parentChild(parentRelation, childRelation)); |
| builder.append(" "); |
| } |
| } |
| builder.deleteCharAt(builder.length() - 1); |
| } |
| } |
| builder.append(")"); |
| } |
| return builder.toString(); |
| } |
| |
| private String[] expectedResponse(int[] parents, int[] children, int[] facetNumbers) { |
| List<String> result = new LinkedList<>(); |
| if (children != null && children.length == 0) { |
| result.add("//*[@numFound='" + 0 + "']"); |
| } else { |
| if (parents == null) { |
| result.add("//*[@numFound='" + NUMBER_OF_PARENTS + "']"); |
| for (int i = 0; i < NUMBER_OF_PARENTS; i++) { |
| result.add("//doc/str[@name=\"parent_s\"]='" + parent(i) + "'"); |
| } |
| } else { |
| result.add("//*[@numFound='" + parents.length + "']"); |
| for (int parent : parents) { |
| result.add("//doc/str[@name=\"parent_s\"]='" + parent(parent) + "'"); |
| } |
| } |
| } |
| if (facetNumbers != null) { |
| for (int facetNumber : facetNumbers) { |
| result.add("//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='" + facets[facetNumber].getFieldNameForIndex() + "']"); |
| RandomFacetValue[] facetValues = facets[facetNumber].facetValues; |
| for (RandomFacetValue facetValue : facetValues) { |
| int expectedFacetCount = facetValue.getFacetCount(parents, children); |
| if (expectedFacetCount > 0) { |
| result.add("//lst[@name='facet_counts']/lst[@name='facet_fields']/lst[@name='" + |
| facets[facetNumber].getFieldNameForIndex() + "']/int[@name='" + |
| facetValue.facetValue + "' and text()='" + expectedFacetCount + "']"); |
| } |
| } |
| } |
| } |
| return result.toArray(new String[result.size()]); |
| } |
| |
| private static Facet[] createFacets() { |
| int[] facetsToCreate = getRandomArray(facetFields.length); |
| Facet[] facets = new Facet[facetsToCreate.length]; |
| int i = 0; |
| for (int facetNumber : facetsToCreate) { |
| facets[i++] = new Facet(facetFields[facetNumber]); |
| } |
| return facets; |
| } |
| |
| private static int[] getRandomArray(int maxNumber) { |
| int[] buffer = new int[maxNumber]; |
| int count = 0; |
| for (int i = 0; i < maxNumber; i++) { |
| if (random().nextBoolean()) { |
| buffer[count++] = i; |
| } |
| } |
| int[] result = new int[count]; |
| System.arraycopy(buffer, 0, result, 0, count); |
| return result; |
| } |
| |
| private static class Facet { |
| private String fieldName; |
| private boolean multiValued = true; |
| FieldType fieldType; |
| RandomFacetValue[] facetValues; |
| |
| Facet(String fieldName) { |
| this.fieldName = fieldName; |
| fieldType = FieldType.values()[random().nextInt(FieldType.values().length)]; |
| if ( FieldType.String.equals(fieldType)) { |
| // sortedDocValues are supported for string fields only |
| multiValued = random().nextBoolean(); |
| } |
| |
| fieldType = FieldType.String; |
| facetValues = new RandomFacetValue[NUMBER_OF_VALUES]; |
| for (int i = 0; i < NUMBER_OF_VALUES; i++) { |
| String value = createRandomValue(i); |
| facetValues[i] = new RandomFacetValue(value); |
| } |
| if (!multiValued) { |
| makeValuesSingle(); |
| } |
| } |
| |
| private String createRandomValue(int i) { |
| switch( fieldType ) { |
| case String: |
| return fieldName.substring(0, 2) + "_" + i; |
| case Float: |
| return createFloatValue(i); |
| default: |
| return String.valueOf(i); |
| } |
| } |
| |
| String getFieldNameForIndex() { |
| String multiValuedPostfix = multiValued ? "_multi" : "_single"; |
| return fieldName + fieldType.fieldPostfix + multiValuedPostfix; |
| } |
| |
| private void makeValuesSingle() { |
| for ( int i = 0; i < NUMBER_OF_PARENTS; i++) { |
| List<Integer> values = getValuesForParent(i); |
| if ( values.size() > 0) { |
| int singleValueOrd = values.get(random().nextInt(values.size())); |
| setSingleValueForParent(i,singleValueOrd); |
| } |
| for ( int j=0; j < NUMBER_OF_CHILDREN; j++) { |
| values = getValuesForChild(i,j); |
| if ( values.size() > 0 ) { |
| int singleValueOrd = values.get(random().nextInt(values.size())); |
| setSingleValueForChild(i, j, singleValueOrd); |
| } |
| } |
| } |
| } |
| |
| private List<Integer> getValuesForParent(int parentNumber) { |
| List<Integer> result = new ArrayList<>(); |
| for (int i = 0; i<NUMBER_OF_VALUES; i++) { |
| if (facetValues[i].postings[parentNumber].parentHasOwnValue) { |
| result.add(i); |
| } |
| } |
| return result; |
| } |
| |
| private void setSingleValueForParent(int parentNumber, int valueOrd) { |
| for (int i = 0; i<NUMBER_OF_VALUES; i++) { |
| facetValues[i].postings[parentNumber].parentHasOwnValue = (i == valueOrd); |
| } |
| } |
| |
| boolean additionalValueIsAllowedForParent(int parentNumber) { |
| return multiValued || getValuesForParent(parentNumber).size() == 0; |
| } |
| |
| private List<Integer> getValuesForChild(int parentNumber, int childNumber) { |
| List<Integer> result = new ArrayList<>(); |
| for (int i = 0; i<NUMBER_OF_VALUES; i++) { |
| if (facetValues[i].postings[parentNumber].childrenHaveValue[childNumber]) { |
| result.add(i); |
| } |
| } |
| return result; |
| } |
| |
| private void setSingleValueForChild(int parentNumber, int childNumber, int valueOrd) { |
| for (int i = 0; i<NUMBER_OF_VALUES; i++) { |
| facetValues[i].postings[parentNumber].childrenHaveValue[childNumber] = (i == valueOrd); |
| } |
| } |
| |
| boolean additionalValueIsAllowedForChild(int parentNumber, int childNumber) { |
| return multiValued || getValuesForChild(parentNumber,childNumber).size() == 0; |
| } |
| } |
| |
| private static class RandomFacetValue { |
| final String facetValue; |
| // rootDoc, level, docsOnLevel |
| RandomParentPosting[] postings; |
| |
| |
| public RandomFacetValue(String facetValue) { |
| this.facetValue = facetValue; |
| postings = new RandomParentPosting[NUMBER_OF_PARENTS]; |
| for (int i = 0; i < NUMBER_OF_PARENTS; i++) { |
| postings[i] = new RandomParentPosting(random().nextBoolean()); |
| } |
| } |
| |
| int getFacetCount(int[] parentNumbers, int[] childNumbers) { |
| int result = 0; |
| if (parentNumbers != null) { |
| for (int parentNumber : parentNumbers) { |
| if (postings[parentNumber].isMatched(childNumbers)) { |
| result++; |
| } |
| } |
| } else { |
| for (int i = 0; i < NUMBER_OF_PARENTS; i++) { |
| if (postings[i].isMatched(childNumbers)) { |
| result++; |
| } |
| } |
| } |
| return result; |
| } |
| } |
| |
| private enum FieldType { |
| Integer("_i"), |
| Float("_f"), |
| String("_s"); |
| private final String fieldPostfix; |
| |
| FieldType(String fieldPostfix) { |
| this.fieldPostfix = fieldPostfix; |
| } |
| } |
| |
| private static class RandomParentPosting { |
| boolean parentHasOwnValue; |
| boolean[] childrenHaveValue; |
| |
| RandomParentPosting(boolean expected) { |
| childrenHaveValue = new boolean[NUMBER_OF_CHILDREN]; |
| if (expected) { |
| // don't count parents |
| parentHasOwnValue = false;// random().nextBoolean(); |
| if (random().nextBoolean()) { |
| for (int i = 0; i < NUMBER_OF_CHILDREN; i++) { |
| childrenHaveValue[i] = random().nextBoolean(); |
| } |
| } |
| } |
| } |
| |
| boolean isMatched(int[] childNumbers) { |
| boolean result = parentHasOwnValue && (childNumbers == null || childNumbers.length > 0); |
| if (!result) { |
| if (childNumbers == null) { |
| for (boolean childHasValue : childrenHaveValue) { |
| result = childHasValue; |
| if (result) { |
| break; |
| } |
| } |
| } else { |
| for (int child : childNumbers) { |
| result = childrenHaveValue[child]; |
| if (result) { |
| break; |
| } |
| } |
| } |
| } |
| return result; |
| } |
| } |
| } |