多索引搜索 #

多索引搜索概述 #

多索引搜索允许同时查询多个索引,适用于:

  • 跨类型搜索(商品、文章、用户)
  • 分类搜索(不同分类使用不同索引)
  • 多语言搜索(每种语言一个索引)

基本用法 #

单个客户端搜索 #

javascript
const client = algoliasearch('APP_ID', 'SEARCH_KEY');

const queries = [
  {
    indexName: 'products',
    query: 'iphone',
    params: {
      hitsPerPage: 5
    }
  },
  {
    indexName: 'articles',
    query: 'iphone',
    params: {
      hitsPerPage: 3
    }
  },
  {
    indexName: 'users',
    query: 'iphone',
    params: {
      hitsPerPage: 3
    }
  }
];

const { results } = await client.multipleQueries(queries);

console.log(results);
// [
//   { hits: [...], nbHits: 100, ... },  // products结果
//   { hits: [...], nbHits: 50, ... },   // articles结果
//   { hits: [...], nbHits: 10, ... }    // users结果
// ]

结果结构 #

javascript
{
  results: [
    {
      index: 'products',
      hits: [...],
      nbHits: 100,
      page: 0,
      hitsPerPage: 5
    },
    {
      index: 'articles',
      hits: [...],
      nbHits: 50,
      page: 0,
      hitsPerPage: 3
    }
  ]
}

搜索策略 #

并行搜索(默认) #

javascript
const { results } = await client.multipleQueries(queries, {
  strategy: 'none'  // 并行执行所有查询
});

停止于足够结果 #

javascript
const { results } = await client.multipleQueries(queries, {
  strategy: 'stopIfEnoughMatches'  // 找到足够结果后停止
});

实际应用 #

跨类型搜索 #

javascript
async function federatedSearch(query) {
  const queries = [
    {
      indexName: 'products',
      query,
      params: {
        hitsPerPage: 5,
        attributesToHighlight: ['name', 'description'],
        attributesToRetrieve: ['name', 'price', 'image', 'category']
      }
    },
    {
      indexName: 'articles',
      query,
      params: {
        hitsPerPage: 3,
        attributesToHighlight: ['title', 'content'],
        attributesToRetrieve: ['title', 'excerpt', 'author', 'publishedAt']
      }
    },
    {
      indexName: 'users',
      query,
      params: {
        hitsPerPage: 3,
        attributesToHighlight: ['name', 'bio'],
        attributesToRetrieve: ['name', 'avatar', 'bio']
      }
    }
  ];
  
  const { results } = await client.multipleQueries(queries);
  
  return {
    products: results[0],
    articles: results[1],
    users: results[2]
  };
}

渲染结果 #

javascript
function renderFederatedResults(data) {
  const { products, articles, users } = data;
  
  let html = '';
  
  // 商品结果
  if (products.hits.length > 0) {
    html += `
      <section class="result-section">
        <h3>商品 (${products.nbHits})</h3>
        <div class="items">
          ${products.hits.map(hit => `
            <div class="product-item">
              <img src="${hit.image}" alt="${hit.name}">
              <div class="info">
                <h4>${hit._highlightResult.name.value}</h4>
                <span class="price">$${hit.price}</span>
              </div>
            </div>
          `).join('')}
        </div>
        ${products.nbHits > 5 ? `<a href="/search?type=products&q=${query}">查看全部 ${products.nbHits} 个商品</a>` : ''}
      </section>
    `;
  }
  
  // 文章结果
  if (articles.hits.length > 0) {
    html += `
      <section class="result-section">
        <h3>文章 (${articles.nbHits})</h3>
        <div class="items">
          ${articles.hits.map(hit => `
            <div class="article-item">
              <h4>${hit._highlightResult.title.value}</h4>
              <p>${hit.excerpt}</p>
              <span class="author">${hit.author}</span>
            </div>
          `).join('')}
        </div>
      </section>
    `;
  }
  
  // 用户结果
  if (users.hits.length > 0) {
    html += `
      <section class="result-section">
        <h3>用户 (${users.nbHits})</h3>
        <div class="items">
          ${users.hits.map(hit => `
            <div class="user-item">
              <img src="${hit.avatar}" alt="${hit.name}">
              <div class="info">
                <h4>${hit._highlightResult.name.value}</h4>
                <p>${hit.bio}</p>
              </div>
            </div>
          `).join('')}
        </div>
      </section>
    `;
  }
  
  return html;
}

多索引分页 #

分页查询 #

javascript
async function paginatedMultiSearch(query, pages = {}) {
  const queries = [
    {
      indexName: 'products',
      query,
      params: {
        page: pages.products || 0,
        hitsPerPage: 10
      }
    },
    {
      indexName: 'articles',
      query,
      params: {
        page: pages.articles || 0,
        hitsPerPage: 5
      }
    }
  ];
  
  return await client.multipleQueries(queries);
}

无限滚动 #

javascript
class MultiIndexInfiniteScroll {
  constructor(query, indices) {
    this.query = query;
    this.indices = indices.map(index => ({
      name: index,
      page: 0,
      hasMore: true
    }));
  }
  
