You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

293 lines
11 KiB

11 months ago
  1. const { Pinecone } = require("@pinecone-database/pinecone");
  2. const { TextSplitter } = require("../../TextSplitter");
  3. const { SystemSettings } = require("../../../models/systemSettings");
  4. const { storeVectorResult, cachedVectorInformation } = require("../../files");
  5. const { v4: uuidv4 } = require("uuid");
  6. const { toChunks, getEmbeddingEngineSelection } = require("../../helpers");
  7. const { sourceIdentifier } = require("../../chats");
  8. const PineconeDB = {
  9. name: "Pinecone",
  10. connect: async function () {
  11. if (process.env.VECTOR_DB !== "pinecone")
  12. throw new Error("Pinecone::Invalid ENV settings");
  13. const client = new Pinecone({
  14. apiKey: process.env.PINECONE_API_KEY,
  15. });
  16. const pineconeIndex = client.Index(process.env.PINECONE_INDEX);
  17. const { status } = await client.describeIndex(process.env.PINECONE_INDEX);
  18. if (!status.ready) throw new Error("Pinecone::Index not ready.");
  19. return { client, pineconeIndex, indexName: process.env.PINECONE_INDEX };
  20. },
  21. totalVectors: async function () {
  22. const { pineconeIndex } = await this.connect();
  23. const { namespaces } = await pineconeIndex.describeIndexStats();
  24. return Object.values(namespaces).reduce(
  25. (a, b) => a + (b?.recordCount || 0),
  26. 0
  27. );
  28. },
  29. namespaceCount: async function (_namespace = null) {
  30. const { pineconeIndex } = await this.connect();
  31. const namespace = await this.namespace(pineconeIndex, _namespace);
  32. return namespace?.recordCount || 0;
  33. },
  34. similarityResponse: async function ({
  35. client,
  36. namespace,
  37. queryVector,
  38. similarityThreshold = 0.25,
  39. topN = 4,
  40. filterIdentifiers = [],
  41. }) {
  42. const result = {
  43. contextTexts: [],
  44. sourceDocuments: [],
  45. scores: [],
  46. };
  47. const pineconeNamespace = client.namespace(namespace);
  48. const response = await pineconeNamespace.query({
  49. vector: queryVector,
  50. topK: topN,
  51. includeMetadata: true,
  52. });
  53. response.matches.forEach((match) => {
  54. if (match.score < similarityThreshold) return;
  55. if (filterIdentifiers.includes(sourceIdentifier(match.metadata))) {
  56. console.log(
  57. "Pinecone: A source was filtered from context as it's parent document is pinned."
  58. );
  59. return;
  60. }
  61. result.contextTexts.push(match.metadata.text);
  62. result.sourceDocuments.push(match);
  63. result.scores.push(match.score);
  64. });
  65. return result;
  66. },
  67. namespace: async function (index, namespace = null) {
  68. if (!namespace) throw new Error("No namespace value provided.");
  69. const { namespaces } = await index.describeIndexStats();
  70. return namespaces.hasOwnProperty(namespace) ? namespaces[namespace] : null;
  71. },
  72. hasNamespace: async function (namespace = null) {
  73. if (!namespace) return false;
  74. const { pineconeIndex } = await this.connect();
  75. return await this.namespaceExists(pineconeIndex, namespace);
  76. },
  77. namespaceExists: async function (index, namespace = null) {
  78. if (!namespace) throw new Error("No namespace value provided.");
  79. const { namespaces } = await index.describeIndexStats();
  80. return namespaces.hasOwnProperty(namespace);
  81. },
  82. deleteVectorsInNamespace: async function (index, namespace = null) {
  83. const pineconeNamespace = index.namespace(namespace);
  84. await pineconeNamespace.deleteAll();
  85. return true;
  86. },
  87. addDocumentToNamespace: async function (
  88. namespace,
  89. documentData = {},
  90. fullFilePath = null,
  91. skipCache = false
  92. ) {
  93. const { DocumentVectors } = require("../../../models/vectors");
  94. try {
  95. const { pageContent, docId, ...metadata } = documentData;
  96. if (!pageContent || pageContent.length == 0) return false;
  97. console.log("Adding new vectorized document into namespace", namespace);
  98. if (!skipCache) {
  99. const cacheResult = await cachedVectorInformation(fullFilePath);
  100. if (cacheResult.exists) {
  101. const { pineconeIndex } = await this.connect();
  102. const pineconeNamespace = pineconeIndex.namespace(namespace);
  103. const { chunks } = cacheResult;
  104. const documentVectors = [];
  105. for (const chunk of chunks) {
  106. // Before sending to Pinecone and saving the records to our db
  107. // we need to assign the id of each chunk that is stored in the cached file.
  108. const newChunks = chunk.map((chunk) => {
  109. const id = uuidv4();
  110. documentVectors.push({ docId, vectorId: id });
  111. return { ...chunk, id };
  112. });
  113. await pineconeNamespace.upsert([...newChunks]);
  114. }
  115. await DocumentVectors.bulkInsert(documentVectors);
  116. return { vectorized: true, error: null };
  117. }
  118. }
  119. // If we are here then we are going to embed and store a novel document.
  120. // We have to do this manually as opposed to using LangChains `PineconeStore.fromDocuments`
  121. // because we then cannot atomically control our namespace to granularly find/remove documents
  122. // from vectordb.
  123. // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L167
  124. const EmbedderEngine = getEmbeddingEngineSelection();
  125. const textSplitter = new TextSplitter({
  126. chunkSize: TextSplitter.determineMaxChunkSize(
  127. await SystemSettings.getValueOrFallback({
  128. label: "text_splitter_chunk_size",
  129. }),
  130. EmbedderEngine?.embeddingMaxChunkLength
  131. ),
  132. chunkOverlap: await SystemSettings.getValueOrFallback(
  133. { label: "text_splitter_chunk_overlap" },
  134. 20
  135. ),
  136. chunkHeaderMeta: TextSplitter.buildHeaderMeta(metadata),
  137. });
  138. const textChunks = await textSplitter.splitText(pageContent);
  139. console.log("Chunks created from document:", textChunks.length);
  140. const documentVectors = [];
  141. const vectors = [];
  142. const vectorValues = await EmbedderEngine.embedChunks(textChunks);
  143. if (!!vectorValues && vectorValues.length > 0) {
  144. for (const [i, vector] of vectorValues.entries()) {
  145. const vectorRecord = {
  146. id: uuidv4(),
  147. values: vector,
  148. // [DO NOT REMOVE]
  149. // LangChain will be unable to find your text if you embed manually and dont include the `text` key.
  150. // https://github.com/hwchase17/langchainjs/blob/2def486af734c0ca87285a48f1a04c057ab74bdf/langchain/src/vectorstores/pinecone.ts#L64
  151. metadata: { ...metadata, text: textChunks[i] },
  152. };
  153. vectors.push(vectorRecord);
  154. documentVectors.push({ docId, vectorId: vectorRecord.id });
  155. }
  156. } else {
  157. throw new Error(
  158. "Could not embed document chunks! This document will not be recorded."
  159. );
  160. }
  161. if (vectors.length > 0) {
  162. const chunks = [];
  163. const { pineconeIndex } = await this.connect();
  164. const pineconeNamespace = pineconeIndex.namespace(namespace);
  165. console.log("Inserting vectorized chunks into Pinecone.");
  166. for (const chunk of toChunks(vectors, 100)) {
  167. chunks.push(chunk);
  168. await pineconeNamespace.upsert([...chunk]);
  169. }
  170. await storeVectorResult(chunks, fullFilePath);
  171. }
  172. await DocumentVectors.bulkInsert(documentVectors);
  173. return { vectorized: true, error: null };
  174. } catch (e) {
  175. console.error("addDocumentToNamespace", e.message);
  176. return { vectorized: false, error: e.message };
  177. }
  178. },
  179. deleteDocumentFromNamespace: async function (namespace, docId) {
  180. const { DocumentVectors } = require("../../../models/vectors");
  181. const { pineconeIndex } = await this.connect();
  182. if (!(await this.namespaceExists(pineconeIndex, namespace))) return;
  183. const knownDocuments = await DocumentVectors.where({ docId });
  184. if (knownDocuments.length === 0) return;
  185. const vectorIds = knownDocuments.map((doc) => doc.vectorId);
  186. const pineconeNamespace = pineconeIndex.namespace(namespace);
  187. for (const batchOfVectorIds of toChunks(vectorIds, 1000)) {
  188. await pineconeNamespace.deleteMany(batchOfVectorIds);
  189. }
  190. const indexes = knownDocuments.map((doc) => doc.id);
  191. await DocumentVectors.deleteIds(indexes);
  192. return true;
  193. },
  194. "namespace-stats": async function (reqBody = {}) {
  195. const { namespace = null } = reqBody;
  196. if (!namespace) throw new Error("namespace required");
  197. const { pineconeIndex } = await this.connect();
  198. if (!(await this.namespaceExists(pineconeIndex, namespace)))
  199. throw new Error("Namespace by that name does not exist.");
  200. const stats = await this.namespace(pineconeIndex, namespace);
  201. return stats
  202. ? stats
  203. : { message: "No stats were able to be fetched from DB" };
  204. },
  205. "delete-namespace": async function (reqBody = {}) {
  206. const { namespace = null } = reqBody;
  207. const { pineconeIndex } = await this.connect();
  208. if (!(await this.namespaceExists(pineconeIndex, namespace)))
  209. throw new Error("Namespace by that name does not exist.");
  210. const details = await this.namespace(pineconeIndex, namespace);
  211. await this.deleteVectorsInNamespace(pineconeIndex, namespace);
  212. return {
  213. message: `Namespace ${namespace} was deleted along with ${details.vectorCount} vectors.`,
  214. };
  215. },
  216. performSimilaritySearch: async function ({
  217. namespace = null,
  218. input = "",
  219. LLMConnector = null,
  220. similarityThreshold = 0.25,
  221. topN = 4,
  222. filterIdentifiers = [],
  223. }) {
  224. if (!namespace || !input || !LLMConnector)
  225. throw new Error("Invalid request to performSimilaritySearch.");
  226. const { pineconeIndex } = await this.connect();
  227. if (!(await this.namespaceExists(pineconeIndex, namespace)))
  228. throw new Error(
  229. "Invalid namespace - has it been collected and populated yet?"
  230. );
  231. const queryVector = await LLMConnector.embedTextInput(input);
  232. const { contextTexts, sourceDocuments } = await this.similarityResponse({
  233. client: pineconeIndex,
  234. namespace,
  235. queryVector,
  236. similarityThreshold,
  237. topN,
  238. filterIdentifiers,
  239. });
  240. const sources = sourceDocuments.map((metadata, i) => {
  241. return { ...metadata, text: contextTexts[i] };
  242. });
  243. return {
  244. contextTexts,
  245. sources: this.curateSources(sources),
  246. message: false,
  247. };
  248. },
  249. curateSources: function (sources = []) {
  250. const documents = [];
  251. for (const source of sources) {
  252. const { metadata = {} } = source;
  253. if (Object.keys(metadata).length > 0) {
  254. documents.push({
  255. ...metadata,
  256. ...(source.hasOwnProperty("pageContent")
  257. ? { text: source.pageContent }
  258. : {}),
  259. });
  260. }
  261. }
  262. return documents;
  263. },
  264. };
  265. module.exports.Pinecone = PineconeDB;