本教程是关于利用 Redis 在 NodeJS 环境中进行向量相似性搜索的综合指南。本教程面向具有 NodeJS/JavaScript 生态系统专业知识的软件开发人员,将为您提供进行高级向量操作所需的知识和技术。以下是涵盖的内容
在机器学习的背景下,向量是数据的数学表示。它是一个有序的数字列表,对数据片段的特征或属性进行编码。
向量可以被认为是多维空间中的点,其中每个维度对应一个特征。 例如,考虑关于电子商务 产品
的简单数据集。每个产品可能具有 价格
、 质量
和 人气
等特征。
ID | 产品 | 价格 (美元) | 质量 (1 - 10) | 人气 (1 - 10) |
1 | Puma 男士 Race 黑色手表 | 150 | 5 | 8 |
2 | Puma 男士 Top Fluctuation 红黑手表 | 180 | 7 | 6 |
3 | Inkfruit 女士 Behind 奶油色 T 恤 | 5 | 9 | 7 |
现在,产品 1 Puma 男士 Race 黑色手表
可以用向量 [150, 5, 8]
表示
在更复杂的情况下,例如自然语言处理 (NLP),单词或整个句子可以转换为密集向量(通常称为嵌入),这些向量捕获文本的语义含义。向量在许多机器学习算法中起着基础性作用,特别是那些涉及距离测量的算法,例如聚类和分类算法。
向量数据库是一种专门的系统,针对存储和搜索向量进行了优化。这些数据库明确设计为高效,在为向量搜索应用程序提供动力方面发挥着至关重要的作用,包括推荐系统、图像搜索和文本内容检索。这些数据库通常被称为向量存储、向量索引或向量搜索引擎,它们采用向量相似性算法来识别与给定查询向量密切匹配的向量。
Redis Cloud 是向量数据库的流行选择,因为它提供了一套丰富的适合向量存储和搜索的数据结构和命令。Redis Cloud 允许您对向量进行索引并在本教程中进一步介绍的不同方式执行向量相似性搜索。它还保持着高性能和可扩展性。
向量相似度是一种量化两个向量之间相似程度的指标,通常通过评估多维空间中向量之间的距离
或角度
来实现。当向量代表数据点(如文本或图像)时,相似度得分可以表明底层数据点在特征或内容方面的相似程度。
向量相似度的用例
如果您想了解更多关于向量相似度背后的数学知识,请向下滚动到 如何计算向量相似度? 部分。
在我们的场景中,我们的重点是生成句子(产品描述)和图像(产品图像)嵌入或向量。像 GitHub 这样的 AI 模型仓库有很多,在那里预先训练、维护和共享 AI 模型。
对于句子嵌入,我们将使用来自 Hugging Face 模型中心的模型,而对于图像嵌入,我们将使用来自 TensorFlow 集中库 的模型,以提供多样性。
下面是克隆本教程中使用的源代码的命令
git clone https://github.com/redis-developer/redis-vector-nodejs-solutions.git
为了生成句子嵌入,我们将使用一个名为 Xenova/all-distilroberta-v1的 Hugging Face 模型。它是 sentence-transformers/all-distilroberta-v1 的兼容版本,适用于带有 ONNX 权重的 transformer.js。
Hugging Face Transformers 是用于自然语言处理 (NLP) 任务的知名开源工具。它简化了尖端 NLP 模型的使用。
transformers.j 库本质上是 Hugging Face 广受欢迎的 Python 库的 JavaScript 版本。
ONNX(开放神经网络交换) 是一种开放标准,定义了表示各种框架(包括 PyTorch 和 TensorFlow)中的深度学习模型的通用运算符集和通用文件格式
下面,您将找到一个 Node.js 代码片段,它说明了如何为任何提供的 句子
创建向量嵌入
npm install @xenova/transformers
import * as transformers from '@xenova/transformers';
async function generateSentenceEmbeddings(_sentence): Promise<number[]> {
let modelName = 'Xenova/all-distilroberta-v1';
let pipe = await transformers.pipeline('feature-extraction', modelName);
let vectorOutput = await pipe(_sentence, {
pooling: 'mean',
normalize: true,
});
const embeddings: number[] = Object.values(vectorOutput?.data);
return embeddings;
}
export { generateSentenceEmbeddings };
以下是示例文本的向量输出
const embeddings = await generateSentenceEmbeddings('I Love Redis !');
console.log(embeddings);
/*
768 dim vector output
embeddings = [
-0.005076113156974316, -0.006047232076525688, -0.03189406543970108,
-0.019677048549056053, 0.05152582749724388, -0.035989608615636826,
-0.009754283353686333, 0.002385444939136505, -0.04979122802615166,
....]
*/
为了获取图像嵌入,我们将利用 TensorFlow 中的 mobilenet 模型。
下面,您将找到一个 Node.js 代码片段,它说明了如何为任何提供的 图像创建向量嵌入
npm i @tensorflow/tfjs @tensorflow/tfjs-node @tensorflow-models/mobilenet jpeg-js
import * as tf from '@tensorflow/tfjs-node';
import * as mobilenet from '@tensorflow-models/mobilenet';
import * as jpeg from 'jpeg-js';
import * as path from 'path';
import { fileURLToPath } from 'url';
import * as fs from 'fs/promises';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
async function decodeImage(imagePath) {
imagePath = path.join(__dirname, imagePath);
const buffer = await fs.readFile(imagePath);
const rawImageData = jpeg.decode(buffer);
const imageTensor = tf.browser.fromPixels(rawImageData);
return imageTensor;
}
async function generateImageEmbeddings(imagePath: string) {
const imageTensor = await decodeImage(imagePath);
// Load MobileNet model
const model = await mobilenet.load();
// Classify and predict what the image is
const prediction = await model.classify(imageTensor);
console.log(`${imagePath} prediction`, prediction);
// Preprocess the image and get the intermediate activation.
const activation = model.infer(imageTensor, true);
// Convert the tensor to a regular array.
const vectorOutput = await activation.data();
imageTensor.dispose(); // Clean up tensor
return vectorOutput; //DIM 1024
}
以下是示例手表图像的向量输出:
//watch image
const imageEmbeddings = await generateImageEmbeddings('images/11001.jpg');
console.log(imageEmbeddings);
/*
1024 dim vector output
imageEmbeddings = [
0.013823275454342365, 0.33256298303604126, 0,
2.2764432430267334, 0.14010703563690186, 0.972867488861084,
1.2307443618774414, 2.254523992538452, 0.44696325063705444,
....]
images/11001.jpg (mobilenet model) prediction [
{ className: 'digital watch', probability: 0.28117117285728455 },
{ className: 'spotlight, spot', probability: 0.15369531512260437 },
{ className: 'digital clock', probability: 0.15267866849899292 }
]
*/
下面是克隆本教程中使用的源代码的命令
git clone https://github.com/redis-developer/redis-vector-nodejs-solutions.git
在本教程中,让我们考虑一个简化的电子商务上下文。提供的 products
JSON 提供了我们将在讨论的向量搜索功能的概述。
const products = [
{
_id: '1',
price: 4950,
productDisplayName: 'Puma Men Race Black Watch',
brandName: 'Puma',
ageGroup: 'Adults-Men',
gender: 'Men',
masterCategory: 'Accessories',
subCategory: 'Watches',
imageURL: 'images/11002.jpg',
productDescription:
'<p>This watch from puma comes in a heavy duty design. The asymmetric dial and chunky casing gives this watch a tough appearance perfect for navigating the urban jungle.<br /><strong><br />Dial shape</strong>: Round<br /><strong>Case diameter</strong>: 32 cm<br /><strong>Warranty</strong>: 2 Years<br /><br />Stainless steel case with a fixed bezel for added durability, style and comfort<br />Leather straps with a tang clasp for comfort and style<br />Black dial with cat logo on the 12 hour mark<br />Date aperture at the 3 hour mark<br />Analog time display<br />Solid case back made of stainless steel for enhanced durability<br />Water resistant upto 100 metres</p>',
},
{
_id: '2',
price: 5450,
productDisplayName: 'Puma Men Top Fluctuation Red Black Watches',
brandName: 'Puma',
ageGroup: 'Adults-Men',
gender: 'Men',
masterCategory: 'Accessories',
subCategory: 'Watches',
imageURL: 'images/11001.jpg',
productDescription:
'<p style="text-align: justify;">This watch from puma comes in a clean sleek design. This active watch is perfect for urban wear and can serve you well in the gym or a night of clubbing.<br /><strong><br />Case diameter</strong>: 40 mm<</p>',
},
{
_id: '3',
price: 499,
productDisplayName: 'Inkfruit Women Behind Cream Tshirts',
brandName: 'Inkfruit',
ageGroup: 'Adults-Women',
gender: 'Women',
masterCategory: 'Apparel',
subCategory: 'Topwear',
imageURL: 'images/11008.jpg',
productDescription:
'<p><strong>Composition</strong><br />Yellow round neck t-shirt made of 100% cotton, has short sleeves and graphic print on the front<br /><br /><strong>Fitting</strong><br />Comfort<br /><br /><strong>Wash care</strong><br />Hand wash separately in cool water at 30 degrees <br />Do not scrub <br />Do not bleach <br />Turn inside out and dry flat in shade <br />Warm iron on reverse<br />Do not iron on print<br /><br />Flaunt your pretty, long legs in style with this inkfruit t-shirt. The graphic print oozes sensuality, while the cotton fabric keeps you fresh and comfortable all day. Team this with a short denim skirt and high-heeled sandals and get behind the wheel in style.<br /><br /><em>Model statistics</em><br />The model wears size M in t-shirts <br />Height: 5\'7", Chest: 33"</p>',
},
];
以下是将 products 数据作为 JSON 播种到 Redis 中的示例代码。该数据还包含产品描述和图像的向量。
async function addProductWithEmbeddings(_products) {
const nodeRedisClient = getNodeRedisClient();
if (_products && _products.length) {
for (let product of _products) {
console.log(
`generating description embeddings for product ${product._id}`,
);
const sentenceEmbedding = await generateSentenceEmbeddings(
product.productDescription,
);
product['productDescriptionEmbeddings'] = sentenceEmbedding;
console.log(`generating image embeddings for product ${product._id}`);
const imageEmbedding = await generateImageEmbeddings(product.imageURL);
product['productImageEmbeddings'] = imageEmbedding;
await nodeRedisClient.json.set(`products:${product._id}`, '$', {
...product,
});
console.log(`product ${product._id} added to redis`);
}
}
}
您可以在 RedisInsight 中观察 products JSON 数据
下载 RedisInsight 以可视化地浏览您的 Redis 数据或在工作台中使用原始 Redis 命令。
为了对 Redis 中的 JSON 字段进行搜索,必须对它们进行索引。以下方法突出显示了索引不同类型字段的过程。这包括向量字段,例如 productDescriptionEmbeddings
和 productImageEmbeddings
。
import {
createClient,
SchemaFieldTypes,
VectorAlgorithms,
RediSearchSchema,
} from 'redis';
const PRODUCTS_KEY_PREFIX = 'products';
const PRODUCTS_INDEX_KEY = 'idx:products';
const REDIS_URI = 'redis://localhost:6379';
let nodeRedisClient = null;
const getNodeRedisClient = async () => {
if (!nodeRedisClient) {
nodeRedisClient = createClient({ url: REDIS_URI });
await nodeRedisClient.connect();
}
return nodeRedisClient;
};
const createRedisIndex = async () => {
/* (RAW COMMAND)
FT.CREATE idx:products
ON JSON
PREFIX 1 "products:"
SCHEMA
"$.productDisplayName" as productDisplayName TEXT NOSTEM SORTABLE
"$.brandName" as brandName TEXT NOSTEM SORTABLE
"$.price" as price NUMERIC SORTABLE
"$.masterCategory" as "masterCategory" TAG
"$.subCategory" as subCategory TAG
"$.productDescriptionEmbeddings" as productDescriptionEmbeddings VECTOR "FLAT" 10
"TYPE" FLOAT32
"DIM" 768
"DISTANCE_METRIC" "L2"
"INITIAL_CAP" 111
"BLOCK_SIZE" 111
"$.productDescription" as productDescription TEXT NOSTEM SORTABLE
"$.imageURL" as imageURL TEXT NOSTEM
"$.productImageEmbeddings" as productImageEmbeddings VECTOR "HNSW" 8
"TYPE" FLOAT32
"DIM" 1024
"DISTANCE_METRIC" "COSINE"
"INITIAL_CAP" 111
*/
const nodeRedisClient = await getNodeRedisClient();
const schema: RediSearchSchema = {
'$.productDisplayName': {
type: SchemaFieldTypes.TEXT,
NOSTEM: true,
SORTABLE: true,
AS: 'productDisplayName',
},
'$.brandName': {
type: SchemaFieldTypes.TEXT,
NOSTEM: true,
SORTABLE: true,
AS: 'brandName',
},
'$.price': {
type: SchemaFieldTypes.NUMERIC,
SORTABLE: true,
AS: 'price',
},
'$.masterCategory': {
type: SchemaFieldTypes.TAG,
AS: 'masterCategory',
},
'$.subCategory': {
type: SchemaFieldTypes.TAG,
AS: 'subCategory',
},
'$.productDescriptionEmbeddings': {
type: SchemaFieldTypes.VECTOR,
TYPE: 'FLOAT32',
ALGORITHM: VectorAlgorithms.FLAT,
DIM: 768,
DISTANCE_METRIC: 'L2',
INITIAL_CAP: 111,
BLOCK_SIZE: 111,
AS: 'productDescriptionEmbeddings',
},
'$.productDescription': {
type: SchemaFieldTypes.TEXT,
NOSTEM: true,
SORTABLE: true,
AS: 'productDescription',
},
'$.imageURL': {
type: SchemaFieldTypes.TEXT,
NOSTEM: true,
AS: 'imageURL',
},
'$.productImageEmbeddings': {
type: SchemaFieldTypes.VECTOR,
TYPE: 'FLOAT32',
ALGORITHM: VectorAlgorithms.HNSW, //Hierarchical Navigable Small World graphs
DIM: 1024,
DISTANCE_METRIC: 'COSINE',
INITIAL_CAP: 111,
AS: 'productImageEmbeddings',
},
};
console.log(`index ${PRODUCTS_INDEX_KEY} created`);
try {
await nodeRedisClient.ft.dropIndex(PRODUCTS_INDEX_KEY);
} catch (indexErr) {
console.error(indexErr);
}
await nodeRedisClient.ft.create(PRODUCTS_INDEX_KEY, schema, {
ON: 'JSON',
PREFIX: PRODUCTS_KEY_PREFIX,
});
};
FLAT:当向量以“FLAT”结构进行索引时,它们会以原始形式存储,没有任何附加的层次结构。对 FLAT 索引的搜索将要求算法线性扫描每个向量,以找到最相似的匹配项。虽然这很准确,但它计算量大且速度较慢,使其成为较小数据集的理想选择。
HNSW(分层可导航小世界):HNSW 是一种以图形为中心的算法,专为索引高维数据而设计。对于较大的数据集,对索引中每个向量进行线性比较会很耗时。HNSW 采用概率方法,确保更快的搜索结果,但准确性略有下降。
INITIAL_CAP 和 BLOCK_SIZE 都是控制向量存储和索引方式的配置参数。
INITIAL_CAP 定义向量索引的初始容量。它有助于为索引预先分配空间。
BLOCK_SIZE 定义向量索引中每个块的大小。随着更多向量的添加,Redis 将以块的形式分配内存,每个块的大小为 BLOCK_SIZE。它有助于在索引增长期间优化内存分配。
KNN 或 k 近邻,是用于分类和回归任务的算法,但当提到“KNN 搜索”时,我们通常指的是在数据集中找到与给定查询点最近(最相似)的“k”个点的任务。在向量搜索的背景下,这意味着识别数据库中与给定查询向量最相似的“k”个向量,通常基于某种距离度量,例如余弦相似度或欧几里德距离。
Redis 允许您对向量进行索引,然后使用 KNN 方法进行搜索。
下面,您将找到一个 Node.js 代码片段,它说明了如何为任何提供的 搜索文本
执行 KNN 查询
const float32Buffer = (arr) => {
const floatArray = new Float32Array(arr);
const float32Buffer = Buffer.from(floatArray.buffer);
return float32Buffer;
};
const queryProductDescriptionEmbeddingsByKNN = async (
_searchTxt,
_resultCount,
) => {
//A KNN query will give us the top n documents that best match the query vector.
/* sample raw query
FT.SEARCH idx:products
"*=>[KNN 5 @productDescriptionEmbeddings $searchBlob AS score]"
RETURN 4 score brandName productDisplayName imageURL
SORTBY score
PARAMS 2 searchBlob "6\xf7\..."
DIALECT 2
*/
//https://redis.ac.cn/docs/interact/search-and-query/query/
console.log(`queryProductDescriptionEmbeddingsByKNN started`);
let results = {};
if (_searchTxt) {
_resultCount = _resultCount ?? 5;
const nodeRedisClient = getNodeRedisClient();
const searchTxtVectorArr = await generateSentenceEmbeddings(_searchTxt);
const searchQuery = `*=>[KNN ${_resultCount} @productDescriptionEmbeddings $searchBlob AS score]`;
results = await nodeRedisClient.ft.search(PRODUCTS_INDEX_KEY, searchQuery, {
PARAMS: {
searchBlob: float32Buffer(searchTxtVectorArr),
},
RETURN: ['score', 'brandName', 'productDisplayName', 'imageURL'],
SORTBY: {
BY: 'score',
// DIRECTION: "DESC"
},
DIALECT: 2,
});
} else {
throw 'Search text cannot be empty';
}
return results;
};
请在 Redis 中找到 KNN 查询的输出 (输出中较低的得分或距离表示更高的相似度。)
const result = await queryProductDescriptionEmbeddingsByKNN(
'Puma watch with cat', //search text
3, //max number of results expected
);
console.log(JSON.stringify(result, null, 4));
/*
{
"total": 3,
"documents": [
{
"id": "products:1",
"value": {
"score": "0.762174725533",
"brandName": "Puma",
"productDisplayName": "Puma Men Race Black Watch",
"imageURL": "images/11002.jpg"
}
},
{
"id": "products:2",
"value": {
"score": "0.825711071491",
"brandName": "Puma",
"productDisplayName": "Puma Men Top Fluctuation Red Black Watches",
"imageURL": "images/11001.jpg"
}
},
{
"id": "products:3",
"value": {
"score": "1.79949247837",
"brandName": "Inkfruit",
"productDisplayName": "Inkfruit Women Behind Cream Tshirts",
"imageURL": "images/11008.jpg"
}
}
]
}
*/
KNN 查询可以使用 混合查询与标准 Redis 搜索功能结合使用
范围查询检索落在指定值范围内的数据。对于向量,"范围查询"通常指的是检索与目标向量一定距离内的所有向量。"范围"在这种情况下是指向量空间中的半径。
下面,您将找到一个 Node.js 代码片段,它说明了如何为任何提供的范围(半径/距离)执行向量 范围查询
const queryProductDescriptionEmbeddingsByRange = async (_searchTxt, _range) => {
/* sample raw query
FT.SEARCH idx:products
"@productDescriptionEmbeddings:[VECTOR_RANGE $searchRange $searchBlob]=>{$YIELD_DISTANCE_AS: score}"
RETURN 4 score brandName productDisplayName imageURL
SORTBY score
PARAMS 4 searchRange 0.685 searchBlob "A=\xe1\xbb\x8a\xad\x...."
DIALECT 2
*/
console.log(`queryProductDescriptionEmbeddingsByRange started`);
let results = {};
if (_searchTxt) {
_range = _range ?? 1.0;
const nodeRedisClient = getNodeRedisClient();
const searchTxtVectorArr = await generateSentenceEmbeddings(_searchTxt);
const searchQuery =
'@productDescriptionEmbeddings:[VECTOR_RANGE $searchRange $searchBlob]=>{$YIELD_DISTANCE_AS: score}';
results = await nodeRedisClient.ft.search(PRODUCTS_INDEX_KEY, searchQuery, {
PARAMS: {
searchBlob: float32Buffer(searchTxtVectorArr),
searchRange: _range,
},
RETURN: ['score', 'brandName', 'productDisplayName', 'imageURL'],
SORTBY: {
BY: 'score',
// DIRECTION: "DESC"
},
DIALECT: 2,
});
} else {
throw 'Search text cannot be empty';
}
return results;
};
请在 Redis 中找到范围查询的输出
const result2 = await queryProductDescriptionEmbeddingsByRange(
'Puma watch with cat', //search text
1.0, //with in range or distance
);
console.log(JSON.stringify(result2, null, 4));
/*
{
"total": 2,
"documents": [
{
"id": "products:1",
"value": {
"score": "0.762174725533",
"brandName": "Puma",
"productDisplayName": "Puma Men Race Black Watch",
"imageURL": "images/11002.jpg"
}
},
{
"id": "products:2",
"value": {
"score": "0.825711071491",
"brandName": "Puma",
"productDisplayName": "Puma Men Top Fluctuation Red Black Watches",
"imageURL": "images/11001.jpg"
}
}
]
}
*/
示例正文向量 KNN/范围查询的语法一致,无论您是使用图像向量还是文本向量。就像存在名为 queryProductDescriptionEmbeddingsByKNN
的文本向量查询方法一样,在代码库中也存在对应于图像的 queryProductImageEmbeddingsByKNN
方法。
示例正文下面是克隆本教程中使用的源代码的命令
git clone https://github.com/redis-developer/redis-vector-nodejs-solutions.git
希望本教程能帮助您了解如何使用 Redis 进行向量相似度搜索。
(可选)如果您还想了解向量相似度搜索背后的数学原理,请阅读以下内容
有多种技术可用于评估向量相似度,其中一些最流行的技术包括
欧几里德距离(L2 范数) 计算多维空间中两点之间的线性距离。较低的值表示更接近,因此相似度更高。
为了说明,让我们评估来自早期电子商务数据集的 产品 1
和 产品 2
并确定 欧几里德距离
,同时考虑所有特征。
例如,我们将使用 chart.js 制作的二维图表来比较我们产品的 价格与质量
特征,只关注这两个属性来计算 欧几里德距离
。
余弦相似度 测量两个向量之间角度的余弦。余弦相似度值介于 -1 和 1 之间。更接近 1 的值表示更小的角度和更高的相似度,而更接近 -1 的值表示更大的角度和更低的相似度。余弦相似度在处理文本向量时,在 NLP 中尤其受欢迎。
如果两个向量指向相同方向,它们之间的角度的余弦值为 1。如果它们是正交的,则余弦值为 0,如果它们指向相反方向,则余弦值为 -1。
再次考虑之前数据集中 产品 1
和 产品 2
,并计算所有特征的 余弦距离
。
使用 chart.js,我们制作了一个 价格与质量
特征的二维图表。它仅根据这些属性可视化 余弦相似度
。
内积(点积) 内积(或点积)不是传统意义上的距离度量,但可用于计算相似度,尤其是在向量被归一化(具有 1 的大小)时。它是两个数字序列的对应项的乘积的总和。
内积可以被认为是在给定向量空间中两个向量“对齐”程度的度量。较高的值表示较高的相似度。然而,对于较长的向量,原始值可能会很大;因此,建议进行归一化以更好地解释。如果向量被归一化,它们的点积将是 1 如果它们相同
和 0 如果它们是正交的
(不相关)。
考虑到我们的 产品 1
和 产品 2
,让我们计算所有特征的 内积
。
向量也可以以 二进制格式 存储在数据库中以节省空间。在实际应用中,在向量的维数(影响存储和计算成本)与它们捕获的信息的质量或粒度之间取得平衡至关重要。