Skip to main content Skills Marketplace Découvrez et explorez les compétences IA créées par la communauté.
Installer avec Codex ou Claude Copiez ce prompt, collez-le dans Codex, Claude ou un autre assistant, puis laissez-le vérifier la page du skill et l'installer pour vous.
Copier le promptAfficher les détails du prompt Une commande directe contourne le prompt de vérification. Examinez la source avant de l'exécuter.
npx skills add https://github.com/InugamiDev/ultrathink-oss --skill searchLa commande reste sur une seule ligne. Faites défiler horizontalement pour la vérifier avant de la copier.
Vous préférez une copie locale ? Téléchargez les fichiers actuellement disponibles dans SkillsMP.
Télécharger Zip Téléchargement... Unified design foundations — design system architecture, tokens, component specs, visual principles, creative vision, figma integration, plus brand design system loader (66 real brands via DESIGN.md). Absorbs design, design-system, design-systems, design-principles, design-router, creative-vision, figma, design-md.
Métiers associés SOC
Basé sur la classification professionnelle SOC
name search description Full-text search implementation with Algolia, MeiliSearch, PostgreSQL pg_trgm, search UX patterns, autocomplete, and relevance tuning layer domain category backend triggers ["full-text search","search implementation","algolia","meilisearch","pg_trgm","search autocomplete","fuzzy search","search relevance","typeahead","search index"] inputs ["Data corpus size and type","Search requirements (fuzzy, faceted, geo, typo-tolerant)","Latency budget","Infrastructure constraints","UX requirements (autocomplete, filters, highlighting)"] outputs ["Search architecture recommendation","Index configuration and schema","Query implementation with ranking","Autocomplete/typeahead component","Sync strategy between DB and search index"] linksTo ["postgresql","api-designer","react","performance-profiler","caching"] linkedFrom ["ecommerce","ui-ux-pro"] preferredNextSkills ["caching","api-designer"] fallbackSkills ["postgresql","redis"] riskLevel low memoryReadPolicy selective memoryWritePolicy selective sideEffects ["May add search service dependencies (algoliasearch, meilisearch)","May require background sync jobs","May add PostgreSQL extensions (pg_trgm, unaccent)"]
Search Domain Skill
Purpose
Search is a core UX feature that users judge harshly -- slow or irrelevant results destroy trust. This skill covers choosing the right search backend, building indexes, tuning relevance, and delivering the fast autocomplete-driven UX that users expect.
When to Use What
Solution Best For Latency Setup Cost PostgreSQL pg_trgm < 1M rows, simple fuzzy search 10-100ms None (extension) Free PostgreSQL FTS < 5M rows, structured search 5-50ms Extension + config Free MeiliSearch < 10M docs, self-hosted, great defaults 1-20ms Docker container Free (self-hosted) Algolia Any scale, zero-ops, instant UX 1-10ms SaaS $$$ (per search) Typesense Self-hosted alternative to Algolia 1-15ms Docker container Free (self-hosted) Elasticsearch > 100M docs, complex aggregations, logs 5-50ms Cluster management $$$ (infra)
Default recommendation : PostgreSQL pg_trgm for MVPs. MeiliSearch for growing apps. Algolia for search-critical products with budget.
Key Concepts
Search Relevance Fundamentals
TF-IDF (Term Frequency - Inverse Document Frequency):
- Words that appear often in a document but rarely across all documents score higher
- "the" appears everywhere -> low signal. "kubernetes" appears rarely -> high signal.
BM25 (Best Match 25):
- Improved TF-IDF used by PostgreSQL FTS, MeiliSearch, Elasticsearch
- Accounts for document length normalization
- Tunable parameters: k1 (term frequency saturation), b (length normalization)
Ranking factors to combine:
1. Text relevance (BM25/TF-IDF)
2. Recency (newer content ranked higher)
3. Popularity (views, sales, ratings)
4. Exact match boost (title match > body match)
5. Personalization (user history, preferences)
Patterns
1. PostgreSQL pg_trgm (Zero Infrastructure)
EXTENSION IF pg_trgm;
EXTENSION IF unaccent;
INDEX idx_products_name_trgm products
gin (name gin_trgm_ops);
INDEX idx_products_description_trgm products
gin (description gin_trgm_ops);
id,
name,
description,
similarity(name, ) name_score,
similarity(description, ) desc_score,
(similarity(name, ) similarity(description, )) combined_score
products
name
description
combined_score
LIMIT ;
pg_trgm.similarity_threshold ;
id, name
products
name ILIKE
name
LIMIT ;
CREATE
NOT
EXISTS
CREATE
NOT
EXISTS
CREATE
ON
USING
CREATE
ON
USING
SELECT
'wireles mouse'
AS
'wireles mouse'
AS
'wireles mouse'
*
2
+
'wireles mouse'
AS
FROM
WHERE
%
'wireles mouse'
OR
%
'wireles mouse'
ORDER
BY
DESC
20
SET
=
0.2
SELECT
FROM
WHERE
'wire%'
ORDER
BY
10
import { sql, desc } from 'drizzle-orm' ;
import { products } from './schema' ;
async function searchProducts (query : string , limit = 20 ) {
const results = await db
.select ({
id : products.id ,
name : products.name ,
price : products.price ,
score : sql<number >`
similarity(${products.name} , ${query} ) * 2 +
similarity(${products.description} , ${query} )
` .as ('score' ),
})
.from (products)
.where (sql`
${products.name} % ${query}
OR ${products.description} % ${query}
` )
.orderBy (desc (sql`score` ))
.limit (limit);
return results;
}
2. PostgreSQL Full-Text Search (Built-in FTS)
ALTER TABLE articles ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (
setweight(to_tsvector('english' , coalesce (title, '' )), 'A' ) ||
setweight(to_tsvector('english' , coalesce (subtitle, '' )), 'B' ) ||
setweight(to_tsvector('english' , coalesce (body, '' )), 'C' ) ||
setweight(to_tsvector('english' , coalesce (tags_text, '' )), 'B' )
) STORED;
CREATE INDEX idx_articles_search ON articles USING gin(search_vector);
SELECT
id,
title,
ts_rank_cd(search_vector, query) AS rank,
ts_headline('english' , body, query,
'StartSel=<mark>, StopSel=</mark>, MaxFragments=2, MaxWords=30'
) AS snippet
FROM articles,
websearch_to_tsquery('english' , 'typescript database migration' ) AS query
WHERE search_vector @@ query
ORDER BY rank DESC
LIMIT 20 ;
SELECT * FROM articles
WHERE search_vector @@ phraseto_tsquery('english' , 'react server components' );
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english' , 'react:* & server:*' );
async function fullTextSearch (query : string , options : {
limit?: number ;
offset?: number ;
category?: string ;
} = {} ) {
const { limit = 20 , offset = 0 , category } = options;
const results = await db.execute (sql`
SELECT
a.id,
a.title,
a.slug,
a.published_at,
ts_rank_cd(a.search_vector, query) AS rank,
ts_headline('english', a.body, query,
'StartSel=<mark>, StopSel=</mark>, MaxFragments=2, MaxWords=30'
) AS snippet
FROM articles a,
websearch_to_tsquery('english', ${query} ) AS query
WHERE a.search_vector @@ query
${category ? sql`AND a.category = ${category} ` : sql`` }
AND a.published_at IS NOT NULL
ORDER BY rank DESC
LIMIT ${limit}
OFFSET ${offset}
` );
return results.rows ;
}
3. MeiliSearch (Self-Hosted, Instant Search)
docker run -d --name meilisearch \
-p 7700:7700 \
-e MEILI_MASTER_KEY='your-master-key' \
-v meili_data:/meili_data \
getmeili/meilisearch:latest
import { MeiliSearch } from 'meilisearch' ;
const client = new MeiliSearch ({
host : process.env .MEILISEARCH_URL ?? 'http://localhost:7700' ,
apiKey : process.env .MEILISEARCH_MASTER_KEY ,
});
async function setupSearchIndex ( ) {
const index = client.index ('products' );
await index.updateSettings ({
searchableAttributes : ['name' , 'description' , 'brand' , 'tags' ],
displayedAttributes : ['id' , 'name' , 'description' , 'price' , 'image_url' , 'brand' ],
filterableAttributes : ['category' , 'brand' , 'price' , 'in_stock' , 'rating' ],
sortableAttributes : ['price' , 'rating' , 'created_at' ],
typoTolerance : {
enabled : true ,
minWordSizeForTypos : { oneTypo : 4 , twoTypos : 8 },
},
rankingRules : [
'words' ,
'typo' ,
'proximity' ,
'attribute' ,
'sort' ,
'exactness' ,
'rating:desc' ,
],
synonyms : {
phone : ['smartphone' , 'mobile' , 'cell phone' ],
laptop : ['notebook' , 'computer' ],
tv : ['television' , 'monitor' , 'screen' ],
},
});
}
async function indexProducts (products : Product [] ) {
const index = client.index ('products' );
const task = await index.addDocuments (products, { primaryKey : 'id' });
await client.waitForTask (task.taskUid );
}
async function searchProducts (query : string , options : {
category?: string ;
minPrice?: number ;
maxPrice?: number ;
sort?: 'price:asc' | 'price:desc' | 'rating:desc' ;
page?: number ;
hitsPerPage?: number ;
} = {} ) {
const index = client.index ('products' );
const filters : string [] = [];
if (options.category ) filters.push (`category = "${options.category} "` );
if (options.minPrice ) filters.push (`price >= ${options.minPrice} ` );
if (options.maxPrice ) filters.push (`price <= ${options.maxPrice} ` );
filters.push ('in_stock = true' );
const results = await index.search (query, {
filter : filters.join (' AND ' ),
sort : options.sort ? [options.sort ] : undefined ,
page : options.page ?? 1 ,
hitsPerPage : options.hitsPerPage ?? 20 ,
attributesToHighlight : ['name' , 'description' ],
highlightPreTag : '<mark>' ,
highlightPostTag : '</mark>' ,
attributesToCrop : ['description' ],
cropLength : 30 ,
showMatchesPosition : true ,
});
return {
hits : results.hits ,
totalHits : results.totalHits ,
page : results.page ,
totalPages : results.totalPages ,
processingTimeMs : results.processingTimeMs ,
facets : results.facetDistribution ,
};
}
4. Algolia (Managed, Highest Quality)
import algoliasearch from 'algoliasearch' ;
const client = algoliasearch (
process.env .ALGOLIA_APP_ID !,
process.env .ALGOLIA_ADMIN_KEY !
);
const index = client.initIndex ('products' );
async function configureIndex ( ) {
await index.setSettings ({
searchableAttributes : [
'name' ,
'brand' ,
'description' ,
'tags' ,
],
attributesForFaceting : [
'searchable(brand)' ,
'filterOnly(in_stock)' ,
'category' ,
'price_range' ,
],
customRanking : ['desc(rating)' , 'desc(sales_count)' ],
distinct : 1 ,
attributeForDistinct : 'product_group_id' ,
typoTolerance : true ,
queryLanguages : ['en' ],
removeStopWords : true ,
ignorePlurals : true ,
hitsPerPage : 20 ,
});
}
async function syncProducts (products : Product [] ) {
const objects = products.map ((p ) => ({
objectID : p.id ,
name : p.name ,
description : p.description ,
brand : p.brand ,
category : p.category ,
price : p.price ,
price_range : getPriceRange (p.price ),
rating : p.rating ,
sales_count : p.salesCount ,
in_stock : p.stock > 0 ,
image_url : p.imageUrl ,
tags : p.tags ,
}));
await index.partialUpdateObjects (objects, { createIfNotExists : true });
}
const searchClient = algoliasearch (
process.env .NEXT_PUBLIC_ALGOLIA_APP_ID !,
process.env .NEXT_PUBLIC_ALGOLIA_SEARCH_KEY !
);
5. Search UX: Autocomplete Component
'use client' ;
import { useState, useRef, useEffect, useCallback } from 'react' ;
interface SearchResult {
id : string ;
title : string ;
subtitle ?: string ;
url : string ;
highlight ?: string ;
}
interface SearchBarProps {
onSearch : (query : string ) => Promise <SearchResult []>;
placeholder ?: string ;
debounceMs ?: number ;
}
export function SearchBar ({
onSearch,
placeholder = 'Search...' ,
debounceMs = 200 ,
}: SearchBarProps ) {
const [query, setQuery] = useState ('' );
const [results, setResults] = useState<SearchResult []>([]);
const [isOpen, setIsOpen] = useState (false );
const [activeIndex, setActiveIndex] = useState (-1 );
const [isLoading, setIsLoading] = useState (false );
const inputRef = useRef<HTMLInputElement >(null );
const listRef = useRef<HTMLUListElement >(null );
const abortRef = useRef<AbortController | null >(null );
useEffect (() => {
if (query.length < 2 ) {
setResults ([]);
setIsOpen (false );
return ;
}
const timer = setTimeout (async () => {
abortRef.current ?.abort ();
abortRef.current = new AbortController ();
setIsLoading (true );
try {
const hits = await onSearch (query);
setResults (hits);
setIsOpen (hits.length > 0 );
setActiveIndex (-1 );
} catch {
} finally {
setIsLoading (false );
}
}, debounceMs);
return () => clearTimeout (timer);
}, [query, onSearch, debounceMs]);
const handleKeyDown = useCallback (
(e : React .KeyboardEvent ) => {
switch (e.key ) {
case 'ArrowDown' :
e.preventDefault ();
setActiveIndex ((i ) => Math .min (i + 1 , results.length - 1 ));
break ;
case 'ArrowUp' :
e.preventDefault ();
setActiveIndex ((i ) => Math .max (i - 1 , -1 ));
break ;
case 'Enter' :
e.preventDefault ();
if (activeIndex >= 0 && results[activeIndex]) {
window .location .href = results[activeIndex].url ;
}
break ;
case 'Escape' :
setIsOpen (false );
inputRef.current ?.blur ();
break ;
}
},
[results, activeIndex]
);
useEffect (() => {
const handler = (e : MouseEvent ) => {
if (!listRef.current ?.contains (e.target as Node ) &&
!inputRef.current ?.contains (e.target as Node )) {
setIsOpen (false );
}
};
document .addEventListener ('mousedown' , handler);
return () => document .removeEventListener ('mousedown' , handler);
}, []);
return (
<div className ="relative w-full max-w-xl" role ="combobox" aria-expanded ={isOpen} >
<div className ="relative" >
<svg
className ="absolute left-4 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400"
fill ="none" viewBox ="0 0 24 24" stroke ="currentColor"
>
<path strokeLinecap ="round" strokeLinejoin ="round" strokeWidth ={2}
d ="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg >
<input
ref ={inputRef}
type ="search"
value ={query}
onChange ={(e) => setQuery(e.target.value)}
onFocus={() => results.length > 0 && setIsOpen(true)}
onKeyDown={handleKeyDown}
placeholder={placeholder}
className="w-full px-4 py-3 pl-12 rounded-lg border border-gray-200
text-base bg-white shadow-sm
transition-all duration-200
focus-visible:ring-2 focus-visible:ring-blue-500 focus-visible:ring-offset-2
focus-visible:outline-none
motion-reduce:transition-none"
role="searchbox"
aria-autocomplete="list"
aria-controls="search-results"
aria-activedescendant={activeIndex >= 0 ? `result-${activeIndex}` : undefined}
/>
{isLoading && (
)}
{isOpen && (
{results.map((result, i) => (
0 ? 'border-t border-gray-100' : ''}`}
onMouseEnter={() => setActiveIndex(i)}
onClick={() => { window.location.href = result.url; }}
>
{result.subtitle && (
{result.subtitle}
)}
))}
)}
);
}
6. Database-to-Search Sync
import { db } from '@/db' ;
import { products } from '@/db/schema' ;
import { gt, sql } from 'drizzle-orm' ;
const BATCH_SIZE = 500 ;
async function incrementalSync (searchIndex : SearchIndex , since : Date ) {
let offset = 0 ;
let synced = 0 ;
while (true ) {
const batch = await db
.select ()
.from (products)
.where (gt (products.updatedAt , since))
.orderBy (products.updatedAt )
.limit (BATCH_SIZE )
.offset (offset);
if (batch.length === 0 ) break ;
await searchIndex.addDocuments (
batch.map (transformForSearch),
{ primaryKey : 'id' }
);
synced += batch.length ;
offset += BATCH_SIZE ;
if (batch.length === BATCH_SIZE ) {
await new Promise ((r ) => setTimeout (r, 100 ));
}
}
return synced;
}
async function fullReindex (searchIndex : SearchIndex ) {
const tempIndex = client.index ('products_temp' );
await tempIndex.updateSettings ({ });
let offset = 0 ;
while (true ) {
const batch = await db
.select ()
.from (products)
.limit (BATCH_SIZE )
.offset (offset);
if (batch.length === 0 ) break ;
await tempIndex.addDocuments (batch.map (transformForSearch));
offset += BATCH_SIZE ;
}
await client.swapIndexes ([{ indexes : ['products' , 'products_temp' ] }]);
await client.index ('products_temp' ).delete ();
}
function transformForSearch (product : typeof products.$inferSelect ) {
return {
id : product.id ,
name : product.name ,
description : product.description ?.slice (0 , 500 ),
brand : product.brand ,
category : product.category ,
price : product.price ,
rating : product.rating ,
in_stock : product.stock > 0 ,
tags : product.tags ,
};
}
Best Practices
Start with PostgreSQL -- pg_trgm and FTS handle most use cases under 1M rows with zero extra infra
Debounce autocomplete -- 150-250ms delay prevents hammering your search backend
Return results in < 100ms -- users perceive > 200ms as slow for search
Show results as-you-type -- do not require pressing Enter; update on every keystroke (debounced)
Highlight matched terms -- users need to see why a result matched
Include faceted filtering -- category, price range, brand filters alongside search
Handle zero results gracefully -- suggest corrections, related terms, or popular items
Log search queries -- the single most valuable analytics for product improvement
Use synonyms and stop words -- "tv" should match "television"; "the" should be ignored
Separate search API key from admin key -- Algolia/MeiliSearch search keys are safe for client-side
Common Pitfalls Pitfall Impact Fix No debounce on keystroke search API hammered, rate limits hit Debounce 150-250ms, abort previous requests Syncing search index synchronously Slow writes, failed search updates block UX Use background jobs, event-driven sync Not tokenizing properly "don't" fails to match "dont" Configure tokenizer, use unaccent extension Returning entire documents Slow network, wasted bandwidth Return only needed fields (id, title, snippet) Case-sensitive search "React" does not match "react" Normalize to lowercase in index and query No relevance tuning Title matches ranked same as body matches Weight searchable attributes (title > body) Single search field for everything Users cannot filter results Add faceted filters (category, date, type) Stale search index Search returns deleted/outdated content Webhook-based sync or frequent incremental sync
<div className ="absolute right-4 top-1/2 -translate-y-1/2" >
<div className ="w-4 h-4 border-2 border-gray-300 border-t-blue-500 rounded-full animate-spin" />
</div >
</div >
<ul
ref ={listRef}
id ="search-results"
role ="listbox"
className ="absolute z-50 w-full mt-2 bg-white rounded-xl border border-gray-200
shadow-lg overflow-hidden max-h-96 overflow-y-auto"
>
<li
key ={result.id}
id ={ `result- ${i }`}
role ="option"
aria-selected ={i === activeIndex}
className ={ `px-4 py-3 cursor-pointer transition-colors duration-150
${i === activeIndex ? 'bg-blue-50 ' : 'hover:bg-gray-50 '}
${i >
<div className ="text-base font-medium text-gray-900"
dangerouslySetInnerHTML ={{ __html: result.highlight ?? result.title }}
/>
<div className ="text-sm text-gray-500 mt-0.5" >
</div >
</li >
</ul >
</div >