<?xml version="1.0" encoding="utf-8"?>
<?xml-stylesheet type="text/xsl" href="https://shazwazza.com/rss/xslt"?>
<rss xmlns:a10="http://www.w3.org/2005/Atom" version="2.0">
  <channel>
    <title>Shazwazza</title>
    <link>https://shazwazza.com/</link>
    <description>My blog which is pretty much just all about coding</description>
    <generator>Articulate, blogging built on Umbraco</generator>
    <image>
      <url>/media/0libq25y/frog.png?rmode=max&amp;v=1da0e911f4e6890</url>
      <title>Shazwazza</title>
      <link>https://shazwazza.com/</link>
    </image>
    <item>
      <guid isPermaLink="false">1336</guid>
      <link>https://shazwazza.com/post/letting-ai-agents-loose-on-examine/</link>
      <category>Examine</category>
      <title>Letting AI agents loose on Examine</title>
      <description>&lt;p&gt;I've been maintaining &lt;a href="https://github.com/Shazwazza/Examine"&gt;Examine&lt;/a&gt; for a very long time now. It's the search and indexing library that sits underneath every Umbraco site, and like any project that's been around long enough, it has plenty of hot paths that could be faster, a steady trickle of issues that need triaging, and a backlog of &amp;quot;I'll get to that eventually&amp;quot; performance work that, well… I never actually get to.&lt;/p&gt;
