> ## Content Index
> Fetch the complete content index at: https://devinside.cloud/llms.txt
> Use this file to discover other available public pages before exploring further.

# Semantic Search in Blazor WebAssembly: Where the Cost Moves
- URL: https://devinside.cloud/semantic-search-in-blazor-webassembly-where-the-cost-moves/
- Published: 2026-08-20T14:45:00.000Z
- Updated: 2026-11-12T17:08:32.000Z
- Description: Actual query embeddings run in ONNX Runtime Web inside a Blazor WebAssembly app, with verified tokenizer parity, real browser results and explicit first-use asset costs.
- Author: Marcin Borecki
- Tags: Engineering, Blazor & Local AI, #eng27, #devinside-review

START WITH THE MECHANISM

Interactive companion and laboratory update: 12 November 2026\. The editorial date places the topic on the timeline; this current explanation and its measured evidence retain their actual preparation and execution dates.

THE FULL INVESTIGATION

## Semantic Search in Blazor WebAssembly: Where the Cost Moves

Editorial context: 20 August 2026 · Prepared and updated: 12 November 2026

Dates and technology context

This is a retrospective editorial timeline. Topic context: 20 August 2026\. First publicly available: 12 November 2026\. Current text, visuals and laboratory revisions were prepared on 12 November 2026.

The actual ORTWeb1.22.0/Blazor10.0.15 implementation, 40 token checks, raw-text query runs, model-delivery observations and screenshots are 2026-11-12 additions. Do not move those precise pins or measurements into the historical core.

*Prepared on 12 November 2026\. This reference runs an actual query embedding model in the browser with ONNX Runtime Web on the CPU, presents results through Blazor WebAssembly, and uses preserved real document embeddings from M08\. It calls no hosted model or server inference endpoint.*

Removing an inference server does not remove the work. It moves model delivery, initialization, memory pressure and query execution onto the client device. That can be a useful architectural choice, but only if the comparison includes the work the browser now has to perform.

The easy demonstration is to load precomputed query vectors and call the result local semantic search. This application does something more specific: it accepts raw text, tokenizes that text locally, runs the pinned ONNX model in WebAssembly, pools and normalizes the output, and ranks a small local document index.

The documents use previously computed real embeddings. That preparation remains outside the browser and is explicitly recorded. The query vector, however, is not looked up from a list of known questions. The browser computes it when the user searches.

The result is a real, bounded client implementation with functional evidence, screenshots and measured initialization and query observations. It is not a claim that every device can run the model comfortably, that local execution guarantees privacy for the whole application, or that a small synthetic corpus represents production search quality.

## Draw the execution boundary before drawing the interface

01 / See the idea

### Two browser runtimes, one local search path

![Two browser runtimes, one local search path. The UI and model execute in separate browser runtime layers connected by JavaScript interop. The local server supplies files without receiving a query for inference.](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v1.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v1.svg)

The UI and model execute in separate browser runtime layers connected by JavaScript interop. The local server supplies files without receiving a query for inference.

Interpretation and evidence
- Identify where computation actually runs.
- Do not confuse asset hosting with server inference.
- Keep both runtime dependencies reproducible.
ENG01 correctness/measurement records, preserved M08 artifacts or ENG27 browser evidence, as described in the article.

The application has two browser runtimes with different responsibilities. Blazor WebAssembly runs the .NET UI component. ONNX Runtime Web executes the model through its WebAssembly backend. JavaScript interop connects the component to the tokenizer, inference session and ranking function.

The local HTTP server supplies static assets: the application, runtime files, model, vocabulary and corpus. It does not receive query text for inference. After initialization, the tested queries execute without additional network requests.

That boundary is narrower than saying there is no server. A server is still used to deliver the files. The claim is that the query's embedding and ranking are computed in the browser after the required assets arrive.

