blob: d21fbf634864765b2c7e62cc70601c42dcbce0ef [file] [log] [blame]
package org.apache.lucene.facet.search;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.lucene.facet.taxonomy.CategoryPath;
import org.apache.lucene.facet.taxonomy.TaxonomyReader;
import org.apache.lucene.util.CollectionUtil;
/*
* 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.
*/
/**
* Result of faceted search.
*
* @lucene.experimental
*/
public class FacetResult {
private static FacetResultNode addIfNotExist(Map<CategoryPath, FacetResultNode> nodes, FacetResultNode node) {
FacetResultNode n = nodes.get(node.label);
if (n == null) {
nodes.put(node.label, node);
n = node;
}
return n;
}
/**
* A utility for merging multiple {@link FacetResult} of the same
* (hierarchical) dimension into a single {@link FacetResult}, to reconstruct
* the hierarchy. The results are merged according to the following rules:
* <ul>
* <li>If two results share the same dimension (first component in their
* {@link CategoryPath}), they are merged.
* <li>If a result is missing ancestors in the other results, e.g. A/B/C but
* no corresponding A or A/B, these nodes are 'filled' with their label,
* ordinal and value (obtained from the respective {@link FacetArrays}).
* <li>If a result does not share a dimension with other results, it is
* returned as is.
* </ul>
* <p>
* <b>NOTE:</b> the returned results are not guaranteed to be in the same
* order of the input ones.
*
* @param results
* the results to merge
* @param taxoReader
* the {@link TaxonomyReader} to use when creating missing ancestor
* nodes
* @param dimArrays
* a mapping from a dimension to the respective {@link FacetArrays}
* from which to pull the nodes values
*/
public static List<FacetResult> mergeHierarchies(List<FacetResult> results, TaxonomyReader taxoReader,
Map<String, FacetArrays> dimArrays) throws IOException {
final Map<String, List<FacetResult>> dims = new HashMap<String,List<FacetResult>>();
for (FacetResult fr : results) {
String dim = fr.getFacetRequest().categoryPath.components[0];
List<FacetResult> frs = dims.get(dim);
if (frs == null) {
frs = new ArrayList<FacetResult>();
dims.put(dim, frs);
}
frs.add(fr);
}
final List<FacetResult> res = new ArrayList<FacetResult>();
for (List<FacetResult> frs : dims.values()) {
FacetResult mergedResult = frs.get(0);
if (frs.size() > 1) {
CollectionUtil.introSort(frs, new Comparator<FacetResult>() {
@Override
public int compare(FacetResult fr1, FacetResult fr2) {
return fr1.getFacetRequest().categoryPath.compareTo(fr2.getFacetRequest().categoryPath);
}
});
Map<CategoryPath, FacetResultNode> mergedNodes = new HashMap<CategoryPath,FacetResultNode>();
FacetArrays arrays = dimArrays != null ? dimArrays.get(frs.get(0).getFacetRequest().categoryPath.components[0]) : null;
for (FacetResult fr : frs) {
FacetResultNode frn = fr.getFacetResultNode();
FacetResultNode merged = mergedNodes.get(frn.label);
if (merged == null) {
CategoryPath parent = frn.label.subpath(frn.label.length - 1);
FacetResultNode childNode = frn;
FacetResultNode parentNode = null;
while (parent.length > 0 && (parentNode = mergedNodes.get(parent)) == null) {
int parentOrd = taxoReader.getOrdinal(parent);
double parentValue = arrays != null ? fr.getFacetRequest().getValueOf(arrays, parentOrd) : -1;
parentNode = new FacetResultNode(parentOrd, parentValue);
parentNode.label = parent;
parentNode.subResults = new ArrayList<FacetResultNode>();
parentNode.subResults.add(childNode);
mergedNodes.put(parent, parentNode);
childNode = parentNode;
parent = parent.subpath(parent.length - 1);
}
// at least one parent was added, so link the final (existing)
// parent with the child
if (parent.length > 0) {
if (!(parentNode.subResults instanceof ArrayList)) {
parentNode.subResults = new ArrayList<FacetResultNode>(parentNode.subResults);
}
parentNode.subResults.add(childNode);
}
// for missing FRNs, add new ones with label and value=-1
// first time encountered this label, add it and all its children to
// the map.
mergedNodes.put(frn.label, frn);
for (FacetResultNode child : frn.subResults) {
addIfNotExist(mergedNodes, child);
}
} else {
if (!(merged.subResults instanceof ArrayList)) {
merged.subResults = new ArrayList<FacetResultNode>(merged.subResults);
}
for (FacetResultNode sub : frn.subResults) {
// make sure sub wasn't already added
sub = addIfNotExist(mergedNodes, sub);
if (!merged.subResults.contains(sub)) {
merged.subResults.add(sub);
}
}
}
}
// find the 'first' node to put on the FacetResult root
CategoryPath min = null;
for (CategoryPath cp : mergedNodes.keySet()) {
if (min == null || cp.compareTo(min) < 0) {
min = cp;
}
}
FacetRequest dummy = new FacetRequest(min, frs.get(0).getFacetRequest().numResults) {
@Override
public double getValueOf(FacetArrays arrays, int idx) {
throw new UnsupportedOperationException("not supported by this request");
}
@Override
public FacetArraysSource getFacetArraysSource() {
throw new UnsupportedOperationException("not supported by this request");
}
};
mergedResult = new FacetResult(dummy, mergedNodes.get(min), -1);
}
res.add(mergedResult);
}
return res;
}
private final FacetRequest facetRequest;
private final FacetResultNode rootNode;
private final int numValidDescendants;
public FacetResult(FacetRequest facetRequest, FacetResultNode rootNode, int numValidDescendants) {
this.facetRequest = facetRequest;
this.rootNode = rootNode;
this.numValidDescendants = numValidDescendants;
}
/**
* Facet result node matching the root of the {@link #getFacetRequest() facet request}.
* @see #getFacetRequest()
* @see FacetRequest#categoryPath
*/
public final FacetResultNode getFacetResultNode() {
return rootNode;
}
/**
* Number of descendants of {@link #getFacetResultNode() root facet result
* node}, up till the requested depth.
*/
public final int getNumValidDescendants() {
return numValidDescendants;
}
/**
* Request for which this result was obtained.
*/
public final FacetRequest getFacetRequest() {
return this.facetRequest;
}
/**
* String representation of this facet result.
* Use with caution: might return a very long string.
* @param prefix prefix for each result line
* @see #toString()
*/
public String toString(String prefix) {
StringBuilder sb = new StringBuilder();
String nl = "";
// request
if (this.facetRequest != null) {
sb.append(nl).append(prefix).append("Request: ").append(
this.facetRequest.toString());
nl = "\n";
}
// total facets
sb.append(nl).append(prefix).append("Num valid Descendants (up to specified depth): ").append(
this.numValidDescendants);
nl = "\n";
// result node
if (this.rootNode != null) {
sb.append(nl).append(this.rootNode.toString(prefix + "\t"));
}
return sb.toString();
}
@Override
public String toString() {
return toString("");
}
}