# 🧪 Yandex Search Scraper — SERP With Adverts Flagged (`thenetaji/yandex-search-scraper`) Actor

Export Yandex web results as rows: title, destination address, breadcrumb, snippet and nested sitelinks, with rank counted across pages. Paid placements are flagged on every row rather than hidden — Yandex renders them in the same markup as organic results and labels neither.

- **URL**: https://apify.com/thenetaji/yandex-search-scraper.md
- **Developed by:** [The Netaji](https://apify.com/thenetaji) (community)
- **Categories:** SEO tools, Marketing, Automation
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $2.13 / 1,000 search results

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.
Since this Actor supports Apify Store discounts, the price gets lower the higher subscription plan you have.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Yandex Search Scraper

Yandex web results as a spreadsheet. Each row carries its rank counted across pages, the
title, the destination address, the breadcrumb Yandex shows under it, the snippet, and the
sitelinks nested beneath it — plus the count of organic and paid rows on the page it came
from, and Yandex's own phrasing of how many results the query has.

And one flag that is the reason to take this from a dataset rather than from a screenshot.

**Yandex does not label its adverts.** A paid placement is an ordinary result row with the
same markup and no class of its own, sitting among the organic ones. The same query answered
from a US route put five of them above the first organic result. Every row here carries
`is_advert`, and every row also carries `organic_count` beside `count`, so the split never has
to be inferred.

### Accepted input

`queries` is required and takes one search term per line. A term is never split on a comma, so
a phrase containing one stays a single search. Each term is walked separately and every row
names the term it answered, so a run over twenty terms splits back apart cleanly.

`regionId` is optional and takes Yandex's own region number — `213` is Moscow, `84` is the
USA. It changes which results come back and how many adverts sit above them. It does **not**
translate them; it is a market selector, not a language one.

`startPage` defaults to `0` and accepts `0` to `24`. It is where each query's walk begins, not
the page it fetches — the run continues forward from there. Page 25 serves a shell rather than
results, which is why the ceiling is what it is.

`maxItems` defaults to `100` and caps **each query separately**, so twenty terms with a cap of
100 returns up to 2,000 rows rather than 100 shared between them. `0` means no limit, which
here means 250 organic results plus whatever adverts sit among them.

```json
{
  "queries": ["stripe payments", "payment gateway"],
  "regionId": 213,
  "startPage": 0,
  "maxItems": 100
}
```

### Response fields

```json
{
  "query": "stripe payments",
  "region_id": 213,
  "page": 0,
  "page_exists": true,
  "count": 14,
  "organic_count": 10,
  "advert_count": 4,
  "results_found_text": "stripe payments — Яндекс: нашлось 28 тыс. результатов",
  "position": 5,
  "is_advert": false,
  "title": "Stripe | Financial Infrastructure to Grow Your Revenue",
  "url": "https://stripe.com/",
  "displayed_path": "stripe.com › payments",
  "snippet": "Millions of companies use Stripe to accept payments online…",
  "sitelinks": [{ "title": "Pricing", "url": "https://stripe.com/pricing" }]
}
```

Fields absent from a result are returned as null rather than omitted, so every row has the
same shape.

`is_advert` is the column to read first. On an advert `url` is null, because Yandex does not
state a paid result's destination — its link goes through a click counter instead.

`url` on an organic result is the destination Yandex names, not the counter redirect the link
on the page points at. That means a row is usable as a rank-tracking record without following
anything.

`position` is the rank across pages **with adverts included**, so page 1's first row is
position 11. That is the position as the page renders it, which is what a rank-tracking caller
means by rank.

`results_found_text` is Yandex's own approximate phrase, published **verbatim and unparsed**.
It moves: three fetches of one query said 26, 28 and 29 thousand. A parsed integer would look
authoritative and be none of those things.

`page_exists` false means the query has fewer pages than the run asked for. It is a real
answer rather than a failure — Yandex serves a shell past the end of a result set — and the
walk stops there.

### Behaviour on partial results

**One fetch is a sample of a page, not the page.** Fetched twice from one pinned route, the
first page shared 7, 9 and 9 of its ten destinations across three pairs, and never came back
in the same order. So this Actor does not promise a stable ordered set and you should not
build one on it: treat a run as a sample of the results, and take a repeated run as a second
sample rather than a correction of the first.

Paging is real and clears that noise easily — page 0 against page 1 shared 0, 1 and 1 of 10
over the same pairs — which is why a walk is worth making at all.

A destination already collected in the same walk is skipped rather than saved twice, and the
run log says how many it skipped. Repeats across pages are rare here, so that is a safety net
rather than the main event.

A query that answers with nothing is skipped with a line in the log and the rest of the list
still runs.

**Deeper pages take more work to obtain and are the one place this source gives up.** Measured
per attempt over two runs of ten per page, the first page was served twenty times out of
twenty, while pages 1, 3, 10 and 24 all landed between 30% and 55%. Read that as one step
rather than a curve — ten samples is nowhere near enough to rank the deep pages against each
other, and the two runs disagreed by 3 of 10 on page 1 alone. A deep-page run is slower and
more likely to end early than a first-page run.

### Frequently asked questions

**Why does the same query return different results each time?**
Because Yandex resamples its own first page. Two fetches from one pinned route shared 7 to 9
of ten destinations and never repeated their order. This is a property of the search engine
rather than of the Actor, and pretending otherwise — by promising a stable ranked set — would
be the wrong thing to do with it. Take a run as a sample. If you need a rank you can defend,
take several runs and use the median position for a destination.

**How do I tell an advert from an organic result?**
`is_advert`. There is nothing else to go on: Yandex renders both in the same markup with the
same classes. The discriminator used is that an organic result names its own destination and
an advert does not, sending the click through a counter instead — which is also why `url` is
null on an advert.

**Why does `position` start at 11 on the second page?**
Because it is the rank as rendered, across pages and adverts included. Page 0 holds positions
1 to about 14 depending on how many adverts sat in it; page 1 continues from there. A
per-page position would make two rows from different pages look equally ranked.

**Can I get more than 250 results for one query?**
No. Yandex serves 25 pages of 10 organic results and page 25 is a shell. Narrowing the query
reaches different results; going deeper does not exist.

**Does `regionId` translate the results?**
No. It changes which results Yandex serves and how many adverts sit above them — a market
selector. The results come back in whatever language the pages are written in.

**Is a Yandex account or API key required?**
No. No account, session cookie or key of any kind is supplied to the Actor or needed by it.

### Related Actors

[Yandex Maps Scraper](https://apify.com/thenetaji/yandex-maps-scraper) is the other half of
this site and a completely different dataset: organisations with ratings, phone numbers,
websites, opening hours and coordinates, searched by area rather than by keyword. Unlike the
web results, the map surface is stable — one page fetched twice returned all 25 rows in the
same order.

[Yandex Maps Place Scraper](https://apify.com/thenetaji/yandex-maps-place-scraper) reads one
organisation in full, and [Yandex Maps Reviews
Scraper](https://apify.com/thenetaji/yandex-maps-reviews-scraper) exports its reviews.

# Actor input Schema

## `queries` (type: `array`):

What to search for, one per line. A term is never split on a comma, so a phrase with one in it stays a single search. Each term is run separately and every row names the term it answered.

## `regionId` (type: `integer`):

Yandex's own region number — 213 is Moscow, 84 is the USA. It changes which results come back and how many adverts sit above them; it does not translate them. Leave it empty for Yandex's default.

## `startPage` (type: `integer`):

Which page to begin each query at, counted from 0. Pages run 0 to 24 at 10 organic results each; page 25 serves a shell rather than results. Deeper pages take more work to obtain and are likelier to fail.

## `maxItems` (type: `integer`):

Maximum rows to save for each query in the list. Set 0 for no limit, which on this surface means 250 organic results plus whatever adverts sit among them.

## Actor input object example

```json
{
  "queries": [
    "coffee"
  ],
  "regionId": 213,
  "startPage": 0,
  "maxItems": 20
}
```

# Actor output Schema

## `dataset` (type: `string`):

All records scraped by this run

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "queries": [
        "coffee"
    ],
    "startPage": 0,
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("thenetaji/yandex-search-scraper").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "queries": ["coffee"],
    "startPage": 0,
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("thenetaji/yandex-search-scraper").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "queries": [
    "coffee"
  ],
  "startPage": 0,
  "maxItems": 20
}' |
apify call thenetaji/yandex-search-scraper --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,thenetaji/yandex-search-scraper"
        }
    }
}

```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/GBMHa8CgQPje1nkJ5/builds/Po8fLfuPEdtcAgVLl/openapi.json
