DEV Community

Cover image for Blazor AutoComplete That Actually Scales: From 10 Items to 100K (with AI Superpowers)
Mashrul Haque
Mashrul Haque

Posted on

Blazor AutoComplete That Actually Scales: From 10 Items to 100K (with AI Superpowers)

A high-performance Blazor AutoComplete and typeahead component with AI semantic search, 8 display modes, and virtualization for 100K+ items. Fully AOT compatible for .NET 8, 9, and 10.

If you've ever needed a Blazor autocomplete, typeahead, or autosuggest component that handles real-world datasets without choking, you know the pain. Most components work fine with 100 items. Load 10,000 products? The browser tab freezes. Add semantic search requirements? Now you're building custom infrastructure.

This article walks through a Blazor AutoComplete component I built after implementing search-as-you-type functionality one too many times. It handles 100,000+ items at 60fps, includes AI-powered semantic search, and ships under 15KB gzipped. Call it autocomplete, typeahead, autosuggest. Whatever. Same problem, same solution.


TL;DR

Built a Blazor AutoComplete component that:

  • Handles 100,000+ items at 60fps (seriously, try scrolling)
  • AI-powered semantic search - type "automobile" and find "car"
  • 8 built-in display modes - stop rewriting the same ItemTemplate
  • < 15KB gzipped - smaller than most favicons these days
  • AOT compatible - core package is fully trimmable (AI packages have SK dependency)
  • 5 vector database providers - PostgreSQL, Azure AI Search, Pinecone, Qdrant, CosmosDB

Works on: .NET 8, 9, and 10 | Rendering modes: WebAssembly, Server, Auto

dotnet add package EasyAppDev.Blazor.AutoComplete
Enter fullscreen mode Exit fullscreen mode

The Problem: Why Most Autocomplete Components Fall Short

Every Blazor project eventually needs a typeahead or autosuggest input. You start with the basics: an input field, an @oninput handler, and a foreach loop. It works for your demo with 50 items:

<input @bind="searchText" @oninput="Filter" />
@foreach (var item in filteredItems)
{
    <div @onclick="() => Select(item)">@item.Name</div>
}
Enter fullscreen mode Exit fullscreen mode

Then reality hits. Requirements creep in one by one:

  • "Can it search by description too?" - Now you need multi-field filtering
  • "We need it to look like our design system" - Custom templates for every project
  • "It's slow with 5,000 products" - Virtualization becomes mandatory
  • "Users keep misspelling things" - Fuzzy matching or they can't find anything
  • "Can it find related items, like 'vehicle' matching 'car'?" - Semantic search territory

Six months later, you've got 800 lines of autocomplete code, zero test coverage, and a new developer asking "what does this do?" The component that started as 20 lines now handles edge cases you forgot existed.

I've been there. Multiple times across different projects. This component exists because there had to be a better way.


Getting Started: Add Blazor AutoComplete in 30 Seconds

The fastest way to add a production-ready typeahead to your Blazor app. Two lines of setup, then drop the component into any page. The component handles keyboard navigation, ARIA accessibility attributes, input debouncing, and focus management automatically. No configuration required.

Step 1: Register the service in Program.cs

// Program.cs
builder.Services.AddAutoComplete();
Enter fullscreen mode Exit fullscreen mode

Step 2: Add the component to your Razor page

The generic TItem parameter works with any class. The TextField expression tells the component which property to display and search against. Two-way binding with @bind-Value gives you the selected item.

@using EasyAppDev.Blazor.AutoComplete

<AutoComplete TItem="Product"
              Items="@products"
              TextField="@(p => p.Name)"
              @bind-Value="@selectedProduct"
              Placeholder="Search products..." />
Enter fullscreen mode Exit fullscreen mode

Done. You get a combobox with keyboard navigation (Arrow keys, Enter, Escape, Home, End), screen reader support, and animations. No JavaScript.


Search Multiple Fields: Name, Description, SKU in One Query

The most requested feature for any autosuggest component. Real users don't know which field contains the data they're looking for. They just type "ergonomic" and expect to find everything related, whether it's in the name, description, category, or SKU.

