← 返回博客
跳至主要内容

使用 TypeScript 实现的 AI 驱动的电子商务

·11 分钟阅读
Daniel Phiri

AI Enabled eCommerce in TypeScript

如今,全球有 26.4亿在线购物者。占世界总人口的33%以上。作为对比,截至2024年,53.5亿人可以使用互联网。几乎一半使用互联网的人都在网上购物。然而,这种体验可能并不完美。一份 Google报告显示,由于糟糕的在线搜索体验,每年损失近3000亿美元,全球85%的在线客户在搜索失败后会改变对品牌的看法。搜索对于卓越的电商体验显然非常重要。

以下是一些更详细的数据来进一步说明这一点。

一些突出的数据,但显然都表明搜索是任何电商产品的重要组成部分。

电商搜索陷阱

一些因素导致糟糕的搜索体验,主要是…

  • 响应不佳或无响应(尤其是在非产品搜索时): 用户有时搜索的是问题,而不是产品。
  • 缺乏多语言搜索能力: 期望网站搜索能够以用户舒适的语言找到内容。
  • 有限的“缩小”搜索方式: 有时,文本是不够的。图片胜过千言万语,如今多模态搜索不再是奢侈品。

在本文中,我们将探讨如何通过解决上述陷阱来改善电商搜索。在本文结尾,我们将提供一个演示电商应用程序,作为良好搜索的基础。

理解电商语义搜索

利用语义搜索来驱动我们的电商搜索可以解决我们详细描述的每个电商搜索陷阱,但语义搜索到底是什么?

语义搜索,也称为向量搜索,使用机器学习来理解数据的上下文,方法是将数据转换为数值向量。然后,它使用这些向量来基于概念相似性查找匹配项,从而获得准确、相关的搜索结果。

Semantic Search visualised

在幕后,它使用近似最近邻 (ANN) 算法来计算查询的近似最近邻,而不是 kNN 算法,后者计算真实的最近邻。

Semantic Search explained

使用向量数据库的语义搜索将消除最糟糕的响应,并确保对非关键词搜索的响应。第一个问题解决了。

构建用于电商的AI赋能搜索Web应用程序

我们将使用 Nuxt.js、Weaviate 和 Cohere 来构建我们的Web应用程序。

要求

你需要以下内容才能完成本教程。

  • Node.js 的LTS版本(Node 18+)
  • JavaScript 的基本知识
  • Git 的基本知识
  • Cohere 帐户

本项目基于的代码可在 GitHub 上找到,如果您想在完成教程之前尝试一下,请随意使用。

步骤 1:安装依赖项

使用以下命令创建您的 Nuxt.js 应用程序。

npx nuxi@latest init <project-name>

您还需要安装一些依赖项。Weaviate Typescript 客户端使我们在应用程序中使用 Weaviate 更容易,dotenv 用于处理密钥,zod 用于模式验证,tsc 和 typescript 用于编译我们的 Typescript 代码,以及 Tailwind CSS 用于样式设置。

npm install weaviate-client dotenv zod tsc typescript

由于我们将其作为 Nuxt 模块安装,因此我们需要为 tailwind 执行其他步骤。

npx nuxi@latest module add tailwindcss

之后,您应该将以下内容添加到您的 nuxt.config 文件中。

export default defineNuxtConfig({
modules: ['@nuxtjs/tailwindcss']
})

要设置 Typescript 和 Weaviate,请按照 客户端库文档 中详细说明的说明进行操作。

步骤 2:运行 Weaviate

要开始使用 Weaviate,我们将在 Weaviate 云服务上创建一个 Weaviate 实例,如 本指南 中所述。Weaviate 是一种 AI 原生数据库。它让您可以灵活地选择要使用的嵌入模型。嵌入模型有各种形状和大小,对于本项目,您将使用 Cohere 的多语言嵌入模型。这将使我们能够以多种语言运行搜索,从而克服其中一个陷阱。

