Skip to content

Commit 42588ce

Browse files
authored
Merge pull request #1048 from marklogic/feature/26338-add-vec-precision-trunc
MLE-26340 expose vec.precision and vec.trunc in Node client.
2 parents ad3e64a + 3024522 commit 42588ce

File tree

4 files changed

+157
-58
lines changed

4 files changed

+157
-58
lines changed

lib/plan-builder-base.js

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44
'use strict';
55

@@ -58,8 +58,11 @@ function castArg(arg, funcName, paramName, argPos, paramTypes) {
5858
} else if (arg instanceof Number || arg instanceof Boolean || arg instanceof String) {
5959
arg = arg.valueOf();
6060
} else if (arg instanceof types.ServerType) {
61+
// We added new VecVector ServerType which is not a sub-type of Item. This makes it a one-off type
62+
// as paramTypes will not include VecVector in any other type checks.
63+
// And if we get here the arg must be and only be a VecVector (arg._ns === 'vec') or we throw an Error
6164
if(arg._ns === 'vec'){
62-
return arg._args;
65+
return arg;
6366
}
6467
throw new Error(
6568
`${argLabel(funcName, paramName, argPos)} must have type ${typeLabel(paramTypes)}`

lib/plan-builder-generated.js

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44
'use strict';
55

@@ -5458,6 +5458,23 @@ normalize(...args) {
54585458
const paramdef = ['vector1', [types.VecVector, PlanColumn, PlanParam], false, false];
54595459
const checkedArgs = bldrbase.makeSingleArgs('vec.normalize', 1, paramdef, args);
54605460
return new types.VecVector('vec', 'normalize', checkedArgs);
5461+
}
5462+
/**
5463+
* Returns a new vector which is a copy of the input vector with reduced precision. The precision reduction is achieved by clearing the bottom (32 - precision) bits of the mantissa for each dimension's float value. This can be useful for reducing storage requirements or for creating approximate vector representations. Provides a client interface to a server function. See {@link http://docs.marklogic.com/vec.precision|vec.precision}
5464+
* @method planBuilder.vec#precision
5465+
* @since 4.1.0
5466+
* @param { VecVector } [vector] - The input vector to reduce precision.
5467+
* @param { XsUnsignedInt } [precision] - The number of mantissa bits to preserve (9-32 inclusive). Default is 16. Higher values preserve more precision. If the value is outside the valid range, throw VEC-INVALIDPRECISION.
5468+
* @returns { VecVector }
5469+
*/
5470+
precision(...args) {
5471+
const namer = bldrbase.getNamer(args, 'vector');
5472+
const paramdefs = [['vector', [types.VecVector, PlanColumn, PlanParam], false, false], ['precision', [types.XsUnsignedInt, PlanColumn, PlanParam], false, false]];
5473+
const checkedArgs = (namer !== null) ?
5474+
bldrbase.makeNamedArgs(namer, 'vec.precision', 1, new Set(['vector', 'precision']), paramdefs, args) :
5475+
bldrbase.makePositionalArgs('vec.precision', 1, false, paramdefs, args);
5476+
return new types.VecVector('vec', 'precision', checkedArgs);
5477+
54615478
}
54625479
/**
54635480
* Returns the difference of two vectors. The vectors must be of the same dimension. Provides a client interface to a server function. See {@link http://docs.marklogic.com/vec.subtract|vec.subtract}
@@ -5493,6 +5510,23 @@ subvector(...args) {
54935510
bldrbase.makePositionalArgs('vec.subvector', 2, false, paramdefs, args);
54945511
return new types.VecVector('vec', 'subvector', checkedArgs);
54955512

5513+
}
5514+
/**
5515+
* Returns a new vector which is a copy of the input vector with each element truncated to a specific number of digits. Provides a client interface to a server function. See {@link http://docs.marklogic.com/vec.trunc|vec.trunc}
5516+
* @method planBuilder.vec#trunc
5517+
* @since 4.1.0
5518+
* @param { VecVector } [vector] - The input vector to truncate.
5519+
* @param { XsInt } [n] - The numbers of decimal places to truncate to. The default is 0. Negative values cause that many digits to the left of the decimal point to be truncated.
5520+
* @returns { VecVector }
5521+
*/
5522+
trunc(...args) {
5523+
const namer = bldrbase.getNamer(args, 'vector');
5524+
const paramdefs = [['vector', [types.VecVector, PlanColumn, PlanParam], false, false], ['n', [types.XsInt, PlanColumn, PlanParam], false, false]];
5525+
const checkedArgs = (namer !== null) ?
5526+
bldrbase.makeNamedArgs(namer, 'vec.trunc', 1, new Set(['vector', 'n']), paramdefs, args) :
5527+
bldrbase.makePositionalArgs('vec.trunc', 1, false, paramdefs, args);
5528+
return new types.VecVector('vec', 'trunc', checkedArgs);
5529+
54965530
}
54975531
/**
54985532
* Returns a vector value. Provides a client interface to a server function. See {@link http://docs.marklogic.com/vec.vector|vec.vector}

test-basic/optic-vector.js

Lines changed: 112 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
/*
2-
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44
'use strict';
55

66
const should = require('should');
77

88
const marklogic = require('../');
9-
const p = marklogic.planBuilder;
9+
const op = marklogic.planBuilder;
1010

1111
const pbb = require('./plan-builder-base');
1212
const execPlan = pbb.execPlan;
@@ -34,129 +34,189 @@ describe('tests for new vector functions.', function() {
3434
});
3535

3636
it('vec.add', function(done) {
37-
const vec1 = p.vec.vector([0.000000000001]);
38-
const vec2 = p.vec.vector([0.000000000001, 0.000000000002]);
39-
testPlan([""],p.vec.add(p.vec.subvector(vec1,0),p.vec.subvector(vec2,1)))
37+
const vec1 = op.vec.vector([1]);
38+
const vec2 = op.vec.vector([2]);
39+
testPlan([""],op.vec.add(vec1, vec2))
4040
.then(function(response) {
41-
assert(response.rows[0].t.value[0][0] =='1e-12')
42-
assert(response.rows[0].t.value[1][0]=='2e-12')
41+
// add([1], [2]) = [3] (element-wise addition)
42+
assert(response.rows[0].t.value[0] == 3, 'vec.add did not return expected value: [1] + [2] should be [3]');
4343
done();
4444
}).catch(error => done(error));
4545
});
4646

4747
it('vec.subtract', function(done) {
48-
const vec1 = p.vec.vector([0.000000000002]);
49-
const vec2 = p.vec.vector([0.000000000001]);
50-
testPlan([""],p.vec.subtract(p.vec.subvector(vec1,0),p.vec.subvector(vec2,0)))
48+
const vec1 = op.vec.vector([2]);
49+
const vec2 = op.vec.vector([1]);
50+
testPlan([""],op.vec.subtract(vec1, vec2))
5151
.then(function(response) {
52-
assert(response.rows[0].t.value[0][0] =='2e-12')
53-
assert(response.rows[0].t.value[1][0]=='1e-12')
52+
// vec.subtract([2], [1]) = [1] (element-wise subtraction)
53+
assert(response.rows[0].t.value[0] == 1, 'vec.subtract did not return expected value: [2] - [1] should be [1]');
5454
done();
5555
}).catch(error => done(error));
5656
});
5757

58-
it('vec.base64decode', function(done) {
59-
60-
const vec1 = p.vec.vector([0.002]);
61-
testPlan([""],p.vec.subvector(p.vec.base64Decode(p.vec.base64Encode(p.vec.subvector(vec1,0))),0))
58+
it('vec.base64Decode', function(done) {
59+
const vec1 = op.vec.vector([0.002]);
60+
testPlan([""],op.vec.subvector(op.vec.base64Decode(op.vec.base64Encode(op.vec.subvector(vec1,0))),0))
6261
.then(function(response) {
63-
assert(response.rows[0].t.value[0][0] =='0.002')
62+
// Round-trip encode/decode returns [0.002]
63+
assert(response.rows[0].t.value[0] == 0.002, 'vec.base64Decode did not return expected value for vector [0.002] after round-trip encode/decode');
6464
done();
6565
}).catch(error => done(error));
6666
});
6767

6868
it('vec.base64Encode', function(done) {
69-
const vec1 = p.vec.vector([0.002]);
70-
testPlan([""],p.vec.base64Encode(p.vec.subvector(vec1,0)))
69+
testPlan([""],op.vec.base64Encode(op.vec.vector([0.002])))
7170
.then(function(response) {
72-
assert(response.rows[0].t.value =='AAAAAAEAAABvEgM7')
71+
// qconsole shows that encoding vector vec.base64Encode([0.002]) returns 'AAAAAAEAAABvEgM7'
72+
assert(response.rows[0].t.value =='AAAAAAEAAABvEgM7', 'vec.base64Encode did not return expected value for vector [0.002] which should be "AAAAAAEAAABvEgM7"');
7373
done();
7474
}).catch(error => done(error));
7575
});
7676

7777
it('vec.cosine', function(done) {
78-
const vec1 = p.vec.vector([1, 2, 3])
79-
const vec2 = p.vec.vector([4, 5, 6,7])
78+
// orthogonal vectors should have cosine similarity of 0, round down to 0 to be sure (floats)
79+
const vec1 = op.vec.vector([1, 1])
80+
const vec2 = op.vec.vector([-1, 1])
8081

81-
testPlan([""],p.vec.cosine(p.vec.subvector(vec1,0),p.vec.subvector(vec2,1)))
82+
testPlan([""],op.math.floor(op.vec.cosine(vec1, vec2)))
8283
.then(function(response) {
83-
assert(response.rows[0].t.value != null);
84+
assert(response.rows[0].t.value == 0, 'Cosine similarity between orthogonal vectors should be 0');
85+
}).catch(error => done(error));
86+
87+
// cosine similarity between subvectors [1,2,3] and [5,6,7] should be approximately 0.968329 (to 6 decimal places)
88+
testPlan([""],op.math.trunc(op.vec.cosine(op.vec.vector([1,2,3]),op.vec.vector([5,6,7])), 6))
89+
.then(function(response) {
90+
assert(response.rows[0].t.value == '0.968329', 'Cosine (directional similarity) between vectors [1,2,3] and [5,6,7] should be approximately 0.968329');
91+
}).catch(error => done(error));
92+
93+
// cosine similarity between subvectors [1,2,3] and [5,6,7] should be approximately 0.96832
94+
// and cosine distance should be approximately 0.03167 which is 1 - cosine similarity.
95+
testPlan([""],op.vec.vector([
96+
op.math.trunc(op.vec.cosine(op.vec.vector([1,2,3]),op.vec.vector([5,6,7])), 5),
97+
op.math.trunc(op.vec.cosineDistance(op.vec.vector([1,2,3]),op.vec.vector([5,6,7])), 5),
98+
]))
99+
.then(function(response) {
100+
assert(response.rows[0].t.value[0] == '0.96832', 'Cosine (directional similarity) between subvectors [1,2,3] and [5,6,7] should be approximately 0.96833.');
101+
assert(response.rows[0].t.value[1] == '0.03167', 'Cosine distance between subvectors [1,2,3] and [5,6,7] should be approximately 0.03167, or 1 - cosine similarity.');
84102
done();
85103
}).catch(error => done(error));
104+
105+
86106
});
87107

88108
it('vec.dimension', function(done) {
89-
90-
testPlan([""],p.vec.dimension(p.vec.vector([1, 2, 3])))
109+
testPlan([""],op.vec.dimension(op.vec.vector([1, 2, 3])))
91110
.then(function(response) {
92-
assert(response.rows[0].t.value == 3);
111+
assert(response.rows[0].t.value == 3, 'Dimension of vector [1,2,3] should be 3');
93112
done();
94113
}).catch(error => done(error));
95114
});
96115

97-
it('vec.dotproduct', function(done) {
98-
const vec1 = p.vec.vector([1, 2, 3])
99-
const vec2 = p.vec.vector([4, 5, 6,7])
116+
it('vec.dotProduct', function(done) {
117+
const vec1 = op.vec.vector([1, 2, 3])
118+
const vec2 = op.vec.vector([4, 5, 6, 7])
100119

101-
testPlan([""],p.vec.cosine(p.vec.subvector(vec1,0),p.vec.subvector(vec2,1)))
120+
// Dot product between subvectors [1,2,3] and [5,6,7] is (1*5) + (2*6) + (3*7) = 5 + 12 + 21 = 38
121+
testPlan([""],op.vec.dotProduct(
122+
op.vec.subvector(vec1,0),
123+
op.vec.subvector(vec2,1,3))
124+
)
102125
.then(function(response) {
103-
assert(response.rows[0].t.value == '0.968329608440399');
126+
assert(response.rows[0].t.value == 38, 'Dot product between subvectors [1,2,3] and [5,6,7] should be 38');
104127
done();
105128
}).catch(error => done(error));
106129
});
107130

108131
it('vec.euclideanDistance', function(done) {
109-
const vec1 = p.vec.vector([1, 2, 3])
110-
const vec2 = p.vec.vector([4, 5, 6,7])
111-
112-
testPlan([""],p.vec.euclideanDistance(p.vec.subvector(vec1,0,2),p.vec.subvector(vec2,1,2)))
132+
const vec1 = op.vec.vector([1, 2, 3])
133+
const vec2 = op.vec.vector([4, 5, 6,7])
134+
135+
// Euclidean distance between subvectors [1,2] and [5,6] is
136+
// sqrt((1-5)^2 + (2-6)^2) = sqrt(16 + 16) = sqrt(32)
137+
// This is approx 5.65685
138+
testPlan([""], op.math.trunc(
139+
op.vec.euclideanDistance(
140+
op.vec.vector([1,2]),
141+
op.vec.vector([5,6]))
142+
, 5))
113143
.then(function(response) {
114-
assert(response.rows[0].t.value == '5.65685415267944');
144+
assert(response.rows[0].t.value == '5.65685', 'Euclidean distance between subvectors [1,2] and [5,6] should be approximately 5.65685, trunc() to 5 decimal places');
115145
done();
116146
}).catch(error => done(error));
117147
});
118148

119149
it('vec.get', function(done) {
120-
121-
testPlan([""],p.vec.get(p.vec.vector([1, 2, 3]),1))
150+
testPlan([""],op.vec.get(op.vec.vector([1, 2, 3]),1))
122151
.then(function(response) {
123-
assert(response.rows[0].t.value == 2);
152+
assert(response.rows[0].t.value == 2, 'Element at index 1 of vector [1,2,3] should be 2');
124153
done();
125154
}).catch(error => done(error));
126155
});
127156

128157
it('vec.magnitude', function(done) {
129-
130-
testPlan([""],p.vec.magnitude(p.vec.vector([1, 2, 3])))
158+
testPlan([""],op.math.trunc(op.vec.magnitude(op.vec.vector([1, 2, 3])), 5))
131159
.then(function(response) {
132-
assert(response.rows[0].t.value == '3.74165749549866');
160+
// sqrt(1^2 + 2^2 + 3^2) = sqrt(14) ~= 3.74165 to 5 decimal places
161+
assert(response.rows[0].t.value == '3.74165', 'Magnitude of [1,2,3] should be sqrt(1 + 4 + 9) ~= 3.74165 (trunc to 5 decimal places)');
133162
done();
134163
}).catch(error => done(error));
135164
});
136165

137166
it('vec.normalize', function(done) {
138-
139-
testPlan([""],(p.vec.normalize(p.vec.subvector(p.vec.vector([1,2]),1))))
167+
testPlan([""],(op.vec.normalize(op.vec.vector([1,2]))))
140168
.then(function(response) {
141-
assert(response.rows[0].t.value == 2);
169+
// normalize([1,2]) = [1/sqrt(5), 2/sqrt(5)]
170+
// value[0] = 1/sqrt(5) ~= 0.447214
171+
// value[1] = 2/sqrt(5) ~= 0.894427
172+
assert(response.rows[0].t.value[0] == 0.447214, 'Normalized first element of vector [1,2] should be approximately 0.447214');
173+
assert(response.rows[0].t.value[1] == 0.894427, 'Normalized second element of vector [1,2] should be approximately 0.894427');
142174
done();
143175
}).catch(error => done(error));
144176
});
145177

146178
it('vec.vectorScore', function(done) {
147-
const vec1 = p.vec.vector([1, 2, 3])
148-
testPlan([""],(p.vec.vectorScore(24684,0.1,0.1)))
179+
testPlan([""],(op.vec.vectorScore(24684,0.1,0.1)))
149180
.then(function(response) {
150-
assert(response.rows[0].t.value == 113124);
181+
assert(response.rows[0].t.value == 113124, 'vectorScore(24684,0.1,0.1) should be 113124');
151182
done();
152183
}).catch(error => done(error));
153184
});
154185

155186
it('vec.cosineDistance', function(done) {
156-
testPlan([""],(p.vec.cosineDistance(p.vec.subvector(p.vec.vector([1, 2, 3]),0),
157-
p.vec.subvector(p.vec.vector([4, 5, 6,7]),1))))
187+
// return a vector with two cosine distance calculations: one between identical direction vectors,
188+
// one between opposite direction vectors
189+
testPlan([""],(op.vec.vector([
190+
op.vec.cosineDistance(op.vec.vector([2, 2]), op.vec.vector([1, 1])),
191+
op.vec.cosineDistance(op.vec.vector([2, 2]), op.vec.vector([-3, -3]))
192+
])))
193+
.then(function(response) {
194+
assert(response.rows[0].t.value[0] == 0, 'Cosine distance should be 0 between identical direction vectors [2,2] and [1,1]');
195+
assert(response.rows[0].t.value[1] == 2, 'Cosine distance should be 2 between opposite direction vectors [2,2] and [-3,-3]');
196+
done();
197+
}).catch(error => done(error));
198+
});
199+
200+
it('vec.precision', function(done) {
201+
// return a vector with the values of pi, e, and sqrt(2) by truncation; we expect to see [3, 2, 1]
202+
testPlan([""],op.vec.precision(op.vec.vector([3.14159265, 2.71828182, 1.41421356]), 10))
203+
.then(function(response) {
204+
assert(response.rows[0].t.value != null);
205+
assert(response.rows[0].t.value[0] == 3);
206+
assert(response.rows[0].t.value[1] == 2);
207+
assert(response.rows[0].t.value[2] == 1);
208+
done();
209+
}).catch(error => done(error));
210+
});
211+
212+
it('vec.trunc', function(done) {
213+
// return a vector with the values of 1.123456789, 2.123456789, 3.123456789 truncated to 1 decimal place;
214+
// we expect to see [1.1, 2.1, 3.1]
215+
testPlan([""],(op.vec.trunc(op.vec.vector([1.123456789, 2.123456789, 3.123456789]), 1)))
158216
.then(function(response) {
159-
assert(response.rows[0].t.value == 0.0316703915596008);
217+
assert(response.rows[0].t.value[0] == 1.1);
218+
assert(response.rows[0].t.value[1] == 2.1);
219+
assert(response.rows[0].t.value[2] == 3.1);
160220
done();
161221
}).catch(error => done(error));
162222
});

test-basic/plan-builder-generated.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
2+
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
33
*/
44
'use strict';
55

@@ -1727,9 +1727,11 @@ describe('plan builder', function() {
17271727
if(serverConfiguration.serverVersion < 12) {
17281728
this.skip();
17291729
}
1730-
testPlan([p.xs.string("abc")], p.vec.base64Decode(p.col("1")))
1730+
// AAAAAAMAAAAAAIA/AAAAQAAAQEA= is the ML base64 encoding string of the vector [1,2,3]
1731+
// Run vec.base64Encode(vec.vector([1,2,3])) in query console to generate
1732+
testPlan([p.xs.string("AAAAAAMAAAAAAIA/AAAAQAAAQEA=")],p.vec.base64Decode(p.col("1")))
17311733
.then(function(response) {
1732-
should(String(getResult(response).value).replace(/^ /, '')).equal('abc');
1734+
should(String(getResult(response).value)).equal('1,2,3');
17331735
done();
17341736
}).catch(done);
17351737
});

0 commit comments

Comments
 (0)