feat: Initial clean release of CHM
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
一些检查失败了
automerge / automerge (push) Has been cancelled
react-native-android-build-apk / AssembleRelease (push) Has been cancelled
Build macOS M1 / Main (push) Has been cancelled
Joplin Continuous Integration / Main (macos-15-intel) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / Main (ubuntu-22.04-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-11-vs2026-arm) (push) Has been cancelled
Joplin Continuous Integration / Main (windows-2025) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04) (push) Has been cancelled
Joplin Continuous Integration / ServerDockerImage (ubuntu-22.04-arm) (push) Has been cancelled
Joplin UI tests / Main (ubuntu-22.04) (push) Has been cancelled
Joplin UI tests / Main (windows-2025) (push) Has been cancelled
Joplin UI tests / Mobile (push) Has been cancelled
Close stale issues / ProcessStaleIssues (push) Has been cancelled
这个提交包含在:
@@ -0,0 +1,54 @@
|
||||
import TestEmbeddingProvider from './TestEmbeddingProvider';
|
||||
|
||||
// Sanity tests for the test stub. Not exhaustive — the goal is just to
|
||||
// confirm the properties the indexer/search integration tests depend on:
|
||||
// determinism, similarity for related text, distance for unrelated text.
|
||||
|
||||
const dot = (a: number[], b: number[]): number => {
|
||||
let sum = 0;
|
||||
for (let i = 0; i < a.length; i++) sum += a[i] * b[i];
|
||||
return sum;
|
||||
};
|
||||
|
||||
describe('TestEmbeddingProvider', () => {
|
||||
|
||||
it('produces vectors of the configured dimension', async () => {
|
||||
const provider = new TestEmbeddingProvider({ dimension: 16 });
|
||||
const [v] = await provider.embed(['hello world']);
|
||||
expect(v.length).toBe(16);
|
||||
});
|
||||
|
||||
it('is deterministic: same input always produces the same vector', async () => {
|
||||
const provider = new TestEmbeddingProvider();
|
||||
const [v1] = await provider.embed(['the quick brown fox']);
|
||||
const [v2] = await provider.embed(['the quick brown fox']);
|
||||
expect(v1).toEqual(v2);
|
||||
});
|
||||
|
||||
it('returns unit-length vectors (cosine similarity == dot product)', async () => {
|
||||
const provider = new TestEmbeddingProvider();
|
||||
const [v] = await provider.embed(['anything reasonable']);
|
||||
const norm = Math.sqrt(dot(v, v));
|
||||
expect(norm).toBeCloseTo(1, 5);
|
||||
});
|
||||
|
||||
it('scores related text more similarly than unrelated text', async () => {
|
||||
const provider = new TestEmbeddingProvider();
|
||||
const [vQuery, vRelated, vUnrelated] = await provider.embed([
|
||||
'cats and kittens',
|
||||
'kittens like cats',
|
||||
'engine repair manual',
|
||||
]);
|
||||
|
||||
const relatedSim = dot(vQuery, vRelated);
|
||||
const unrelatedSim = dot(vQuery, vUnrelated);
|
||||
expect(relatedSim).toBeGreaterThan(unrelatedSim);
|
||||
});
|
||||
|
||||
it('handles empty and single-character inputs without producing zero vectors', async () => {
|
||||
const provider = new TestEmbeddingProvider();
|
||||
const [vEmpty, vSingle] = await provider.embed(['', 'a']);
|
||||
expect(Math.sqrt(dot(vEmpty, vEmpty))).toBeCloseTo(1, 5);
|
||||
expect(Math.sqrt(dot(vSingle, vSingle))).toBeCloseTo(1, 5);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,86 @@
|
||||
import { EmbeddingProvider, ProviderClassification } from '../types';
|
||||
|
||||
// Deterministic embedding provider used in CI tests for the indexer and the
|
||||
// search layer. Same text always produces the same vector, and texts that share
|
||||
// characters produce more-similar vectors than texts that don't — so
|
||||
// similaritySearch() returns sensible orderings.
|
||||
//
|
||||
// This is NOT a real embedding model. It can't capture semantics that span
|
||||
// multiple words or handle synonyms. It exists purely so we can exercise the
|
||||
// indexing + search pipeline end-to-end in CI without bundling onnxruntime-node
|
||||
// or a 100MB model file.
|
||||
|
||||
const DEFAULT_DIMENSION = 32;
|
||||
|
||||
const hashString = (s: string): number => {
|
||||
// Simple FNV-1a 32-bit hash, used to map each character bigram to a vector
|
||||
// index. Deterministic and fast.
|
||||
let h = 0x811c9dc5;
|
||||
for (let i = 0; i < s.length; i++) {
|
||||
h ^= s.charCodeAt(i);
|
||||
h = Math.imul(h, 0x01000193);
|
||||
}
|
||||
return h >>> 0;
|
||||
};
|
||||
|
||||
const l2Norm = (v: number[]): number => {
|
||||
let sum = 0;
|
||||
for (const x of v) sum += x * x;
|
||||
return Math.sqrt(sum);
|
||||
};
|
||||
|
||||
const normalise = (v: number[]): number[] => {
|
||||
const norm = l2Norm(v);
|
||||
if (norm === 0) return v;
|
||||
return v.map(x => x / norm);
|
||||
};
|
||||
|
||||
const addUnitX = (v: number[]) => {
|
||||
return v.map((x, i) => (i === 0 ? x + 1 : x));
|
||||
};
|
||||
|
||||
const embedOne = (text: string, dimension: number): number[] => {
|
||||
const vec = new Array<number>(dimension).fill(0);
|
||||
const lower = text.toLowerCase();
|
||||
// Character bigrams give a richer signal than unigrams (which would
|
||||
// collapse "cat" and "act" together) without exploding the keyspace.
|
||||
for (let i = 0; i < lower.length - 1; i++) {
|
||||
const bigram = lower.slice(i, i + 2);
|
||||
const bucket = hashString(bigram) % dimension;
|
||||
vec[bucket] += 1;
|
||||
}
|
||||
// Always include a constant signal so empty or one-character strings
|
||||
// still produce a non-zero vector (otherwise normalise() returns zeros
|
||||
// and similarity is undefined).
|
||||
vec[0] += 1;
|
||||
|
||||
// Add a constant post-normalization to increase the similarity scores to more
|
||||
// closely match the production model (which distributes scores roughly sin [0.7, 1])
|
||||
return normalise(
|
||||
addUnitX(normalise(vec)),
|
||||
);
|
||||
};
|
||||
|
||||
export interface TestEmbeddingProviderOptions {
|
||||
id?: string;
|
||||
modelId?: string;
|
||||
dimension?: number;
|
||||
}
|
||||
|
||||
export default class TestEmbeddingProvider implements EmbeddingProvider {
|
||||
|
||||
public id: string;
|
||||
public modelId: string;
|
||||
public dimension: number;
|
||||
public classification: ProviderClassification = 'local';
|
||||
|
||||
public constructor(options: TestEmbeddingProviderOptions = {}) {
|
||||
this.id = options.id ?? 'test';
|
||||
this.modelId = options.modelId ?? 'test-model-v1';
|
||||
this.dimension = options.dimension ?? DEFAULT_DIMENSION;
|
||||
}
|
||||
|
||||
public async embed(texts: string[]): Promise<number[][]> {
|
||||
return texts.map(t => embedOne(t, this.dimension));
|
||||
}
|
||||
}
|
||||
在新工单中引用
屏蔽一个用户