@o-lang/semantic-doc-search 1.0.27 → 1.0.29
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,31 +1,11 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Base Vector Adapter
|
|
3
|
-
* All vector backends must extend this class.
|
|
4
|
-
*
|
|
5
|
-
* Responsibilities:
|
|
6
|
-
* - dimension validation
|
|
7
|
-
* - capability exposure
|
|
8
|
-
* - ingestion guards
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
1
|
class VectorAdapter {
|
|
12
2
|
constructor(config = {}) {
|
|
13
3
|
this.backend = config.backend || "unknown";
|
|
14
4
|
this.dimension = config.dimension || null;
|
|
15
|
-
|
|
16
|
-
// Optional but recommended
|
|
17
|
-
this.capabilities = config.capabilities || [];
|
|
18
|
-
this.supportsHybrid = config.supportsHybrid ?? false;
|
|
19
|
-
this.requiresIdempotentInsert = config.requiresIdempotentInsert ?? false;
|
|
20
5
|
}
|
|
21
6
|
|
|
22
|
-
/* ---------------- VALIDATION ---------------- */
|
|
23
|
-
|
|
24
7
|
validateVector(vector) {
|
|
25
|
-
if (!Array.isArray(vector))
|
|
26
|
-
throw new Error("Vector must be an array");
|
|
27
|
-
}
|
|
28
|
-
|
|
8
|
+
if (!Array.isArray(vector)) throw new Error("Vector must be an array");
|
|
29
9
|
if (this.dimension && vector.length !== this.dimension) {
|
|
30
10
|
throw new Error(
|
|
31
11
|
`Vector dimension mismatch: expected ${this.dimension}, got ${vector.length}`
|
|
@@ -33,49 +13,16 @@ class VectorAdapter {
|
|
|
33
13
|
}
|
|
34
14
|
}
|
|
35
15
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
);
|
|
41
|
-
}
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
/* ---------------- INGESTION GUARDS ---------------- */
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Optional hook for hybrid-safe ingestion.
|
|
48
|
-
* Adapters that require idempotent inserts SHOULD override this.
|
|
49
|
-
*/
|
|
50
|
-
async ensureNotIndexed(/* fingerprint */) {
|
|
51
|
-
if (this.requiresIdempotentInsert) {
|
|
52
|
-
throw new Error(
|
|
53
|
-
`ensureNotIndexed() must be implemented for backend "${this.backend}"`
|
|
54
|
-
);
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/* ---------------- CORE OPS ---------------- */
|
|
59
|
-
|
|
60
|
-
async upsert() {
|
|
61
|
-
throw new Error("upsert() not implemented");
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
async query() {
|
|
65
|
-
throw new Error("query() not implemented");
|
|
66
|
-
}
|
|
67
|
-
|
|
68
|
-
/* ---------------- HEALTH ---------------- */
|
|
16
|
+
async upsert() { throw new Error("upsert() not implemented"); }
|
|
17
|
+
async query() { throw new Error("query() not implemented"); }
|
|
18
|
+
async health() { return { backend: this.backend, status: "unknown" }; }
|
|
19
|
+
async close() {}
|
|
69
20
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
hybrid: this.supportsHybrid
|
|
75
|
-
};
|
|
21
|
+
/** ✅ Checks if backend supports a capability */
|
|
22
|
+
supports(capability) {
|
|
23
|
+
const caps = this.constructor.capabilities?.()?.capabilities || [];
|
|
24
|
+
return caps.includes(capability);
|
|
76
25
|
}
|
|
77
|
-
|
|
78
|
-
async close() {}
|
|
79
26
|
}
|
|
80
27
|
|
|
81
28
|
module.exports = VectorAdapter;
|
|
@@ -1,45 +1,23 @@
|
|
|
1
1
|
const VectorAdapter = require("./VectorAdapter");
|
|
2
|
-
const
|
|
2
|
+
const capabilities = require("./vectorCapabilities");
|
|
3
3
|
|
|
4
|
-
/**
|
|
5
|
-
* In-memory vector adapter
|
|
6
|
-
* - Non-persistent
|
|
7
|
-
* - Offline-safe
|
|
8
|
-
* - No hybrid ingestion risk
|
|
9
|
-
*/
|
|
10
4
|
class InMemoryAdapter extends VectorAdapter {
|
|
11
5
|
constructor(config = {}) {
|
|
12
|
-
super({
|
|
13
|
-
...config,
|
|
14
|
-
backend: "memory",
|
|
15
|
-
capabilities: vectorCapabilities.memory.capabilities,
|
|
16
|
-
supportsHybrid: false,
|
|
17
|
-
requiresIdempotentInsert: false
|
|
18
|
-
});
|
|
19
|
-
|
|
6
|
+
super({ ...config, backend: "memory" });
|
|
20
7
|
this.dimension = config.dimension || 384;
|
|
21
8
|
this.store = [];
|
|
22
9
|
}
|
|
23
10
|
|
|
24
|
-
|
|
11
|
+
static capabilities() {
|
|
12
|
+
return capabilities.memory;
|
|
13
|
+
}
|
|
25
14
|
|
|
26
15
|
async upsert({ id, vector, content, source, metadata = {} }) {
|
|
27
|
-
this.ensureCapability("vector.insert");
|
|
28
16
|
this.validateVector(vector);
|
|
29
|
-
|
|
30
|
-
this.store.push({
|
|
31
|
-
id,
|
|
32
|
-
vector,
|
|
33
|
-
content,
|
|
34
|
-
source,
|
|
35
|
-
metadata
|
|
36
|
-
});
|
|
17
|
+
this.store.push({ id, vector, content, source, metadata });
|
|
37
18
|
}
|
|
38
19
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
async query(vector, { topK = 5, minScore = 0 } = {}) {
|
|
42
|
-
this.ensureCapability("vector.search");
|
|
20
|
+
async query(vector, { topK = 5 } = {}) {
|
|
43
21
|
this.validateVector(vector);
|
|
44
22
|
|
|
45
23
|
return this.store
|
|
@@ -47,36 +25,18 @@ class InMemoryAdapter extends VectorAdapter {
|
|
|
47
25
|
...doc,
|
|
48
26
|
score: cosineSimilarity(vector, doc.vector)
|
|
49
27
|
}))
|
|
50
|
-
.filter(doc => doc.score >= minScore)
|
|
51
28
|
.sort((a, b) => b.score - a.score)
|
|
52
29
|
.slice(0, topK);
|
|
53
30
|
}
|
|
54
|
-
|
|
55
|
-
/* ---------------- HEALTH ---------------- */
|
|
56
|
-
|
|
57
|
-
async health() {
|
|
58
|
-
return {
|
|
59
|
-
backend: this.backend,
|
|
60
|
-
status: "ok",
|
|
61
|
-
vectors: this.store.length,
|
|
62
|
-
offline: true
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
31
|
}
|
|
66
32
|
|
|
67
|
-
/* ---------------- UTIL ---------------- */
|
|
68
|
-
|
|
69
33
|
function cosineSimilarity(a, b) {
|
|
70
|
-
let dot = 0;
|
|
71
|
-
let na = 0;
|
|
72
|
-
let nb = 0;
|
|
73
|
-
|
|
34
|
+
let dot = 0, na = 0, nb = 0;
|
|
74
35
|
for (let i = 0; i < a.length; i++) {
|
|
75
36
|
dot += a[i] * b[i];
|
|
76
37
|
na += a[i] ** 2;
|
|
77
38
|
nb += b[i] ** 2;
|
|
78
39
|
}
|
|
79
|
-
|
|
80
40
|
return dot / (Math.sqrt(na) * Math.sqrt(nb));
|
|
81
41
|
}
|
|
82
42
|
|
|
@@ -1,25 +1,14 @@
|
|
|
1
1
|
const { Pool } = require("pg");
|
|
2
2
|
const VectorAdapter = require("./VectorAdapter");
|
|
3
|
-
const
|
|
4
|
-
|
|
5
|
-
/* ---------------- UTIL ---------------- */
|
|
3
|
+
const capabilities = require("./vectorCapabilities");
|
|
6
4
|
|
|
7
5
|
function toPgVectorLiteral(vector) {
|
|
8
6
|
return `[${vector.join(",")}]`;
|
|
9
7
|
}
|
|
10
8
|
|
|
11
|
-
/* ---------------- ADAPTER ---------------- */
|
|
12
|
-
|
|
13
9
|
class PgVectorAdapter extends VectorAdapter {
|
|
14
10
|
constructor(config = {}) {
|
|
15
|
-
super({
|
|
16
|
-
...config,
|
|
17
|
-
backend: "pgvector",
|
|
18
|
-
capabilities: vectorCapabilities.pgvector.capabilities,
|
|
19
|
-
supportsHybrid: true,
|
|
20
|
-
requiresIdempotentInsert: true
|
|
21
|
-
});
|
|
22
|
-
|
|
11
|
+
super({ ...config, backend: "pgvector" });
|
|
23
12
|
this.dimension = config.dimension || 384;
|
|
24
13
|
|
|
25
14
|
this.pool = new Pool({
|
|
@@ -27,77 +16,43 @@ class PgVectorAdapter extends VectorAdapter {
|
|
|
27
16
|
});
|
|
28
17
|
}
|
|
29
18
|
|
|
30
|
-
|
|
19
|
+
static capabilities() {
|
|
20
|
+
return capabilities.pgvector;
|
|
21
|
+
}
|
|
31
22
|
|
|
32
23
|
async upsert({ id, vector, content, source, metadata = {} }) {
|
|
33
|
-
this.ensureCapability("vector.insert");
|
|
34
24
|
this.validateVector(vector);
|
|
35
|
-
|
|
36
25
|
const pgVector = toPgVectorLiteral(vector);
|
|
37
26
|
|
|
38
27
|
await this.pool.query(
|
|
39
|
-
`
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
updated_at = NOW()
|
|
48
|
-
`,
|
|
28
|
+
`INSERT INTO doc_embeddings (id, embedding, content, source, metadata)
|
|
29
|
+
VALUES ($1, $2::vector, $3, $4, $5::jsonb)
|
|
30
|
+
ON CONFLICT (id) DO UPDATE
|
|
31
|
+
SET embedding = $2::vector,
|
|
32
|
+
content = $3,
|
|
33
|
+
source = $4,
|
|
34
|
+
metadata = $5::jsonb,
|
|
35
|
+
updated_at = NOW()`,
|
|
49
36
|
[id, pgVector, content, source, JSON.stringify(metadata)]
|
|
50
37
|
);
|
|
51
38
|
}
|
|
52
39
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
async query(vector, { topK = 5, minScore = 0 } = {}) {
|
|
56
|
-
this.ensureCapability("vector.search");
|
|
40
|
+
async query(vector, { topK = 5 } = {}) {
|
|
57
41
|
this.validateVector(vector);
|
|
58
|
-
|
|
59
42
|
const pgVector = toPgVectorLiteral(vector);
|
|
60
43
|
|
|
61
44
|
const res = await this.pool.query(
|
|
62
|
-
`
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
FROM doc_embeddings
|
|
69
|
-
WHERE 1 - (embedding <=> $1::vector) >= $2
|
|
70
|
-
ORDER BY embedding <=> $1::vector
|
|
71
|
-
LIMIT $3
|
|
72
|
-
`,
|
|
73
|
-
[pgVector, minScore, topK]
|
|
45
|
+
`SELECT id, content, source, metadata,
|
|
46
|
+
1 - (embedding <=> $1::vector) AS score
|
|
47
|
+
FROM doc_embeddings
|
|
48
|
+
ORDER BY embedding <=> $1::vector
|
|
49
|
+
LIMIT $2`,
|
|
50
|
+
[pgVector, topK]
|
|
74
51
|
);
|
|
75
52
|
|
|
76
53
|
return res.rows;
|
|
77
54
|
}
|
|
78
55
|
|
|
79
|
-
/* ---------------- HEALTH ---------------- */
|
|
80
|
-
|
|
81
|
-
async health() {
|
|
82
|
-
try {
|
|
83
|
-
await this.pool.query("SELECT 1");
|
|
84
|
-
return {
|
|
85
|
-
backend: this.backend,
|
|
86
|
-
status: "ok",
|
|
87
|
-
offline: false,
|
|
88
|
-
hybrid: true
|
|
89
|
-
};
|
|
90
|
-
} catch (err) {
|
|
91
|
-
return {
|
|
92
|
-
backend: this.backend,
|
|
93
|
-
status: "error",
|
|
94
|
-
error: err.message
|
|
95
|
-
};
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/* ---------------- CLEANUP ---------------- */
|
|
100
|
-
|
|
101
56
|
async close() {
|
|
102
57
|
await this.pool.end();
|
|
103
58
|
}
|
|
@@ -1,101 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
* Pinecone Vector Store Adapter
|
|
3
|
-
*
|
|
4
|
-
* Requirements:
|
|
5
|
-
* npm install @pinecone-database/pinecone
|
|
6
|
-
*
|
|
7
|
-
* Env:
|
|
8
|
-
* PINECONE_API_KEY
|
|
9
|
-
* PINECONE_INDEX=olang-docs
|
|
10
|
-
*/
|
|
1
|
+
let Pinecone;
|
|
11
2
|
|
|
12
|
-
|
|
13
|
-
const VectorAdapter = require("./VectorAdapter");
|
|
14
|
-
const vectorCapabilities = require("./vectorCapabilities");
|
|
15
|
-
|
|
16
|
-
class PineconeAdapter extends VectorAdapter {
|
|
3
|
+
class PineconeAdapter {
|
|
17
4
|
constructor(config = {}) {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
this.indexName = config.PINECONE_INDEX || process.env.PINECONE_INDEX || "olang-docs";
|
|
5
|
+
if (!Pinecone) {
|
|
6
|
+
try {
|
|
7
|
+
Pinecone = require("@pinecone-database/pinecone");
|
|
8
|
+
} catch (err) {
|
|
9
|
+
throw new Error(
|
|
10
|
+
"Pinecone adapter requires '@pinecone-database/pinecone'. Please install it via npm."
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
28
14
|
|
|
29
|
-
|
|
30
|
-
if (!apiKey) throw new Error("Missing PINECONE_API_KEY");
|
|
15
|
+
this.apiKey = process.env.PINECONE_API_KEY;
|
|
16
|
+
if (!this.apiKey) throw new Error("Missing PINECONE_API_KEY");
|
|
31
17
|
|
|
32
|
-
|
|
33
|
-
this.
|
|
18
|
+
const indexName = process.env.PINECONE_INDEX || "olang-docs";
|
|
19
|
+
this.client = new Pinecone({ apiKey: this.apiKey });
|
|
20
|
+
this.indexName = indexName;
|
|
34
21
|
}
|
|
35
22
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
async upsert({ id, vector, content, source, metadata = {} }) {
|
|
39
|
-
this.ensureCapability("vector.insert");
|
|
40
|
-
this.validateVector(vector);
|
|
41
|
-
|
|
42
|
-
await this.index.upsert([
|
|
43
|
-
{
|
|
44
|
-
id: id.toString(),
|
|
45
|
-
values: vector,
|
|
46
|
-
metadata: {
|
|
47
|
-
content,
|
|
48
|
-
source,
|
|
49
|
-
...metadata
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
]);
|
|
23
|
+
static capabilities() {
|
|
24
|
+
return { persistent: true, offline: false, distance: "cosine", maxDimension: 1536, capabilities: ["vector.insert", "vector.search"] };
|
|
53
25
|
}
|
|
54
26
|
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
async query(vector, { topK = 5, minScore = 0 } = {}) {
|
|
58
|
-
this.ensureCapability("vector.search");
|
|
59
|
-
this.validateVector(vector);
|
|
60
|
-
|
|
61
|
-
const res = await this.index.query({
|
|
62
|
-
vector,
|
|
63
|
-
topK,
|
|
64
|
-
includeMetadata: true
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
return res.matches
|
|
68
|
-
.filter(m => m.score >= minScore)
|
|
69
|
-
.map(m => ({
|
|
70
|
-
id: m.id,
|
|
71
|
-
score: m.score,
|
|
72
|
-
content: m.metadata?.content,
|
|
73
|
-
source: m.metadata?.source,
|
|
74
|
-
metadata: m.metadata
|
|
75
|
-
}));
|
|
27
|
+
async init() {
|
|
28
|
+
// ... your init logic here
|
|
76
29
|
}
|
|
77
30
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
async health() {
|
|
81
|
-
try {
|
|
82
|
-
await this.client.listIndexes();
|
|
83
|
-
return {
|
|
84
|
-
backend: this.backend,
|
|
85
|
-
status: "ok",
|
|
86
|
-
offline: false,
|
|
87
|
-
hybrid: true
|
|
88
|
-
};
|
|
89
|
-
} catch (err) {
|
|
90
|
-
return {
|
|
91
|
-
backend: this.backend,
|
|
92
|
-
status: "error",
|
|
93
|
-
error: err.message
|
|
94
|
-
};
|
|
95
|
-
}
|
|
31
|
+
async upsert(id, vector, metadata) {
|
|
32
|
+
// ...
|
|
96
33
|
}
|
|
97
34
|
|
|
98
|
-
async
|
|
35
|
+
async search(queryVector, limit = 5) {
|
|
36
|
+
// ...
|
|
37
|
+
}
|
|
99
38
|
}
|
|
100
39
|
|
|
101
40
|
module.exports = PineconeAdapter;
|
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
const InMemoryAdapter = require("./inMemoryAdapter");
|
|
2
|
+
const PgVectorAdapter = require("./pgvectorAdapter");
|
|
3
|
+
const RedisAdapter = require("./redisAdapter");
|
|
4
|
+
const PineconeAdapter = require("./pineconeAdapter");
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* VectorRouter
|
|
8
|
+
* Dynamically selects vector backend at runtime
|
|
9
|
+
*/
|
|
1
10
|
class VectorRouter {
|
|
2
11
|
static create(config = {}) {
|
|
3
12
|
// 1. Explicit backend override (advanced users only)
|
|
@@ -7,36 +16,26 @@ class VectorRouter {
|
|
|
7
16
|
|
|
8
17
|
// 2. Auto-detect pgvector (DEFAULT)
|
|
9
18
|
if (config.POSTGRES_URL) {
|
|
10
|
-
const PgVectorAdapter = require("./pgvectorAdapter");
|
|
11
19
|
return new PgVectorAdapter(config);
|
|
12
20
|
}
|
|
13
21
|
|
|
14
22
|
// 3. Fallback to in-memory (safe default)
|
|
15
|
-
const InMemoryAdapter = require("./inMemoryAdapter");
|
|
16
23
|
return new InMemoryAdapter(config);
|
|
17
24
|
}
|
|
18
25
|
|
|
19
26
|
static _createExplicitBackend(backend, config) {
|
|
20
|
-
switch (backend) {
|
|
21
|
-
case "pgvector":
|
|
22
|
-
const PgVectorAdapter = require("./pgvectorAdapter");
|
|
27
|
+
switch (backend.toLowerCase()) {
|
|
28
|
+
case "pgvector":
|
|
23
29
|
return new PgVectorAdapter(config);
|
|
24
|
-
}
|
|
25
30
|
|
|
26
|
-
case "memory":
|
|
27
|
-
const InMemoryAdapter = require("./inMemoryAdapter");
|
|
31
|
+
case "memory":
|
|
28
32
|
return new InMemoryAdapter(config);
|
|
29
|
-
}
|
|
30
33
|
|
|
31
|
-
case "redis":
|
|
32
|
-
const RedisAdapter = require("./redisAdapter");
|
|
34
|
+
case "redis":
|
|
33
35
|
return new RedisAdapter(config);
|
|
34
|
-
}
|
|
35
36
|
|
|
36
|
-
case "pinecone":
|
|
37
|
-
const PineconeAdapter = require("./pineconeAdapter");
|
|
37
|
+
case "pinecone":
|
|
38
38
|
return new PineconeAdapter(config);
|
|
39
|
-
}
|
|
40
39
|
|
|
41
40
|
default:
|
|
42
41
|
throw new Error(`Unknown vector backend: ${backend}`);
|