Building an AI-Powered Search Intent and ReRanking System for Automotive Wheels and Parts

Building an AI-Powered Search Intent and ReRanking System for Automotive Wheels and Parts

Automotive product search is difficult because customers rarely enter clean, structured queries. A shopper may type nissan 18, 17 honda 2015, a misspelled vehicle name, or an exact original equipment number. Each query can contain several possible meanings, but a traditional keyword search engine treats the entire input as plain text.

This creates three common problems: relevant products are not retrieved, irrelevant products appear above better matches, and exact SKU or original equipment number searches are not prioritized. The proposed solution is an AI-powered search intelligence layer that interprets the query before search execution and re-ranks the returned products afterward.

In this article, we will design that system around a Ruby on Rails application, an existing search engine such as Elasticsearch or OpenSearch, and a lightweight intent extraction service. This is the kind of problem where solid Ruby on Rails development practices make the difference between a system that's maintainable and one that collapses under edge cases.

Why Traditional Automotive Search Produces Weak Results

A standard search flow looks like this:

User query → Search engine → Product results

The search engine receives the raw query and compares its terms with indexed product fields. That works for a precise search such as 4030ET007, but it becomes unreliable when the input contains abbreviations, spelling mistakes, incomplete fitment data, or ambiguous numbers.

Consider this query:

nissan 18

The number 18 might refer to:

  • An 18-inch wheel
  • A 2018 Nissan vehicle
  • A wheel width or another fitment value
  • Part of a product code

Keyword matching alone cannot reliably determine which interpretation the customer intended. In a wheel catalog, however, an 18-inch diameter is often a more likely interpretation than a vehicle year written without four digits.

The same problem appears in queries such as:

17 honda 2015

A human can infer that the customer probably wants a 17-inch wheel for a 2015 Honda. A text search engine may instead treat 17 and 2015 as unrelated keywords.

Misspellings create another failure mode:

chevrolett silverado

Unless the search engine has effective fuzzy matching or synonym handling, the catalog may return no results even when Chevrolet Silverado products are available.

Introducing a Search Intelligence Layer

A stronger architecture separates intent interpretation, candidate retrieval, and final ranking.

User query

Intent extraction and normalization

Structured search request

Elasticsearch or OpenSearch

Candidate products

Relevance re-ranking

Validation and optional retry

Final results

(Diagram: Request flow between the Rails application, AI intent service, search engine, and product database.)

Alt text: Architecture diagram showing an automotive search query moving through intent extraction, candidate retrieval, re-ranking, validation, and final results.

This design does not require replacing the existing search infrastructure. The search engine still performs the expensive candidate retrieval. The intelligence layer improves the query sent to it and evaluates the quality of the products it returns.

That distinction matters. Large language models are useful for interpreting ambiguous language, but they should not scan an entire product catalog for every request. Elasticsearch or OpenSearch remains better suited for indexed retrieval, filtering, and pagination.

Define a Structured Automotive Search Intent

The first implementation step is converting free-form input into a predictable schema.

A useful intent object might contain:

{
  "raw_query": "17 honda 2015",
  "normalized_query": "2015 Honda 17 inch wheel",
  "make": "Honda",
  "model": null,
  "year": 2015,
  "wheel_diameter": 17,
  "product_type": "wheel",
  "sku": null,
  "oe_number": null,
  "confidence": 0.93
}

The schema should support the attributes your catalog can verify. For wheels and related parts, that often includes:

  • Vehicle make
  • Vehicle model
  • Vehicle year
  • Wheel diameter
  • Wheel width
  • Bolt pattern
  • Finish
  • Product type
  • SKU
  • OE number
  • Hollander or interchange number

Do not ask the AI to invent values that are not present or strongly implied. Missing information should remain null.

Create the Rails intent object

A plain Ruby value object keeps the rest of the application independent from the AI provider.

