GAP School Module 07 — AI Layer Lesson 7.4

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 situation

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.


What I did

Intent extraction

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:

PHP
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 : []; }

Query building from intent

The extracted intent is translated into WP_Query arguments. Each extracted field maps to the appropriate meta query, taxonomy query, or keyword search:

PHP
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 search endpoint

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:

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

Why it matters

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 Anchor build

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.


Do this, not that

  • Use low temperature (0.1) for intent extraction. This is parsing, not generation. You want deterministic, precise output — not creative interpretation.
  • Define a closed list of categories for the model to choose from. Open-ended category extraction produces idiosyncratic values. Providing the exhaustive list constrains the output to values your query builder knows how to handle.
  • Always fall back to keyword search on failure. AI infrastructure is not 100% available. Core search can’t fail because the intent parser hit its daily cap.
  • Return the extracted intent to the frontend. Displaying “Showing results for: pontoon boats under $40k” based on the parsed intent helps the buyer understand what the system understood — and helps them refine if it got something wrong.
  • Cap NL search requests per session. A user running 50 searches in a session shouldn’t generate 50 AI calls. Cache the intent extraction for the same query string for 1 hour.
When you’re ready to build

The lessons are yours. When you want it built, we’re here.

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.

We’ll use your email to send you a fast-track to a GAP build and occasional notes on how GAP builds digital sales departments. Lessons stay 100% free — no email required to read any of them. We never share or sell your information. Unsubscribe any time. Privacy policy at gapindustriesllc.com/privacy.html.

Done learning how it’s built? We’ll build it.

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 →