Intent extraction from free text, WP_Query builder from structured output, keyword fallback, and why temperature 0.1 is correct for parsing tasks.
Keyword search for inventory is the default, but keyword search is bad at capturing intent. A buyer who types “something under 30k good for the lake and not too big” has clear intent — they’re not going to break it into the right filter combinations. The NL query engine translates natural language into structured inventory queries.
The Anchor site’s existing search had keyword matching against title, description, and stock number, plus manual filters for price range and category. The problem: buyers don’t think in filters. They think in outcomes. “A family boat that won’t break the bank” isn’t a set of checkboxes — it’s a set of constraints that need to be extracted and translated.
About 30% of search queries on the site were clearly intent-phrased (“boats for fishing under 20k”, “used ski boat with low hours”) and returning poor results because the keyword matcher was looking for the literal words, not interpreting the intent.
The NL engine takes a natural language query and extracts structured constraints. The system prompt defines the exact output schema and constrains categories to a closed list:
function [client]_parse_nl_query( string $query ): array {
$system = <<<PROMPT
You are a search intent parser for a boat dealership inventory system.
Given a natural language search query, extract the following fields if present:
- price_max: maximum price in USD (integer)
- price_min: minimum price in USD (integer)
- length_max: maximum length in feet (integer)
- length_min: minimum length in feet (integer)
- year_min: minimum model year (integer)
- hours_max: maximum engine hours (integer)
- categories: array of relevant categories from: [ski_wake, fishing, pontoon, deck, cruiser, runabout, jet, sailboat]
- condition: "new" or "used" (string, if specified)
- keywords: array of important keywords not captured above (brand names, specific features)
Return ONLY valid JSON. If a field is not specified in the query, omit it entirely.
Example output: {"price_max": 30000, "categories": ["fishing"], "hours_max": 500}
PROMPT;
$result = [client]_ai_request( [
'task' => 'nl_query',
'system' => $system,
'max_tokens' => 256,
'temperature' => 0.1, // Low temperature — this is extraction, not generation
'messages' => [
[ 'role' => 'user', 'content' => $query ],
],
] );
if ( ! $result['ok'] ) {
return []; // Fall back to keyword search on failure
}
$parsed = json_decode( $result['content'], true );
return is_array( $parsed ) ? $parsed : [];
}
The extracted intent is translated into WP_Query arguments. Each extracted field maps to the appropriate meta query, taxonomy query, or keyword search:
function [client]_build_query_from_intent( array $intent ): array {
$query_args = [
'post_type' => '[client]_unit',
'post_status' => 'publish',
'posts_per_page' => 24,
];
$meta_query = [ 'relation' => 'AND' ];
$tax_query = [];
if ( ! empty( $intent['price_max'] ) ) {
$meta_query[] = [
'key' => '[client]_price',
'value' => (int) $intent['price_max'],
'compare' => '<=',
'type' => 'NUMERIC',
];
}
if ( ! empty( $intent['price_min'] ) ) {
$meta_query[] = [
'key' => '[client]_price',
'value' => (int) $intent['price_min'],
'compare' => '>=',
'type' => 'NUMERIC',
];
}
if ( ! empty( $intent['hours_max'] ) ) {
$meta_query[] = [
'key' => '[client]_hours',
'value' => (int) $intent['hours_max'],
'compare' => '<=',
'type' => 'NUMERIC',
];
}
if ( ! empty( $intent['year_min'] ) ) {
$meta_query[] = [
'key' => '[client]_year',
'value' => (int) $intent['year_min'],
'compare' => '>=',
'type' => 'NUMERIC',
];
}
if ( ! empty( $intent['condition'] ) ) {
$meta_query[] = [
'key' => '[client]_condition',
'value' => sanitize_key( $intent['condition'] ),
];
}
if ( ! empty( $intent['categories'] ) ) {
$tax_query[] = [
'taxonomy' => '[client]_category',
'field' => 'slug',
'terms' => array_map( 'sanitize_key', $intent['categories'] ),
];
}
if ( count( $meta_query ) > 1 ) {
$query_args['meta_query'] = $meta_query;
}
if ( ! empty( $tax_query ) ) {
$query_args['tax_query'] = $tax_query;
}
// Fall back to keyword search for remaining terms
if ( ! empty( $intent['keywords'] ) ) {
$query_args['s'] = implode( ' ', array_map( 'sanitize_text_field', $intent['keywords'] ) );
}
return $query_args;
}
The NL search is exposed as an AJAX handler. If intent extraction fails for any reason — API down, daily cap hit, malformed query — the handler falls back to a standard keyword search:
add_action( 'wp_ajax_nopriv_[client]_nl_search', '[client]_handle_nl_search' );
add_action( 'wp_ajax_[client]_nl_search', '[client]_handle_nl_search' );
function [client]_handle_nl_search(): void {
check_ajax_referer( '[client]_search_nonce', 'nonce' );
$query_text = sanitize_text_field( $_POST['query'] ?? '' );
if ( ! $query_text ) {
wp_send_json_error( 'No query' );
return;
}
// Try NL parsing first
$intent = [client]_parse_nl_query( $query_text );
$query_args = ! empty( $intent )
? [client]_build_query_from_intent( $intent )
: [
'post_type' => '[client]_unit',
'post_status' => 'publish',
's' => $query_text,
'posts_per_page' => 24,
];
$results = new WP_Query( $query_args );
$units = array_map( '[client]_format_search_result', $results->posts );
wp_send_json_success( [
'units' => $units,
'total' => $results->found_posts,
'used_nl' => ! empty( $intent ),
'intent' => $intent, // Returned for frontend labeling
] );
}
The NL engine is a translation layer, not a magic search. Its value comes from correctly extracting structured constraints that the buyer stated in natural language. “Under $30k” → price_max: 30000 is useful. A model confusing “good for the lake” with a geographic filter is not.
The temperature-0.1 setting is intentional. Intent extraction is a classification task, not a generative one. Low temperature makes the model deterministic and precise. High temperature produces creative interpretations that are wrong in subtle ways.
The fallback to keyword search on intent extraction failure is critical. If the AI budget cap is hit, if the API is down, or if the query is malformed enough that parsing fails, the user still gets results. Never let AI infrastructure be a single point of failure for a core feature.
The NL engine handles 30% of search queries — specifically the queries that used to return zero results with keyword matching. Of those, 74% return at least one result with the NL interpretation. Conversion rate on NL-interpreted searches is 1.8× the conversion rate on keyword searches: the buyer who knows what they want and can express it naturally is further along in the buying process than a generic browser.
The used_nl flag in the response is displayed as a small label (“Showing AI-matched results”) on the search page. It sets buyer expectations and explains why results may look different from a keyword search.
Every lesson stays free — no account, no paywall, no email gate, ever. But if you’d rather have this system standing on your business than wire all 48 lessons yourself, leave your email. We’ll send you a direct line to a build — and you’ll be first to hear when we add new tools to the curriculum.
None of this gates a single lesson. The curriculum was free before you got here and it stays that way.
You came here to understand the system, and now you do. If you’d rather have it standing on your business than spend the next three months wiring it yourself, GAP Concierge is the same architecture from these lessons — a white-label AI agent that knows your catalog and captures your leads — set up for you, from $97/mo.
See GAP Concierge →