# app/models/search_intent.rb
class SearchIntent
  include ActiveModel::Model
  include ActiveModel::Attributes

  attribute :raw_query, :string
  attribute :normalized_query, :string
  attribute :make, :string
  attribute :model, :string
  attribute :year, :integer
  attribute :wheel_diameter, :integer
  attribute :product_type, :string
  attribute :sku, :string
  attribute :oe_number, :string
  attribute :confidence, :float, default: 0.0

  validates :raw_query, presence: true
  validates :confidence,
    numericality: {
      greater_than_or_equal_to: 0.0,
      less_than_or_equal_to: 1.0
    }
end

This object validates the response before the application uses it. It also prevents provider-specific response formats from spreading through controllers and search services.

Detect Exact Identifiers Before Calling an AI Model

SKU and OE number searches should usually bypass natural-language interpretation.

Examples include:

  • 560-64920
  • 202011
  • 4030ET007

These values may look meaningless to a language model, but they are highly meaningful to the catalog. An exact identifier match should receive the highest possible priority.

# app/services/search/identifier_detector.rb
module Search
  class IdentifierDetector
    SKU_PATTERN = /\A[A-Z0-9][A-Z0-9\-_.]{3,30}\z/i

    def self.call(query)
      normalized = query.to_s.strip.upcase
      return nil unless normalized.match?(SKU_PATTERN)

      {
        candidate: normalized,
        identifier_like: true
      }
    end
  end
end

The regular expression should be adapted to your real identifier formats. Avoid making it too broad. A query such as honda should not be treated as a SKU simply because it contains valid alphanumeric characters.

The search pipeline can first try an exact match against fields such as:

  • sku.keyword
  • oe_number.keyword
  • interchange_number.keyword
  • manufacturer_part_number.keyword

Only when no strong identifier match exists should the application continue to full intent extraction.

Extract and Normalize Search Intent

The intent service should return structured JSON rather than prose. Its instructions should include automotive-specific rules such as:

  • Four-digit values between supported catalog years are probably vehicle years.
  • One- or two-digit values commonly used as wheel diameters may represent inches.
  • Known makes and models should be normalized to canonical spellings.
  • Product identifiers must remain unchanged.
  • Unsupported assumptions should be returned as null.
  • Confidence should decrease when multiple interpretations are plausible.

A Rails service can isolate the provider call:

# app/services/search/intent_extractor.rb
module Search
  class IntentExtractor
    class InvalidResponse < StandardError; end

    def initialize(client:)
      @client = client
    end

    def call(query)
      payload = @client.extract_search_intent(
        query: query,
        schema: response_schema
      )

      intent = SearchIntent.new(payload.merge(raw_query: query))
      raise InvalidResponse, intent.errors.full_messages.join(", ") unless intent.valid?

      intent
    rescue StandardError => error
      Rails.logger.warn(
        event: "search_intent_extraction_failed",
        query: query,
        error: error.class.name
      )
      fallback_intent(query)
    end

    private

    def response_schema
      {
        type: "object",
        required: %w[normalized_query confidence],
        properties: {
          normalized_query: { type: "string" },
          make: { type: %w[string null] },
          model: { type: %w[string null] },
          year: { type: %w[integer null] },
          wheel_diameter: { type: %w[integer null] },
          product_type: { type: %w[string null] },
          sku: { type: %w[string null] },
          oe_number: { type: %w[string null] },
          confidence: { type: "number" }
        }
      }
    end

    def fallback_intent(query)
      SearchIntent.new(
        raw_query: query,
        normalized_query: query,
        confidence: 0.0
      )
    end
  end
end

The fallback is important. Search should continue when the AI provider is unavailable, slow, or returns invalid output. The existing keyword search remains a valid degraded mode.

Build a Structured Elasticsearch Query

Once the intent is available, use it to build a weighted search request.

