# Metadata Management and Filtered Search

**Last updated:** 2026-04 | Verified: MCP 2026-04
**Status:** GA
**Category:** RAG Architecture & Semantic Search

---

## Introduksjon

Metadata management og filtrering er fundamentale byggeklosser for å skape presise og kontekstbevisste RAG-løsninger. I Azure AI Search utgjør OData-filtre og faceted navigation et kraftig rammeverk for å organisere, kategorisere og filtrere søkeresultater basert på metadata-egenskaper som kategori, dato, klassifikasjon, eller geografisk plassering.

Effektiv metadata-håndtering gjør det mulig å kombinere semantisk søk med strukturerte begrensninger — for eksempel å finne "kontrakter signert etter 2023 som gjelder GDPR-prosessering i Norge" eller "dokumenter klassifisert som 'konfidensielt' fra divisjon HR". Denne kombinasjonen av fri tekstsøk og strukturert filtrering er ofte kritisk for offentlig sektor, hvor compliance, tilgangskontroll og revisjonsspor er juridiske krav.

Azure AI Search støtter OData v4-syntaks for filtre, som integreres sømløst med både keyword search, vector search og hybrid search. Metadata-design påvirker både søkeytelse, brukeropplevelse (via faceted navigation), og evnen til å implementere security trimming og data governance.

## Kjernekomponenter

### Index Schema Design for Metadata

| Field Type | Egenskaper | Bruk i Metadata |
|------------|------------|-----------------|
| `Edm.String` | Filterable, Facetable, Sortable | Kategorier, tags, klassifikasjonsnivå |
| `Collection(Edm.String)` | Filterable, Facetable | Multi-value tags, key phrases, roller |
| `Edm.Int32/Int64` | Filterable, Facetable, Sortable | Rating, score, versjonsnummer |
| `Edm.DateTimeOffset` | Filterable, Facetable, Sortable | Opprettelsesdato, modifikasjonsdato, gyldighetsperiode |
| `Edm.Boolean` | Filterable, Facetable | IsPublic, IsArchived, RequiresApproval |
| `Edm.GeographyPoint` | Filterable (kun via `geo.distance`) | Geografisk lokasjon (ikke facetable) |

**Best Practices for Schema:**
- **Low Cardinality Fields**: Facets fungerer best på felt med få unike verdier (5-50 distinkte verdier).
- **Normalizers**: Bruk normalizers på facetable string-felt for å unngå duplikater pga. casing ("Norge" vs "norge").
- **Default Attributes (REST API)**: String, DateTime, Boolean, og numeric typer er filterable/facetable by default i REST API.
- **Security Metadata**: Inkluder alltid felt for `ownerPrincipalIds`, `classificationLevel`, `department` hvis security trimming er nødvendig.

### OData Filter Syntax

**Comparison Operators:**
```odata
$filter=Rating ge 4                           // Greater than or equal
$filter=Category eq 'Confidential'            // Equality
$filter=LastModified ge 2024-01-01T00:00:00Z  // Date filtering
```

**Logical Operators (precedence: not > and > or):**
```odata
$filter=Category eq 'Contract' and Status ne 'Archived'
$filter=(IsPublic eq true) or (Department eq 'HR' and OwnerRole eq 'Admin')
$filter=not (IsExpired eq true)
```

**Collection Filters (any/all):**
```odata
// Minimum ett tag matcher
$filter=Tags/any(tag: tag eq 'GDPR')

// Alle roller må være interne
$filter=ApproverRoles/all(role: role/Type eq 'Internal')

// Kombinasjon med search.in for effektivitet
$filter=Tags/any(tag: search.in(tag, 'GDPR,ISO27001,SOC2', ','))
```

**Geo-Spatial Filtering:**
```odata
// Innenfor 50 km fra Oslo
$filter=geo.distance(Location, geography'POINT(10.7522 59.9139)') le 50

// Innenfor polygon (fylkesgrense)
$filter=geo.intersects(Location, geography'POLYGON((...))')
```

**Full-Text + Filter Hybrid:**
```odata
// Søk etter "GDPR" kun i konfidensielle dokumenter fra 2024
$filter=search.ismatch('GDPR') and Classification eq 'Confidential' and Year eq 2024
```

