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