# app/services/search/product_query_builder.rb
module Search
  class ProductQueryBuilder
    def call(intent)
      {
        size: 100,
        query: {
          bool: {
            must: text_queries(intent),
            should: relevance_boosts(intent),
            filter: catalog_filters(intent),
            minimum_should_match: 0
          }
        }
      }
    end

    private

    def text_queries(intent)
      [
        {
          multi_match: {
            query: intent.normalized_query,
            fields: [
              "name^5",
              "make^4",
              "model^4",
              "description",
              "keywords^2"
            ],
            fuzziness: "AUTO"
          }
        }
      ]
    end

    def relevance_boosts(intent)
      boosts = []
      boosts << term_boost("make.keyword", intent.make, 8)
      boosts << term_boost("model.keyword", intent.model, 10)
      boosts << term_boost("year", intent.year, 6)
      boosts << term_boost("wheel_diameter", intent.wheel_diameter, 8)
      boosts << term_boost("product_type.keyword", intent.product_type, 5)
      boosts << term_boost("sku.keyword", intent.sku, 30)
      boosts << term_boost("oe_number.keyword", intent.oe_number, 30)
      boosts.compact
    end

    def catalog_filters(_intent)
      [
        { term: { active: true } },
        { term: { searchable: true } }
      ]
    end

    def term_boost(field, value, boost)
      return if value.blank?

      {
        term: {
          field => {
            value: value,
            boost: boost
          }
        }
      }
    end
  end
end

This query uses the normalized text for recall while applying strong boosts to structured matches. A Honda product should therefore rank above a Toyota product when the extracted make is Honda.

Avoid using every inferred attribute as a hard filter. If the AI incorrectly interprets 18 as a wheel diameter, a hard filter could remove all otherwise relevant results. Boosting is safer for lower-confidence attributes.

Hard filters are more appropriate when:

  • The user selected a value through a filter control.
  • The value is an exact verified SKU.
  • The attribute has high confidence.
  • Showing incompatible products would create a safety or fitment risk.

Re-Rank the Candidate Products

Search engines can produce a strong candidate set, but the first-pass score may still favor popular or generic products. A separate re-ranker can compare each product with the structured intent.

A deterministic scoring model is a good starting point:

# app/services/search/product_reranker.rb
module Search
  class ProductReranker
    WEIGHTS = {
      sku: 100,
      oe_number: 100,
      make: 30,
      model: 35,
      year: 20,
      wheel_diameter: 25,
      product_type: 15
    }.freeze

    def call(products, intent)
      products
        .map { |product| [product, score(product, intent)] }
        .sort_by { |(_, score)| -score }
        .map(&:first)
    end

    private

    def score(product, intent)
      total = product.search_score.to_f
      total += WEIGHTS[:sku] if exact_match?(product.sku, intent.sku)
      total += WEIGHTS[:oe_number] if exact_match?(product.oe_number, intent.oe_number)
      total += WEIGHTS[:make] if exact_match?(product.make, intent.make)
      total += WEIGHTS[:model] if exact_match?(product.model, intent.model)
      total += WEIGHTS[:year] if product.years.include?(intent.year)
      total += WEIGHTS[:wheel_diameter] if product.wheel_diameter == intent.wheel_diameter
      total += WEIGHTS[:product_type] if exact_match?(product.product_type, intent.product_type)
      total
    end

    def exact_match?(left, right)
      return false if left.blank? || right.blank?

      left.to_s.casecmp?(right.to_s)
    end
  end
end

This method is fast, explainable, and easy to test. It also gives the business control over ranking behavior.

An AI or machine-learning re-ranker may be useful later, but only after you collect enough search and conversion data. A deterministic baseline provides a clear benchmark against which a learned model can be evaluated.

Validate Relevance and Retry Weak Searches

Re-ranking cannot fix a poor candidate set. If the search engine returns Toyota, Nissan, and universal wheels for Honda Accord wheel, the system should not simply reorder those products and present them as good matches.

Add a validation stage that checks whether the highest-ranked products satisfy the primary intent.

