Table of contents :

Building a Product Search Query with Vespa

100-days-of-vespa-ai-woocommerce-017

Table of contents :

A WooCommerce product search query should combine the shopper’s text with the filters and ranking signals needed by the storefront. Vespa’s Query API provides the query execution layer, while YQL defines which product documents are eligible and which operators are used to match them.

Start with a product document model

The exact field names depend on the Vespa schema, but a product search collection commonly contains fields such as:

  • title and description for lexical matching.
  • sku and product_id for exact identification.
  • categories, brand, and attributes for filtering and faceting.
  • price for numeric range filters.
  • stock_status and visibility for catalog eligibility.
  • embedding for semantic matching.

Fields used in filters and ranking should generally be declared as attributes. Text fields that participate in full-text search need appropriate indexing, such as index or summary | index, depending on the field definition.

Build a basic lexical query

For a conventional keyword search, use the userInput operator. It parses the shopper’s text using Vespa’s query language and creates the corresponding full-text query tree.

select * from product
where userInput(@user-query)

The @user-query reference reads the value from the query parameter named user-query. A request for “blue waterproof jacket” can therefore be sent as:

GET /search/?yql=select%20*%20from%20product%20where%20userInput%28%40user-query%29&user-query=blue%20waterproof%20jacket&ranking=product_search

Always URL-encode the YQL expression and user input in a real HTTP request. Vespa is not a relational database, but treating query construction as structured input rather than string concatenation still makes the integration easier to validate and maintain.

Add WooCommerce catalog filters

Storefront filters should be expressed as YQL predicates alongside the text query. For example, an in-stock search for products between 50 and 150 euros in the “running” category could be written as:

select * from product
where userInput(@user-query)
and stock_status = "instock"
and visibility = "catalog"
and price >= 50
and price <= 150
and categories contains "running"

This assumes that categories is an array-like attribute and that price is numeric. If a value is stored as a single string, use the predicate appropriate for that field representation. The schema and the query must agree on types; otherwise, filters may fail at query compilation or produce unexpected matches.

For multiple selected brands or categories, generate a grouped expression rather than repeating unrelated conditions:

and (brand = "Acme" or brand = "Northwind")

In a WooCommerce integration, map URL parameters such as min_price, max_price, brand, and taxonomy slugs to typed query predicates in application code. Do not allow arbitrary client-supplied YQL to pass through to the search endpoint.

Use a rank profile for product relevance

Matching determines which documents are eligible. Ranking determines their order. Keep ranking logic in a Vespa rank profile instead of embedding large ranking expressions in every request.

rank-profile product_search inherits default {
    first-phase {
        expression: nativeRank
    }
}

A production profile can combine text relevance with business signals. For example, a normalized popularity attribute can be blended with the text score:

rank-profile product_search inherits default {
    first-phase {
        expression: 0.85 * nativeRank + 0.15 * attribute(popularity_score)
    }
}

The business score should be normalized before combining it with a text score. Inventory, product visibility, and hard catalog rules are usually better implemented as filters, not as ranking penalties. That prevents unavailable products from appearing merely because they have strong textual relevance.

Build a hybrid lexical and semantic query

Semantic retrieval is useful for queries whose meaning does not exactly match the product copy. A hybrid query can retrieve lexical matches and nearest-neighbor matches in the same request:

select * from product
where stock_status = "instock"
and rank(
    userInput(@user-query),
    ({targetHits:100}nearestNeighbor(embedding, q_embedding))
)

The document schema needs an indexed tensor field, and the rank profile needs a query tensor input. A simplified schema fragment might look like:

field embedding type tensor<float>(x[384]) {
    indexing: attribute | index
}

rank-profile hybrid {
    inputs {
        query(q_embedding) tensor<float>(x[384])
    }
    first-phase {
        expression: 0.7 * nativeRank + 0.3 * closeness(field, embedding)
    }
}

The application must provide q_embedding with the same dimensionality and tensor schema as the document embeddings. If the deployment has a configured embedding model, the query can be generated at request time with Vespa’s embedding functionality; otherwise, generate the vector in the application or an embedding service and send it as a query tensor.

GET /search/?yql=select%20*%20from%20product%20where%20rank%28userInput%28%40user-query%29%2C%20%7BtargetHits%3A100%7DnearestNeighbor%28embedding%2Cq_embedding%29%29&user-query=lightweight%20trail%20shoes&ranking=hybrid&input.query%28q_embedding%29=...

The targetHits annotation controls how many approximate nearest-neighbor candidates Vespa should consider before ranking. Increase it when filters remove many candidates, but validate the latency and relevance impact with production-like data.

Return fields needed by the WooCommerce storefront

Use a document summary to control the response payload. A product result normally needs an identifier, title, permalink, price, currency, stock status, image URL, and selected attributes. Avoid returning the full indexed document if the storefront does not need it.

GET /search/?yql=select%20*%20from%20product%20where%20userInput%28%40user-query%29&user-query=running%20shoes&ranking=product_search&presentation.summary=product_result

The WooCommerce adapter can transform each Vespa hit into the response shape expected by the shop frontend. Keep the canonical WooCommerce product ID in the document so that prices, stock, and links can be reconciled with the source catalog.

Validate and debug the query

  1. Test the generated YQL and parameters with the Vespa Query API before connecting the storefront.
  2. Verify that filters use the correct field types and values, especially taxonomy slugs and numeric prices.
  3. Inspect relevance scores and rank features with the appropriate ranking diagnostics when results are unexpected.
  4. Test empty queries, misspellings, SKU searches, quoted phrases, and combinations of filters.
  5. Measure latency with realistic catalog size, filter selectivity, and concurrent WooCommerce traffic.

A practical request builder should produce a stable query structure, validate allowed filter names, omit empty predicates, encode all parameter values, and select the rank profile explicitly. This gives the agency control over search behavior without coupling the storefront to undocumented Vespa internals.

Trending posts
You might also like