[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill-6bdc043a-8c85-4f2a-809e-ecf16d77a5b5":3,"$fjdgbPWYL1bMEg8FmMYvaKTklnm3x9ts2fq78xEbSTyw":43},{"id":4,"title":5,"description":6,"categoryId":7,"moduleId":8,"tags":9,"prompt":10,"icon":11,"source":12,"sourceUrl":13,"authorId":14,"authorName":15,"isPublic":16,"stars":17,"runs":18,"createdAt":19,"updatedAt":19,"module":20,"category":27,"packages":34},"6bdc043a-8c85-4f2a-809e-ecf16d77a5b5","algolia-search","Algolia搜索实现的专业模式，索引","cat_life_career","mod_other","sickn33,other","---\nname: algolia-search\ndescription: Expert patterns for Algolia search implementation, indexing\n  strategies, React InstantSearch, and relevance tuning\nrisk: unknown\nsource: vibeship-spawner-skills (Apache 2.0)\ndate_added: 2026-02-27\n---\n\n# Algolia Search Integration\n\nExpert patterns for Algolia search implementation, indexing strategies, React InstantSearch, and relevance tuning\n\n## Patterns\n\n### React InstantSearch with Hooks\n\nModern React InstantSearch setup using hooks for type-ahead search.\n\nUses react-instantsearch-hooks-web package with algoliasearch client.\nWidgets are components that can be customized with classnames.\n\nKey hooks:\n- useSearchBox: Search input handling\n- useHits: Access search results\n- useRefinementList: Facet filtering\n- usePagination: Result pagination\n- useInstantSearch: Full state access\n\n### Code_example\n\n\u002F\u002F lib\u002Falgolia.ts\nimport algoliasearch from 'algoliasearch\u002Flite';\n\nexport const searchClient = algoliasearch(\n  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,\n  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!  \u002F\u002F Search-only key!\n);\n\nexport const INDEX_NAME = 'products';\n\n\u002F\u002F components\u002FSearch.tsx\n'use client';\nimport { InstantSearch, SearchBox, Hits, Configure } from 'react-instantsearch';\nimport { searchClient, INDEX_NAME } from '@\u002Flib\u002Falgolia';\n\nfunction Hit({ hit }: { hit: ProductHit }) {\n  return (\n    \u003Carticle>\n      \u003Ch3>{hit.name}\u003C\u002Fh3>\n      \u003Cp>{hit.description}\u003C\u002Fp>\n      \u003Cspan>${hit.price}\u003C\u002Fspan>\n    \u003C\u002Farticle>\n  );\n}\n\nexport function ProductSearch() {\n  return (\n    \u003CInstantSearch searchClient={searchClient} indexName={INDEX_NAME}>\n      \u003CConfigure hitsPerPage={20} \u002F>\n      \u003CSearchBox\n        placeholder=\"Search products...\"\n        classNames={{\n          root: 'relative',\n          input: 'w-full px-4 py-2 border rounded',\n        }}\n      \u002F>\n      \u003CHits hitComponent={Hit} \u002F>\n    \u003C\u002FInstantSearch>\n  );\n}\n\n\u002F\u002F Custom hook usage\nimport { useSearchBox, useHits, useInstantSearch } from 'react-instantsearch';\n\nfunction CustomSearch() {\n  const { query, refine } = useSearchBox();\n  const { hits } = useHits\u003CProductHit>();\n  const { status } = useInstantSearch();\n\n  return (\n    \u003Cdiv>\n      \u003Cinput\n        value={query}\n        onChange={(e) => refine(e.target.value)}\n        placeholder=\"Search...\"\n      \u002F>\n      {status === 'loading' && \u003Cp>Loading...\u003C\u002Fp>}\n      \u003Cul>\n        {hits.map((hit) => (\n          \u003Cli key={hit.objectID}>{hit.name}\u003C\u002Fli>\n        ))}\n      \u003C\u002Ful>\n    \u003C\u002Fdiv>\n  );\n}\n\n### Anti_patterns\n\n- Pattern: Using Admin API key in frontend code | Why: Admin key exposes full index control including deletion | Fix: Use search-only API key with restrictions\n- Pattern: Not using \u002Flite client for frontend | Why: Full client includes unnecessary code for search | Fix: Import from algoliasearch\u002Flite for smaller bundle\n\n### References\n\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fapi-reference\u002Fwidgets\u002Freact\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Flibraries\u002Fjavascript\u002Fv5\u002Fmethods\u002Fsearch\u002F\n\n### Next.js Server-Side Rendering\n\nSSR integration for Next.js with react-instantsearch-nextjs package.\n\nUse \u003CInstantSearchNext> instead of \u003CInstantSearch> for SSR.\nSupports both Pages Router and App Router (experimental).\n\nKey considerations:\n- Set dynamic = 'force-dynamic' for fresh results\n- Handle URL synchronization with routing prop\n- Use getServerState for initial state\n\n### Code_example\n\n\u002F\u002F app\u002Fsearch\u002Fpage.tsx\nimport { InstantSearchNext } from 'react-instantsearch-nextjs';\nimport { searchClient, INDEX_NAME } from '@\u002Flib\u002Falgolia';\nimport { SearchBox, Hits, RefinementList } from 'react-instantsearch';\n\n\u002F\u002F Force dynamic rendering for fresh search results\nexport const dynamic = 'force-dynamic';\n\nexport default function SearchPage() {\n  return (\n    \u003CInstantSearchNext\n      searchClient={searchClient}\n      indexName={INDEX_NAME}\n      routing={{\n        router: {\n          cleanUrlOnDispose: false,\n        },\n      }}\n    >\n      \u003Cdiv className=\"flex gap-8\">\n        \u003Caside className=\"w-64\">\n          \u003Ch3>Categories\u003C\u002Fh3>\n          \u003CRefinementList attribute=\"category\" \u002F>\n          \u003Ch3>Brand\u003C\u002Fh3>\n          \u003CRefinementList attribute=\"brand\" \u002F>\n        \u003C\u002Faside>\n        \u003Cmain className=\"flex-1\">\n          \u003CSearchBox placeholder=\"Search products...\" \u002F>\n          \u003CHits hitComponent={ProductHit} \u002F>\n        \u003C\u002Fmain>\n      \u003C\u002Fdiv>\n    \u003C\u002FInstantSearchNext>\n  );\n}\n\n\u002F\u002F For custom routing (URL synchronization)\nimport { history } from 'instantsearch.js\u002Fes\u002Flib\u002Frouters';\nimport { simple } from 'instantsearch.js\u002Fes\u002Flib\u002FstateMappings';\n\n\u003CInstantSearchNext\n  searchClient={searchClient}\n  indexName={INDEX_NAME}\n  routing={{\n    router: history({\n      getLocation: () =>\n        typeof window === 'undefined'\n          ? new URL(url) as unknown as Location\n          : window.location,\n    }),\n    stateMapping: simple(),\n  }}\n>\n  {\u002F* widgets *\u002F}\n\u003C\u002FInstantSearchNext>\n\n### Anti_patterns\n\n- Pattern: Using InstantSearch component for Next.js SSR | Why: Regular component doesn't support server-side rendering | Fix: Use InstantSearchNext from react-instantsearch-nextjs\n- Pattern: Static rendering for search pages | Why: Search results must be fresh for each request | Fix: Set export const dynamic = 'force-dynamic'\n\n### References\n\n- https:\u002F\u002Fwww.npmjs.com\u002Fpackage\u002Freact-instantsearch-nextjs\n- https:\u002F\u002Fwww.algolia.com\u002Fdevelopers\u002Fcode-exchange\u002Finstantsearch-and-next-js-starter\n\n### Data Synchronization and Indexing\n\nIndexing strategies for keeping Algolia in sync with your data.\n\nThree main approaches:\n1. Full Reindexing - Replace entire index (expensive)\n2. Full Record Updates - Replace individual records\n3. Partial Updates - Update specific attributes only\n\nBest practices:\n- Batch records (ideal: 10MB, 1K-10K records per batch)\n- Use incremental updates when possible\n- partialUpdateObjects for attribute-only changes\n- Avoid deleteBy (computationally expensive)\n\n### Code_example\n\n\u002F\u002F lib\u002Falgolia-admin.ts (SERVER ONLY)\nimport algoliasearch from 'algoliasearch';\n\n\u002F\u002F Admin client - NEVER expose to frontend\nconst adminClient = algoliasearch(\n  process.env.ALGOLIA_APP_ID!,\n  process.env.ALGOLIA_ADMIN_KEY!  \u002F\u002F Admin key for indexing\n);\n\nconst index = adminClient.initIndex('products');\n\n\u002F\u002F Batch indexing (recommended approach)\nexport async function indexProducts(products: Product[]) {\n  const records = products.map((p) => ({\n    objectID: p.id,  \u002F\u002F Required unique identifier\n    name: p.name,\n    description: p.description,\n    price: p.price,\n    category: p.category,\n    inStock: p.inventory > 0,\n    createdAt: p.createdAt.getTime(),  \u002F\u002F Use timestamps for sorting\n  }));\n\n  \u002F\u002F Batch in chunks of ~1000-5000 records\n  const BATCH_SIZE = 1000;\n  for (let i = 0; i \u003C records.length; i += BATCH_SIZE) {\n    const batch = records.slice(i, i + BATCH_SIZE);\n    await index.saveObjects(batch);\n  }\n}\n\n\u002F\u002F Partial update - update only specific fields\nexport async function updateProductPrice(productId: string, price: number) {\n  await index.partialUpdateObject({\n    objectID: productId,\n    price,\n    updatedAt: Date.now(),\n  });\n}\n\n\u002F\u002F Partial update with operations\nexport async function incrementViewCount(productId: string) {\n  await index.partialUpdateObject({\n    objectID: productId,\n    viewCount: {\n      _operation: 'Increment',\n      value: 1,\n    },\n  });\n}\n\n\u002F\u002F Delete records (prefer this over deleteBy)\nexport async function deleteProducts(productIds: string[]) {\n  await index.deleteObjects(productIds);\n}\n\n\u002F\u002F Full reindex with zero-downtime (atomic swap)\nexport async function fullReindex(products: Product[]) {\n  const tempIndex = adminClient.initIndex('products_temp');\n\n  \u002F\u002F Index to temp index\n  await tempIndex.saveObjects(\n    products.map((p) => ({\n      objectID: p.id,\n      ...p,\n    }))\n  );\n\n  \u002F\u002F Copy settings from main index\n  await adminClient.copyIndex('products', 'products_temp', {\n    scope: ['settings', 'synonyms', 'rules'],\n  });\n\n  \u002F\u002F Atomic swap\n  await adminClient.moveIndex('products_temp', 'products');\n}\n\n### Anti_patterns\n\n- Pattern: Using deleteBy for bulk deletions | Why: deleteBy is computationally expensive and rate limited | Fix: Use deleteObjects with array of objectIDs\n- Pattern: Indexing one record at a time | Why: Creates indexing queue, slows down process | Fix: Batch records in groups of 1K-10K\n- Pattern: Full reindex for small changes | Why: Wastes operations, slower than incremental | Fix: Use partialUpdateObject for attribute changes\n\n### References\n\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fguides\u002Fsending-and-managing-data\u002Fsend-and-update-your-data\u002Fin-depth\u002Fthe-different-synchronization-strategies\n- https:\u002F\u002Fwww.algolia.com\u002Fblog\u002Fengineering\u002Fsearch-indexing-best-practices-for-top-performance-with-code-samples\n\n### API Key Security and Restrictions\n\nSecure API key configuration for Algolia.\n\nKey types:\n- Admin API Key: Full control (indexing, settings, deletion)\n- Search-Only API Key: Safe for frontend\n- Secured API Keys: Generated from base key with restrictions\n\nRestrictions available:\n- Indices: Limit accessible indices\n- Rate limit: Limit API calls per hour per IP\n- Validity: Set expiration time\n- HTTP referrers: Restrict to specific URLs\n- Query parameters: Enforce search parameters\n\n### Code_example\n\n\u002F\u002F NEVER do this - admin key in frontend\n\u002F\u002F const client = algoliasearch(appId, ADMIN_KEY);  \u002F\u002F WRONG!\n\n\u002F\u002F Correct: Use search-only key in frontend\nconst searchClient = algoliasearch(\n  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,\n  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!\n);\n\n\u002F\u002F Server-side: Generate secured API key\n\u002F\u002F lib\u002Falgolia-secured-key.ts\nimport algoliasearch from 'algoliasearch';\n\nconst adminClient = algoliasearch(\n  process.env.ALGOLIA_APP_ID!,\n  process.env.ALGOLIA_ADMIN_KEY!\n);\n\n\u002F\u002F Generate user-specific secured key\nexport function generateSecuredKey(userId: string) {\n  const searchKey = process.env.ALGOLIA_SEARCH_KEY!;\n\n  return adminClient.generateSecuredApiKey(searchKey, {\n    \u002F\u002F User can only see their own data\n    filters: `userId:${userId}`,\n    \u002F\u002F Key expires in 1 hour\n    validUntil: Math.floor(Date.now() \u002F 1000) + 3600,\n    \u002F\u002F Restrict to specific index\n    restrictIndices: ['user_documents'],\n  });\n}\n\n\u002F\u002F Rate-limited key for public APIs\nexport async function createRateLimitedKey() {\n  const { key } = await adminClient.addApiKey({\n    acl: ['search'],\n    indexes: ['products'],\n    description: 'Public search with rate limit',\n    maxQueriesPerIPPerHour: 1000,\n    referers: ['https:\u002F\u002Fmysite.com\u002F*'],\n    validity: 0,  \u002F\u002F Never expires\n  });\n\n  return key;\n}\n\n\u002F\u002F API endpoint to get user's secured key\n\u002F\u002F app\u002Fapi\u002Fsearch-key\u002Froute.ts\nimport { auth } from '@\u002Flib\u002Fauth';\nimport { generateSecuredKey } from '@\u002Flib\u002Falgolia-secured-key';\n\nexport async function GET() {\n  const session = await auth();\n  if (!session?.user) {\n    return Response.json({ error: 'Unauthorized' }, { status: 401 });\n  }\n\n  const securedKey = generateSecuredKey(session.user.id);\n\n  return Response.json({ key: securedKey });\n}\n\n### Anti_patterns\n\n- Pattern: Hardcoding Admin API key in client code | Why: Exposes full index control to attackers | Fix: Use search-only key with restrictions\n- Pattern: Using same key for all users | Why: Can't restrict data access per user | Fix: Generate secured API keys with user filters\n- Pattern: No rate limiting on public search | Why: Bots can exhaust your search quota | Fix: Set maxQueriesPerIPPerHour on API key\n\n### References\n\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fguides\u002Fsecurity\u002Fapi-keys\n- https:\u002F\u002Fsupport.algolia.com\u002Fhc\u002Fen-us\u002Farticles\u002F14339249272977-What-are-the-best-practices-to-manage-Algolia-API-keys-in-my-code-and-protect-them\n\n### Custom Ranking and Relevance Tuning\n\nConfigure searchable attributes and custom ranking for relevance.\n\nSearchable attributes (order matters):\n1. Most important fields first (title, name)\n2. Secondary fields next (description, tags)\n3. Exclude non-searchable fields (image_url, id)\n\nCustom ranking:\n- Add business metrics (popularity, rating, date)\n- Use desc() for descending, asc() for ascending\n\n### Code_example\n\n\u002F\u002F scripts\u002Fconfigure-index.ts\nimport algoliasearch from 'algoliasearch';\n\nconst adminClient = algoliasearch(\n  process.env.ALGOLIA_APP_ID!,\n  process.env.ALGOLIA_ADMIN_KEY!\n);\n\nconst index = adminClient.initIndex('products');\n\nasync function configureIndex() {\n  await index.setSettings({\n    \u002F\u002F Searchable attributes in order of importance\n    searchableAttributes: [\n      'name',              \u002F\u002F Most important\n      'brand',\n      'category',\n      'description',       \u002F\u002F Least important\n    ],\n\n    \u002F\u002F Attributes for faceting\u002Ffiltering\n    attributesForFaceting: [\n      'category',\n      'brand',\n      'filterOnly(inStock)',  \u002F\u002F Filter only, not displayed\n      'searchable(tags)',     \u002F\u002F Searchable facet\n    ],\n\n    \u002F\u002F Custom ranking (after text relevance)\n    customRanking: [\n      'desc(popularity)',     \u002F\u002F Most popular first\n      'desc(rating)',         \u002F\u002F Then by rating\n      'desc(createdAt)',      \u002F\u002F Then by recency\n    ],\n\n    \u002F\u002F Typo tolerance\n    typoTolerance: true,\n    minWordSizefor1Typo: 4,\n    minWordSizefor2Typos: 8,\n\n    \u002F\u002F Query settings\n    queryLanguages: ['en'],\n    removeStopWords: ['en'],\n\n    \u002F\u002F Highlighting\n    attributesToHighlight: ['name', 'description'],\n    highlightPreTag: '\u003Cmark>',\n    highlightPostTag: '\u003C\u002Fmark>',\n\n    \u002F\u002F Pagination\n    hitsPerPage: 20,\n    paginationLimitedTo: 1000,\n\n    \u002F\u002F Distinct (deduplication)\n    attributeForDistinct: 'productFamily',\n    distinct: true,\n  });\n\n  \u002F\u002F Add synonyms\n  await index.saveSynonyms([\n    {\n      objectID: 'phone-mobile',\n      type: 'synonym',\n      synonyms: ['phone', 'mobile', 'cell', 'smartphone'],\n    },\n    {\n      objectID: 'laptop-notebook',\n      type: 'oneWaySynonym',\n      input: 'laptop',\n      synonyms: ['notebook', 'portable computer'],\n    },\n  ]);\n\n  \u002F\u002F Add rules (query-based customization)\n  await index.saveRules([\n    {\n      objectID: 'boost-sale-items',\n      condition: {\n        anchoring: 'contains',\n        pattern: 'sale',\n      },\n      consequence: {\n        params: {\n          filters: 'onSale:true',\n          optionalFilters: ['featured:true'],\n        },\n      },\n    },\n  ]);\n\n  console.log('Index configured successfully');\n}\n\nconfigureIndex();\n\n### Anti_patterns\n\n- Pattern: Searching all attributes equally | Why: Reduces relevance, matches in descriptions rank same as titles | Fix: Order searchableAttributes by importance\n- Pattern: No custom ranking | Why: Relies only on text matching, ignores business value | Fix: Add popularity, rating, or recency to customRanking\n- Pattern: Indexing raw dates as strings | Why: Can't sort by date correctly | Fix: Use timestamps (getTime()) for date sorting\n\n### References\n\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fguides\u002Fmanaging-results\u002Frelevance-overview\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fguides\u002Fmanaging-results\u002Fmust-do\u002Fcustom-ranking\n\n### Faceted Search and Filtering\n\nImplement faceted navigation with refinement lists, range sliders,\nand hierarchical menus.\n\nWidget types:\n- RefinementList: Multi-select checkboxes\n- Menu: Single-select list\n- HierarchicalMenu: Nested categories\n- RangeInput\u002FRangeSlider: Numeric ranges\n- ToggleRefinement: Boolean filters\n\n### Code_example\n\n'use client';\nimport {\n  InstantSearch,\n  SearchBox,\n  Hits,\n  RefinementList,\n  HierarchicalMenu,\n  RangeInput,\n  ToggleRefinement,\n  ClearRefinements,\n  CurrentRefinements,\n  Stats,\n  SortBy,\n} from 'react-instantsearch';\nimport { searchClient, INDEX_NAME } from '@\u002Flib\u002Falgolia';\n\nexport function ProductSearch() {\n  return (\n    \u003CInstantSearch searchClient={searchClient} indexName={INDEX_NAME}>\n      \u003Cdiv className=\"flex gap-8\">\n        {\u002F* Filters Sidebar *\u002F}\n        \u003Caside className=\"w-64 space-y-6\">\n          \u003CClearRefinements \u002F>\n          \u003CCurrentRefinements \u002F>\n\n          {\u002F* Category hierarchy *\u002F}\n          \u003Cdiv>\n            \u003Ch3 className=\"font-semibold mb-2\">Categories\u003C\u002Fh3>\n            \u003CHierarchicalMenu\n              attributes={[\n                'categories.lvl0',\n                'categories.lvl1',\n                'categories.lvl2',\n              ]}\n              limit={10}\n              showMore\n            \u002F>\n          \u003C\u002Fdiv>\n\n          {\u002F* Brand filter *\u002F}\n          \u003Cdiv>\n            \u003Ch3 className=\"font-semibold mb-2\">Brand\u003C\u002Fh3>\n            \u003CRefinementList\n              attribute=\"brand\"\n              searchable\n              searchablePlaceholder=\"Search brands...\"\n              showMore\n              limit={5}\n              showMoreLimit={20}\n            \u002F>\n          \u003C\u002Fdiv>\n\n          {\u002F* Price range *\u002F}\n          \u003Cdiv>\n            \u003Ch3 className=\"font-semibold mb-2\">Price\u003C\u002Fh3>\n            \u003CRangeInput\n              attribute=\"price\"\n              precision={0}\n              classNames={{\n                input: 'w-20 px-2 py-1 border rounded',\n              }}\n            \u002F>\n          \u003C\u002Fdiv>\n\n          {\u002F* In stock toggle *\u002F}\n          \u003CToggleRefinement\n            attribute=\"inStock\"\n            label=\"In Stock Only\"\n            on={true}\n          \u002F>\n\n          {\u002F* Rating filter *\u002F}\n          \u003Cdiv>\n            \u003Ch3 className=\"font-semibold mb-2\">Rating\u003C\u002Fh3>\n            \u003CRefinementList\n              attribute=\"rating\"\n              transformItems={(items) =>\n                items.map((item) => ({\n                  ...item,\n                  label: '★'.repeat(Number(item.label)),\n                }))\n              }\n            \u002F>\n          \u003C\u002Fdiv>\n        \u003C\u002Faside>\n\n        {\u002F* Results *\u002F}\n        \u003Cmain className=\"flex-1\">\n          \u003Cdiv className=\"flex justify-between items-center mb-4\">\n            \u003CSearchBox placeholder=\"Search products...\" \u002F>\n            \u003CSortBy\n              items={[\n                { label: 'Relevance', value: 'products' },\n                { label: 'Price (Low to High)', value: 'products_price_asc' },\n                { label: 'Price (High to Low)', value: 'products_price_desc' },\n                { label: 'Rating', value: 'products_rating_desc' },\n              ]}\n            \u002F>\n          \u003C\u002Fdiv>\n          \u003CStats \u002F>\n          \u003CHits hitComponent={ProductHit} \u002F>\n        \u003C\u002Fmain>\n      \u003C\u002Fdiv>\n    \u003C\u002FInstantSearch>\n  );\n}\n\n\u002F\u002F For sorting, create replica indices\n\u002F\u002F products_price_asc: customRanking: ['asc(price)']\n\u002F\u002F products_price_desc: customRanking: ['desc(price)']\n\u002F\u002F products_rating_desc: customRanking: ['desc(rating)']\n\n### Anti_patterns\n\n- Pattern: Faceting on non-faceted attributes | Why: Must declare attributesForFaceting in settings | Fix: Add attributes to attributesForFaceting array\n- Pattern: Not using filterOnly() for hidden filters | Why: Wastes facet computation on non-displayed attributes | Fix: Use filterOnly(attribute) for filters you won't show\n\n### References\n\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fguides\u002Fmanaging-results\u002Frefine-results\u002Ffaceting\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fapi-reference\u002Fwidgets\u002Frefinement-list\u002Freact\n\n### Query Suggestions and Autocomplete\n\nImplement autocomplete with query suggestions and instant results.\n\nUses @algolia\u002Fautocomplete-js for standalone autocomplete or\nintegrate with InstantSearch using SearchBox.\n\nQuery Suggestions require a separate index generated by Algolia.\n\n### Code_example\n\n\u002F\u002F Standalone Autocomplete\n\u002F\u002F components\u002FAutocomplete.tsx\n'use client';\nimport { autocomplete, getAlgoliaResults } from '@algolia\u002Fautocomplete-js';\nimport algoliasearch from 'algoliasearch\u002Flite';\nimport { useEffect, useRef } from 'react';\nimport '@algolia\u002Fautocomplete-theme-classic';\n\nconst searchClient = algoliasearch(\n  process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!,\n  process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY!\n);\n\nexport function Autocomplete() {\n  const containerRef = useRef\u003CHTMLDivElement>(null);\n\n  useEffect(() => {\n    if (!containerRef.current) return;\n\n    const search = autocomplete({\n      container: containerRef.current,\n      placeholder: 'Search for products',\n      openOnFocus: true,\n      getSources({ query }) {\n        if (!query) return [];\n\n        return [\n          \u002F\u002F Query suggestions\n          {\n            sourceId: 'suggestions',\n            getItems() {\n              return getAlgoliaResults({\n                searchClient,\n                queries: [\n                  {\n                    indexName: 'products_query_suggestions',\n                    query,\n                    params: { hitsPerPage: 5 },\n                  },\n                ],\n              });\n            },\n            templates: {\n              header() {\n                return 'Suggestions';\n              },\n              item({ item, html }) {\n                return html`\u003Cspan>${item.query}\u003C\u002Fspan>`;\n              },\n            },\n          },\n          \u002F\u002F Instant results\n          {\n            sourceId: 'products',\n            getItems() {\n              return getAlgoliaResults({\n                searchClient,\n                queries: [\n                  {\n                    indexName: 'products',\n                    query,\n                    params: { hitsPerPage: 8 },\n                  },\n                ],\n              });\n            },\n            templates: {\n              header() {\n                return 'Products';\n              },\n              item({ item, html }) {\n                return html`\n                  \u003Ca href=\"\u002Fproducts\u002F${item.objectID}\">\n                    \u003Cimg src=\"${item.image}\" alt=\"${item.name}\" \u002F>\n                    \u003Cspan>${item.name}\u003C\u002Fspan>\n                    \u003Cspan>$${item.price}\u003C\u002Fspan>\n                  \u003C\u002Fa>\n                `;\n              },\n            },\n            onSelect({ item, setQuery, refresh }) {\n              \u002F\u002F Navigate on selection\n              window.location.href = `\u002Fproducts\u002F${item.objectID}`;\n            },\n          },\n        ];\n      },\n    });\n\n    return () => search.destroy();\n  }, []);\n\n  return \u003Cdiv ref={containerRef} \u002F>;\n}\n\n\u002F\u002F Combined with InstantSearch\nimport { connectSearchBox } from 'react-instantsearch';\nimport { autocomplete } from '@algolia\u002Fautocomplete-js';\n\n\u002F\u002F Or use built-in Autocomplete widget\nimport { Autocomplete as AlgoliaAutocomplete } from 'react-instantsearch';\n\nexport function SearchWithAutocomplete() {\n  return (\n    \u003CInstantSearch searchClient={searchClient} indexName=\"products\">\n      \u003CAlgoliaAutocomplete\n        placeholder=\"Search products...\"\n        detachedMediaQuery=\"(max-width: 768px)\"\n      \u002F>\n      \u003CHits hitComponent={ProductHit} \u002F>\n    \u003C\u002FInstantSearch>\n  );\n}\n\n### Anti_patterns\n\n- Pattern: Creating autocomplete without debouncing | Why: Every keystroke triggers search, wastes operations | Fix: Algolia autocomplete handles debouncing automatically\n- Pattern: Not using Query Suggestions index | Why: Missing search analytics for popular queries | Fix: Enable Query Suggestions in Algolia dashboard\n\n### References\n\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fui-libraries\u002Fautocomplete\u002Fintroduction\u002Fwhat-is-autocomplete\n- https:\u002F\u002Fwww.algolia.com\u002Fdoc\u002Fguides\u002Fbuilding-search-ui\u002Fui-and-ux-patterns\u002Fquery-suggestions\u002Fhow-to\u002Foptimizing-query-suggestions-relevance\u002Fjs\n\n## Sharp Edges\n\n### Admin API Key in Frontend Code\n\nSeverity: CRITICAL\n\n### Indexing Rate Limits and Throttling\n\nSeverity: HIGH\n\n### Record Size and Index Limits\n\nSeverity: MEDIUM\n\n### PII in Index Names Visible in Network\n\nSeverity: MEDIUM\n\n### Searchable Attributes Order Affects Relevance\n\nSeverity: MEDIUM\n\n### Full Reindex Consumes All Operations\n\nSeverity: MEDIUM\n\n### Every Keystroke Counts as Search Operation\n\nSeverity: MEDIUM\n\n### SSR Hydration Mismatch with InstantSearch\n\nSeverity: MEDIUM\n\n### Replica Indices for Sorting Multiply Storage\n\nSeverity: LOW\n\n### Faceting Requires attributesForFaceting Declaration\n\nSeverity: MEDIUM\n\n## Validation Checks\n\n### Admin API Key in Client Code\n\nSeverity: ERROR\n\nAdmin API key must never be exposed to client-side code\n\nMessage: Admin API key exposed to client. Use search-only key.\n\n### Hardcoded Algolia API Key\n\nSeverity: ERROR\n\nAPI keys should use environment variables\n\nMessage: Hardcoded Algolia credentials. Use environment variables.\n\n### Search Key Used for Indexing\n\nSeverity: ERROR\n\nIndexing operations require admin key, not search key\n\nMessage: Search key used for indexing. Use admin key for write operations.\n\n### Single Record Indexing in Loop\n\nSeverity: WARNING\n\nBatch records together for efficient indexing\n\nMessage: Single record indexing in loop. Use saveObjects for batch indexing.\n\n### Using deleteBy for Deletion\n\nSeverity: WARNING\n\ndeleteBy is expensive and rate-limited\n\nMessage: deleteBy is expensive. Prefer deleteObjects with specific IDs.\n\n### Frequent Full Reindex\n\nSeverity: WARNING\n\nFull reindex wastes operations on unchanged data\n\nMessage: Frequent full reindex. Consider incremental sync for unchanged data.\n\n### Full Client Instead of Lite\n\nSeverity: INFO\n\nUse lite client for smaller bundle in frontend\n\nMessage: Full Algolia client imported. Use algoliasearch\u002Flite for frontend.\n\n### Regular InstantSearch in Next.js\n\nSeverity: WARNING\n\nUse react-instantsearch-nextjs for SSR support\n\nMessage: Using regular InstantSearch. Use InstantSearchNext for Next.js SSR.\n\n### Missing Searchable Attributes Configuration\n\nSeverity: WARNING\n\nConfigure searchableAttributes for better relevance\n\nMessage: No searchableAttributes configured. Set attribute priority for relevance.\n\n### Missing Custom Ranking\n\nSeverity: INFO\n\nCustom ranking improves business relevance\n\nMessage: No customRanking configured. Add business metrics (popularity, rating).\n\n## Collaboration\n\n### Delegation Triggers\n\n- user needs e-commerce checkout -> stripe-integration (Product search leading to purchase)\n- user needs search analytics -> segment-cdp (Track search queries and results)\n- user needs user authentication -> clerk-auth (Secured API keys per user)\n- user needs database setup -> postgres-wizard (Source data for indexing)\n- user needs serverless deployment -> aws-serverless (Lambda for indexing jobs)\n\n## When to Use\n- User mentions or implies: adding search to\n- User mentions or implies: algolia\n- User mentions or implies: instantsearch\n- User mentions or implies: search api\n- User mentions or implies: search functionality\n- User mentions or implies: typeahead\n- User mentions or implies: autocomplete search\n- User mentions or implies: faceted search\n- User mentions or implies: search index\n- User mentions or implies: search as you type\n\n## Limitations\n- Use this skill only when the task clearly matches the scope described above.\n- Do not treat the output as a substitute for environment-specific validation, testing, or expert review.\n- Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.\n","","imported","https:\u002F\u002Fgithub.com\u002Fsickn33\u002Fantigravity-awesome-skills","user_system_seed","SkillOPIC",true,112,2035,"2026-05-16 13:02:28",{"id":8,"name":21,"slug":22,"icon":23,"description":24,"sort":25,"createdAt":26},"其他","other","mdi-page-next-outline","其他类型Skill",5,"2026-05-16 12:53:40",{"id":7,"name":28,"slug":29,"icon":30,"description":31,"moduleId":8,"sort":32,"skillCount":33,"createdAt":26},"职场发展","career","mdi-briefcase-outline","面试准备、简历优化、职业规划",4,575,[35],{"id":36,"skillId":4,"version":37,"fileName":38,"fileSize":39,"filePath":40,"fileHash":41,"manifest":42,"createdAt":19},"d4130848-5a1f-407e-b817-8fdd2c6c74bc","1.0.0","algolia-search.zip",8324,"uploads\u002Fskills\u002F6bdc043a-8c85-4f2a-809e-ecf16d77a5b5\u002Falgolia-search.zip","fad69110ae9b961cc8590b3b836baaf54d325351284c0b5255088fd9e3ebf29d","[{\"path\":\"SKILL.md\",\"isDirectory\":false,\"size\":26448}]",{"code":44,"message":45,"data":46},200,"success",{"items":47,"stats":48,"page":51},[],{"averageRating":49,"totalRatings":49,"ratingCounts":50},0,[49,49,49,49,49],{"limit":52,"offset":49,"hasMore":53,"nextOffset":52,"ratedOnly":16},15,false]