Skip to main content
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.

Overview

Searchers participate in search chains where they:
  • Modify queries before passing them to the next searcher (query rewriting)
  • Process results before returning them (result processing)
  • Federate to multiple backends in parallel
  • Generate results from external sources
  • Implement workflows with multiple search calls
Source: container-search/src/main/java/com/yahoo/search/Searcher.java:12

Basic Searcher

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;
    }
}
Source: container-search/src/main/java/com/yahoo/search/Searcher.java:89

Searcher Types

Query Processor

Modifies the query before passing it down:
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.searchchain.Execution;

public class QueryRewriter extends Searcher {
    @Override
    public Result search(Query query, Execution execution) {
        // Modify the query
        String originalQuery = query.getModel().getQueryString();
        if (originalQuery.contains("fix")) {
            query.getModel().setQueryString(originalQuery.replace("fix", "repair"));
        }

        // Add a ranking parameter
        query.getRanking().setProfile("bm25");

        // Pass the modified query down the chain
        return execution.search(query);
    }
}

Result Processor

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

Result Source

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

Federator

Searches multiple backends in parallel:
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.Searcher;
import com.yahoo.search.searchchain.Execution;
import com.yahoo.search.searchchain.AsyncExecution;
import java.util.List;
import java.util.ArrayList;
import java.util.concurrent.TimeUnit;

public class FederatingSearcher extends Searcher {
    @Override
    public Result search(Query query, Execution execution) {
        // Create async executions for parallel search
        AsyncExecution backend1 = new AsyncExecution("backend1", execution);
        AsyncExecution backend2 = new AsyncExecution("backend2", execution);

        // Start searches in parallel
        backend1.search(query);
        backend2.search(query);

        // Get results (blocks until complete)
        Result result1 = backend1.get(1000, TimeUnit.MILLISECONDS);
        Result result2 = backend2.get(1000, TimeUnit.MILLISECONDS);

        // Merge results
        Result merged = new Result(query);
        merged.hits().addAll(result1.hits().asUnorderedHits());
        merged.hits().addAll(result2.hits().asUnorderedHits());

        return merged;
    }
}

Searcher Lifecycle

1

Construction

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.
@Override
public void deconstruct() {
    // Close connections, release resources
    super.deconstruct();
}
Source: container-search/src/main/java/com/yahoo/search/Searcher.java:41

Working with Queries

Query Properties

@Override
public Result search(Query query, Execution execution) {
    // Get query string
    String queryString = query.getModel().getQueryString();

    // Get custom properties
    String customParam = query.properties().getString("myParam");
    int limit = query.properties().getInteger("limit", 10); // with default

    // Set properties
    query.properties().set("newParam", "value");

    // Access ranking
    query.getRanking().setProfile("bm25");
    query.getRanking().setQueryCache(false);

    return execution.search(query);
}

Query Tracing

@Override
public Result search(Query query, Execution execution) {
    query.trace("Starting custom processing", 2);

    // Do work
    String modified = processQuery(query.getModel().getQueryString());
    query.trace("Modified query to: " + modified, 3);

    query.getModel().setQueryString(modified);
    return execution.search(query);
}

Working with Results

Adding Hits

import com.yahoo.search.result.Hit;

@Override
public 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

Filtering Hits

@Override
public Result search(Query query, Execution execution) {
    Result result = execution.search(query);

    // Remove hits based on criteria
    result.hits().removeIf(hit ->
        hit.getField("language") != null &&
        !hit.getField("language").equals("en")
    );

    return result;
}

Reordering Hits

import java.util.Comparator;

@Override
public 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;
}

Fill Operations

The fill() method fetches additional fields for hits:
@Override
public 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;
}

@Override
public void fill(Result result, String summaryClass, Execution execution) {
    // Custom fill logic if needed
    // Otherwise just delegate:
    execution.fill(result, summaryClass);
}
Source: container-search/src/main/java/com/yahoo/search/Searcher.java:139

Error Handling

Expected Errors

Return errors in the result:
import com.yahoo.search.result.ErrorMessage;

@Override
public 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;
}

Unexpected Errors

Throw runtime exceptions:
@Override
public Result search(Query query, Execution execution) {
    if (!isInitialized()) {
        throw new RuntimeException("Searcher not properly initialized");
    }
    return execution.search(query);
}
Source: container-search/src/main/java/com/yahoo/search/Searcher.java:117

Configuration

Searchers can receive configuration through dependency injection:
import com.yahoo.search.Searcher;
import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.searchchain.Execution;

public class ConfigurableSearcher extends Searcher {
    private final String endpoint;
    private final int timeout;

    @Inject
    public ConfigurableSearcher(MySearcherConfig config) {
        this.endpoint = config.endpoint();
        this.timeout = config.timeout();
    }

    @Override
    public Result search(Query query, Execution execution) {
        // Use configuration
        String url = endpoint + "/search?q=" + query.getModel().getQueryString();
        return execution.search(query);
    }
}

Thread Safety

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);
    }
}
Source: container-search/src/main/java/com/yahoo/search/Searcher.java:37

Best Practices

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

Testing Searchers

import com.yahoo.search.Query;
import com.yahoo.search.Result;
import com.yahoo.search.searchchain.Execution;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class MySearcherTest {
    @Test
    void testQueryRewriting() {
        MySearcher searcher = new MySearcher();
        Query query = new Query("?query=test");
        Execution execution = new Execution(
            Execution.Context.createContextStub());

        Result result = searcher.search(query, execution);

        assertNotNull(result);
        assertEquals("modified-test", query.getModel().getQueryString());
    }
}

Build docs developers (and LLMs) love