# I upgraded my AI concierge by turning it to a plain search box

> Source: <https://dev.to/tomer_liran/i-upgraded-my-ai-concierge-by-turning-it-to-a-plain-search-box-2nma>
> Published: 2026-06-29 09:10:19+00:00

I wanted [Beaches of Greece](https://beachesofgreece.com) to be an AI concierge. You would describe your perfect beach day in plain language, and it would just understand you.

Under the hood that meant a proper NLP stack: `wink-nlp`

for entities, compromise for parsing, a sentiment analyzer, fuzzy matching for place names. It felt smart.

It mostly produced confident nonsense. So I deleted it and shipped a plain search box, and search got faster, smaller, and genuinely better. Here is what went wrong.

The failures were not subtle:

It scanned keywords as substrings, so "shade" matched inside "shaded", and "bar" (as in beach bar) matched inside "sandbar".

Place lookup did `places.find(p => query.includes(p))`

, so "ios" inside "agios nikolaos" resolved to the island of Ios. Wrong island, zero results.

"near heraklion" returned nothing. It made Heraklion (a city) a location filter, but beaches are filed under their island, Crete, so the filter could never match.

Sentiment analysis scored "I hate crowds but love calm water" as mixed polarity. A fun trick that never changed which beaches to show.

Every time, the stack performed the look of understanding, then did something a human never would. And when it was wrong, I had to debug a model's guess.

They list wants: calm, sandy, shade, parking, near somewhere. The vocabulary is small and knowable, so I matched against it literally, on whole-word boundaries:

```
function hasPhrase(q: string, phrase: string): boolean {
  // matches "bar" but not "sandbar"
  return new RegExp(`(?:^|[^a-z0-9])${escapeRegExp(phrase)}(?:[^a-z0-9]|$)`).test(q);
}
```

That single boundary check erased the whole "bar inside sandbar" class of bug. The rest of the vocabulary is just data: lists of phrases mapped to filter values.

Boring matching also forces decisions the NLP layer used to hide. "quiet" now means few people, not flat water. "no parking" is checked before "parking" so it wins outright. Each one is a product call sitting in plain sight instead of inside a model.

Place names get the same treatment: whole-word, longest match, and every name resolves to an area that beaches actually live in, so "heraklion" maps to Crete instead of nuking the results. The one bit of fuzziness I kept is a tight typo fallback. And because each town carries a coordinate, "near heraklion" can now rank by real distance, which the AI version never managed.

Gained: predictable and debuggable (a wrong result is a phrase to fix, not a model to retrain), faster, a smaller bundle, and honest. It does what a human would do with those words, no theater.

Gave up: novel phrasing. Someone on Reddit instantly tried a mixed Greek-and-English query and it caught only half. Fair trade for this domain.

"Add AI" is not free. It costs latency, bundle size, and worst of all legibility: when the clever thing is wrong, you cannot tell why. For a bounded problem with a knowable vocabulary, a search box that makes no claim to intelligence can be the smarter build.

It is live at [beachesofgreece.com](https://beachesofgreece.com). Throw a weird query at it and tell me what breaks.