设置完成后,将您的 Weaviate URL、Admin API 密钥和 Cohere API 密钥添加到项目根目录中的 .env 文件中。

您的 nuxt.config 文件应包含您的项目密钥,格式如下 所示

步骤 3:导入数据

我们将使用 Amazon Products Sales Dataset 2023。您可以在 Github 上找到清理后的版本的文件。下载它并将其放在您的 ./public 文件夹中。

在文件根目录下,创建一个名为 import 的文件夹。创建一个名为 simple.ts 的文件,作为您的导入脚本。

import weaviate, { type WeaviateClient } from "weaviate-client";
import 'dotenv/config'

import * as fs from 'fs';
import { join } from 'path';
import { parse } from 'csv-parse/sync';

let client: WeaviateClient | null = null;

async function initClient() {
if (!client) {
client = await weaviate.connectToWeaviateCloud(process.env.NUXT_WEAVIATE_URL || '', {
authCredentials: new weaviate.ApiKey(process.env.NUXT_WEAVIATE_API_KEY!!),
headers: {
"X-Cohere-Api-Key": process.env.NUXT_COHERE_API_KEY!!,
},
});
}

const ready = await client.isReady()
console.info('Client is ready?', ready)
return client
}

async function createCollection() {
client = await initClient()

const productsResponse = await client.collections.create({
name: "TestProduct",
vectorizers: weaviate.configure.vectorizer.text2VecCohere({
model: 'embed-multilingual-v3.0',
sourceProperties: ['name', 'sub_category']
}),
})

}

async function importProductData(fileName: string, collectionName: string) {
client = await initClient();
const filePath = join(process.cwd(), `./public/${fileName}`);
const content = await fs.readFileSync(filePath)

// Parse the CSV content
console.log('content', content)
const records = parse(content, { delimiter: ';' });

const myCollection = client.collections.get(collectionName);
let itemsToInsert = [];
let counter = 0;

for (const item of records) {
counter++
if (counter % 1000 == 0) {
console.log(`Import: ${counter}`);
}

itemsToInsert.push({
name: item[0],
main_category: item[1],
sub_category: item[2],
image: item[3],
link: item[4],
rating: item[5],
price: item[8]
});

// insert data in batches of 2k objects
if (itemsToInsert.length == 2000) {
const response = await myCollection.data.insertMany(itemsToInsert);
itemsToInsert = [];

if (response.hasErrors) {
throw new Error("Something went wrong in import!");
}
}

}


// insert the remaining objects
if (itemsToInsert.length > 0) {
const response = await myCollection.data.insertMany(itemsToInsert);

if (response.hasErrors) {
throw new Error("Something went wrong in import!");
}
}

return { status: "Import Complete" };
}


// Uncomment to create collection and import data before you run the script
// await createCollection()
// await importProductData('products.csv','TestProduct')

这会创建一个名为“TestProduct”的集合,该集合使用 Cohere 嵌入模型定义为向量化器。然后,它将我们 products.csv 文件中的数据导入到 Weaviate。

要运行我们的导入过程,我们需要将以下脚本添加到我们的 package.json 文件中。


"scripts": {

"import": "npx tsc && node import/simple.js"

},


现在我们可以运行 npm run import 来启动导入过程。根据您导入的数据量,可能需要一些时间。在运行的同时,让我们创建我们的搜索体验。

步骤 4:构建语义搜索功能

首先,我们将创建一个 API 路由以进行初始调用并显示产品。

server/api 中创建一个名为 init.ts 的文件并粘贴以下代码。

import weaviate, { WeaviateClient } from "weaviate-client"

