DEV Community

Cover image for Why I Added an LLM Parser on Top of Vector Search (And What It Changed)
Rafał Groń
Rafał Groń

Posted on

Why I Added an LLM Parser on Top of Vector Search (And What It Changed)

I thought vector search was enough.

I'd built Queryra — an AI search plugin for WooCommerce and Shopify. Replaced keyword matching with semantic embeddings. Customers could search "something warm for winter" and find sweaters, fleece jackets, blankets. Zero results became rare. It worked.

Then someone searched: "wireless headphones under $80, not Beats"

The vector search returned wireless headphones. Some were $200. Several were Beats. The price cap and brand exclusion were completely invisible to the embedding model.

That's when I realized: vector search was layer one. I was missing layer two.


The Problem With Pure Vector Search

Embeddings are brilliant at one thing: encoding semantic similarity. "Sneakers" lands close to "trainers" and "running shoes" in vector space. "Gift for dad" finds garden tools, BBQ sets, and watches — even without those words in the query.

But a query like "laptop under $1000 for video editing, not Chromebook" contains two fundamentally different types of information:

  1. Semantic intent — what the customer wants (a powerful laptop for video work)
  2. Structural constraints — how to filter results (price cap, category exclusion)

Embeddings handle #1 well. They have no mechanism for #2.

You can't encode "under $1000" as a direction in vector space. "Not Chromebook" isn't a semantic concept — it's an instruction to the search system. Every vector-only implementation has this blind spot, and it gets worse as queries get more specific.

The customers most affected? Highest-intent buyers. The ones ready to purchase right now.


The Solution: LLM Parser as Layer Two

I added a query parser that runs before the vector search. Its job: decompose the query into structured components.

Here's the logic (simplified):

Input: "organic shampoo without sulfates under $25, best rated"

Parser output:
{
  "semantic_query": "organic shampoo",
  "price_max": 25,
  "attribute_exclude": ["sulfates"],
  "sort_by": "rating"
}
Enter fullscreen mode Exit fullscreen mode

Each component then goes to the right system:

  • semantic_query → vector search (finds semantically relevant products)
  • price_max → database filter (hard cut at $25)
  • attribute_exclude → post-filter (removes sulfate-containing products)
  • sort_by → result reranking (surfaces highest-rated first)

The vector layer finds what the customer means. The parser layer applies what they said.


The Bypass Problem (Latency)

The parser adds ~700–800ms latency. For a simple query like "blue t-shirt", that's pure overhead — embeddings handle it fine alone.

So I added a pre-filter that routes queries before hitting the parser:

def should_parse(query: str) -> bool:
    # Price signals
    if re.search(r'under \$|below \$|\$\d+|budget|cheap|premium', query, re.I):
        return True
    # Exclusion signals  
    if re.search(