逐步指南:為您的 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 Cloud 帳戶。登入後,我們會進入「專案」頁面。從那裡建立一個專案來產生新的資料庫(給它一個很酷的名稱,例如 awesome-ecommerce-tutorial
😏),選擇引擎版本,然後按一下「建立」–資料庫應該會在幾分鐘內準備好。在 Meili 小精靈為我們接上纜線的同時,讓我們繼續前進!
當專案準備就緒時,我們可以存取「專案總覽」頁面以擷取稍後章節中有用的資訊
- 資料庫網址
- 預設搜尋 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 並匯入資料。 🎉
您可以透過在瀏覽器中導覽至您的資料庫網址來瀏覽 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"
在繼續之前,請確保 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 的插槽 props。插槽 props 允許父元件存取在子範圍內管理的狀態。在這裡,這些插槽 props 讓我們可以存取與搜尋相關的狀態,從而使我們能夠建構自訂 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 值
按名稱或計數排序 facet 值
排序 facet 值
請查看儲存庫 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
。我們的元件將接受兩個 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 />
然後就完成了!
評分篩選器元件允許按最低評分進行篩選。
為結果分頁
我們將實作一個分頁系統,以允許使用者更輕鬆地找到結果。在電子商務情境中,[建議使用編號分頁](/blog/pagination-vs-infinite-scroll-vs-load-more/?utm_campaign=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 社群 中告訴我!
以下是與我們聯絡的其他方式
- 參與我們的 產品討論
- 發現錯誤?提出 Github Issue
- 有任何遺漏?請查看我們的 路線圖