Why Keyword Search Fails for Food Delivery

When a customer types 'cold coffee', keyword search can't find 'Iced Americano'. Food delivery has a discovery problem that string matching will never solve.

By Aditya Patni

Your search box is a black hole for orders

A customer types cold coffee into your app. You have an Iced Americano, a Frappe, and a Cold Brew across three restaurants. Keyword search returns none of them. Zero keyword overlap, zero results, lost order.

This is the default experience on most food delivery platforms. It happens on every query that uses a synonym, a transliteration, or a language the index wasn't built for. Someone searching "Murgh Makhani" won't find "Butter Chicken (Boneless)" even though they're the same dish. Someone searching "mac and cheese" won't find "Penne al Formaggio." Someone typing "biryani" in a tier-3 Indian city will miss half the listings because they were uploaded as photographs, not text, and the OCR garbled the name.

Four ways food breaks keyword search

  • Transliteration chaos. Kadhai Chicken, Karahi Chicken, Kadai Chicken, Karai Chiken. All valid spellings. None are typos a spellchecker can fix. Korean has Bibimbap vs. Bi Bim Bap. Japanese has "Tonkotsu" (pork bone broth) and "Tonkatsu" (fried pork cutlet), one character apart, completely different dishes. A keyword index treats each variant as a separate entity.
  • Cross-lingual synonyms. "Prawns" and "Shrimp" are the same protein. "Capsicum" in India is "Bell Pepper" in the US. "Aubergine" in London is "Eggplant" in New York. Cottage Cheese Tikka and Paneer Tikka are the same dish. None of these connections exist in a token index.
  • Intent queries. Real users search "something light for dinner," "cheap filling meal," "kid-friendly pasta." These carry clear meaning. Keyword search can't do anything with "light" or "filling" because those words don't appear in menu item titles.
  • Promotional noise. What should be "Chicken Biryani" arrives from the POS as BESTSELLER Chicken Dum Biryani (Serves 2-3) Extra Raita Free!!! Now a search for "biryani" competes with matches on "bestseller" and "serves" across thousands of listings.

Synonym dictionaries don't scale. I've watched teams try.

The honest reality: your competitor here isn't bare keyword search. It's keyword search plus Elasticsearch fuzzy matching, plus hand-maintained synonym dictionaries, plus a team of 50-200 catalog operations people manually mapping aliases. That stack handles the top 100 queries well. Maybe the top 500.

It falls apart on the long tail. There are tens of thousands of distinct dish concepts across world cuisines, each with its own transliteration tree. Add modifiers (sizes, add-ons, combos, customizations like "no onion" or "extra cheese"), and the combinatorial space explodes. When 40% of queries in food delivery are generic terms like "biryani" or "pizza," the synonym table gives you nothing because the problem is ranking, not recall.

General-purpose embedding models are the other common attempt. They can tell that "dog" and "puppy" are related. But they can't reliably distinguish "Classic Chicken Burger" from "Classic Veggie Burger" because the embedding space overweights the shared tokens. They put "Kadhai" and "Karahi" far apart in vector space. Cross-script matching, "बटर चिकन" to "Butter Chicken," barely works. Food language is short, dense with domain meaning, and packed with conventions that only make sense if you know the cuisine. General models have shallow coverage of this corner of language.

Semantic search that actually understands food

This is the problem we built Latimal to solve. The POST /search endpoint takes a natural-language query and a set of menu items, returns ranked results with relevance scores, and handles all the failure modes above: transliterations across scripts, ingredient synonyms, intent queries, promotional noise stripping.

Here's what it looks like in practice:

import requests

resp = requests.post("https://dish-embed.latimal.com/search",
    headers={"X-API-Key": "YOUR_KEY", "Content-Type": "application/json"},
    json={
        "query": "cold coffee",
        "corpus": [
            "Iced Americano",
            "Hot Chocolate",
            "BESTSELLER Frappe (Mocha) [Buy 1 Get 1]",
            "Masala Chai",
            "Cold Brew (Tall)",
            "Mango Lassi",
        ],
        "top_k": 3,
    },
)

for r in resp.json()["results"]:
    print(f"{r['text']}  score={r['score']:.2f}")

Returns: Iced Americano, Cold Brew (Tall), and the Frappe buried under promotional noise. All three are conceptual matches for "cold coffee." None share a keyword with the query. The model reads through the "BESTSELLER ... [Buy 1 Get 1]" cruft and finds the Frappe underneath.

Swap the query for "something sweet" and it surfaces desserts. Try "healthy lunch" and it returns salads and grain bowls. Try the Hindi query "तीखा" (spicy) against English listings and it still ranks Andhra Chicken Fry and Szechuan Noodles at the top. Try it yourself on the live search playground.

Semantic search is one layer, not the whole stack

I want to be honest about what this does and what it doesn't. A production food search system has multiple layers: keyword retrieval for exact matches (someone types "McDonald's" and wants McDonald's), semantic ranking for conceptual matches, business rules for availability and delivery radius, filters for dietary restrictions, and real-time signals like restaurant hours and estimated delivery time.

Semantic search replaces the ranking layer. You still need your availability filters, your geo-fencing, your business logic. What changes is that the set of candidates being filtered is actually relevant to what the customer wanted. If your platform currently runs Elasticsearch with a synonym dictionary, the Latimal API slots in as a reranker over your existing candidate set.

Two integration paths. For small catalogs (under a few hundred items per restaurant), send the corpus with each search request. For large catalogs, pre-compute embeddings once via POST /embed, store the vectors, and pass them alongside the corpus at query time to skip the encoding step. With pre-computed embeddings, query latency sits comfortably under 100ms at p99, which is fast enough for typeahead.

The integration guide covers both paths with full code, including embedding caching and incremental updates when menus change.

How to measure if this matters for your platform

I'm not going to throw a made-up revenue number at you. Your search failure rate depends on your catalog size, your customer base, and what languages your menus are in. Here's how to figure out your own number.

Pull your search logs for the last 30 days. Count the queries that returned zero results or where the user didn't tap any result in the first page. That's your failure rate. Multiply by your average order value. That's the ceiling on what you're losing. The real number is lower (some users will browse instead of searching, some would have abandoned anyway), but the ceiling tells you whether this is a $10K/month problem or a $10M/month problem for your platform.

The failure rate is typically highest in markets with multilingual menus, high transliteration variance, and large catalog diversity. India, Southeast Asia, the Middle East. But it shows up in Western markets too, especially in cities with dense immigrant food scenes where a single neighborhood has menus in English, Mandarin, Spanish, Korean, and Arabic.

The thing nobody talks about

There's a deeper problem here that the industry hasn't confronted. As more restaurant menus arrive as photographs (a large share in smaller Indian cities, growing everywhere as restaurants skip POS systems entirely), the text-search assumption breaks before you even get to the synonym problem. You can't keyword-search an image. You can't fuzzy-match an image. You need to extract meaning from the image and map it to a query, which is a semantic operation whether you call it that or not.