Vibeship-spawner-skills algolia-search

Algolia Search Integration Skill

install
source · Clone the upstream repo
git clone https://github.com/vibeforge1111/vibeship-spawner-skills
manifest: integrations/algolia-search/skill.yaml
source content

Algolia Search Integration Skill

Patterns for search API implementation, indexing, and InstantSearch

id: algolia-search name: Algolia Search Integration display_id: algolia-search description: Expert patterns for Algolia search implementation, indexing strategies, React InstantSearch, and relevance tuning version: 1.0.0 category: integrations tags:

  • algolia
  • search
  • instantsearch
  • indexing
  • relevance
  • faceted-search
  • autocomplete

triggers:

  • "adding search to"
  • "algolia"
  • "instantsearch"
  • "search api"
  • "search functionality"
  • "typeahead"
  • "autocomplete search"
  • "faceted search"
  • "search index"
  • "search as you type"

capabilities:

  • "React InstantSearch integration with hooks"
  • "Server-side rendering with Next.js"
  • "Indexing strategies (full, incremental, partial)"
  • "API key security and restrictions"
  • "Custom ranking and relevance tuning"
  • "Faceted search and filtering"
  • "Query suggestions and autocomplete"
  • "Multi-index search"

patterns:

  • id: react-instantsearch-setup name: React InstantSearch with Hooks description: | Modern React InstantSearch setup using hooks for type-ahead search.

    Uses react-instantsearch-hooks-web package with algoliasearch client. Widgets are components that can be customized with classnames.

    Key hooks:

    • useSearchBox: Search input handling
    • useHits: Access search results
    • useRefinementList: Facet filtering
    • usePagination: Result pagination
    • useInstantSearch: Full state access

    code_example: | // lib/algolia.ts import algoliasearch from 'algoliasearch/lite';

    export const searchClient = algoliasearch( process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!, process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY! // Search-only key! );

    export const INDEX_NAME = 'products';

    // components/Search.tsx 'use client'; import { InstantSearch, SearchBox, Hits, Configure } from 'react-instantsearch'; import { searchClient, INDEX_NAME } from '@/lib/algolia';

    function Hit({ hit }: { hit: ProductHit }) { return ( <article> <h3>{hit.name}</h3> <p>{hit.description}</p> <span>${hit.price}</span> </article> ); }

    export function ProductSearch() { return ( <InstantSearch searchClient={searchClient} indexName={INDEX_NAME}> <Configure hitsPerPage={20} /> <SearchBox placeholder="Search products..." classNames={{ root: 'relative', input: 'w-full px-4 py-2 border rounded', }} /> <Hits hitComponent={Hit} /> </InstantSearch> ); }

    // Custom hook usage import { useSearchBox, useHits, useInstantSearch } from 'react-instantsearch';

    function CustomSearch() { const { query, refine } = useSearchBox(); const { hits } = useHits<ProductHit>(); const { status } = useInstantSearch();

    return (
      <div>
        <input
          value={query}
          onChange={(e) => refine(e.target.value)}
          placeholder="Search..."
        />
        {status === 'loading' && <p>Loading...</p>}
        <ul>
          {hits.map((hit) => (
            <li key={hit.objectID}>{hit.name}</li>
          ))}
        </ul>
      </div>
    );
    

    }

    anti_patterns:

    • 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"

    • pattern: "Not using /lite client for frontend" why: "Full client includes unnecessary code for search" fix: "Import from algoliasearch/lite for smaller bundle"

    references:

  • id: nextjs-ssr-search name: Next.js Server-Side Rendering description: | SSR integration for Next.js with react-instantsearch-nextjs package.

    Use <InstantSearchNext> instead of <InstantSearch> for SSR. Supports both Pages Router and App Router (experimental).

    Key considerations:

    • Set dynamic = 'force-dynamic' for fresh results
    • Handle URL synchronization with routing prop
    • Use getServerState for initial state

    code_example: | // app/search/page.tsx import { InstantSearchNext } from 'react-instantsearch-nextjs'; import { searchClient, INDEX_NAME } from '@/lib/algolia'; import { SearchBox, Hits, RefinementList } from 'react-instantsearch';

    // Force dynamic rendering for fresh search results export const dynamic = 'force-dynamic';

    export default function SearchPage() { return ( <InstantSearchNext searchClient={searchClient} indexName={INDEX_NAME} routing={{ router: { cleanUrlOnDispose: false, }, }} > <div className="flex gap-8"> <aside className="w-64"> <h3>Categories</h3> <RefinementList attribute="category" /> <h3>Brand</h3> <RefinementList attribute="brand" /> </aside> <main className="flex-1"> <SearchBox placeholder="Search products..." /> <Hits hitComponent={ProductHit} /> </main> </div> </InstantSearchNext> ); }

    // For custom routing (URL synchronization) import { history } from 'instantsearch.js/es/lib/routers'; import { simple } from 'instantsearch.js/es/lib/stateMappings';

    <InstantSearchNext searchClient={searchClient} indexName={INDEX_NAME} routing={{ router: history({ getLocation: () => typeof window === 'undefined' ? new URL(url) as unknown as Location : window.location, }), stateMapping: simple(), }}

    {/* widgets */}
    
    </InstantSearchNext>

    anti_patterns:

    • pattern: "Using InstantSearch component for Next.js SSR" why: "Regular component doesn't support server-side rendering" fix: "Use InstantSearchNext from react-instantsearch-nextjs"

    • pattern: "Static rendering for search pages" why: "Search results must be fresh for each request" fix: "Set export const dynamic = 'force-dynamic'"

    references:

  • id: indexing-strategies name: Data Synchronization and Indexing description: | Indexing strategies for keeping Algolia in sync with your data.

    Three main approaches:

    1. Full Reindexing - Replace entire index (expensive)
    2. Full Record Updates - Replace individual records
    3. Partial Updates - Update specific attributes only

    Best practices:

    • Batch records (ideal: 10MB, 1K-10K records per batch)
    • Use incremental updates when possible
    • partialUpdateObjects for attribute-only changes
    • Avoid deleteBy (computationally expensive)

    code_example: | // lib/algolia-admin.ts (SERVER ONLY) import algoliasearch from 'algoliasearch';

    // Admin client - NEVER expose to frontend const adminClient = algoliasearch( process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY! // Admin key for indexing );

    const index = adminClient.initIndex('products');

    // Batch indexing (recommended approach) export async function indexProducts(products: Product[]) { const records = products.map((p) => ({ objectID: p.id, // Required unique identifier name: p.name, description: p.description, price: p.price, category: p.category, inStock: p.inventory > 0, createdAt: p.createdAt.getTime(), // Use timestamps for sorting }));

    // Batch in chunks of ~1000-5000 records
    const BATCH_SIZE = 1000;
    for (let i = 0; i < records.length; i += BATCH_SIZE) {
      const batch = records.slice(i, i + BATCH_SIZE);
      await index.saveObjects(batch);
    }
    

    }

    // Partial update - update only specific fields export async function updateProductPrice(productId: string, price: number) { await index.partialUpdateObject({ objectID: productId, price, updatedAt: Date.now(), }); }

    // Partial update with operations export async function incrementViewCount(productId: string) { await index.partialUpdateObject({ objectID: productId, viewCount: { _operation: 'Increment', value: 1, }, }); }

    // Delete records (prefer this over deleteBy) export async function deleteProducts(productIds: string[]) { await index.deleteObjects(productIds); }

    // Full reindex with zero-downtime (atomic swap) export async function fullReindex(products: Product[]) { const tempIndex = adminClient.initIndex('products_temp');

    // Index to temp index
    await tempIndex.saveObjects(
      products.map((p) => ({
        objectID: p.id,
        ...p,
      }))
    );
    
    // Copy settings from main index
    await adminClient.copyIndex('products', 'products_temp', {
      scope: ['settings', 'synonyms', 'rules'],
    });
    
    // Atomic swap
    await adminClient.moveIndex('products_temp', 'products');
    

    }

    anti_patterns:

    • pattern: "Using deleteBy for bulk deletions" why: "deleteBy is computationally expensive and rate limited" fix: "Use deleteObjects with array of objectIDs"

    • pattern: "Indexing one record at a time" why: "Creates indexing queue, slows down process" fix: "Batch records in groups of 1K-10K"

    • pattern: "Full reindex for small changes" why: "Wastes operations, slower than incremental" fix: "Use partialUpdateObject for attribute changes"

    references:

  • id: api-key-security name: API Key Security and Restrictions description: | Secure API key configuration for Algolia.

    Key types:

    • Admin API Key: Full control (indexing, settings, deletion)
    • Search-Only API Key: Safe for frontend
    • Secured API Keys: Generated from base key with restrictions

    Restrictions available:

    • Indices: Limit accessible indices
    • Rate limit: Limit API calls per hour per IP
    • Validity: Set expiration time
    • HTTP referrers: Restrict to specific URLs
    • Query parameters: Enforce search parameters

    code_example: | // NEVER do this - admin key in frontend // const client = algoliasearch(appId, ADMIN_KEY); // WRONG!

    // Correct: Use search-only key in frontend const searchClient = algoliasearch( process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!, process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY! );

    // Server-side: Generate secured API key // lib/algolia-secured-key.ts import algoliasearch from 'algoliasearch';

    const adminClient = algoliasearch( process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY! );

    // Generate user-specific secured key export function generateSecuredKey(userId: string) { const searchKey = process.env.ALGOLIA_SEARCH_KEY!;

    return adminClient.generateSecuredApiKey(searchKey, {
      // User can only see their own data
      filters: `userId:${userId}`,
      // Key expires in 1 hour
      validUntil: Math.floor(Date.now() / 1000) + 3600,
      // Restrict to specific index
      restrictIndices: ['user_documents'],
    });
    

    }

    // Rate-limited key for public APIs export async function createRateLimitedKey() { const { key } = await adminClient.addApiKey({ acl: ['search'], indexes: ['products'], description: 'Public search with rate limit', maxQueriesPerIPPerHour: 1000, referers: ['https://mysite.com/*'], validity: 0, // Never expires });

    return key;
    

    }

    // API endpoint to get user's secured key // app/api/search-key/route.ts import { auth } from '@/lib/auth'; import { generateSecuredKey } from '@/lib/algolia-secured-key';

    export async function GET() { const session = await auth(); if (!session?.user) { return Response.json({ error: 'Unauthorized' }, { status: 401 }); }

    const securedKey = generateSecuredKey(session.user.id);
    
    return Response.json({ key: securedKey });
    

    }

    anti_patterns:

    • pattern: "Hardcoding Admin API key in client code" why: "Exposes full index control to attackers" fix: "Use search-only key with restrictions"

    • pattern: "Using same key for all users" why: "Can't restrict data access per user" fix: "Generate secured API keys with user filters"

    • pattern: "No rate limiting on public search" why: "Bots can exhaust your search quota" fix: "Set maxQueriesPerIPPerHour on API key"

    references:

  • id: custom-ranking-relevance name: Custom Ranking and Relevance Tuning description: | Configure searchable attributes and custom ranking for relevance.

    Searchable attributes (order matters):

    1. Most important fields first (title, name)
    2. Secondary fields next (description, tags)
    3. Exclude non-searchable fields (image_url, id)

    Custom ranking:

    • Add business metrics (popularity, rating, date)
    • Use desc() for descending, asc() for ascending

    code_example: | // scripts/configure-index.ts import algoliasearch from 'algoliasearch';

    const adminClient = algoliasearch( process.env.ALGOLIA_APP_ID!, process.env.ALGOLIA_ADMIN_KEY! );

    const index = adminClient.initIndex('products');

    async function configureIndex() { await index.setSettings({ // Searchable attributes in order of importance searchableAttributes: [ 'name', // Most important 'brand', 'category', 'description', // Least important ],

      // Attributes for faceting/filtering
      attributesForFaceting: [
        'category',
        'brand',
        'filterOnly(inStock)',  // Filter only, not displayed
        'searchable(tags)',     // Searchable facet
      ],
    
      // Custom ranking (after text relevance)
      customRanking: [
        'desc(popularity)',     // Most popular first
        'desc(rating)',         // Then by rating
        'desc(createdAt)',      // Then by recency
      ],
    
      // Typo tolerance
      typoTolerance: true,
      minWordSizefor1Typo: 4,
      minWordSizefor2Typos: 8,
    
      // Query settings
      queryLanguages: ['en'],
      removeStopWords: ['en'],
    
      // Highlighting
      attributesToHighlight: ['name', 'description'],
      highlightPreTag: '<mark>',
      highlightPostTag: '</mark>',
    
      // Pagination
      hitsPerPage: 20,
      paginationLimitedTo: 1000,
    
      // Distinct (deduplication)
      attributeForDistinct: 'productFamily',
      distinct: true,
    });
    
    // Add synonyms
    await index.saveSynonyms([
      {
        objectID: 'phone-mobile',
        type: 'synonym',
        synonyms: ['phone', 'mobile', 'cell', 'smartphone'],
      },
      {
        objectID: 'laptop-notebook',
        type: 'oneWaySynonym',
        input: 'laptop',
        synonyms: ['notebook', 'portable computer'],
      },
    ]);
    
    // Add rules (query-based customization)
    await index.saveRules([
      {
        objectID: 'boost-sale-items',
        condition: {
          anchoring: 'contains',
          pattern: 'sale',
        },
        consequence: {
          params: {
            filters: 'onSale:true',
            optionalFilters: ['featured:true'],
          },
        },
      },
    ]);
    
    console.log('Index configured successfully');
    

    }

    configureIndex();

    anti_patterns:

    • pattern: "Searching all attributes equally" why: "Reduces relevance, matches in descriptions rank same as titles" fix: "Order searchableAttributes by importance"

    • pattern: "No custom ranking" why: "Relies only on text matching, ignores business value" fix: "Add popularity, rating, or recency to customRanking"

    • pattern: "Indexing raw dates as strings" why: "Can't sort by date correctly" fix: "Use timestamps (getTime()) for date sorting"

    references:

  • id: faceted-search name: Faceted Search and Filtering description: | Implement faceted navigation with refinement lists, range sliders, and hierarchical menus.

    Widget types:

    • RefinementList: Multi-select checkboxes
    • Menu: Single-select list
    • HierarchicalMenu: Nested categories
    • RangeInput/RangeSlider: Numeric ranges
    • ToggleRefinement: Boolean filters

    code_example: | 'use client'; import { InstantSearch, SearchBox, Hits, RefinementList, HierarchicalMenu, RangeInput, ToggleRefinement, ClearRefinements, CurrentRefinements, Stats, SortBy, } from 'react-instantsearch'; import { searchClient, INDEX_NAME } from '@/lib/algolia';

    export function ProductSearch() { return ( <InstantSearch searchClient={searchClient} indexName={INDEX_NAME}> <div className="flex gap-8"> {/* Filters Sidebar */} <aside className="w-64 space-y-6"> <ClearRefinements /> <CurrentRefinements />

            {/* Category hierarchy */}
            <div>
              <h3 className="font-semibold mb-2">Categories</h3>
              <HierarchicalMenu
                attributes={[
                  'categories.lvl0',
                  'categories.lvl1',
                  'categories.lvl2',
                ]}
                limit={10}
                showMore
              />
            </div>
    
            {/* Brand filter */}
            <div>
              <h3 className="font-semibold mb-2">Brand</h3>
              <RefinementList
                attribute="brand"
                searchable
                searchablePlaceholder="Search brands..."
                showMore
                limit={5}
                showMoreLimit={20}
              />
            </div>
    
            {/* Price range */}
            <div>
              <h3 className="font-semibold mb-2">Price</h3>
              <RangeInput
                attribute="price"
                precision={0}
                classNames={{
                  input: 'w-20 px-2 py-1 border rounded',
                }}
              />
            </div>
    
            {/* In stock toggle */}
            <ToggleRefinement
              attribute="inStock"
              label="In Stock Only"
              on={true}
            />
    
            {/* Rating filter */}
            <div>
              <h3 className="font-semibold mb-2">Rating</h3>
              <RefinementList
                attribute="rating"
                transformItems={(items) =>
                  items.map((item) => ({
                    ...item,
                    label: '★'.repeat(Number(item.label)),
                  }))
                }
              />
            </div>
          </aside>
    
          {/* Results */}
          <main className="flex-1">
            <div className="flex justify-between items-center mb-4">
              <SearchBox placeholder="Search products..." />
              <SortBy
                items={[
                  { label: 'Relevance', value: 'products' },
                  { label: 'Price (Low to High)', value: 'products_price_asc' },
                  { label: 'Price (High to Low)', value: 'products_price_desc' },
                  { label: 'Rating', value: 'products_rating_desc' },
                ]}
              />
            </div>
            <Stats />
            <Hits hitComponent={ProductHit} />
          </main>
        </div>
      </InstantSearch>
    );
    

    }

    // For sorting, create replica indices // products_price_asc: customRanking: ['asc(price)'] // products_price_desc: customRanking: ['desc(price)'] // products_rating_desc: customRanking: ['desc(rating)']

    anti_patterns:

    • pattern: "Faceting on non-faceted attributes" why: "Must declare attributesForFaceting in settings" fix: "Add attributes to attributesForFaceting array"

    • 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"

    references:

  • id: autocomplete-suggestions name: Query Suggestions and Autocomplete description: | Implement autocomplete with query suggestions and instant results.

    Uses @algolia/autocomplete-js for standalone autocomplete or integrate with InstantSearch using SearchBox.

    Query Suggestions require a separate index generated by Algolia.

    code_example: | // Standalone Autocomplete // components/Autocomplete.tsx 'use client'; import { autocomplete, getAlgoliaResults } from '@algolia/autocomplete-js'; import algoliasearch from 'algoliasearch/lite'; import { useEffect, useRef } from 'react'; import '@algolia/autocomplete-theme-classic';

    const searchClient = algoliasearch( process.env.NEXT_PUBLIC_ALGOLIA_APP_ID!, process.env.NEXT_PUBLIC_ALGOLIA_SEARCH_KEY! );

    export function Autocomplete() { const containerRef = useRef<HTMLDivElement>(null);

    useEffect(() => {
      if (!containerRef.current) return;
    
      const search = autocomplete({
        container: containerRef.current,
        placeholder: 'Search for products',
        openOnFocus: true,
        getSources({ query }) {
          if (!query) return [];
    
          return [
            // Query suggestions
            {
              sourceId: 'suggestions',
              getItems() {
                return getAlgoliaResults({
                  searchClient,
                  queries: [
                    {
                      indexName: 'products_query_suggestions',
                      query,
                      params: { hitsPerPage: 5 },
                    },
                  ],
                });
              },
              templates: {
                header() {
                  return 'Suggestions';
                },
                item({ item, html }) {
                  return html`<span>${item.query}</span>`;
                },
              },
            },
            // Instant results
            {
              sourceId: 'products',
              getItems() {
                return getAlgoliaResults({
                  searchClient,
                  queries: [
                    {
                      indexName: 'products',
                      query,
                      params: { hitsPerPage: 8 },
                    },
                  ],
                });
              },
              templates: {
                header() {
                  return 'Products';
                },
                item({ item, html }) {
                  return html`
                    <a href="/products/${item.objectID}">
                      <img src="${item.image}" alt="${item.name}" />
                      <span>${item.name}</span>
                      <span>$${item.price}</span>
                    </a>
                  `;
                },
              },
              onSelect({ item, setQuery, refresh }) {
                // Navigate on selection
                window.location.href = `/products/${item.objectID}`;
              },
            },
          ];
        },
      });
    
      return () => search.destroy();
    }, []);
    
    return <div ref={containerRef} />;
    

    }

    // Combined with InstantSearch import { connectSearchBox } from 'react-instantsearch'; import { autocomplete } from '@algolia/autocomplete-js';

    // Or use built-in Autocomplete widget import { Autocomplete as AlgoliaAutocomplete } from 'react-instantsearch';

    export function SearchWithAutocomplete() { return ( <InstantSearch searchClient={searchClient} indexName="products"> <AlgoliaAutocomplete placeholder="Search products..." detachedMediaQuery="(max-width: 768px)" /> <Hits hitComponent={ProductHit} /> </InstantSearch> ); }

    anti_patterns:

    • pattern: "Creating autocomplete without debouncing" why: "Every keystroke triggers search, wastes operations" fix: "Algolia autocomplete handles debouncing automatically"

    • pattern: "Not using Query Suggestions index" why: "Missing search analytics for popular queries" fix: "Enable Query Suggestions in Algolia dashboard"

    references:

handoff_triggers:

  • condition: "needs e-commerce checkout" target_skill: stripe-integration context: "Product search leading to purchase flow"

  • condition: "needs analytics" target_skill: segment-cdp context: "Search event tracking"

  • condition: "needs database" target_skill: postgres-wizard context: "Source data for indexing"