搜索是网上购物中不可或缺的一部分。Forrester 是一家专注于客户体验的研究公司,报告称,使用站点搜索的访客转化率几乎高出两倍,而且在购物中花费的时间也更长。但是,糟糕的搜索结果会降低销量和品牌信誉。电子商务网站搜索需要快速、相关且针对你企业特定需求量身定制。
在本指南中,我们将引导你使用 Nuxt 3(一个 JavaScript 框架)为电子商务网站构建搜索体验。
本指南分为三个部分
- 设置全文搜索数据库
- 构建“边输入边搜索”体验
- 使用过滤器和多面搜索优化搜索结果
本指南中的代码也位于 GitHub 存储库 中,其中包含不同的检查点,可以帮助你跟踪进度。在本指南结束时,我们的应用程序将如下所示

内容
- 要求
- 设置 Meilisearch 全文搜索数据库
- 构建“边输入边搜索”体验
- 使用排序、多面搜索和分页的复杂搜索模式
要求
为了构建连接到 Meilisearch 数据库的 Nuxt Web 应用程序,我们将使用
- Node 18 或更高版本 - 我们建议使用 nvm 来轻松切换版本
- yarn 3 - Node.js 的包管理器
- Nuxt 3 - 用于使用 Vue 3 和 TypeScript 构建生产应用程序的框架
- Meilisearch 1.3 - 用于创建开箱即用的相关搜索体验的搜索引擎
为了专注于搜索相关事宜,我们将使用模板存储库。此存储库包含 UI 组件,用于构建传统的电子商务布局。让我们先克隆它
git clone https://github.com/meilisearch/ecommerce-demo
然后,让我们安装依赖项
# Navigate to the project directory
cd ecommerce-demo
# Make sure to use Node.js 18.x before installing dependencies!
# nvm use v18
# Install dependencies
yarn
安装完成后,我们就可以开始设置数据库了。
设置 Meilisearch 全文搜索数据库
在构建前端应用程序之前,我们将初始化 Meilisearch 数据库。在本节中,我们将
- 启动 Meilisearch 数据库
- 将我们的数据集导入产品索引中
- 为电子商务搜索配置 Meilisearch 实例
如果你使用的是本教程的存储库,请检出 1-setup-database
分支
git checkout 1-setup-database
启动 Meilisearch 数据库
启动 Meilisearch 实例最简单的方法是使用 Meilisearch Cloud。有 14 天免费试用期,无需信用卡。Meilisearch 是开源的,所以如果你更愿意在本地运行它,可以参考 本地安装 文档。在本指南中,我们将使用 Meilisearch Cloud。
接下来,我们需要 创建一个 Meilisearch Cloud 帐户。登录后,我们将进入“项目”页面。从那里,创建一个项目来启动一个新的数据库(给它起一个酷炫的名字,比如 awesome-ecommerce-tutorial
😏),选择一个引擎版本,然后点击“创建” - 数据库应该在一分钟内准备就绪。让我们在小 Meili 精灵为我们接线的时候继续前进吧!
项目准备就绪后,我们可以访问“项目概述”页面,获取在后续部分中将用到的信息
- 数据库 URL
- 默认搜索 API 密钥
- 默认管理 API 密钥
导入我们的产品数据集
我们的存储库包含 database/data.json
中的电子商务产品样本数据集。我们将通过在 database/setup.js
文件中创建 Meilisearch 客户端,将它导入数据库。
我们还需要为应用程序提供必要的凭据。为此,我们将使用位于项目根目录下的 .env
文件。.env
文件是存储凭据变量的常用方法,我们将通过我们添加到 database/setup.js
中的代码读取它。
首先,复制现有的 .env.example
文件,并将其重命名为 .env
。然后,更新变量以匹配在“项目概述”页面中找到的凭据。更新与 Meilisearch 相关的变量,使你的 .env
文件如下所示
# .env
# Meilisearch configuration
MEILISEARCH_HOST="use the Database URL here"
MEILISEARCH_ADMIN_API_KEY="use the Default Admin API Key here"
MEILISEARCH_SEARCH_API_KEY="use the Default Search API Key here"
# …
现在我们的环境包含了数据库凭据,我们可以创建一个 Meilisearch 客户端,将内容添加到数据库,这个过程称为播种。使用 Meilisearch,对数据库执行的操作是异步的 - 我们称之为“任务”。我们将使用 watchTasks
辅助函数来等待任务完成,然后再退出脚本。
database/setup.js
中的以下代码将存储在 database/data.json
中的数据发送到 Meilisearch
// database/setup.js
import * as dotenv from 'dotenv'
import { MeiliSearch } from 'meilisearch'
import { watchTasks } from './utils.js'
import data from './data.json' assert { type: 'json' }
// Load environment
dotenv.config()
const credentials = {
host: process.env.MEILISEARCH_HOST,
apiKey: process.env.MEILISEARCH_ADMIN_API_KEY
}
const INDEX_NAME = 'products'
const setup = async () => {
console.log('🚀 Seeding your Meilisearch instance')
if (!credentials.host) {
console.error('Missing `MEILISEARCH_HOST` environment variable')
process.exit(1)
}
if (!credentials.apiKey) {
console.error('Missing `MEILISEARCH_ADMIN_API_KEY` environment variable')
process.exit(1)
}
const client = new MeiliSearch(credentials)
console.log(`Adding documents to \`${INDEX_NAME}\``)
await client.index(INDEX_NAME).addDocuments(data)
await watchTasks(client, INDEX_NAME)
}
setup()
使用 Yarn 运行我们的安装脚本
yarn setup
你应该看到类似于以下内容的输出
🚀 Seeding your Meilisearch instance
Adding documents to `products`
Start update watch for products
-------------
products index: adding documents
-------------
All documents added to "products"
✨ Done in 2.92s.
如果一切正常,恭喜你 - 我们已经连接到 Meilisearch 并导入了数据。🎉

