Skip to content

Commit 379f946

Browse files
committed
Add initial Markdown support
1 parent 176d977 commit 379f946

7 files changed

Lines changed: 119 additions & 12 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: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,34 @@ client.close
5555

5656
- [Asynchronous searches](./demo/demo_async.rb) for submitting non-blocking jobs and retrieving completed results from the Search Archive API.
5757
- [Persistent connections and connection pooling](./demo/demo_thread_pool.rb) for reusing HTTP connections across searches.
58-
- JSON responses as Ruby hashes with `search`, or raw search-engine HTML with `html`.
58+
- Search results as Ruby hashes with `search`, token-efficient Markdown with `markdown`, or raw search-engine HTML with `html`.
5959
- SDK methods for the [Location API](https://serpapi.com/locations-api), [Search Archive API](https://serpapi.com/search-archive-api), and [Account API](https://serpapi.com/account-api).
6060
- Configurable HTTP timeouts and symbolized or string JSON keys.
6161

62+
## Response formats
63+
64+
Use `search` for structured results decoded into a Ruby `Hash`:
65+
66+
```ruby
67+
results = client.search(q: "coffee")
68+
```
69+
70+
Use `markdown` for a token-efficient Markdown `String` optimized for LLMs and AI agents:
71+
72+
```ruby
73+
markdown = client.markdown(q: "coffee")
74+
```
75+
76+
Use `html` when you need the raw search-engine response:
77+
78+
```ruby
79+
raw_html = client.html(q: "coffee")
80+
```
81+
82+
Archived results are also available as Markdown with `client.search_archive(search_id, :markdown)`.
83+
84+
Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output).
85+
6286
## Configuration
6387

6488
Set defaults when creating a client, then override search parameters in individual calls:

‎README.md.erb‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,34 @@ client.close
5555

5656
- [Asynchronous searches](./demo/demo_async.rb) for submitting non-blocking jobs and retrieving completed results from the Search Archive API.
5757
- [Persistent connections and connection pooling](./demo/demo_thread_pool.rb) for reusing HTTP connections across searches.
58-
- JSON responses as Ruby hashes with `search`, or raw search-engine HTML with `html`.
58+
- Search results as Ruby hashes with `search`, token-efficient Markdown with `markdown`, or raw search-engine HTML with `html`.
5959
- SDK methods for the [Location API](https://serpapi.com/locations-api), [Search Archive API](https://serpapi.com/search-archive-api), and [Account API](https://serpapi.com/account-api).
6060
- Configurable HTTP timeouts and symbolized or string JSON keys.
6161

62+
## Response formats
63+
64+
Use `search` for structured results decoded into a Ruby `Hash`:
65+
66+
```ruby
67+
results = client.search(q: "coffee")
68+
```
69+
70+
Use `markdown` for a token-efficient Markdown `String` optimized for LLMs and AI agents:
71+
72+
```ruby
73+
markdown = client.markdown(q: "coffee")
74+
```
75+
76+
Use `html` when you need the raw search-engine response:
77+
78+
```ruby
79+
raw_html = client.html(q: "coffee")
80+
```
81+
82+
Archived results are also available as Markdown with `client.search_archive(search_id, :markdown)`.
83+
84+
Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output).
85+
6286
## Configuration
6387

6488
Set defaults when creating a client, then override search parameters in individual calls:

‎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)