# app/services/search/result_validator.rb
module Search
  class ResultValidator
    MINIMUM_SCORE = 35

    def call(products, intent)
      return false if products.empty?

      top_products = products.first(5)
      top_products.any? do |product|
        relevance_score(product, intent) >= MINIMUM_SCORE
      end
    end

    private

    def relevance_score(product, intent)
      score = 0
      score += 20 if matches?(product.make, intent.make)
      score += 20 if matches?(product.model, intent.model)
      score += 15 if product.years.include?(intent.year)
      score += 15 if product.wheel_diameter == intent.wheel_diameter
      score
    end

    def matches?(left, right)
      right.present? && left.to_s.casecmp?(right.to_s)
    end
  end
end

When validation fails, retry with a controlled fallback query. Do not let the model retry indefinitely.

A practical sequence is:

  • Search with the complete structured intent.
  • Remove the least confident optional attribute.
  • Search with the normalized make, model, and product type.
  • Fall back to the original query with fuzzy matching.
  • Stop after a fixed number of attempts.

For 2015 Honda 17 inch wheel, the fallback sequence might be:

  • 2015 Honda 17 inch wheel
  • Honda 17 inch wheel
  • Honda wheel

Each retry broadens recall, but it should preserve the customer's strongest constraints for as long as possible.

Coordinate the Complete Search Pipeline

A single Rails service can coordinate detection, extraction, retrieval, re-ranking, and retry behavior.

# app/services/search/products.rb
module Search
  class Products
    MAX_ATTEMPTS = 3

    def initialize(
      intent_extractor:,
      search_client:,
      query_builder: ProductQueryBuilder.new,
      reranker: ProductReranker.new,
      validator: ResultValidator.new
    )
      @intent_extractor = intent_extractor
      @search_client = search_client
      @query_builder = query_builder
      @reranker = reranker
      @validator = validator
    end

    def call(query)
      intent = @intent_extractor.call(query)
      attempts = build_attempts(intent).first(MAX_ATTEMPTS)

      attempts.each do |attempt_intent|
        candidates = retrieve(attempt_intent)
        ranked = @reranker.call(candidates, attempt_intent)
        return result(ranked, attempt_intent) if @validator.call(ranked, attempt_intent)
      end

      result([], intent)
    end

    private

    def retrieve(intent)
      body = @query_builder.call(intent)
      @search_client.search(index: "products", body: body)
    end

    def build_attempts(intent)
      [
        intent,
        intent.dup.tap { |value| value.year = nil },
        intent.dup.tap do |value|
          value.year = nil
          value.wheel_diameter = nil
        end
      ]
    end

    def result(products, intent)
      {
        products: products,
        interpreted_query: intent.normalized_query,
        intent: intent
      }
    end
  end
end

In production, preserve the reason for each retry and include it in observability data. That information will help you identify weak catalog fields, bad extraction rules, and common query patterns.

Test Ambiguous and Misspelled Queries

Search quality cannot be verified with unit tests alone. Create a representative evaluation set containing real or carefully anonymized customer queries.

RSpec.describe Search::ProductReranker do
  subject(:reranker) { described_class.new }

  it "places a matching make above unrelated products" do
    intent = SearchIntent.new(
      raw_query: "honda wheel",
      normalized_query: "Honda wheel",
      make: "Honda",
      product_type: "wheel",
      confidence: 0.98
    )

    honda = build(:product, make: "Honda", product_type: "wheel", search_score: 2)
    toyota = build(:product, make: "Toyota", product_type: "wheel", search_score: 8)
    universal = build(:product, make: nil, product_type: "wheel", search_score: 10)

    results = reranker.call([universal, toyota, honda], intent)

    expect(results.first).to eq(honda)
  end
end

Your evaluation set should include:

  • Misspelled makes and models
  • Reversed word order
  • Wheel-size and year ambiguity
  • Exact SKU and OE number searches
  • Queries with no matching inventory
  • Generic product searches
  • Queries containing several numbers
  • Vehicle models shared across years or trims

