/*
* Licensed to Elastic Search and Shay Banon under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. Elastic Search 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.elasticsearch.index.search.child;
import gnu.trove.map.hash.TIntObjectHashMap;
import org.apache.lucene.index.IndexReader;
import org.apache.lucene.index.Term;
import org.apache.lucene.search.*;
import org.apache.lucene.util.ToStringUtils;
import org.elasticsearch.ElasticSearchIllegalArgumentException;
import org.elasticsearch.ElasticSearchIllegalStateException;
import org.elasticsearch.common.bytes.HashedBytesArray;
import org.elasticsearch.common.lucene.search.EmptyScorer;
import org.elasticsearch.search.internal.ScopePhase;
import org.elasticsearch.search.internal.SearchContext;
import java.io.IOException;
import java.util.*;
/**
*
*/
public class TopChildrenQuery extends Query implements ScopePhase.TopDocsPhase {
public static enum ScoreType {
MAX,
AVG,
SUM;
public static ScoreType fromString(String type) {
if ("max".equals(type)) {
return MAX;
} else if ("avg".equals(type)) {
return AVG;
} else if ("sum".equals(type)) {
return SUM;
}
throw new ElasticSearchIllegalArgumentException("No score type for child query [" + type + "] found");
}
}
private Query query;
private String scope;
private String parentType;
private String childType;
private ScoreType scoreType;
private int factor;
private int incrementalFactor;
private Map<Object, ParentDoc[]> parentDocs;
private int numHits = 0;
// Need to know if this query is properly used, otherwise the results are unexpected for example in the count api
private boolean properlyInvoked = false;
// Note, the query is expected to already be filtered to only child type docs
public TopChildrenQuery(Query query, String scope, String childType, String parentType, ScoreType scoreType, int factor, int incrementalFactor) {
this.query = query;
this.scope = scope;
this.childType = childType;
this.parentType = parentType;
this.scoreType = scoreType;
this.factor = factor;
this.incrementalFactor = incrementalFactor;
}
@Override
public Query query() {
return this;
}
@Override
public String scope() {
return scope;
}
@Override
public void clear() {
properlyInvoked = true;
parentDocs = null;
numHits = 0;
}
@Override
public int numHits() {
return numHits;
}
@Override
public int factor() {
return this.factor;
}
@Override
public int incrementalFactor() {
return this.incrementalFactor;
}
@Override
public void processResults(TopDocs topDocs, SearchContext context) {
Map<Object, TIntObjectHashMap<ParentDoc>> parentDocsPerReader = new HashMap<Object, TIntObjectHashMap<ParentDoc>>();
for (ScoreDoc scoreDoc : topDocs.scoreDocs) {
int readerIndex = context.searcher().readerIndex(scoreDoc.doc);
IndexReader subReader = context.searcher().subReaders()[readerIndex];
int subDoc = scoreDoc.doc - context.searcher().docStarts()[readerIndex];
// find the parent id
HashedBytesArray parentId = context.idCache().reader(subReader).parentIdByDoc(parentType, subDoc);
if (parentId == null) {
// no parent found
continue;
}
// now go over and find the parent doc Id and reader tuple
for (IndexReader indexReader : context.searcher().subReaders()) {
int parentDocId = context.idCache().reader(indexReader).docById(parentType, parentId);
if (parentDocId != -1 && !indexReader.isDeleted(parentDocId)) {
// we found a match, add it and break
TIntObjectHashMap<ParentDoc> readerParentDocs = parentDocsPerReader.get(indexReader.getCoreCacheKey());
if (readerParentDocs == null) {
readerParentDocs = new TIntObjectHashMap<ParentDoc>();
parentDocsPerReader.put(indexReader.getCoreCacheKey(), readerParentDocs);
}
ParentDoc parentDoc = readerParentDocs.get(parentDocId);
if (parentDoc == null) {
numHits++; // we have a hit on a parent
parentDoc = new ParentDoc();
parentDoc.docId = parentDocId;
parentDoc.count = 1;
parentDoc.maxScore = scoreDoc.score;
parentDoc.sumScores = scoreDoc.score;
readerParentDocs.put(parentDocId, parentDoc);
} else {
parentDoc.count++;
parentDoc.sumScores += scoreDoc.score;
if (scoreDoc.score > parentDoc.maxScore) {
parentDoc.maxScore = scoreDoc.score;
}
}
}
}
}
this.parentDocs = new HashMap<Object, ParentDoc[]>();
for (Map.Entry<Object, TIntObjectHashMap<ParentDoc>> entry : parentDocsPerReader.entrySet()) {
ParentDoc[] values = entry.getValue().values(new ParentDoc[entry.getValue().size()]);
Arrays.sort(values, PARENT_DOC_COMP);
parentDocs.put(entry.getKey(), values);
}
}
private static final ParentDocComparator PARENT_DOC_COMP = new ParentDocComparator();
static class ParentDocComparator implements Comparator<ParentDoc> {
@Override
public int compare(ParentDoc o1, ParentDoc o2) {
return o1.docId - o2.docId;
}
}
public static class ParentDoc {
public int docId;
public int count;
public float maxScore = Float.NaN;
public float sumScores = 0;
}
@Override
public Query rewrite(IndexReader reader) throws IOException {
Query newQ = query.rewrite(reader);
if (newQ == query) return this;
TopChildrenQuery bq = (TopChildrenQuery) this.clone();
bq.query = newQ;
return bq;
}
@Override
public void extractTerms(Set<Term> terms) {
query.extractTerms(terms);
}
@Override
public Weight createWeight(Searcher searcher) throws IOException {
if (!properlyInvoked) {
throw new ElasticSearchIllegalStateException("top_children query hasn't executed properly");
}
if (parentDocs != null) {
return new ParentWeight(searcher, query.weight(searcher));
}
return query.weight(searcher);
}
public String toString(String field) {
StringBuilder sb = new StringBuilder();
sb.append("score_child[").append(childType).append("/").append(parentType).append("](").append(query.toString(field)).append(')');
sb.append(ToStringUtils.boost(getBoost()));
return sb.toString();
}
class ParentWeight extends Weight {
final Searcher searcher;
final Weight queryWeight;
public ParentWeight(Searcher searcher, Weight queryWeight) throws IOException {
this.searcher = searcher;
this.queryWeight = queryWeight;
}
public Query getQuery() {
return TopChildrenQuery.this;
}
public float getValue() {
return getBoost();
}
@Override
public float sumOfSquaredWeights() throws IOException {
float sum = queryWeight.sumOfSquaredWeights();
sum *= getBoost() * getBoost();
return sum;
}
@Override
public void normalize(float norm) {
// nothing to do here....
}
@Override
public Scorer scorer(IndexReader reader, boolean scoreDocsInOrder, boolean topScorer) throws IOException {
ParentDoc[] readerParentDocs = parentDocs.get(reader.getCoreCacheKey());
if (readerParentDocs != null) {
return new ParentScorer(getSimilarity(searcher), readerParentDocs);
}
return new EmptyScorer(getSimilarity(searcher));
}
@Override
public Explanation explain(IndexReader reader, int doc) throws IOException {
return new Explanation(getBoost(), "not implemented yet...");
}
}
class ParentScorer extends Scorer {
private final ParentDoc[] docs;
private int index = -1;
private ParentScorer(Similarity similarity, ParentDoc[] docs) throws IOException {
super(similarity);
this.docs = docs;
}
@Override
public int docID() {
if (index >= docs.length) {
return NO_MORE_DOCS;
}
return docs[index].docId;
}
@Override
public int advance(int target) throws IOException {
int doc;
while ((doc = nextDoc()) < target) {
}
return doc;
}
@Override
public int nextDoc() throws IOException {
if (++index >= docs.length) {
return NO_MORE_DOCS;
}
return docs[index].docId;
}
@Override
public float score() throws IOException {
if (scoreType == ScoreType.MAX) {
return docs[index].maxScore;
} else if (scoreType == ScoreType.AVG) {
return docs[index].sumScores / docs[index].count;
} else if (scoreType == ScoreType.SUM) {
return docs[index].sumScores;
}
throw new ElasticSearchIllegalStateException("No support for score type [" + scoreType + "]");
}
}
}