### Faceted Navigation

**Facet Parameters:**

| Parameter | Beskrivelse | Eksempel |
|-----------|-------------|----------|
| `count` | Maks antall facet-verdier returnert (default: 10, 0 = unlimited) | `"facets": ["Category,count:5"]` |
| `sort` | Sortering: `count`/`-count` (etter frekvens), `value`/`-value` (alfabetisk) | `"facets": ["Rating,sort:-value"]` |
| `values` | Eksplisitte ranges for numeriske/dato-felt | `"facets": ["BaseRate,values:100\|200\|300"]` |
| `interval` | Auto-ranges for numeriske/dato-felt | `"facets": ["Price,interval:50"]` |
| `timeoffset` | UTC-offset for dato-intervals | `"facets": ["Created,interval:day,timeoffset:+01:00"]` |

**JSON Example:**
```json
{
  "search": "*",
  "facets": [
    "Category",
    "Tags,count:20,sort:count",
    "Rating,values:1|2|3|4|5",
    "CreatedDate,interval:month"
  ],
  "filter": "Department eq 'HR'"
}
```

**Response Structure:**
```json
{
  "@search.facets": {
    "Category": [
      { "count": 42, "value": "Contract" },
      { "count": 31, "value": "Policy" }
    ],
    "Tags": [
      { "count": 18, "value": "GDPR" },
      { "count": 12, "value": "ISO27001" }
    ]
  },
  "value": [ /* search results */ ]
}
```

### Security Trimming via Metadata

**Pattern: Principal-based filtering**
```json
// Index schema
{
  "name": "ownerPrincipalIds",
  "type": "Collection(Edm.String)",
  "filterable": true
}

// Query-time filter (injected server-side)
$filter=ownerPrincipalIds/any(p: search.in(p, 'user@org.no,group-id-123', ','))
```

**Pattern: Role-based access**
```odata
$filter=RequiredRoles/any(role: search.in(role, 'HR-Admin,Legal-Read', ','))
```

## Arkitekturmønstre

### 1. Hierarchical Metadata Navigation (Preview)

**Use Case:** Navigere gjennom multi-level kategorier (f.eks. "Juridiske dokumenter > Kontrakter > Leverandøravtaler").

**Implementation (Preview API):**
```json
{
  "facets": [
    "CategoryPath,hierarchical,delimiter:>,levels:3"
  ]
}
```

**Fordeler:**
- Naturlig navigasjon for domeneeksperter
- Reduserer informasjonsoverload

**Ulemper:**
- Preview-feature (ikke produksjonsklar uten testing)
- Krever konsistent metadata-struktur i kildesystemene

---

### 2. Dynamic Facet Filtering (Drilldown)

**Use Case:** Brukeren raffinerer søk ved å velge facets (f.eks. "Vis bare dokumenter fra 2025 med tag 'GDPR'").

**Implementation:**
```javascript
// Initial query - get all facets
{ "search": "*", "facets": ["Year", "Tags", "Department"] }

// User selects Year=2025 -> add filter
{ "search": "*", "filter": "Year eq 2025", "facets": ["Tags", "Department"] }

// User selects Tag=GDPR -> append filter
{ "search": "*", "filter": "Year eq 2025 and Tags/any(t: t eq 'GDPR')", "facets": ["Department"] }
```

**Fordeler:**
- Intuitiv brukeropplevelse
- Alltid minst 0 resultater (hvis implementert riktig)

**Ulemper:**
- Frontend-kompleksitet (state management)
- Facet counts kan være unøyaktige ved sharded indexes (se workarounds nedenfor)

---

### 3. Prefilter vs Postfilter (Vector Search)

**Use Case:** Kombinere metadata-filtre med vector search.

| Mode | Timing | Performance | Presisjon |
|------|--------|-------------|-----------|
| `preFilter` | Filter FØR vector search | Raskere hvis filter ekskluderer mange dokumenter | Kan gi færre resultater enn `k` |
| `postFilter` | Filter ETTER vector search | Alltid `k` resultater (hvis tilgjengelig) | Tregere hvis filter ekskluderer mange |
| `strictPostFilter` (preview) | Hybrid: filter etter vector, re-rank | Balanse mellom speed og presisjon | Kompleks scoring-logikk |