配置 Meilisearch 以用于电子商务
Meilisearch 提供了出色的搜索默认设置,包括容忍错字和预定义的排名规则以优化相关性。电子商务搜索的其他关键功能包括排序和过滤。此外,根据营销活动、合作伙伴关系或 <insert business reason>
,你可能想要实施自定义排名规则。
你可以通过调整数据库设置来自定义 Meilisearch。我们将通过我们的 database/setup.js
文件来完成此操作。
首先,让我们确定一个配置
- **过滤**:我们希望产品能够按品牌、类别、标签、评级、评论数量和价格进行过滤;
- **排序**:我们希望产品能够按价格或评级进行排序;
- **排名**:我们希望算法优先考虑排序而不是其他因素(在真正的商店中,你可能希望特色产品排在首位)。
我们可以在 database/setup.js
文件中实现这一点。我们将更新 setup()
函数体,使其看起来像这样
// database/setup.js
// …
const setup = async () => {
// Credentials verification code…
const client = new MeiliSearch(credentials)
console.log(`Adding filterable attributes to \`${INDEX_NAME}\``)
await client.index(INDEX_NAME).updateFilterableAttributes([
'brand',
'category',
'tag',
'rating',
'reviews_count',
'price'
])
console.log(`Adding ranking rules to \`${INDEX_NAME}\``)
await client.index(INDEX_NAME).updateRankingRules([
'sort',
'words',
'typo',
'proximity',
'attribute',
'exactness'
])
console.log(`Adding sortable attributes to \`${INDEX_NAME}\``)
await client.index(INDEX_NAME).updateSortableAttributes([
'rating',
'price'
])
// Adding documents and watching tasks…
}
setup()
在上面的代码中,我们更新了
- **可过滤属性** - 这将启用 过滤和多面搜索;
- **排名规则** - 我们保留了 Meilisearch 默认设置,但将排序放在顶部;
- **可排序属性** - 启用结果排序。
就这样,我们完成了 Meilisearch 数据库设置。✅ 那么,让我们开始构建 Nuxt 3 电子商务网站吧?
构建“边输入边搜索”体验
如果你正在使用 git 存储库,请检出 2-search-as-you-type
分支
git checkout "2-search-as-you-type"
在继续之前,请确保 MEILISEARCH_SEARCH_API_KEY
已在我们的 .env
文件中定义。
创建 Meilisearch 客户端
我们有一个正在运行的 Meilisearch 数据库,但我们仍然需要一个客户端应用程序与它交互。如果我们看一下我们的 package.json
,我们会发现我们有两个库可以使用
vue-instantsearch
(Vue InstantSearch) 用于构建与搜索客户端交互的 UI 组件;@meilisearch/instant-meilisearch
(Instant Meilisearch) 用于创建与 InstantSearch 兼容的 Meilisearch 客户端。
我们需要一个组件来处理对数据库的认证,并使搜索相关状态在应用程序的其他部分可用。让我们在 MeiliSearchProvider.vue
组件中完成此操作。它将以 prop 的形式接收索引名称,并包含一个插槽来包装将访问状态的子组件。
<!-- components/organisms/MeiliSearchProvider.vue -->
<script lang="ts" setup>
import { instantMeiliSearch } from '@meilisearch/instant-meilisearch'
import { AisInstantSearch } from 'vue-instantsearch/vue3/es'
const props = defineProps<{
indexName: string
}>()
const { indexName } = toRefs(props)
const { host, searchApiKey, options } = useRuntimeConfig().meilisearch
const searchClient = instantMeiliSearch(host, searchApiKey, options)
</script>
<template>
<AisInstantSearch :index-name="indexName" :search-client="searchClient">
<slot name="default" />
</AisInstantSearch>
</template>
我们的组件本质上是 AisInstantSearch 组件的包装器。AisInstantSearch 是基于 InstantSearch 的集成的基础:它处理认证并将状态提供给其他 InstantSearch 组件。我们的代码做了三件事
- 从运行时配置中拉取凭据和选项
- 创建一个 InstantMeilisearch 客户端(即与 InstantSearch 兼容的 Meilisearch 客户端)
- 实例化一个 Vue InstantSearch 组件
我们将在主页的根目录 HomeTemplate.vue
中使用此组件。但是,仅凭此组件,它无法执行太多操作。所以,在我们能够将所有内容联系起来之前,让我们实现搜索栏和结果。
使用搜索栏发送查询我们的应用程序需要一个搜索栏,用户可以在其中输入他们的查询。
我们将更新 MeiliSearchBar.vue
组件来处理此操作。在此组件中,我们将输入字段的内容作为查询发送到 Meilisearch 数据库。由于存在 SearchInput 组件,我们的代码可以很简单
<!-- components/organisms/MeiliSearchBar.vue -->
<script lang="ts" setup>
import { AisSearchBox } from 'vue-instantsearch/vue3/es'
</script>
<template>
<AisSearchBox>
<template #default="{ currentRefinement, refine }">
<SearchInput
:value="currentRefinement"
@input="refine($event.currentTarget.value)"
/>
</template>
</AisSearchBox>
</template>
我们的组件使用来自 AisSearchBox 的插槽 prop。插槽 prop 允许父组件访问子作用域中管理的状态。在这里,这些插槽 prop 使我们能够访问搜索相关状态,从而使我们能够构建自定义 UI。有了它,我们就可以向 Meilisearch 数据库发送请求了。现在,只剩下一个东西了 - 显示搜索结果。
显示搜索结果
最后,让我们更新 MeiliSearchResults.vue
组件,以显示搜索结果。我们将以标准网格布局显示结果。我们可以使用 ProductCard 组件
<!-- components/organisms/MeiliSearchResults.vue -->
<script lang="ts" setup>
import { AisHits } from 'vue-instantsearch/vue3/es'
</script>
<template>
<AisHits>
<template #default="{ items }">
<div class="items">
<ProductCard
v-for="product in items"
:key="product.id"
:name="product.title"
:brand="product.brand"
:price="product.price"
:image-url="product.images[0]"
:rating="product.rating"
:reviews-count="product.reviews_count"
/>
</div>
</template>
</AisHits>
</template>
<style src="~/assets/css/components/results-grid.css" scoped />
将所有内容联系起来
我们构建了三个组件:**搜索客户端提供程序**、**搜索栏**和**搜索结果网格**。这些组件在 HomeTemplate.vue
中使用。使用这些组件的行目前已注释掉。随着我们逐步完成指南,我们将取消注释相应的行,以查看我们的组件在实际中的效果。
让我们通过取消注释使用 <MeiliSearchProvider/>
、<MeiliSearchBar/>
和 <MeiliSearchResults/>
的行来检查我们的实现是否成功。我们的代码应如下所示
<!-- components/templates/HomeTemplate.vue -->
<script lang="ts" setup>
const sortingOptions = [
{ value: 'products', label: 'Featured' },
{ value: 'products:price:asc', label: 'Price: Low to High' },
{ value: 'products:price:desc', label: 'Price: High to Low' },
{ value: 'products:rating:desc', label: 'Rating: High to Low' }
]
</script>
<template>
<MeiliSearchProvider index-name="products">
<TheNavbar class="mb-5 shadow-l">
<template #search>
<MeiliSearchBar />
</template>
</TheNavbar>
<div class="container mb-5">
<div class="filters">
<!-- Removed for clarity -->
</div>
<div class="results">
<div class="mb-5 results-meta">
<!-- <MeiliSearchStats /> -->
<!-- <MeiliSearchSorting /> -->
</div>
<MeiliSearchResults class="mb-5" />
<!-- <MeiliSearchPagination /> -->
</div>
</div>
</MeiliSearchProvider>
</template>
<style src="~/assets/css/components/home.css" scoped />
现在,我们有了与 Meilisearch 集成的基本 Nuxt 3 应用程序的框架。要以开发模式启动应用程序,请运行以下命令
yarn dev
默认情况下,开发服务器 URL 是 localhost:3000。我们可以在浏览器中打开它,然后... 瞧!🎉 我们应该能够在搜索框中键入并看到结果出现