export default defineLazyEventHandler(async () => {
const config = useRuntimeConfig()

const client: WeaviateClient = await weaviate.connectToWeaviateCloud(config.weaviateHostURL,{
authCredentials: new weaviate.ApiKey(config.weaviateReadKey),
headers: {
'X-Cohere-Api-Key': config.cohereApiKey,
}
}
)

async function initialFetch() {
const myProductCollection = client.collections.get('TestProduct')

const response = await myProductCollection.query.fetchObjects({ limit : 20 })

return response.objects

}

return defineEventHandler(async () => {

return await initialFetch()
})
})

这段代码调用 initialFetch(),它从 Weaviate 数据库中获取 20 个对象并显示它们,以便我们的用户在访问我们的页面时拥有产品。

接下来,在 server/api 中创建一个名为 search.ts 的文件

import weaviate, { WeaviateClient } from "weaviate-client"
import { z } from 'zod'

export default defineLazyEventHandler(async () => {
const config = useRuntimeConfig()

const client: WeaviateClient = await weaviate.connectToWeaviateCloud(config.weaviateHostURL,{
authCredentials: new weaviate.ApiKey(config.weaviateReadKey),
headers: {
'X-Cohere-Api-Key': config.cohereApiKey,
}
}
)

const responseSchema = z.object({
query: z.string(),
})

async function vectorSearch(searchTerm:string) {
const myProductCollection = client.collections.get('TestProduct')

const response = await myProductCollection.query.nearText(searchTerm, { limit : 10 })

return response.objects


}

return defineEventHandler<{query: { query: string } }>(async (event) => {

const result = await getValidatedQuery(event, body => responseSchema.safeParse(body))
if (!result.success)
throw result.error.issues

const searchTerm = result.data.query

return await vectorSearch(searchTerm)
})
})

为了给一切一个界面,在 app.vue 中粘贴以下代码。

<template>
<div>
<div class="bg-white text-gray-600 work-sans leading-normal text-base tracking-normal">
<section class="bg-white py-8 container py-8 px-6 mx-auto box pt-6 box-wrapper">
<div class=" bg-white rounded flex items-center w-full p-3 shadow-sm border border-gray-200">
<button class="outline-none focus:outline-none"><svg class=" w-5 text-gray-600 h-5 cursor-pointer"
fill="none" stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
stroke="currentColor" viewBox="0 0 24 24">
<path d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
</svg></button>
<input v-model="searchTerm" type="search" name="" id="" placeholder="beautifully gorpy salomon sneakers"
x-model="q" class="w-full pl-4 text-sm outline-none focus:outline-none bg-transparent">
<div>
<button @click="submitSearch"> Search
</button>
</div>
</div>
</section>

<section class="bg-white py-4">
<div class="container mx-auto flex items-center flex-wrap pb-12">
<div v-if="!searchMade">
<section>
<div class="mx-auto max-w-screen-xl px-4 py-8 sm:px-6 sm:py-4 lg:px-">

<div class="mt-4 lg:mt-8 lg:grid lg:grid-cols-4 lg:items-start lg:gap-8">

<div class="lg:col-span-3">
<ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div v-for="item in products">
<li>
<a href="#" class="group block overflow-hidden">
<img :src="item.properties.image" alt=""
class="h-[350px] w-[350px] object-none transition duration-500 group-hover:scale-105 sm:h-[350px]" />
<div class="relative bg-white pt-3">
<h3
class="text-xs text-gray-700 group-hover:underline group-hover:underline-offset-4">
{{ item.properties.name.slice(0, 80) }}...
</h3>

<p class="mt-2">
<span class="sr-only"> Regular Price </span>
<span class="tracking-wider text-gray-900">
${{ parseFloat(item.properties.price.replace("₹", ''))
}}</span>
</p>

</div>
</a>
<button @click="addToCart(item)" class="bg-blue-300 text-white p-1">Add to Cart</button>
</li>
</div>
</ul>
</div>
</div>
</div>
</section>
</div>

<div v-if="searchMade">
<section>
<div class="mx-auto max-w-screen-xl px-4 py-8 sm:px-6 sm:py-4 lg:px-">
<header>
<h2 class="text-xl font-bold text-gray-900 sm:text-3xl">Product Collection</h2>