**Eksempel (Prefilter):**
```json
{
  "vectorQueries": [{
    "kind": "vector",
    "vector": [0.123, ...],
    "fields": "contentVector",
    "k": 50
  }],
  "filter": "Department eq 'Legal'",
  "vectorFilterMode": "preFilter"
}
```

**Anbefaling:** Bruk `preFilter` for høy-cardinalitet metadata (f.eks. departement, klassifikasjon), `postFilter` for low-cardinalitet.

## Beslutningsveiledning

### Når bruke hvilken filtreringsteknikk?

| Scenario | Anbefalt Tilnærming | Rationale |
|----------|---------------------|-----------|
| **Compliance-krav** (kun vis dokumenter med sikkerhetsnivå X) | `$filter` med `eq` på classification-felt | Garantert nøyaktighet, juridisk forsvarlig |
| **Brukernavigasjon** (la bruker utforske innhold) | Faceted navigation med `count:0` for nøyaktighet | Bedre UX enn statiske menyer |
| **Performance optimization** (reduser corpus før vector search) | `preFilter` mode + indekserte metadata-felt | Reduserer vector search-scope |
| **Geo-bounded search** (kun dokumenter fra region X) | `geo.distance` eller `geo.intersects` | Native geo-støtte i Azure AI Search |
| **Multi-tenant isolation** | Security trimming via `ownerPrincipalIds/any()` | GDPR Art. 32 - access control |

### Vanlige Feil

| Feil | Symptom | Fix |
|------|---------|-----|
| **Facet count unøyaktighet** | Facet viser 10 kategorier, men reell total er 15 | Sett `count:0` eller `count:>=distinct_values` |
| **Case-sensitive duplicates** | Facet viser både "Norge" og "norge" | Bruk normalizer på felt-definisjon |
| **Filter + Vector gir 0 resultater** | `preFilter` ekskluderer alle dokumenter | Bytt til `postFilter` eller utvid filter-kriterier |
| **Geo-filter feil** | `geo.distance` returnerer feil | Sjekk at felt er `Edm.GeographyPoint`, ikke string |
| **OR-filter performance** | Lang liste med `or Category eq 'X'` | Bruk `search.in(Category, 'X,Y,Z', ',')` (counts as single clause) |

### Røde Flagg

- **Faceting på high-cardinality fields** (f.eks. DocumentId, full-text content) → Ingen verdi, spis storage.
- **Manglende `filterable` attributt** → Runtime-feil ved bruk av `$filter`.
- **Geo-coordinates som facets** → Ikke støttet (bruk by/region i stedet).
- **Ubegrenset filter-kompleksitet** → Azure AI Search har clause limits (~1000), bruk `search.in()` for lange lister.

## Integrasjon med Microsoft-stakken

### SharePoint Metadata

**Pattern:** Synkroniser SharePoint-kolonner til Azure AI Search-metadata.

| SharePoint Column Type | Azure AI Search Type | Filterable? | Facetable? |
|------------------------|----------------------|-------------|------------|
| Single line of text | `Edm.String` | Ja | Ja |
| Choice | `Edm.String` | Ja | Ja (perfect for facets) |
| Managed Metadata | `Collection(Edm.String)` | Ja | Ja |
| Date and Time | `Edm.DateTimeOffset` | Ja | Ja |
| Person or Group | `Collection(Edm.String)` (extrahér UPN/ID) | Ja | Ja |

**Indexer Configuration:**
```json
{
  "fieldMappings": [
    {
      "sourceFieldName": "/Department",
      "targetFieldName": "department"
    },
    {
      "sourceFieldName": "/Confidentiality",
      "targetFieldName": "classificationLevel"
    }
  ]
}
```

### Microsoft Purview (Data Governance)

**Pattern:** Bruk Purview-klassifikasjoner som metadata i search index.

```json
// Purview extraherer sensitivity labels
{
  "name": "purviewClassifications",
  "type": "Collection(Edm.String)",
  "filterable": true,
  "facetable": true
}

// Query-time enforcement
$filter=purviewClassifications/any(c: search.in(c, 'Public,Internal', ','))
```