好的。我们有一个能够实时搜索产品的应用程序。让我们添加一些闪闪发光的特性,使其更适合现实世界的电子商务。✨
使用排序、多面搜索和分页的复杂搜索模式
如果你正在使用 git 存储库,请检出 3-advanced-search-patterns
分支
git checkout "3-advanced-search-patterns"
排序结果
排序对于浏览搜索结果至关重要。例如,用户可能希望按价格或评分对产品进行排序。我们将更新我们的 `MeiliSearchSorting.vue` 组件,以允许用户使用现有的 *BaseSelect* 组件更改结果的排序方式。我们将使其排序选项作为 props 接收。
<!-- components/organisms/MeiliSearchSorting.vue -->
<script lang="ts" setup>
import { AisSortBy } from 'vue-instantsearch/vue3/es'
const props = defineProps<{
options: Array<{
value: string
label: string
}>
}>()
const { options } = toRefs(props)
</script>
<template>
<AisSortBy :items="options">
<template #default="{ items, refine }">
<BaseSelect
:options="items"
@change="refine($event.target.value)"
/>
</template>
</AisSortBy>
</template>
如果我们回顾一下我们的 `HomeTemplate.vue` 文件,我们可以看到以下数组被定义为用于 `options` prop
const sortingOptions = [
{ value: 'products', label: 'Featured' },
{ value: 'products:price:asc', label: 'Price: Low to High' },
{ value: 'products:price:desc', label: 'Price: High to Low' },
{ value: 'products:rating:desc', label: 'Rating: High to Low' }
]
要查看我们的排序组件的实际效果,请取消对使用 `<MeiliSearchSorting/>` 的行的注释。请注意,排序只有在您事先配置了可排序属性的情况下才能正常工作。
使用 facets 和过滤器缩小结果范围
对结果进行排序很好。但是对于庞大的产品目录,电子商务网站还需要过滤器来细化搜索结果。这就是 facets 的用途。让我们先添加一个用于按产品类别或品牌进行过滤的筛选列表。然后,我们将添加组件来按价格范围和评分进行过滤。
Facet 过滤器
让我们更新我们的 `MeiliSearchFacetFilter.vue` 组件,以显示给定属性的所有可能值的清单。我们将使 `attribute` 成为一个 prop,以便组件可重复使用。在本例中,我们将它用于 *category* 和 *brand*。组件代码应如下所示
<!-- components/organisms/MeiliSearchFacetFilter.vue -->
<script lang="ts" setup>
import { AisRefinementList } from 'vue-instantsearch/vue3/es'
const props = defineProps<{
attribute: string
}>()
const { attribute } = toRefs(props)
</script>
<template>
<AisRefinementList
:attribute="attribute"
operator="or"
>
<template #default="{ items, refine }">
<BaseTitle class="mb-3 text-valhalla-100">
{{ attribute }}
</BaseTitle>
<BaseCheckbox
v-for="item in items"
:key="item.value"
:value="item.isRefined"
:label="item.label"
:name="item.value"
:disabled="item.count === 0"
@change="refine(item.value)"
>
<BaseText tag="span" size="m" :class="[ item.count ? 'text-valhalla-500' : 'text-ashes-900']">
{{ item.label }} <BaseText tag="span" size="s" class="text-ashes-900">
({{ item.count.toLocaleString() }})
</BaseText>
</BaseText>
</BaseCheckbox>
</template>
</AisRefinementList>
</template>
在取消注释我们 `HomeTemplate.vue` 中的相关行之后,我们的应用程序现在应该显示类别和品牌的列表。类别列表应如下所示
🆕 可选 - Facet 搜索和排序 facet 值
Meilisearch v1.3 引入了两个功能:搜索 facet 值 和 排序 facet 值。
搜索 facet 值
按名称或计数排序 facet 值
查看 MeiliSearchFacetFilter.vue
组件 在存储库的 `main` 分支上了解如何实现它。
价格过滤器
要添加价格范围过滤器,我们将更新我们的 `MeiliSearchRangeFilter.vue` 组件。我们将使用现有的 *RangeSlider* 组件来显示一个滑块,允许用户设置最小值和最大值
<!-- components/organisms/MeiliSearchRangeFilter.vue -->
<script lang="ts" setup>
import { AisRangeInput } from 'vue-instantsearch/vue3/es'
interface Range {
min: number
max: number
}
const props = defineProps<{
attribute: string
}>()
const { attribute } = toRefs(props)
const toValue = (currentValue: Range, boundaries: Range): [number, number] => {
return [
typeof currentValue.min === 'number' ? currentValue.min : boundaries.min,
typeof currentValue.max === 'number' ? currentValue.max : boundaries.max
]
}
</script>
<template>
<AisRangeInput :attribute="attribute">
<template #default="{ currentRefinement, range, refine }">
<BaseTitle class="mb-3 text-valhalla-100">
{{ attribute }}
</BaseTitle>
<div class="slider-labels text-valhalla-500 mb-2">
<BaseText size="m">
<span class="text-ashes-900">$ </span>{{ currentRefinement.min ?? range.min }}
</BaseText>
<BaseText size="m">
<span class="text-ashes-900">$ </span>{{ currentRefinement.max ?? range.max }}
</BaseText>
</div>
<RangeSlider
:model-value="toValue(currentRefinement, range)"
:min="range.min"
:max="range.max"
@update:model-value="refine($event)"
/>
</template>
</AisRangeInput>
</template>
<style scoped>
.slider-labels {
display: flex;
justify-content: space-between;
}
</style>
删除 `HomeTemplate.vue` 中对应行的注释,瞧!
评分过滤器
对于在线购物者来说,一种有用的过滤方法是移除低于给定平均评分的产品,因此让我们更新我们的 `MeiliSearchRatingFilter.vue` 组件来处理这种情况。我们将使用 `vue-instantsearch` 中的 *AisRatingMenu* 组件,它有一个限制:它只能使用整数作为评分值。因此,我们将为它提供 `rating_rounded` 属性,而不是 `rating`。我们的组件将接受两个 props:`attribute` 和 `label`(可选)。
<!-- components/organisms/MeiliSearchRatingFilter.vue -->
<script lang="ts" setup>
import { AisRatingMenu } from 'vue-instantsearch/vue3/es'
const props = defineProps<{
attribute: string
label?: string
}>()
const { attribute, label } = toRefs(props)
</script>
<template>
<AisRatingMenu
:attribute="attribute"
:max="5"
>
<template #default="{ items, refine }">
<BaseTitle class="mb-3 text-valhalla-100">
{{ label ?? attribute }}
</BaseTitle>
<a
v-for="item in items"
:key="item.value"
class="rating-link"
:class="[item.isRefined ? 'text-dodger-500' : 'text-valhalla-500']"
href="#"
@click.prevent="refine(item.value)"
>
<span class="rating-label">
<StarRating :rating="Number(item.value)" />
<BaseText
tag="span"
size="m"
class="ml-1"
>
& Up
<BaseText tag="span" size="s" class="text-ashes-900">
({{ item.count.toLocaleString() }})
</BaseText>
</BaseText>
</span>
</a>
</template>
</AisRatingMenu>
</template>
<style src="~/assets/css/components/rating-filter.css" scoped />
然后,瞧!
对结果进行分页
我们将实现一个分页系统,以允许用户更容易地找到结果。在电子商务场景中,编号分页是推荐的方法,因为它允许用户记住页面,从而更容易返回到他们想要查找先前看到的产品的页面。让我们更新我们的 `MeiliSearchPagination.vue` 组件
<script lang="ts" setup>
import { AisPagination } from 'vue-instantsearch/vue3/es'
</script>
<template>
<AisPagination>
<template #default="{ currentRefinement, pages, refine, nbPages, isFirstPage, isLastPage }">
<!-- First page -->
<PageNumber
v-if="!isFirstPage && !pages.includes(0)"
:has-gap-separator="!pages.includes(1)"
:is-current="currentRefinement === 0"
@page-click="refine(0)"
>
Page 1
</PageNumber>
<!-- Current page and 3 previous/next -->
<PageNumber
v-for="(page, index) in pages"
:key="page"
:show-separator="index < (pages.length-1)"
:is-current="currentRefinement === page"
@page-click="refine(page)"
>
Page {{ page + 1 }}
</PageNumber>
<!-- Last page -->
<PageNumber
v-if="!isLastPage && !pages.includes(nbPages-1)"
separator="before"
:has-gap-separator="!pages.includes(nbPages-2)"
:is-current="currentRefinement === nbPages-1"
@page-click="refine(nbPages-1)"
>
Page {{ nbPages }}
</PageNumber>
</template>
</AisPagination>
</template>
在取消注释我们 `HomeTemplate.vue` 文件中的对应行之后,我们现在将在结果下方看到一个页面列表。该列表将始终显示第一页和最后一页,以及当前页和最多前后的 2 页。
就这样,我们完成了我们的电子商务应用程序。祝贺您完成了本指南!🎉
我们的最终应用程序应如下所示

总结
让我们回顾一下我们构建的内容
- 一个 Nuxt 3 电子商务网站
- 一个 Node.js 脚本,用于初始化我们的 Meilisearch 数据库以进行电子商务搜索
- 用于搜索产品以及显示、过滤和排序结果的 InstantSearch 集成
所有代码都可以在演示存储库中找到:https://github.com/meilisearch/ecommerce-demo
存储库的 `main` 分支包含一些细微的差异,例如 Meilisearch 作为 Nuxt 模块实现。这种方法对于希望实现服务器端渲染以改善 SEO 的用户来说非常有用。为了简洁起见,本指南中省略了服务器端渲染和与路由器同步状态等高级主题。
感谢您的阅读!希望本指南对您有所帮助。在我们的 Discord 社区 中告诉我吧!
以下是与我们联系的其他方式