&lt;p&gt;So a couple of months ago I thought I'd try an experiment - what if I stopped doing all of that myself and instead let a bunch of AI agents do it for me, on a schedule, while I got on with everything else? This post is about how that went. I've shipped a few releases off the back of it and some of the hot-path improvements are genuinely a bit ridiculous.&lt;/p&gt;
&lt;h2 id="so-what-are-these-agentic-workflows"&gt;So what are these &amp;quot;agentic workflows&amp;quot;?&lt;/h2&gt;
&lt;p&gt;The thing I installed is &lt;a href="https://github.com/githubnext/agentics"&gt;GitHub Next's Agentic Workflows&lt;/a&gt; - a set of markdown-defined workflows that run in GitHub Actions and drive an AI agent to do actual maintenance work on your repo. They're not chat bots. They run on a schedule, they have persistent memory, they open real pull requests, and they leave the &amp;quot;should this ship?&amp;quot; decision to me.&lt;/p&gt;
&lt;p&gt;I ended up with a few of them running on Examine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;Perf Improver&lt;/strong&gt; - runs daily, hunts for performance bottlenecks, writes benchmarks to actually prove the improvement, and opens a draft PR with the before/after numbers.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Efficiency Improver&lt;/strong&gt; - its scrappier sibling, focused on the smaller allocation-and-LINQ-state-machine wins that add up over time.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Daily Issue Triage&lt;/strong&gt; - goes through untriaged issues, sets types, applies labels, spots duplicates, and leaves a tidy triage report for me.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Agentic Maintenance&lt;/strong&gt; - keeps the whole setup ticking along.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Each one is just a markdown file in &lt;code&gt;.github/workflows/&lt;/code&gt; with a description, a schedule, some &lt;code&gt;safe-outputs&lt;/code&gt; limits (things like &amp;quot;you may open at most 4 PRs per run, and they must be drafts&amp;quot;) and a big prompt describing how to behave. That's it. The nice part is that the guardrails are declarative, so the agent can't merge its own PRs, it can't touch protected files, and it can only comment so many times per run. All of that is baked in.&lt;/p&gt;
&lt;h2 id="how-it-actually-works-day-to-day"&gt;How it actually works day to day&lt;/h2&gt;
&lt;p&gt;The bit that makes this more than a gimmick is the persistent memory. Every run, the Perf Improver reads its own notes - which build/test/benchmark commands it validated, what's on its optimisation backlog, what it worked on last time, and which suggestions I've already ticked off. Then it does a couple of tasks in a round-robin fashion so it's not endlessly poking at the same corner of the codebase.&lt;/p&gt;
&lt;p&gt;It also keeps a single rolling &amp;quot;Monthly Activity&amp;quot; issue open with a checklist of what needs my attention. So my side of it is pretty simple: the agent opens a draft PR with measured before/after numbers, I read it and run CI, and if I'm happy with it I merge. That's the whole loop. I'm the reviewer, the agent is the workhorse doing the grind I never had time for.&lt;/p&gt;
&lt;h2 id="so-what-did-they-actually-get-done"&gt;So what did they actually get done?&lt;/h2&gt;
&lt;p&gt;Over roughly the last two months, here's what these workflows actually got up to on Examine:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;124&lt;/strong&gt; successful Perf Improver runs and &lt;strong&gt;9&lt;/strong&gt; successful Efficiency Improver runs&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;20&lt;/strong&gt; successful Daily Issue Triage runs quietly keeping the issue tracker tidy&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;24&lt;/strong&gt; performance/efficiency PRs reviewed and merged (13 from Perf Improver, 11 from Efficiency Improver) between late May and the end of June&lt;/li&gt;
&lt;li&gt;And the bit that actually matters - &lt;strong&gt;three releases shipped&lt;/strong&gt; off this work: &lt;strong&gt;v3.8.0&lt;/strong&gt;, &lt;strong&gt;v3.9.0&lt;/strong&gt; and the &lt;strong&gt;v4.0.0-beta.7&lt;/strong&gt; pre-release&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;That last point is really the whole thing. This isn't a pile of speculative branches rotting in a fork somewhere - it's code that went through my review, passed CI, and is now sitting in NuGet packages that real Umbraco sites are running.&lt;/p&gt;
&lt;h2 id="show-me-the-numbers"&gt;Show me the numbers&lt;/h2&gt;
&lt;p&gt;Right, this is the part I actually get excited about. One of my favourite things about the Perf Improver is that its prompt tells it to only attempt improvements it can actually measure - establish a baseline first, make the change, then measure again and document both numbers. For the algorithmic hot-path stuff that means benchmarks, so along the way it built out a proper &lt;a href="https://benchmarkdotnet.org/"&gt;BenchmarkDotNet&lt;/a&gt; suite that compares the current source against the published NuGet packages (3.0.1 through 3.3.0), which means I can show you real, reproducible deltas instead of hand waving.&lt;/p&gt;
&lt;h3 id="the-full-text-search-hot-path"&gt;The full-text search hot path&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;ManagedQuery&lt;/code&gt; is &lt;em&gt;the&lt;/em&gt; primary full-text search entry point in Examine - it's what runs on basically every search. The agent noticed it had no dedicated benchmark, wrote one, and then stacked up a series of small, individually measured changes: a volatile factory cache in &lt;code&gt;SearchContext.GetFieldValueType&lt;/code&gt;, an early return in the extract-terms check, and killing off some redundant &lt;code&gt;ConcurrentDictionary&lt;/code&gt; lookups in &lt;code&gt;AddDocument&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Here's the current source vs the most recent 3.3.0 release, on a 1,000-document index:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;ManagedQueryAllFields&lt;/td&gt;
&lt;td&gt;3.3.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;11.42 ms&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,323 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;ManagedQueryAllFields&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;2.17 ms&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;371 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That's about &lt;strong&gt;5.3x faster&lt;/strong&gt; and roughly &lt;strong&gt;3.6x less memory allocated&lt;/strong&gt; on the single most travelled code path in the whole library. On the busiest thing Examine does. I'll happily take that.&lt;/p&gt;
&lt;h3 id="building-queries"&gt;Building queries&lt;/h3&gt;
&lt;p&gt;&lt;code&gt;GroupedAnd&lt;/code&gt; / &lt;code&gt;GroupedOr&lt;/code&gt; / &lt;code&gt;GroupedNot&lt;/code&gt; are the workhorses of the query builder. A little &lt;code&gt;string[]&lt;/code&gt; fast-path (skipping a defensive &lt;code&gt;.ToArray()&lt;/code&gt; copy when the caller already handed it a &lt;code&gt;string[]&lt;/code&gt;) plus some allocation trimming got this:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;CreateQueryOnly&lt;/td&gt;
&lt;td&gt;3.3.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;3,995 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;8.34 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;CreateQueryOnly&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;319 ns&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;2.20 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GroupedAndStringArray&lt;/td&gt;
&lt;td&gt;3.3.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;21,377 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;21.10 KB&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;GroupedAndStringArray&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;16,659 ns&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;14.34 KB&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;The &lt;code&gt;CreateQuery()&lt;/code&gt; baseline dropping from 8.34 KB down to 2.2 KB is a lovely little win, and the grouped clauses are about 25% faster with a third of the allocation shaved off.&lt;/p&gt;
&lt;h3 id="constructing-a-valueset"&gt;Constructing a ValueSet&lt;/h3&gt;
&lt;p&gt;Every single document you index goes through a &lt;code&gt;ValueSet&lt;/code&gt; constructor. The old path allocated an intermediate dictionary and a generator state machine per field, which is exactly the kind of thing you don't notice until you're bulk indexing a big site. The agent got rid of both:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Method&lt;/th&gt;
&lt;th&gt;Version&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Mean&lt;/th&gt;
&lt;th style="text-align: right;"&gt;Allocated&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;FromDictionary5Fields&lt;/td&gt;
&lt;td&gt;3.3.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;1,183 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;2,200 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FromDictionary5Fields&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;226 ns&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;592 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FromDictionary20Fields&lt;/td&gt;
&lt;td&gt;3.3.0&lt;/td&gt;
&lt;td style="text-align: right;"&gt;4,007 ns&lt;/td&gt;
&lt;td style="text-align: right;"&gt;6,544 B&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;FromDictionary20Fields&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;Source&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;654 ns&lt;/strong&gt;&lt;/td&gt;
&lt;td style="text-align: right;"&gt;&lt;strong&gt;1,520 B&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;That's roughly &lt;strong&gt;5-6x faster&lt;/strong&gt; and about &lt;strong&gt;4x less allocated&lt;/strong&gt; on indexing, and when you're rebuilding the index on a large Umbraco site that adds up fast.&lt;/p&gt;
&lt;p&gt;The really nice touch is that these benchmark result tables now live in &lt;code&gt;&amp;lt;remarks&amp;gt;&lt;/code&gt; doc-comments right next to the benchmark code, so the numbers are versioned in the repo alongside the thing they measure. The agent did that bit too.&lt;/p&gt;
&lt;h2 id="was-it-actually-worth-it"&gt;Was it actually worth it?&lt;/h2&gt;
&lt;p&gt;For me the thing that makes it work is the measurement. Everything comes to me as a small, focused, draft PR with the numbers attached, so I can look at it, sanity check it, run CI, and decide in a couple of minutes. It's not zero effort - I still read every change before it goes anywhere near main - but the ratio is fantastic. I'm getting a steady stream of well-measured, single-purpose performance PRs on a library I care about, on paths I'd genuinely never have found the time to optimise by hand, and I've shipped real releases because of it. For a project I maintain around everything else, that's a pretty great deal.&lt;/p&gt;
&lt;p&gt;If you maintain a repo with a backlog you never get to, especially performance work that needs benchmarks to justify it, I'd recommend giving &lt;a href="https://github.com/githubnext/agentics"&gt;GitHub Next's agentics&lt;/a&gt; a go.&lt;/p&gt;
&lt;p&gt;You can see all of it out in the open on the &lt;a href="https://github.com/Shazwazza/Examine"&gt;Examine repo&lt;/a&gt; - the &lt;code&gt;[perf-improver]&lt;/code&gt; and &lt;code&gt;[efficiency-improver]&lt;/code&gt; PRs, the benchmark suite, and the releases they fed into. And there's a nice bonus here for me too: &lt;a href="https://examinex.online/"&gt;ExamineX&lt;/a&gt;, my managed, cloud-hosted Examine search offering, runs on this exact same internal plumbing. So all this work the agents have been doing to tighten up Examine's query and indexing hot paths feeds straight through into ExamineX - the underlying engine gets faster and leaner, and every ExamineX site gets those wins for free without changing a thing. If you'd rather have your search running as a managed service instead of hosting Lucene indexes on your own servers, that's what ExamineX is there for. 🙂&lt;/p&gt;
</description>
      <pubDate>Fri, 03 Jul 2026 17:45:20 Z</pubDate>
      <a10:updated>2026-07-03T17:45:20Z</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">1334</guid>
      <link>https://shazwazza.com/post/an-examine-fix-for-umbraco-index-corruption/</link>
      <category>Examine</category>
      <title>An Examine fix for Umbraco index corruption</title>
      <description>&lt;p&gt;A new Examine version 3.3.0 has been released to address a long awaited &lt;a href="https://github.com/umbraco/Umbraco-CMS/issues/16163"&gt;bug fix for Umbraco websites&lt;/a&gt; that use the &lt;a href="https://github.com/Shazwazza/Examine/blob/release/3.0/src/Examine.Lucene/Directories/SyncedFileSystemDirectoryFactory.cs"&gt;&lt;code&gt;SyncedFileSystemDirectoryFactory&lt;/code&gt;&lt;/a&gt; which is the default setting for Umbraco CMS.&lt;/p&gt;