**Integration:** Azure AI Search indexer kan kalle Microsoft Graph API for å hente sensitivity labels fra Purview/Information Protection.

### Dataverse (Power Platform)

**Pattern:** Synkroniser Dataverse choice columns og lookups til facetable fields.

| Dataverse Type | Mapping | Eksempel |
|----------------|---------|----------|
| Choice | `Edm.String` | Status (Active, Inactive, Archived) |
| Choices (multi-select) | `Collection(Edm.String)` | Tags, Categories |
| Lookup | `Edm.String` (ID eller Name) | OwnerDepartment |
| DateTime | `Edm.DateTimeOffset` | CreatedOn, ModifiedOn |

**Query Example:**
```odata
// Finn alle Dataverse-records med status 'Active' fra HR-divisjonen
$filter=statuscode eq 'Active' and owningbusinessunit eq 'HR'
```

### Azure Blob Storage (Metadata Indexing)

**Pattern:** Bruk Blob index tags som metadata i Azure AI Search.

```json
// Blob index tags (set via Azure Storage API)
{
  "Department": "Legal",
  "Classification": "Confidential",
  "RetentionPolicy": "7years"
}

// Azure AI Search indexer extracts metadata
{
  "name": "metadata_storage_blob_index_tags",
  "type": "Edm.String",
  "filterable": true
}
```

**Lifecycle Management:** Kombiner med Azure Storage lifecycle policies for automatisk arkivering basert på metadata.

## Offentlig sektor (Norge)

### GDPR Art. 32 - Access Control

**Krav:** "Evnen til å sikre fortrolighet, integritet, tilgjengelighet og robusthet til behandlingssystemene."

**Implementation:**
```json
// Index schema
{
  "name": "dataSubjectIds",
  "type": "Collection(Edm.String)",
  "filterable": true
}

// Query-time (server-side filter injection)
$filter=dataSubjectIds/any(id: id eq 'current-user-id')
```

**Audit Trail:** Logg alle søk med filter-kriterier til Azure Monitor for GDPR Art. 30 (register over behandlingsaktiviteter).

### Offentleglova § 3 - Meroffentlighet

**Krav:** Dokumenter kan være "gradert offentlig" (ugradert, begrenset, konfidensielt, hemmelig).

**Implementation:**
```json
{
  "name": "securityClassification",
  "type": "Edm.String",
  "filterable": true,
  "facetable": true
}

// Eksempel: Vis kun ugradert + begrenset for eksterne brukere
$filter=securityClassification eq 'Ugradert' or securityClassification eq 'Begrenset offentlig'
```

**Facet Navigation:** La saksbehandlere filtrere på klassifikasjonsnivå i selvbetjeningsportaler.

### Arkivloven - Kassasjonsklasser

**Krav:** Dokumenter skal ha bevaring-/kassasjonsklasse (B = bevares, K5 = kasseres etter 5 år, etc.).

**Implementation:**
```json
{
  "name": "retentionClass",
  "type": "Edm.String",
  "filterable": true,
  "facetable": true
},
{
  "name": "retentionExpiry",
  "type": "Edm.DateTimeOffset",
  "filterable": true
}

// Query: Finn dokumenter som skal kasseres i 2026
$filter=retentionClass eq 'K5' and retentionExpiry ge 2026-01-01T00:00:00Z and retentionExpiry lt 2027-01-01T00:00:00Z
```

**Automatisering:** Kombiner med Azure Functions for automatisk sletting/arkivering basert på metadata.

### Personvernkonsekvensutredning (DPIA)

**Anbefaling:** For systemer som indekserer personopplysninger, inkluder alltid:
- `personalDataCategories` (Collection(Edm.String)) → "Navn", "Fødselsnummer", "Helseopplysninger"
- `dataProcessingPurpose` (Edm.String) → Behandlingsformål per GDPR Art. 6
- `legalBasis` (Edm.String) → Rettslig grunnlag (samtykke, kontrakt, rettslig forpliktelse)

