Skip to content
Merged
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ serpapi search engine=google q=coffee --all-pages --max-pages 3 --jq '.organic_r

#### Image Search (Google Lens)

- `--image <file>` — Upload an image with the [Image API](https://serpapi.com/image-api) and search with its `image_id` in one step. Use `-` to read the image from stdin. Only `engine=google_lens` takes an `image_id`, so other engines are refused before the upload; to pass an upload to another engine, use [`serpapi image`](#image) and `image_id=<id>`.
- `--image <file>` — Upload an image with the [Image API](https://serpapi.com/image-api) and search with its `image_id` in one step. Use `-` to read the image from stdin. Only `engine=google_lens` takes an `image_id`: other engines drop it and would run a normal search without the image, so they are refused before the upload, and so is `url=`.

```bash
# Reverse image search with Google Lens
Expand Down Expand Up @@ -112,7 +112,7 @@ serpapi account --jq .total_searches_left

### image

Upload an image with the [Image API](https://serpapi.com/image-api) and print its `image_id` without searching. Useful when you want to reuse the same upload across several searches; image IDs expire after 10 minutes. For a one-off search, prefer `serpapi search --image <file>`.
Upload an image with the [Image API](https://serpapi.com/image-api) and print its `image_id` without searching. Useful when you want to reuse the same upload across several `engine=google_lens` searches, the only engine that takes an `image_id`; image IDs expire after 10 minutes. For a one-off search, prefer `serpapi search engine=google_lens --image <file>`.

```bash
# Upload an image file
Expand All @@ -124,7 +124,7 @@ curl -s https://example.com/photo.jpg | serpapi image -
# Reuse the image_id across searches
IMAGE_ID=$(serpapi image ./photo.jpg --jq .image_id)
serpapi search engine=google_lens image_id=$IMAGE_ID
serpapi search engine=google_lens image_id=$IMAGE_ID type=products
serpapi search engine=google_lens image_id=$IMAGE_ID type=exact_matches
```

### login
Expand Down Expand Up @@ -217,7 +217,7 @@ A config file that can't be read or isn't valid TOML is reported by path and lin
| 1 | `network_error` | No answer, such as a timeout. |
| 130 | `interrupted` | Ctrl-C or SIGTERM stopped it before it finished, after printing what had arrived, such as the `--all-pages` pages fetched so far. |

- **Cost**: each successful search counts against your monthly searches, including each `--all-pages` page; cached, errored and failed ones don't, though errored ones count toward the hourly limit ([pricing FAQ](https://serpapi.com/pricing)). An identical search, `--fields` included, repeated within an hour comes from the cache. `no_cache=true` forces a new, counted search; cached results are at most an hour old, so add it only when that is too old. `zero_trace=true` (Enterprise) stores nothing, so it skips the cache too and leaves no search for `archive` to read. `account`, `locations` and `archive` are free.
- **Cost**: each successful search counts against your monthly searches, including each `--all-pages` page; cached, errored and failed ones don't, though errored ones count toward the hourly limit ([pricing FAQ](https://serpapi.com/pricing)). An identical search, `--fields` included, repeated within an hour comes from the cache. `no_cache=true` forces a new, counted search; cached results are at most an hour old, so add it only when that is too old. `zero_trace=true` (Enterprise) stores nothing, so it skips the cache too and leaves no search for `archive` to read. `account`, `locations` and `archive` are free, and so is an `image` upload, though it needs searches left this month and this hour, and has its own hourly limit, `account_rate_limit_per_hour` in `serpapi account`.
- **No prompts without a terminal.** Only `serpapi login` and an image given as `-` (`serpapi image -`, `search --image -`) read stdin. `login` waits for the first line, so pipe the key in (`printf %s "$KEY" | serpapi login`) or, simpler, set `SERPAPI_KEY`; an image is read until stdin closes.

## Links
Expand Down
4 changes: 2 additions & 2 deletions pkg/api/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -356,8 +356,8 @@ func (c *Client) Locations(ctx context.Context, params map[string]string) (json.
}

// UploadImage uploads an image file using the Image API. The returned payload
// contains an image_id usable with engines that accept uploaded images, such
// as google_lens. Image IDs expire after 10 minutes.
// contains an image_id, which only engine=google_lens takes. Image IDs expire
// after 10 minutes.
func (c *Client) UploadImage(ctx context.Context, path string) (json.RawMessage, error) {
f, err := os.Open(path)
if err != nil {
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/debug.go
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ func round3(f float64) float64 {

// clientTrace returns httptrace hooks that report each connection phase.
// DNS/connect hooks are not fired for connections made via an HTTP proxy;
// GotConn still is, which is why the proxy is logged separately in get.
// GotConn still is, which is why logRequest reports the proxy.
func (t *tracer) clientTrace() *httptrace.ClientTrace {
if t == nil {
return nil
Expand Down
11 changes: 7 additions & 4 deletions pkg/cmd/image.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,15 @@ import (
var imageCmd = &cobra.Command{
Use: "image <file>",
Aliases: []string{"upload-image"},
Short: "Upload an image for use with image search engines like Google Lens",
Short: "Upload an image for a Google Lens search",
Long: `Upload an image with the SerpApi Image API and print the resulting image_id.

The image_id can be passed to Search API engines that accept uploaded images,
such as google_lens. Supported formats are JPG/JPEG, PNG and WebP up to 500 KB.
Uploaded image IDs expire after 10 minutes.
The image_id works only with engine=google_lens, for 10 minutes. Supported
formats are JPG/JPEG, PNG and WebP up to 500 KB.

An upload doesn't count against your searches, but it needs searches left this
month and this hour, and uploads have their own hourly limit:
account_rate_limit_per_hour in "serpapi account".

To upload and search Google Lens in one step, use
"serpapi search engine=google_lens --image <file>" instead.
Expand Down
20 changes: 18 additions & 2 deletions pkg/cmd/image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,18 +182,34 @@ func TestSearchWithImageRefusesOtherEnginesUnsent(t *testing.T) {
}{
{"no engine", nil, "--image needs engine=google_lens"},
{"empty engine", []string{"engine="}, "--image needs engine=google_lens"},
{"engine=google", []string{"engine=google"}, "not engine=google; for another engine, upload with 'serpapi image FILE'"},
{"engine=google", []string{"engine=google"}, "--image works only with engine=google_lens, not engine=google; other engines drop image_id and search without the image"},
{"engine=bing_reverse_image", []string{"engine=bing_reverse_image"}, "not engine=bing_reverse_image"},
{"url as well", []string{"engine=google_lens", "url=https://example.com/a.png"}, "Use either --image or url=<url>, not both"},
} {
t.Run(tc.name, func(t *testing.T) {
fakeAPI(t, nil)
args := append([]string{"--api-key", imageKey, "search", "q=cat"}, tc.args...)
res := execute(t, "", append(args, "--image", writeImage(t, "photo.png", png1x1))...)
wantError(t, res, 2, "usage_error", tc.msg)
msg, _ := wantError(t, res, 2, "usage_error", tc.msg)["message"].(string)
if strings.Contains(msg, "pass image_id") {
t.Errorf("message %q suggests image_id for an engine that drops it", msg)
}
})
}
}

// SerpApi lowercases the engine, so --image takes google_lens in any case.
func TestSearchWithImageTakesGoogleLensInAnyCase(t *testing.T) {
reqs, uploads := imageAPI(t)

res := execute(t, "", "--api-key", imageKey, "search", "engine=Google_Lens", "--image", writeImage(t, "photo.png", png1x1))

wantOK(t, res)
if n, u := len(reqs.list()), len(uploads()); n != 2 || u != 1 {
t.Errorf("got %d requests and %d uploads, want the upload, then the search", n, u)
}
}

func TestImageRefusesEmptyAndOversizedImagesUnsent(t *testing.T) {
big := append(append([]byte{}, png1x1...), make([]byte, 600<<10)...)
for _, tc := range []struct {
Expand Down
4 changes: 3 additions & 1 deletion pkg/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,9 @@ search, --fields included, repeated within an hour comes from the cache.
no_cache=true forces a new, counted search; cached results are at most an
hour old, so add it only when that is too old. zero_trace=true (Enterprise)
stores nothing, so it skips the cache too and leaves no search for archive to
read. account, locations and archive are free.`,
read. account, locations and archive are free, and so is an image upload,
though it needs searches left this month and this hour, and has its own hourly
limit.`,
Example: ` serpapi search engine=google q=coffee --fields "organic_results[0:3]"
serpapi search engine=google coffee shops --jq ".organic_results[].link"
serpapi search engine=google q=coffee output=md
Expand Down
11 changes: 7 additions & 4 deletions pkg/cmd/search.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,17 @@ func runSearch(cmd *cobra.Command, args []string) error {
if _, exists := p["image_id"]; exists && imageFlag != "" {
return &clierrors.UsageError{Message: "Use either --image or image_id=<id>, not both"}
}
// Only Google Lens documents image_id; other engines would ignore it and
// run an unrelated search after the upload.
if e := p["engine"]; imageFlag != "" && e != "google_lens" {
// SerpApi takes image_id only on google_lens, whatever the case; other
// engines drop it and would run a normal, counted search after the upload.
if e := p["engine"]; imageFlag != "" && !strings.EqualFold(e, "google_lens") {
if e == "" {
return &clierrors.UsageError{Message: "--image needs engine=google_lens"}
}
return &clierrors.UsageError{Message: "--image works only with engine=google_lens, not engine=" + e +
"; for another engine, upload with 'serpapi image FILE' and pass image_id=<id>"}
"; other engines drop image_id and search without the image"}
}
if _, exists := p["url"]; exists && imageFlag != "" {
return &clierrors.UsageError{Message: "Use either --image or url=<url>, not both"}
}

maxPagesSet := cmd.Flags().Changed("max-pages")
Expand Down
Loading