Skip to content

Commit ff0800a

Browse files
working HASH + JSON vector search
1 parent 07a9861 commit ff0800a

File tree

6 files changed

+226
-122
lines changed

6 files changed

+226
-122
lines changed

lib/entity/fields/entity-binary-field.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,19 @@ import { EntityValue } from "../entity-value";
55
export class EntityBinaryField extends EntityField {
66
toRedisJson(): RedisJsonData {
77
const data: RedisJsonData = {};
8-
if (this.value !== null) data[this.name] = [...this.valueAsBuffer]
8+
if (this.value !== null) {
9+
const bytes = this.valueAsBuffer
10+
const arr = new Float32Array(bytes.buffer, bytes.byteOffset, bytes.length / Float32Array.BYTES_PER_ELEMENT)
11+
data[this.name] = [...arr]
12+
}
913
return data;
1014
}
1115

1216
fromRedisJson(value: any) {
1317
if (!this.isBuffer(value)) {
1418
throw Error(`Non-binary value of '${value}' read from Redis for binary field.`)
1519
}
16-
this.value = Buffer.from([...value]);
20+
this.value = value
1721
}
1822

1923
toRedisHash(): RedisHashData {

spec/functional/search/products.ts

Lines changed: 22 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { createClient } from 'redis';
2+
3+
import { products } from './products';
4+
import { Client } from '$lib/client';
5+
import { Schema } from '$lib/schema/schema';
6+
import { Entity } from '$lib/entity/entity';
7+
import { Repository } from '$lib/repository';
8+
import { removeAll } from '../helpers/redis-helper';
9+
10+
describe("Vector HASH", () => {
11+
let redis: ReturnType<typeof createClient>
12+
let client: Client
13+
let repository: Repository<Product>
14+
let entityIDs: string[]
15+
16+
// define the interface, just for TypeScript
17+
interface Product {
18+
name: string;
19+
price: number;
20+
image: Buffer;
21+
}
22+
23+
// define the entity class and add any business logic to it
24+
class Product extends Entity {
25+
}
26+
27+
beforeAll(async () => {
28+
// establish an existing connection to Redis
29+
redis = createClient();
30+
redis.on('error', (err) => console.log('Redis Client Error', err));
31+
await redis.connect();
32+
33+
// get a client use an existing Redis connection
34+
client = await new Client().use(redis);
35+
36+
await removeAll(client, 'ProductHASH:')
37+
38+
entityIDs = []
39+
})
40+
41+
afterAll(async () => {
42+
await removeAll(client, 'ProductHASH:')
43+
44+
await repository.dropIndex();
45+
46+
// close the client
47+
await client.close()
48+
})
49+
50+
it("demo", async () => {
51+
let schema = new Schema<Product>(
52+
Product, {
53+
name: { type: 'text' },
54+
price: { type: 'number' },
55+
image: { type: 'binary', vector: { algorithm: 'FLAT', dim: 512, distance_metric: 'COSINE', initial_cap: 5, block_size: 5 } },
56+
}, {
57+
prefix: 'ProductHASH',
58+
dataStructure: 'HASH',
59+
});
60+
61+
repository = client.fetchRepository<Product>(schema);
62+
63+
await repository.createIndex();
64+
65+
async function loadProduct(product: { name: string, price: number, image: string }) {
66+
let entity = await repository.createEntity();
67+
entity.name = product.name
68+
entity.price = product.price
69+
entity.image = Buffer.from(product.image, 'hex')
70+
return await repository.save(entity);
71+
}
72+
73+
for (const product of products) {
74+
const entityID = await loadProduct(product)
75+
entityIDs.push(entityID)
76+
}
77+
78+
// TODO: figure out rawSearch / where query encoding is happening ...
79+
80+
// execute a raw search for the first product image ...
81+
const results = await redis.sendCommand([
82+
'FT.SEARCH', 'ProductHASH:index', '*=>[KNN 2 @image $query_vector]', 'PARAMS', '2',
83+
'query_vector', Buffer.from(products[0].image, 'hex'),
84+
'RETURN', '3', '__image_score", "name", "price',
85+
'SORTBY', '__image_score',
86+
'DIALECT', '2'
87+
]) as any[]
88+
89+
// ... and we should get the first 2 products back in order
90+
expect(results).toBeDefined()
91+
expect(results).toBeInstanceOf(Array)
92+
expect(results.length).toBe(5)
93+
expect(results[1]).toBe('ProductHASH:' + entityIDs[0])
94+
expect(parseFloat(results[2][1])).toBeLessThan(1e-4)
95+
expect(results[3]).toBe('ProductHASH:' + entityIDs[1])
96+
expect(parseFloat(results[4][1])).toBeGreaterThan(0.2)
97+
});
98+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { createClient } from 'redis';
2+
3+
import { products } from './products';
4+
import { Client } from '$lib/client';
5+
import { Schema } from '$lib/schema/schema';
6+
import { Entity } from '$lib/entity/entity';
7+
import { Repository } from '$lib/repository';
8+
import { removeAll } from '../helpers/redis-helper';
9+
10+
describe("Vector JSON", () => {
11+
let redis: ReturnType<typeof createClient>
12+
let client: Client
13+
let repository: Repository<Product>
14+
let entityIDs: string[]
15+
16+
// define the interface, just for TypeScript
17+
interface Product {
18+
name: string;
19+
price: number;
20+
image: Buffer;
21+
}
22+
23+
// define the entity class and add any business logic to it
24+
class Product extends Entity {
25+
}
26+
27+
beforeAll(async () => {
28+
// establish an existing connection to Redis
29+
redis = createClient();
30+
redis.on('error', (err) => console.log('Redis Client Error', err));
31+
await redis.connect();
32+
33+
// get a client use an existing Redis connection
34+
client = await new Client().use(redis);
35+
36+
await removeAll(client, 'ProductJSON:')
37+
38+
entityIDs = []
39+
})
40+
41+
afterAll(async () => {
42+
await removeAll(client, 'ProductJSON:')
43+
44+
await repository.dropIndex();
45+
46+
// close the client
47+
await client.close()
48+
})
49+
50+
it("demo", async () => {
51+
let schema = new Schema<Product>(
52+
Product, {
53+
name: { type: 'text' },
54+
price: { type: 'number' },
55+
image: { type: 'binary', vector: { algorithm: 'FLAT', dim: 512, distance_metric: 'COSINE', initial_cap: 5, block_size: 5 } },
56+
}, {
57+
prefix: 'ProductJSON',
58+
dataStructure: 'JSON',
59+
});
60+
61+
repository = client.fetchRepository<Product>(schema);
62+
63+
await repository.createIndex();
64+
65+
async function loadProduct(product: { name: string, price: number, image: string }) {
66+
let entity = await repository.createEntity();
67+
entity.name = product.name
68+
entity.price = product.price
69+
entity.image = Buffer.from(product.image, 'hex')
70+
return await repository.save(entity);
71+
}
72+
73+
for (const product of products) {
74+
const entityID = await loadProduct(product)
75+
entityIDs.push(entityID)
76+
}
77+
78+
// TODO: figure out rawSearch / where query encoding is happening ...
79+
80+
// execute a raw search for the first product image ...
81+
const results = await redis.sendCommand([
82+
'FT.SEARCH', 'ProductJSON:index', '*=>[KNN 2 @image $query_vector]', 'PARAMS', '2',
83+
'query_vector', Buffer.from(products[0].image, 'hex'),
84+
'RETURN', '3', '__image_score", "name", "price',
85+
'SORTBY', '__image_score',
86+
'DIALECT', '2'
87+
]) as any[]
88+
89+
// ... and we should get the first 2 products back in order
90+
expect(results).toBeDefined()
91+
expect(results).toBeInstanceOf(Array)
92+
expect(results.length).toBe(5)
93+
expect(results[1]).toBe('ProductJSON:' + entityIDs[0])
94+
expect(parseFloat(results[2][1])).toBeLessThan(1e-4)
95+
expect(results[3]).toBe('ProductJSON:' + entityIDs[1])
96+
expect(parseFloat(results[4][1])).toBeGreaterThan(0.2)
97+
});
98+
});

0 commit comments

Comments
 (0)