Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions lib/plan-builder-base.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';

Expand Down Expand Up @@ -58,8 +58,11 @@ function castArg(arg, funcName, paramName, argPos, paramTypes) {
} else if (arg instanceof Number || arg instanceof Boolean || arg instanceof String) {
arg = arg.valueOf();
} else if (arg instanceof types.ServerType) {
// We added new VecVector ServerType which is not a sub-type of Item. This makes it a one-off type
// as paramTypes will not include VecVector in any other type checks.
// And if we get here the arg must be and only be a VecVector (arg._ns === 'vec') or we throw an Error
if(arg._ns === 'vec'){
return arg._args;
return arg;
}
throw new Error(
`${argLabel(funcName, paramName, argPos)} must have type ${typeLabel(paramTypes)}`
Expand Down
36 changes: 35 additions & 1 deletion lib/plan-builder-generated.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';

Expand Down Expand Up @@ -5458,6 +5458,23 @@ normalize(...args) {
const paramdef = ['vector1', [types.VecVector, PlanColumn, PlanParam], false, false];
const checkedArgs = bldrbase.makeSingleArgs('vec.normalize', 1, paramdef, args);
return new types.VecVector('vec', 'normalize', checkedArgs);
}
/**
* 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}
* @method planBuilder.vec#precision
* @since 4.1.0
* @param { VecVector } [vector] - The input vector to reduce precision.
* @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.
* @returns { VecVector }
*/
precision(...args) {
const namer = bldrbase.getNamer(args, 'vector');
const paramdefs = [['vector', [types.VecVector, PlanColumn, PlanParam], false, false], ['precision', [types.XsUnsignedInt, PlanColumn, PlanParam], false, false]];
const checkedArgs = (namer !== null) ?
bldrbase.makeNamedArgs(namer, 'vec.precision', 1, new Set(['vector', 'precision']), paramdefs, args) :
bldrbase.makePositionalArgs('vec.precision', 1, false, paramdefs, args);
return new types.VecVector('vec', 'precision', checkedArgs);

}
/**
* 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}
Expand Down Expand Up @@ -5493,6 +5510,23 @@ subvector(...args) {
bldrbase.makePositionalArgs('vec.subvector', 2, false, paramdefs, args);
return new types.VecVector('vec', 'subvector', checkedArgs);

}
/**
* 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}
* @method planBuilder.vec#trunc
* @since 4.1.0
* @param { VecVector } [vector] - The input vector to truncate.
* @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.
* @returns { VecVector }
*/
trunc(...args) {
const namer = bldrbase.getNamer(args, 'vector');
const paramdefs = [['vector', [types.VecVector, PlanColumn, PlanParam], false, false], ['n', [types.XsInt, PlanColumn, PlanParam], false, false]];
const checkedArgs = (namer !== null) ?
bldrbase.makeNamedArgs(namer, 'vec.trunc', 1, new Set(['vector', 'n']), paramdefs, args) :
bldrbase.makePositionalArgs('vec.trunc', 1, false, paramdefs, args);
return new types.VecVector('vec', 'trunc', checkedArgs);

}
/**
* Returns a vector value. Provides a client interface to a server function. See {@link http://docs.marklogic.com/vec.vector|vec.vector}
Expand Down
164 changes: 112 additions & 52 deletions test-basic/optic-vector.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';

const should = require('should');

const marklogic = require('../');
const p = marklogic.planBuilder;
const op = marklogic.planBuilder;

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

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

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

it('vec.base64decode', function(done) {

const vec1 = p.vec.vector([0.002]);
testPlan([""],p.vec.subvector(p.vec.base64Decode(p.vec.base64Encode(p.vec.subvector(vec1,0))),0))
it('vec.base64Decode', function(done) {
const vec1 = op.vec.vector([0.002]);
testPlan([""],op.vec.subvector(op.vec.base64Decode(op.vec.base64Encode(op.vec.subvector(vec1,0))),0))
.then(function(response) {
assert(response.rows[0].t.value[0][0] =='0.002')
// Round-trip encode/decode returns [0.002]
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');
done();
}).catch(error => done(error));
});

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

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

testPlan([""],p.vec.cosine(p.vec.subvector(vec1,0),p.vec.subvector(vec2,1)))
testPlan([""],op.math.floor(op.vec.cosine(vec1, vec2)))
.then(function(response) {
assert(response.rows[0].t.value != null);
assert(response.rows[0].t.value == 0, 'Cosine similarity between orthogonal vectors should be 0');
}).catch(error => done(error));

// cosine similarity between subvectors [1,2,3] and [5,6,7] should be approximately 0.968329 (to 6 decimal places)
testPlan([""],op.math.trunc(op.vec.cosine(op.vec.vector([1,2,3]),op.vec.vector([5,6,7])), 6))
.then(function(response) {
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');
}).catch(error => done(error));

// cosine similarity between subvectors [1,2,3] and [5,6,7] should be approximately 0.96832
// and cosine distance should be approximately 0.03167 which is 1 - cosine similarity.
testPlan([""],op.vec.vector([
op.math.trunc(op.vec.cosine(op.vec.vector([1,2,3]),op.vec.vector([5,6,7])), 5),
op.math.trunc(op.vec.cosineDistance(op.vec.vector([1,2,3]),op.vec.vector([5,6,7])), 5),
]))
.then(function(response) {
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.');
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.');
done();
}).catch(error => done(error));


});

it('vec.dimension', function(done) {

testPlan([""],p.vec.dimension(p.vec.vector([1, 2, 3])))
testPlan([""],op.vec.dimension(op.vec.vector([1, 2, 3])))
.then(function(response) {
assert(response.rows[0].t.value == 3);
assert(response.rows[0].t.value == 3, 'Dimension of vector [1,2,3] should be 3');
done();
}).catch(error => done(error));
});

it('vec.dotproduct', function(done) {
const vec1 = p.vec.vector([1, 2, 3])
const vec2 = p.vec.vector([4, 5, 6,7])
it('vec.dotProduct', function(done) {
const vec1 = op.vec.vector([1, 2, 3])
const vec2 = op.vec.vector([4, 5, 6, 7])

testPlan([""],p.vec.cosine(p.vec.subvector(vec1,0),p.vec.subvector(vec2,1)))
// Dot product between subvectors [1,2,3] and [5,6,7] is (1*5) + (2*6) + (3*7) = 5 + 12 + 21 = 38
testPlan([""],op.vec.dotProduct(
op.vec.subvector(vec1,0),
op.vec.subvector(vec2,1,3))
)
.then(function(response) {
assert(response.rows[0].t.value == '0.968329608440399');
assert(response.rows[0].t.value == 38, 'Dot product between subvectors [1,2,3] and [5,6,7] should be 38');
done();
}).catch(error => done(error));
});

it('vec.euclideanDistance', function(done) {
const vec1 = p.vec.vector([1, 2, 3])
const vec2 = p.vec.vector([4, 5, 6,7])

testPlan([""],p.vec.euclideanDistance(p.vec.subvector(vec1,0,2),p.vec.subvector(vec2,1,2)))
const vec1 = op.vec.vector([1, 2, 3])
const vec2 = op.vec.vector([4, 5, 6,7])

// Euclidean distance between subvectors [1,2] and [5,6] is
// sqrt((1-5)^2 + (2-6)^2) = sqrt(16 + 16) = sqrt(32)
// This is approx 5.65685
testPlan([""], op.math.trunc(
op.vec.euclideanDistance(
op.vec.vector([1,2]),
op.vec.vector([5,6]))
, 5))
.then(function(response) {
assert(response.rows[0].t.value == '5.65685415267944');
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');
done();
}).catch(error => done(error));
});

it('vec.get', function(done) {

testPlan([""],p.vec.get(p.vec.vector([1, 2, 3]),1))
testPlan([""],op.vec.get(op.vec.vector([1, 2, 3]),1))
.then(function(response) {
assert(response.rows[0].t.value == 2);
assert(response.rows[0].t.value == 2, 'Element at index 1 of vector [1,2,3] should be 2');
done();
}).catch(error => done(error));
});

it('vec.magnitude', function(done) {

testPlan([""],p.vec.magnitude(p.vec.vector([1, 2, 3])))
testPlan([""],op.math.trunc(op.vec.magnitude(op.vec.vector([1, 2, 3])), 5))
.then(function(response) {
assert(response.rows[0].t.value == '3.74165749549866');
// sqrt(1^2 + 2^2 + 3^2) = sqrt(14) ~= 3.74165 to 5 decimal places
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)');
done();
}).catch(error => done(error));
});

it('vec.normalize', function(done) {

testPlan([""],(p.vec.normalize(p.vec.subvector(p.vec.vector([1,2]),1))))
testPlan([""],(op.vec.normalize(op.vec.vector([1,2]))))
.then(function(response) {
assert(response.rows[0].t.value == 2);
// normalize([1,2]) = [1/sqrt(5), 2/sqrt(5)]
// value[0] = 1/sqrt(5) ~= 0.447214
// value[1] = 2/sqrt(5) ~= 0.894427
assert(response.rows[0].t.value[0] == 0.447214, 'Normalized first element of vector [1,2] should be approximately 0.447214');
assert(response.rows[0].t.value[1] == 0.894427, 'Normalized second element of vector [1,2] should be approximately 0.894427');
done();
}).catch(error => done(error));
});

it('vec.vectorScore', function(done) {
const vec1 = p.vec.vector([1, 2, 3])
testPlan([""],(p.vec.vectorScore(24684,0.1,0.1)))
testPlan([""],(op.vec.vectorScore(24684,0.1,0.1)))
.then(function(response) {
assert(response.rows[0].t.value == 113124);
assert(response.rows[0].t.value == 113124, 'vectorScore(24684,0.1,0.1) should be 113124');
done();
}).catch(error => done(error));
});

it('vec.cosineDistance', function(done) {
testPlan([""],(p.vec.cosineDistance(p.vec.subvector(p.vec.vector([1, 2, 3]),0),
p.vec.subvector(p.vec.vector([4, 5, 6,7]),1))))
// return a vector with two cosine distance calculations: one between identical direction vectors,
// one between opposite direction vectors
testPlan([""],(op.vec.vector([
op.vec.cosineDistance(op.vec.vector([2, 2]), op.vec.vector([1, 1])),
op.vec.cosineDistance(op.vec.vector([2, 2]), op.vec.vector([-3, -3]))
])))
.then(function(response) {
assert(response.rows[0].t.value[0] == 0, 'Cosine distance should be 0 between identical direction vectors [2,2] and [1,1]');
assert(response.rows[0].t.value[1] == 2, 'Cosine distance should be 2 between opposite direction vectors [2,2] and [-3,-3]');
done();
}).catch(error => done(error));
});

it('vec.precision', function(done) {
// return a vector with the values of pi, e, and sqrt(2) by truncation; we expect to see [3, 2, 1]
testPlan([""],op.vec.precision(op.vec.vector([3.14159265, 2.71828182, 1.41421356]), 10))
.then(function(response) {
assert(response.rows[0].t.value != null);
assert(response.rows[0].t.value[0] == 3);
assert(response.rows[0].t.value[1] == 2);
assert(response.rows[0].t.value[2] == 1);
done();
}).catch(error => done(error));
});

it('vec.trunc', function(done) {
// return a vector with the values of 1.123456789, 2.123456789, 3.123456789 truncated to 1 decimal place;
// we expect to see [1.1, 2.1, 3.1]
testPlan([""],(op.vec.trunc(op.vec.vector([1.123456789, 2.123456789, 3.123456789]), 1)))
.then(function(response) {
assert(response.rows[0].t.value == 0.0316703915596008);
assert(response.rows[0].t.value[0] == 1.1);
assert(response.rows[0].t.value[1] == 2.1);
assert(response.rows[0].t.value[2] == 3.1);
done();
}).catch(error => done(error));
});
Expand Down
8 changes: 5 additions & 3 deletions test-basic/plan-builder-generated.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/*
* Copyright (c) 2015-2025 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
* Copyright (c) 2015-2026 Progress Software Corporation and/or its subsidiaries or affiliates. All Rights Reserved.
*/
'use strict';

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