Heratio Help Center article. Category: Technical / AI & Automation.

Fuzzy Search - Technical Documentation

Plugin: ahgDisplayPlugin Version: 3.2.23+ Last Updated: February 2026


1. Architecture Overview

FUZZYSEARCHARCHITECTURELAYER1:SPELLCORRECTIONFuzzySearchService.phpLevenshteinSOUNDEXMetaphone(editdist)(phonetic)(phonetic)VocabularySources:•display_facet_cache(616terms)•ahg_thesaurus_term(2,946terms,try/catch)•term_i18n(taxonomy35/42/78)•actor_i18n(creatornames)LAYER2:FULLTEXTSEARCHMySQLFULLTEXTindexesoni18ntablesft_ioi_titleft_ioi_scopeft_ai_name(title)(scope_and_(authorized_content)form_of_name)ft_ti_nameFallsbacktoLIKE%term%ifindexesmissing(termname)LAYER3:ELASTICSEARCHFUZZYFALLBACKActivatedwhenSQLreturns0resultsUsesmulti_matchwithfuzziness:AUTOFieldssearched:•i18n.en.title(boost:3)•i18n.en.scopeAndContent(boost:1)•display.creator(boost:2)•autocomplete(boost:2)Returnsupto200matchingIDsrebuildsSQLquerywithwhereIn()

2. Files

New Files

File Purpose
ahgDisplayPlugin/lib/Services/FuzzySearchService.php Core spell-correction service
ahgDisplayPlugin/database/fulltext_indexes.sql FULLTEXT index definitions
ahgDisplayPlugin/lib/task/ahgAddFulltextIndexesTask.class.php CLI task to create indexes

Modified Files

File Changes
ahgDisplayPlugin/modules/display/actions/actions.class.php Fuzzy correction in executeBrowse(), FULLTEXT in applyTextSearchFilter(), ES fallback, new helper methods
ahgDisplayPlugin/modules/display/templates/browseSuccess.php "Did you mean?" and auto-correct alert banners

3. FuzzySearchService

Class Structure

namespace AhgDisplay\Services;

class FuzzySearchService
{
    private array $vocabulary = [];      // normalized => original
    private array $soundexIndex = [];    // soundex_code => [terms]
    private array $metaphoneIndex = [];  // metaphone_code => [terms]

    public function loadVocabulary(): void;
    public function correctQuery(string $query): array;
    private function findLevenshteinMatch(string $word): ?array;
    private function findPhoneticMatch(string $word): ?array;
    private function buildPhoneticIndexes(): void;
}

correctQuery() Return Value

[
    'original'    => string,          // Original query
    'corrected'   => string|null,     // Corrected query (null if no correction)
    'suggestion'  => string|null,     // Same as corrected
    'confidence'  => float,           // 0.0 - 1.0
    'corrections' => array,           // Per-word correction details
    'method'      => string|null      // 'levenshtein', 'soundex', 'metaphone', or null
]

Vocabulary Sources

VOCABULARYLOADING(inorder)1.display_facet_cache~616terms(subjects,places,genres,creators,levels)Alwaysavailableonanyinstallation2.ahg_thesaurus_term(try/catch)~2,946termsfromahgSemanticSearchPluginOnlyavailableifsemanticsearchpluginisinstalled3.term_i18nTermsfromtaxonomies35(subject),42(place),78(genre)CoreHeratioterms4.actor_i18nCreator/authoritynamesEnablesnamecorrectionTotal:~5,000termsPerformance:PHPlevenshtein()isC-implemented<5msperword

Levenshtein Thresholds

Word Length Max Edit Distance Example
1-5 chars 2 "musem" → "museum" (distance 1)
6+ chars 3 "archieves" → "archives" (distance 2)

Confidence Thresholds

Confidence Action
>= 0.9 Auto-correct (replace query, show "Showing results for X")
0.5 - 0.89 Suggest ("Did you mean: X?")
< 0.5 No suggestion shown

4. FULLTEXT Indexes

Index Definitions

CREATE FULLTEXT INDEX ft_ioi_title ON information_object_i18n(title);
CREATE FULLTEXT INDEX ft_ioi_scope ON information_object_i18n(scope_and_content);
CREATE FULLTEXT INDEX ft_ai_name ON actor_i18n(authorized_form_of_name);
CREATE FULLTEXT INDEX ft_ti_name ON term_i18n(name);

Installation

