为您的 Nuxt 电子商务添加站点搜索的分步指南
希望为您的 Vue 电子商务添加站点搜索?在这份 Nuxt 完整指南中了解如何集成实时排序、过滤和分面搜索。

搜索是网上购物不可或缺的一部分。专注于客户体验的研究公司 Forrester 报告称,使用站内搜索的访客转化率几乎是两倍,并且花费更多时间购物。但糟糕的搜索结果会降低销售额和品牌信任度。电子商务网站搜索需要快速、相关,并根据您业务的特定需求进行定制。
在本指南中,我们将引导您使用 JavaScript 框架 Nuxt 3 为电子商务网站构建搜索体验。
本指南分为三个部分
- 设置全文搜索数据库
- 构建“边输入边搜索”体验
- 使用筛选和分面细化搜索结果
本指南中的代码也可在 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
安装完成后,我们就可以开始设置数据库了。
遇到任何问题?在 Discord 上使用帮助频道!
设置 Meilisearch 全文搜索数据库
在构建前端应用程序之前,我们将初始化 Meilisearch 数据库。在本节中,我们将
- 启动一个 Meilisearch 数据库
- 将数据集导入到产品索引中
- 配置 Meilisearch 实例以用于电子商务搜索
如果您正在使用本教程的存储库,请查看 1-setup-database
分支
git checkout 1-setup-database
启动 Meilisearch 数据库
启动 Meilisearch 实例最简单的方法是使用 Meilisearch Cloud。它提供 14 天免费试用,无需信用卡。Meilisearch 是开源的,所以如果您更喜欢在本地运行它,可以参考本地安装文档。在本指南中,我们将使用 Meilisearch Cloud。
接下来,我们需要创建一个 Meilisearch 云账户。登录后,我们进入“项目”页面。从那里,创建一个项目以生成一个新的数据库(给它一个很酷的名字,比如 awesome-ecommerce-tutorial
😏),选择一个引擎版本,然后点击“创建”——数据库应该在一分钟内准备好。当小小的 Meili 精灵们为我们插电时,我们继续前进!
项目准备就绪后,我们可以访问“项目概览”页面以检索将在后续章节中有用的信息
- 数据库 URL
- 默认搜索 API 密钥
- 默认管理 API 密钥
“搜索 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 并导入了数据。🎉
您可以通过在浏览器中导航到您的数据库 URL 来浏览您的 Meilisearch 实例的内容。
迷你仪表板允许您浏览数据库内容。
配置 Meilisearch 用于电子商务
Meilisearch 带有出色的搜索默认设置,包括对拼写错误的容忍度和预定义的排序规则以优化相关性。电子商务搜索的其他关键功能包括排序和筛选。此外,根据营销活动、合作伙伴关系或 <插入业务原因>
,您可能希望实施自定义排序规则。
您可以通过调整数据库设置来定制 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"
在继续之前,请确保在我们的 .env
文件中定义了 MEILISEARCH_SEARCH_API_KEY
。
创建 Meilisearch 客户端
我们有一个正在运行的 Meilisearch 数据库,但我们仍然需要一个客户端应用程序来与之交互。如果查看 package.json
,我们会发现有两个库可供使用
vue-instantsearch
(Vue InstantSearch) 用于构建与搜索客户端交互的 UI 组件;@meilisearch/instant-meilisearch
(Instant Meilisearch) 用于创建与 InstantSearch 兼容的 Meilisearch 客户端。
我们需要一个组件来处理对数据库的身份验证,并使搜索相关的状态在应用程序的其他部分可用。让我们在 MeiliSearchProvider.vue
组件中完成此操作。它将索引名称作为属性,并包含一个插槽以包装将访问状态的子组件。
<!-- 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 的插槽属性。插槽属性允许父组件访问子作用域中管理的状态。在这里,这些插槽属性让我们能够访问与搜索相关的状态,从而能够构建自定义 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 组件。我们将使排序选项作为 prop 接收。
<!-- 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
属性
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/>
的行。请注意,排序仅在您事先配置了可排序属性的情况下才有效。
使用分面和筛选器缩小结果范围
排序结果很好。但对于庞大的产品目录,电子商务网站还需要过滤器来优化搜索结果。这就是分面(facet)的作用。让我们首先添加一个细化列表,用于按产品类别或品牌进行过滤。然后,我们将添加组件以按价格范围和评分进行过滤。
分面过滤器
让我们更新 MeiliSearchFacetFilter.vue
组件以显示给定属性所有可能值的清单。我们将把 attribute
设为属性,这样组件就可以复用。在我们的例子中,我们将它用于类别和品牌。组件代码应该如下所示
<!-- 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
中的相关行之后,我们的应用程序现在应该显示类别和品牌列表。类别列表应该如下所示
类别筛选器允许只显示符合特定类别的产品。
🆕 可选 – 分面搜索与分面值排序
Meilisearch v1.3 引入了两个功能:搜索分面值和排序分面值。
搜索分面值
搜索分面值
按名称或数量排序分面值
排序分面值
查看存储库 main
分支上的 MeiliSearchFacetFilter.vue
组件,了解如何实现它。
价格过滤器
要添加价格范围过滤器,我们将更新 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
。我们的组件将接受两个属性: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 />
瞧!
评分筛选器组件允许按最低评分进行筛选。
结果分页
我们将实现一个分页系统,让用户更容易找到结果。在电子商务场景中,[编号分页是推荐的方法](/blog/pagination-vs-infinite-scroll-vs-load-more/#ecommerce-demo&utm_source=blog),因为它允许用户记住页面,从而更容易返回到他们之前看过的产品。让我们更新 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 社区中与我们交流!
以下是其他联系方式