# How to turn Reddit upvotes into AI training data

> Source: <https://blog.apify.com/reddit-ai-training-data/>
> Published: 2026-07-13 14:06:59+00:00

Reddit threads have something most AI training datasets lack: **a built-in human approval score (upvotes and downvotes) from niche communities**. Instead of scraping random web chatter, you're getting feedback from subreddits filled with people who actively care about the topic.

A comment with 4,000 upvotes versus one with two gives you a strong signal on what resonates most with particular audiences, and Reddit attaches that number to every opinion shared.

While Reddit is arguably the internet’s largest archive of candid human opinions, extracting its data has never been more challenging. [Reddit recently shut down unauthenticated .json endpoints in May 2026](https://www.reddit.com/r/modnews/comments/1tq9vxo/protecting_communities_from_scrapers_and_platform/), severing the easiest routes for bulk data extraction.

This guide solves that problem using Apify. You'll learn how to extract posts and comments with upvote counts intact, categorize reaction types using OpenAI, and store the enriched data with embeddings in Supabase for fine-tuning or retrieval.

## Extract Reddit posts, comments, and vote counts with Apify

[Reddit Scraper](https://apify.com/labrat011/reddit-scraper) is a ready-made Apify Actor that reads Reddit's public pages directly, so it doesn't depend on the endpoints Reddit retired. Every run returns:

- Post titles, body text, subreddit, and flair
- Score and upvote ratio on every post
- Full comment trees, each comment carrying its own score
- Authors, permalinks, and timestamps

With Reddit Scraper, you can:

- Pull posts from any subreddit, sorted by hot, new, top, or rising
- Search across all of Reddit or inside a single community
- Pull the full comment tree for a specific post
- Run it from Apify Console, on a schedule, or from n8n, which is what you'll do here

Pricing starts at $1.20 per 1,000 results.

[Get web data](https://console.apify.com/sign-up)

## Workflow summary

Reddit Scraper is scheduled to extract posts and comments from Reddit, along with their respective vote counts. From there, the pipeline splits into two distinct workflows: the post branch stores and embeds the original post as the primary context, while the comment branch classifies each opinion, logs a unique signal score, and generates its embedding, before joining it back to the parent post via its ID. The stored posts and comments serve as the core training dataset and can be easily exported in JSONL or Parquet formats for LLM fine-tuning or training.

## Prerequisites

[Apify account](https://console.apify.com/sign-up).- Self-hosted n8n instance via Docker (
**skip if you prefer**).** n8n’s cloud tier** [OpenAI account](https://platform.openai.com/signup/)with an API key (used to generate embeddings and for data classification).[Supabase account](https://supabase.com/).

## Phase 1: Accounts setup

### Step #1: Grab your Apify API token

- Sign up at
[apify.com](https://apify.com/)if you don’t have an account. - Open
[Apify Console](https://console.apify.com/). - In the left sidebar, click
**Settings**, then open the** API & Integrations**tab. - Under "Personal API tokens," copy your default token, or click
**Add new token** to create one and name it. - Paste it somewhere safe for a moment.

This token lets n8n run the scrapers on your behalf. Keep it private, and revoke or regenerate it from this same screen if it ever leaks.

### Step #2: Get your OpenAI API key

- Go to
[platform.openai.com](https://platform.openai.com/)and sign in or create an account. - Open the
**API keys** section, click**Create new secret key**, name it, and copy the key as soon as it appears. - Paste it somewhere safe.
- Open
**Billing** under**Settings**, add a payment method, and buy a small amount of credit; $5 is plenty for testing.

You’ll use this key in two separate n8n nodes: one to label each comment, the other to generate embeddings. Copy it and paste it somewhere safe.

### Step #3: Create the Supabase project and keys

- Sign in at
[supabase.com](https://app.notion.com/p/Supabase-use-case-research-383f39950a2280c383d6d8300706d8b6?pvs=21)and click**New project**. - Create an organization, name the project, input a database password, choose the region nearest you, then click
**Create new project**. - In the left sidebar, click the gear to open
**Project Settings**, then click Data** API**. - On the
**Data API** page, copy the Project URL. - Under
**API keys**, find the** service_role**key, click** Reveal**, and copy it.

The service_role key can write to your tables, which is what the pipeline needs. Keep it server-side inside n8n and never put it in client code, since it bypasses row-level security.

### Step #4: Create the database tables

- In the left sidebar, click the
**SQL Editor** icon, then click**New query**. - Paste the first block below into the editor, then click
**Run**. You should see "Success. No rows returned."

```
create extension if not exists vector;

create table documents (
  id bigserial primary key,
  content text,
  metadata jsonb,
  embedding vector(1536)
);

create function match_documents (
  query_embedding vector(1536),
  match_count int default null,
  filter jsonb default '{}'
) returns table (
  id bigint,
  content text,
  metadata jsonb,
  similarity float
)
language plpgsql
as $$
begin
  return query
  select
    documents.id,
    documents.content,
    documents.metadata,
    1 - (documents.embedding <=> query_embedding) as similarity
  from documents
  where documents.metadata @> filter
  order by documents.embedding <=> query_embedding
  limit match_count;
end;
$$;
```

- Open a new query tab, paste this second block, and click
**Run** again to create the relational tables.

```
create table posts (
  post_id text primary key,
  subreddit text,
  title text,
  self_text text,
  score bigint,
  upvote_ratio numeric,
  num_comments int,
  link_flair text,
  domain text,
  author text,
  url text,
  permalink text,
  context_text text,
  created_at timestamptz,
  scraped_at timestamptz,
  source_sort text
);

create table comments (
  comment_id text primary key,
  post_id text,
  subreddit text,
  author text,
  text text,
  score bigint,
  reaction_type text,
  permalink text,
  created_at timestamptz,
  scraped_at timestamptz,
  source_sort text
);

create index on comments (post_id);
```

- Confirm the tables exist by opening the
**Table Editor** in the left sidebar; you should see`documents`

,`posts`

, and`comments`

listed.

The **posts table** stores the context, while the **comments table** captures the reactions by logging the type of opinion (`reaction_type`

) and using the vote score to measure how deeply it resonated. The **documents table** handles the embeddings for AI retrieval. Because everything is linked by a shared `post_id`

, you can easily take any post and instantly pull up every community reaction it sparked.

## Phase 2: Install and set up n8n

Self-hosting n8n lets you build and run unlimited workflows for free, with full control over your data privacy. But, if you don’t want to manage your own infrastructure, the [n8n cloud tier](https://n8n.io/pricing/) works fine.

### Step #1: Install Docker Desktop

[Download and install Docker Desktop for your OS](https://medium.com/@piyushkashyap045/comprehensive-guide-installing-docker-and-docker-compose-on-windows-linux-and-macos-a022cf82ac0b).- Confirm it's running by opening your terminal and running:

```
docker --version
```

- You should see a version number. If you see "command not found," Docker isn't in your PATH yet, so restart your terminal or reboot.
- Afterward, confirm the engine is running by typing:

```
docker ps
```

### Step #2: Run n8n in Docker

- Open your terminal.
- Run the command below to pull and start n8n:

```
docker run -d --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n n8nio/n8n
```

- The first time you run this command, Docker will pause to pull the official n8n image from the internet. After downloading, it will assign and print your Container ID.
- Open a browser and go to
`http://localhost:5678`

. - You'll see n8n's initial setup screen. Create an owner account with your email and a password.

### Step #3: Install the Apify community node

n8n doesn't feature the Apify node natively. You’ll need to install it as an official community node before you can use it. Here’s how to do that:

- In your n8n workspace, click Settings at the bottom of the left sidebar, then navigate to
**Community Nodes and click “Install”.** - In the npm package field, paste exactly this:
`@apify/n8n-nodes-apify`

- Check the risk acknowledgment box and click
**Install**.

## Phase 3: Build the pipeline

The Actor feeds two branches, both of which read its output: a post branch that writes context (post content and metadata), and a comment branch that writes opinions and their scores. Build the post branch first, then come back to the scraper and start the comment branch from the same output.

### Step #1: Schedule Trigger node

- Create a new workflow by clicking
**Add workflow** in the top left. - On the empty canvas, click the large
**+** in the center or the "**Add first step" option**. - In the search panel, type
`Schedule Trigger`

and select it to place it on the canvas. - Set
**Trigger Interval** to**Weeks** - Set
**Weeks Between Triggers** to`1`

and**Trigger on Weekdays** to Monday.

### Step #2: Define dataset inputs

- On the Schedule Trigger node, click the
**+** on its right-hand output dot. - In the search panel, type
`Edit Fields`

and select the**Edit Fields (Set)** node. - Open the node and click its title to rename it to
`Define Dataset Inputs`

. - Leave
**Mode** on "Manual Mapping". - Click
**Add Field** once for each input below. For each one, type the name in the left box, open the**Type** dropdown on the right to select the shown type, and enter the value in the value box.`subreddits`

, type String, value`AskReddit: 40, changemyview: 40, technology: 40, movies: 40`

.`sort`

, type String, value`top`

.`timeFilter`

, type String, value`week`

.`minScore`

, type Number, value`50`

.`maxCommentsPerPost`

, type Number, value`100`

.

Keeping all your inputs in a single node makes it easier to update your settings. For example, the **subreddits** field uses a simple `name:count`

format, allowing you to pull more posts from massive communities and fewer from niche ones. If you omit the count, it defaults to 100.

### Step #3: Build subreddit list

- On the Define dataset inputs node, click the
**+** on its output dot. - Search
`Code`

and select the**Code** node. Rename it to`Build subreddit list`

. - Set
**Mode** to "Run Once for All Items" and**Language** to "JavaScript". - Delete the sample code and paste the code below.

``` js
const plan = $('Define Dataset Inputs').first().json.subreddits || '';

return plan
  .split(',')
  .map((entry) => entry.trim())
  .filter(Boolean)
  .map((entry) => {
    const [name, count] = entry.split(':').map((s) => s.trim());
    return {
      json: {
        subreddit: name,
        maxResults: Number(count) || 100,
      },
    };
  });
```

This converts the single `subreddits`

string into one item per community, each carrying its own `maxResults`

. It matters because, without it, the Actor spends its entire result budget on the first subreddit before moving to the next, so pointing it at four communities at once would exhaust the first and never reach the others. By emitting a separate item per subreddit, you run the scraper once for each subreddit.

### Step #4: Reddit scraper (Apify) node

- On the Build subreddit list node, click the
**+** on its output dot. - Search
`Apify`

, select the**Run an Actor and get dataset** operation, and rename it to`Reddit scraper`

. - Click the
**Credential to open the** dropdown, then choose**+ Create new credential**. - Paste your Apify API token into the
**API token** field, click**Save**, then close. - Confirm
**Resource** is "Actor" and**Operation** is "Run an Actor and Get Dataset". - Under
**Actor**, choose "From list" then type and select`labrat011/reddit-scraper`

- Scroll to the
**Input** field for the Actor, set it to**Expression,** and paste this JSON:

```
{
  "mode": "subreddit_posts",
  "subreddits": [{{ JSON.stringify($json.subreddit) }}],
  "sort": "{{ $('Define Dataset Inputs').first().json.sort }}",
  "timeFilter": "{{ $('Define Dataset Inputs').first().json.timeFilter }}",
  "maxResults": {{ $json.maxResults }},
  "includeComments": true,
  "maxCommentsPerPost": {{ $('Define Dataset Inputs').first().json.maxCommentsPerPost || 100 }},
  "proxyConfiguration": { "useApifyProxy": true, "apifyProxyGroups": ["RESIDENTIAL"] }
}
```

This node runs once per community. It uses `{{ $json.subreddit }}`

and `{{ $json.maxResults }}`

to read the current subreddit and its target post count, while referencing the inputs node for global settings like sort and comment depth.

### Step #5: Code node for post rows

- On the Reddit scraper node, click the
**+** on its output dot. - Search
`Code`

and select the**Code** node. Rename it to`Build post rows`

. - Set
**Mode** to "Run Once for All Items" and**Language** to "JavaScript". - Delete the sample code and paste the code below.

``` js
const items = $input.all();
const inputs = $('Define Dataset Inputs').first().json || {};
const minScore = Number(inputs.minScore) || 0;
const sortTag = inputs.sort || '';

return items
  .filter((item) => item.json.type === 'post' && (Number(item.json.score) || 0) >= minScore)
  .map((item) => {
    const p = item.json;
    const title = p.title || '';
    const selfText = p.selftext || '';
    const subreddit = p.subreddit || '';

    const contextText = [
      `Subreddit: r/${subreddit}`,
      `Title: ${title}`,
      selfText ? `Body: ${selfText}` : '',
    ].filter(Boolean).join('\n\n');

    return {
      json: {
        post_id: p.id,
        subreddit,
        title,
        self_text: selfText,
        score: Number(p.score) || 0,
        upvote_ratio: Number(p.upvoteRatio) || 0,
        num_comments: Number(p.numComments) || 0,
        link_flair: p.flair || '',
        domain: p.domain || '',
        author: p.author || '',
        url: p.url || '',
        permalink: p.url || '',
        context_text: contextText,
        created_at: p.created || null,
        scraped_at: new Date().toISOString(),
        source_sort: sortTag,
      },
    };
  });
```

- Click
**Execute step** once, with the scraper already run, to confirm the output shows post rows.

This step prepares your posts for the database. It filters out everything except `post`

records that meet your `minScore`

, and translates the raw fields into `snake_case`

(like `selftext`

to `self_text`

). This exact matching allows the next node to auto-map the columns easily.

### Step #6: Create a row (Supabase) node for posts

- On the Build post rows node, click the
**+** on its output dot. - Search
`Supabase`

and select the**Create a row** operation. Rename it to`Insert post row`

. - Click
**Credential to connect with**, choose** Create new credential**, paste your Supabase Project URL into the** Host**field and your service_role key into the** Service Role Secret**field, click** Save**, and close the dialog. - Note that the host is the bare project URL,
`https://YOUR-REF.supabase.co`

, with no`/rest/v1/`

path on the end. - Set
**Table** to`posts`

. - Set the
**Mapping Column Mode** to "Map Automatically". - Open the
**Settings** tab, set**On Error** to "Continue", and leave**Retry On Fail** off.

This step writes a single context row for each post. Because your incoming data already carries every necessary column, including the newly created `context_text`

and `source_sort`

, you can rely entirely on auto-map to handle the insert without manual configuration.

### Step #7: Supabase Vector Store node for post context

- On the Insert post row node, click the
**+** on its output dot. - Search
`Supabase Vector Store`

and select it. Rename it to`Embed post context`

. - Set
**Operation Mode** to "Insert Documents". - Under
**Credential**, reuse your Supabase credential. - Set
**Table Name** to`documents`

. - This node has two connection points beneath it:
**Embedding** and**Document**. On the** Embedding**point, click the**+**, add`Embeddings OpenAI`

, set its**Model** to`text-embedding-3-small`

, and pick your OpenAI credential. - On the
**Document** point, click the**+**, add`Default Data Loader`

. Set**Type of Data** to "JSON" and**Mode** to "Load Specific Data". In the data field, switch it to**Expression** and enter`{{ $('Build post rows').all()[$itemIndex].json.context_text }}`

. - Under the loader's
**Metadata**, click** Add Property**three times, switching each value to Expression and input the following accordingly:`source_type`

set to`post`

,`post_id`

set to`{{ $('Build post rows').all()[$itemIndex].json.post_id }}`

, and`subreddit`

set to`{{ $('Build post rows').all()[$itemIndex].json.subreddit }}`

.

This step embeds the post context, storing the content as searchable vectors tagged with `source_type: post`

. Pay close attention to the syntax: you must use `.all()[$itemIndex]`

instead of the usual `.item`

. The previous insert skips duplicates, so fetching by index is required to grab the right row.

### Step #8: Code node for comment rows

- Scroll back to the
**Reddit scraper** node and click the**+** on its output dot a second time. A node output can feed more than one path, so this starts the parallel comment branch. - Search
`Code`

and select the**Code** node. Rename it to`Build comment rows`

. - Set
**Mode** to "Run Once for All Items" and**Language** to "JavaScript". - Delete the sample code and paste this:

``` js
const items = $input.all();

return items
  .filter((item) => item.json.type === 'comment')
  .map((item) => {
    const c = item.json;
    return {
      json: {
        comment_id: c.id,
        post_id: c.postId || null,
        subreddit: c.subreddit || '',
        author: c.author || '',
        text: c.body || '',
        score: Number(c.score) || 0,
        permalink: c.url || '',
        created_at: c.created || null,
        scraped_at: new Date().toISOString(),
      },
    };
  })
  .filter((row) => {
    const t = (row.json.text || '').trim();
    return t !== '' && t !== '[deleted]' && t !== '[removed]';
  });
```

This branch processes only the `comment`

records. Since your Actor automatically attaches a `postId`

to each comment, they link cleanly back to their parent posts without any permalink parsing. The opinion itself is pulled from the `body`

field, while the `score`

acts as your resonance signal.

### Step #9: OpenAI node to classify the reaction

- On the Build comment rows node, click the
**+** on its output dot. - Search
`OpenAI`

and, under the OpenAI node's actions, select**Message a Model**. Rename it to`Classify reaction type`

. - Under
**Credential to connect with**, reuse your OpenAI credential. - Open the
**Model** dropdown and pick`gpt-4.1-mini`

. - Under
**Messages**, click** Add Message**. Set its** Role**to "System" and paste the prompt below into the** Content**box.

```
You are labeling Reddit comments for a machine learning dataset about how people react to and endorse opinions. You will be given a single comment. Classify the nature of the opinion or reaction it expresses into exactly one category from this list: "agreement", "disagreement", "insight", "humor", "personal experience", "critique", "question", "other". Judge only the comment text. Return a single JSON object with two keys: "reaction_type" (one of the categories) and "reasoning" (a short phrase under 10 words). Return only the JSON object.
```

- Click
**Add Message** again. Set its**Role** to "User", switch its**Content** box to**Expression**, and enter`{{ $('Build comment rows').item.json.text }}`

. - Leave
**Simplify Output** on; the prompt already forces JSON. If your version shows a**Response Format** option, set it to "JSON".

This step labels each opinion, while shielding the vote score from the model and feeding it only the text, ensuring that each label evaluates the opinion purely on its own terms.

### Step #10: Assemble the comment row and insert it

Because the classifier replaces the current item with the model's output, your original comment fields are temporarily lost, and the new reaction label is buried in the response. This node fixes that by rebuilding a new comment row by reaching back to the 'Build comment rows' step to retrieve the original text and scores, extracts the label from the classifier, and appends your global sort preference.

- On the Classify reaction type node, click the
**+** on its output dot. - Search
`Code`

and select the**Code** node. Rename it to`Build comment insert row`

. - Set
**Mode** to "Run Once for All Items" and**Language** to "JavaScript". - Delete the sample code and paste the code below.

``` js
const comments = $('Build comment rows').all();
const sortInputs = $('Define Dataset Inputs').first().json || {};
const sort = sortInputs.sort || '';

// Dig the reaction label out of whatever shape the classifier returned, then parse it.
const readReaction = (j) => {
  const text =
    j?.output?.[0]?.content?.[0]?.text ??
    j?.content?.[0]?.text ??
    j?.content ??
    j?.text ??
    (typeof j?.message?.content === 'string' ? j.message.content : null);

  if (!text || typeof text !== 'string') return null;

  try {
    return JSON.parse(text).reaction_type ?? null;
  } catch {
    const m = text.match(/"reaction_type"\s*:\s*"([^"]+)"/);
    return m ? m[1] : null;
  }
};

return $input.all().map((item, i) => {
  const c = comments[i].json;
  return {
    json: {
      comment_id: c.comment_id,
      post_id: c.post_id,
      subreddit: c.subreddit,
      author: c.author,
      text: c.text,
      score: c.score,
      reaction_type: readReaction(item.json),
      permalink: c.permalink,
      created_at: c.created_at,
      scraped_at: c.scraped_at,
      source_sort: sort,
    },
  };
});
```

- On the Build comment insert row node, click the
**+** on its output dot. - Search
`Supabase`

and select the**Create a row** operation. Rename it to`Insert comment row`

. - Under
**Credential to connect with**, reuse your Supabase credential. - Set
**Table** to`comments`

. - Set the
**Mapping Column Mode** to "Map Automatically". - Open the
**Settings** tab, set**On Error** to "Continue", and leave**Retry On Fail** off, so a comment already stored gets skipped on its duplicate primary key instead of failing the run.

### Step #11: Supabase Vector Store node for comments

- On the Insert comment row node, click the
**+** on its output dot. - Search
`Supabase Vector Store`

and select it. Rename it to`Embed comment`

. - Set
**Operation Mode** to "Insert Documents". - Under
**Credential**, reuse your Supabase credential. - Set
**Table Name** to`documents`

. - On the
**Embedding** point, add`Embeddings OpenAI`

with model`text-embedding-3-small`

and your OpenAI credential. - On the
**Document** point, add`Default Data Loader`

. Set**Type of Data** to "JSON" and**Mode** to "Load Specific Data", switch the data field to Expression, and enter`{{ $('Build comment rows').all()[$itemIndex].json.text }}`

. - Under
**Metadata**, add four properties, switching to Expression where needed:`source_type`

set to`comment`

,`comment_id`

set to`{{ $('Build comment rows').all()[$itemIndex].json.comment_id }}`

,`post_id`

set to`{{ $('Build comment rows').all()[$itemIndex].json.post_id }}`

, and`score`

set to`{{ $('Build comment rows').all()[$itemIndex].json.score }}`

.

This node embeds each opinion to make the comments searchable, saving the `score`

directly into the metadata so you can easily filter and rank by resonance. Keep an eye on the syntax here, just like the post embedder, you must use `.all()[$itemIndex]`

instead of `.item`

.

## Phase 4: Test and publish

### Step #1: Execute the workflow

- Click
**Execute workflow** at the bottom center of the canvas to run the whole pipeline once. - Watch the nodes turn green in sequence.

### Step #2: Check the tables in Supabase

- In Supabase, open the
**Table Editor** in the left sidebar. - Open
`posts`

. You should see one context row per post, each with a real`score`

. - Open
`comments`

. You should see the opinions, each with its`score`

, a`reaction_type`

label, and a`post_id`

that matches a row in`posts`

. - Open
`documents`

. You should see the post and comment rows, distinguishable by`source_type`

in the metadata.

### Step #3: Confirm the relationship with a join query

- Open the
**SQL Editor**, click** New query**, paste the query below, and click** Run**.

```
select p.title, c.text, c.score, c.reaction_type
from comments c
join posts p on p.post_id = c.post_id
order by c.score desc
limit 20;
```

This script shows the relationship the dataset captures: each row pairs a post's content with one opinion about it, that opinion's reaction type, and how strongly the crowd endorsed it.

## The workflow results

Understanding your database schema is critical for utilizing this dataset effectively. Here is a complete breakdown of what each column represents across the three tables:

**1. **`posts`

`posts`

Stores the original context of every scraped post:

| Column | Description |
|---|---|
`post_id` |
The post's unique ID, which serves as the master key that other tables link back to. |
`subreddit` |
The community where the post originated. |
`title` |
The post's headline. |
`self_text` |
The post's body text (this will often be empty for link or title-only posts). |
`score` |
The post's net upvotes. |
`upvote_ratio` |
The share of total votes that were upvotes, expressed as a decimal from 0 to 1. |
`num_comments` |
The total number of replies the post received. |
`link_flair` |
The post's category tag, if applicable. |
`domain` |
The source of the link (e.g., `self.AskReddit` for text posts). |
`author` |
The original poster's username. |
`url` |
The direct link to the post's content. |
`permalink` |
The relative Reddit URL for the thread. |
`context_text` |
A single text block bundling the subreddit, title, and body. This is the exact string that gets vectorized for embeddings. |
`created_at` |
The timestamp of when the post was originally published. |
`scraped_at` |
The timestamp of when you captured the data. |
`source_sort` |
The specific filter used during extraction (e.g., top, new). |

**2. **`comments`

`comments`

Captures human reactions and tabulates their resonance strength relative to the post's context:

| Column | Description |
|---|---|
`comment_id` |
The comment's unique ID and primary key. |
`post_id` |
The foreign key linking this opinion back to its parent post in the `posts` table. |
`subreddit` |
The community where the comment was made. |
`author` |
The commenter's username. |
`text` |
The actual words of the voiced opinion. |
`score` |
The comment's net upvotes. This is your core resonance signal. |
`reaction_type` |
The AI-assigned label categorizing the nature of the opinion (e.g., agreement, disagreement, humor). |
`permalink` |
The direct Reddit link to the comment. |
`created_at` |
The timestamp of when the comment was originally published. |
`scraped_at` |
The timestamp of when you captured the data. |
`source_sort` |
The specific filter used during extraction. |

**3. **`documents`

`documents`

Functions as a standard vector-store table with a fixed schema:

| Column | Description |
|---|---|
`id` |
An auto-incrementing row number. |
`content` |
The raw text that was embedded (either a post's `context_text` or a comment's `text` ). |
`metadata` |
JSON storing your tags. For posts, this includes the `source_type` , `post_id` , and `subreddit` . For comments, it includes the `source_type` , `comment_id` , `post_id` , and `score` . |
`embedding` |
The numerical vector representation of the content used for semantic search. |

The columns in your `posts`

and `comments`

tables directly mirror the outputs from your Build post rows and Build comment rows Code nodes in n8n. If you ever add or rename a field within those nodes, you must manually update your database tables to reflect the changes. The `documents`

table, by contrast, is strictly controlled by the vector store node and should never be altered.

## Summary

You now have a dataset where every opinion carries a real number for how hard a crowd endorsed it. Posts, comments, and scores are linked through `post_id`

, ready for classifiers, retrieval, or resonance modeling.

Accessing this level of context would be nearly impossible without t[he Reddit Actor used in this build](https://apify.com/labrat011/reddit-scraper/api).

[Apify Store](https://apify.com/store) offers one of the largest catalogs of scraping and automation tools on the web, with 50,000+ ready-made Actors designed to extract data from virtually any site with proxies, anti-bot handling, and infrastructure maintained for you.

Start with a free Apify account and $5 in platform credits every month.

**FAQ**

### Can I see how many downvotes a comment got?

No. Reddit only exposes the net score (upvotes minus downvotes), so a comment sitting at 12 could have been 12 up and 0 down, or 40 up and 28 down. The split isn't published; this is a Reddit limitation, not a scraper limitation.

### How do I estimate downvotes?

You can, but only on posts, not comments. Posts carry an `upvote_ratio`

alongside the score, so you can work backward: `Total Votes = Net Score / (2 × Upvote% − 1)`

. A post with a score of 9 and an 80% upvote rate received 15 total votes: 12 up and 3 down. Comments have no published ratio, so the math doesn't work there.

### Are the scores exact vote counts?

No. Reddit uses "vote fuzzing" to deliberately alter counts slightly on every refresh to frustrate bots and vote manipulators. Your scores are close approximations, reliable for ranking opinions against each other, but not exact tallies. That's fine for this dataset, since what matters is which opinions outscored which, not the precise numbers.

### Why are almost all my comment scores positive?

Because you're pulling top comments by default, you're seeing the well-received end of the distribution. Raise `maxCommentsPerPost`

to dig deeper into each thread and reach downvoted opinions. Even then, Reddit collapses and fuzzes heavily buried comments, so the negative tail is always partial.

### Why is `self_text`

empty on so many posts?

They're link or title-only posts, which is normal on subreddits like AskReddit, where the question is the entire post. There's no body to capture, and nothing is lost, since the title alone is the full prompt the comments are responding to.vv
