Use this file to discover all available pages before exploring further.
The Searcher API allows you to implement custom search logic in Vespa. Searchers are components that process queries and results in a chain of responsibility pattern.
Every searcher extends the Searcher class and implements the search method:
package com.example;import com.yahoo.search.Query;import com.yahoo.search.Result;import com.yahoo.search.Searcher;import com.yahoo.search.searchchain.Execution;public class SimpleSearcher extends Searcher { @Override public Result search(Query query, Execution execution) { // Process the query, then pass it down the chain Result result = execution.search(query); // Process the result before returning return result; }}
Modifies the result after getting it from downstream:
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;public class ResultEnricher extends Searcher { @Override public Result search(Query query, Execution execution) { // Get result from downstream Result result = execution.search(query); // Process each hit for (Hit hit : result.hits()) { // Add custom field to each hit hit.setField("customField", computeValue(hit)); } return result; } private String computeValue(Hit hit) { return "enriched-" + hit.getId(); }}
Example from: application/src/test/java/com/yahoo/application/container/searchers/AddHitSearcher.java:10
Creates results without calling downstream searchers:
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;public class CustomBackendSearcher extends Searcher { @Override public Result search(Query query, Execution execution) { // Create a new result Result result = new Result(query); // Fetch data from external source (database, API, etc.) for (int i = 0; i < 10; i++) { Hit hit = new Hit("custom-" + i); hit.setField("title", "Custom result " + i); hit.setField("url", "http://example.com/" + i); hit.setRelevance(1.0 - (i * 0.1)); result.hits().add(hit); } result.setTotalHitCount(100); // Total available results return result; }}
The container creates the searcher instance and injects dependencies via constructor.
public class ConfiguredSearcher extends Searcher { private final String apiKey; @Inject public ConfiguredSearcher(MyConfig config) { this.apiKey = config.apiKey(); }}
2
In Service
The search() method is called by multiple threads concurrently. Keep shared state immutable.
3
Deconstruction
Override deconstruct() to clean up resources when the searcher is replaced.
@Overridepublic void deconstruct() { // Close connections, release resources super.deconstruct();}
import com.yahoo.search.result.Hit;@Overridepublic Result search(Query query, Execution execution) { Result result = execution.search(query); // Create and add a hit Hit hit = new Hit("custom-id", 1.0); // id and relevance hit.setField("title", "Custom Title"); hit.setField("description", "Custom description"); result.hits().add(hit); return result;}
Example from: application/src/test/java/com/yahoo/application/container/searchers/AddHitSearcher.java:19
import java.util.Comparator;@Overridepublic Result search(Query query, Execution execution) { Result result = execution.search(query); // Sort by custom field result.hits().sort(Comparator.comparing( hit -> (String) hit.getField("customField") )); return result;}
The fill() method fetches additional fields for hits:
@Overridepublic Result search(Query query, Execution execution) { Result result = execution.search(query); // Ensure hits have the 'full' summary class filled ensureFilled(result, "full", execution); // Now all hits have full summary fields available return result;}@Overridepublic void fill(Result result, String summaryClass, Execution execution) { // Custom fill logic if needed // Otherwise just delegate: execution.fill(result, summaryClass);}
import com.yahoo.search.result.ErrorMessage;@Overridepublic Result search(Query query, Execution execution) { if (query.getModel().getQueryString() == null) { return new Result(query, ErrorMessage.createBadRequest("Query string is required")); } Result result = execution.search(query); // Add error if something goes wrong if (result.getTotalHitCount() == 0) { result.hits().addError(ErrorMessage.createNoBackendsInService( "No results available")); } return result;}
@Overridepublic Result search(Query query, Execution execution) { if (!isInitialized()) { throw new RuntimeException("Searcher not properly initialized"); } return execution.search(query);}
Searchers are called by multiple threads concurrently. All mutable shared state must be thread-safe.
public class ThreadSafeSearcher extends Searcher { // Safe: Immutable, built in constructor private final Map<String, String> config; // Unsafe: Mutable shared state private int counter = 0; // DON'T DO THIS public ThreadSafeSearcher(MyConfig config) { Map<String, String> temp = new HashMap<>(); temp.put("key", config.value()); this.config = Collections.unmodifiableMap(temp); } @Override public Result search(Query query, Execution execution) { // Safe: Read-only access to immutable data String value = config.get("key"); // Safe: Local variables int localCounter = 0; return execution.search(query); }}
Unless you’re a source searcher, always call execution.search(query) to continue the chain.
// Goodpublic Result search(Query query, Execution execution) { modifyQuery(query); return execution.search(query);}// Bad - breaks the chainpublic Result search(Query query, Execution execution) { modifyQuery(query); return new Result(query);}
Respect Hit Windows
Searchers must return at least hits number of hits starting at offset.
public Result search(Query query, Execution execution) { int hits = query.getHits(); int offset = query.getOffset(); // Ensure you return the right window Result result = execution.search(query); // Don't remove hits that would make result < hits return result;}
Use Tracing for Debugging
Add trace messages at appropriate levels:
public Result search(Query query, Execution execution) { query.trace("MySearcher processing", 2); // Level 2: High-level operations // Level 3-5: Detailed debugging // Level 6+: Very detailed if (query.getTraceLevel() >= 3) { query.trace("Detailed info: " + someDetail, 3); } return execution.search(query);}
Handle Timeouts
Check query timeout and return early if time is running out:
public Result search(Query query, Execution execution) { long timeout = query.getTimeout(); long startTime = System.currentTimeMillis(); Result result = execution.search(query); // Check if we have time for additional processing if (System.currentTimeMillis() - startTime > timeout - 100) { return result; // Skip extra processing } enrichResults(result); return result;}