&lt;p&gt;The bug typically means that indexes cannot be used and log entries such as &lt;code&gt;Lucene.Net.Index.CorruptIndexException: invalid deletion count: 2 vs docCount=1&lt;/code&gt; are present.&lt;/p&gt;
&lt;h2 id="understanding-the-problem"&gt;Understanding the problem:&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;SyncedFileSystemDirectoryFactory&lt;/code&gt; directory is there to avoid performance implications of rebuilding indexes on startup when a site is moved to another worker in Azure. The reason an index rebuild would occur is because in Azure, Lucene files need to work off of the local 'fast drive' (C:\%temp%), not the default/shared network 'slow drive' (D:), and whenever a site is moved, or spawned on a new worker in Azure, the local 'fast drive' is empty, meaning no indexes exist. The &lt;code&gt;SyncedFileSystemDirectoryFactory&lt;/code&gt; attempts to work around this challenge by continually synchronizing a copy of the indexes from the 'fast drive' to the 'slow drive' so that when a site is moved to another worker, it can sync (restore) from the 'slow drive' back to the 'fast drive' in order to avoid the index rebuild overhead.&lt;/p&gt;
&lt;p&gt;The problem with &lt;code&gt;SyncedFileSystemDirectoryFactory&lt;/code&gt; is that this implementation doesn't take into account what happens if the index files in your main storage ('slow drive') become corrupted which can happen for a number of reasons - misconfiguration, network latency, process termination, etc...&lt;/p&gt;
&lt;h2 id="understanding-the-solution"&gt;Understanding the solution:&lt;/h2&gt;
&lt;p&gt;The &lt;code&gt;SyncedFileSystemDirectoryFactory&lt;/code&gt; has been updated to:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Check the health of the main index if it exists ('slow drive').&lt;/li&gt;
&lt;li&gt;Check the health of the local index if it exists ('fast drive').&lt;/li&gt;
&lt;li&gt;If the main index is unhealthy or doesn't exist and the local index is healthy, it will synchronize the local index to the main index. This can occur only if a site hasn't moved to a new worker.&lt;/li&gt;
&lt;li&gt;If the main index is unhealthy and the local index doesn't exist or is unhealthy, then it will delete the main (corrupted) index.&lt;/li&gt;
&lt;li&gt;Once health checks are done, the index from main is always synced to local. If the main index was deleted due to corruption, this will mean that the local index is empty and an index rebuild will occur.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This change will attempt to keep any healthy index that is available (main vs local), but if nothing can be read, the indexes will be deleted and an &lt;strong&gt;index rebuild will occur&lt;/strong&gt;.&lt;/p&gt;
&lt;p&gt;There's also a new option to fix a corrupted index but this is not enabled by default since it can mean a loss of documents.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;a href="https://github.com/Shazwazza/Examine/blob/release/3.0/src/Examine.Lucene/Directories/SyncedFileSystemDirectoryFactory.cs"&gt;Source code&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://github.com/Shazwazza/Examine/blob/release/3.0/src/Examine.Test/Examine.Lucene/Directories/SyncedFileSystemDirectoryFactoryTests.cs"&gt;Tests&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;
&lt;h2 id="understanding-the-rebuilding-overhead"&gt;Understanding the rebuilding overhead&lt;/h2&gt;
&lt;p&gt;The performance overhead of index rebuilding is due to the Umbraco database queries that need to be executed in order to populate the indexes. The only reason &lt;code&gt;SyncedFileSystemDirectoryFactory&lt;/code&gt; exists is to prevent this overhead when hosting in &lt;a href="https://azure.microsoft.com/en-us/products/app-service"&gt;Azure App Service&lt;/a&gt; (which is what &lt;a href="https://umbraco.com/products/umbraco-cloud/"&gt;Umbraco Cloud&lt;/a&gt; uses), and it can only be used on your Umbraco primary node. It does not prevent index rebuilding overhead for non-primary nodes when load balancing or scaling out because the main network 'slow drive' is shared between all workers and an index can only be read/written to be a single process.&lt;/p&gt;
&lt;p&gt;This means that it's only useful if you are hosting in Azure App Service without any load balancing while keeping in mind that it does not always prevent index rebuilds (see above).&lt;/p&gt;
&lt;p&gt;The index rebuilding overhead can be dramatic when load balancing or scaling out, for example: If you scale out to +5 nodes in a load balancing setup, that means that 5x nodes will be performing index rebuilds around the same time, this means that your DB is going to get hammered by queries to build all of those new indexes. The performance hit isn't the index building - it is the DB queries and this can lead to DB locks and lead to the dreaded SQL Lock Timeout issue in the Umbraco back office. Plus, if search is critical to your front-end, than for a while after your site has started up, there won't be any index which means there won't be any search until the background processing is done.&lt;/p&gt;
&lt;p&gt;... Many of these reasons is why &lt;a href="https://examinex.online/"&gt;ExamineX&lt;/a&gt; was created:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Reliably and centrally persisted indexes means nothing is out of sync between nodes.&lt;/li&gt;
&lt;li&gt;No index rebuilding when your site is moved or scaled = no rebuilding overhead.&lt;/li&gt;
&lt;li&gt;Prevents SQL Timeout locks due to DB rebuilding queries.&lt;/li&gt;
&lt;li&gt;Very easy to setup and seamlessly changes your Lucene based indexes to &lt;a href="https://azure.microsoft.com/en-us/products/ai-services/ai-search"&gt;Azure&lt;/a&gt;/&lt;a href="https://www.elastic.co/"&gt;Elastic&lt;/a&gt; search indexes.&lt;/li&gt;
&lt;li&gt;Ideal when hosting Umbraco on Azure Web Apps (or Umbraco Cloud) and a perfect solution for load balancing and scaling.&lt;/li&gt;
&lt;li&gt;Automatically index Umbraco media file content without the need for additional indexes with support for PDFs, Microsoft Office documents and more.&lt;/li&gt;
&lt;li&gt;&lt;em&gt;(coming soon)&lt;/em&gt; Automatically generate Umbraco media image descriptions, tags, locations, and more using AI allowing your editors to quickly find the media/images they need.&lt;/li&gt;
&lt;/ul&gt;
</description>
      <pubDate>Wed, 31 Jul 2024 17:49:54 Z</pubDate>
      <a10:updated>2024-07-31T17:49:54Z</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">1332</guid>
      <link>https://shazwazza.com/post/configuring-a-suggester-with-examinex-and-azure-ai-search/</link>
      <category>Examine</category>
      <title>Configuring a Suggester with ExamineX and Azure AI Search</title>
      <description>&lt;p&gt;We recently had a customer ask about integrating the &lt;a href="https://learn.microsoft.com/en-us/azure/search/index-add-suggesters"&gt;Azure AI Search suggester&lt;/a&gt; API with &lt;a href="https://examinex.online/"&gt;ExamineX&lt;/a&gt; and this turns out to be quite straight forward :)&lt;/p&gt;