  async loadMore() {
    const queries = this.indices
      .filter(idx => idx.hasMore)
      .map(idx => ({
        indexName: idx.name,
        query: this.query,
        params: {
          page: idx.page,
          hitsPerPage: 10
        }
      }));
    
    if (queries.length === 0) return null;
    
    const { results } = await client.multipleQueries(queries);
    
    results.forEach((result, i) => {
      const index = this.indices.find(idx => idx.name === queries[i].indexName);
      index.page++;
      index.hasMore = result.page < result.nbPages - 1;
    });
    
    return results;
  }
}

多索引过滤 #

不同过滤条件 #

javascript
const queries = [
  {
    indexName: 'products',
    query: 'phone',
    params: {
      filters: 'inStock:true',
      hitsPerPage: 5
    }
  },
  {
    indexName: 'articles',
    query: 'phone',
    params: {
      filters: 'status:published',
      hitsPerPage: 3
    }
  }
];

统一过滤 #

javascript
function buildMultiSearchQueries(query, filters) {
  const baseParams = {
    query,
    hitsPerPage: 5
  };
  
  return [
    {
      indexName: 'products',
      params: {
        ...baseParams,
        filters: filters.products || ''
      }
    },
    {
      indexName: 'articles',
      params: {
        ...baseParams,
        filters: filters.articles || ''
      }
    }
  ];
}

使用InstantSearch #

多索引配置 #

javascript
import instantsearch from 'instantsearch.js';
import { configure, index } from 'instantsearch.js/es/widgets';

const search = instantsearch({
  indexName: 'products',
  searchClient: client
});

search.addWidgets([
  configure({
    hitsPerPage: 5
  }),
  
  // 商品索引
  index({
    indexName: 'products',
    indexId: 'products'
  }).addWidgets([
    hits({
      container: '#products-hits',
      templates: {
        item: (hit) => `<div>${hit.name}</div>`
      }
    })
  ]),
  
  // 文章索引
  index({
    indexName: 'articles',
    indexId: 'articles'
  }).addWidgets([
    hits({
      container: '#articles-hits',
      templates: {
        item: (hit) => `<div>${hit.title}</div>`
      }
    })
  ])
]);

search.start();

性能优化 #

并行请求 #

javascript
// multipleQueries自动并行执行
// 无需手动Promise.all

限制索引数量 #

javascript
// 建议:最多同时搜索5个索引
const queries = [
  { indexName: 'index1', query, params },
  { indexName: 'index2', query, params },
  { indexName: 'index3', query, params }
];

条件查询 #

javascript
async function smartMultiSearch(query, types) {
  const queries = [];
  
  if (types.includes('products')) {
    queries.push({
      indexName: 'products',
      query,
      params: { hitsPerPage: 5 }
    });
  }
  
  if (types.includes('articles')) {
    queries.push({
      indexName: 'articles',
      query,
      params: { hitsPerPage: 3 }
    });
  }
  
  return await client.multipleQueries(queries);
}

完整示例 #

全站搜索 #

javascript
class SiteSearch {
  constructor(client) {
    this.client = client;
    this.indices = ['products', 'articles', 'users', 'categories'];
  }
  
  async search(query, options = {}) {
    const {
      hitsPerPage = 5,
      filters = {},
      facets = {}
    } = options;
    
    const queries = this.indices.map(indexName => ({
      indexName,
      query,
      params: {
        hitsPerPage,
        filters: filters[indexName] || '',
        facets: facets[indexName] || [],
        attributesToHighlight: this.getHighlightAttributes(indexName),
        attributesToRetrieve: this.getRetrieveAttributes(indexName)
      }
    }));
    
    const { results } = await this.client.multipleQueries(queries);
    
    return this.formatResults(results);
  }
  
  getHighlightAttributes(indexName) {
    const attrs = {
      products: ['name', 'description', 'brand'],
      articles: ['title', 'content'],
      users: ['name', 'bio'],
      categories: ['name', 'description']
    };
    return attrs[indexName] || [];
  }
  
  getRetrieveAttributes(indexName) {
    const attrs = {
      products: ['name', 'price', 'image', 'brand', 'category'],
      articles: ['title', 'excerpt', 'author', 'publishedAt'],
      users: ['name', 'avatar', 'bio'],
      categories: ['name', 'icon']
    };
    return attrs[indexName] || [];
  }
  
  formatResults(results) {
    const formatted = {};
    
    results.forEach(result => {
      const indexName = result.index || result.params?.indexName;
      formatted[indexName] = {
        hits: result.hits,
        nbHits: result.nbHits,
        page: result.page,
        nbPages: result.nbPages,
        hasMore: result.page < result.nbPages - 1
      };
    });
    
    return formatted;
  }
  
  getTotalHits(results) {
    return Object.values(results).reduce((sum, r) => sum + r.nbHits, 0);
  }
  
  hasResults(results) {
    return Object.values(results).some(r => r.hits.length > 0);
  }
}

// 使用
const siteSearch = new SiteSearch(client);

const results = await siteSearch.search('iphone');
console.log(`找到 ${siteSearch.getTotalHits(results)} 条结果`);

总结 #

多索引搜索要点:

要点 说明
方法 multipleQueries
参数 indexName, query, params
策略 none(并行), stopIfEnoughMatches
应用 跨类型搜索、分类搜索
优化 限制索引数、条件查询

接下来,让我们学习 A/B测试

最后更新:2026-03-28