// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. package com.yahoo.search.searchchain.testutil; import java.util.Collection; import java.util.HashSet; import java.util.Set; import java.util.Map; import java.util.HashMap; import java.util.List; import com.yahoo.net.URI; 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; /** *

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 */ public class DocumentSourceSearcher extends Searcher { // using null as name in the API would just be a horrid headache public static final String DEFAULT_SUMMARY_CLASS = "default"; // TODO: update tests to explicitly set hits, so that the default results can be removed entirely. private Result defaultFilledResult; private Map completelyFilledResults = new HashMap<>(); private Map unFilledResults = new HashMap<>(); private Map> summaryClasses = new HashMap<>(); private int queryCount; public DocumentSourceSearcher() { addDefaultResults(); } /** * Adds a result which can be searched for and filled. * Summary fields starting by "a" are attributes, others are not. * * @return true when replacing an existing <query, result> pair. */ public boolean addResult(Query query, Result fullResult) { Result emptyResult = new Result(query.clone()); emptyResult.setTotalHitCount(fullResult.getTotalHitCount()); for (Hit fullHit : fullResult.hits().asList()) { Hit emptyHit = fullHit.clone(); emptyHit.clearFields(); emptyHit.setFillable(); emptyHit.setRelevance(fullHit.getRelevance()); emptyResult.hits().add(emptyHit); } unFilledResults.put(getQueryKeyClone(query), emptyResult); if (completelyFilledResults.put(getQueryKeyClone(query), fullResult.clone()) != null) { // TODO: throw exception if the key exists from before, change the method to void return true; } return false; } public void addSummaryClass(String name, Set fields) { summaryClasses.put(name,fields); } public void addSummaryClassByCopy(String name, Collection fields) { addSummaryClass(name, new HashSet<>(fields)); } private void addDefaultResults() { Query q = new Query("?query=default"); Result r = new Result(q); r.hits().add(new Hit("http://default-1.html", 0)); r.hits().add(new Hit("http://default-2.html", 0)); r.hits().add(new Hit("http://default-3.html", 0)); r.hits().add(new Hit("http://default-4.html", 0)); defaultFilledResult = r; addResult(q, r); } @Override public Result search(Query query, Execution execution) { queryCount++; Result 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; } /** * Returns a query clone which has sourcr, 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 Query getQueryKeyClone(Query query) { Query key = query.clone(); key.setWindow(0,0); key.getModel().setSources(""); return key; } @Override public void fill(Result result, String summaryClass, Execution execution) { Result filledResult; filledResult = completelyFilledResults.get(getQueryKeyClone(result.getQuery())); if (filledResult == null) { filledResult = defaultFilledResult; } fillHits(filledResult,result,summaryClass); } private void fillHits(Result filledHits, Result hitsToFill, String summaryClass) { Set fieldsToFill = summaryClasses.get(summaryClass); if (fieldsToFill == null ) { fieldsToFill = summaryClasses.get(DEFAULT_SUMMARY_CLASS); } for (Hit hitToFill : hitsToFill.hits()) { Hit filledHit = getMatchingFilledHit(hitToFill.getId(), filledHits); if (filledHit != null) { if (fieldsToFill != null) { copyFieldValuesThatExist(filledHit,hitToFill,fieldsToFill); } else { // TODO: remove this block and update fieldsToFill above to throw an exception if no appropriate summary class is found for (Map.Entry propertyEntry : filledHit.fields().entrySet()) { hitToFill.setField(propertyEntry.getKey(), propertyEntry.getValue()); } } hitToFill.setFilled(summaryClass == null ? DEFAULT_SUMMARY_CLASS : summaryClass); } } hitsToFill.analyzeHits(); } private Hit getMatchingFilledHit(URI hitToFillId, Result filledHits) { Hit filledHit = null; for ( Hit filledHitCandidate : filledHits.hits()) { if ( hitToFillId == filledHitCandidate.getId() ) { filledHit = filledHitCandidate; break; } } return filledHit; } private void copyFieldValuesThatExist(Hit filledHit, Hit hitToFill, Set fieldsToFill) { for (String fieldToFill : fieldsToFill ) { if ( filledHit.getField(fieldToFill) != null ) { hitToFill.setField(fieldToFill, filledHit.getField(fieldToFill)); } } } /** * 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; } }