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;}