// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.prelude.searcher;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.result.Hit;
import com.yahoo.search.searchchain.Execution;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
*
Implements a document source. You pass in a query and a Result
* set. When this Searcher is called with that query it will return
* that result set.
*
* This supports multi-phase search.
*
* To avoid having to add type information for the fields, a quck hack is used to
* support testing of attribute prefetching.
* Any field in the configured hits which has a name starting by attribute
* will be returned when attribute prefetch filling is requested.
*
* @author bratseth
*/
@SuppressWarnings({"rawtypes"})
public class DocumentSourceSearcher extends Searcher {
// as for the SuppressWarnings annotation above, we are inside
// com.yahoo.prelude, this is old stuff, really no point firing off those
// warnings here...
private Result defaultFilledResult;
private Map completelyFilledResults = new HashMap<>();
private Map attributeFilledResults = new HashMap<>();
private Map unFilledResults = new HashMap<>();
//private Result defaultUnfilledResult;
/** Time (in ms) at which the index of this searcher was last modified */
long editionTimeStamp=0;
private int queryCount;
public DocumentSourceSearcher() {
addDefaultResults();
}
/**
* Adds a result which can be returned either as empty,
* filled or attribute only filled later.
* Summary fields starting by "a" are attributes, others are not.
*
* @return true when replacing an existing <query, result> pair.
*/
public boolean addResultSet(Query query, Result fullResult) {
Result emptyResult = new Result(query.clone());
Result attributeResult = new Result(query.clone());
emptyResult.setTotalHitCount(fullResult.getTotalHitCount());
attributeResult.setTotalHitCount(fullResult.getTotalHitCount());
int counter=0;
for (Iterator i = fullResult.hits().deepIterator();i.hasNext();) {
Hit fullHit = (Hit)i.next();
Hit emptyHit = fullHit.clone();
emptyHit.clearFields();
emptyHit.setFillable();
emptyHit.setRelevance(fullHit.getRelevance());
Hit attributeHit = fullHit.clone();
removePropertiesNotStartingByA(attributeHit);
attributeHit.setFillable();
attributeHit.setRelevance(fullHit.getRelevance());
for (Object propertyKeyObject : fullHit.fields().keySet()) {
String propertyKey=propertyKeyObject.toString();
if (propertyKey.startsWith("attribute"))
attributeHit.setField(propertyKey, fullHit.getField(propertyKey));
}
if (fullHit.getField(Hit.SDDOCNAME_FIELD)!=null)
attributeHit.setField(Hit.SDDOCNAME_FIELD, fullHit.getField(Hit.SDDOCNAME_FIELD));
// A simple summary lookup mechanism, similar to FastSearch's
emptyHit.setField("summaryid", String.valueOf(counter));
attributeHit.setField("summaryid", String.valueOf(counter));
fullHit.setField("summaryid", String.valueOf(counter));
counter++;
emptyResult.hits().add(emptyHit);
attributeResult.hits().add(attributeHit);
}
unFilledResults.put(getQueryKeyClone(query), emptyResult);
attributeFilledResults.put(getQueryKeyClone(query), attributeResult);
if (completelyFilledResults.put(getQueryKeyClone(query), fullResult.clone()) != null) {
setEditionTimeStamp(System.currentTimeMillis());
return true;
}
return false;
}
/**
* Returns a query clone which has offset and hits set to null. This is used by access to
* the maps using the query as key to achieve lookup independent of offset/hits value
*/
private com.yahoo.search.Query getQueryKeyClone(com.yahoo.search.Query query) {
com.yahoo.search.Query key=query.clone();
key.setWindow(0,0);
key.getModel().setSources("");
return key;
}
private void removePropertiesNotStartingByA(Hit hit) {
List toRemove=new java.util.ArrayList<>();
for (Iterator i= ((Set) hit.fields().keySet()).iterator(); i.hasNext(); ) {
String key=(String)i.next();
if (!key.startsWith("a"))
toRemove.add(key);
}
for (Iterator i=toRemove.iterator(); i.hasNext(); ) {
String propertyName=i.next();
hit.removeField(propertyName);
}
}
private void addDefaultResults() {
Query q = new Query("?query=default");
Result r = new Result(q);
r.hits().add(new Hit("http://default-1.html"));
r.hits().add(new Hit("http://default-2.html"));
r.hits().add(new Hit("http://default-3.html"));
r.hits().add(new Hit("http://default-4.html"));
defaultFilledResult = r;
addResultSet(q, r);
}
public long getEditionTimeStamp(){
long myEditionTime;
synchronized(this){
myEditionTime=this.editionTimeStamp;
}
return myEditionTime;
}
public void setEditionTimeStamp(long editionTime) {
synchronized(this){
this.editionTimeStamp=editionTime;
}
}
public Result search(com.yahoo.search.Query query, Execution execution) {
queryCount++;
Result r;
r = unFilledResults.get(getQueryKeyClone(query));
if (r == null) {
r = defaultFilledResult.clone();
} else {
r = r.clone();
}
r.setQuery(query);
r.hits().trim(query.getOffset(), query.getHits());
return r;
}
@Override
public void fill(com.yahoo.search.Result result, String summaryClass, Execution execution) {
Result filledResult;
if ("attributeprefetch".equals(summaryClass))
filledResult=attributeFilledResults.get(getQueryKeyClone(result.getQuery()));
else
filledResult = completelyFilledResults.get(getQueryKeyClone(result.getQuery()));
if (filledResult == null) {
filledResult = defaultFilledResult;
}
fillHits(filledResult,result,summaryClass);
}
private void fillHits(Result source,Result target,String summaryClass) {
for (Iterator hitsToFill= target.hits().deepIterator() ; hitsToFill.hasNext();) {
Hit hitToFill = (Hit) hitsToFill.next();
String summaryId= (String) hitToFill.getField("summaryid");
if (summaryId==null) continue; // Can not fill this
Hit filledHit = lookupBySummaryId(source,summaryId);
if (filledHit==null)
throw new RuntimeException("Can't fill hit with summaryid '" + summaryId + "', not present");
for (Iterator props= filledHit.fieldIterator();props.hasNext();) {
Map.Entry propertyEntry = (Map.Entry)props.next();
hitToFill.setField(propertyEntry.getKey().toString(),
propertyEntry.getValue());
}
hitToFill.setFilled(summaryClass);
}
target.analyzeHits();
}
private Hit lookupBySummaryId(Result result,String summaryId) {
for (Iterator i= result.hits().deepIterator(); i.hasNext(); ) {
Hit hit=(Hit)i.next();
if (summaryId.equals(hit.getField("summaryid"))) {
return hit;
}
}
return null;
}
/**
* Returns the number of queries made to this searcher since the last
* reset. For testing - not reliable if multiple threads makes
* queries simultaneously
*/
public int getQueryCount() {
return queryCount;
}
public void resetQueryCount() {
queryCount=0;
}
}