---
title: "Introducing RazorSearch for Umbraco"
description: "RazorSearch is my successor to Full Text Search for Umbraco, with weighted titles and headings, background rendering and tools for inspecting extracted text."
date: 2026-10-01
tags: ["umbraco", "razor", "package", "backoffice"]
---

I've maintained [Full Text Search](https://marketplace.umbraco.com/package/our.umbraco.fulltextsearch) to solve a particular problem: searching the text a visitor sees, including blocks and related content assembled by Razor templates. Umbraco Search gave me a reason to rethink how the package does that.

I built [RazorSearch](https://marketplace.umbraco.com/package/umbraco.community.razorsearch) as the successor to Full Text Search for Umbraco 17 and 18. It renders published pages, extracts their text, and indexes the result through Umbraco Search. I've added a small API for building a search page, separate weighting for rendered titles and headings, and backoffice tools for inspecting the rendering work.

## Why a new package?

Full Text Search did this by adding fields containing the rendered text to Umbraco's `ExternalIndex`. It also provided a simpler way to query Examine and turn the results into something useful on a search page. I've written about [earlier improvements to Full Text Search](/blog/4-new-features-in-full-text-search-umbraco) before.

[Umbraco Search](https://github.com/umbraco/Umbraco.Cms.Search) introduces a search abstraction with providers underneath it. According to Umbraco's [Umbraco Search announcement](https://github.com/umbraco/Announcements/issues/36), it will be included in Umbraco 19 and replace the existing core Examine indexes. Examine will remain the default provider, but search implementations should use the Umbraco Search abstractions.

I built RazorSearch on that abstraction, with a separate index for the text extracted from stored page snapshots. I've kept the core package independent of the search provider and put the Examine index setup in a companion package.

I also wanted a different name. "Full Text Search" never quite described what made the package useful. RazorSearch is a separate package with the same starting point, and the new name makes that distinction clearer.

## Giving titles and headings more weight

This is the change I wanted most. Once the rendered text is split into separate parts, the search can distinguish a match in a heading from one in the body of the page.

By default, RazorSearch extracts and indexes these parts:

| Part of the page | Default source | Relevance level |
| --- | --- | --- |
| Title | `<title>` | Highest, using `TextsR1` |
| Headings | `<h1>` through `<h6>` | Next, using `TextsR2` |
| Body | `<body>` | Normal, using `Texts` |

These are [Umbraco Search's relevance levels](https://github.com/umbraco/Umbraco.Cms.Search/blob/main/src/Umbraco.Cms.Search.Core/Models/Indexing/IndexValue.cs). The search provider applies them when scoring results. All heading levels share the same relevance level here.

I already had a title boost for configured index fields in Full Text Search. In RazorSearch, I extract the rendered title and headings separately, then pass them to Umbraco Search with their intended relevance.

For a query like "installation", I want a match in a heading to carry more weight than a mention halfway through a paragraph. The separate fields give the provider that information, though other scoring factors still affect the final order.

The extraction sources are configurable. You can use CSS selectors to select parts of the HTML, or read published Umbraco properties when those are a better source. The [configuration guide](https://github.com/skttl/umbraco-razor-search/blob/main/docs/configuration/README.md) covers the available options.

## Seeing what RazorSearch is doing

Full Text Search already lets you trigger reindexing from the content tree and includes health checks. Older versions also had a dashboard. In RazorSearch, I've added a view of the rendering queue and the text extracted from individual pages.

The operations view shows pending, running, completed and failed jobs, along with progress for the current batch. You can queue a single page, a page and its descendants, or a backfill of published content.

![RazorSearch operations view showing an active rendering batch](/images/blog/introducing-razorsearch-for-umbraco/queue-running.png)

For an individual page, you can inspect the stored title, headings, summary and body text, along with the culture and rendering status. Failed jobs also expose their error messages.

That gives you somewhere to start when a page is missing from search or contains unexpected text. You can check whether the rendering failed and whether the text you expected is present in the snapshot.

![RazorSearch snapshot details showing the Danish title, heading and rendering status, with empty summary and body fields](/images/blog/introducing-razorsearch-for-umbraco/snapshot-details.png)

Rendering happens in the background. Publishing queues the work, and saving the resulting snapshot triggers an update of RazorSearch's index. That means there can be a short delay between publishing a page and seeing its updated text in search.

Changing a template, shared content or extraction rules requires a manual rebuild of the affected snapshots. Rebuilding the search index alone reads the stored snapshots without rendering the pages again. The rendering queue lives in memory, so a restart loses pending work. If a restart interrupts a batch, queue another rebuild. The [migration guide](https://github.com/skttl/umbraco-razor-search/blob/main/docs/migrating-from-fulltextsearch/README.md) covers these operational differences too.

## Coming from Full Text Search

I've changed both the setup and the search API, so migrating involves more than swapping the NuGet package. I've written a [migration guide](https://github.com/skttl/umbraco-razor-search/blob/main/docs/migrating-from-fulltextsearch/README.md) covering the full process. Here are the main changes.

### Set up Umbraco Search

Replace `Our.Umbraco.FullTextSearch` with `Umbraco.Community.RazorSearch`. If you use the Examine provider, install `Umbraco.Community.RazorSearch.Examine`, which includes the core package as a dependency.

For Umbraco 17:

```bash
dotnet add package Umbraco.Community.RazorSearch.Examine --version 17.0.0
```

For Umbraco 18:

```bash
dotnet add package Umbraco.Community.RazorSearch.Examine --version 18.0.0
```

I target Umbraco `17.0.0` and later in the 17.x line, and Umbraco `18.0.0` and later in the 18.x line. Both package lines require .NET 10 and Umbraco Search. Choose the RazorSearch package line matching your Umbraco major version.

Enable Umbraco Search and its provider in your existing `Program.cs`. For Examine, add these namespaces and registrations to the usual builder chain:

```csharp
using Umbraco.Cms.Search.Core.DependencyInjection;
using Umbraco.Cms.Search.Provider.Examine.DependencyInjection;

builder.CreateUmbracoBuilder()
    .AddBackOffice()
    .AddWebsite()
    .AddComposers()
    .AddSearchCore()
    .AddExamineSearchProvider()
    .Build();
```

Keep the rest of your Umbraco startup code. I've made RazorSearch register its own services automatically; your application still needs to register the search provider.

After installation, queue a backfill in the backoffice so RazorSearch can render the existing pages and populate its index. The old Full Text Search cache does not provide those new snapshots.

See the [installation guide](https://github.com/skttl/umbraco-razor-search/blob/main/docs/installation/README.md) for the complete setup.

### Update the search request

Replace the injected `ISearchService` with `IRazorSearchService`, which lives in the `Umbraco.Community.RazorSearch` namespace. The request model changes from `Search` to `RazorSearch`, and the search call becomes asynchronous.

For example, with a non-empty `query` and a one-based `pageNumber`, a Full Text Search request might look like this:

```csharp
using Our.Umbraco.FullTextSearch.Models;

var search = new Search(query)
    .SetCulture(CurrentPage.GetCultureFromDomains())
    .AddRootNodeId(CurrentPage.Root().Id)
    .AddAllowedContentTypes("article")
    .SetPageLength(10);

var result = _searchService.Search(search, pageNumber);
```

The corresponding RazorSearch request is:

```csharp
using Umbraco.Community.RazorSearch.Models;
using Umbraco.Extensions;

var search = new RazorSearch(query)
    .InCulture(CurrentPage.GetCultureFromDomains())
    .UnderRoot(CurrentPage.Root().Key)
    .IncludeContentTypes("article")
    .Page(pageNumber, 10);

var result = await _razorSearchService.SearchAsync(search);
```

The root now uses the content's GUID `Key`, and paging is part of the request. Set the culture to a configured Umbraco language to search that language and invariant content. Without a culture, RazorSearch searches invariant content only.

The [usage guide](https://github.com/skttl/umbraco-razor-search/blob/main/docs/usage/README.md) includes a complete Razor template for a search page.

I've kept the results focused on what a search page needs: a title, URL, published content and highlighted `SummaryHtml`.

### Translate configuration and rendering helpers

Configuration moves from `Umbraco:FullTextSearch` to `Umbraco:Community:RazorSearch` in `appsettings.json`.

I've included an appsettings schema that Umbraco picks up automatically when you build the application. That gives you IntelliSense for RazorSearch's settings when you edit `appsettings.json`.

XPath rules such as `XPathsToRemove` become CSS selectors in `SnapshotExtraction.RemoveSelectors`. Separate source lists control the title, summary, headings and body. If you use a helper in your templates to omit navigation or other markup during search rendering, replace `FullTextSearchHelper.IsRenderingActive()` with `SearchRenderingContext.IsActive`.

> **Note:** HTTP rendering was already the default in Full Text Search, so the site still needs to be able to request its own pages. The alternative `RenderTemplate()` renderer was there for backwards compatibility, and I haven't carried it over to RazorSearch.

Some other Full Text Search options also have no direct equivalent in RazorSearch's API, including custom Lucene queries, automatic wildcard and fuzziness settings, and selecting an arbitrary index. Result items no longer expose raw Examine fields or scores. If your implementation uses those features, review that code as part of the migration.

## What happens to Full Text Search?

Full Text Search is now in maintenance mode. Its [18.0.0 release](https://www.nuget.org/packages/Our.Umbraco.FullTextSearch/18.0.0) supports Umbraco 18. I don't expect to add more features, but I may still release occasional patches if something needs fixing.

You can continue using the existing releases on Umbraco 17 and 18. Installing RazorSearch is a separate migration, so an existing search implementation does not need to change simply because this package exists.

I plan to put future development into RazorSearch rather than Full Text Search. For projects moving to Umbraco Search, it is the successor I intend people to use. Because Umbraco Search will become part of Umbraco 19, projects on Umbraco 17 or 18 can start the migration before that breaking change arrives.

## Get RazorSearch

I've released `17.0.0` for Umbraco 17 and `18.0.0` for Umbraco 18. The [release notes](https://github.com/skttl/umbraco-razor-search/blob/main/docs/release-notes.md) cover the features and remaining limitations.

[RazorSearch is available on Umbraco Marketplace](https://marketplace.umbraco.com/package/umbraco.community.razorsearch), free and MIT-licensed. Start with the [installation guide](https://github.com/skttl/umbraco-razor-search/blob/main/docs/installation/README.md), or the [migration guide](https://github.com/skttl/umbraco-razor-search/blob/main/docs/migrating-from-fulltextsearch/README.md) if you're coming from Full Text Search.