feat(search): recherche intelligente — parsing backend + pg_trgm fuzzy

- parseSearchTokens() côté backend : extrait type (async/openapi),
  version (v1, 1.2.3) et latestOnly depuis la chaîne brute
- Recherche fuzzy via word_similarity (pg_trgm, seuil 0.5) + ILIKE
- latestOnly automatique quand champ non vide sans version explicite
- Suppression de parseSearch() dans BrowseComponent et CatalogComponent
- Suppression de version/latestOnly de ApiListQuery (géré par le backend)
- Activation de pg_trgm via CREATE EXTENSION dans SeederService
- Tests unitaires Vitest pour parseSearchTokens (24 cas)

Generated with [Claude Code](https://claude.ai/code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
This commit is contained in:
z3n
2026-06-17 08:57:45 +00:00
parent e1a3dbc85b
commit 498c6577e4
6 changed files with 262 additions and 26 deletions

View File

@@ -74,7 +74,7 @@ import { CategoryDeleteModalComponent } from '../../shared/category-delete-modal
<!-- Barre de recherche -->
<div class="fr-search-bar fr-mb-3w">
<label class="fr-label" for="search-api">Rechercher</label>
<input id="search-api" class="fr-input" type="search" placeholder="Titre, description..."
<input id="search-api" class="fr-input" type="search" placeholder="Ex : auth async v1.0, openapi latest..."
[value]="searchQuery()"
(input)="onSearchChange($any($event.target).value)" />
</div>
@@ -365,10 +365,11 @@ export class BrowseComponent implements OnInit {
loadSearch() {
this.searchLoading.set(true);
const q = this.searchQuery().trim().toLowerCase();
const raw = this.searchQuery();
const q = raw.trim().toLowerCase();
forkJoin({
apis: this.apiService.list({ search: this.searchQuery(), page: this.searchPage(), limit: this.searchLimit }),
apis: this.apiService.list({ search: raw || undefined, page: this.searchPage(), limit: this.searchLimit }),
categories: this.categoryService.list(),
}).subscribe({
next: ({ apis, categories }) => {
@@ -376,9 +377,7 @@ export class BrowseComponent implements OnInit {
this.searchTotal.set(apis.total);
this.searchCategoryResults.set(
categories.items.filter(
(c) =>
c.name.toLowerCase().includes(q) ||
(c.description ?? '').toLowerCase().includes(q),
(c) => c.name.toLowerCase().includes(q) || (c.description ?? '').toLowerCase().includes(q),
),
);
this.searchLoading.set(false);

View File

@@ -41,7 +41,7 @@ import { ApiEntryListItem, ApiType } from '@datacat/shared';
id="search-input"
class="fr-input"
type="search"
placeholder="Rechercher par titre ou description..."
placeholder="Ex : auth async v1.0, openapi latest..."
[ngModel]="search()"
(ngModelChange)="onSearchChange($event)"
/>