Track metrics such as:

  • Zero-result rate
  • Percentage of searches with a relevant product in the top three
  • Click-through rate
  • Add-to-cart rate after search
  • Query reformulation rate
  • Average number of retries
  • Intent extraction latency
  • Search abandonment rate

The most useful metric is not whether the AI generated a grammatically correct interpretation. It is whether customers found and selected the correct product.

Security, Performance, and Operational Risks

An AI search layer introduces new failure modes. Design for them before rollout.

Protect customer and catalog data

Do not send unnecessary personal information, pricing rules, internal notes, or supplier data to the model. Send only the query and the minimum contextual data needed for interpretation.

Enforce structured output

Treat model output as untrusted input. Validate types, ranges, known makes, supported years, and product categories before building a search request.

Set strict timeouts

Intent extraction should have a short timeout. If it fails, continue with the original search query. Search availability should not depend entirely on an external model.

Cache repeated interpretations

Common queries such as honda accord wheel can be cached by normalized query. Use a bounded expiration period so catalog and interpretation changes can take effect.

Avoid per-product model calls

Do not call a language model once for every candidate result. Retrieve a limited candidate set and use deterministic scoring, a batch re-ranker, or a dedicated ranking model.

Keep explanations for ranking decisions

Store the matched signals that produced a score. For example:

{
  "product_id": 4812,
  "score": 92,
  "signals": [
    "make_exact_match",
    "model_exact_match",
    "wheel_diameter_match"
  ]
}

Explainable scores make debugging and merchandising review much easier.

Roll Out the System Incrementally

A safe rollout starts in observation mode.

  • Run intent extraction without changing customer-visible results.
  • Compare the interpreted query with the original query.
  • Record how the proposed re-ranking would change the top results.
  • Review low-confidence and high-impact cases.
  • Enable re-ranking for a small traffic percentage.
  • Compare search and conversion metrics against a control group.
  • Expand only when the new pipeline produces measurable improvements.

This approach prevents an incorrect interpretation rule from affecting the entire catalog at once.

Conclusion

An effective automotive search system must understand more than keywords. It must distinguish years from wheel sizes, correct misspelled vehicle names, prioritize exact product identifiers, recognize fitment intent, and place the most relevant products at the top.

The strongest architecture combines several specialized components. An AI layer interprets ambiguous input, the existing search engine retrieves candidates, deterministic or learned scoring re-ranks those candidates, and a validation stage retries searches when the result set is weak.

For a Rails application, keep these responsibilities in separate service objects and preserve a non-AI fallback path. Start with explainable ranking rules, measure actual customer behavior, and introduce more advanced models only when your data shows where they can improve the system. Getting this architecture right is a software development discipline in its own right, and teams building or scaling this kind of system often benefit from partnering with a dedicated Ruby on Rails expert who can keep the intent layer, search engine, and re-ranking logic cleanly decoupled as the catalog grows.

Amrendra Pratap Singh

Related articles

Our two bytes give the latest technology trends and information that gives you fair information about the subject.

How Software Development Services Help Companies Scale Faster

How Software Development Services Help Companies Scale Faster

In today's competitive digital economy, businesses need technology that can support growth, improve efficiency, and adapt to changing market demand...
Understanding Rails link_to Method with Practical Examples

Understanding Rails link_to Method with Practical Examples

Navigation is a fundamental part of every web application. Whether users are browsing products, reading blog posts, accessing dashboards, or managi...
Benefits of Using Ruby on Rails for Cloud-Native Applications

Benefits of Using Ruby on Rails for Cloud-Native Applications

In today's digital landscape, businesses are rapidly shifting toward cloud-native application development to achieve greater scalability, flexibili...

Cookie Preferences

We use cookies to deliver the best possible experience on our website. To learn more, visit our Privacy Policy. Please accept the cookies for optimal performance.Cookie Notice.