Minthee
Topic 09•Engine•5 min read

Multi-Entity Search: Autocomplete Suggester & Search Scorer

Master fast offline search across dishes, tags, ingredients, and categories with word-position ranking and instant autocomplete.

When your recipe collection grows to dozens or hundreds of family meals, finding the exact recipe you need should take seconds, not minutes. Whether you are standing in front of your refrigerator with three random ingredients or looking for quick dinner ideas under a specific tag, search is your primary navigation tool.

Minthee features a 100% offline-first, multi-entity search engine powered by two specialized domain components:

  1. SearchAutocompleteSuggester: Dynamically analyzes your entire database as you type to surface instant, context-aware suggestions across dishes, ingredients, tags, and categories.
  2. SearchScorer: Calculates multi-field relevance scores using word-position decay, field weighting, household rating boosts, and cooking recency bonuses.

1. Quick Operator Syntax

The search bar supports intuitive prefix operators for precise scoping, alongside normal free-text typing:

Operator Syntax Example Purpose
Free Text query Chicken Soup Searches across dish titles, tags, ingredients, and categories simultaneously using Boolean AND logic.
Tag Filter #tag or tag: #Comfort Food Constrains matching strictly to assigned recipe tags.
Ingredient Filter @ingredient, in:, ing: @carrots Constrains matching strictly to ingredients contained in the recipe.
Exclusion Filter -exclude -mushrooms Strictly eliminates any dish containing that keyword from its title, tags, ingredients, or category.

All queries feature Unicode NFD diacritic stripping, meaning queries like spatzle automatically match Spätzle, and creme matches Crème Brûlée.


2. Autocomplete Suggester (SearchAutocompleteSuggester)

As you type each character into the search field, the SearchAutocompleteSuggester inspects every entity in your local database in real time.

┌─────────────────────────────────────────────────────────────┐
│                       User Types                            │
│           "chi"  /  "#chi"  /  "@chi"  /  "Dinner"          │
└──────────────────────────────┬──────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────┐
│                 SearchAutocompleteSuggester                 │
│  - Detects Scoping: Free Text vs #Tag vs @Ingredient        │
│  - Evaluates Word Position: 1st word > 2nd > 3rd > 4th+     │
│  - Applies Category Prioritization Rule                     │
│  - Preserves Multi-Token Prefixes ("#Vegan Chi")            │
└──────────────────────────────┬──────────────────────────────┘
                               │ Ranked Candidates
                               ▼
┌─────────────────────────────────────────────────────────────┐
│             AutocompleteSuggestionsOverlay (UI)             │
│  - Entity Iconography: Dish, Ingredient, Tag, Category      │
│  - Hardware-Accelerated Vertical Scrollbar                  │
│  - Frequency Subtitle ("6 dishes", "4 dishes")              │
│  - Tapping injects suggestion before/into active query      │
└─────────────────────────────────────────────────────────────┘

Context-Aware Intent & Scoping

The suggester detects what you are trying to find based on your active prefix:

  • Free-Text Queries (no prefix): Assumes you are likely looking for a dish name. Dishes are prioritized first (DISH > TAG > INGREDIENT > CATEGORY).
  • # Prefix Queries: Assumes you are searching for a tag. Tags are prioritized first (TAG > DISH > INGREDIENT > CATEGORY).
  • @ Prefix Queries: Assumes you are searching for an ingredient. Ingredients are prioritized first (INGREDIENT > DISH > TAG > CATEGORY).

Word-Position Hierarchy (wordIndex)

Matches appearing earlier in entity titles are ranked strictly above matches appearing later:

  • 1st Word Exact Match (wordIndex = 0): 95 points (e.g. “Chicken Tikka” when typing "Chicken").
  • 1st Word Prefix Match (wordIndex = 0): 90 points (e.g. “Chicken” when typing "Chi").
  • 2nd Word Match (wordIndex = 1): 75–80 points (e.g. “Boneless Chicken Breast” when typing "Chi").
  • 3rd Word Match (wordIndex = 2): 65–70 points (e.g. “Slow Cooker Chicken Curry” when typing "Chi").
  • 4th+ Word Match (wordIndex = 3+): 55–60 points.
  • Substring Containment: 35 points (wordIndex = 50).
  • Fuzzy Typo Tolerance: 20 points (wordIndex = 100) with Damerau-Levenshtein distance $\le 2$.

