Search looks simple until you watch real users use it.
At Pinjoy, we initially thought about search in fairly conventional terms: users enter a restaurant name, a food item, or something they want to find, and the system returns relevant businesses.
That model worked.
Until users started using search for something much broader.
They weren't always searching for a name.
They weren't always searching for a menu item.
And they weren't always thinking in terms of filters.
They were describing what they wanted.
“A quiet place for a romantic dinner with good seafood.”
“A nice brunch spot in Casablanca with outdoor seating.”
“Somewhere to go with friends tonight, not too expensive.”
These queries aren't really keywords.
They're expressions of intent.
That forced us to rethink what "search" meant inside Pinjoy.
We ended up evolving the system through three phases:
Searching for explicit things
Understanding what the user is actually asking for
Combining natural-language intent with structured search and retrieval
The interesting part wasn't simply introducing AI.
The interesting part was deciding where AI belongs in a search architecture.
Phase 1: Search What the User Explicitly Typed
The first version of search was straightforward.
A user could search for:
a restaurant name
a food
a menu item
a description
a cuisine
other textual information associated with a business
The underlying problem was essentially lexical retrieval.
If someone searched for:
“pizza”
we wanted restaurants containing relevant occurrences of "pizza" in their searchable data to appear.
If someone searched:
“La Sqala”
we wanted the restaurant matching that name.
If someone searched:
“tacos”
we wanted restaurants whose menus or descriptions contained relevant taco-related content.
At this stage, search was mostly about matching what was written.
That distinction matters.
A lexical search system answers something close to:
"Where does this query appear in the data?"
It doesn't necessarily answer:
"Which businesses best satisfy what this person wants?"
For simple queries, those two questions are almost identical.
For natural language, they aren't.
Phase 2: Users Started Searching for Intent
As Pinjoy evolved, we noticed something important in how people searched.
We already had dedicated UI for structured discovery.
Users could select a city.
They could use filters.
They could navigate through categories.
In other words, the product already had a way to express structured constraints.
But users didn't always use it.
Instead, they would put several pieces of information into the search box.
For example:
“Best seafood restaurants in Casablanca”
There are at least three concepts here:
restaurant
seafood
Casablanca
A traditional search engine can treat this as a collection of terms.
But the user isn't really asking:
"Find documents containing these three strings."
They're expressing a structured request using natural language.
This led us to a broader question:
Should the search box behave like a keyword field, or should it understand the request?
That is where Phase 2 started.
Phase 2: Search Became a Query Understanding Problem
Once a query contains multiple concepts, simply matching text starts becoming insufficient.
Consider:
“A romantic restaurant in Casablanca for a date night.”
There may be no restaurant whose database record literally contains:
“romantic restaurant for a date night”
But there may be restaurants with:
strong descriptions suggesting a romantic atmosphere
relevant reviews
dinner-oriented menus
appropriate cuisine
location in Casablanca
signals associated with date-night dining
The search problem has changed.
We're no longer only retrieving text.
We're trying to map a natural-language request onto a structured representation of businesses.
This is where LLMs and semantic retrieval become useful.
But there is an important architectural distinction.
There are two fundamentally different ways we could approach this.
Phase 3: Two Architectures for Natural-Language Search
Once we decided that users should be able to describe what they want, we considered two approaches.
Approach 1: Natural Language → Structured Filters
The first approach was to treat the LLM as a query parser.
The user writes:
“Moroccan restaurant in Rabat with outdoor seating.”
The language model analyzes the query and extracts something like:
location = Rabat
cuisine = Moroccan
feature = outdoor seating
category = restaurant
The search system then translates those entities into the existing filter model.
This architecture is commonly described as:
Natural Language Querying (NLQ)
or:
Query Translation / Parsing
or:
Entity Extraction + Faceted Search
Conceptually, the architecture looks like:
User Query
↓
Natural Language Understanding
↓
Intent / Entity Extraction
↓
Structured Filters
↓
Database / Search Index
↓
Filtered Results
This approach has a major advantage:
predictability.
If the user says:
“Restaurants in Casablanca”
and the model extracts:
city = Casablanca
category = restaurant
the system can apply those constraints directly.
The result is easy to reason about.
It also integrates naturally with an existing filter architecture.
But there is a limitation.
The model can only express the user's request through the vocabulary and dimensions that our filtering system understands.
Imagine the user asks:
“I want somewhere cozy and quiet for a first date, with good seafood.”
Now we have a much harder problem.
What exactly is:
cozy?
What exactly is:
quiet?
What exactly is:
good for a first date?
If those concepts don't exist as structured attributes, the LLM has nowhere reliable to put them.
We could keep expanding the filter schema.
But that creates another problem.
The more concepts we try to represent as categorical filters, the more complex the search model becomes.
Eventually, we would be building a giant taxonomy of human preferences.
And users don't naturally think in that taxonomy.
Approach 2: Hybrid Search
The second approach was fundamentally different.
Instead of forcing the entire query into structured filters, we allow the search engine to retrieve results using multiple signals.
The core idea is:
combine lexical relevance with semantic relevance.
The lexical side answers:
"Does this business contain terms relevant to the query?"
The semantic side answers:
"Does this business mean something similar to what the user is asking for?"
This is commonly called:
Hybrid Search.
At a high level:
┌── Lexical Search
│ BM25 / keyword relevance
│
User Query ──────────────┤
│
└── Semantic Search
Embeddings / vector similarity
↓
Result Combination
↓
Re-ranking
↓
Results
This changes the role of the search query.
We don't have to turn:
“A cozy seafood restaurant for a romantic dinner”
into a rigid set of categories.
Instead, the search system can use the entire meaning of the request as a retrieval signal.
Why We Chose Hybrid Search
We chose the hybrid approach because of one architectural fact:
Pinjoy already had structured filters.
We didn't need natural language to replace them.
We needed natural language to complement them.
That distinction became central to the design.
There are things a structured filter is extremely good at.
For example:
Casablanca
should be treated as an actual location constraint.
If the user explicitly asks for Casablanca, we don't want semantic similarity to decide that a restaurant in Rabat is "close enough."
Similarly, some attributes are naturally categorical.
If the user explicitly selects:
Italian
or:
Vegetarian
or another supported filter, those constraints should remain deterministic.
But other parts of a query are inherently fuzzy.
Consider:
“A nice place for a relaxed dinner with friends.”
There isn't necessarily a clean database field for "relaxed."
Trying to turn every adjective into a filter would make the system increasingly rigid.
So we separated two concepts:
Hard constraints
Things the user explicitly specifies that the system can represent reliably.
Examples:
city
category
cuisine
price-related filters
other supported structured attributes
Relevance signals
Things that help determine how well a business matches the user's overall intent.
Examples:
atmosphere
descriptions
menu context
semantic similarity
review language
conceptual relationships between the query and business content
This gives us a much more flexible architecture.
The Search Architecture We Were Actually Building
The important thing is that hybrid search isn't simply:
keyword search + vector database
That would be an oversimplification.
The real problem is designing the retrieval pipeline.
A useful mental model is:
USER QUERY
│
▼
Query Understanding
│
┌───────────┴───────────┐
│ │
▼ ▼
Structured Signals Search Query
│ │
│ ┌────────┴────────┐
│ │ │
│ ▼ ▼
│ Lexical Search Semantic Search
│ │ │
│ └────────┬────────┘
│ │
▼ ▼
Hard Filters Candidate Results
│ │
└───────────┬───────────┘
▼
Result Fusion
│
▼
Ranking
│
▼
Final Results
The exact implementation can evolve, but the architectural principle remains the same:
Don't force every piece of information through the same retrieval mechanism.
Different signals are good at different things.
Why Not Just Use Semantic Search?
This was another important design consideration.
Once you introduce embeddings, it is tempting to think:
"Why do we still need keyword search?"
Because semantic similarity has its own weaknesses.
Imagine someone searches for a specific restaurant:
“Dar Naji”
A semantic system may understand the general concept of a restaurant, but that doesn't mean semantic similarity is the best mechanism for identifying the exact named entity.
Exact names, menu items, brands, neighborhoods, and uncommon terms often benefit from lexical matching.
Keyword search is also much easier to reason about for exact textual relevance.
Semantic search is powerful when the user expresses a concept differently from how the data is written.
For example:
“somewhere peaceful”
may match a description containing:
“quiet atmosphere”
even though the words aren't identical.
That is exactly where semantic retrieval adds value.
So instead of choosing between lexical and semantic search, we use both.
Lexical Search and Semantic Search Solve Different Problems
This is one of the most important lessons from the architecture.
Lexical search is strong at:
exact terms
names
menu items
uncommon words
explicit textual matches
precision around specific vocabulary
Semantic search is strong at:
paraphrases
concepts
intent
contextual similarity
natural-language descriptions
cases where the query and document use different wording
Neither mechanism completely replaces the other.
The goal is not:
"Make search semantic."
The goal is:
Use the right retrieval signal for the right part of the user's request.
Candidate Retrieval vs Final Ranking
Another architectural distinction matters here.
Search doesn't necessarily need to decide the final order in a single operation.
A modern retrieval architecture can be thought of as two stages.
Stage 1: Candidate Generation
Retrieve a reasonably broad set of potentially relevant businesses.
Lexical search can produce one candidate set.
Semantic search can produce another.
Structured filters can reduce the search space.
The objective here is recall.
We want the correct businesses to make it into the candidate pool.
Stage 2: Ranking
Once we have candidates, we can determine their relative relevance.
This is where multiple signals can be combined.
For example:
Final relevance
=
lexical relevance
+
semantic relevance
+
structured relevance
+
business-specific signals
The exact scoring strategy is an implementation detail.
The architectural idea is more important:
retrieval finds possibilities; ranking determines relevance.
This separation gives us much more flexibility than trying to solve everything inside one database query.
Result Fusion
If lexical and semantic retrieval produce separate ranked lists, we need a way to combine them.
One common approach is Reciprocal Rank Fusion (RRF).
The important idea behind RRF is simple:
A document that consistently appears near the top across multiple retrieval systems should receive a strong combined ranking.
For example:
Lexical Search
1. Restaurant A
2. Restaurant C
3. Restaurant B
Semantic Search
1. Restaurant B
2. Restaurant A
3. Restaurant D
Instead of asking which search system is "correct", we can combine their rankings.
Now:
Restaurant A is strong in both
Restaurant B is strong semantically and appears lexically
Restaurant C has lexical evidence
Restaurant D has semantic evidence
The result is a ranking that incorporates multiple perspectives on relevance.
This is especially useful because lexical and semantic search fail in different ways.
Structured Filters Still Matter
One of the biggest mistakes in AI search is assuming that natural language should replace every existing search interaction.
We didn't want that.
Pinjoy already had explicit filters.
Those filters provide something natural-language retrieval cannot guarantee:
deterministic constraints.
If a user selects a specific city, that selection has a different semantic meaning from a vector similarity score.
A user choosing:
Casablanca
is not expressing a vague preference.
They're specifying a constraint.
That means our architecture should respect the difference between:
"I prefer something like this."
and:
"It must be this."
Semantic relevance is appropriate for the first.
Structured filtering is appropriate for the second.
The Search Box Becomes Another Interface
This led to an important product insight.
We didn't actually need to choose between:
filters
and:
natural language.
They are two interfaces over the same discovery system.
Some users prefer structured exploration.
They might think:
Casablanca → Seafood → Restaurant
Other users think:
“I want a seafood restaurant in Casablanca for a quiet dinner.”
Both users are expressing essentially the same underlying intent.
The difference is the interface.
That means natural-language search doesn't have to replace the filter UI.
It can sit alongside it.
And eventually, the two can interact.
A natural-language query can provide context.
Explicit filters can provide constraints.
The retrieval system can combine both.
The Data Model Becomes Part of Search Quality
Another lesson from this architecture is that search quality isn't only a search-engine problem.
It is also a data-modeling problem.
Semantic retrieval is only as useful as the information being retrieved.
If a restaurant has:
poor descriptions
incomplete menus
missing categories
weak metadata
badly structured content
then a sophisticated retrieval system cannot magically recover all of the missing information.
This is why search architecture has to consider the entire data pipeline.
The searchable representation of a restaurant can include multiple sources of information:
Restaurant
├── Name
├── Description
├── Category
├── Cuisine
├── Menu
├── Menu Items
├── Attributes
├── Reviews
├── Location
└── Other metadata
Each source carries a different kind of signal.
A restaurant name is highly useful for entity matching.
A menu is useful for food discovery.
A description can capture atmosphere and positioning.
Reviews can contain natural descriptions of experiences.
Structured attributes provide deterministic filtering.
The search system therefore isn't really searching "restaurants."
It is searching a representation of restaurants.
That distinction becomes increasingly important as the system becomes more intelligent.
Why This Architecture Scales Better Than "LLM Search"
Another reason we preferred hybrid retrieval is that it keeps the LLM out of places where deterministic systems are better.
We don't want the LLM to become the database.
We don't want every search query to become:
User
↓
LLM
↓
Huge prompt containing businesses
↓
LLM decides results
That architecture may look impressive in a demo, but it creates difficult problems around:
latency
cost
consistency
observability
explainability
scalability
freshness
deterministic constraints
Instead, the LLM or embedding model becomes one component of a larger retrieval architecture.
That's a much more scalable mental model.
AI should enhance retrieval, not replace the retrieval system.
Search Is Also a Latency Problem
A search feature isn't successful simply because the results are intelligent.
It also needs to feel immediate.
Every additional stage in the pipeline introduces latency.
A naïve architecture could potentially look like:
Query
↓
LLM
↓
Entity extraction
↓
Embedding generation
↓
Vector search
↓
Keyword search
↓
LLM re-ranking
↓
Results
That can quickly become expensive and slow.
So the architecture needs to distinguish between operations that are:
query-time
and operations that can be performed:
ahead of time.
For example, embeddings for relatively stable restaurant content can be generated during ingestion or indexing rather than every time a user searches.
Then the query only needs the representation necessary for the current request.
This is one of the broader principles of production AI systems:
Move expensive computation out of the critical request path whenever the data allows it.
Search Quality Is Not Just "Did the AI Understand Me?"
Once we moved toward intent-based search, another question became unavoidable:
How do we know whether the new system is actually better?
A search engine can feel impressive in isolated examples and still perform badly at scale.
We therefore need to think about search evaluation in terms of retrieval quality.
At a high level, there are two important concepts:
Recall
Did we retrieve the relevant businesses at all?
If the correct restaurant never enters the candidate set, no ranking algorithm can recover it.
Precision
Among the results we returned, how many are actually relevant?
A system can retrieve many potentially relevant restaurants but still produce poor results if ranking is weak.
This creates a fundamental trade-off.
Candidate generation should generally prioritize enough recall to avoid missing good results.
Ranking can then focus more heavily on precision.
Search Should Be Designed Around Real Queries
This is probably the most important operational lesson.
You can't design a good natural-language search system entirely from architecture diagrams.
You need to observe what users actually ask.
The query distribution matters.
Users will produce:
short keywords
restaurant names
menu items
misspellings
combinations of filters
full sentences
vague preferences
local expressions
mixed languages
queries containing both hard constraints and soft preferences
The system should be designed around those actual behaviors.
For Pinjoy, that observation was what pushed us beyond the original keyword-search model.
The users were already telling us what the search box should become.
We just had to build the architecture capable of understanding it.
The Bigger Architectural Lesson
Looking back at the three phases, the evolution wasn't really:
Keyword search → AI search.
It was more subtle.
It was:
Text matching → Query understanding → Multi-signal retrieval
Phase 1 asked:
"Does the data contain what the user typed?"
Phase 2 asked:
"What is the user actually looking for?"
Phase 3 asks:
"How do we retrieve and rank the best results using every useful signal available to us?"
That last question is much closer to how I think about production AI systems.
AI doesn't need to own the entire architecture.
It needs to be placed where it provides the most leverage.
For Pinjoy, that means:
structured filters for explicit constraints
lexical retrieval for precise textual matching
semantic retrieval for meaning and intent
result fusion for combining retrieval signals
ranking for final relevance
data modeling and indexing for retrieval quality
evaluation for measuring whether the system actually improves
The result isn't an "AI search box."
It's a search architecture where natural language becomes a first-class way of interacting with the system.
And that's a much more interesting engineering problem.
The Direction We're Taking
The long-term goal isn't to make users learn how Pinjoy's database is structured.
It is the opposite.
The user should be able to describe what they're looking for naturally.
The system should determine which parts of that request are:
hard constraints
and which parts are:
relevance signals.
Then the retrieval architecture should use the appropriate mechanism for each.
That is the direction we're taking with Pinjoy search.
Not replacing structured search with AI.
Not replacing keyword search with vectors.
But building a system where all of them work together.
Because the future of search probably isn't:
Keyword OR AI.
It's:
Structured constraints + lexical relevance + semantic understanding + intelligent ranking.
And the hardest part isn't choosing an embedding model.
It's designing the system so that every signal has a clear rol