Skip to content
Closed
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
# Changelog
* [Unreleased] Add Markdown search and archive output support
* [2026-02-23] 1.0.3 Enhance error object #16
* [2025-11-17] 1.0.2 Implement `inspect` functions for client #13
* [2025-07-18] 1.0.1 Add support for old Ruby versions (2.7, 3.0)
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBa
Query a vast range of data at scale, including web search results, flight schedules, stock market data, news headlines, and [more](https://serpapi.com).

## Features
* `markdown` → Return token-efficient Markdown output optimized for LLMs and AI agents
* `persistent` → Keep socket connection open to save on SSL handshake / reconnection (2x faster). [Search at scale](#Search-At-Scale)
* `async` → Support non-blocking job submission. [Search Asynchronous](#Search-Asynchronous)
* extensive documentation → easy to follow
Expand Down Expand Up @@ -42,6 +43,7 @@ pp results
```

This example runs a search for "coffee" on Google. It then returns the results as a regular Ruby Hash.
Use `client.markdown(q: "coffee")` to return the same search as a token-efficient Markdown string optimized for LLMs and AI agents.
See the [playground](https://serpapi.com/playground) to generate your own code.

The SerpApi key can be obtained from [serpapi.com/signup](https://serpapi.com/users/sign_up?plan=free).
Expand Down Expand Up @@ -95,6 +97,9 @@ params = {
# search results as a symbolized Hash (per performance)
results = client.search(params)

# search results as a token-efficient Markdown string for LLMs and AI agents
markdown = client.markdown(params)

# search results as a raw HTML string
raw_html = client.html(params)
```
Expand Down Expand Up @@ -287,6 +292,7 @@ Now we can retrieve the previous search results from the archive using the searc
require 'serpapi'
client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'])
results = client.search_archive(search_id)
markdown = client.search_archive(search_id, :markdown)
pp results
```

Expand Down
6 changes: 6 additions & 0 deletions README.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBa
Query a vast range of data at scale, including web search results, flight schedules, stock market data, news headlines, and [more](https://serpapi.com).

## Features
* `markdown` → Return token-efficient Markdown output optimized for LLMs and AI agents
* `persistent` → Keep socket connection open to save on SSL handshake / reconnection (2x faster). [Search at scale](#Search-At-Scale)
* `async` → Support non-blocking job submission. [Search Asynchronous](#Search-Asynchronous)
* extensive documentation → easy to follow
Expand Down Expand Up @@ -42,6 +43,7 @@ pp results
```

This example runs a search for "coffee" on Google. It then returns the results as a regular Ruby Hash.
Use `client.markdown(q: "coffee")` to return the same search as a token-efficient Markdown string optimized for LLMs and AI agents.
See the [playground](https://serpapi.com/playground) to generate your own code.

The SerpApi key can be obtained from [serpapi.com/signup](https://serpapi.com/users/sign_up?plan=free).
Expand Down Expand Up @@ -95,6 +97,9 @@ params = {
# search results as a symbolized Hash (per performance)
results = client.search(params)

# search results as a token-efficient Markdown string for LLMs and AI agents
markdown = client.markdown(params)

# search results as a raw HTML string
raw_html = client.html(params)
```
Expand Down Expand Up @@ -287,6 +292,7 @@ Now we can retrieve the previous search results from the archive using the searc
require 'serpapi'
client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'])
results = client.search_archive(search_id)
markdown = client.search_archive(search_id, :markdown)
pp results
```

Expand Down
36 changes: 28 additions & 8 deletions lib/serpapi/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ module SerpApi
# features:
# * async non-block search
# * persistent HTTP connection
# * search API
# * search API with JSON, HTML, and Markdown output
# * location API
# * account API
# * search archive API
Expand Down Expand Up @@ -123,6 +123,15 @@ def html(params = {})
get('/search', :html, params)
end

# Perform a search using SerpApi.com and return results optimized for LLMs and AI agents.
# The output contains Markdown tables, links, and YAML frontmatter.
#
# @param [Hash] params includes engine, api_key, search fields and more.
# @return [String] search results formatted as Markdown.
def markdown(params = {})
get('/search.md', :markdown, params)
end

# Get location using Location API
#
# example: spec/serpapi/location_api_spec.rb
Expand All @@ -146,12 +155,13 @@ def location(params = {})
# doc: https://serpapi.com/search-archive-api
#
# @param [String|Integer] search_id from original search `results[:search_metadata][:id]`
# @param [Symbol] format :json or :html [default: json, optional]
# @return [String|Hash] raw html or JSON / Hash
# @param [Symbol] format :json, :html, or :markdown [default: json, optional]
# @return [String|Hash] raw HTML, Markdown, or JSON / Hash
def search_archive(search_id, format = :json)
raise SerpApiError, 'format must be json or html' unless [:json, :html].include?(format)
raise SerpApiError, 'format must be json, html, or markdown' unless [:json, :html, :markdown].include?(format)

get("/searches/#{search_id}.#{format}", format)
extension = format == :markdown ? :md : format
get("/searches/#{search_id}.#{extension}", format)
end

# Get account information using Account API
Expand Down Expand Up @@ -211,9 +221,9 @@ def persistent?
# Perform HTTP GET request to the SerpApi.com backend endpoint.
#
# @param [String] endpoint HTTP service URI
# @param [Symbol] decoder type :json or :html
# @param [Symbol] decoder type :json, :html, or :markdown
# @param [Hash] params custom search inputs
# @return [String|Hash] raw HTML or decoded response as JSON / Hash
# @return [String|Hash] raw text or decoded response as JSON / Hash
def get(endpoint, decoder = :json, params = {})
response = execute_request(endpoint, params)
handle_response(response, decoder, endpoint, params)
Expand All @@ -234,8 +244,10 @@ def handle_response(response, decoder, endpoint, params)
process_json_response(response, endpoint, params)
when :html
process_html_response(response, endpoint, params)
when :markdown
process_markdown_response(response, endpoint, params)
else
raise SerpApiError, "not supported decoder: #{decoder}, available: :json, :html"
raise SerpApiError, "not supported decoder: #{decoder}, available: :json, :html, :markdown"
end
end

Expand All @@ -258,6 +270,14 @@ def process_html_response(response, endpoint, params)
response.body
end

def process_markdown_response(response, endpoint, params)
raise_http_error(response, nil, endpoint, params, decoder: :markdown) if response.status != 200

data = response.body.to_s
response.flush if persistent?
data
end

def validate_json_content!(data, response, endpoint, params)
if data.is_a?(Hash) && data.key?(:error)
raise_http_error(response, data, endpoint, params, explicit_error: data[:error])
Expand Down
4 changes: 2 additions & 2 deletions lib/serpapi/error.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ module SerpApi
# - search_params: Hash of search parameters used (optional)
# - response_status: Integer HTTP or response status code (optional)
# - search_id: String id returned by the service for the search (optional)
# - decoder: Symbol representing the decoder/format used (optional) (e.g. :json)
# - decoder: Symbol representing the decoder/format used (optional) (e.g. :json or :markdown)
class SerpApiError < StandardError
attr_reader :serpapi_error, :search_params, :response_status, :search_id, :decoder

Expand All @@ -21,7 +21,7 @@ class SerpApiError < StandardError
# @param search_params [Hash, nil] optional hash of the search parameters used
# @param response_status [Integer, nil] optional HTTP or response status code
# @param search_id [String, nil] optional id returned by the service for the search
# @param decoder [Symbol, nil] optional decoder/format used (e.g. :json)
# @param decoder [Symbol, nil] optional decoder/format used (e.g. :json or :markdown)
def initialize(message = nil,
serpapi_error: nil,
search_params: nil,
Expand Down
26 changes: 26 additions & 0 deletions spec/serpapi/client/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,32 @@
expect(results).to match(/coffee/i)
end

it 'search for coffee in Austin, TX and receive Markdown' do
results = client.markdown(q: 'Coffee', location: 'Austin, TX')

expect(results).to be_a(String)
expect(results).to start_with('---')
expect(results).to include('## Organic Results')
end

it 'requests the Markdown endpoint' do
response = double(status: 200, body: "---\n## Organic Results\n", flush: :clean)
expect(client.socket).to receive(:get)
.with('/search.md', params: hash_including(q: 'Coffee'))
.and_return(response)

expect(client.markdown(q: 'Coffee')).to start_with('---')
end

it 'reports Markdown HTTP errors with their decoder' do
response = double(status: 400, body: 'Invalid search')
allow(client.socket).to receive(:get).and_return(response)

expect {
client.markdown(q: 'Coffee')
}.to raise_error(SerpApi::SerpApiError) { |error| expect(error.decoder).to eq(:markdown) }
end

it 'missing query' do
begin
client.search
Expand Down
12 changes: 12 additions & 0 deletions spec/serpapi/client/search_archive_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,4 +32,16 @@
expect(archive_search).to eq(results)
end
end

it 'fetches an archived search as Markdown' do
client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'], engine: 'google')
response = double(status: 200, body: "---\n## Organic Results\n", flush: :clean)

expect(client.socket).to receive(:get)
.with('/searches/search-id.md', params: hash_including(api_key: ENV['SERPAPI_KEY']))
.and_return(response)

results = client.search_archive('search-id', :markdown)
expect(results).to eq("---\n## Organic Results\n")
end
end
Loading