Vespa components (searchers, document processors, handlers) are packaged as OSGi bundles and deployed to the container. This guide covers creating, building, and deploying custom component bundles.
Overview
Vespa uses OSGi bundles to:
Isolate components : Each bundle has its own classpath
Manage dependencies : Explicit import/export of packages
Enable hot deployment : Update components without restarting
Version management : Multiple versions can coexist
Project Structure
A typical Vespa plugin project:
my-plugin/
├── pom.xml
├── src/
│ ├── main/
│ │ ├── java/
│ │ │ └── com/
│ │ │ └── example/
│ │ │ ├── MySearcher.java
│ │ │ ├── MyDocProc.java
│ │ │ └── MyHandler.java
│ │ └── resources/
│ │ └── configdefinitions/
│ │ └── my-config.def
│ └── test/
│ └── java/
│ └── com/
│ └── example/
│ └── MySearcherTest.java
└── target/
└── my-plugin-1.0.0-deploy.jar
Maven Configuration
Basic POM Setup
<? xml version = "1.0" encoding = "UTF-8" ?>
< project xmlns = "http://maven.apache.org/POM/4.0.0"
xmlns:xsi = "http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation = "http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd" >
< modelVersion > 4.0.0 </ modelVersion >
< groupId > com.example </ groupId >
< artifactId > my-plugin </ artifactId >
< version > 1.0.0 </ version >
< packaging > container-plugin </ packaging >
< properties >
< vespa.version > 8.350.43 </ vespa.version >
< maven.compiler.source > 17 </ maven.compiler.source >
< maven.compiler.target > 17 </ maven.compiler.target >
</ properties >
< dependencies >
<!-- Vespa dependencies -->
< dependency >
< groupId > com.yahoo.vespa </ groupId >
< artifactId > container </ artifactId >
< version > ${vespa.version} </ version >
< scope > provided </ scope >
</ dependency >
<!-- For search components -->
< dependency >
< groupId > com.yahoo.vespa </ groupId >
< artifactId > container-search </ artifactId >
< version > ${vespa.version} </ version >
< scope > provided </ scope >
</ dependency >
<!-- For document processing -->
< dependency >
< groupId > com.yahoo.vespa </ groupId >
< artifactId > docproc </ artifactId >
< version > ${vespa.version} </ version >
< scope > provided </ scope >
</ dependency >
<!-- Testing -->
< dependency >
< groupId > org.junit.jupiter </ groupId >
< artifactId > junit-jupiter </ artifactId >
< version > 5.9.3 </ version >
< scope > test </ scope >
</ dependency >
</ dependencies >
< build >
< plugins >
< plugin >
< groupId > com.yahoo.vespa </ groupId >
< artifactId > bundle-plugin </ artifactId >
< version > ${vespa.version} </ version >
< extensions > true </ extensions >
</ plugin >
</ plugins >
</ build >
</ project >
Bundle Plugin
The bundle-plugin automatically generates the OSGi manifest from your code:
< plugin >
< groupId > com.yahoo.vespa </ groupId >
< artifactId > bundle-plugin </ artifactId >
< version > ${vespa.version} </ version >
< extensions > true </ extensions >
< configuration >
<!-- Optional: Specify bundle activator -->
< bundleActivator > com.example.MyBundleActivator </ bundleActivator >
<!-- Optional: Control warnings -->
< suppressWarningPublicApi > false </ suppressWarningPublicApi >
< failOnWarnings > false </ failOnWarnings >
</ configuration >
</ plugin >
Dependency Management
Provided Dependencies
Use provided scope for all Vespa artifacts to avoid bundling them:
< dependency >
< groupId > com.yahoo.vespa </ groupId >
< artifactId > container </ artifactId >
< scope > provided </ scope >
</ dependency >
Compile Dependencies
Libraries in compile scope are bundled with your plugin:
<!-- This will be included in the bundle -->
< dependency >
< groupId > com.google.guava </ groupId >
< artifactId > guava </ artifactId >
< version > 32.1.2-jre </ version >
< scope > compile </ scope >
</ dependency >
Excluding Transitive Dependencies
< dependency >
< groupId > com.example </ groupId >
< artifactId > some-library </ artifactId >
< version > 1.0.0 </ version >
< exclusions >
< exclusion >
< groupId > org.unwanted </ groupId >
< artifactId > unwanted-dep </ artifactId >
</ exclusion >
</ exclusions >
</ dependency >
Component Examples
Simple Searcher Plugin
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 ) {
// Add custom query processing
query . trace ( "SimpleSearcher activated" , 2 );
Result result = execution . search (query);
// Add custom result processing
result . hits (). forEach (hit ->
hit . setField ( "plugin_version" , "1.0.0" )
);
return result;
}
}
Document Processor Plugin
package com.example;
import com.yahoo.docproc.SimpleDocumentProcessor;
import com.yahoo.document.DocumentPut;
public class SimpleDocProc extends SimpleDocumentProcessor {
@ Override
public void process ( DocumentPut put ) {
var doc = put . getDocument ();
// Add processing timestamp
doc . setFieldValue ( "processed_at" ,
System . currentTimeMillis ());
// Add processor identifier
doc . setFieldValue ( "processor" , "SimpleDocProc" );
}
}
HTTP Handler Plugin
package com.example;
import com.yahoo.container.jdisc.HttpRequest;
import com.yahoo.container.jdisc.HttpResponse;
import com.yahoo.container.jdisc.ThreadedHttpRequestHandler;
import java.io.IOException;
import java.io.OutputStream;
import java.util.concurrent.Executor;
public class SimpleHandler extends ThreadedHttpRequestHandler {
public SimpleHandler ( Executor executor ) {
super (executor);
}
@ Override
public HttpResponse handle ( HttpRequest request ) {
return new JsonResponse ();
}
private static class JsonResponse extends HttpResponse {
JsonResponse () {
super ( 200 );
}
@ Override
public void render ( OutputStream output ) throws IOException {
output . write ( "{ \" status \" : \" ok \" }" . getBytes ());
}
@ Override
public String getContentType () {
return "application/json" ;
}
}
}
Building the Bundle
Build with Maven
This creates target/my-plugin-1.0.0-deploy.jar
Verify the Bundle
jar -tf target/my-plugin-1.0.0-deploy.jar
Check that your classes and the OSGi manifest are present: META-INF/MANIFEST.MF
com/example/MySearcher.class
com/example/MyDocProc.class
com/example/MyHandler.class
Inspect the Manifest
jar -xf target/my-plugin-1.0.0-deploy.jar META-INF/MANIFEST.MF
cat META-INF/MANIFEST.MF
Look for OSGi headers like Export-Package and Import-Package
Deployment
Application Package Structure
application/
├── services.xml
├── schemas/
│ └── myschema.sd
└── components/
└── my-plugin-1.0.0-deploy.jar
services.xml Configuration
<? xml version = "1.0" encoding = "UTF-8" ?>
< services version = "1.0" >
< container id = "default" version = "1.0" >
<!-- Search chain with custom searcher -->
< search >
< chain id = "default" inherits = "vespa" >
< searcher id = "com.example.SimpleSearcher"
bundle = "my-plugin" />
</ chain >
</ search >
<!-- Document processing with custom processor -->
< document-processing >
< chain id = "default" >
< documentprocessor id = "com.example.SimpleDocProc"
bundle = "my-plugin" />
</ chain >
</ document-processing >
<!-- Custom HTTP handler -->
< handler id = "com.example.SimpleHandler"
bundle = "my-plugin" >
< binding > http://*/custom/* </ binding >
</ handler >
< nodes >
< node hostalias = "node1" />
</ nodes >
</ container >
</ services >
Deploy the Application
vestpa deploy application/
Configuration Integration
Define Configuration Schema
Create src/main/resources/configdefinitions/my-plugin.def:
namespace=com.example
# Maximum number of results
maxResults int default=100
# Enable debug mode
enableDebug bool default=false
# Service endpoint
serviceUrl string default="http://localhost:8080"
# Timeout in milliseconds
timeout int default=5000
Use Configuration in Component
package com.example;
import com.yahoo.search.Searcher;
import com.example.MyPluginConfig;
public class ConfiguredSearcher extends Searcher {
private final int maxResults ;
private final boolean debugEnabled ;
private final String serviceUrl ;
public ConfiguredSearcher ( MyPluginConfig config ) {
this . maxResults = config . maxResults ();
this . debugEnabled = config . enableDebug ();
this . serviceUrl = config . serviceUrl ();
}
@ Override
public Result search ( Query query , Execution execution ) {
if (debugEnabled) {
query . trace ( "ConfiguredSearcher with maxResults=" +
maxResults, 1 );
}
Result result = execution . search (query);
// Trim results if needed
while ( result . hits (). size () > maxResults) {
result . hits (). remove ( result . hits (). size () - 1 );
}
return result;
}
}
Override Configuration in services.xml
< searcher id = "com.example.ConfiguredSearcher" bundle = "my-plugin" >
< config name = "com.example.my-plugin" >
< maxResults > 50 </ maxResults >
< enableDebug > true </ enableDebug >
< serviceUrl > https://api.example.com </ serviceUrl >
</ config >
</ searcher >
OSGi Manifest Details
The bundle plugin generates the OSGi manifest based on your code. Key headers:
Export-Package
Packages your bundle exposes to others:
Export-Package: com.example.api;version="1.0.0"
Import-Package
Packages your bundle needs from the platform:
Import-Package: com.yahoo.search;version="[8,9)",
com.yahoo.component;version="[8,9)"
Bundle-SymbolicName
Unique identifier for your bundle:
Bundle-SymbolicName: my-plugin
Advanced Bundle Features
Bundle Activator
For initialization logic when the bundle starts:
package com.example;
import org.osgi.framework.BundleActivator;
import org.osgi.framework.BundleContext;
public class MyBundleActivator implements BundleActivator {
@ Override
public void start ( BundleContext context ) {
System . out . println ( "Bundle started: " +
context . getBundle (). getSymbolicName ());
}
@ Override
public void stop ( BundleContext context ) {
System . out . println ( "Bundle stopped" );
}
}
Configure in pom.xml:
< plugin >
< groupId > com.yahoo.vespa </ groupId >
< artifactId > bundle-plugin </ artifactId >
< configuration >
< bundleActivator > com.example.MyBundleActivator </ bundleActivator >
</ configuration >
</ plugin >
Multiple Components in One Bundle
// All in the same bundle
package com.example;
public class SearchPlugin extends Searcher { /* ... */ }
public class FeedPlugin extends SimpleDocumentProcessor { /* ... */ }
public class ApiPlugin extends ThreadedHttpRequestHandler { /* ... */ }
All are available once the bundle is deployed.
Testing
Unit Tests
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions. * ;
public class SimpleSearcherTest {
@ Test
public void testSearcher () {
SimpleSearcher searcher = new SimpleSearcher ();
Query query = new Query ( "test" );
Execution execution = createMockExecution ();
Result result = searcher . search (query, execution);
assertNotNull (result);
assertTrue ( result . hits (). get ( 0 ). fields ()
. containsKey ( "plugin_version" ));
}
}
Integration Tests
import com.yahoo.application.Application;
import com.yahoo.application.container.Search;
public class IntegrationTest {
@ Test
public void testWithApplication () throws Exception {
try ( Application app = Application . fromApplicationPackage (
Paths . get ( "src/test/application" ))) {
Search search = app . getJDisc ( "default" ). search ();
Result result = search . process (
new Query ( "?query=test" ));
assertEquals ( 200 , result . getHitCount ());
}
}
}
Troubleshooting
Check that:
The class is in the bundle JAR
Import-Package includes required packages
Bundle is properly deployed to components/
Verify with: jar -tf target/my-plugin-1.0.0-deploy.jar | grep MyClass
Missing dependency. Either:
Add the dependency with compile scope to bundle it
Use provided scope and ensure it’s available in the platform
Check imports in manifest: unzip -p target/my-plugin-1.0.0-deploy.jar META-INF/MANIFEST.MF
Verify:
Bundle artifact ID matches reference in services.xml
Component ID is the fully qualified class name
Bundle is in the components/ directory
Check deployment: vespa status
vespa log --level error
Best Practices
Keep bundles focused : One responsibility per bundle
Version carefully : Use semantic versioning
Test thoroughly : Unit and integration tests
Document configuration : Provide clear config examples
Monitor performance : Profile component behavior
Handle errors gracefully : Don’t crash the container
Bundle Plugin Details
The Vespa bundle plugin (from ~/workspace/source/bundle-plugin) automatically:
Analyzes your compiled classes
Generates OSGi Import-Package headers
Generates OSGi Export-Package headers
Validates dependency scope correctness
Warns about non-public API usage
Creates a deployable JAR with OSGi metadata
Next Steps