Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
PINECONE_API_KEY=
PINECONE_ENVIRONMENT=
PINECONE_INDEX=
PINECONE_INDEX=
PINECONE_CLOUD="aws"
PINECONE_REGION="us-west-2"
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,5 @@ dist
.turbo
data/
all-the-news-2-1.zip
all-the-news-2-1.csv
all-the-news-2-1.csv
.DS_Store
102 changes: 62 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,24 +10,27 @@ npm install

## Required configuration

In order to run this example, you have to supply the Pinecone credentials needed to interact with the Pinecone API. You can find these credentials in the [Pinecone web console](https://app.pinecone.io) under **API Keys**. This project uses `dotenv` to easily load values from the `.env` file into the environment when executing.
In order to run this example, you have to supply the Pinecone credentials needed to interact with the Pinecone API. You can find these credentials in the [Pinecone web console](https://app.pinecone.io) under **API Keys**. This project uses `dotenv` to easily load values from the `.env` file into the environment when executing.

Copy the template file:

```sh
cp .env.example .env
```

And fill in your API key and environment details:
And fill in your API key and index name:

```sh
PINECONE_API_KEY=<your-api-key>
PINECONE_ENVIRONMENT=<your-environment>
PINECONE_INDEX=article-recommendations
PINECONE_INDEX="article-recommendations"
PINECONE_CLOUD="aws"
PINECONE_REGION="us-west-2"
```

`PINECONE_INDEX` is the name of the index where this demo will store and query embeddings. You can change `PINECONE_INDEX` to any name you like, but make sure the name not going to collide with any indexes you are already using.

`PINECONE_CLOUD` and `PINECONE_REGION` define where the index should be deployed. Currently, this is the only available cloud and region combination (`aws` and `us-west-2`), so it's recommended to leave them defaulted.

## Data preparation

Next, we will prepare data for the Pinecone vector index, and insert it in batches.
Expand Down Expand Up @@ -97,27 +100,35 @@ Next we'll create a function that will generate the embeddings and upsert them i

```typescript
async function embedAndUpsert(dataFrame: dfd.DataFrame, chunkSize: number) {
const chunkGenerator = processInChunks<ArticleRecord, 'section' | 'url' | 'title' | 'publication' | 'author' | 'article', 'article'>(
const chunkGenerator = processInChunks<
ArticleRecord,
"section" | "url" | "title" | "publication" | "author" | "article",
"article"
>(
dataFrame,
100,
['section', 'url', 'title', 'publication', 'author', 'article'],
'article'
["section", "url", "title", "publication", "author", "article"],
"article"
);
const index = pinecone.index(indexName);

for await (const documents of chunkGenerator) {
await embedder.embedBatch(documents, chunkSize, async (embeddings: PineconeRecord[]) => {
await chunkedUpsert(index, embeddings, "default");
progressBar.increment(embeddings.length);
});
await embedder.embedBatch(
documents,
chunkSize,
async (embeddings: PineconeRecord[]) => {
await chunkedUpsert(index, embeddings, "default");
progressBar.increment(embeddings.length);
}
);
}
}
```

We'll use the `splitFile` utility function to split the CSV file we downloaded into chunks of 100k parts each. For the purposes of this example, we'll only use the first 100k records.

```typescript
const fileParts = await splitFile("./data/all-the-news-2-1.csv", 1000000);
const fileParts = await splitFile("./data/all-the-news-2-1.csv", 100000);
const firstFile = fileParts[0];
```

Expand All @@ -133,8 +144,13 @@ Now we'll create the Pinecone index and kick off the embedding and upserting pro
```typescript
// Create the index if it doesn't already exist
const indexList = await pinecone.listIndexes();
if (indexList.indexOf({ name: indexName }) === -1) {
await pinecone.createIndex({ name: indexName, dimension: 384, waitUntilReady: true })
if (!indexList.indexes?.some((index) => index.name === indexName)) {
await pinecone.createIndex({
name: indexName,
dimension: 384,
spec: { serverless: { region: indexRegion, cloud: indexCloud } },
waitUntilReady: true,
});
}

progressBar.start(clean.shape[0], 0);
Expand All @@ -155,45 +171,52 @@ const pinecone = new Pinecone();
try {
const description = await pinecone.describeIndex(indexName);
if (!description.status?.ready) {
throw `Index not ready, description was ${JSON.stringify(description)}`
throw new Error(
`Index not ready, description was ${JSON.stringify(description)}`
);
}
} catch (e) {
console.log('An error occurred. Run "npm run index" to load data into the index before querying.')
console.log(
'An error occurred. Run "npm run index" to load data into the index before querying.'
);
throw e;
}

const index = pinecone.index<ArticleRecord>(indexName).namespace('default');
const index = pinecone.index<ArticleRecord>(indexName).namespace("default");

await embedder.init("Xenova/all-MiniLM-L6-v2");

const { query, section } = getQueryingCommandLineArguments();

// We create a simulated user with an interest given a query and a specific section
const queryEmbedding = await embedder.embed(query)
const queryEmbedding = await embedder.embed(query);
const queryResult = await index.query({
vector: queryEmbedding.values,
includeMetadata: true,
includeValues: true,
filter: {
section: { "$eq": section }
},
topK: 10
vector: queryEmbedding.values,
includeMetadata: true,
includeValues: true,
filter: {
section: { $eq: section },
},
topK: 10,
});
```

We'll calculate the **mean** vector given the results of the query. The mean vector represents the user's interests based on the articles they've read.

```typescript
// We extract the vectors of the results
const userVectors = queryResult?.matches?.map((result: ScoredPineconeRecord<ArticleRecord>) => result.values);
const userVectors = queryResult?.matches?.map(
(result: ScoredPineconeRecord<ArticleRecord>) => result.values
);

// A couple of functions to calculate mean vector
const mean = (arr: number[]): number => arr.reduce((a, b) => a + b, 0) / arr.length;
const mean = (arr: number[]): number =>
arr.reduce((a, b) => a + b, 0) / arr.length;
const meanVector = (vectors: number[][]): number[] => {
const { length } = vectors[0];

return Array.from({ length }).map((_, i) =>
mean(vectors.map(vec => vec[i]))
mean(vectors.map((vec) => vec[i]))
);
};

Expand All @@ -205,14 +228,11 @@ const meanVec = meanVector(userVectors!);
To resolve the recommendations, we'll query the index with the mean vector and filter out the articles that the user has already read.

```typescript
const recommendations = await pineconeIndex.query({
queryRequest: {
vector: meanVec,
includeMetadata: true,
includeValues: true,
namespace: "default",
topK: 10,
},
const recommendations = await index.query({
vector: meanVec,
includeMetadata: true,
includeValues: true,
topK: 10,
});
```

Expand All @@ -236,10 +256,11 @@ const userRecommendations = new Table({
});

queryResult?.matches?.slice(0, 10).forEach((result: any) => {
const { title, author, section } = result.metadata;
const { title, article, publication, section } = result.metadata;
userPreferences.addRow({
title,
author,
article: `${article.slice(0, 70)}...`,
publication,
section,
});
});
Expand All @@ -248,10 +269,11 @@ console.log("========== User Preferences ==========");
userPreferences.printTable();

recommendations?.matches?.slice(0, 10).forEach((result: any) => {
const { title, author, section } = result.metadata;
const { title, article, publication, section } = result.metadata;
userRecommendations.addRow({
title,
author,
article: `${article.slice(0, 70)}...`,
publication,
section,
});
});
Expand Down
85 changes: 56 additions & 29 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
"author": "",
"license": "MIT",
"dependencies": {
"@pinecone-database/pinecone": "^1.0.0",
"@pinecone-database/pinecone": "^2.0.0",
"@xenova/transformers": "^2.2.0",
"chalk": "^5.2.0",
"cli-progress": "^3.12.0",
Expand Down
Loading