refactor(search): remove NLP/smart-search feature entirely (#772)
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m23s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 3m46s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
All checks were successful
CI / Unit & Component Tests (push) Successful in 3m23s
CI / OCR Service Tests (push) Successful in 24s
CI / Backend Unit Tests (push) Successful in 3m46s
CI / fail2ban Regex (push) Successful in 46s
CI / Semgrep Security Scan (push) Successful in 25s
CI / Compose Bucket Idempotency (push) Successful in 1m8s
## Summary - Removes the NLP/smart-search feature completely — the feature was too unreliable and slow; users get better results with the regular search filters - Deletes the entire backend `search/` package (NlSearchController, NlQueryParserService, NlpClient, NlSearchRateLimiter — 14 classes + 6 test classes) - Deletes the `nlp-service/` Python microservice (FastAPI, rapidfuzz, DB-backed person matching) - Removes all frontend NL search components: SmartModeToggle, SmartSearchStatus, InterpretationChipRow, DisambiguationPicker, chip-types, theme-chip-removal - Strips smart-mode logic from SearchFilterBar and documents/+page.svelte - Removes `SMART_SEARCH_UNAVAILABLE` / `SMART_SEARCH_RATE_LIMITED` error codes from backend, frontend types, and all three i18n files (de/en/es) - Removes `nlp-service` container and `APP_NLP_BASE_URL` from both docker-compose files - Removes Ollama/NLP Prometheus scrape job and Grafana dashboard - Deletes ADRs 028 (×2), 034, 035 ## Test plan - [ ] Backend compiles: `cd backend && ./mvnw compile -q` → BUILD SUCCESS - [ ] Frontend server tests pass: `cd frontend && npm run test -- --project=server` - [ ] No NLP/smart-search references remain in source: `grep -r "SmartSearch\|NlSearch\|nlp-service\|SMART_SEARCH" backend/src frontend/src` - [ ] `docker compose config` validates both compose files - [ ] Search page loads, filter bar works, no smart-mode toggle visible 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-authored-by: Marcel <marcel@familienarchiv> Reviewed-on: #772
This commit was merged in pull request #772.
This commit is contained in:
@@ -1,113 +0,0 @@
|
||||
import AxeBuilder from '@axe-core/playwright';
|
||||
import { test, expect } from '@playwright/test';
|
||||
|
||||
// NL search is mocked at the network boundary — Ollama is not required in CI.
|
||||
// CSRF enforcement is bypassed by page.route (the real request is never sent),
|
||||
// so it is only verified in manual full-stack runs (see issue #739 DevOps notes).
|
||||
const interpretation = {
|
||||
resolvedPersons: [
|
||||
{ id: '11111111-1111-1111-1111-111111111111', displayName: 'Walter Raddatz' },
|
||||
{ id: '22222222-2222-2222-2222-222222222222', displayName: 'Emma Raddatz' }
|
||||
],
|
||||
ambiguousPersons: [],
|
||||
dateFrom: '1914-01-01',
|
||||
dateTo: '1918-12-31',
|
||||
keywords: ['krieg'],
|
||||
resolvedTags: [{ id: '33333333-3333-3333-3333-333333333333', name: 'Weltkrieg', color: 'sage' }],
|
||||
rawQuery: 'Was hat Walter an Emma im Krieg geschrieben?',
|
||||
keywordsApplied: true,
|
||||
tagsApplied: true
|
||||
};
|
||||
|
||||
const nlResponse = {
|
||||
result: {
|
||||
items: [],
|
||||
totalElements: 0,
|
||||
pageNumber: 0,
|
||||
pageSize: 20,
|
||||
totalPages: 0,
|
||||
undatedCount: 0
|
||||
},
|
||||
interpretation
|
||||
};
|
||||
|
||||
test.describe('NL (smart) search — happy path', () => {
|
||||
test('toggle → loading → chips → remove chip re-runs keyword search; axe clean light + dark', async ({
|
||||
page
|
||||
}) => {
|
||||
// Deliberate delay so the loading state is assertable before the response arrives.
|
||||
await page.route('**/api/search/nl', async (route) => {
|
||||
await new Promise((resolve) => setTimeout(resolve, 150));
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(nlResponse)
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/documents');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
|
||||
// Switch to smart mode via the toggle pill (keyword label = "Text").
|
||||
await page.getByRole('button', { name: /Text/ }).click();
|
||||
|
||||
const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen…');
|
||||
await input.fill('Was hat Walter an Emma im Krieg geschrieben?');
|
||||
await input.press('Enter');
|
||||
|
||||
// Loading panel announced to screen readers.
|
||||
await expect(page.getByText(/Archiv wird befragt/)).toBeVisible();
|
||||
|
||||
// Directional chip (Walter → Emma) + keyword chip + theme chip render once the fixture resolves.
|
||||
await expect(page.getByText('→')).toBeVisible();
|
||||
await expect(page.getByText('Stichwort: krieg')).toBeVisible();
|
||||
await expect(page.getByText(/Thema:.*Weltkrieg/)).toBeVisible();
|
||||
|
||||
// Accessibility — light mode.
|
||||
const lightScan = await new AxeBuilder({ page })
|
||||
.include('[data-testid="smart-search-results"]')
|
||||
.analyze();
|
||||
expect(lightScan.violations).toEqual([]);
|
||||
|
||||
// Accessibility — dark mode.
|
||||
await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'dark'));
|
||||
const darkScan = await new AxeBuilder({ page })
|
||||
.include('[data-testid="smart-search-results"]')
|
||||
.analyze();
|
||||
expect(darkScan.violations).toEqual([]);
|
||||
await page.evaluate(() => document.documentElement.setAttribute('data-theme', 'light'));
|
||||
|
||||
// Removing the keyword chip re-runs a keyword GET with the remaining resolved
|
||||
// params (sender + receiver from the directional pair).
|
||||
await page.getByRole('button', { name: 'Filter entfernen: Stichwort: krieg' }).click();
|
||||
await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/);
|
||||
await expect(page).toHaveURL(/receiverId=22222222-2222-2222-2222-222222222222/);
|
||||
});
|
||||
|
||||
test('removing the last theme chip drops tag/tagOp but keeps person params', async ({ page }) => {
|
||||
await page.route('**/api/search/nl', async (route) => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(nlResponse)
|
||||
});
|
||||
});
|
||||
|
||||
await page.goto('/documents');
|
||||
await page.waitForSelector('[data-hydrated]');
|
||||
await page.getByRole('button', { name: /Text/ }).click();
|
||||
|
||||
const input = page.getByPlaceholder('Titel, Personen, Tags durchsuchen…');
|
||||
await input.fill('Was hat Walter an Emma im Krieg geschrieben?');
|
||||
await input.press('Enter');
|
||||
|
||||
await expect(page.getByText(/Thema:.*Weltkrieg/)).toBeVisible();
|
||||
|
||||
// Remove the single theme chip — URL must carry sender UUID but no tag/tagOp.
|
||||
await page.getByRole('button', { name: 'Filter entfernen: Thema: Weltkrieg' }).click();
|
||||
await page.waitForURL(/senderId=11111111-1111-1111-1111-111111111111/);
|
||||
const url = page.url();
|
||||
expect(url).not.toMatch(/tag=/);
|
||||
expect(url).not.toMatch(/tagOp=/);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user