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
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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`.
- 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).
- SDK methods for the [Image API](https://serpapi.com/image-api), [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.

## Configuration
Expand Down Expand Up @@ -186,6 +186,21 @@ pp results[:images_results]

A [light variant](https://serpapi.com/google-images-light-api) engine called `google_images_light` is also available for faster, lower-cost image searches.

### Google Lens with File Upload

Upload an image with the Image API, then use its image ID in a Google Lens search. Uploaded image IDs expire after 10 minutes.

```ruby
require 'serpapi'

client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'])
upload = client.upload_image('./image.jpg')
results = client.search(engine: 'google_lens', image_id: upload[:image_id])
pp results[:visual_matches]
```

[See Image API documentation](https://serpapi.com/image-api) · [See Google Lens image upload documentation](https://serpapi.com/google-lens-upload-an-image)

### Google Trends

Track search interest over time and compare the popularity of search terms.
Expand Down Expand Up @@ -332,6 +347,6 @@ The older library (google-search-results-ruby) was performing at 55 req/s on Rub

## Contributing

Contributions are welcome. Make sure to read our [contributing guide](./CONTRIBUTING.md).
Contributions are welcome. Make sure to read our [contributing guide](https://github.com/serpapi/serpapi-ruby/blob/master/CONTRIBUTING.md).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I originally kept a local link so when you work with the repo or it's hosted somewhere it works as usual. The .erb version has a full link because it's on the website. Can you explain more?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It was generated from the .erb version. We are actually using just the .md on the website, it is broken atm.

CleanShot 2026-08-18 at 15 57 14@2x

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah, okay. Thanks for noticing.


© 2026 [SerpApi](https://serpapi.com)
17 changes: 16 additions & 1 deletion README.md.erb
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ 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`.
- 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).
- SDK methods for the [Image API](https://serpapi.com/image-api), [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.

## Configuration
Expand Down Expand Up @@ -186,6 +186,21 @@ pp results[:images_results]

A [light variant](https://serpapi.com/google-images-light-api) engine called `google_images_light` is also available for faster, lower-cost image searches.

### Google Lens with File Upload

Upload an image with the Image API, then use its image ID in a Google Lens search. Uploaded image IDs expire after 10 minutes.

```ruby
require 'serpapi'

client = SerpApi::Client.new(api_key: ENV['SERPAPI_KEY'])
upload = client.upload_image('./image.jpg')
results = client.search(engine: 'google_lens', image_id: upload[:image_id])
pp results[:visual_matches]
```

[See Image API documentation](https://serpapi.com/image-api) · [See Google Lens image upload documentation](https://serpapi.com/google-lens-upload-an-image)

### Google Trends

Track search interest over time and compare the popularity of search terms.
Expand Down
47 changes: 38 additions & 9 deletions lib/serpapi/client.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ module SerpApi
# * async non-block search
# * persistent HTTP connection
# * search API
# * image API
# * location API
# * account API
# * search archive API
Expand Down Expand Up @@ -134,6 +135,25 @@ def location(params = {})
get('/locations.json', :json, params)
end

# Upload an image using the Image API.
# The returned image ID can be supplied to Search API engines that support
# uploaded images, such as Google Lens. Image IDs expire after 10 minutes.
#
# doc: https://serpapi.com/image-api
#
# @param [String, Pathname, IO] image path or readable image stream
# @param [Hash] params request parameters, such as an API key overriding the client default
# @return [Hash] upload result containing the image ID
def upload_image(image, params = {})
if image.is_a?(String) || (defined?(Pathname) && image.is_a?(Pathname))
::File.open(image, 'rb') do |file|
post('/image', :json, params, image: HTTP::FormData::File.new(file))
end
else
post('/image', :json, params, image: HTTP::FormData::File.new(image).tap(&:rewind))
end
end

# Retrieve search result from the Search Archive API
#
# ```ruby
Expand Down Expand Up @@ -215,17 +235,26 @@ def persistent?
# @param [Hash] params custom search inputs
# @return [String|Hash] raw HTML or decoded response as JSON / Hash
def get(endpoint, decoder = :json, params = {})
response = execute_request(endpoint, params)
response = execute_request(:get, endpoint, params: query(params))
handle_response(response, decoder, endpoint, params)
end

def execute_request(endpoint, params)
if persistent?
@socket.get(endpoint, params: query(params))
else
url = "https://#{BACKEND}#{endpoint}"
HTTP.timeout(timeout).get(url, params: query(params))
end
# Perform an HTTP POST request with a multipart/form-data body
#
# @param [String] endpoint relative SerpApi endpoint, such as "/image"
# @param [Symbol] decoder response decoder, either :json or :html
# @param [Hash] params request parameters
# @param [Hash] form multipart-specific fields, including uploaded files
# @return [String|Hash] raw HTML or decoded response as JSON / Hash
def post(endpoint, decoder = :json, params = {}, form = {})
response = execute_request(:post, endpoint, form: query(params).merge(form))
handle_response(response, decoder, endpoint, params)
end
Comment thread
tanysheng marked this conversation as resolved.

def execute_request(method, endpoint, options)
client = persistent? ? @socket : HTTP.timeout(timeout)
url = persistent? ? endpoint : "https://#{BACKEND}#{endpoint}"
client.public_send(method, url, **options)
end

def handle_response(response, decoder, endpoint, params)
Expand Down Expand Up @@ -283,7 +312,7 @@ def raise_http_error(response, data, endpoint, params, explicit_error: nil, deco

def raise_parser_error(response, endpoint, params)
raise SerpApiError.new(
"JSON parse error: #{response.body} on get url: https://#{BACKEND}#{endpoint}",
"JSON parse error: #{response.body} from url: https://#{BACKEND}#{endpoint}",
search_params: params,
response_status: response.status,
decoder: :json
Expand Down
66 changes: 66 additions & 0 deletions spec/serpapi/client/image_api_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
require 'spec_helper'
require 'stringio'
require 'tempfile'

describe 'Image API' do
let(:response) do
double(
status: 200,
body: '{"message":"Image uploaded successfully.","image_id":"test-image-id"}'
)
end
let(:socket) { double }
let(:client) { SerpApi::Client.new(api_key: api_key) }

before do
allow(HTTP).to receive(:persistent).and_return(socket)
allow(response).to receive(:flush)
end

it 'uploads an image from a file path' do
Tempfile.create(['image', '.png']) do |image|
expect(socket).to receive(:post) do |endpoint, options|
uploaded_image = options[:form][:image]

expect(endpoint).to eq('/image')
expect(uploaded_image.filename).to end_with('.png')
response
end
Comment thread
tanysheng marked this conversation as resolved.

result = client.upload_image(image.path)

expect(result[:message]).to eq('Image uploaded successfully.')
expect(result[:image_id]).to eq('test-image-id')
end
end

it 'rewinds and uploads an image from an IO object' do
image = StringIO.new('image data')
image.read(5)

expect(socket).to receive(:post) do |endpoint, options|
expect(endpoint).to eq('/image')
expect(image.pos).to eq(0)
response
end

result = client.upload_image(image)

expect(result[:image_id]).to eq('test-image-id')
end

it 'raises an error when image is rejected' do
error_response = double(
status: 400,
body: '{"error":"Invalid image format. Supported format: jpg, jpeg, png, webp"}'
)
allow(socket).to receive(:post).with('/image', anything).and_return(error_response)

expect {
client.upload_image(StringIO.new('invalid image data'))
}.to raise_error(
SerpApi::SerpApiError,
/Invalid image format\. Supported format: jpg, jpeg, png, webp/
)
end
end
Loading