Users type sentences, not filters. Cheap rules take every fact they can, an LLM gets only the words they missed, and whatever is still not a fact decides the order.
Example domain: a real-estate search. The pattern works for any catalog: products, jobs, cars, docs. Code: github.com/Amrabrazek/homescout-ai
messy queries read fully right: rules only, then rules + LLM.
of the top 10 homes match what the words ask for: random order, then meaning ranking.
median LLM call. A cached phrase skips it entirely.
From the project's search eval: 35 messy queries (slang, Arabic, landmarks, negation) and 16 lifestyle queries over 2,591 sample homes.
Caveat: in the homepage search, a home with no keyword hit and a meaning score past a cutoff is dropped, not just ranked lower. The chat assistant only ranks.
Each search sends two things in one request. The filters are the yes / no gate. The search text is matched two ways at once: keyword (the exact words appear in the listing's name, unit number, project, area, city, developer, amenities or description) and vector (the meaning of the text is compared with each listing's stored embedding). Only the search text is embedded, never the whole query, so "20M" can't blur the meaning.
| Buyer types | Filters (gate) | Search text (keyword + vector) |
|---|---|---|
| 3br villa in new cairo under 20M, quiet street | area = New Cairo · villa · rooms = 3 · price ≤ 20M | quiet street |
| 3br villa in tagamo3 under 20M, quiet street | villa · rooms = 3 · price ≤ 20M · area = New Cairo | quiet street |
| apartment, not on the ground floor, no pool, near a park | apartment · floor ≥ 1 · no Swimming Pool | park |
| شقة بدون حمام سباحة في التجمع | apartment · area = New Cairo · no Swimming Pool | (empty: newest first) |
| chalet in north coast | city = North Coast · chalet | (empty: no ranking, newest first) |
Purple = set by the LLM.
Send "no pool" as search text and both halves rank pool homes first: the words match, and the meaning sits right next to "pool". So a "no" never reaches the search text.
| Choice | Why | Cost |
|---|---|---|
| Rules before the LLM | Most queries are simple. No latency, no bill. | Two parsers to maintain. |
| Numbers as filters, not vectors | Embeddings blur "under 5M" and "over 5M". | A wrong filter hides good results. |
| A "no" only on what we store | Only a field can enforce a "no". Rules catch the clear cases; the LLM reads the rest. | A feature we don't store ("no water feature") is only left out of the search, never hidden. "Without pool and gym" excludes only the pool. |
| LLM returns fixed JSON | Every value is checked. If it fails, use the rules' answer. | It can only fill fields you defined. |
| LLM sees only the leftover words | The same phrase gets the same answer, so it can be cached. | Less context. 34/35 test queries pass; the miss is a size ("250m2+") that gets dropped. |
| Cache by leftover, not by query | Whole sentences rarely repeat, but "near AUC" does. A hit skips a 2.0 s call. | A wrong but valid answer would be reused for 30 days. So every LLM guess shows as a removable chip, and removing it also clears the cached answer. |
| Keyword + vector on the same words | Keyword finds the exact word ("Mivida", "park"). Vector finds the same idea in other words ("calm area" for "quiet street"). Each covers the other's blind spot. | Vector needs a closeness cutoff. On the homepage, homes past it are dropped, not ranked lower (see the caveat above). |
Vectors are for meaning. Prices, sizes and dates belong in filters.
Search engines and embeddings ignore negation. Turn it into a filter, or keep it out of the search text.
Let code handle the easy cases. Call the model only for what code can't read.
The LLM will sometimes be wrong. Make its guess visible and one tap to undo, and let the undo fix the cache too.
Clean test queries all passed. Real phrasing found most of the bugs, including a "no swimming pool" that ranked pool homes first.
From HomeScout, a portfolio project: NestJS, Postgres + pgvector, Typesense, Claude. Code and evals: github.com/Amrabrazek/homescout-ai