A comprehensive, type-safe query builder for Vespa AI that provides a fluent API for constructing YQL (Vespa Query Language) queries. This library replaces manual string building with an intuitive, maintainable, and error-resistant approach to query construction.
go get github.com/vipulsodha/[email protected]Or for the latest version:
go get github.com/vipulsodha/vespa-go@latestThen import in your Go code:
import "github.com/vipulsodha/vespa-go"query, err := vespa.NewQueryBuilder().
Select("id", "title", "price").
From("products").
Where(vespa.Field("price").Between(10.0, 100.0)).
Build()
if err != nil {
log.Fatal(err)
}
// Generated YQL: select id, title, price from sources products where ((price >= 10) and (price <= 100))queryVector := []float32{0.1, 0.2, 0.3, 0.4, 0.5}
query, err := vespa.NewQueryBuilder().
Select("*").
From("products").
Where(vespa.Field("category").In("electronics", "gadgets")).
Rank(
vespa.NewRank().
AddCondition(vespa.Field("embedding_field").NearestNeighbor("query_vector", 1000)).
AddCondition(vespa.Field("brand").Contains("nike")),
).
WithInput("input.query(query_vector)", queryVector).
WithRanking("hybrid_profile").
Build()This version introduces significant improvements to make the builder more flexible and aligned with Vespa's full YQL capabilities:
- NEW: Use
nearestNeighbordirectly in WHERE clauses for primary filtering - NEW: Combine vector search with traditional filters using AND/OR logic
- NEW: Support for labels and distance thresholds in WHERE conditions
- NEW: Add any WHERE condition to RANK expressions using
AddCondition() - ENHANCED: Mix traditional rank features with condition-based ranking
- BACKWARD COMPATIBLE: All existing code continues to work unchanged
// Vector search as primary filter with additional ranking
query := vespa.NewQueryBuilder().
Where(
vespa.And(
vespa.Field("embedding").NearestNeighbor("query_vector", 1000),
vespa.Field("price").Between(20.0, 200.0),
),
).
Rank(
vespa.NewRank().
AddCondition(vespa.Field("popularity").Gte(0.5)).
AddCondition(vespa.Field("description").Contains("premium")),
).
Build()The QueryBuilder interface provides a fluent API for constructing queries:
type QueryBuilder interface {
Select(fields ...string) QueryBuilder
From(sources ...string) QueryBuilder
Where(condition WhereCondition) QueryBuilder
Rank(rankExpression RankExpression) QueryBuilder
WithRanking(profile string) QueryBuilder
WithHits(hits int) QueryBuilder
WithOffset(offset int) QueryBuilder
WithDefaultIndex(index string) QueryBuilder
WithInput(key string, value interface{}) QueryBuilder
WithQuery(query string) QueryBuilder
Build() (*VespaQuery, error)
BuildYQL() (string, error)
}The library supports comprehensive comparison operators:
| Operator | Method | Example |
|---|---|---|
= |
Eq(value) |
Field("price").Eq(100) |
!= |
NotEq(value) |
Field("status").NotEq("inactive") |
> |
Gt(value) |
Field("price").Gt(50) |
>= |
Gte(value) |
Field("rating").Gte(4.0) |
< |
Lt(value) |
Field("stock").Lt(10) |
<= |
Lte(value) |
Field("price").Lte(200) |
in |
In(values...) |
Field("category").In("a", "b") |
not in |
NotIn(values...) |
Field("brand").NotIn("excluded") |
contains |
Contains(value) |
Field("description").Contains("wireless") |
not contains |
NotContains(value) |
Field("title").NotContains("refurbished") |
matches |
Matches(pattern) |
Field("sku").Matches("^PRD-.*") |
| Logical Operators | ||
not |
Not(condition) |
Not(Field("brand").Contains("excluded")) |
sameElement |
ContainsSameElement(conditions...) |
Field("sizes").ContainsSameElement(Field("family").Contains("clothing"), Field("size_value").Contains("M")) |
| Vector Search | ||
nearestNeighbor |
NearestNeighbor(vector, hits, ...opts) |
Field("embedding").NearestNeighbor("query_vector", 1000) |
nearestNeighbor |
NearestNeighbor(vector, hits, WithLabel()) |
Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithLabel("main")) |
nearestNeighbor |
NearestNeighbor(vector, hits, WithThreshold()) |
Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithThreshold(0.8)) |
nearestNeighbor |
NearestNeighbor(vector, hits, WithApproximate()) |
Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithApproximate(false)) |
Rank expressions define how documents are scored and ranked. The API uses a single unified method AddCondition() for all ranking elements:
rank := vespa.NewRank().
AddCondition(vespa.Field("embedding").NearestNeighbor("query_vector", 1000)). // Vector similarity
AddCondition(vespa.Field("brand").Contains("nike air", vespa.WithPhraseMatching())). // Field conditions
AddCondition(vespa.UserQuery("default_index")). // Text queries (query text via WithQuery())
AddCondition(vespa.Custom("log(popularity) * 0.1")) // Custom expressionsKey Insight: All ranking elements (vector search, field conditions, user queries, custom expressions) implement the same WhereCondition interface and can be added using AddCondition(). This provides a clean, consistent API.
Use the generic field builder for all fields:
vespa.Field("price") // price field
vespa.Field("rating") // rating field
vespa.Field("category") // category field
vespa.Field("brand") // brand field
vespa.Field("color") // color field
vespa.Field("stock") // stock field
vespa.Field("status") // status field
vespa.Field("any_name") // any custom fieldCombine conditions with AND/OR/NOT logic:
// AND combination
condition := vespa.And(
vespa.Field("price").Gte(10),
vespa.Field("price").Lte(100),
)
// OR combination
condition := vespa.Or(
vespa.Field("brand").Eq("nike"),
vespa.Field("brand").Eq("adidas"),
)
// NOT combination - negates any condition
condition := vespa.Not(
vespa.Field("brand").Contains("excluded"),
)
// Nested logic with NOT
condition := vespa.And(
vespa.Field("price").Between(10, 100),
vespa.Not(
vespa.Or(
vespa.Field("brand").Contains("excluded1"),
vespa.Field("brand").Contains("excluded2"),
),
),
)
// XOR logic using NOT - either condition is true, but not both
condition := vespa.And(
vespa.Or(
vespa.Field("brand").Contains("nike"),
vespa.Field("item_types").Contains("shoes"),
),
vespa.Not(
vespa.And(
vespa.Field("brand").Contains("nike"),
vespa.Field("item_types").Contains("shoes"),
),
),
)Convenient range filtering:
// Price between 10 and 100
vespa.Field("price").Between(10.0, 100.0)
// Equivalent to: (price >= 10) AND (price <= 100)NEW: Use sameElement for querying arrays of structs or maps where all conditions must match within the same element:
// Find products with medium clothing sizes
vespa.Field("sizes").ContainsSameElement(
vespa.Field("family").Contains("clothing"),
vespa.Field("size_value").In("M", "Medium"),
)
// Find persons named John Smith born after 1980
vespa.Field("persons").ContainsSameElement(
vespa.Field("first_name").Contains("John"),
vespa.Field("last_name").Contains("Smith"),
vespa.Field("year_of_birth").Gt(1980),
)
// Find attributes with specific key-value pairs
vespa.Field("attributes").ContainsSameElement(
vespa.Field("key").Eq("color"),
vespa.Field("value").In("red", "blue", "green"),
)
// Combined with other conditions
vespa.And(
vespa.Field("sizes").ContainsSameElement(
vespa.Field("family").Contains("clothing"),
vespa.Field("size_value").Contains("L"),
),
vespa.Field("brand").Contains("nike"),
)Generated YQL Examples:
(sizes contains sameElement(family contains 'clothing', size_value in ('M', 'Medium')))
(persons contains sameElement(first_name contains 'John', last_name contains 'Smith', year_of_birth > 1980))
(attributes contains sameElement(key = 'color', value in ('red', 'blue', 'green')))
((sizes contains sameElement(family contains 'clothing', size_value contains 'L')) AND (brand contains 'nike'))
Why SameElement? Without sameElement, conditions might match across different elements of an array, leading to false positives. For example, a query for "John Smith" might match a document with one person named "John Doe" and another named "Jane Smith".
NEW: Use nearestNeighbor directly in WHERE clauses for vector-based filtering:
// Basic nearestNeighbor in WHERE clause
vespa.Field("embedding").NearestNeighbor("query_vector", 1000)
// With label for query tracking
vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithLabel("main_search"))
// With distance threshold
vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithThreshold(0.8))
// With approximate search (true for approximate, false for exact)
vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithApproximate(true))
// With exact search (more precise but slower)
vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithApproximate(false))
// Combined with other conditions
vespa.And(
vespa.Field("embedding").NearestNeighbor("query_vector", 1000),
vespa.Field("brand").Contains("nike"),
)Generated YQL Examples:
{targetHits:1000}nearestNeighbor(embedding, query_vector)
{targetHits:1000,label:'main_search'}nearestNeighbor(embedding, query_vector)
{targetHits:1000,distanceThreshold:0.800000}nearestNeighbor(embedding, query_vector)
{targetHits:1000,approximate:true}nearestNeighbor(embedding, query_vector)
{targetHits:1000,approximate:false}nearestNeighbor(embedding, query_vector)
({targetHits:1000}nearestNeighbor(embedding, query_vector) AND (brand contains 'nike'))
The approximate parameter controls whether Vespa performs approximate or exact nearest neighbor search:
approximate:true(Default for HNSW indexes): Uses approximate search algorithms for better performanceapproximate:false: Uses exact search for maximum precision but with higher computational cost
When to use approximate vs exact search:
// Use approximate search for:
// - Real-time applications requiring fast response times
// - Large-scale vector databases where slight precision trade-offs are acceptable
// - Most production search scenarios
vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithApproximate(true))
// Use exact search for:
// - High-precision requirements where accuracy is critical
// - Smaller datasets where performance isn't a bottleneck
// - Research or validation scenarios requiring perfect results
vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithApproximate(false))
// Combining with other parameters for exact search with filtering
vespa.Field("embedding").NearestNeighbor("query_vector", 1000,
vespa.WithApproximate(false),
vespa.WithThreshold(0.95),
vespa.WithLabel("high_precision_search"),
)Performance vs Accuracy Trade-off:
- Approximate: ~10-100x faster, 95-99% accuracy
- Exact: Slower but 100% accurate results
The query builder provides NearestNeighbor functionality that works in both WHERE clauses and RANK expressions using the Field-based API with functional options:
// Basic nearest neighbor (works in both WHERE and RANK)
vespa.Field("embedding_field").NearestNeighbor("query_vector", 1000)
// With functional options
vespa.Field("embedding_field").NearestNeighbor("query_vector", 1000,
vespa.WithLabel("main_search"),
vespa.WithThreshold(0.8),
vespa.WithApproximate(false),
)
// In RANK expressions
rank.AddCondition(vespa.Field("embedding_field").NearestNeighbor("query_vector", 1000, vespa.WithLabel("main_search")))
// In WHERE clauses
vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithThreshold(0.8))NEW: Use any WHERE condition within rank expressions:
rank := vespa.NewRank().
// All rank features and conditions use the same AddCondition method
AddCondition(vespa.Field("embedding").NearestNeighbor("query_vector", 1000)).
AddCondition(vespa.Field("brand").Contains("nike")).
AddCondition(vespa.Field("price").Gte(50.0)).
AddCondition(vespa.Field("category").In("electronics", "gadgets"))
// Generated: rank({targetHits:1000}nearestNeighbor(embedding, query_vector), (brand contains 'nike'), (price >= 50), (category in ('electronics', 'gadgets')))// Exact match using conditions
rank.AddCondition(vespa.Field("color").Contains("red"))
// Phrase matching for multiple keywords
rank.AddCondition(vespa.Field("brand").Contains("nike air max", vespa.WithPhraseMatching()))
// User query (query text passed via WithQuery())
rank.AddCondition(vespa.UserQuery("default_index"))NEW: The UserQuery() function provides flexible text search capabilities that can be used in both WHERE clauses and rank expressions.
Key Features:
- Standalone WHERE condition: Use
userQuery()as a primary filter - Rank expression: Use
userQuery()for text-based ranking - Flexible defaultIndex: Optional field specification for text search
- Separate query text: Query text passed via
WithQuery()method
Function Signature:
func UserQuery(defaultIndex ...string) WhereConditionUsage Examples:
// 1. Basic userQuery without default index
vespa.UserQuery() // Generates: userQuery()
// 2. UserQuery with specific field/index
vespa.UserQuery("title") // Generates: {defaultIndex:"title"}userQuery()
// 3. UserQuery in WHERE clause
query := vespa.NewQueryBuilder().
Select("id", "title", "price").
From("products").
Where(vespa.UserQuery("title")). // YQL: {defaultIndex:"title"}userQuery()
WithQuery("wireless headphones"). // HTTP: {"query": "wireless headphones"}
Build()
// 4. UserQuery combined with other conditions
query := vespa.NewQueryBuilder().
Select("*").
From("products").
Where(
vespa.And(
vespa.UserQuery("description"), // Text search in description field
vespa.Field("price").Between(20, 100),
vespa.Field("category").Eq("electronics"),
),
).
WithQuery("bluetooth speaker"). // The actual search text
Build()
// 5. UserQuery in rank expressions
query := vespa.NewQueryBuilder().
Select("id", "title", "score").
From("products").
Where(vespa.Field("category").In("electronics", "gadgets")).
Rank(
vespa.NewRank().
AddCondition(vespa.UserQuery("title")). // Text relevance ranking
AddCondition(vespa.Field("brand").Contains("premium")),
).
WithQuery("gaming headset"). // The actual search text
WithRanking("text_ranking").
Build()Generated Output:
{
"yql": "select id, title, score from sources products where (category in ('electronics', 'gadgets')) and rank({defaultIndex:\"title\"}userQuery(), (brand contains 'premium'))",
"query": "gaming headset",
"ranking": "text_ranking"
}Important Notes:
- Query text is passed via
WithQuery(), not as a parameter toUserQuery() - The YQL contains
userQuery()or{defaultIndex:"field"}userQuery() - The HTTP request contains the actual query text in the
"query"parameter - Works seamlessly in both WHERE clauses and rank expressions
// Add custom ranking expressions
rank.AddCondition(vespa.Custom("log(popularity) * 0.1"))NEW: The query builder now supports pagination through the WithOffset() method, enabling easy result paging:
// Basic pagination - page 1 (first 20 results)
query := vespa.NewQueryBuilder().
Select("id", "title", "price").
From("products").
Where(vespa.Field("category").Eq("electronics")).
WithHits(20).
Build()
// Page 2 (results 21-40)
query := vespa.NewQueryBuilder().
Select("id", "title", "price").
From("products").
Where(vespa.Field("category").Eq("electronics")).
WithHits(20).
WithOffset(20).
Build()
// Page 3 (results 41-60)
query := vespa.NewQueryBuilder().
Select("id", "title", "price").
From("products").
Where(vespa.Field("category").Eq("electronics")).
WithHits(20).
WithOffset(40).
Build()Pagination Helper Pattern:
func BuildPagedQuery(page int, itemsPerPage int) (*vespa.VespaQuery, error) {
offset := (page - 1) * itemsPerPage
return vespa.NewQueryBuilder().
Select("id", "title", "price", "brand").
From("products").
Where(vespa.Field("active").Eq(true)).
WithHits(itemsPerPage).
WithOffset(offset).
WithRanking("relevance").
Build()
}
// Usage:
query, err := BuildPagedQuery(3, 25) // Get page 3 with 25 items per page (offset=50)Generated Query Parameters:
hits: Controls the number of results returned (equivalent to SQLLIMIT)offset: Controls the number of results to skip (equivalent to SQLOFFSET)
Example Result:
{
"yql": "select id, title, price from sources products where (category contains 'electronics')",
"hits": 20,
"offset": 40,
"ranking": "bm25"
}Handle query vectors and other input parameters:
queryVector := []float32{0.1, 0.2, 0.3}
themeVector := []float32{0.4, 0.5, 0.6}
builder.
WithInput("input.query(query_vector)", queryVector).
WithInput("input.query(sort_vector)", themeVector)query, err := vespa.NewQueryBuilder().
Select("id", "title", "price", "brand").
From("products").
Where(
vespa.And(
vespa.And(
vespa.Field("price").Between(20.0, 200.0),
vespa.Field("category").Eq("electronics"),
),
vespa.Field("stock").Gt(0),
),
).
WithHits(20).
Build()queryVector := []float32{0.1, 0.2, 0.3, 0.4, 0.5}
query, err := vespa.NewQueryBuilder().
Select("*").
From("products").
Where(vespa.Field("category").In("electronics", "gadgets")).
Rank(
vespa.NewRank().
AddCondition(vespa.Field("embedding").NearestNeighbor("query_vector", 1000, vespa.WithLabel("semantic"))).
AddCondition(vespa.UserQuery("text_field")).
AddCondition(vespa.Field("brand").Contains("sony")),
).
WithInput("input.query(query_vector)", queryVector).
WithQuery("wireless bluetooth headphones").
WithRanking("hybrid_semantic_text").
WithHits(50).
Build()// Example: Research application requiring exact results
queryVector := []float32{0.1, 0.2, 0.3, 0.4, 0.5}
query, err := vespa.NewQueryBuilder().
Select("id", "title", "similarity_score", "research_metadata").
From("research_papers").
Where(
vespa.And(
// Exact vector search for maximum precision
vespa.Field("paper_embedding").NearestNeighbor("query_vector", 500,
vespa.WithApproximate(false),
vespa.WithThreshold(0.90),
vespa.WithLabel("exact_search"),
),
vespa.Field("peer_reviewed").Eq(true),
),
).
WithInput("input.query(query_vector)", queryVector).
WithRanking("academic_relevance").
WithHits(20).
Build()
// Generated query uses exact search: {targetHits:500,distanceThreshold:0.900000,approximate:false,label:'exact_search'}nearestNeighbor(paper_embedding, query_vector)// Example: E-commerce filters with exclusions
query, err := vespa.NewQueryBuilder().
Select("id", "title", "price", "brand", "category").
From("products").
Where(
vespa.And(
vespa.Field("category").Eq("electronics"),
vespa.Field("price").Between(50.0, 500.0),
// Exclude specific brands
vespa.Not(
vespa.Or(
vespa.Field("brand").Contains("excluded_brand1"),
vespa.Field("brand").Contains("excluded_brand2"),
),
),
// Exclude discontinued products
vespa.Not(vespa.Field("status").Eq("discontinued")),
),
).
WithHits(30).
Build()
// Generated YQL: select id, title, price, brand, category from sources products
// where ((category contains 'electronics') AND ((price >= 50) and (price <= 500))
// AND !(((brand contains 'excluded_brand1') OR (brand contains 'excluded_brand2')))
// AND !((status contains 'discontinued')))// Example: Find products with either nike brand OR shoes category, but not both
// This is useful for recommendation systems or A/B testing scenarios
query, err := vespa.NewQueryBuilder().
Select("brand", "item_types", "title", "price").
From("listings_sg_v1").
Where(
vespa.And(
// At least one condition is true
vespa.Or(
vespa.Field("brand").Contains("nike"),
vespa.Field("item_types").Contains("shoes"),
),
// But not both are true (XOR logic)
vespa.Not(
vespa.And(
vespa.Field("brand").Contains("nike"),
vespa.Field("item_types").Contains("shoes"),
),
),
),
).
WithHits(25).
Build()
// Generated YQL: select brand, item_types, title, price from sources listings_sg_v1
// where (((brand contains 'nike') OR (item_types contains 'shoes'))
// AND !(((brand contains 'nike') AND (item_types contains 'shoes'))))
// This will match:
// - Nike shirts (nike brand, non-shoes category) ✓
// - Adidas shoes (non-nike brand, shoes category) ✓
// But exclude:
// - Nike shoes (nike brand AND shoes category) ✗This demonstrates the new capability to use nearestNeighbor directly in WHERE clauses:
queryVector := []float32{0.1, 0.2, 0.3, 0.4, 0.5}
query, err := vespa.NewQueryBuilder().
Select("id", "title", "price", "brand").
From("products").
Where(
vespa.And(
// Vector search as primary filter
vespa.Field("embedding").NearestNeighbor("query_vector", 1000),
// Combined with traditional filters
vespa.Field("price").Between(20.0, 200.0),
vespa.Field("brand").Contains("nike"),
),
).
// Additional ranking on top of the filtered results
Rank(
vespa.NewRank().
AddCondition(vespa.Field("popularity").Gte(0.5)).
AddContains("description", "premium"),
).
WithInput("input.query(query_vector)", queryVector).
WithRanking("popularity_boost").
WithHits(50).
Build()
// Generated YQL:
// select id, title, price, brand from sources products
// where ({targetHits:1000}nearestNeighbor(embedding, query_vector) AND ((price >= 20) and (price <= 200)) AND (brand contains 'nike'))
// rank((popularity >= 0.5), description contains 'premium')This demonstrates querying arrays of structs or maps with sameElement:
// Find products with specific size characteristics and attributes
query, err := vespa.NewQueryBuilder().
Select("id", "name", "sizes", "attributes").
From("products").
Where(
vespa.And(
// Find products with medium clothing sizes
vespa.Field("sizes").ContainsSameElement(
vespa.Field("family").Contains("clothing"),
vespa.Field("size_value").In("M", "Medium"),
),
// Find products with red color attribute
vespa.Field("attributes").ContainsSameElement(
vespa.Field("key").Eq("color"),
vespa.Field("value").Contains("red"),
),
// Regular filters
vespa.Field("brand").Contains("nike"),
vespa.Field("price").Between(50.0, 200.0),
),
).
WithHits(25).
Build()
if err != nil {
log.Fatal(err)
}
// Generated YQL:
// select id, name, sizes, attributes from sources products
// where ((sizes contains sameElement(family contains 'clothing', size_value in ('M', 'Medium')))
// AND (attributes contains sameElement(key = 'color', value contains 'red'))
// AND (brand contains 'nike')
// AND ((price >= 50) and (price <= 200)))Real-world use case: This query ensures that:
- The "M" or "Medium" size is specifically for "clothing" (not shoes, accessories, etc.)
- The "red" color is a defined attribute (not just mentioned in description)
- Avoids false matches where conditions span different array elements
// Complex e-commerce search with vector and text filtering
queryVector := []float32{0.1, 0.2, 0.3}
categories := []string{"fashion", "accessories"}
sizeFilter := []string{"M", "L", "XL"}
query, err := vespa.NewQueryBuilder().
Select("id", "title", "price", "brand", "category").
From("products").
Where(
vespa.And(
vespa.Field("category").In(categories...),
vespa.Field("size").In(sizeFilter...),
vespa.Field("price").Between(25.0, 150.0),
),
).
Rank(
vespa.NewRank().
AddCondition(vespa.Field("embedding_field").NearestNeighbor("query_vector", 1000)).
AddCondition(vespa.UserQuery("text_field")).
AddCondition(vespa.Field("brand").Contains("nike")).
AddCondition(vespa.Field("color").Contains("blue")),
).
WithInput("input.query(query_vector)", queryVector).
WithQuery("summer casual wear").
Build()
if err != nil {
log.Fatal(err)
}
// The generated query includes:
// - Vector search with query vector
// - Category, size, and price filters
// - Text search with user query
// - Brand and color ranking featuresquery, err := vespa.NewQueryBuilder().
Select("id", "title", "price", "rating", "brand").
From("products").
Where(
vespa.And(
vespa.And(
vespa.And(
vespa.Field("price").Between(10.0, 500.0),
vespa.Or(
vespa.And(
vespa.Field("brand").In("premium1", "premium2"),
vespa.Field("rating").Gte(4.5),
),
vespa.And(
vespa.And(
vespa.Field("brand").In("budget1", "budget2"),
vespa.Field("rating").Gte(4.0),
),
vespa.Field("price").Lte(100.0),
),
),
),
vespa.Field("stock").Gt(0),
),
vespa.Field("status").Eq("active"),
),
).
WithHits(30).
Build()queryVector := []float32{/* main embedding */}
imageVector := []float32{/* image embedding */}
colorVector := []float32{/* color embedding */}
query, err := vespa.NewQueryBuilder().
Select("id", "title", "image_url", "price").
From("visual_products").
Where(vespa.Field("category").In("fashion", "accessories")).
Rank(
vespa.NewRank().
AddCondition(vespa.Field("text_embedding").NearestNeighbor("query_vector", 1000, vespa.WithLabel("text_sim"))).
AddCondition(vespa.Field("image_embedding").NearestNeighbor("image_vector", 500, vespa.WithLabel("visual_sim"))).
AddCondition(vespa.Field("color_embedding").NearestNeighbor("color_vector", 200, vespa.WithLabel("color_sim"))),
).
WithInput("input.query(query_vector)", queryVector).
WithInput("input.query(image_vector)", imageVector).
WithInput("input.query(color_vector)", colorVector).
WithRanking("multi_modal_ranking").
Build()// Realistic e-commerce pagination scenario
func SearchProductsWithPagination(category string, page int, itemsPerPage int) (*vespa.VespaQuery, error) {
// Calculate offset for the requested page
offset := (page - 1) * itemsPerPage
query, err := vespa.NewQueryBuilder().
Select("id", "title", "price", "image_url", "rating").
From("products").
Where(
vespa.And(
vespa.Field("category").Contains(category),
vespa.Field("active").Eq(true),
vespa.Field("stock").Gt(0),
),
).
WithHits(itemsPerPage).
WithOffset(offset).
WithRanking("popularity_boost").
Build()
return query, err
}
// Usage examples:
// Page 1: results 1-20
query1, _ := SearchProductsWithPagination("electronics", 1, 20) // offset=0, hits=20
// Page 3: results 41-60
query3, _ := SearchProductsWithPagination("electronics", 3, 20) // offset=40, hits=20
// Large page: results 981-1000
queryLarge, _ := SearchProductsWithPagination("electronics", 50, 20) // offset=980, hits=20Generated queries show pagination parameters:
- Page 1:
{"hits": 20, "offset": 0} - Page 3:
{"hits": 20, "offset": 40} - Page 50:
{"hits": 20, "offset": 980}
Handle query vectors and other input parameters:
queryVector := []float32{0.1, 0.2, 0.3}
builder.WithInput("input.query(query_vector)", queryVector)// ✅ Good: Use field builders
vespa.Field("price").Gte(10.0)
// ❌ Avoid: Manual string building
"price >= 10.0"// ✅ Good: Use Between for ranges
condition := vespa.Field("price").Between(10.0, 100.0)
// ❌ Avoid: Manual range building
condition := vespa.And(
vespa.Field("price").Gte(10.0),
vespa.Field("price").Lte(100.0),
)// ✅ Good: Direct parameter assignment
builder.WithInput("input.query(query_vector)", vector)// ✅ Good: Break down complex logic
priceCondition := vespa.Field("price").Between(10, 100)
categoryCondition := vespa.Field("category").In("electronics", "gadgets")
stockCondition := vespa.Field("stock").Gt(0)
finalCondition := vespa.And(priceCondition, categoryCondition, stockCondition)
// ❌ Avoid: Deeply nested inline conditionsRun the test suite:
go test ./internal/lib/vespa/...Key test coverage includes:
- ✅ All comparison operators (eq, neq, gt, gte, lt, lte, in, not in, contains, matches)
- ✅ Boolean logic combinations (AND, OR, nested conditions)
- ✅ Range conditions and field builders
- ✅ SameElement conditions for complex fields (arrays of structs/maps)
- ✅ Rank expressions and features
- ✅ Nearest neighbor search with all parameters (label, distanceThreshold, approximate)
- ✅ Approximate vs exact vector search functionality
- ✅ Text matching (exact, phrase, fuzzy)
- ✅ Complete query building and validation
- ✅ Helper functions and convenience methods
- ✅ Input parameter handling
- ✅ Error cases and validation failures
The library provides detailed validation errors:
type ValidationError struct {
Field string
Message string
}
// Example usage
query, err := builder.Build()
if err != nil {
if validationErr, ok := err.(*vespa.ValidationError); ok {
log.Printf("Validation error in field '%s': %s",
validationErr.Field, validationErr.Message)
}
return err
}yqlBuilder := strings.Builder{}
yqlBuilder.WriteString(fmt.Sprintf("select color, brand, price from sources %s where ", indexName))
if len(categories) > 0 {
categoryFilter := buildCategoryFilter(categories)
yqlBuilder.WriteString(fmt.Sprintf("(%s) and ", categoryFilter))
}
if priceFilter.Min > 0 {
yqlBuilder.WriteString(fmt.Sprintf("(price >= %f) and ", priceFilter.Min))
}
yqlBuilder.WriteString(fmt.Sprintf("rank(({targetHits:%d}nearestNeighbor(%s, query_vector))",
1000, retrievalFieldName))
// ... more manual buildingquery, err := vespa.NewQueryBuilder().
Select("color", "brand", "price").
From(indexName).
Where(
vespa.And(
vespa.Field("category").In(categories...),
vespa.Field("price").Between(priceFilter.Min, priceFilter.Max),
),
).
Rank(
vespa.NewRank().
AddCondition(vespa.Field(retrievalFieldName).NearestNeighbor("query_vector", 1000)),
).
Build()- Memory: Query builders reuse internal slices and maps efficiently
- String Building: YQL generation uses efficient string building techniques
- Type Safety: Compile-time type checking prevents runtime errors
When adding new features:
- Add corresponding types in
types.go - Implement functionality in appropriate files (
conditions.go,rank.go, etc.) - Add helper functions in
helpers.go - Include validation in
validation.go - Write comprehensive tests in
builder_test.go - Update this README with examples
This query builder transforms complex, error-prone string manipulation into clean, maintainable, and type-safe Go code. It provides the full power of Vespa's YQL while ensuring correctness and readability.