@langchain/neo4j 0.0.1

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.
Files changed (76) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/dist/_virtual/_rolldown/runtime.cjs +23 -0
  4. package/dist/chains/graph_qa/cypher.cjs +117 -0
  5. package/dist/chains/graph_qa/cypher.cjs.map +1 -0
  6. package/dist/chains/graph_qa/cypher.d.cts +73 -0
  7. package/dist/chains/graph_qa/cypher.d.cts.map +1 -0
  8. package/dist/chains/graph_qa/cypher.d.ts +73 -0
  9. package/dist/chains/graph_qa/cypher.d.ts.map +1 -0
  10. package/dist/chains/graph_qa/cypher.js +115 -0
  11. package/dist/chains/graph_qa/cypher.js.map +1 -0
  12. package/dist/chains/graph_qa/index.cjs +2 -0
  13. package/dist/chains/graph_qa/index.d.ts +2 -0
  14. package/dist/chains/graph_qa/index.js +3 -0
  15. package/dist/chains/graph_qa/prompts.cjs +42 -0
  16. package/dist/chains/graph_qa/prompts.cjs.map +1 -0
  17. package/dist/chains/graph_qa/prompts.d.cts +14 -0
  18. package/dist/chains/graph_qa/prompts.d.cts.map +1 -0
  19. package/dist/chains/graph_qa/prompts.d.ts +14 -0
  20. package/dist/chains/graph_qa/prompts.d.ts.map +1 -0
  21. package/dist/chains/graph_qa/prompts.js +40 -0
  22. package/dist/chains/graph_qa/prompts.js.map +1 -0
  23. package/dist/graphs/document.cjs +63 -0
  24. package/dist/graphs/document.cjs.map +1 -0
  25. package/dist/graphs/document.d.cts +56 -0
  26. package/dist/graphs/document.d.cts.map +1 -0
  27. package/dist/graphs/document.d.ts +56 -0
  28. package/dist/graphs/document.d.ts.map +1 -0
  29. package/dist/graphs/document.js +60 -0
  30. package/dist/graphs/document.js.map +1 -0
  31. package/dist/graphs/index.cjs +3 -0
  32. package/dist/graphs/index.d.ts +3 -0
  33. package/dist/graphs/index.js +4 -0
  34. package/dist/graphs/memgraph_graph.cjs +74 -0
  35. package/dist/graphs/memgraph_graph.cjs.map +1 -0
  36. package/dist/graphs/memgraph_graph.d.cts +36 -0
  37. package/dist/graphs/memgraph_graph.d.cts.map +1 -0
  38. package/dist/graphs/memgraph_graph.d.ts +36 -0
  39. package/dist/graphs/memgraph_graph.d.ts.map +1 -0
  40. package/dist/graphs/memgraph_graph.js +74 -0
  41. package/dist/graphs/memgraph_graph.js.map +1 -0
  42. package/dist/graphs/neo4j_graph.cjs +429 -0
  43. package/dist/graphs/neo4j_graph.cjs.map +1 -0
  44. package/dist/graphs/neo4j_graph.d.cts +96 -0
  45. package/dist/graphs/neo4j_graph.d.cts.map +1 -0
  46. package/dist/graphs/neo4j_graph.d.ts +96 -0
  47. package/dist/graphs/neo4j_graph.d.ts.map +1 -0
  48. package/dist/graphs/neo4j_graph.js +425 -0
  49. package/dist/graphs/neo4j_graph.js.map +1 -0
  50. package/dist/index.cjs +28 -0
  51. package/dist/index.d.cts +8 -0
  52. package/dist/index.d.ts +8 -0
  53. package/dist/index.js +12 -0
  54. package/dist/stores/message/index.cjs +1 -0
  55. package/dist/stores/message/index.d.ts +1 -0
  56. package/dist/stores/message/index.js +2 -0
  57. package/dist/stores/message/neo4j.cjs +114 -0
  58. package/dist/stores/message/neo4j.cjs.map +1 -0
  59. package/dist/stores/message/neo4j.d.cts +40 -0
  60. package/dist/stores/message/neo4j.d.cts.map +1 -0
  61. package/dist/stores/message/neo4j.d.ts +40 -0
  62. package/dist/stores/message/neo4j.d.ts.map +1 -0
  63. package/dist/stores/message/neo4j.js +112 -0
  64. package/dist/stores/message/neo4j.js.map +1 -0
  65. package/dist/vectorstores/index.cjs +1 -0
  66. package/dist/vectorstores/index.d.ts +1 -0
  67. package/dist/vectorstores/index.js +2 -0
  68. package/dist/vectorstores/neo4j_vector.cjs +622 -0
  69. package/dist/vectorstores/neo4j_vector.cjs.map +1 -0
  70. package/dist/vectorstores/neo4j_vector.d.cts +96 -0
  71. package/dist/vectorstores/neo4j_vector.d.cts.map +1 -0
  72. package/dist/vectorstores/neo4j_vector.d.ts +96 -0
  73. package/dist/vectorstores/neo4j_vector.d.ts.map +1 -0
  74. package/dist/vectorstores/neo4j_vector.js +616 -0
  75. package/dist/vectorstores/neo4j_vector.js.map +1 -0
  76. package/package.json +98 -0
