← Terug naar blog
AI Engineering
16 december 2025

LLM-gegenereerde zoekaliassen voor fuzzy attribuut-zoekopdrachten

Hoe je mensen kunt vinden op basis van velden die de gebruiker niet kent — door het LLM te laten beslissen welke synoniemen op welke databasequery mappen
Aron Heesakkers
Aron HeesakkersFullstack Developer & AI Engineer

Stel je hebt een medewerkerstabel met een attribuutveld talen dat waarden bevat als ['Nederlands', 'Engels', 'Spaans']. Een gebruiker vraagt: "wie is er meertalig?"

Het woord "meertalig" staat nergens in de database. Er is geen kolom meertalig, en de waarde "Spaans" bevat het woord niet. Een directe FTS- of vector-zoekopdracht op "meertalig" vindt niks.

Dit is het aliassing-probleem: de gebruiker gebruikt andere termen dan de data. En de termen variëren per organisatie — het ene bedrijf slaat vaardigheden op als skills, het andere als competenties, het derde als certificaten. Er is geen generieke mapping te hard-coderen.

De oplossing: laat het LLM eenmalig per attribuutveld de aliassen genereren, sla ze op, en injecteer ze in elke zoekopdracht als structurele hints.

Aliassen genereren

De generator ontvangt de veldnaam, het label, het type, en een set voorbeeldwaarden:

const userMessage = `
Veld: talen (label: "Talen", type: array)
Voorbeeldwaarden: Nederlands, Engels, Spaans, Frans, Duits
`;

Het LLM retourneert een JSON-array van alias-objecten:

[
  {
    "terms": ["meertalig", "meerdere talen", "tweetalig", "polyglot"],
    "constraint": {
      "groups": [
        {
          "required": [
            { "field": "talen", "operator": "contains", "value": "Engels" }
          ]
        },
        {
          "required": [
            { "field": "talen", "operator": "contains", "value": "Spaans" }
          ]
        }
      ],
      "logic": "OR"
    }
  },
  {
    "terms": ["spreekt Spaans", "Spaanstalig"],
    "constraint": {
      "required": [
        { "field": "talen", "operator": "contains", "value": "Spaans" }
      ]
    }
  }
]

Elke alias heeft een terms-array (de Nederlandse synoniemen die een gebruiker zou typen) en een constraint (de databasefilter die daarbij hoort). Het systeem prompt instrueert het LLM om alleen aliassen te genereren waarbij ze echt zinvol zijn — niet voor voor de hand liggende termen:

const SYSTEM_PROMPT = `Je bent een zoekassistent-configurator. Je krijgt informatie
over een attribuutveld en genereert semantische aliassen: natuurlijke zoektermen die
mensen gebruiken, plus de bijbehorende zoekopdracht.

Genereer alleen aliassen voor waarden waarbij een alias echt zinvol is — niet voor
voor de hand liggende termen die de AI zelf al zou afleiden uit de veldnaam.
Genereer maximaal 4 aliassen per veld.
Als er geen zinvolle aliassen zijn, geef dan een lege array terug.`;

De temperatuur staat op 0.2 — zo laag mogelijk voor consistente output, maar iets boven nul voor variatie in de formulering.

Robuust parsen

Het LLM retourneert soms JSON omgeven door een code-fence. We strippen dat vóór het parsen:

export function parseAliasResponse(raw: string): AliasHint[] {
  const cleaned = raw
    .replace(/^```(?:json)?\n?/, "")
    .replace(/\n?```$/, "")
    .trim();
  try {
    const parsed = JSON.parse(cleaned);
    if (!Array.isArray(parsed)) return [];
    return parsed.filter(
      (item): item is AliasHint =>
        Array.isArray(item.terms) &&
        item.terms.length > 0 &&
        typeof item.constraint === "object",
    );
  } catch {
    return [];
  }
}

Als het LLM iets onverwachts retourneert, is het resultaat een lege array — de zoekopdracht werkt dan zonder aliassen in plaats van dat hij crasht.

