Skip to content
Open
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
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,34 @@ client.close

- [Asynchronous searches](./demo/demo_async.rb) for submitting non-blocking jobs and retrieving completed results from the Search Archive API.
- [Persistent connections and connection pooling](./demo/demo_thread_pool.rb) for reusing HTTP connections across searches.
- JSON responses as Ruby hashes with `search`, or raw search-engine HTML with `html`.
- Search results as Ruby hashes with `search`, token-efficient Markdown with `md`, or raw search-engine HTML with `html`.
- 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).
- Configurable HTTP timeouts and symbolized or string JSON keys.

## Response formats

Use `search` for structured results decoded into a Ruby `Hash`:

```ruby
results = client.search(q: "coffee")
```

Use `md` for a token-efficient Markdown `String` optimized for LLMs and AI agents:

```ruby
markdown = client.md(q: "coffee")
```

Use `html` when you need the raw search-engine response:

```ruby
raw_html = client.html(q: "coffee")
```

Archived results are also available as Markdown with `client.search_archive(search_id, :md)`.

Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output).

## Configuration

Set defaults when creating a client, then override search parameters in individual calls:
Expand Down
26 changes: 25 additions & 1 deletion README.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,34 @@ client.close

- [Asynchronous searches](./demo/demo_async.rb) for submitting non-blocking jobs and retrieving completed results from the Search Archive API.
- [Persistent connections and connection pooling](./demo/demo_thread_pool.rb) for reusing HTTP connections across searches.
- JSON responses as Ruby hashes with `search`, or raw search-engine HTML with `html`.
- Search results as Ruby hashes with `search`, token-efficient Markdown with `md`, or raw search-engine HTML with `html`.
- 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).
- Configurable HTTP timeouts and symbolized or string JSON keys.

## Response formats

Use `search` for structured results decoded into a Ruby `Hash`:

```ruby
results = client.search(q: "coffee")
```

Use `md` for a token-efficient Markdown `String` optimized for LLMs and AI agents:

```ruby
markdown = client.md(q: "coffee")
```

Use `html` when you need the raw search-engine response:

```ruby
raw_html = client.html(q: "coffee")
```

Archived results are also available as Markdown with `client.search_archive(search_id, :md)`.

Learn more about [SerpApi Markdown output](https://serpapi.com/markdown-output).

## Configuration

Set defaults when creating a client, then override search parameters in individual calls:
Expand Down
33 changes: 26 additions & 7 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 md(params = {})
get('/search.md', :md, params)
end

# Get location using Location API
#
# example: spec/serpapi/location_api_spec.rb
Expand All @@ -146,10 +155,10 @@ 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 :md [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 md' unless [:json, :html, :md].include?(format)

get("/searches/#{search_id}.#{format}", format)
end
Expand Down Expand Up @@ -211,9 +220,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 :md
# @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 +243,10 @@ def handle_response(response, decoder, endpoint, params)
process_json_response(response, endpoint, params)
when :html
process_html_response(response, endpoint, params)
when :md
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, :md"
end
end

Expand All @@ -258,6 +269,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: :md) 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 :md)
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 :md)
def initialize(message = nil,
serpapi_error: nil,
search_params: nil,
Expand Down
14 changes: 14 additions & 0 deletions spec/serpapi/client/client_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,20 @@
expect(results).to match(/coffee/i)
end

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

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

it 'reports Markdown HTTP errors with their decoder' do
expect {
client.md
}.to raise_error(SerpApi::SerpApiError) { |error| expect(error.decoder).to eq(:md) }
end

it 'missing query' do
begin
client.search
Expand Down
4 changes: 4 additions & 0 deletions spec/serpapi/client/search_archive_api_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
client = SerpApi::Client.new(api_key: client.api_key, engine: 'google')
results = client.search_archive(search_id)
expect(archive_search).to eq(results)

markdown = client.search_archive(search_id, :md)
expect(markdown).to be_a(String)
expect(markdown).to start_with('---')
else
client = SerpApi::Client.new(api_key: client.api_key, engine: 'google')
allow(client).to receive(:get) { search_response_mock }
Expand Down
Loading