These four FULLTEXT indexes ship as part of the Heratio core schema (database/core/00_core_schema.sql) and are created when the database is installed, so no separate command is required. To add them to an existing database, run the CREATE FULLTEXT INDEX statements above - MySQL skips any index that already exists.

Behavior

  • With indexes: Uses MATCH(column) AGAINST(? IN NATURAL LANGUAGE MODE) for relevance-ranked results
  • Without indexes: Falls back to LIKE %term% (exact substring matching)
  • Detection is cached per request via isFulltextAvailable() static property

5. Elasticsearch Fuzzy Fallback

Trigger Condition

ES fuzzy is called only when:

  1. The primary SQL query returns 0 results
  2. A search query exists
  3. SearchEngineFactory class is available

Query Structure

{
  "query": {
    "multi_match": {
      "query": "archieves",
      "fields": [
        "i18n.en.title^3",
        "i18n.en.scopeAndContent",
        "display.creator^2",
        "autocomplete^2"
      ],
      "fuzziness": "AUTO",
      "type": "best_fields"
    }
  },
  "size": 200,
  "_source": false
}

Fuzziness AUTO Behavior

Term Length Max Edits Allowed
1-2 chars Exact match only
3-5 chars 1 edit
6+ chars 2 edits

6. Request Flow

executeBrowse()1.Readqueryfromrequest:$this->queryFilter2.FuzzySearchService.correctQuery($this->queryFilter)loadVocabulary()Foreachword:findLevenshteinMatch()findPhoneticMatch()(ifnoLevenshteinmatch)confidence=0.9auto-correct($this->queryFilter=corrected)confidence0.9set$this->didYouMean3.applyTextSearchFilter($this->queryFilter)isFulltextAvailable()?YESMATCH...AGAINST(naturallanguagemode)NOLIKE%term%IdentifierfieldalwaysusesLIKE4.Executequery,get$this->total5.If$this->total===0tryElasticsearchFuzzy()class_exists('SearchEngineFactory')?YESmulti_matchwithfuzziness:AUTOgetIDsRebuildquerywithwhereIn('io.id',$esIds)NOskip(noESavailable)Allwrappedintry/catch(ESfailureneverbreaksbrowse)6.Templaterendersalerts:$this->didYouMean"Didyoumean:X?"infoalert$this->correctedQuery"ShowingresultsforX"successalert$this->esAssistedSearch"Fuzzymatches"warningalert

7. Template Alerts

Three alert types in browseSuccess.php (no <script> or <style> tags, no CSP nonce needed):

Variable Alert Type Content
$didYouMean alert-info "Did you mean: [link]?"
$correctedQuery alert-success "Showing results for X. Search instead for: [link]"
$esAssistedSearch alert-warning "No exact matches. Showing fuzzy matches from search index."

The "Search instead for" link appends &noCorrect=1 to bypass correction.


8. Graceful Degradation

ScenarioBehaviorAlllayersavailableFullcorrectionFULLTEXTESfallbackESnotrunningLevenshteinFULLTEXTstillworkNoFULLTEXTindexesFallsbacktoLIKE,LevenshteinESworkNoahgSemanticSearchPluginVocabularyfromfacet_cacheterm_i18nAlllayersunavailableBehavesexactlylikepre-fuzzysearch(LIKE%term%)

Every layer is wrapped in try/catch. A failure in any layer never breaks the browse page.


9. FULLTEXT Index Setup

Create FULLTEXT Indexes

The four FULLTEXT indexes ship with the Heratio core schema (database/core/00_core_schema.sql) and are created at database install time. To (re)create them on an existing database, run the CREATE FULLTEXT INDEX statements from section 4. MySQL skips any index that already exists, and index creation is non-blocking on MySQL 8.0.12+.


10. Configuration

No configuration is needed. Fuzzy search activates automatically on the GLAM Browse page. The noCorrect URL parameter is the only user-facing control.


11. Dependencies

Dependency Required Purpose
MySQL 8 InnoDB Yes FULLTEXT indexes, Levenshtein vocabulary queries
Elasticsearch/OpenSearch No Fuzzy fallback (graceful degradation)
ahgSemanticSearchPlugin No Adds thesaurus vocabulary (graceful degradation)
PHP levenshtein() Yes Built-in PHP function (C implementation)
PHP soundex() Yes Built-in PHP function
PHP metaphone() Yes Built-in PHP function

Part of the Heratio AHG Framework - ahgDisplayPlugin