Microsoft documents Blazor WebAssembly as client-side execution on a WebAssembly-based .NET runtime. ONNX Runtime separately documents its web package and WebAssembly CPU execution option. The reference uses those mechanisms rather than a server-side Blazor circuit whose computation happens remotely. [Blazor hosting models](https://learn.microsoft.com/en-us/aspnet/core/blazor/hosting-models?view=aspnetcore-10.0&ref=devinside.cloud), [ONNX Runtime Web](https://onnxruntime.ai/docs/tutorials/web/?ref=devinside.cloud).

This distinction matters when evaluating cost. A responsive Blazor page does not prove that model inference is local. The execution evidence must identify where the model graph runs and which network requests occur during a query.

The browser test records both. It also blocks requests outside the temporary local origin so an unexpected remote dependency cannot silently make the demonstration work.

## Reuse the model artifact without hiding its size

03 / See the idea

### The model is part of the first-use cost

![The model is part of the first-use cost. These are local artifact sizes in decimal megabytes, excluding other app assets and HTTP overhead. Removing server inference transfers substantial delivery and initialization work to the client.](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v3.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v3.svg)

These are local artifact sizes in decimal megabytes, excluding other app assets and HTTP overhead. Removing server inference transfers substantial delivery and initialization work to the client.

Interpretation and evidence
- Include required assets in the architecture decision.
- Do not present warm query time as total first use.
- Keep model and runtime licenses with the source package.
ENG01 correctness/measurement records, preserved M08 artifacts or ENG27 browser evidence, as described in the article.

The model is the same official `sentence-transformers/all-MiniLM-L6-v2` ONNX artifact used by M08\. The pinned repository commit and SHA-256 manifest remain the source of truth. The ONNX file contains 90,405,214 bytes before HTTP overhead.

That size is a material part of the architecture. A local inference demonstration that reports only a warm query time hides the cost of making the model available to the browser in the first place.

The reference keeps the model in the owned work directory and serves it locally for the experiment. It is excluded from the source ZIP, which instead includes provenance and a reproducible verified acquisition path. No pickle file or repository-supplied executable model code is loaded.

ONNX Runtime Web is pinned to version 1.22.0 through the package lock. Its JavaScript, module and WebAssembly files are served from local paths, not fetched from a CDN at runtime. The model's Apache-2.0 declaration and the runtime's accompanying license information remain part of the reproduction materials.

The browser session uses the `wasm` execution provider and one thread. It does not use WebGPU or WebNN, and the results should not be attributed to those backends. The runtime configuration is explicit because different providers and thread settings create different compatibility and resource questions. [ONNX Runtime environment and session options](https://onnxruntime.ai/docs/tutorials/web/env-flags-and-session-options.html?ref=devinside.cloud).

The example proves that this artifact and configuration executed in the tested browser. It does not establish a universal model-size limit or a supported-device matrix beyond that observation.

## Keep document preparation distinct from query inference

The document vectors come from `tokens-reference.json`, a preserved output of the earlier M08 reference preparation. Each vector is associated with the document's original text and identifier. The dataset and model manifest are copied with hashes so the source of those vectors remains inspectable.

The browser also receives saved query reference vectors for validation, but the search function does not use them as a query lookup table. It calls the model on the entered text and compares that newly computed vector only with document entries. Keeping the reference artifact available for checks should not blur that execution distinction. The observed inference counter provides another check: it increases for initialization parity and again for each submitted search.

This is not a browser ingestion pipeline. The application does not parse a new document collection, compute every document vector or maintain an online index as users edit content. Those tasks belong to the preparation process and would require their own update design.

The query path is different. For each submitted question, the browser constructs token IDs and executes the model. The resulting vector is compared with the stored document vectors using an exact dot product over normalized representations.

The two paths must agree on the representation. A different tokenizer, model snapshot, pooling rule or normalization convention could make the vectors incompatible even if their dimensions match. The functional checks therefore compare the browser's output against the existing reference before accepting the session as ready.

This separation is useful for a static documentation site. Document preparation can occur when the site is built, while visitors perform local query computation. It also exposes a cost that should not disappear from the design: updating the documentation may require rebuilding and redistributing its index.

The current experiment keeps that corpus fixed. It demonstrates a local query over a known prepared index, not a complete synchronization protocol for changing content.

## A small WordPiece adapter needs a declared scope

The browser uses the pinned model vocabulary and a small JavaScript tokenizer matching the relevant uncased WordPiece behavior for the English fixture. It performs text cleanup, lowercasing, accent normalization, punctuation separation and longest-match subword lookup, with the expected special tokens.

The tokenizer is not advertised as a general replacement for every Hugging Face tokenizer pipeline. Its scope is the model and input family examined here. Unusual Unicode behavior, other languages or a different serialized tokenizer can require additional implementation and tests.

Before model initialization, the application compares its token IDs with the saved reference IDs for all forty document and query inputs. It also verifies the expected vocabulary IDs for the classification, separator and unknown tokens. A mismatch prevents the application from becoming ready.

This is stronger evidence than obtaining plausible results from one example query. A tokenizer can produce a vector of the correct shape while changing the model's meaning through different normalization or special-token handling.

The input limit is explicit. If the tokenized question exceeds the declared 256-token boundary, the adapter refuses it rather than silently truncating a condition at the end. The UI also limits the raw input length, but character count and token count remain different units.

The golden checks cover the forty known fixture inputs, not every possible string a user could type. The article therefore describes a verified adapter for this demonstration, with a clear boundary for further testing, rather than claiming complete tokenizer compatibility from a small test set.

## Run the graph and pool the actual token output

02 / Watch the mechanism

### Raw query to locally ranked evidence

![Raw query to locally ranked evidence. The browser computes each submitted query vector rather than looking it up. The document vectors remain outputs of the earlier M08 preparation boundary.](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v2.svg)

Raw query to locally ranked evidenceENG27 / VECTORS AND BOUNDARIESRaw query to locally ranked evidenceRaw questionVerified-scopeWordPiece IDsWASM modelToken output, meanpooling, normalizationExact localrankingCompare with realprepared documentvectorsBounded reference evidence · no generated-answer quality claim

Play animation[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v2.svg)

Static view. Play to see how the mechanism unfolds.

The browser computes each submitted query vector rather than looking it up. The document vectors remain outputs of the earlier M08 preparation boundary.

Interpretation and evidence
- Separate document preparation from query execution.
- Check tokenizer and vector parity.
- Dispose temporary tensors without claiming zero retention.
ENG01 correctness/measurement records, preserved M08 artifacts or ENG27 browser evidence, as described in the article.

The query path creates three integer tensors: input IDs, attention mask and token-type IDs. They have batch size one and the current tokenized sequence length. The ONNX session returns the token-level `last_hidden_state` output with 384 features per position.

The adapter checks the output dimension, averages the active token features and normalizes the resulting sentence vector. Its mathematical purpose is the same as the documented model usage in M08, but the computation now occurs through the browser runtime.

The JavaScript explicitly disposes input and output tensors after use. That is part of resource management, not proof that the browser immediately returns every byte to the operating system. The session and model remain loaded for subsequent queries, and the document vectors remain in application state.

The initialization check embeds one known reference text and compares every component of its normalized vector with the preserved reference. The maximum observed difference was approximately `6.93 × 10^-8`, below the declared `10^-4` tolerance.

That agreement supports the implemented browser path: tokenization, tensor construction, graph execution, pooling and normalization work together for the checked input. It does not prove semantic correctness of the model or quality on every query.

The later UI queries are actual new calls through the same path. They do not substitute stored query vectors, even when a question happens to match a known evaluation example.

## The Blazor component owns interaction state, not the model internals

The Razor component calls JavaScript through `IJSRuntime`. Loading and searching set a busy state and update a polite status message. The component displays ranked evidence records returned by the local search function.

The relevant call is explicit:

```csharp
hits = await JS.InvokeAsync<Hit[]>("searchLab.search", query);

```

The component does not generate an answer from those hits. Its completion message tells the reader to inspect the evidence and states that no answer was generated. This keeps a ranked document list from acquiring the authority of a conversational conclusion.

The load button is disabled after successful initialization, preventing repeated user clicks from creating unnecessary model sessions in this small demonstration. Search is disabled while another operation is active. Failures are reported as failures rather than converted into empty successful results.

The status text describes what the application knows: assets are loading, a question is being embedded, or ranking has completed. It does not invent a completion percentage for work whose remaining duration has not been measured.

This is a small UI, not a complete production interaction design. It does not provide cancellation of an in-flight ONNX invocation, persistent search history or background worker isolation for every UI operation. The reference makes those omissions visible rather than implying that a working button establishes all the ergonomics needed on a constrained device.

## Client-side filtering is not a confidentiality boundary

05 / See the idea

### Downloaded data is already visible to the client

![Downloaded data is already visible to the client. The Blue/public filter controls displayed candidates, but cannot hide Red fixture records already downloaded. The experiment uses nonsensitive synthetic data and makes no production confidentiality claim.](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v5.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v5.svg)

The Blue/public filter controls displayed candidates, but cannot hide Red fixture records already downloaded. The experiment uses nonsensitive synthetic data and makes no production confidentiality claim.

Interpretation and evidence
- A local filter is not access control over downloaded bytes.
- No query requests is a narrow privacy observation.
- Review all data and telemetry paths before broader claims.
ENG01 correctness/measurement records, preserved M08 artifacts or ENG27 browser evidence, as described in the article.

The search function filters the displayed candidates to Blue and public scope. That matches the query's fixture contract and makes its results comparable with the earlier server-side experiment.

However, the browser has downloaded the entire synthetic dataset and reference artifact, including the Red-tenant fixture records. A client-side filter cannot make downloaded information secret from the person controlling that browser.

This is safe for the demonstration because all records are invented and nonsensitive. It would be a serious error to present the same arrangement as protection for private tenant documents.

A production client would need to receive only material it is permitted to possess, under an appropriate authentication and delivery contract. The local ranking filter could still improve relevance within that authorized set, but it would not replace the server-side decision about which bytes may be delivered.

The same care applies to query privacy. The tested searches sent no network requests after initialization, which establishes a narrow useful fact about this application run. It does not prove that an entire website has no analytics, that a browser extension cannot observe input, or that every future version preserves the same boundary.

The correct claim is that this fixture's raw query embedding and ranking occurred locally without a server inference call. Broader privacy guarantees require a broader system analysis.

## Functional evidence from the actual browser

The test launched the built Blazor WebAssembly application in Chrome, loaded the local runtime and model, and waited for initialization to finish. It required all forty token-ID comparisons to pass and the reference-vector error to remain below tolerance.

The first query was the raw sentence asking for the maximum size of one uploaded file. The browser produced the same top-three document identifiers as the saved dense ranking for that question. That includes the earlier experiment's limitation: the storage-capacity document can rank ahead of the per-file limit document. Matching the reference does not make the top result semantically ideal.

The second raw query asked whether a suspended user may delete a project. The expected policy document ranked first. The test also verified that no network requests occurred between the end of initialization and completion of the two queries.

The application completed three actual browser inferences in the run: one reference check and two user-query checks. The test did not evaluate all eighteen questions through the browser or conduct a new relevance study.

At a 390-pixel viewport, the layout had no horizontal overflow. Desktop and mobile screenshots show the actual executed interface. A mobile-sized viewport on a desktop machine is not a mobile-device performance test, so the article does not claim one.

The browser and temporary local HTTP server were closed after the test. No persistent inference service remained running.

## What the measured initialization and queries include

04 / See the idea

### One measured browser run, distinct intervals

![One measured browser run, distinct intervals. Initialization and the two queries are individual observations on one desktop Chrome configuration. The timing does not provide a device matrix or a server-versus-client winner.](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v4.svg)

[Enlarge diagram](https://devinside.cloud/content/images/2026/11/devinside/ENG27-v4.svg)

Initialization and the two queries are individual observations on one desktop Chrome configuration. The timing does not provide a device matrix or a server-versus-client winner.

Interpretation and evidence
- State where each interval starts.
- Do not turn two queries into a steady-state curve.
- Viewport size is not mobile hardware.
ENG01 correctness/measurement records, preserved M08 artifacts or ENG27 browser evidence, as described in the article.

The integration owner ran the measurement mode once in the exclusive lane. Initialization took 837.5 milliseconds in that run. The two local query operations took approximately 25.3 and 15.5 milliseconds.

Those numbers are single observations, not medians, percentiles or a device-class guarantee. The initialization interval includes fetching local vocabulary and corpus inputs, token parity checks, model/session initialization and the reference inference. It begins after the page has already loaded the Blazor application and initial JavaScript resources.

The query interval includes local tokenization, ONNX inference, pooling, normalization and exact ranking. It does not include a network model call or a generated answer. The two different query times should not be interpreted as a stable warm-up curve from such a small sample.

The model resource transfer reported approximately 90.4 MB including the browser's recorded transfer overhead. The selected ONNX WebAssembly runtime resource contributed approximately 11.2 MB, with additional module, vocabulary and application resources. The local server used a no-store policy, so this experiment does not establish a persistent browser-cache strategy.

The measurements were taken in Chrome 157.0.8103.52 on the shared developer machine. A different CPU, memory limit, browser, network path or backend could change the result substantially. No low-end-device or offline-reload study was performed.

The useful result is therefore bounded: a real browser implementation initialized and answered two local retrieval queries under the recorded configuration, with the stated timing and asset costs. It is not evidence that client-side inference is always faster or cheaper than a server implementation.

## A static site still needs an update contract

Moving search into the browser changes where stale state can live. A user may hold an older model or index after the source documents have changed. The page can continue to produce plausible rankings from that old representation.

The current fixture avoids ambiguity by serving a fixed snapshot and recording its hashes. It does not implement a persistent cache, rolling index migration or a compatibility check across independently updated model and document assets.

A production design should version those assets together or define a compatibility contract. It should be able to tell which model and tokenizer produced the document vectors and whether the query path uses the same representation. Replacing only the model while retaining incompatible vectors can make the search mathematically executable and semantically invalid.

The ingestion companion demonstrates why changed text should not inherit an old vector merely because the source identifier stayed the same. The same principle applies to a downloadable browser index. Fresh labels cannot repair stale semantic preparation.

Update policy also affects cost. Downloading the full model on every visit is different from caching it, and caching introduces questions about invalidation and storage availability. The present single-run measurement should not be used to choose that policy without another experiment.

The architectural decision is therefore broader than where the query executes. It includes how the client obtains, identifies, retains and replaces the representation on which that query depends.

## The screenshots show an executed interaction

The desktop capture was taken after the first raw-text query completed. It displays three actual ranked source records and their similarity values. Those values are outputs of the browser computation, not labels entered to make a mockup resemble a working product.

![Executed Blazor WebAssembly search showing the raw question and three locally ranked evidence records.](https://devinside.cloud/content/images/2026/11/devinside/ENG27-ui-results.png)

The mobile capture follows the second query. It shows the same application at a narrow viewport, with the policy document returned by local inference and ranking.

![Executed narrow-viewport Blazor WebAssembly result for the suspended-user project-deletion question.](https://devinside.cloud/content/images/2026/11/devinside/ENG27-ui-mobile.png)

These images establish visible states, not a complete user-experience evaluation. They do not show how quickly a low-end phone would initialize the model or how a screen reader would announce every update. The functional report names the checks that actually ran and leaves those broader questions unclaimed.

## Reproduction needs the assets as well as the Razor component

The source package contains the Blazor project, JavaScript adapter, immutable input manifest and setup instructions. It excludes the ninety-megabyte model and generated runtime build output. The model can be acquired through the pinned verification script, while the ONNX Runtime Web package is installed at its declared version with a lockfile.

The local server maps only explicit asset roots: application files, the built Blazor framework directory, preserved inputs, model artifacts and the selected runtime package. It is a fixture server for reproduction, not a hardened public hosting implementation.

The browser wrapper blocks external requests during the test. This prevents a missing local asset from being silently replaced by a CDN or hosted inference call. It also records the resource paths so the execution boundary remains inspectable after the browser closes.

Reproducing only the visible component would not be enough. The tokenizer vocabulary, special-token convention, model graph, document-vector artifact and runtime backend all participate in the result. A change to any of them should be treated as a new configuration requiring parity and behavior checks.

That is why the package does not use an unversioned model URL as a convenient startup shortcut. A working demonstration today should remain explainable when a repository or package changes later.

## A parity check is a guardrail, not a universal tokenizer proof

The forty saved token cases are a strong check for this corpus because they compare the actual IDs supplied to the model. They can expose differences in casing, punctuation and special-token handling before a plausible ranking hides the problem.

They cannot establish compatibility for every Unicode string. The adapter's normalization and WordPiece logic are intentionally limited, and additional scripts or unusual control characters may require cases not present in this fixture. A production implementation should use a fully supported tokenizer or expand and maintain its compatibility tests for the input it promises to accept.

The vector comparison has a similar boundary. Close agreement for the reference input is evidence that the browser graph and pooling path are connected correctly for that case. It is not a mathematical proof that every operator on every input matches another runtime exactly.

The useful response to these limits is not to abandon local inference. It is to keep the supported input contract visible and make a mismatch fail before results are presented as trustworthy. That is why initialization refuses to continue if token IDs or the reference-vector tolerance do not match.

The application can then be extended deliberately. A new language, longer question or different model becomes a concrete compatibility task rather than an assumption hidden inside a general label such as semantic search.

## Use local execution where its actual boundary is valuable

The strongest argument for this architecture is that some applications have a small public corpus, a reusable model and a reason to avoid sending query text to a remote service. In that setting, browser inference can be a practical option worth measuring.

The strongest objection is the transferred cost. The client must download and initialize substantial assets, keep model and index state in memory and execute the query on hardware the application may not control. A server-side deployment can amortize some of that work across requests and centralize updates.

Neither argument settles every case. The right comparison should use the same corpus, query contract and quality criteria while accounting for first use, subsequent queries, update frequency and the actual devices involved.

This reference supplies one verified side of that comparison and a shared representation with the earlier .NET experiment. It does not include a simultaneous server endpoint benchmark, so it cannot claim a measured client-versus-server winner.

Its contribution is more concrete than a promise of local AI: real raw-text tokenization, actual ONNX graph execution in WebAssembly, reference parity, local ranking, visible results and an honest account of the bytes and work involved. The cost did not vanish. We can now point to where it moved.

## Source code and execution evidence

[Download the complete lab, source record and raw execution results](https://devinside.cloud/content/files/2026/11/ENG27-lab.zip). This package supports the specific execution claims in this article. It is not a production certification.