Category Prioritization

Because categories represent major meal classifications, a 1st-word match on a Category (e.g. Category “Dinner”, wordIndex = 0) always outranks recipes where the word appears only in the 3rd or 4th position (e.g. “Grandma’s Sunday Roast Dinner”, wordIndex = 3).

Multi-Token Editing Preservation

When you type multi-word queries like #Comfort Food Chi, the suggester isolates the active token (Chi) while preserving your preceding filters (#Comfort Food ). Tapping a suggestion like Chicken breast produces #Comfort Food @Chicken breast .


3. Search Relevance Scorer (SearchScorer)

Once candidate dishes are identified, SearchScorer calculates a unified relevance score to sort your results list with pinpoint precision.

Multi-Field Weighting

Matches in different fields carry proportional weight reflecting user search intent:

  • Dish Title (10.0x): The recipe title is the strongest indicator of relevance.
  • Assigned Tags (5.0x): High thematic relevance (e.g. #Vegetarian, #Quick).
  • Ingredients (3.0x): Ingredients contained within the recipe.
  • Category (2.0x): Meal categories (e.g. Dinner, Lunch, Dessert).

Word-Position Decay Multiplier (scoreTokenMatch)

Within each field, the match quality multiplier decays based on word position:

Match Type 1st Word 2nd Word 3rd Word 4th+ Word
Exact Match 0.95 0.80 0.70 0.60
Prefix Match 0.85 0.75 0.65 0.55
Substring Match 0.50 0.50 0.50 0.50
Fuzzy Match (≥ 4 chars) 0.35 0.35 0.35 0.35

Household Quality & Recency Boosts

To make search practical for dinnertime decisions, SearchScorer adds contextual bonuses:

  • Star Rating Bonus: (dish.rating ?: 0) * 0.2 (adds up to +2.0 points for 10-star family favorites).
  • Overdue Recency Bonus: min(daysOverdue, 30) * 0.05 (prioritizes beloved recipes that haven’t been cooked in weeks).
  • Ready-to-Cook Bonus: +1.5 points when a dish is outside its repeat frequency window.

Strict Boolean AND Intersection

SearchDishesUseCase requires dishes to satisfy all positive search tokens. A query for #Quick @garlic pasta only returns dishes that possess the tag #Quick, list garlic as an ingredient, and contain pasta in their title, tags, or ingredients. Any match containing an excluded term (-exclude) is immediately eliminated.


4. Search UX & One-Hand Ergonomics

The search experience is built for real-world handheld usability in the kitchen:

  1. Right-Aligned Dismiss Arrow (<-): The close search button is situated on the right side of the search input field. This allows easy thumb reach on modern large phones without needing to stretch across the display.
  2. Prepend Suggested Filter Chips: Tapping a suggested tag chip (e.g. #Comfort Food) while typing query text (e.g. Carro) inserts the tag before your typed text (#Comfort Food Carro) while maintaining your cursor position at the end.
  3. Silent 100-Character Constraint: The input field silently caps queries at 100 characters (DishValidator.MAX_SEARCH_QUERY_LENGTH = 100), preventing buffer issues without intrusive UI error banners or distracting counters.
  4. Interactive Autocomplete Overlay:
    • Features domain-specific iconography: RestaurantMenu for dishes, Eco (fresh leaf) for ingredients, LocalOffer for tags, and Category for categories.
    • Includes a custom hardware-accelerated vertical scrollbar indicator (drawVerticalScrollbar).
    • Automatically dismisses when you touch or scroll the main dish results list.
  5. Contextual Ingredient Badges: When an ingredient matches your query, a clean badge ([RestaurantMenu] <Ingredient>) appears on the dish card in both List and Grid views. You can toggle badges on or off anytime in Settings > Preferences.