&lt;p&gt;First thing is to ensure that the fields you want to use the Suggester for are configured correctly:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;// Configuration for the External Index for the City and Country fields.
// The Azure AI Search Suggester requires fields to be 'string fields only'
// and using the Standard Analyzer (default on the External Index)
services.Configure&amp;lt;AzureSearchIndexOptions&amp;gt;(
    Constants.UmbracoIndexes.ExternalIndexName,
    options =&amp;gt;
    {
         options.FieldDefinitions = new FieldDefinitionCollection(
            new FieldDefinition(&amp;quot;City&amp;quot;, AzureSearchFieldDefinitionTypes.FullText),
            new FieldDefinition(&amp;quot;Country&amp;quot;, AzureSearchFieldDefinitionTypes.FulText));
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Next up, is to create the Suggester which is done as part of the &lt;a href="https://examinex.online/customization#events"&gt;CreatingOrUpdatingIndex&lt;/a&gt; event:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;externalIndex.CreatingOrUpdatingIndex += AzureIndex_CreatingOrUpdatingIndex_ScoringProfile;

private void AzureIndex_CreatingOrUpdatingIndex_Suggester(object sender, CreatingOrUpdatingIndexEventArgs e)
{
    switch (e.EventType)
    {
        case IndexModifiedEventType.Creating:
        case IndexModifiedEventType.Rebuilding:
            var index = e.AzureSearchIndexDefinition;

            var suggester = new SearchSuggester(
                &amp;quot;sg&amp;quot;,
                new[] { &amp;quot;Country&amp;quot;, &amp;quot;City&amp;quot; });

            index.Suggesters.Add(suggester);

            break;
    }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;When data has been indexed for these fields, using the Suggester API is simple. For example, lets assume that the following ValueSets were indexed:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;externalIndex.IndexItems(new[]
{
    ValueSet.FromObject(id1, new { City = &amp;quot;Calgary&amp;quot;, Country = &amp;quot;Canada&amp;quot; }),
    ValueSet.FromObject(id2, new { City = &amp;quot;Sydney&amp;quot;, Country = &amp;quot;Australia&amp;quot; }),
    ValueSet.FromObject(id3, new { City = &amp;quot;Copenhagen&amp;quot;, Country = &amp;quot;Denmark&amp;quot; }),
    ValueSet.FromObject(id4, new { City = &amp;quot;Vienna&amp;quot;, Country = &amp;quot;Austria&amp;quot; }),
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Then to get suggestions for &amp;quot;Au&amp;quot;:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;// cast to AzureSearchIndex to expose underlying Azure Search APIs
var azureSearchIndex = (AzureSearchIndex)externalIndex;
var result = await  azureSearchIndex.IndexClient.SuggestAsync&amp;lt;ExamineDocument&amp;gt;(
    &amp;quot;Au&amp;quot;,
    &amp;quot;sg&amp;quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The result will contain 2x entries, one for the Australia and one for the Austria documents. Super easy!&lt;/p&gt;
&lt;p&gt;One thing to watch out for is if your index is not configured to use the Standard Analyzer by default (i.e. like the Internal Index in Umbraco). In that case, you'll need to define a custom field type with that indexer:&lt;/p&gt;
&lt;pre&gt;&lt;code class="language-cs"&gt;services.Configure&amp;lt;AzureSearchIndexOptions&amp;gt;(
    Constants.UmbracoIndexes.ExternalIndexName,
    options =&amp;gt;
    {
        // This adds a custom field type that uses the Standard Analyzer
        options.IndexValueTypesFactory = new Dictionary&amp;lt;string, IAzureSearchFieldValueTypeFactory&amp;gt;
        {
            [&amp;quot;standard&amp;quot;] = new AzureSearchFieldValueTypeFactory(s =&amp;gt;
                new AzureSearchFieldValueType(s, SearchFieldDataType.String, LexicalAnalyzerName.StandardLucene.ToString()))
        };

        // Set these field types to the one defined above
        options.FieldDefinitions = new FieldDefinitionCollection(
            new FieldDefinition(&amp;quot;City&amp;quot;, &amp;quot;standard&amp;quot;),
            new FieldDefinition(&amp;quot;Country&amp;quot;, &amp;quot;standard&amp;quot;));
    });
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Happy Searching!&lt;/p&gt;
</description>
      <pubDate>Wed, 01 May 2024 14:51:52 Z</pubDate>
      <a10:updated>2024-05-01T14:51:52Z</a10:updated>
    </item>
    <item>
      <guid isPermaLink="false">1248</guid>
      <link>https://shazwazza.com/post/searching-with-ipublishedcontentquery-in-umbraco/</link>
      <category>Umbraco</category>
      <category>Examine</category>
      <title>Searching with IPublishedContentQuery in Umbraco</title>
      <description>&lt;p&gt;I recently realized that I don’t think Umbraco’s APIs on &lt;em&gt;IPublishedContentQuery&lt;/em&gt; are documented so hopefully this post may inspire some docs to be written or at least guide some folks on some functionality they may not know about.&lt;/p&gt;
&lt;p&gt;A long while back even in Umbraco v7 &lt;em&gt;UmbracoHelper&lt;/em&gt; was split into different components and &lt;em&gt;UmbracoHelper&lt;/em&gt; just wrapped these. One of these components was called &lt;em&gt;ITypedPublishedContentQuery&lt;/em&gt; and in v8 is now called &lt;em&gt;IPublishedContentQuery&lt;/em&gt;, and this component is responsible for executing queries for content and media on the front-end in razor templates. In v8 a lot of methods were removed or obsoleted from &lt;em&gt;UmbracoHelper&lt;/em&gt; so that it wasn’t one gigantic object and tries to steer developers to use these sub components directly instead. For example if you try to access &lt;em&gt;UmbracoHelper.ContentQuery&lt;/em&gt; you’ll see that has been deprecated saying:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;Inject and use an instance of IPublishedContentQuery in the constructor for using it in classes or get it from Current.PublishedContentQuery in views&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;and the &lt;em&gt;UmbracoHelper.Search&lt;/em&gt; methods from v7 have been removed and now only exist on &lt;em&gt;IPublishedContentQuery&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;There are &lt;a rel="noopener" href="https://our.umbraco.com/apidocs/v8/csharp/api/Umbraco.Web.IPublishedContentQuery.html" target="_blank"&gt;API docs for IPublishedContentQuery&lt;/a&gt; which are a bit helpful, at least will tell you what all available methods and parameters are. The main one’s I wanted to point out are the &lt;strong&gt;Search&lt;/strong&gt; methods.&lt;/p&gt;
&lt;h2&gt;Strongly typed search responses&lt;/h2&gt;
&lt;p&gt;When you use Examine directly to search you will get an Examine &lt;em&gt;ISearchResults&lt;/em&gt; object back which is more or less raw data. It’s possible to work with that data but most people want to work with some strongly typed data and at the very least in Umbraco with &lt;em&gt;IPublishedContent&lt;/em&gt;. That is pretty much what &lt;a rel="noopener" href="https://our.umbraco.com/apidocs/v8/csharp/api/Umbraco.Web.IPublishedContentQuery.html#Umbraco_Web_IPublishedContentQuery_Search_System_String_System_String_System_String_" target="_blank"&gt;IPublishedContentQuery.Search&lt;/a&gt; methods are solving. Each of these methods will return an &lt;em&gt;IEnumerable&amp;lt;PublishedSearchResult&amp;gt;&lt;/em&gt; and each &lt;a rel="noopener" href="https://our.umbraco.com/apidocs/v8/csharp/api/Umbraco.Core.Models.PublishedContent.PublishedSearchResult.html" target="_blank"&gt;PublishedSearchResult&lt;/a&gt; contains an &lt;em&gt;IPublishedContent&lt;/em&gt; instance along with a &lt;em&gt;Score&lt;/em&gt; value. A quick example in razor:&lt;/p&gt;
&lt;pre&gt;&lt;code class="lang-csharp"&gt;@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using Current = Umbraco.Web.Composing.Current;
@{
    var search = Current.PublishedContentQuery.Search(Request.QueryString["query"]);
}

&amp;lt;div&amp;gt;
    &amp;lt;h3&amp;gt;Search Results&amp;lt;/h3&amp;gt;
    &amp;lt;ul&amp;gt;
        @foreach (var result in search)
        {
            &amp;lt;li&amp;gt;
                Id: @result.Content.Id
                &amp;lt;br/&amp;gt;
                Name: @result.Content.Name
                &amp;lt;br /&amp;gt;
                Score: @result.Score
            &amp;lt;/li&amp;gt;
        }
    &amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The ordering of this search is by Score so the highest score is first. This makes searching very easy while the underlying mechanism is still Examine. The &lt;em&gt;IPublishedContentQuery.Search &lt;/em&gt;methods make working with the results a bit nicer.&lt;/p&gt;
&lt;h2&gt;Paging results&lt;/h2&gt;
&lt;p&gt;You may have noticed that there’s a few overloads and optional parameters to these search methods too. 2 of the overloads support paging parameters and these take care of all of the quirks with Lucene paging for you. I wrote &lt;a rel="noopener" href="/post/paging-with-examine/" target="_blank"&gt;a previous post about paging with Examine&lt;/a&gt; and you need to make sure you do that correctly else you’ll end up iterating over possibly tons of search results which can have performance problems. To expand on the above example with paging is super easy:&lt;/p&gt;
&lt;pre&gt;&lt;code class="lang-csharp"&gt;@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using Current = Umbraco.Web.Composing.Current;
@{
    var pageSize = 10;
    var pageIndex = int.Parse(Request.QueryString["page"]);
    var search = Current.PublishedContentQuery.Search(
        Request.QueryString["query"],
        pageIndex * pageSize,   // skip
        pageSize,               // take
        out var totalRecords);
}

&amp;lt;div&amp;gt;
    &amp;lt;h3&amp;gt;Search Results&amp;lt;/h3&amp;gt;
    &amp;lt;ul&amp;gt;
        @foreach (var result in search)
        {
            &amp;lt;li&amp;gt;
                Id: @result.Content.Id
                &amp;lt;br/&amp;gt;
                Name: @result.Content.Name
                &amp;lt;br /&amp;gt;
                Score: @result.Score
            &amp;lt;/li&amp;gt;
        }
    &amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;&lt;/code&gt;&lt;/pre&gt;
&lt;h2&gt;Simple search with cultures&lt;/h2&gt;
&lt;p&gt;Another optional parameter you might have noticed is the culture parameter. The docs state this about the culture parameter:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;When the &lt;code&gt;culture&lt;/code&gt; is not specified or is *, all cultures are searched. To search for only invariant documents and fields use null. When searching on a specific culture, all culture specific fields are searched for the provided culture and all invariant fields for all documents. While enumerating results, the ambient culture is changed to be the searched culture.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;What this is saying is that if you aren’t using culture variants in Umbraco then don’t worry about it. But if you are, you will also generally not have to worry about it either! What?! By default the simple Search method will use the “ambient” (aka ‘Current’) culture to search and return data. So if you are currently browsing your “fr-FR” culture site this method will automatically only search for your data in your French culture but will also search on any invariant (non-culture) data. And as a bonus, the IPublishedContent returned also uses this ambient culture so any values you retrieve from the content item without specifying the culture will just be the ambient/default culture.&lt;/p&gt;
&lt;p&gt;So why is there a “culture” parameter? It’s just there in case you want to search on a specific culture instead of relying on the ambient/current one.&lt;/p&gt;
&lt;h2&gt;Search with IQueryExecutor&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;IQueryExecutor &lt;/em&gt;is the resulting object created when creating a query with the Examine fluent API. This means you can build up any complex Examine query you want, even with raw Lucene, and then pass this query to one of the &lt;em&gt;IPublishedContentQuery.Search&lt;/em&gt; overloads and you’ll get all the goodness of the above queries. There’s also paging overloads with &lt;em&gt;IQueryExecutor&lt;/em&gt; too. To further expand on the above example:&lt;/p&gt;
&lt;pre&gt;&lt;code class="lang-csharp"&gt;@inherits Umbraco.Web.Mvc.UmbracoViewPage
@using Current = Umbraco.Web.Composing.Current;
@{
    // Get the external index with error checking
    if (ExamineManager.Instance.TryGetIndex(
        Constants.UmbracoIndexes.ExternalIndexName, out var index))
    {
        throw new InvalidOperationException(
            $"No index found with name {Constants.UmbracoIndexes.ExternalIndexName}");
    }

    // build an Examine query
    var query = index.GetSearcher().CreateQuery()
        .GroupedOr(new [] { "pageTitle", "pageContent"},
            Request.QueryString["query"].MultipleCharacterWildcard());


    var pageSize = 10;
    var pageIndex = int.Parse(Request.QueryString["page"]);
    var search = Current.PublishedContentQuery.Search(
        query,                  // pass the examine query in!
        pageIndex * pageSize,   // skip
        pageSize,               // take
        out var totalRecords);
}

&amp;lt;div&amp;gt;
    &amp;lt;h3&amp;gt;Search Results&amp;lt;/h3&amp;gt;
    &amp;lt;ul&amp;gt;
        @foreach (var result in search)
        {
            &amp;lt;li&amp;gt;
                Id: @result.Content.Id
                &amp;lt;br/&amp;gt;
                Name: @result.Content.Name
                &amp;lt;br /&amp;gt;
                Score: @result.Score
            &amp;lt;/li&amp;gt;
        }
    &amp;lt;/ul&amp;gt;
&amp;lt;/div&amp;gt;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The base interface of the fluent parts of Examine’s queries are &lt;em&gt;IQueryExecutor&lt;/em&gt; so you can just pass in your query to the method and it will work.&lt;/p&gt;
&lt;h2&gt;Recap&lt;/h2&gt;
&lt;p&gt;The &lt;a rel="noopener" href="https://our.umbraco.com/apidocs/v8/csharp/api/Umbraco.Web.IPublishedContentQuery.html#Umbraco_Web_IPublishedContentQuery_Search_IQueryExecutor_" target="_blank"&gt;IPublishedContentQuery.Search overloads are listed in the API docs&lt;/a&gt;, they are:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Search(String term, String culture, String indexName)&lt;/li&gt;
&lt;li&gt;Search(String term, Int32 skip, Int32 take, out Int64 totalRecords, String culture, String indexName)&lt;/li&gt;
&lt;li&gt;Search(IQueryExecutor query)&lt;/li&gt;
&lt;li&gt;Search(IQueryExecutor query, Int32 skip, Int32 take, out Int64 totalRecords)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Should you always use this instead of using Examine directly? As always it just depends on what you are doing. If you need a ton of flexibility with your search results than maybe you want to use Examine’s search results directly but if you want simple and quick access to IPublishedContent results, then these methods will work great.&lt;/p&gt;
&lt;p&gt;Does this all work with &lt;a rel="noopener" href="https://examinex.online/" target="_blank"&gt;ExamineX&lt;/a&gt; ? Absolutely!! One of the best parts of ExamineX is that it’s completely seamless. ExamineX is just an index implementation of Examine itself so all Examine APIs and therefore all Umbraco APIs that use Examine will ‘just work’.&lt;/p&gt;</description>
      <pubDate>Thu, 23 Mar 2023 15:10:02 Z</pubDate>
      <a10:updated>2023-03-23T15:10:02Z</a10:updated>
    </item>
  </channel>
</rss>