Repository: langchain4j/langchain4j (Java)

Issue:
[FEATURE] [💎 $400 bounty] Integrate SearchApi as a WebSearchEngine and as a Tool
## Feature Overview

Integrate [SearchApi](https://www.searchapi.io/) as a `WebSearchEngine` and as a `Tool` for function calling.

## Requirements

- Support [Google Search via SearchApi](https://www.searchapi.io/docs/google)
- Add SearchApi tool documentation
- Write at least 1 example of using SearchApi as a tool

Adhere to [langchain4j contribution guidelines](https://github.com/langchain4j/langchain4j/blob/main/CONTRIBUTING.md)

## Related work

- https://github.com/langchain4j/langchain4j/pull/642 - ground work for adding search tools
- https://github.com/langchain4j/langchain4j/pull/641
- https://github.com/langchain4j/langchain4j/pull/676

Related issues:
- https://github.com/langchain4j/langchain4j/issues/146

## Existing SearchApi integrations

- [SearchApi LangChain Python](https://python.langchain.com/v0.2/docs/integrations/tools/searchapi/)
- [SearchApi LangChain JS](https://js.langchain.com/v0.2/docs/integrations/tools/searchapi)
- [SearchApi Summarization example](https://js.langchain.com/v0.2/docs/tutorials/summarization)

## Design considerations

SearchApi support not only Google Search, but 30+ other APIs such as Youtube Search, Transcripts, Bing Search (similar JSON response keys). In a statically typed language, it is probably better to have separate tools for separate APIs, but implementation could be flexible enough to extend SearchApi and easily adapt to other engines. All engines use the same HTTP GET request. The only difference is the parameters they accept. And how you want the response to be parsed. 

## Other notes

- Ping us if you need more search credits when implementing.
- Someone from the SearchApi Team will help review the functionality.

## Bounty

There is a $400 bounty on this awarded by SearchApi to the community.

In the event of multiple attempts, the one that is going to be merged by the maintainers of the repository will pick the bounty.


Discussion: 💎 **SearchApi** is offering a **$400** bounty for this issue
👉 Got a pull request resolving this? Claim the bounty by adding `@algora-pbc /claim #1132` in the PR body and joining algora.io
@mark-searchapi  can i get this assigned?
Given the speed at which we're moving, we don't assign issues or "give" issues to anyone.

@abhishek818 In the event of multiple attempts, the one that is going to be merged by the maintainers of the repository will pick the bounty.
Hello @mark-searchapi, I just started work on the SearchApi integration as a search engine and tool. 
If anyone else ( and @abhishek818 ) has already started work on this please let me know as soon as you can.
Please adhere to these two implementations:
https://github.com/langchain4j/langchain4j/tree/main/web-search-engines
Yes of course.
>  If anyone else ( and ...) has already started work on this please let me know as soon as you can.

@jemiluv8 
I've but I've not publicly added an "attempt", yet. Since you've also started with the issue, what difference does it make if you know or not?
@ayewo. Just wanted to know so I may halt my attempt since I just started.
I'm aborting my attempt now so as to not duplicate your efforts.
Happy hunting ...
Hi, was setting this up. Just confused as to how I am supposed to set up env like OPENAI_API_KEY. 
> Hi, was setting this up. Just confused as to how I am supposed to set up env like OPENAI_API_KEY.

I have raised  a draft PR with my current work. Need help with this to continue. Also pls assign this to me if satisfactory
@Ahmadkhan02 

The [comment](https://github.com/langchain4j/langchain4j/issues/1132#issuecomment-2120699867) by @mark-searchapi above clearly says:
> Given the speed at which we're moving, we don't assign issues or "give" issues to anyone.
@abhishek818 why do you need open ai key? In any way, you can register with openai and get it there: https://github.com/langchain4j/langchain4j?tab=readme-ov-file#how-to-get-an-api-key
Hi @mark-searchapi 
Looking at the docs for the [Google Search API](https://www.searchapi.io/docs/google), it seems the API will return JSON to match the type of search query. In other words, the endpoint returns differently shaped JSON depending on whether the search is for a well-known fact, a local business, a product search etc.

The docs itself lists about 34 API examples which I pulled out into the list of JSON elements below:
* `"knowledge_graph"#"knowledge_graph_type": "Kp3 verticals"`
* `"knowledge_graph"#"knowledge_graph_type": "Local nav"`
* `"knowledge_graph"#"knowledge_graph_type": "Shopping entity"`
* `"knowledge_graph"#"knowledge_graph_type": "Company"`
* `"answer_box"#"type": "organic_result"`
* `"sports_results"#"type": "team_sport"`
* `"answer_box"#"type": "open_hours"`
* `"answer_box"#"type": "population_graph"`
* `"answer_box"#"type": "finance_results"`
* `"answer_box"#"type": "currency_converter"`
* `"answer_box"#"type": "local_time"`
* `"weather_result"`
* `"local_map"`
* `"job_results"`
* `"salary_estimates"`
* `"shopping_ads"`
* `"ads"`
* `"local_ads"`
* `"inline_shopping"`
* `"from_sources_across_the_web"`
* `"discussions_and_forums"`
* `"related_questions"`
* `"questions_and_answers"`
* `"explore_brands"`
* `"about_this_result"`
* `"things_to_do"`
* `"events"`
* `"scholarly_articles"`
* `"inline_videos"`
* `"inline_tweets"`
* `"inline_images"`
* `"inline_recipes"`
* `"top_stories"`
* `"courses"`

Since the task here is to "Integrate [SearchApi](https://www.searchapi.io/) as a [`WebSearchEngine`](https://github.com/langchain4j/langchain4j/blob/57e3185765c80c2975b3b91fce8626cf6d3ad0de/langchain4j-core/src/main/java/dev/langchain4j/web/search/WebSearchEngine.java) and as a [`Tool`](https://github.com/langchain4j/langchain4j/blob/57e3185765c80c2975b3b91fce8626cf6d3ad0de/langchain4j-core/src/main/java/dev/langchain4j/web/search/WebSearchTool.java) for function calling", the common format between the two classes is the [`WebSearchResults`](https://github.com/langchain4j/langchain4j/blob/57e3185765c80c2975b3b91fce8626cf6d3ad0de/langchain4j-core/src/main/java/dev/langchain4j/web/search/WebSearchResults.java) object.

The `WebSearchResults` object in turn is hydrated from 3 things from a search:
> "list of organic search results, information about the search, and pagination information"

Can you clarify which of the JSON elements are to scope and which ones are to be skipped for this issue?

Hey @mark-searchapi, looks like the `num` parameter for pagination is not working properly, the endpoint always returns 4 results if the parameter 5 is present, could you take a look? This happens when using the query "LangChain4j", which is being tested in WebSearchEngineIT
how many tests should one write for this?
and also is validation of parameters based on selected 'engine' required?
@ayewo I have reviewed the Langchain4j codebase and can provide the following insights:

## WebSearchEngine as ContentRetriever Integration (for RAG use case)

After https://github.com/langchain4j/langchain4j/pull/642 PR, the core implementation seems to provide:

- Swappable providers (Google Search, SearchApi, Bing and others)
- Automatic integration with ContentRetriever
- Best for RAG use cases

Cons:
- Very basic integration as a tool (bare minimum MVP)

I think the most important part of [WebSearchResults](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/main/java/dev/langchain4j/web/search/WebSearchResults.java) is [WebSearchOrganicResult](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/main/java/dev/langchain4j/web/search/WebSearchOrganicResult.java#L24). To construct it, we iterate through the `organic_results` and take `title`, `snippet` and `link`. In this particular use case, none of the other fields seem to matter. 

`SearchApiWebSearchEngine` should allow configuring the `engine` to easily swap between Google, Bing, Google News, Baidu (we will release this week) and later other search engines. The JSON structure won't change here and will always stay the same on SearchApi side as:

```json
{
  "organic_results": [
    {
      "title": "String",
      "link": "String",
      "snippet": "String",
      ...
    },
    ...
  ]  
}
```

If you need error handling, rely on the `error` JSON key presence in the response.

After implementing SearchApi as a core WebSearchEngine, the basic tool should work out of the box:

```java
 googleSearchApi = SearchApiWebSearchEngine.builder()
                .apiKey(System.getenv("SEARCHAPI_API_KEY"))
                .engine("google")
  WebSearchTool webSearchTool = WebSearchTool.from(googleSearchEngine);
...
```

## Integration as a Tool (Function calling)

`WebSearchTool` is built into the langchain4j core and as mentioned should work outside the box.

However, it does not seem to be very useful when used as a function since it constructs the String for LLMs only based on `organic_results` as seen [here](https://github.com/langchain4j/langchain4j/blob/main/langchain4j-core/src/main/java/dev/langchain4j/web/search/WebSearchTool.java#L30-L37).

I think we should create independent tool definitions and start with a couple of engines for this issue:
- GoogleSearchTool
- GoogleNewsTool
- YoutubeTranscriptsTool
- Your selected engine that might be useful with LLM

This way we can control the final string that is being built. For instance, in youtube_transcripts engine, we are interested only in [transcript text](https://github.com/langgenius/dify/blob/d058a234ba76acaa457f3da10b4dd13daed6ca82/api/core/tools/provider/builtin/searchapi/tools/youtube_transcripts.py#L44-L56). You can also check sample [Google Search string construction](https://github.com/langgenius/dify/blob/d058a234ba76acaa457f3da10b4dd13daed6ca82/api/core/tools/provider/builtin/searchapi/tools/google.py#L44-L61) and [Google News](https://github.com/langgenius/dify/blob/d058a234ba76acaa457f3da10b4dd13daed6ca82/api/core/tools/provider/builtin/searchapi/tools/google_news.py#L49-L58) example in our recent other integration. You can also add extra fields to the tools that you think might be important for LLMs.

## Documentation and Examples

Throwing some ideas:

- RAG use case with Search: How to use SearchApiWebSearchEngine + ContentRetriever + LLM.
- Summarization: SearchApi YoutubeTranscripts tool + TokenSplitter + LLM. Great for YouTube podcasts.
- Blog post writing: SearchApi Google Search Tool + Content retriever + LLM.

Most of the stuff (apart custom tools) is already implemented in [langchain4j Google Custom Search directory](https://github.com/langchain4j/langchain4j/tree/57e3185765c80c2975b3b91fce8626cf6d3ad0de/web-search-engines/langchain4j-web-search-engine-google-custom/src). Tests contains hints on how to implement examples for documentation (how to use web search engine, content retriever, tool, etc..). 

Hope the above information helps!
@mark-searchapi I opened a PR with the idea of adding new search api engines in the future using an interface that handles the requests and responses
> Hey @mark-searchapi, looks like the num parameter for pagination is not working properly, the endpoint always returns 4 results if the parameter 5 is present, could you take a look? This happens when using the query "LangChain4j", which is being tested in WebSearchEngineIT

@zambrinf, you can try using the default num parameter and verify that the total amount of organic results is greater than 0. This issue happens with the "LangChain4j" query because Google counts different elements toward the num parameter. WebSearchEngine results only consider organic results, so elements like inline videos are not included as organic results.
> > Hey @mark-searchapi, looks like the num parameter for pagination is not working properly, the endpoint always returns 4 results if the parameter 5 is present, could you take a look? This happens when using the query "LangChain4j", which is being tested in WebSearchEngineIT
> 
> @zambrinf, you can try using the default num parameter and verify that the total amount of organic results is greater than 0. This issue happens with the "LangChain4j" query because Google counts different elements toward the num parameter. WebSearchEngine results only consider organic results, so elements like inline videos are not included as organic results.

@mark-searchapi I’ve already implemented it as you described in my PR. Please have a look at #1215.



Fix (patch):
diff --git a/docs/docs/integrations/web-search/_category_.json b/docs/docs/integrations/web-search/_category_.json
new file mode 100644
index 00000000000..1402f0821f0
--- /dev/null
+++ b/docs/docs/integrations/web-search/_category_.json
@@ -0,0 +1,8 @@
+{
+  "label": "Web Search",
+  "position": 19,
+  "link": {
+    "type": "generated-index",
+    "description": "Web Search"
+  }
+}
diff --git a/docs/docs/integrations/web-search/searchapi.md b/docs/docs/integrations/web-search/searchapi.md
new file mode 100644
index 00000000000..101d1937dd7
--- /dev/null
+++ b/docs/docs/integrations/web-search/searchapi.md
@@ -0,0 +1,109 @@
+---
+sidebar_position: 1
+---
+
+# SearchApi
+
+[SearchApi](https://www.searchapi.io/) is a real-time SERP API. You can use it to perform searches in Google, Google News, Bing, Bing News, Baidu, Google Scholar, or any other engine that returns organic results.
+
+## Usage
+
+### Dependencies setup
+
+Add the following dependencies to your project's `pom.xml`:
+```xml
+<dependency>
+  <groupId>dev.langchain4j</groupId>
+  <artifactId>langchain4j-web-search-engine-searchapi</artifactId>
+  <version>{your-version}</version> <!-- Specify langchain4j version here -->
+</dependency>
+```
+
+or project's `build.gradle`:
+
+```groovy
+implementation 'dev.langchain4j:langchain4j-web-search-engine-searchapi:{your-version}'
+```
+
+### Example code:
+
+```java
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import dev.langchain4j.model.chat.ChatLanguageModel;
+import dev.langchain4j.model.openai.OpenAiChatModel;
+import dev.langchain4j.model.openai.OpenAiChatModelName;
+import dev.langchain4j.service.AiServices;
+import dev.langchain4j.web.search.WebSearchTool;
+import dev.langchain4j.web.search.searchapi.SearchApiEngine;
+import dev.langchain4j.web.search.searchapi.SearchApiWebSearchEngine;
+
+public class SearchApiTool {
+
+    interface Assistant {
+        @dev.langchain4j.service.SystemMessage({
+                "You are a web search support agent.",
+                "If there is any event that has not happened yet",
+                "You MUST create a web search request with user query and",
+                "use the web search tool to search the web for organic web results.",
+                "Include the source link in your final response."
+        })
+        String answer(String userMessage);
+    }
+
+    private static final String SEARCHAPI_API_KEY = "YOUR_SEARCHAPI_KEY";
+    private static final String OPENAI_API_KEY = "YOUR_OPENAI_KEY";
+
+    public static void main(String[] args) {
+        Map<String, Object> optionalParameters = new HashMap<>();
+        optionalParameters.put("gl", "us");
+        optionalParameters.put("hl", "en");
+        optionalParameters.put("google_domain", "google.com");
+        
+        SearchApiWebSearchEngine searchEngine = SearchApiWebSearchEngine.builder()
+                .apiKey(SEARCHAPI_API_KEY)
+                .engine("google")
+                .optionalParameters(optionalParameters)
+                .build();
+        ChatLanguageModel chatModel = OpenAiChatModel.builder()
+                .apiKey(OPENAI_API_KEY)
+                .modelName(OpenAiChatModelName.GPT_3_5_TURBO)
+                .logRequests(true)
+                .build();
+
+        WebSearchTool webTool = WebSearchTool.from(searchEngine);
+
+        Assistant assistant = AiServices.builder(Assistant.class)
+                .chatLanguageModel(chatModel)
+                .tools(webTool)
+                .build();
+
+        String answer = assistant.answer("My family is coming to visit me in Madrid next week, list the best tourist activities suitable for the whole family");
+        System.out.println(answer);
+        /*
+            Here are some of the best tourist activities suitable for the whole family in Madrid:
+            
+            1. **Parque del Retiro** - A beautiful public park where families can enjoy nature and various activities.
+            2. **Prado Museum** - A renowned art museum that can be fascinating for both adults and children.
+            3. **Mercado de San Miguel** - A market where you can explore and taste delicious Spanish food.
+            4. **Royal Palace** - Explore the grandeur of the Royal Palace of Madrid.
+            5. **Plaza Mayor** and **Puerta del Sol** - Historic squares with a vibrant atmosphere.
+            6. **Santiago Bernabeu Stadium** - Perfect for sports enthusiasts and soccer fans.
+            7. **Gran Via** - A famous street for shopping, entertainment, and sightseeing.
+            8. **National Archaeological Museum** - Discover Spain's rich history through archaeological artifacts.
+            9. **Templo de Debod** - An ancient Egyptian temple in the heart of Madrid.
+         */
+    }
+}
+```
+
+### Available engines in Langchain4j
+
+| SearchApi Engine                                          | Available |
+|-----------------------------------------------------------|-----------|
+| [Google Web Search](https://www.searchapi.io/docs/google) | ✅         |
+| [Google News](https://www.searchapi.io/docs/google-news)  | ✅         |
+| [Bing](https://www.searchapi.io/docs/bing)                | ✅         |
+| [Bing News](https://www.searchapi.io/docs/bing-news)      | ✅         |
+| [Baidu](https://www.searchapi.io/docs/baidu)              | ✅         |
+
+Any other engine that returns the `organic_results` array and the organic result has `title`, `link`, and `snippet` is supported by this library even if not listed above.
diff --git a/langchain4j-bom/pom.xml b/langchain4j-bom/pom.xml
index fae993c7d6b..b612752c1e5 100644
--- a/langchain4j-bom/pom.xml
+++ b/langchain4j-bom/pom.xml
@@ -423,6 +423,16 @@
                 <artifactId>langchain4j-web-search-engine-google-custom</artifactId>
                 <version>${project.version}</version>
             </dependency>
+            <dependency>
+                <groupId>dev.langchain4j</groupId>
+                <artifactId>langchain4j-web-search-engine-tavily</artifactId>
+                <version>${project.version}</version>
+            </dependency>
+            <dependency>
+                <groupId>dev.langchain4j</groupId>
+                <artifactId>langchain4j-web-search-engine-searchapi</artifactId>
+                <version>${project.version}</version>
+            </dependency>
 
             <!-- experimental -->
             <dependency>
diff --git a/pom.xml b/pom.xml
index 8d14fc27be8..4167044b418 100644
--- a/pom.xml
+++ b/pom.xml
@@ -81,6 +81,7 @@
         <!-- web search engines -->
         <module>web-search-engines/langchain4j-web-search-engine-google-custom</module>
         <module>web-search-engines/langchain4j-web-search-engine-tavily</module>
+        <module>web-search-engines/langchain4j-web-search-engine-searchapi</module>
 
         <!-- embedding store filter parsers -->
         <module>embedding-store-filter-parsers/langchain4j-embedding-store-filter-parser-sql</module>
diff --git a/web-search-engines/langchain4j-web-search-engine-searchapi/pom.xml b/web-search-engines/langchain4j-web-search-engine-searchapi/pom.xml
new file mode 100644
index 00000000000..749a439cd90
--- /dev/null
+++ b/web-search-engines/langchain4j-web-search-engine-searchapi/pom.xml
@@ -0,0 +1,94 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+    <modelVersion>4.0.0</modelVersion>
+
+    <parent>
+        <groupId>dev.langchain4j</groupId>
+        <artifactId>langchain4j-parent</artifactId>
+        <version>0.34.0-SNAPSHOT</version>
+        <relativePath>../../langchain4j-parent/pom.xml</relativePath>
+    </parent>
+
+    <artifactId>langchain4j-web-search-engine-searchapi</artifactId>
+    <packaging>jar</packaging>
+
+    <name>LangChain4j :: Web Search Engine :: SearchApi</name>
+
+    <dependencies>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-core</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.retrofit2</groupId>
+            <artifactId>retrofit</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.retrofit2</groupId>
+            <artifactId>converter-jackson</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>com.fasterxml.jackson.core</groupId>
+            <artifactId>jackson-databind</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>com.squareup.okhttp3</groupId>
+            <artifactId>okhttp</artifactId>
+        </dependency>
+
+        <dependency>
+            <groupId>org.projectlombok</groupId>
+            <artifactId>lombok</artifactId>
+            <scope>provided</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.junit.jupiter</groupId>
+            <artifactId>junit-jupiter</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>org.assertj</groupId>
+            <artifactId>assertj-core</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j</artifactId>
+            <scope>test</scope>
+        </dependency>
+
+        <!-- Visibility for WebSearchEngineIT -->
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-core</artifactId>
+            <classifier>tests</classifier>
+            <type>test-jar</type>
+            <scope>test</scope>
+        </dependency>
+
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-open-ai</artifactId>
+            <scope>test</scope>
+        </dependency>
+    </dependencies>
+
+    <licenses>
+        <license>
+            <name>Apache-2.0</name>
+            <url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
+            <distribution>repo</distribution>
+            <comments>A business-friendly OSS license</comments>
+        </license>
+    </licenses>
+
+</project>
\ No newline at end of file
diff --git a/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApi.java b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApi.java
new file mode 100644
index 00000000000..c9d0c56a9e1
--- /dev/null
+++ b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApi.java
@@ -0,0 +1,15 @@
+package dev.langchain4j.web.search.searchapi;
+
+import retrofit2.Call;
+import retrofit2.http.GET;
+import retrofit2.http.Header;
+import retrofit2.http.QueryMap;
+
+import java.util.Map;
+
+interface SearchApi {
+
+    @GET("/api/v1/search")
+    Call<SearchApiWebSearchResponse> search(@QueryMap Map<String, Object> params,
+                                            @Header("Authorization") String bearerToken);
+}
diff --git a/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiClient.java b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiClient.java
new file mode 100644
index 00000000000..53272f007cd
--- /dev/null
+++ b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiClient.java
@@ -0,0 +1,75 @@
+package dev.langchain4j.web.search.searchapi;
+
+import com.fasterxml.jackson.databind.ObjectMapper;
+import lombok.Builder;
+import okhttp3.OkHttpClient;
+import okhttp3.ResponseBody;
+import retrofit2.Response;
+import retrofit2.Retrofit;
+import retrofit2.converter.jackson.JacksonConverterFactory;
+
+import java.io.IOException;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.Map;
+
+import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotNull;
+
+class SearchApiClient {
+
+    private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper().enable(INDENT_OUTPUT);
+
+    private final SearchApi api;
+
+    @Builder
+    SearchApiClient(Duration timeout, String baseUrl) {
+        ensureNotNull(timeout, "timeout");
+        OkHttpClient.Builder okHttpClientBuilder = new OkHttpClient.Builder()
+                .callTimeout(timeout)
+                .connectTimeout(timeout)
+                .readTimeout(timeout)
+                .writeTimeout(timeout);
+        Retrofit retrofit = new Retrofit.Builder()
+                .baseUrl(ensureNotBlank(baseUrl, "baseUrl"))
+                .client(okHttpClientBuilder.build())
+                .addConverterFactory(JacksonConverterFactory.create(OBJECT_MAPPER))
+                .build();
+        this.api = retrofit.create(SearchApi.class);
+    }
+
+    SearchApiWebSearchResponse search(SearchApiWebSearchRequest request) {
+        Map<String, Object> finalParameters = new HashMap<>(request.getFinalOptionalParameters());
+        finalParameters.put("engine", request.getEngine());
+        finalParameters.put("q", request.getQuery());
+        String bearerToken = "Bearer " + request.getApiKey();
+        try {
+            Response<SearchApiWebSearchResponse> response = api.search(finalParameters, bearerToken).execute();
+            return getBody(response);
+        } catch (IOException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private SearchApiWebSearchResponse getBody(Response<SearchApiWebSearchResponse> response) throws IOException {
+        if (response.isSuccessful()) {
+            return response.body();
+        } else {
+            throw toException(response);
+        }
+    }
+
+    private static RuntimeException toException(Response<?> response) throws IOException {
+        try (ResponseBody responseBody = response.errorBody()) {
+            int code = response.code();
+            if (responseBody != null) {
+                String body = responseBody.string();
+                String errorMessage = String.format("status code: %s; body: %s", code, body);
+                return new RuntimeException(errorMessage);
+            } else {
+                return new RuntimeException(String.format("status code: %s;", code));
+            }
+        }
+    }
+}
diff --git a/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchEngine.java b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchEngine.java
new file mode 100644
index 00000000000..e7b881fe4e0
--- /dev/null
+++ b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchEngine.java
@@ -0,0 +1,138 @@
+package dev.langchain4j.web.search.searchapi;
+
+import dev.langchain4j.web.search.WebSearchEngine;
+import dev.langchain4j.web.search.WebSearchInformationResult;
+import dev.langchain4j.web.search.WebSearchOrganicResult;
+import dev.langchain4j.web.search.WebSearchRequest;
+import dev.langchain4j.web.search.WebSearchResults;
+import lombok.Builder;
+
+import java.net.URI;
+import java.time.Duration;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static dev.langchain4j.internal.Utils.getOrDefault;
+import static dev.langchain4j.internal.ValidationUtils.ensureNotBlank;
+import static java.time.Duration.ofSeconds;
+
+/**
+ * An implementation of a {@link WebSearchEngine} that uses
+ * <a href="https://www.searchapi.io">Search API</a> for performing web searches. <p>
+ * Search API supports not only Google Search, but many other engines
+ */
+public class SearchApiWebSearchEngine implements WebSearchEngine {
+
+    private static final String BASE_URL = "https://www.searchapi.io";
+    private static final String DEFAULT_ENGINE = "google";
+
+    private final String apiKey;
+    private final String engine;
+    private final SearchApiClient client;
+    private final Map<String, Object> optionalParameters;
+
+    /**
+     * @param apiKey             Required - the Search API key for accessing their API
+     * @param baseUrl            overrides the default SearchApi base url
+     * @param timeout            the timeout duration for API requests
+     *                           <p>
+     *                           Default value is 30 seconds.
+     * @param engine             the engine used by Search API to execute the search
+     *                           <p>
+     *                           Default engine is Google Search.
+     * @param optionalParameters parameters to be passed on every request of this the engine, they can be overridden by the WebSearchRequest additional parameters for matching keys
+     *                           <p>
+     *                           Check <a href="https://www.searchapi.io">Search API</a> for more information on available parameters for each engine
+     */
+    @Builder
+    public SearchApiWebSearchEngine(String apiKey,
+                                    String baseUrl,
+                                    Duration timeout,
+                                    String engine,
+                                    Map<String, Object> optionalParameters) {
+        this.apiKey = ensureNotBlank(apiKey, "apiKey");
+        this.engine = getOrDefault(engine, DEFAULT_ENGINE);
+        this.optionalParameters = getOrDefault(optionalParameters, new HashMap<>());
+        this.client = SearchApiClient.builder()
+                .timeout(getOrDefault(timeout, ofSeconds(30)))
+                .baseUrl(getOrDefault(baseUrl, BASE_URL))
+                .build();
+    }
+
+    /**
+     * @param webSearchRequest Check <a href="https://www.searchapi.io">Search API</a> for more information on available additional
+     *                         parameters for each engine that can be inside the request
+     */
+    @Override
+    public WebSearchResults search(WebSearchRequest webSearchRequest) {
+        SearchApiWebSearchRequest request = SearchApiWebSearchRequest.builder()
+                .apiKey(apiKey)
+                .engine(engine)
+                .query(webSearchRequest.searchTerms())
+                .optionalParameters(optionalParameters)
+                .additionalRequestParameters(webSearchRequest.additionalParams())
+                .build();
+        SearchApiWebSearchResponse response = client.search(request);
+        return toWebSearchResults(response);
+    }
+
+    private WebSearchResults toWebSearchResults(SearchApiWebSearchResponse response) {
+        List<OrganicResult> organicResults = response.getOrganicResults();
+        Long totalResults = getTotalResults(response.getSearchInformation());
+        WebSearchInformationResult searchInformation = WebSearchInformationResult.from(
+                totalResults,
+                getCurrentPage(response.getPagination()),
+                null
+        );
+        Map<String, Object> searchMetadata = getOrDefault(response.getSearchParameters(), new HashMap<>());
+        addToMetadata(searchMetadata, response.getSearchMetadata());
+        return WebSearchResults.from(
+                searchMetadata,
+                searchInformation,
+                toWebSearchOrganicResults(organicResults));
+    }
+
+    private static long getTotalResults(Map<String, Object> searchInformation) {
+        if (searchInformation != null && searchInformation.containsKey("total_results")) {
+            Object totalResults = searchInformation.get("total_results");
+            return totalResults instanceof Integer
+                    ? ((Integer) totalResults).longValue()
+                    : (Long) totalResults; // changes depending on the amount of total_results
+        }
+        return 0;
+    }
+
+    private Integer getCurrentPage(Map<String, Object> pagination) {
+        if (pagination != null && pagination.containsKey("current")) {
+            return (Integer) pagination.get("current");
+        }
+        return null;
+    }
+
+    private void addToMetadata(Map<String, Object> metadata, Map<String, Object> dataToAdd) {
+        if (dataToAdd != null) {
+            metadata.putAll(dataToAdd);
+        }
+    }
+
+    private List<WebSearchOrganicResult> toWebSearchOrganicResults(List<OrganicResult> organicResults) {
+        return organicResults.stream()
+                .map(result -> {
+                    Map<String, String> metadata = new HashMap<>(2);
+                    metadata.put("position", result.getPosition());
+                    return WebSearchOrganicResult.from(
+                            result.getTitle(),
+                            URI.create(result.getLink()),
+                            getOrDefault(result.getSnippet(), ""),
+                            null,  // by default google custom search api does not return content
+                            metadata);
+                })
+                .collect(Collectors.toList());
+    }
+
+    public static WebSearchEngine withApiKey(String apiKey) {
+        return builder().apiKey(apiKey).build();
+    }
+}
diff --git a/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchRequest.java b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchRequest.java
new file mode 100644
index 00000000000..6f124d93d8a
--- /dev/null
+++ b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchRequest.java
@@ -0,0 +1,33 @@
+package dev.langchain4j.web.search.searchapi;
+
+import lombok.Builder;
+import lombok.Getter;
+
+import java.util.Map;
+
+@Getter
+class SearchApiWebSearchRequest {
+
+    private final String engine;
+    private final String apiKey;
+    private final String query;
+    private final Map<String, Object> finalOptionalParameters;
+
+    /**
+     * @param additionalRequestParameters overrides optionalParameters for matching keys
+     */
+    @Builder
+    SearchApiWebSearchRequest(String engine,
+                              String apiKey,
+                              String query,
+                              Map<String, Object> optionalParameters,
+                              Map<String, Object> additionalRequestParameters) {
+        this.engine = engine;
+        this.apiKey = apiKey;
+        this.query = query;
+        this.finalOptionalParameters = optionalParameters;
+        if (additionalRequestParameters != null) {
+            finalOptionalParameters.putAll(additionalRequestParameters);
+        }
+    }
+}
diff --git a/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchResponse.java b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchResponse.java
new file mode 100644
index 00000000000..1de13565f82
--- /dev/null
+++ b/web-search-engines/langchain4j-web-search-engine-searchapi/src/main/java/dev/langchain4j/web/search/searchapi/SearchApiWebSearchResponse.java
@@ -0,0 +1,55 @@
+package dev.langchain4j.web.search.searchapi;
+
+import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
+import com.fasterxml.jackson.databind.PropertyNamingStrategies;
+import com.fasterxml.jackson.databind.annotation.JsonNaming;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+import java.util.List;
+import java.util.Map;
+
+@Getter
+@NoArgsConstructor
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+@JsonIgnoreProperties(ignoreUnknown = true)
+class SearchApiWebSearchResponse {
+
+    private List<OrganicResult> organicResults;
+
+    /**
+     * Always present in a successful response
+     */
+    private Map<String, Object> searchParameters;
+
+    /**
+     * May or may not be present
+     */
+    private Map<String, Object> pagination;
+
+    /**
+     * May or may not be present
+     */
+    private Map<String, Object> searchInformation;
+
+    /**
+     * May or may not be present
+     */
+    private Map<String, Object> searchMetadata;
+}
+
+@Getter
+@NoArgsConstructor
+@JsonNaming(PropertyNamingStrategies.SnakeCaseStrategy.class)
+@JsonIgnoreProperties(ignoreUnknown = true)
+class OrganicResult {
+
+    private String title;
+    private String link;
+    private String position;
+
+    /**
+     * May or may not be present
+     */
+    private String snippet;
+}
\ No newline at end of file