The SearchFields parameter accepts a lambda returning an array of strings. The component searches all fields with OR logic, so a match in any field returns the item.

<AutoComplete TItem="Product"
              Items="@products"
              TextField="@(p => p.Name)"
              SearchFields="@(p => new[] { p.Name, p.Description, p.Category, p.SKU })"
              FilterStrategy="FilterStrategy.Contains"
              Placeholder="Search everything..." />
Enter fullscreen mode Exit fullscreen mode

What happens when you type "ergonomic":

  • Finds "Ergonomic Chair" (name match)
  • Finds "Wireless Mouse" with description "Ergonomic wireless mouse..."
  • Finds anything tagged "ergonomic" in category

All OR logic. No custom filtering code needed.


8 Built-in Display Modes: Stop Writing Custom Templates

I looked at my old projects. Dozens of autocomplete implementations. Most had nearly identical ItemTemplate markup: title on top, description below, maybe a badge. Writing the same template over and over wastes time and introduces inconsistency.

Now there are 8 built-in display modes that cover 90% of use cases. Pick a mode, map your properties, and move on. Custom templates are still available when you need complete control.

<!-- Two-line layout: bold title + muted description -->
<AutoComplete DisplayMode="ItemDisplayMode.TitleWithDescription"
              DescriptionField="@(p => p.Category)" ... />

<!-- Title with right-aligned price badge -->
<AutoComplete DisplayMode="ItemDisplayMode.TitleWithBadge"
              BadgeField="@(p => $"${p.Price:F2}")" ... />

<!-- Icon/emoji on left + title + description -->
<AutoComplete DisplayMode="ItemDisplayMode.IconTitleDescription"
              IconField="@(p => p.Emoji)"
              DescriptionField="@(p => p.Category)" ... />

<!-- Full card layout with all fields -->
<AutoComplete DisplayMode="ItemDisplayMode.Card"
              IconField="@(p => p.Emoji)"
              SubtitleField="@(p => p.Category)"
              DescriptionField="@(p => p.Description)"
              BadgeField="@(p => $"${p.Price:F2}")" ... />
Enter fullscreen mode Exit fullscreen mode

Available modes: Simple, TitleWithDescription, TitleWithBadge, TitleDescriptionBadge, IconWithTitle, IconTitleDescription, Card, Custom

Card mode packs everything into one row: thumbnail, title, subtitle, description, badge. Looks like what you'd see in Google or Amazon's search dropdowns.


Filter Strategies: From Fast Prefix Matching to Typo Tolerance

Different use cases need different filtering algorithms. A product SKU lookup needs exact prefix matching for speed. A customer-facing search needs typo tolerance because users can't spell. The component includes four built-in strategies.

Strategy Best Use Case Performance (100K items)
StartsWith SKU lookup, known prefixes ~3ms
Contains General search, substring matching ~5ms
Fuzzy User-facing search with typo tolerance ~70ms
Custom Your own algorithm Depends on implementation

Enabling fuzzy search for typo tolerance:

Users type "laptpo" and find "Laptop." They type "chiar" and find "Chair." Fuzzy matching uses Levenshtein distance to handle common typos without requiring exact spelling.

<AutoComplete FilterStrategy="FilterStrategy.Fuzzy"
              ... />
Enter fullscreen mode Exit fullscreen mode

Look, fuzzy search won't find "computer" when someone types "laptop." That's not what Levenshtein does. But transposed letters, missing characters, common typos? Handles those fine. For actual concept matching, you need the AI stuff below.


Virtualization for 100K+ Items at 60fps

Most typeahead components die here. Load 10,000 items, browser struggles. Load 100,000, tab freezes. The DOM can't handle that many elements at once.

Virtualization solves this by only rendering items currently visible in the viewport. Scroll down, and items render on demand. The component maintains a scroll container with calculated height, creating the illusion of a complete list while only materializing visible rows.

<AutoComplete TItem="Product"
              Items="@largeDataset"
              Virtualize="true"
              VirtualizationThreshold="100"
              ItemHeight="40"
              ... />