**Query Example (Audit):**
```odata
// Finn alle dokumenter med helseopplysninger behandlet uten samtykke
$filter=personalDataCategories/any(c: c eq 'Helseopplysninger') and legalBasis ne 'Samtykke'
```

## Kostnad og lisensiering

### Prismodell-oversikt

**Azure AI Search Tier Impact:**

| Tier | Max Index Size | Max Fields | Facet Performance | Anbefaling |
|------|----------------|------------|-------------------|------------|
| Basic | 2 GB | 1000 | Low concurrency | Pilot/POC |
| S1 | 25 GB/partition | 1000 | Medium | Produksjon (< 100K docs) |
| S2 | 100 GB/partition | 1000 | High | Produksjon (100K-1M docs) |
| S3/S3HD | 200 GB/partition | 3000 | Very High | Enterprise (> 1M docs) |

**Storage Cost (Metadata):**
- Filterable fields krever ekstra storage (inverted index).
- Facetable fields krever ytterligere storage (facet cache).
- **Tommelfingerregel:** Metadata utgjør ~10-20% av total index size (ved 5-10 metadata-felt per dokument).

**Query Cost:**
- Facet queries er dyrere enn vanlige queries (shard aggregation overhead).
- `count:0` (unlimited facets) kan øke query latency med 50-200% avhengig av cardinality.
- **Optimalisering:** Bruk `count:10` som default, bare `count:0` når nøyaktighet er kritisk.

### Optimaliseringstips

1. **Replikaer for Facet Performance:** Øk replicas (ikke partitions) for å håndtere høy facet query load.
2. **Cache i Frontend:** Cache facet-strukturer i 5-10 minutter (de endrer sjelden).
3. **Selective Faceting:** Ikke returner alle facets i hver query — la frontend styre hvilke som vises.
4. **Index Partitioning:** For multi-tenant, vurder én index per tenant (isolerer metadata, enklere GDPR-sletting).

## For arkitekten (Cosmo)

### Spørsmål å stille kunden

1. **Metadata-kilder:**
   "Hvilke systemer genererer metadata i dag? (SharePoint, Dataverse, filserver, CRM?)"

2. **Cardinality-analyse:**
   "Hvor mange distinkte verdier har typiske metadata-felt? (f.eks. 'Department' — 5 divisjoner eller 500 avdelinger?)"

3. **Security Requirements:**
   "Må søkeresultater filtreres basert på brukerroller eller organisasjonstilhørighet? (Security trimming)"

4. **Facet Prioritering:**
   "Hvilke metadata-dimensjoner er viktigst for sluttbrukerne å navigere etter? (Kategori? Dato? Avdeling?)"

5. **Data Governance:**
   "Er det krav fra Purview/DLP om å klassifisere dokumenter før indeksering?"

6. **Retention Policies:**
   "Skal søkeindexen respektere arkivloven sine kassasjonsklasser? (Automatisk sletting)"

7. **Geo-filtrering:**
   "Er geografisk lokasjon relevant? (f.eks. 'finn nærmeste kontor med ledig møterom')"

8. **Multi-language:**
   "Har dere metadata på flere språk? (f.eks. 'Category' vs 'Kategori')"

### Fallgruver

| Fallgruve | Konsekvens | Mitigering |
|-----------|------------|------------|
| **Faceting på high-cardinality fields** | Storage-sløsing, ingen UX-verdi | Kun facet på felt med < 100 distinkte verdier |
| **Manglende normalizers** | Duplikater i facets ("Norge", "norge", "NORGE") | Sett normalizer på alle facetable string-felt |
| **Ubegrenset facet count** | Query latency > 2 sekunder | Bruk `count:10` som default, `count:0` kun ved behov |
| **Prefilter på low-cardinality** | Vector search får for lite data å jobbe med | Bruk postfilter for metadata med < 10 verdier |
| **Manglende audit logging** | GDPR non-compliance | Logg alle queries med PII-metadata til Azure Monitor |
| **Hardkodede security filters** | Vedlikeholdsmareritt | Bruk Azure AD groups, inject filter server-side |

### Anbefalinger per modenhetsnivå