<p class="mt-4 max-w-md text-gray-500"> gorp is a lifestyle, gorp is a state of mind, gorp. gorp.
</p>
</header>

<div class="mt-8 block lg:hidden">
<button
class="flex cursor-pointer items-center gap-2 border-b border-gray-400 pb-1 text-gray-900 transition hover:border-gray-600">
<span class="text-sm font-medium"> Filters & Sorting </span>

<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24"
stroke-width="1.5" stroke="currentColor" class="size-4 rtl:rotate-180">
<path stroke-linecap="round" stroke-linejoin="round"
d="M8.25 4.5l7.5 7.5-7.5 7.5" />
</svg>
</button>
</div>

<div class="mt-4 lg:mt-8 lg:grid lg:grid-cols-4 lg:items-start lg:gap-8">
<Sidebar />

<div class="lg:col-span-3">
<ul class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
<div v-for="item in searchResult">
<li>
<a href="#" class="group block overflow-hidden">
<img :src="item.properties.image" alt=""
class="h-[350px] w-[350px] object-none transition duration-500 group-hover:scale-105 sm:h-[350px]" />

<div class="relative bg-white pt-3">
<h3
class="text-xs text-gray-700 group-hover:underline group-hover:underline-offset-4">
{{ item.properties.name.slice(0, 80) }}...
</h3>

<p class="mt-2">
<span class="sr-only"> Regular Price </span>

<span class="tracking-wider text-gray-900">
${{ parseFloat(item.properties.price.replace("₹", ''))
}}</span>
</p>
</div>
</a>
<button @click="addToCart(item)" class="bg-blue-300 text-white p-1">Add to Cart</button>
</li>
</div>
</ul>
</div>
</div>
</div>
</section>
</div>
</div>
</section>

</div>
</div>
</template>

这会创建一个我们可以交互的界面,即搜索输入框和搜索按钮。

为了实际运行我们的搜索,在您的 <template> 标签下方,粘贴以下内容。

<script setup>
import { nextTick } from 'vue';

const searchMade = ref(false)
const loading = ref(false)
const searchTerm = ref('')
const searchResult = ref()

let products = ref({});

onMounted(async () => {
await nextTick();
products.value = await $fetch(`/api/init`)
});

async function submitSearch() {
searchResult.value = null
loading.value = true
searchResult.value = await $fetch(`/api/search?query=${searchTerm.value}`)
searchMade.value = true
loading.value = false
}

</script>

此片段调用我们之前创建的 API 路由,然后将用户的搜索作为查询传递。然后,我们在我们的网页上显示结果。

最终结果

Final Semantic Search Application for eCommerce demo

搜索“too much sunlight”会返回相关的产品。

搜索“randoneé glaceé”(法语,意思是冰上徒步旅行)会返回与冰上徒步旅行相关的产品,展示了多语言理解和满足非产品搜索的能力。

我们还剩下最后一个陷阱,语义搜索使我们能够利用多模态的优势,您可以在 此处 了解更多信息。

作为挑战,如果您能够在您的电商应用程序版本上使用多模态,请 通过 LinkedIn 向我发送消息,我们将向您发送一些小礼物。

结论

我们刚刚看到如何通过 AI 搜索略微改善我们的电商体验。甚至没有涉及 RAG 或推荐。如果您想查看此项目的完整实现,请在 Github 上找到它,并密切关注我们的博客,我们将发布第二部分,讨论电商中的 RAG。如果您发现这很有趣或想继续讨论,请在 malgamves 上与我联系。

准备开始构建了吗?

请查看 快速入门教程,或使用 Weaviate Cloud (WCD) 的免费试用版构建令人惊叹的应用程序。

不想错过另一篇博文?

注册我们的双周时事通讯以保持更新!


提交后,我同意 服务条款 隐私政策.