Aliassen opslaan in het veld-register

Het veld-register (people_field_registry) slaat per attribuutveld op: de sleutel, het label, het type, voorbeeldwaarden én de gegenereerde aliassen. De aliassen worden opgeslagen als JSONB naast het veld, zodat ze beschikbaar zijn bij elke zoekopdracht zonder opnieuw het LLM aan te roepen.

Aliassen worden gegenereerd op twee momenten: bij de eerste ontdekking van een nieuw attribuutveld (na een data-import), en handmatig via de admin-UI als de automatisch gegenereerde aliassen niet kloppen.

Aliassen injecteren als hints in de systeem-prompt

Bij elke zoekopdracht bouwen we een context-blok voor het LLM op basis van het veld-register:

export function buildFieldContext(fields: FieldRegistryEntry[]): string {
  // Deel 1: veldoverzicht met operator-hints
  const fieldLines = fields.map((f) => {
    const hint = fieldOperatorHint(f.field_type);
    // "array" → "contains", "number" → ">=/<=/equals", "boolean" → "equals (true/false)"
    const samples = f.sample_values.slice(0, 5).join(", ");
    return `- ${f.field_key} (${f.field_label}) [${f.field_type}, ${hint}]: bijv. ${samples}`;
  });

  // Deel 2: alias-sectie
  const aliasLines = fields.flatMap((f) =>
    parseAliasHints(f.alias_hints).map((hint) => {
      const termsStr = hint.terms.map((t) => `"${t}"`).join(" of ");
      const constraintStr = formatConstraint(hint.constraint);
      return `- ${termsStr} → ${constraintStr}`;
    }),
  );

  return `
Beschikbare attribuutvelden (gebruik deze exacte sleutels als veldnaam):
${fieldLines.join("\n")}

Semantische aliassen — als de query een van onderstaande termen bevat,
genereer dan de bijbehorende constraint:
${aliasLines.join("\n")}
`;
}

De alias-sectie in de systeem-prompt geeft het LLM expliciete instructies: "als de gebruiker 'meertalig' typt, gebruik dan dit specifieke filter". Dat is sterker dan hopen dat het LLM zelf de juiste kolom-naam en operator afleidt.

Het resultaat in de praktijk

Een query als "wie is er meertalig en werkt in Amsterdam?" wordt door het LLM vertaald naar:

{
  "groups": [
    {
      "required": [
        { "field": "talen", "operator": "contains", "value": "Engels" }
      ]
    },
    {
      "required": [
        { "field": "talen", "operator": "contains", "value": "Spaans" }
      ]
    }
  ],
  "logic": "OR",
  "required": [
    { "field": "locatie", "operator": "equals", "value": "Amsterdam" }
  ]
}

Zonder de alias-hint zou het LLM "meertalig" waarschijnlijk proberen te matchen op een niet-bestaand veld, of de query gewoon negeren. Met de hint weet het precies welke filter te bouwen.

Wanneer werkt dit, wanneer niet?

Dit patroon werkt goed als:

  • Attribuutvelden semi-gestructureerde data bevatten (lijsten, tekstvelden, enums)
  • Gebruikers conceptuele termen gebruiken die niet letterlijk in de data staan
  • Het domein beperkt genoeg is dat de LLM-aliassen accuraat en stabiel zijn

Het werkt minder goed als:

  • Attribuutvelden volledig vrije tekst zijn zonder gedeelde vocabulaire
  • De organisatie dezelfde veldnamen gebruikt voor verschillende concepten

De aliassen worden eenmalig gegenereerd en opgeslagen — niet bij elke zoekopdracht. De LLM-aanroep is een setupkost, geen per-query-kost. Vertel ons over je zoek-use case als je wilt uitzoeken of dit patroon past bij jouw data.


Project bespreken?

Heb je een use case waar je over twijfelt?

We praten graag over of een AI-aanpak past — en zeggen het eerlijk als het beter zonder kan. Vaste prijs, afgesproken scope.
Plan een gesprek
Probeer AI