@@ -0,0 +1,622 @@
1
+ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
2
+ let neo4j_driver = require("neo4j-driver");
3
+ neo4j_driver = require_runtime.__toESM(neo4j_driver);
4
+ let uuid = require("uuid");
5
+ uuid = require_runtime.__toESM(uuid);
6
+ let _langchain_core_vectorstores = require("@langchain/core/vectorstores");
7
+ let _langchain_core_documents = require("@langchain/core/documents");
8
+ //#region src/vectorstores/neo4j_vector.ts
9
+ const DEFAULT_SEARCH_TYPE = "vector";
10
+ const DEFAULT_INDEX_TYPE = "NODE";
11
+ const DEFAULT_DISTANCE_STRATEGY = "cosine";
12
+ const DEFAULT_NODE_EMBEDDING_PROPERTY = "embedding";
13
+ /**
14
+ * @security *Security note*: Make sure that the database connection uses credentials
15
+ * that are narrowly-scoped to only include necessary permissions.
16
+ * Failure to do so may result in data corruption or loss, since the calling
17
+ * code may attempt commands that would result in deletion, mutation
18
+ * of data if appropriately prompted or reading sensitive data if such
19
+ * data is present in the database.
20
+ * The best way to guard against such negative outcomes is to (as appropriate)
21
+ * limit the permissions granted to the credentials used with this tool.
22
+ * For example, creating read only users for the database is a good way to
23
+ * ensure that the calling code cannot mutate or delete data.
24
+ *
25
+ * @link See https://js.langchain.com/docs/security for more information.
26
+ */
27
+ var Neo4jVectorStore = class Neo4jVectorStore extends _langchain_core_vectorstores.VectorStore {
28
+ driver;
29
+ database;
30
+ preDeleteCollection;
31
+ nodeLabel;
32
+ embeddingNodeProperty;
33
+ embeddingDimension;
34
+ textNodeProperty;
35
+ keywordIndexName;
36
+ indexName;
37
+ retrievalQuery;
38
+ searchType;
39
+ indexType;
40
+ distanceStrategy = DEFAULT_DISTANCE_STRATEGY;
41
+ supportMetadataFilter = true;
42
+ isEnterprise = false;
43
+ _vectorstoreType() {
44
+ return "neo4jvector";
45
+ }
46
+ constructor(embeddings, config) {
47
+ super(embeddings, config);
48
+ }
49
+ static async initialize(embeddings, config) {
50
+ const store = new Neo4jVectorStore(embeddings, config);
51
+ await store._initializeDriver(config);
52
+ await store._verifyConnectivity();
53
+ const { preDeleteCollection = false, nodeLabel = "Chunk", textNodeProperty = "text", embeddingNodeProperty = DEFAULT_NODE_EMBEDDING_PROPERTY, keywordIndexName = "keyword", indexName = "vector", retrievalQuery = "", searchType = DEFAULT_SEARCH_TYPE, indexType = DEFAULT_INDEX_TYPE } = config;
54
+ store.embeddingDimension = (await embeddings.embedQuery("foo")).length;
55
+ store.preDeleteCollection = preDeleteCollection;
56
+ store.nodeLabel = nodeLabel;
57
+ store.textNodeProperty = textNodeProperty;
58
+ store.embeddingNodeProperty = embeddingNodeProperty;
59
+ store.keywordIndexName = keywordIndexName;
60
+ store.indexName = indexName;
61
+ store.retrievalQuery = retrievalQuery;
62
+ store.searchType = searchType;
63
+ store.indexType = indexType;
64
+ if (store.preDeleteCollection) await store._dropIndex();
65
+ return store;
66
+ }
67
+ async _initializeDriver({ url, username, password, database = "neo4j" }) {
68
+ try {
69
+ this.driver = neo4j_driver.default.driver(url, neo4j_driver.default.auth.basic(username, password));
70
+ this.database = database;
71
+ } catch {
72
+ throw new Error("Could not create a Neo4j driver instance. Please check the connection details.");
73
+ }
74
+ }
75
+ async _verifyConnectivity() {
76
+ await this.driver.verifyAuthentication();
77
+ }
78
+ async _verifyVersion() {
79
+ try {
80
+ const data = await this.query("CALL dbms.components()");
81
+ const versionString = data[0].versions[0];
82
+ const targetVersion = [
83
+ 5,
84
+ 11,
85
+ 0
86
+ ];
87
+ let version;
88
+ if (versionString.includes("aura")) {
89
+ version = versionString.split("-")[0].split(".").map(Number);
90
+ version.push(0);
91
+ } else version = versionString.split(".").map(Number);
92
+ if (isVersionLessThan(version, targetVersion)) throw new Error("Version index is only supported in Neo4j version 5.11 or greater");
93
+ if (isVersionLessThan(version, [
94
+ 5,
95
+ 18,
96
+ 0
97
+ ])) this.supportMetadataFilter = false;
98
+ this.isEnterprise = data[0].edition === "enterprise";
99
+ } catch (error) {
100
+ console.error("Database version check failed:", error);
101
+ }
102
+ }
103
+ async close() {
104
+ await this.driver.close();
105
+ }
106
+ async _dropIndex() {
107
+ try {
108
+ await this.query(`
109
+ MATCH (n:\`${this.nodeLabel}\`)
110
+ CALL {
111
+ WITH n
112
+ DETACH DELETE n
113
+ }
114
+ IN TRANSACTIONS OF 10000 ROWS;
115
+ `);
116
+ await this.query(`DROP INDEX ${this.indexName}`);
117
+ } catch (error) {
118
+ console.error("An error occurred while dropping the index:", error);
119
+ }
120
+ }
121
+ async query(query, params = {}) {
122
+ return toObjects((await this.driver.session({ database: this.database }).run(query, params)).records);
123
+ }
124
+ static async fromTexts(texts, metadatas, embeddings, config) {
125
+ const docs = [];
126
+ for (let i = 0; i < texts.length; i += 1) {
127
+ const metadata = Array.isArray(metadatas) ? metadatas[i] : metadatas;
128
+ const newDoc = new _langchain_core_documents.Document({
129
+ pageContent: texts[i],
130
+ metadata
131
+ });
132
+ docs.push(newDoc);
133
+ }
134
+ return Neo4jVectorStore.fromDocuments(docs, embeddings, config);
135
+ }
136
+ static async fromDocuments(docs, embeddings, config) {
137
+ const { searchType = DEFAULT_SEARCH_TYPE, createIdIndex = true, textNodeProperties = [] } = config;
138
+ const store = await this.initialize(embeddings, config);
139
+ const embeddingDimension = await store.retrieveExistingIndex();
140
+ if (!embeddingDimension) await store.createNewIndex();
141
+ else if (store.embeddingDimension !== embeddingDimension) throw new Error(`Index with name "${store.indexName}" already exists. The provided embedding function and vector index dimensions do not match.
142
+ Embedding function dimension: ${store.embeddingDimension}
143
+ Vector index dimension: ${embeddingDimension}`);
144
+ if (searchType === "hybrid") {
145
+ const ftsNodeLabel = await store.retrieveExistingFtsIndex();
146
+ if (!ftsNodeLabel) await store.createNewKeywordIndex(textNodeProperties);
147
+ else if (ftsNodeLabel !== store.nodeLabel) throw Error("Vector and keyword index don't index the same node label");
148
+ }
149
+ if (createIdIndex) await store.query(`CREATE CONSTRAINT IF NOT EXISTS FOR (n:${store.nodeLabel}) REQUIRE n.id IS UNIQUE;`);
150
+ await store.addDocuments(docs);
151
+ return store;
152
+ }
153
+ static async fromExistingIndex(embeddings, config) {
154
+ const { searchType = DEFAULT_SEARCH_TYPE, keywordIndexName = "keyword" } = config;
155
+ if (searchType === "hybrid" && !keywordIndexName) throw Error("keyword_index name has to be specified when using hybrid search option");
156
+ const store = await this.initialize(embeddings, config);
157
+ const embeddingDimension = await store.retrieveExistingIndex();
158
+ if (!embeddingDimension) throw Error("The specified vector index name does not exist. Make sure to check if you spelled it correctly");
159
+ if (store.embeddingDimension !== embeddingDimension) throw new Error(`The provided embedding function and vector index dimensions do not match.
160
+ Embedding function dimension: ${store.embeddingDimension}
161
+ Vector index dimension: ${embeddingDimension}`);
162
+ if (searchType === "hybrid") {
163
+ const ftsNodeLabel = await store.retrieveExistingFtsIndex();
164
+ if (!ftsNodeLabel) throw Error("The specified keyword index name does not exist. Make sure to check if you spelled it correctly");
165
+ else if (ftsNodeLabel !== store.nodeLabel) throw Error("Vector and keyword index don't index the same node label");
166
+ }
167
+ return store;
168
+ }
169
+ static async fromExistingGraph(embeddings, config) {
170
+ const { textNodeProperties = [], embeddingNodeProperty = DEFAULT_NODE_EMBEDDING_PROPERTY, searchType = DEFAULT_SEARCH_TYPE, retrievalQuery = "", nodeLabel } = config;
171
+ let _retrievalQuery = retrievalQuery;
172
+ if (textNodeProperties.length === 0) throw Error("Parameter `text_node_properties` must not be an empty array");
173
+ if (!retrievalQuery) _retrievalQuery = `
174
+ RETURN reduce(str='', k IN ${JSON.stringify(textNodeProperties)} |
175
+ str + '\\n' + k + ': ' + coalesce(node[k], '')) AS text,
176
+ node {.*, \`${embeddingNodeProperty}\`: Null, id: Null, ${textNodeProperties.map((prop) => `\`${prop}\`: Null`).join(", ")} } AS metadata, score
177
+ `;
178
+ const store = await this.initialize(embeddings, {
179
+ ...config,
180
+ retrievalQuery: _retrievalQuery
181
+ });
182
+ const embeddingDimension = await store.retrieveExistingIndex();
183
+ if (!embeddingDimension) await store.createNewIndex();
184
+ else if (store.embeddingDimension !== embeddingDimension) throw new Error(`Index with name ${store.indexName} already exists. The provided embedding function and vector index dimensions do not match.\nEmbedding function dimension: ${store.embeddingDimension}\nVector index dimension: ${embeddingDimension}`);
185
+ if (searchType === "hybrid") {
186
+ const ftsNodeLabel = await store.retrieveExistingFtsIndex(textNodeProperties);
187
+ if (!ftsNodeLabel) await store.createNewKeywordIndex(textNodeProperties);
188
+ else if (ftsNodeLabel !== store.nodeLabel) throw Error("Vector and keyword index don't index the same node label");
189
+ }
190
+ while (true) {
191
+ const fetchQuery = `
192
+ MATCH (n:\`${nodeLabel}\`)
193
+ WHERE n.${embeddingNodeProperty} IS null
194
+ AND any(k in $props WHERE n[k] IS NOT null)
195
+ RETURN elementId(n) AS id, reduce(str='', k IN $props |
196
+ str + '\\n' + k + ':' + coalesce(n[k], '')) AS text
197
+ LIMIT 1000
198
+ `;
199
+ const data = await store.query(fetchQuery, { props: textNodeProperties });
200
+ if (!data) break;
201
+ const textEmbeddings = await embeddings.embedDocuments(data.map((el) => el.text));
202
+ const params = { data: data.map((el, index) => ({
203
+ id: el.id,
204
+ embedding: textEmbeddings[index]
205
+ })) };
206
+ await store.query(`
207
+ UNWIND $data AS row
208
+ MATCH (n:\`${nodeLabel}\`)
209
+ WHERE elementId(n) = row.id
210
+ CALL db.create.setVectorProperty(n, '${embeddingNodeProperty}', row.embedding)
211
+ YIELD node RETURN count(*)
212
+ `, params);
213
+ if (data.length < 1e3) break;
214
+ }
215
+ return store;
216
+ }
217
+ async createNewIndex() {
218
+ const indexQuery = `
219
+ CALL db.index.vector.createNodeIndex(
220
+ $index_name,
221
+ $node_label,
222
+ $embedding_node_property,
223
+ toInteger($embedding_dimension),
224
+ $similarity_metric
225
+ )
226
+ `;
227
+ const parameters = {
228
+ index_name: this.indexName,
229
+ node_label: this.nodeLabel,
230
+ embedding_node_property: this.embeddingNodeProperty,
231
+ embedding_dimension: this.embeddingDimension,
232
+ similarity_metric: this.distanceStrategy
233
+ };
234
+ await this.query(indexQuery, parameters);
235
+ }
236
+ async retrieveExistingIndex() {
237
+ let indexInformation = await this.query(`
238
+ SHOW INDEXES YIELD name, type, labelsOrTypes, properties, options
239
+ WHERE type = 'VECTOR' AND (name = $index_name
240
+ OR (labelsOrTypes[0] = $node_label AND
241
+ properties[0] = $embedding_node_property))
242
+ RETURN name, labelsOrTypes, properties, options
243
+ `, {
244
+ index_name: this.indexName,
245
+ node_label: this.nodeLabel,
246
+ embedding_node_property: this.embeddingNodeProperty
247
+ });
248
+ if (indexInformation) {
249
+ indexInformation = this.sortByIndexName(indexInformation, this.indexName);
250
+ try {
251
+ const [index] = indexInformation;
252
+ const [labelOrType] = index.labelsOrTypes;
253
+ const [property] = index.properties;
254
+ this.indexName = index.name;
255
+ this.nodeLabel = labelOrType;
256
+ this.embeddingNodeProperty = property;
257
+ const embeddingDimension = index.options.indexConfig["vector.dimensions"];
258
+ return Number(embeddingDimension);
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+ return null;
264
+ }
265
+ async retrieveExistingFtsIndex(textNodeProperties = []) {
266
+ const indexInformation = await this.query(`
267
+ SHOW INDEXES YIELD name, type, labelsOrTypes, properties, options
268
+ WHERE type = 'FULLTEXT' AND (name = $keyword_index_name
269
+ OR (labelsOrTypes = [$node_label] AND
270
+ properties = $text_node_property))
271
+ RETURN name, labelsOrTypes, properties, options
272
+ `, {
273
+ keyword_index_name: this.keywordIndexName,
274
+ node_label: this.nodeLabel,
275
+ text_node_property: textNodeProperties.length > 0 ? textNodeProperties : [this.textNodeProperty]
276
+ });
277
+ if (indexInformation) {
278
+ const sortedIndexInformation = this.sortByIndexName(indexInformation, this.indexName);
279
+ try {
280
+ const [index] = sortedIndexInformation;
281
+ const [labelOrType] = index.labelsOrTypes;
282
+ const [property] = index.properties;
283
+ this.keywordIndexName = index.name;
284
+ this.textNodeProperty = property;
285
+ this.nodeLabel = labelOrType;
286
+ return labelOrType;
287
+ } catch {
288
+ return null;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+ async createNewKeywordIndex(textNodeProperties = []) {
294
+ const nodeProps = textNodeProperties.length > 0 ? textNodeProperties : [this.textNodeProperty];
295
+ const ftsIndexQuery = `
296
+ CREATE FULLTEXT INDEX ${this.keywordIndexName}
297
+ FOR (n:\`${this.nodeLabel}\`) ON EACH
298
+ [${nodeProps.map((prop) => `n.\`${prop}\``).join(", ")}]
299
+ `;
300
+ await this.query(ftsIndexQuery);
301
+ }
302
+ sortByIndexName(values, indexName) {
303
+ return values.sort((a, b) => (a.name === indexName ? -1 : 0) - (b.name === indexName ? -1 : 0));
304
+ }
305
+ async addVectors(vectors, documents, metadatas, ids) {
306
+ let _ids = ids;
307
+ const _metadatas = metadatas;
308
+ if (!_ids) _ids = documents.map(() => uuid.v1());
309
+ const importQuery = `
310
+ UNWIND $data AS row
311
+ CALL {
312
+ WITH row
313
+ MERGE (c:\`${this.nodeLabel}\` {id: row.id})
314
+ WITH c, row
315
+ CALL db.create.setVectorProperty(c, '${this.embeddingNodeProperty}', row.embedding)
316
+ YIELD node
317
+ SET c.\`${this.textNodeProperty}\` = row.text
318
+ SET c += row.metadata
319
+ } IN TRANSACTIONS OF 1000 ROWS
320
+ `;
321
+ const parameters = { data: documents.map(({ pageContent, metadata }, index) => ({
322
+ text: pageContent,
323
+ metadata: _metadatas ? _metadatas[index] : metadata,
324
+ embedding: vectors[index],
325
+ id: _ids ? _ids[index] : null
326
+ })) };
327
+ await this.query(importQuery, parameters);
328
+ return _ids;
329
+ }
330
+ async addDocuments(documents) {
331
+ const texts = documents.map(({ pageContent }) => pageContent);
332
+ return this.addVectors(await this.embeddings.embedDocuments(texts), documents);
333
+ }
334
+ async similaritySearch(query, k = 4, params = {}) {
335
+ const embedding = await this.embeddings.embedQuery(query);
336
+ return (await this.similaritySearchVectorWithScore(embedding, k, query, params)).map((result) => result[0]);
337
+ }
338
+ async similaritySearchWithScore(query, k = 4, params = {}) {
339
+ const embedding = await this.embeddings.embedQuery(query);
340
+ return this.similaritySearchVectorWithScore(embedding, k, query, params);
341
+ }
342
+ async similaritySearchVectorWithScore(vector, k, query, params = {}) {
343
+ let indexQuery;
344
+ let filterParams;
345
+ const { filter } = params;
346
+ if (filter) {
347
+ if (!this.supportMetadataFilter) throw new Error("Metadata filtering is only supported in Neo4j version 5.18 or greater.");
348
+ if (this.searchType === "hybrid") throw new Error("Metadata filtering can't be use in combination with a hybrid search approach.");
349
+ const baseIndexQuery = `
350
+ ${this.isEnterprise ? "CYPHER runtime = parallel parallelRuntimeSupport=all " : ""}
351
+ MATCH (n:\`${this.nodeLabel}\`)
352
+ WHERE n.\`${this.embeddingNodeProperty}\` IS NOT NULL
353
+ AND size(n.\`${this.embeddingNodeProperty}\`) = toInteger(${this.embeddingDimension}) AND
354
+ `;
355
+ const baseCosineQuery = `
356
+ WITH n as node, vector.similarity.cosine(
357
+ n.\`${this.embeddingNodeProperty}\`,
358
+ $embedding
359
+ ) AS score ORDER BY score DESC LIMIT toInteger($k)
360
+ `;
361
+ const [fSnippets, fParams] = constructMetadataFilter(filter);
362
+ indexQuery = baseIndexQuery + fSnippets + baseCosineQuery;
363
+ filterParams = fParams;
364
+ } else {
365
+ indexQuery = getSearchIndexQuery(this.searchType, this.indexType);
366
+ filterParams = {};
367
+ }
368
+ let defaultRetrieval;
369
+ if (this.indexType === "RELATIONSHIP") defaultRetrieval = `
370
+ RETURN relationship.${this.textNodeProperty} AS text, score,
371
+ relationship {.*, ${this.textNodeProperty}: Null,
372
+ ${this.embeddingNodeProperty}: Null, id: Null } AS metadata
373
+ `;
374
+ else defaultRetrieval = `
375
+ RETURN node.${this.textNodeProperty} AS text, score,
376
+ node {.*, ${this.textNodeProperty}: Null,
377
+ ${this.embeddingNodeProperty}: Null, id: Null } AS metadata
378
+ `;
379
+ const retrievalQuery = this.retrievalQuery ? this.retrievalQuery : defaultRetrieval;
380
+ const readQuery = `${indexQuery} ${retrievalQuery}`;
381
+ const parameters = {
382
+ index: this.indexName,
383
+ k: Number(k),
384
+ embedding: vector,
385
+ keyword_index: this.keywordIndexName,
386
+ query: removeLuceneChars(query),
387
+ ...params,
388
+ ...filterParams
389
+ };
390
+ const results = await this.query(readQuery, parameters);
391
+ if (results) {
392
+ if (results.some((result) => result.text == null)) if (!this.retrievalQuery) throw new Error(`Make sure that none of the '${this.textNodeProperty}' properties on nodes with label '${this.nodeLabel}' are missing or empty`);
393
+ else throw new Error("Inspect the 'retrievalQuery' and ensure it doesn't return null for the 'text' column");
394
+ return results.map((result) => [new _langchain_core_documents.Document({
395
+ pageContent: result.text,
396
+ metadata: Object.fromEntries(Object.entries(result.metadata).filter(([_, v]) => v !== null))
397
+ }), result.score]);
398
+ }
399
+ return [];
400
+ }
401
+ };
402
+ function toObjects(records) {
403
+ return records.map((record) => {
404
+ const rObj = record.toObject();
405
+ const out = {};
406
+ Object.keys(rObj).forEach((key) => {
407
+ out[key] = itemIntToString(rObj[key]);
408
+ });
409
+ return out;
410
+ });
411
+ }
412
+ function itemIntToString(item) {
413
+ if (neo4j_driver.default.isInt(item)) return item.toString();
414
+ if (Array.isArray(item)) return item.map((ii) => itemIntToString(ii));
415
+ if ([
416
+ "number",
417
+ "string",
418
+ "boolean"
419
+ ].indexOf(typeof item) !== -1) return item;
420
+ if (item === null) return item;
421
+ if (typeof item === "object") return objIntToString(item);
422
+ }
423
+ function objIntToString(obj) {
424
+ const entry = extractFromNeoObjects(obj);
425
+ let newObj = null;
426
+ if (Array.isArray(entry)) newObj = entry.map((item) => itemIntToString(item));
427
+ else if (entry !== null && typeof entry === "object") {
428
+ newObj = {};
429
+ Object.keys(entry).forEach((key) => {
430
+ newObj[key] = itemIntToString(entry[key]);
431
+ });
432
+ }
433
+ return newObj;
434
+ }
435
+ function extractFromNeoObjects(obj) {
436
+ if (obj instanceof neo4j_driver.default.types.Node || obj instanceof neo4j_driver.default.types.Relationship) return obj.properties;
437
+ else if (obj instanceof neo4j_driver.default.types.Path) return [].concat.apply([], extractPathForRows(obj));
438
+ return obj;
439
+ }
440
+ function extractPathForRows(path) {
441
+ let { segments } = path;
442
+ if (!Array.isArray(path.segments) || path.segments.length < 1) segments = [{
443
+ ...path,
444
+ end: null
445
+ }];
446
+ return segments.map((segment) => [
447
+ objIntToString(segment.start),
448
+ objIntToString(segment.relationship),
449
+ objIntToString(segment.end)
450
+ ].filter((part) => part !== null));
451
+ }
452
+ function getSearchIndexQuery(searchType, indexType = DEFAULT_INDEX_TYPE) {
453
+ if (indexType === "NODE") return {
454
+ vector: "CALL db.index.vector.queryNodes($index, $k, $embedding) YIELD node, score",
455
+ hybrid: `
456
+ CALL {
457
+ CALL db.index.vector.queryNodes($index, $k, $embedding) YIELD node, score
458
+ WITH collect({node:node, score:score}) AS nodes, max(score) AS max
459
+ UNWIND nodes AS n
460
+ // We use 0 as min
461
+ RETURN n.node AS node, (n.score / max) AS score UNION
462
+ CALL db.index.fulltext.queryNodes($keyword_index, $query, {limit: $k}) YIELD node, score
463
+ WITH collect({node: node, score: score}) AS nodes, max(score) AS max
464
+ UNWIND nodes AS n
465
+ RETURN n.node AS node, (n.score / max) AS score
466
+ }
467
+ WITH node, max(score) AS score ORDER BY score DESC LIMIT toInteger($k)
468
+ `
469
+ }[searchType];
470
+ else return `
471
+ CALL db.index.vector.queryRelationships($index, $k, $embedding)
472
+ YIELD relationship, score
473
+ `;
474
+ }
475
+ function removeLuceneChars(text) {
476
+ if (text === void 0 || text === null) return null;
477
+ const specialChars = [
478
+ "+",
479
+ "-",
480
+ "&",
481
+ "|",
482
+ "!",
483
+ "(",
484
+ ")",
485
+ "{",
486
+ "}",
487
+ "[",
488
+ "]",
489
+ "^",
490
+ "\"",
491
+ "~",
492
+ "*",
493
+ "?",
494
+ ":",
495
+ "\\"
496
+ ];
497
+ let modifiedText = text;
498
+ for (const char of specialChars) modifiedText = modifiedText.split(char).join(" ");
499
+ return modifiedText.trim();
500
+ }
501
+ function isVersionLessThan(v1, v2) {
502
+ for (let i = 0; i < Math.min(v1.length, v2.length); i += 1) if (v1[i] < v2[i]) return true;
503
+ else if (v1[i] > v2[i]) return false;
504
+ return v1.length < v2.length;
505
+ }
506
+ const COMPARISONS_TO_NATIVE = {
507
+ $eq: "=",
508
+ $ne: "<>",
509
+ $lt: "<",
510
+ $lte: "<=",
511
+ $gt: ">",
512
+ $gte: ">="
513
+ };
514
+ const COMPARISONS_TO_NATIVE_OPERATORS = new Set(Object.keys(COMPARISONS_TO_NATIVE));
515
+ const TEXT_OPERATORS = new Set(["$like", "$ilike"]);
516
+ const LOGICAL_OPERATORS = new Set(["$and", "$or"]);
517
+ const SPECIAL_CASED_OPERATORS = new Set([
518
+ "$in",
519
+ "$nin",
520
+ "$between"
521
+ ]);
522
+ const SUPPORTED_OPERATORS = new Set([
523
+ ...COMPARISONS_TO_NATIVE_OPERATORS,
524
+ ...TEXT_OPERATORS,
525
+ ...LOGICAL_OPERATORS,
526
+ ...SPECIAL_CASED_OPERATORS
527
+ ]);
528
+ const IS_IDENTIFIER_REGEX = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
529
+ function combineQueries(inputQueries, operator) {
530
+ let combinedQuery = "";
531
+ const combinedParams = {};
532
+ const paramCounter = {};
533
+ for (const [query, params] of inputQueries) {
534
+ let newQuery = query;
535
+ for (const [param, value] of Object.entries(params)) {
536
+ if (param in paramCounter) paramCounter[param] += 1;
537
+ else paramCounter[param] = 1;
538
+ const newParamName = `${param}_${paramCounter[param]}`;
539
+ newQuery = newQuery.replace(`$${param}`, `$${newParamName}`);
540
+ combinedParams[newParamName] = value;
541
+ }
542
+ if (combinedQuery) combinedQuery += ` ${operator} `;
543
+ combinedQuery += `(${newQuery})`;
544
+ }
545
+ return [combinedQuery, combinedParams];
546
+ }
547
+ function collectParams(inputData) {
548
+ const queryParts = [];
549
+ const params = {};
550
+ for (const [queryPart, param] of inputData) {
551
+ queryParts.push(queryPart);
552
+ Object.assign(params, param);
553
+ }
554
+ return [queryParts, params];
555
+ }
556
+ function handleFieldFilter(field, value, paramNumber = 1) {
557
+ if (typeof field !== "string") throw new Error(`field should be a string but got: ${typeof field} with value: ${field}`);
558
+ if (field.startsWith("$")) throw new Error(`Invalid filter condition. Expected a field but got an operator: ${field}`);
559
+ if (!IS_IDENTIFIER_REGEX.test(field)) throw new Error(`Invalid field name: ${field}. Expected a valid identifier.`);
560
+ let operator;
561
+ let filterValue;
562
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
563
+ const keys = Object.keys(value);
564
+ if (keys.length !== 1) throw new Error(`Invalid filter condition. Expected a value which is a dictionary
565
+ with a single key that corresponds to an operator but got a dictionary
566
+ with ${keys.length} keys. The first few keys are: ${keys.slice(0, 3).join(", ")}
567
+ `);
568
+ operator = keys[0];
569
+ filterValue = value[operator];
570
+ if (!SUPPORTED_OPERATORS.has(operator)) throw new Error(`Invalid operator: ${operator}. Expected one of ${SUPPORTED_OPERATORS}`);
571
+ } else {
572
+ operator = "$eq";
573
+ filterValue = value;
574
+ }
575
+ if (COMPARISONS_TO_NATIVE_OPERATORS.has(operator)) return [`n.${field} ${COMPARISONS_TO_NATIVE[operator]} $param_${paramNumber}`, { [`param_${paramNumber}`]: filterValue }];
576
+ else if (operator === "$between") {
577
+ const [low, high] = filterValue;
578
+ return [`$param_${paramNumber}_low <= n.${field} <= $param_${paramNumber}_high`, {
579
+ [`param_${paramNumber}_low`]: low,
580
+ [`param_${paramNumber}_high`]: high
581
+ }];
582
+ } else if ([
583
+ "$in",
584
+ "$nin",
585
+ "$like",
586
+ "$ilike"
587
+ ].includes(operator)) {
588
+ if (["$in", "$nin"].includes(operator)) filterValue.forEach((val) => {
589
+ if (typeof val !== "string" && typeof val !== "number" && typeof val !== "boolean") throw new Error(`Unsupported type: ${typeof val} for value: ${val}`);
590
+ });
591
+ if (operator === "$in") return [`n.${field} IN $param_${paramNumber}`, { [`param_${paramNumber}`]: filterValue }];
592
+ else if (operator === "$nin") return [`n.${field} NOT IN $param_${paramNumber}`, { [`param_${paramNumber}`]: filterValue }];
593
+ else if (operator === "$like") return [`n.${field} CONTAINS $param_${paramNumber}`, { [`param_${paramNumber}`]: filterValue.slice(0, -1) }];
594
+ else if (operator === "$ilike") return [`toLower(n.${field}) CONTAINS $param_${paramNumber}`, { [`param_${paramNumber}`]: filterValue.slice(0, -1) }];
595
+ else throw new Error("Not Implemented");
596
+ } else throw new Error("Not Implemented");
597
+ }
598
+ function constructMetadataFilter(filter) {
599
+ if (typeof filter !== "object" || filter === null) throw new Error("Expected a dictionary representing the filter condition.");
600
+ const entries = Object.entries(filter);
601
+ if (entries.length === 1) {
602
+ const [key, value] = entries[0];
603
+ if (key.startsWith("$")) {
604
+ if (!["$and", "$or"].includes(key.toLowerCase())) throw new Error(`Invalid filter condition. Expected $and or $or but got: ${key}`);
605
+ if (!Array.isArray(value)) throw new Error(`Expected an array for logical conditions, but got ${typeof value} for value: ${value}`);
606
+ const operation = key.toLowerCase() === "$and" ? "AND" : "OR";
607
+ return combineQueries(value.map((v) => constructMetadataFilter(v)), operation);
608
+ } else return handleFieldFilter(key, value);
609
+ } else if (entries.length > 1) {
610
+ for (const [key] of entries) if (key.startsWith("$")) throw new Error(`Invalid filter condition. Expected a field but got an operator: ${key}`);
611
+ const and_multiple = collectParams(entries.map(([field, val], index) => handleFieldFilter(field, val, index + 1)));
612
+ if (and_multiple.length >= 1) return [and_multiple[0].join(" AND "), and_multiple[1]];
613
+ else throw Error("Invalid filter condition. Expected a dictionary but got an empty dictionary");
614
+ } else throw new Error("Filter condition contains no entries.");
615
+ }
616
+ //#endregion
617
+ exports.Neo4jVectorStore = Neo4jVectorStore;
618
+ exports.constructMetadataFilter = constructMetadataFilter;
619
+ exports.isVersionLessThan = isVersionLessThan;
620
+ exports.removeLuceneChars = removeLuceneChars;
621
+
622
+ //# sourceMappingURL=neo4j_vector.cjs.map