**Level 1 - Starter:**
- Bruk 3-5 facetable fields (Category, Date, Department).
- Implementer basic `$filter` for security trimming.
- Bruk default facet count (10).

**Level 2 - Intermediate:**
- Legg til faceted navigation i UI (React/Angular components).
- Implementer hierarchical metadata (preview).
- Kombiner prefilter/postfilter strategisk.
- Integrer med SharePoint/Dataverse metadata.

**Level 3 - Advanced:**
- Dynamisk metadata-schema (støtte for custom fields per tenant).
- Purview-integrasjon for automated classification.
- Real-time facet count aggregation via Application Insights.
- Multi-index orchestration (separate indexes per data classification).

## Kilder og verifisering

**Verified (MCP Research 2026-02):**
- [OData $filter syntax in Azure AI Search](https://learn.microsoft.com/en-us/azure/search/search-query-odata-filter) — **Confidence: High** (Official docs)
- [Add faceted navigation to search results](https://learn.microsoft.com/en-us/azure/search/search-faceted-navigation) — **Confidence: High** (Official docs)
- [OData language overview](https://learn.microsoft.com/en-us/azure/search/query-odata-filter-orderby-syntax) — **Confidence: High** (Official docs)
- [Support for OData (Azure AI Search)](https://learn.microsoft.com/en-us/rest/api/searchservice/support-for-odata) — **Confidence: High** (REST API reference)
- [Faceted navigation examples](https://learn.microsoft.com/en-us/azure/search/search-faceted-navigation-examples) — **Confidence: High** (Official docs)
- [Search indexes in Azure AI Search](https://learn.microsoft.com/en-us/azure/search/search-what-is-an-index) — **Confidence: High** (Official docs)
- [Create an index in Azure AI Search](https://learn.microsoft.com/en-us/azure/search/search-how-to-create-search-index) — **Confidence: High** (Official docs)

**Baseline (Model Knowledge):**
- Security trimming patterns — **Confidence: Medium** (Standard pattern, not specific to Azure AI Search docs)
- Purview integration patterns — **Confidence: Medium** (Cross-service integration, requires validation)
- Norwegian compliance (Offentleglova, Arkivloven) — **Confidence: High** (Public sector standard)

**Preview Features (Not Production-Ready):**
- Hierarchical facets — **Status: Preview** (Not covered in GA docs retrieved)
- Facet filtering — **Status: Preview** (Mentioned but not detailed)
- Strict postfilter mode — **Status: Preview** (Mentioned in vector search context)

---

**For Cosmo:**
Metadata management er ofte undervurdert i RAG-prosjekter. Kunder fokuserer på embeddings og vector search, men glemmer at 70% av queries i produksjon inneholder strukturerte filter-kriterier ("bare fra min avdeling", "kun siste år", "høyeste klassifikasjon"). Design metadata-schema tidlig, test med reelle cardinality-tall, og prioritér normalizers og security trimming fra dag 1. I offentlig sektor er compliance non-negotiable — bygg audit trail og retention policies inn fra start.


### OData Filter Syntax (oppdatert 2026-04)

Azure AI Search bruker OData `$filter`-syntaks for metadata-filtrering:

**Operatorer:**
- Sammenligning: `eq`, `ne`, `gt`, `lt`, `ge`, `le`
- Logisk: `and`, `or`, `not` (høyest presedens: `not` > sammenlignings-ops > `and` > `or`)
- Samling: `any()`, `all()` for collection-felt
- Geo-spatial: `geo.distance()`, `geo.intersects()`
- Full-text i filter: `search.ismatch()`, `search.ismatchscoring()`
- Effektiv liste-matching: `search.in(field, "val1,val2", ",")`

**Viktig:** `search.in()` teller som én klausul (bedre enn lange OR-kjeder for filter size limits).

**Eksempel — sikkerhetstrimming:**
```odata
$filter=search.in(UserGroup, "GroupA,GroupB", ",")
```

**Hybrid search med filter:**
- `preFilter` (default) — anvendes FØR query execution, reduserer søkerom
- `postFilter` — anvendes ETTER, trimmer resultater (bedre med semantic ranker)
- `filterOverride` (preview) — per-vectorQuery filter som overstyrer globalt filter