From 41a7fd81183215871c119445bd625abeca03cc67 Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:29:43 +0800 Subject: [PATCH 01/10] feat(timeseries): add generalized timeseries extension header - EMBEDDING_SIMILARITY: generic N-dimension cosine/feature similarity - DETECT_DRIFT_POINTS: generic drift detection on sequential embeddings (generalized from bitemporal's character_similarity + detect_turning_points) --- .../include/function/timeseries_function.h | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 timeseries/src/include/function/timeseries_function.h diff --git a/timeseries/src/include/function/timeseries_function.h b/timeseries/src/include/function/timeseries_function.h new file mode 100644 index 0000000..8c0a6d1 --- /dev/null +++ b/timeseries/src/include/function/timeseries_function.h @@ -0,0 +1,21 @@ +#pragma once + +#include "function/function.h" + +namespace lbug { +namespace timeseries_extension { + +// ===== Function registration structs ===== + +struct EmbeddingSimilarityFunction { + static constexpr const char* name = "EMBEDDING_SIMILARITY"; + static function::function_set getFunctionSet(); +}; + +struct DetectDriftPointsFunction { + static constexpr const char* name = "DETECT_DRIFT_POINTS"; + static function::function_set getFunctionSet(); +}; + +} // namespace timeseries_extension +} // namespace lbug From 5bc015252f12adbe43e2039673ab8772a5897ccc Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:30:06 +0800 Subject: [PATCH 02/10] feat(timeseries): add extension header --- .../src/include/main/timeseries_extension.h | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 timeseries/src/include/main/timeseries_extension.h diff --git a/timeseries/src/include/main/timeseries_extension.h b/timeseries/src/include/main/timeseries_extension.h new file mode 100644 index 0000000..5b8d889 --- /dev/null +++ b/timeseries/src/include/main/timeseries_extension.h @@ -0,0 +1,19 @@ +#pragma once + +#include "extension/extension.h" + +namespace lbug { +namespace main { +class ClientContext; +} + +namespace timeseries_extension { + +class TimeseriesExtension { +public: + static constexpr const char* EXTENSION_NAME = "timeseries"; + static void load(main::ClientContext* context); +}; + +} // namespace timeseries_extension +} // namespace lbug From c60596832027e02904899bcbcd3fcd3e81b4fb30 Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:30:25 +0800 Subject: [PATCH 03/10] feat(timeseries): add extension loader --- timeseries/src/main/timeseries_extension.cpp | 36 ++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 timeseries/src/main/timeseries_extension.cpp diff --git a/timeseries/src/main/timeseries_extension.cpp b/timeseries/src/main/timeseries_extension.cpp new file mode 100644 index 0000000..2cbf237 --- /dev/null +++ b/timeseries/src/main/timeseries_extension.cpp @@ -0,0 +1,36 @@ +#include "main/timeseries_extension.h" + +#include "extension/extension.h" +#include "function/timeseries_function.h" +#include "main/client_context.h" + +namespace lbug { +namespace timeseries_extension { + +using namespace lbug::extension; + +void TimeseriesExtension::load(main::ClientContext* context) { + auto& db = *context->getDatabase(); + ExtensionUtils::addTableFunc(db); + ExtensionUtils::addTableFunc(db); +} + +} // namespace timeseries_extension +} // namespace lbug + +#if defined(BUILD_DYNAMIC_LOAD) +extern "C" { +#if defined(_WIN32) +#define INIT_EXPORT __declspec(dllexport) +#else +#define INIT_EXPORT __attribute__((visibility("default"))) +#endif +INIT_EXPORT void init(lbug::main::ClientContext* context) { + lbug::timeseries_extension::TimeseriesExtension::load(context); +} + +INIT_EXPORT const char* name() { + return lbug::timeseries_extension::TimeseriesExtension::EXTENSION_NAME; +} +} +#endif From f7b065a5d2e53b71f09da63c2e5fbf7e908c4c7b Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:30:55 +0800 Subject: [PATCH 04/10] feat(timeseries): add CMakeLists.txt --- timeseries/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 timeseries/CMakeLists.txt diff --git a/timeseries/CMakeLists.txt b/timeseries/CMakeLists.txt new file mode 100644 index 0000000..6751eec --- /dev/null +++ b/timeseries/CMakeLists.txt @@ -0,0 +1,9 @@ +include_directories( + ${PROJECT_SOURCE_DIR}/src/include + ${CMAKE_BINARY_DIR}/src/include + src/include) + +add_subdirectory(src/main) +add_subdirectory(src/function) + +build_extension_lib(${BUILD_STATIC_EXTENSION} "timeseries") From 982701d3c3de72c1825cb0e97fcb6de7c70aa886 Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:31:22 +0800 Subject: [PATCH 05/10] feat(timeseries): add function CMakeLists --- timeseries/src/function/CMakeLists.txt | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 timeseries/src/function/CMakeLists.txt diff --git a/timeseries/src/function/CMakeLists.txt b/timeseries/src/function/CMakeLists.txt new file mode 100644 index 0000000..7e7828c --- /dev/null +++ b/timeseries/src/function/CMakeLists.txt @@ -0,0 +1,9 @@ +add_library(lbug_timeseries_function + OBJECT + embedding_similarity.cpp + detect_drift_points.cpp + ) + +set(TIMESERIES_EXTENSION_OBJECT_FILES + ${TIMESERIES_EXTENSION_OBJECT_FILES} $ + PARENT_SCOPE) From 3ed8dd20f4c225187d84536c62e0970a27831712 Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:31:35 +0800 Subject: [PATCH 06/10] feat(timeseries): add main CMakeLists --- timeseries/src/main/CMakeLists.txt | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 timeseries/src/main/CMakeLists.txt diff --git a/timeseries/src/main/CMakeLists.txt b/timeseries/src/main/CMakeLists.txt new file mode 100644 index 0000000..9460f36 --- /dev/null +++ b/timeseries/src/main/CMakeLists.txt @@ -0,0 +1,8 @@ +add_library(timeseries_extension_main + OBJECT + timeseries_extension.cpp + ) + +set(TIMESERIES_EXTENSION_OBJECT_FILES + ${TIMESERIES_EXTENSION_OBJECT_FILES} $ + PARENT_SCOPE) From 0444e9beec8081b8439ed1953b323d55608a418e Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:32:48 +0800 Subject: [PATCH 07/10] =?UTF-8?q?feat(timeseries):=20add=20EMBEDDING=5FSIM?= =?UTF-8?q?ILARITY=20=E2=80=94=20generic=20N-dimension=20cosine=20similari?= =?UTF-8?q?ty?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalized from character_similarity (hardcoded 4 features) to accept embeddings of any dimension. Returns cosine similarity + mean feature similarity + dimension count. --- .../src/function/embedding_similarity.cpp | 129 ++++++++++++++++++ 1 file changed, 129 insertions(+) create mode 100644 timeseries/src/function/embedding_similarity.cpp diff --git a/timeseries/src/function/embedding_similarity.cpp b/timeseries/src/function/embedding_similarity.cpp new file mode 100644 index 0000000..457ad3f --- /dev/null +++ b/timeseries/src/function/embedding_similarity.cpp @@ -0,0 +1,129 @@ +// EMBEDDING_SIMILARITY — generic N-dimension feature similarity +// +// CALL embedding_similarity( +// vec_a LIST, -- first embedding +// vec_b LIST, -- second embedding +// ) RETURN similarity_score DOUBLE, dimension_count INT64 +// +// Algorithm: +// 1. Compute cosine similarity: dot(a,b) / (|a| × |b|) +// 2. Compute per-dimension feature similarity: 1 - |a-b| / max(|a|,|b|,eps) +// 3. Return cosine similarity as primary score + dimension count +// +// Generalized from bitemporal's character_similarity (which was hardcoded to 4 dimensions). +// Works with embeddings of any dimension (768-dim text, 384-dim code, etc.) + +#include "binder/binder.h" +#include "function/table/bind_data.h" +#include "function/table/bind_input.h" +#include "function/table/simple_table_function.h" +#include "function/timeseries_function.h" +#include "main/client_context.h" +#include "processor/execution_context.h" +#include +#include +#include + +using namespace lbug::binder; +using namespace lbug::common; +using namespace lbug::function; +using namespace lbug::processor; + +namespace lbug { namespace timeseries_extension { + +struct ESBD final : TableFuncBindData { + std::vector vecA; + std::vector vecB; + ESBD(std::vector a, std::vector b, expression_vector c, row_idx_t n) + : TableFuncBindData{std::move(c),n}, vecA{std::move(a)}, vecB{std::move(b)} {} + std::unique_ptr copy() const override { + return std::make_unique(vecA,vecB,columns,numRows); + } +}; + +// Cosine similarity: dot(a,b) / (|a| × |b|) +static double cosineSim(const std::vector& a, const std::vector& b) { + double dot = 0, nA = 0, nB = 0; + size_t len = std::min(a.size(), b.size()); + for (size_t i = 0; i < len; i++) { + dot += a[i] * b[i]; + nA += a[i] * a[i]; + nB += b[i] * b[i]; + } + if (nA < 1e-12 || nB < 1e-12) return 0.0; + double cs = dot / (std::sqrt(nA) * std::sqrt(nB)); + if (cs > 1.0) cs = 1.0; + if (cs < -1.0) cs = -1.0; + return cs; +} + +// Mean per-dimension feature similarity +static double meanFeatureSim(const std::vector& a, const std::vector& b) { + size_t len = std::min(a.size(), b.size()); + if (len == 0) return 0.0; + double sum = 0; + for (size_t i = 0; i < len; i++) { + double d = std::max(std::max(std::abs(a[i]), std::abs(b[i])), 0.001); + sum += 1.0 - std::abs(a[i] - b[i]) / d; + } + return sum / len; +} + +static offset_t tableFunc(const TableFuncMorsel&, const TableFuncInput& in, DataChunk& out) { + auto bd = in.bindData->constPtrCast(); + double cosSim = cosineSim(bd->vecA, bd->vecB); + double featSim = meanFeatureSim(bd->vecA, bd->vecB); + int64_t dims = (int64_t)std::min(bd->vecA.size(), bd->vecB.size()); + + auto pos = out.state->getSelVector()[0]; + out.getValueVectorMutable(0).setValue(pos, cosSim); + out.getValueVectorMutable(1).setValue(pos, featSim); + out.getValueVectorMutable(2).setValue(pos, dims); + return 1; +} + +static std::unique_ptr bindFunc(const main::ClientContext*, + const TableFuncBindInput* in) { + // vec_a (param 0) — LIST + auto av = in->getValue(0); + uint32_t na = NestedVal::getChildrenSize(&av); + std::vector vecA; vecA.reserve(na); + for (uint32_t j = 0; j < na; j++) + vecA.push_back(NestedVal::getChildVal(&av, j)->getValue()); + + // vec_b (param 1) — LIST + auto bv = in->getValue(1); + uint32_t nb = NestedVal::getChildrenSize(&bv); + std::vector vecB; vecB.reserve(nb); + for (uint32_t j = 0; j < nb; j++) + vecB.push_back(NestedVal::getChildVal(&bv, j)->getValue()); + + std::vector ns = {"cosine_similarity", "feature_similarity", "dimension_count"}; + std::vector ts; + ts.push_back(LogicalType::DOUBLE()); + ts.push_back(LogicalType::DOUBLE()); + ts.push_back(LogicalType::INT64()); + ns = TableFunction::extractYieldVariables(ns, in->yieldVariables); + return std::make_unique(std::move(vecA), std::move(vecB), + in->binder->createVariables(ns, ts), 1); +} + +function_set EmbeddingSimilarityFunction::getFunctionSet() { + function_set fs; + auto f = std::make_unique(name, + std::vector{LogicalTypeID::ANY, LogicalTypeID::ANY}); + f->inferInputTypes = [](const expression_vector&) -> std::vector { + std::vector result; + result.push_back(LogicalType::LIST(LogicalType::DOUBLE())); + result.push_back(LogicalType::LIST(LogicalType::DOUBLE())); + return result; + }; + f->tableFunc = SimpleTableFunc::getTableFunc(tableFunc); + f->bindFunc = bindFunc; + f->initSharedStateFunc = SimpleTableFunc::initSharedState; + f->initLocalStateFunc = TableFunction::initEmptyLocalState; + fs.push_back(std::move(f)); + return fs; +} + +}} // namespaces From 30d19d9b6bc0be4a062b2b000c11fb83c1e6a46e Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:33:39 +0800 Subject: [PATCH 08/10] =?UTF-8?q?feat(timeseries):=20add=20DETECT=5FDRIFT?= =?UTF-8?q?=5FPOINTS=20=E2=80=94=20generic=20drift=20detection=20on=20embe?= =?UTF-8?q?ddings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Generalized from detect_turning_points (hardcoded chapter labels) to accept generic labels (commit hashes, timestamps, version numbers, etc.). Use cases: architecture drift, content drift, behavior drift detection. --- .../src/function/detect_drift_points.cpp | 171 ++++++++++++++++++ 1 file changed, 171 insertions(+) create mode 100644 timeseries/src/function/detect_drift_points.cpp diff --git a/timeseries/src/function/detect_drift_points.cpp b/timeseries/src/function/detect_drift_points.cpp new file mode 100644 index 0000000..2f895d1 --- /dev/null +++ b/timeseries/src/function/detect_drift_points.cpp @@ -0,0 +1,171 @@ +// DETECT_DRIFT_POINTS — generic drift detection on sequential embeddings +// +// CALL detect_drift_points( +// flat_embeddings LIST, -- N×D doubles (row-major: e1_1, e1_2, ..., eN_D) +// num_embeddings INT64, -- N +// labels LIST, -- N labels (e.g. chapter numbers, commit hashes, timestamps) +// embedding_dim INT64, -- D (e.g. 768) +// threshold:=0.3 -- [optional] DOUBLE detection threshold +// ) RETURN +// label INT64, drift_magnitude DOUBLE, +// significance DOUBLE, direction STRING +// +// Algorithm: +// 1. Reconstruct N vectors of D dimensions from flat list +// 2. Compute pairwise cosine distance between consecutive vectors +// 3. Min-max normalize distances → significance [0,1] +// 4. Filter by threshold, sort by significance descending +// +// Use cases: +// - Architecture drift: embeddings of code snapshots per commit +// - Content drift: embeddings of document revisions over time +// - Behavior drift: embeddings of API response patterns +// +// Pure computation — no table scans. Data prepared via Cypher COLLECT at app layer. + +#include "binder/binder.h" +#include "binder/expression/literal_expression.h" +#include "common/types/value/nested.h" +#include "common/types/value/value.h" +#include "function/table/bind_data.h" +#include "function/table/bind_input.h" +#include "function/table/simple_table_function.h" +#include "function/timeseries_function.h" +#include "main/client_context.h" +#include "processor/execution_context.h" +#include +#include +#include +#include + +using namespace lbug::binder; +using namespace lbug::common; +using namespace lbug::function; +using namespace lbug::processor; + +namespace lbug { namespace timeseries_extension { + +struct DDPBD final : TableFuncBindData { + std::vector embeds; // N×D doubles (row-major) + std::vector labels; // N labels + int64_t dims; // D + int64_t numEmb; // N + double thr; // detection threshold + DDPBD(std::vector e, std::vector l, int64_t d, int64_t n, double t, + expression_vector co, row_idx_t nr) + : TableFuncBindData{std::move(co),nr}, embeds{std::move(e)}, labels{std::move(l)}, + dims{d}, numEmb{n}, thr{t} {} + std::unique_ptr copy() const override { + return std::make_unique(embeds,labels,dims,numEmb,thr,columns,numRows); + } +}; + +// Cosine distance: 1 − dot(a,b)/(|a|×|b|), range [0,2] +static double cosDist(const double* a, const double* b, int64_t dims) { + double d = 0, nA = 0, nB = 0; + for (int64_t i = 0; i < dims; i++) { d += a[i]*b[i]; nA += a[i]*a[i]; nB += b[i]*b[i]; } + if (nA < 1e-12 || nB < 1e-12) return 0.0; + double cs = d / std::sqrt(nA * nB); + if (cs > 1.0) cs = 1.0; if (cs < -1.0) cs = -1.0; + return 1.0 - cs; +} + +static offset_t tableFunc(const TableFuncMorsel&, const TableFuncInput& in, DataChunk& out) { + auto bd = in.bindData->constPtrCast(); + int64_t N = bd->numEmb, D = bd->dims; + if (N < 2) return 0; + + std::vector dist(N - 1); + for (int64_t i = 1; i < N; i++) + dist[i-1] = cosDist(&bd->embeds[(i-1)*D], &bd->embeds[i*D], D); + + double mn = *std::min_element(dist.begin(), dist.end()); + double mx = *std::max_element(dist.begin(), dist.end()); + double rng = mx - mn + 0.001; + + struct DP { int64_t label; double mag, sig; std::string dir; }; + std::vector dps; + for (int64_t i = 0; i < N-1; i++) { + if (dist[i] > bd->thr) { + double sig = (dist[i] - mn) / rng; + std::string dir = (i > 0 && dist[i] > dist[i-1]) ? "up" : "down"; + dps.push_back({bd->labels[i+1], dist[i], sig, dir}); + } + } + + std::sort(dps.begin(), dps.end(), + [](const DP& a, const DP& b) { return a.sig > b.sig; }); + + for (size_t j = 0; j < dps.size(); j++) { + out.getValueVectorMutable(0).setValue((offset_t)j, dps[j].label); + out.getValueVectorMutable(1).setValue((offset_t)j, dps[j].mag); + out.getValueVectorMutable(2).setValue((offset_t)j, dps[j].sig); + out.getValueVectorMutable(3).setValue((offset_t)j, dps[j].dir); + } + return (offset_t)dps.size(); +} + +static std::unique_ptr bindFunc(const main::ClientContext*, + const TableFuncBindInput* in) { + // flat_embeddings (param 0) — LIST + auto fv = in->getValue(0); + uint32_t nf = NestedVal::getChildrenSize(&fv); + std::vector embeds; embeds.reserve(nf); + for (uint32_t j = 0; j < nf; j++) + embeds.push_back(NestedVal::getChildVal(&fv, j)->getValue()); + + // num_embeddings (param 1) — INT64 + int64_t numEmb = in->getValue(1).getValue(); + + // labels (param 2) — LIST + auto cv = in->getValue(2); + uint32_t nc = NestedVal::getChildrenSize(&cv); + std::vector labels; labels.reserve(nc); + for (uint32_t j = 0; j < nc; j++) + labels.push_back(NestedVal::getChildVal(&cv, j)->getValue()); + + // embedding_dim (param 3) — INT64 + int64_t dims = in->getValue(3).getValue(); + + // threshold (optional param, default 0.3) + double thr = 0.3; + for (auto& p : in->optionalParamsLegacy) + if (p->getAlias() == "threshold") + if (auto le = p->constPtrCast()) + thr = le->getValue().getValue(); + + if (numEmb <= 0 || dims <= 0 || (int64_t)nf != numEmb * dims || (int64_t)nc != numEmb) + numEmb = 0; + + std::vector ns = {"label","drift_magnitude","significance","direction"}; + std::vector ts; ts.reserve(4); + ts.push_back(LogicalType::INT64()); ts.push_back(LogicalType::DOUBLE()); + ts.push_back(LogicalType::DOUBLE()); ts.push_back(LogicalType::STRING()); + ns = TableFunction::extractYieldVariables(ns, in->yieldVariables); + row_idx_t maxRows = (row_idx_t)(numEmb > 1 ? numEmb - 1 : 1); + return std::make_unique(std::move(embeds), std::move(labels), dims, numEmb, thr, + in->binder->createVariables(ns, ts), maxRows); +} + +function_set DetectDriftPointsFunction::getFunctionSet() { + function_set fs; + auto f = std::make_unique(name, + std::vector{LogicalTypeID::ANY, LogicalTypeID::INT64, + LogicalTypeID::ANY, LogicalTypeID::INT64}); + f->inferInputTypes = [](const expression_vector&) -> std::vector { + std::vector result; result.reserve(4); + result.push_back(LogicalType::LIST(LogicalType::DOUBLE())); + result.push_back(LogicalType::INT64()); + result.push_back(LogicalType::LIST(LogicalType::INT64())); + result.push_back(LogicalType::INT64()); + return result; + }; + f->tableFunc = SimpleTableFunc::getTableFunc(tableFunc); + f->bindFunc = bindFunc; + f->initSharedStateFunc = SimpleTableFunc::initSharedState; + f->initLocalStateFunc = TableFunction::initEmptyLocalState; + fs.push_back(std::move(f)); + return fs; +} + +}} // namespaces From f854eb462f715c5fc27ac7a69c4df193453614cb Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:34:10 +0800 Subject: [PATCH 09/10] feat(timeseries): add e2e tests for embedding_similarity and detect_drift_points --- timeseries/test/test_files/timeseries.test | 35 ++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 timeseries/test/test_files/timeseries.test diff --git a/timeseries/test/test_files/timeseries.test b/timeseries/test/test_files/timeseries.test new file mode 100644 index 0000000..6ee243c --- /dev/null +++ b/timeseries/test/test_files/timeseries.test @@ -0,0 +1,35 @@ +-DATASET CSV empty + +-- + +-CASE EmbeddingSimilarity +-LOAD_DYNAMIC_EXTENSION timeseries +# Two 3-dimensional vectors +-STATEMENT CALL embedding_similarity( + CAST([1.0, 0.0, 0.0], 'DOUBLE[]'), + CAST([1.0, 0.0, 0.0], 'DOUBLE[]') +) RETURN cosine_similarity, feature_similarity, dimension_count; +---- 1 +1.0|1.0|3 + +-CASE EmbeddingSimilarityOrthogonal +-LOAD_DYNAMIC_EXTENSION timeseries +# Orthogonal vectors — cosine similarity should be 0 +-STATEMENT CALL embedding_similarity( + CAST([1.0, 0.0, 0.0], 'DOUBLE[]'), + CAST([0.0, 1.0, 0.0], 'DOUBLE[]') +) RETURN cosine_similarity; +---- 1 +0.0 + +-CASE DetectDriftPoints +-LOAD_DYNAMIC_EXTENSION timeseries +# 5 embeddings of dimension 3, with a drift between label 2 and 3 +-STATEMENT CALL detect_drift_points( + CAST([1.0, 0.0, 0.0, 1.0, 0.1, 0.0, 1.0, 0.2, 0.0, 0.0, 1.0, 0.0, 0.0, 1.0, 0.1], 'DOUBLE[]'), + 5, + CAST([100, 200, 300, 400, 500], 'INT64[]'), + 3, + threshold := 0.1 +) RETURN label, drift_magnitude, significance, direction; +---- ok From 34a446a474a0688b05470c1e68bb8216773a3294 Mon Sep 17 00:00:00 2001 From: chiangchenghsin-hash Date: Tue, 4 Aug 2026 13:34:39 +0800 Subject: [PATCH 10/10] feat(timeseries): add README with API documentation and use cases --- timeseries/README.md | 70 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 timeseries/README.md diff --git a/timeseries/README.md b/timeseries/README.md new file mode 100644 index 0000000..e473e7a --- /dev/null +++ b/timeseries/README.md @@ -0,0 +1,70 @@ +# LadybugDB Timeseries Extension + +Time-series analysis functions for sequential data: embedding similarity comparison and drift detection on embedding sequences. + +## Functions + +### EMBEDDING_SIMILARITY + +Compute cosine similarity and per-dimension feature similarity between two embedding vectors of any dimension. + +```cypher +CALL embedding_similarity( + CAST([0.1, 0.2, 0.3], 'DOUBLE[]'), + CAST([0.1, 0.2, 0.4], 'DOUBLE[]') +) RETURN cosine_similarity, feature_similarity, dimension_count; +``` + +**Parameters:** +- `vec_a LIST` — first embedding vector +- `vec_b LIST` — second embedding vector + +**Returns:** +- `cosine_similarity DOUBLE` — cosine similarity [-1, 1] +- `feature_similarity DOUBLE` — mean per-dimension feature similarity [0, 1] +- `dimension_count INT64` — number of dimensions compared + +### DETECT_DRIFT_POINTS + +Detect drift points in a sequence of embeddings by computing pairwise cosine distances between consecutive vectors. Returns drift points sorted by significance. + +```cypher +CALL detect_drift_points( + CAST([1.0, 0.0, 0.0, 0.0, 1.0, 0.0], 'DOUBLE[]'), + 2, -- num_embeddings + CAST([100, 200], 'INT64[]'), -- labels + 3, -- embedding_dim + threshold := 0.1 +) RETURN label, drift_magnitude, significance, direction; +``` + +**Parameters:** +- `flat_embeddings LIST` — N×D doubles (row-major: e1_1, e1_2, ..., eN_D) +- `num_embeddings INT64` — N +- `labels LIST` — N labels (commit hashes, timestamps, version numbers, etc.) +- `embedding_dim INT64` — D (e.g. 768) +- `threshold:=0.3` — optional DOUBLE detection threshold (default 0.3) + +**Returns:** +- `label INT64` — label of the drift point +- `drift_magnitude DOUBLE` — raw cosine distance +- `significance DOUBLE` — normalized significance [0, 1] +- `direction STRING` — "up" or "down" relative to previous distance + +## Use Cases + +- **Architecture drift**: embeddings of code snapshots per commit → detect when architecture changed significantly +- **Content drift**: embeddings of document revisions over time → detect when content drifted +- **Behavior drift**: embeddings of API response patterns → detect behavioral changes +- **Vector comparison**: compare any two embeddings regardless of dimension + +## Building + +```bash +cmake -DBUILD_EXTENSIONS="timeseries" .. +cmake --build . --target libtimeseries.lbug_extension +``` + +## Dependencies + +No external dependencies. Pure C++ implementation.