Skip to content

Commit cea26e4

Browse files
committed
Add initial Markdown support
1 parent 8e5ed5c commit cea26e4

7 files changed

Lines changed: 81 additions & 10 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# Changelog
2+
* [Unreleased] Add Markdown search and archive output support
23
* [2026-02-23] 1.0.3 Enhance error object #16
34
* [2025-11-17] 1.0.2 Implement `inspect` functions for client #13
45
* [2025-07-18] 1.0.1 Add support for old Ruby versions (2.7, 3.0)

README.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBa
99
Query a vast range of data at scale, including web search results, flight schedules, stock market data, news headlines, and [more](https://serpapi.com).
1010

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

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

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

100+
# search results as a token-efficient Markdown string for LLMs and AI agents
101+
markdown = client.markdown(params)
102+
98103
# search results as a raw HTML string
99104
raw_html = client.html(params)
100105
```
@@ -287,6 +292,7 @@ Now we can retrieve the previous search results from the archive using the searc
287292
require 'serpapi'
288293
client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'])
289294
results = client.search_archive(search_id)
295+
markdown = client.search_archive(search_id, :markdown)
290296
pp results
291297
```
292298

README.md.erb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ SerpApi supports Google, Google Maps, Google Shopping, Baidu, Yandex, Yahoo, eBa
99
Query a vast range of data at scale, including web search results, flight schedules, stock market data, news headlines, and [more](https://serpapi.com).
1010

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

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

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

100+
# search results as a token-efficient Markdown string for LLMs and AI agents
101+
markdown = client.markdown(params)
102+
98103
# search results as a raw HTML string
99104
raw_html = client.html(params)
100105
```
@@ -287,6 +292,7 @@ Now we can retrieve the previous search results from the archive using the searc
287292
require 'serpapi'
288293
client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'])
289294
results = client.search_archive(search_id)
295+
markdown = client.search_archive(search_id, :markdown)
290296
pp results
291297
```
292298

lib/serpapi/client.rb

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ module SerpApi
77
# features:
88
# * async non-block search
99
# * persistent HTTP connection
10-
# * search API
10+
# * search API with JSON, HTML, and Markdown output
1111
# * location API
1212
# * account API
1313
# * search archive API
@@ -123,6 +123,15 @@ def html(params = {})
123123
get('/search', :html, params)
124124
end
125125

126+
# Perform a search using SerpApi.com and return results optimized for LLMs and AI agents.
127+
# The output contains Markdown tables, links, and YAML frontmatter.
128+
#
129+
# @param [Hash] params includes engine, api_key, search fields and more.
130+
# @return [String] search results formatted as Markdown.
131+
def markdown(params = {})
132+
get('/search.md', :markdown, params)
133+
end
134+
126135
# Get location using Location API
127136
#
128137
# example: spec/serpapi/location_api_spec.rb
@@ -146,12 +155,13 @@ def location(params = {})
146155
# doc: https://serpapi.com/search-archive-api
147156
#
148157
# @param [String|Integer] search_id from original search `results[:search_metadata][:id]`
149-
# @param [Symbol] format :json or :html [default: json, optional]
150-
# @return [String|Hash] raw html or JSON / Hash
158+
# @param [Symbol] format :json, :html, or :markdown [default: json, optional]
159+
# @return [String|Hash] raw HTML, Markdown, or JSON / Hash
151160
def search_archive(search_id, format = :json)
152-
raise SerpApiError, 'format must be json or html' unless [:json, :html].include?(format)
161+
raise SerpApiError, 'format must be json, html, or markdown' unless [:json, :html, :markdown].include?(format)
153162

154-
get("/searches/#{search_id}.#{format}", format)
163+
extension = format == :markdown ? :md : format
164+
get("/searches/#{search_id}.#{extension}", format)
155165
end
156166

157167
# Get account information using Account API
@@ -211,9 +221,9 @@ def persistent?
211221
# Perform HTTP GET request to the SerpApi.com backend endpoint.
212222
#
213223
# @param [String] endpoint HTTP service URI
214-
# @param [Symbol] decoder type :json or :html
224+
# @param [Symbol] decoder type :json, :html, or :markdown
215225
# @param [Hash] params custom search inputs
216-
# @return [String|Hash] raw HTML or decoded response as JSON / Hash
226+
# @return [String|Hash] raw text or decoded response as JSON / Hash
217227
def get(endpoint, decoder = :json, params = {})
218228
response = execute_request(endpoint, params)
219229
handle_response(response, decoder, endpoint, params)
@@ -234,8 +244,10 @@ def handle_response(response, decoder, endpoint, params)
234244
process_json_response(response, endpoint, params)
235245
when :html
236246
process_html_response(response, endpoint, params)
247+
when :markdown
248+
process_markdown_response(response, endpoint, params)
237249
else
238-
raise SerpApiError, "not supported decoder: #{decoder}, available: :json, :html"
250+
raise SerpApiError, "not supported decoder: #{decoder}, available: :json, :html, :markdown"
239251
end
240252
end
241253

@@ -258,6 +270,14 @@ def process_html_response(response, endpoint, params)
258270
response.body
259271
end
260272

273+
def process_markdown_response(response, endpoint, params)
274+
raise_http_error(response, nil, endpoint, params, decoder: :markdown) if response.status != 200
275+
276+
data = response.body.to_s
277+
response.flush if persistent?
278+
data
279+
end
280+
261281
def validate_json_content!(data, response, endpoint, params)
262282
if data.is_a?(Hash) && data.key?(:error)
263283
raise_http_error(response, data, endpoint, params, explicit_error: data[:error])

lib/serpapi/error.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module SerpApi
1010
# - search_params: Hash of search parameters used (optional)
1111
# - response_status: Integer HTTP or response status code (optional)
1212
# - search_id: String id returned by the service for the search (optional)
13-
# - decoder: Symbol representing the decoder/format used (optional) (e.g. :json)
13+
# - decoder: Symbol representing the decoder/format used (optional) (e.g. :json or :markdown)
1414
class SerpApiError < StandardError
1515
attr_reader :serpapi_error, :search_params, :response_status, :search_id, :decoder
1616

@@ -21,7 +21,7 @@ class SerpApiError < StandardError
2121
# @param search_params [Hash, nil] optional hash of the search parameters used
2222
# @param response_status [Integer, nil] optional HTTP or response status code
2323
# @param search_id [String, nil] optional id returned by the service for the search
24-
# @param decoder [Symbol, nil] optional decoder/format used (e.g. :json)
24+
# @param decoder [Symbol, nil] optional decoder/format used (e.g. :json or :markdown)
2525
def initialize(message = nil,
2626
serpapi_error: nil,
2727
search_params: nil,

spec/serpapi/client/client_spec.rb

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,32 @@
3030
expect(results).to match(/coffee/i)
3131
end
3232

33+
it 'search for coffee in Austin, TX and receive Markdown' do
34+
results = client.markdown(q: 'Coffee', location: 'Austin, TX')
35+
36+
expect(results).to be_a(String)
37+
expect(results).to start_with('---')
38+
expect(results).to include('## Organic Results')
39+
end
40+
41+
it 'requests the Markdown endpoint' do
42+
response = double(status: 200, body: "---\n## Organic Results\n", flush: :clean)
43+
expect(client.socket).to receive(:get)
44+
.with('/search.md', params: hash_including(q: 'Coffee'))
45+
.and_return(response)
46+
47+
expect(client.markdown(q: 'Coffee')).to start_with('---')
48+
end
49+
50+
it 'reports Markdown HTTP errors with their decoder' do
51+
response = double(status: 400, body: 'Invalid search')
52+
allow(client.socket).to receive(:get).and_return(response)
53+
54+
expect {
55+
client.markdown(q: 'Coffee')
56+
}.to raise_error(SerpApi::SerpApiError) { |error| expect(error.decoder).to eq(:markdown) }
57+
end
58+
3359
it 'missing query' do
3460
begin
3561
client.search

spec/serpapi/client/search_archive_api_spec.rb

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,4 +32,16 @@
3232
expect(archive_search).to eq(results)
3333
end
3434
end
35+
36+
it 'fetches an archived search as Markdown' do
37+
client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'], engine: 'google')
38+
response = double(status: 200, body: "---\n## Organic Results\n", flush: :clean)
39+
40+
expect(client.socket).to receive(:get)
41+
.with('/searches/search-id.md', params: hash_including(api_key: ENV['SERPAPI_KEY']))
42+
.and_return(response)
43+
44+
results = client.search_archive('search-id', :markdown)
45+
expect(results).to eq("---\n## Organic Results\n")
46+
end
3547
end

0 commit comments

Comments
 (0)