Enter fullscreen mode Exit fullscreen mode

What this gives you:

  • Only visible items hit the DOM
  • 60fps scrolling, tested with 100K items
  • Kicks in automatically past threshold
  • Works with grouping headers too

The ItemHeight parameter should match your CSS. Accurate height calculation ensures smooth scrolling without visual jumps. I tested this with 100K products on an M1 MacBook Air. Scrolling stayed smooth, memory stayed reasonable, browser never complained.


AI Semantic Search: Find "car" When Users Type "automobile"

Traditional text matching breaks down when users don't know your terminology. They search for what they mean, not what you labeled it.

With semantic search:

  • Type "automobile" → find "Toyota Camry"
  • Type "mobile apps" → find "React Native Tutorial"
  • Type "password security" → find "OAuth Implementation Guide"

Embedding models convert text into vectors. Similar concepts end up near each other in vector space. "Automobile" clusters with "car" and "sedan" even though they share zero letters.

Setting Up AI-Powered Autosuggest

dotnet add package EasyAppDev.Blazor.AutoComplete.AI
Enter fullscreen mode Exit fullscreen mode

For OpenAI embeddings:

// Program.cs
builder.Services.AddAutoCompleteSemanticSearch(
    apiKey: "sk-...",
    model: "text-embedding-3-small"
);
Enter fullscreen mode Exit fullscreen mode

For Azure OpenAI:

builder.Services.AddAutoCompleteSemanticSearchWithAzure(
    endpoint: "https://my-resource.openai.azure.com/",
    apiKey: "...",
    deploymentName: "text-embedding-ada-002"
);
Enter fullscreen mode Exit fullscreen mode

Using the semantic autocomplete component:

<SemanticAutoComplete TItem="Document"
                      Items="@documents"
                      SearchFields="@(d => new[] { d.Title, d.Description, d.Tags })"
                      SimilarityThreshold="0.15"
                      @bind-Value="@selectedDoc"
                      Placeholder="Search by meaning..." />
Enter fullscreen mode Exit fullscreen mode

The SimilarityThreshold controls how closely items must match the query. Lower values (0.1) return more results with looser matching. Higher values (0.3) require stronger semantic similarity.

Managing Embedding Costs with Dual Caching

Embedding API calls cost money. Each search query requires one API call to embed the query. Each item needs embedding to enable similarity comparison. Without caching, costs spiral quickly.

The component uses dual caching to minimize API calls:

Cache Default TTL Max Size Purpose
Item Cache 1 hour 10,000 items Your data embeddings
Query Cache 15 minutes 1,000 queries User search queries

Pre-warming for instant results:

<SemanticAutoComplete PreWarmCache="true" ... />
Enter fullscreen mode Exit fullscreen mode

Pre-warming generates all embeddings on init. Users get instant results because everything's cached before they type. One-time hit, then it's fast.

How it stays fast:

  • SIMD cosine similarity (System.Numerics.Tensors), 3-5x faster than naive loops
  • LRU eviction when caches fill up
  • Background cleanup purges expired entries every 5 min

Vector Database Providers: Production-Grade Semantic Search

In-memory caching works perfectly for development and small datasets. Production deployments with millions of products need persistent vector storage with approximate nearest neighbor (ANN) indexing.

Five providers integrate directly with the component:

Provider Package Best For
PostgreSQL/pgvector .AI.PostgreSql Self-hosted, existing Postgres infrastructure
Azure AI Search .AI.AzureSearch Enterprise, hybrid search (semantic + keyword)
Pinecone .AI.Pinecone Serverless, automatic scaling
Qdrant .AI.Qdrant Open-source, self-hosted with advanced filtering
Azure CosmosDB .AI.CosmosDb Global distribution, multi-model

PostgreSQL with pgvector Example

PostgreSQL with the pgvector extension is the most accessible option for teams already running Postgres. No new infrastructure required. Just enable the extension and create a vector column.

dotnet add package EasyAppDev.Blazor.AutoComplete.AI.PostgreSql
Enter fullscreen mode Exit fullscreen mode

Configuration in appsettings.json:

{
  "VectorSearch": {
    "PostgreSQL": {
      "ConnectionString": "Host=localhost;Database=myapp;...",
      "CollectionName": "products",
      "EmbeddingDimensions": 1536
    }
  },
  "OpenAI": {
    "ApiKey": "sk-..."
  }
}
Enter fullscreen mode Exit fullscreen mode

Service registration:

builder.Services.AddAutoCompletePostgres<Product>(
    builder.Configuration,
    textSelector: p => $"{p.Name} {p.Description}",
    idSelector: p => p.Id.ToString());

builder.Services.AddAutoCompleteVectorSearch<Product>(builder.Configuration);
Enter fullscreen mode Exit fullscreen mode

Indexing your product catalog:

Before semantic search works, your data needs embedding and storage in the vector database. The IVectorIndexer<T> service handles batch indexing with automatic embedding generation.

public class ProductService
{
    private readonly IVectorIndexer<Product> _indexer;

    public async Task IndexProducts(IEnumerable<Product> products)
    {
        await _indexer.EnsureCollectionExistsAsync();
        await _indexer.IndexAsync(products);
    }
}
Enter fullscreen mode Exit fullscreen mode

Run indexing once when your data changes. Queries hit the vector database directly. No runtime embedding of your catalog needed.


OData Integration: Server-Side Typeahead Filtering

For applications with server-side data, the OData package generates $filter queries automatically. The component sends search requests to your API endpoint rather than filtering locally.

dotnet add package EasyAppDev.Blazor.AutoComplete.OData
Enter fullscreen mode Exit fullscreen mode

Configuring the OData data source:

var options = new ODataOptions
{
    EndpointUrl = "https://api.example.com/odata/Products",
    FilterStrategy = ODataFilterStrategy.Contains,
    Top = 20,
    CaseInsensitive = true
};

_odataSource = new ODataDataSource<Product>(Http, options,
    searchFieldNames: new[] { "Name", "Description", "Category" });
Enter fullscreen mode Exit fullscreen mode

Generated query when user types "laptop":

GET /odata/Products?$filter=(contains(tolower(Name),'laptop') or contains(tolower(Description),'laptop') or contains(tolower(Category),'laptop'))&$top=20
Enter fullscreen mode Exit fullscreen mode

Debouncing, request cancellation, loading states: handled. Your API just gets clean OData queries.

Supports both OData v3 and v4 protocols. v3 uses substringof() instead of contains() for substring matching.


Theming: Material, Fluent, Bootstrap, or Custom

Four presets, each with light/dark variants:

<AutoComplete ThemePreset="ThemePreset.Material" ... />
<AutoComplete ThemePreset="ThemePreset.Fluent" ... />
<AutoComplete ThemePreset="ThemePreset.Modern" ... />
<AutoComplete ThemePreset="ThemePreset.Bootstrap" ... />
Enter fullscreen mode Exit fullscreen mode

Bootstrap colors:

All nine Bootstrap theme colors. Hover states, focus rings, selection styles generated automatically.

<AutoComplete BootstrapTheme="BootstrapTheme.Primary" ... />
<AutoComplete BootstrapTheme="BootstrapTheme.Success" ... />
<AutoComplete BootstrapTheme="BootstrapTheme.Danger" ... />
Enter fullscreen mode Exit fullscreen mode

Dark mode:

Theme.Auto follows OS preference. CSS media queries, no JS.

<AutoComplete Theme="Theme.Auto" ... />  <!-- Follows OS preference -->
<AutoComplete Theme="Theme.Dark" ... />  <!-- Force dark mode -->
Enter fullscreen mode Exit fullscreen mode

Custom overrides:

Presets not your thing? Override individual properties:

<AutoComplete PrimaryColor="#FF6B6B"
              BorderRadius="8px"
              FontFamily="Inter, sans-serif"
              DropdownShadow="0 4px 12px rgba(0,0,0,0.15)"
              ... />
Enter fullscreen mode Exit fullscreen mode

Grouping Results by Category

Group items by any property to help users scan large result sets. Each group displays a header, and items appear nested beneath their category.

