diff --git a/lib/plan-builder-base.js b/lib/plan-builder-base.js index 95313193..1e802640 100644 --- a/lib/plan-builder-base.js +++ b/lib/plan-builder-base.js @@ -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'; @@ -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)}` diff --git a/lib/plan-builder-generated.js b/lib/plan-builder-generated.js index 39e73208..16955d63 100755 --- a/lib/plan-builder-generated.js +++ b/lib/plan-builder-generated.js @@ -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'; @@ -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} @@ -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} diff --git a/test-basic/optic-vector.js b/test-basic/optic-vector.js index bc29330d..b552ba1a 100644 --- a/test-basic/optic-vector.js +++ b/test-basic/optic-vector.js @@ -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; @@ -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)); }); diff --git a/test-basic/plan-builder-generated.js b/test-basic/plan-builder-generated.js index 678c24a0..ca5dee68 100755 --- a/test-basic/plan-builder-generated.js +++ b/test-basic/plan-builder-generated.js @@ -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'; @@ -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); });