<AutoComplete TItem="Product"
              Items="@products"
              TextField="@(p => p.Name)"
              GroupBy="@(p => p.Category)">
    <GroupTemplate Context="group">
        <div class="group-header">
            <strong>@group.Key</strong>
            <span class="badge">@group.Count()</span>
        </div>
    </GroupTemplate>
</AutoComplete>
Enter fullscreen mode Exit fullscreen mode

Works with virtualization. Group headers render on scroll like everything else. No perf hit.


Full AOT and Trimming Compatibility

One constraint shaped the whole architecture: no reflection at runtime.

The normal approach to property access in generic components (TextField.Compile()) uses reflection internally. AOT hates that. Trimming hates that. Your deployed app crashes because the runtime can't find types that got trimmed.

So instead: source generators create typed accessors at build time.

Important caveat: This applies to the core EasyAppDev.Blazor.AutoComplete package. The AI packages depend on Semantic Kernel, which isn't trimmable yet. If you need AOT/trimming and semantic search, you'll need to wait for SK to catch up or use the vector database providers with a separate indexing service.

// You write this in your Razor component
TextField="@(p => p.Name)"

// Source generator creates this at compile time
public static string GetName(Product p) => p.Name;
Enter fullscreen mode Exit fullscreen mode

Zero runtime cost. Full AOT compatibility. Works correctly with PublishAot=true and aggressive trimming.

The generators also catch invalid expressions at compile time:

  • EBDAC001 - Invalid TextField expression
  • EBDAC002 - Invalid ValueField expression
  • EBDAC003 - Unsupported expression pattern

Build-time errors beat runtime surprises every time.


Accessibility: WCAG 2.1 AA

ARIA 1.2 Combobox pattern. Screen readers work. Keyboard-only users can navigate everything.

Key Action
Arrow Down Open dropdown / Move to next item
Arrow Up Move to previous item
Enter Select highlighted item
Escape Close dropdown
Home Jump to first item
End Jump to last item

ARIA attributes you don't have to think about:

  • role="combobox" on input
  • role="listbox" on dropdown
  • role="option" on each item
  • aria-activedescendant for focus tracking
  • aria-expanded, aria-selected, aria-busy

Also:

  • High contrast mode support (prefers-contrast: high)
  • Reduced motion (prefers-reduced-motion: reduce)
  • RTL via RightToLeft="true"
  • Works with EditForm validation

Performance Benchmarks

Tested on M1 MacBook Air with .NET 9:

Metric Target Actual
Bundle size (gzipped) < 15KB 12KB
Filter 100K items (StartsWith) < 100ms 3ms
Filter 100K items (Fuzzy) < 100ms 72ms
First render < 50ms 35ms
Virtualized scroll 60fps 60fps
SIMD cosine similarity (1536-dim) N/A 3-5x faster than naive

Why's it fast? No unnecessary re-renders. Efficient DOM updates. Debounced input. Virtualization that actually works instead of just being a checkbox feature.


Fluent Configuration API

Setting 20 parameters inline gets ugly. Builder pattern cleans it up:

var config = AutoCompleteConfig<Product>.Create()
    .WithItems(products)
    .WithTextField(p => p.Name)
    .WithSearchFields(p => new[] { p.Name, p.Description, p.Category })
    .WithDisplayMode(ItemDisplayMode.TitleWithDescription)
    .WithTitleAndDescription(p => p.Description)
    .WithFilterStrategy(FilterStrategy.Contains)
    .WithTheme(Theme.Auto)
    .WithBootstrapTheme(BootstrapTheme.Primary)
    .WithVirtualization(threshold: 1000, itemHeight: 45)
    .WithGrouping(p => p.Category)
    .WithDebounce(300)
    .Build();
Enter fullscreen mode Exit fullscreen mode
<AutoComplete TItem="Product" Config="@config" @bind-Value="@selected" />
Enter fullscreen mode Exit fullscreen mode

Every parameter has a builder method. Nothing's left out.


When to Use What: Quick Reference

Scenario Recommendation
Small dataset (< 1K items) Basic component with StartsWith filter
Medium dataset (1K-10K) Enable virtualization
Large dataset (10K-100K) Virtualization + StartsWith for speed
Users misspell often FilterStrategy.Fuzzy
Need concept matching AI package with embedding cache
Production AI (> 10K items) Vector database provider
Server-side data OData package
Multi-tenant shared data Vector provider with collection per tenant

Troubleshooting Common Issues

Dropdown not opening?

  • Check MinSearchLength parameter (default: 1 character)
  • Verify Items collection or DataSource isn't null
  • Ensure the component has focus

Filtering returns no results?

  • Confirm TextField lambda returns a non-null string
  • Try FilterStrategy.Contains instead of StartsWith
  • Check for leading/trailing whitespace in your data

Virtualization scrolling is jumpy?

  • Set ItemHeight to match your actual CSS item height
  • Verify item count exceeds VirtualizationThreshold
  • Ensure all items have consistent heights

Semantic search returns nothing?

  • Lower SimilarityThreshold (try 0.1 or 0.12)
  • Check MinSearchLength for AI component (default: 3)
  • Verify API key in browser console network tab
  • Confirm items were pre-warmed or embedded

Installation Summary

# Core autocomplete/typeahead component
dotnet add package EasyAppDev.Blazor.AutoComplete

# OData server-side filtering
dotnet add package EasyAppDev.Blazor.AutoComplete.OData

# AI semantic search
dotnet add package EasyAppDev.Blazor.AutoComplete.AI

# Vector database providers (pick one based on your infrastructure)
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.PostgreSql
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.AzureSearch
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.Pinecone
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.Qdrant
dotnet add package EasyAppDev.Blazor.AutoComplete.AI.CosmosDb
Enter fullscreen mode Exit fullscreen mode

Add CSS to your layout:

<link href="_content/EasyAppDev.Blazor.AutoComplete/styles/autocomplete.base.css" rel="stylesheet" />
Enter fullscreen mode Exit fullscreen mode

Register services in Program.cs:

builder.Services.AddAutoComplete();
Enter fullscreen mode Exit fullscreen mode

Frequently Asked Questions

Why choose this over MudBlazor or Radzen autocomplete?

MudBlazor and Radzen are great component libraries. Use them if you need a full suite of UI controls. But their autocompletes weren't built for 100K+ items or semantic search. This component was. Different tools, different problems.

Does it work with Blazor Server and WebAssembly?

Yeah, all of them. WebAssembly, Server, Auto. Same component, same code.

What's the bundle size impact?

Core component: 12KB gzipped. AI package: ~25KB extra. Vector providers: 5-10KB each.

Can I use my own embedding provider?

Anything that implements Microsoft.Extensions.AI.IEmbeddingGenerator. OpenAI, Azure OpenAI, Ollama, whatever. Register it in DI and you're done.

Is this production-ready?

250+ tests, 72% coverage, runs on .NET 8/9/10. Core package is AOT/trim friendly. AI packages pull in Semantic Kernel which isn't trimmable. If that matters, stick to the core component or run AI stuff server-side. I use it in production. MIT licensed, no warranties, but it's not a weekend hack.


Summary

So what's the point? Most autocomplete components work fine until you throw real data at them. This one doesn't choke on 100K items. It understands what users mean, not just what they type. And you don't have to write the same ItemTemplate for every project.

Works on .NET 8, 9, 10. AOT compatible. No reflection tricks that blow up in production.

Get started →


What's Next

Open source on GitHub. Right now I'm working on Elasticsearch and Milvus providers, hybrid search (semantic + keyword combined), and getting test coverage above 90%. Eventually want CI benchmarks so regressions get caught automatically.


Links:


Got tired of building the same autocomplete over and over. Now it's a package. MIT licensed. PRs welcome.


About the Author

I'm Mashrul Haque, a Systems Architect with over 15 years of experience building enterprise applications with .NET, Blazor, ASP.NET Core, and SQL Server. I specialize in Azure cloud architecture, AI integration, and performance optimization.

When production catches fire at 2 AM, I'm the one they call.

Follow me here on dev.to for more .NET and Blazor content

Top comments (0)