Using the Brave Search API

You need to sign up for a free or paid account on the Brave search page and set an environment variable to your assigned API key:

export BRAVE_SEARCH_API_KEY = BSAgQ-Nc5.....

The Brave Search API allows you to access Brave Search results directly within your applications or services. It provides developers with the ability to harness the privacy-focused and independent search capabilities of Brave, returning results for web searches, news, videos, and more. To obtain an API key, simply create an account on the Brave Search API website and subscribe to either the free or one of the paid plans. The Brave Search API offers flexible pricing tiers, including a free option for testing and development, making it accessible to a wide range of users and projects. Currently you can call the API 2000 times a month on the free tier.

Brave Search API Client Architecture
Figure 14. Brave Search API Client Architecture

The library developed in this chapter is implemented in a single file BraveSearch.hs:

  1 {-# LANGUAGE OverloadedStrings #-}
  2 {-# LANGUAGE RecordWildCards #-}
  3 -- Module: BraveSearch
  4 -- Minimal client for the Brave Search API; exposes `getSearchSuggestions`.
  5 -- Uses OverloadedStrings for convenient Text literals and RecordWildCards for concise pattern binding.
  6 
  7 module BraveSearch
  8   ( getSearchSuggestions
  9   ) where
 10 
 11 import Network.HTTP.Simple -- HTTP request/response helpers (parseRequest, setRequestHeader, httpLBS)
 12 import Data.Text.Encoding (encodeUtf8) -- convert Text -> ByteString for query params
 13 import Data.Aeson -- FromJSON and decoding (eitherDecode, (.:), (.:?))
 14 import qualified Data.Text as T -- strict Text type
 15 import Control.Exception (try) -- catch exceptions and return Either
 16 import Network.HTTP.Client (HttpException) -- HTTP error type
 17 import qualified Data.ByteString.Char8 as BS -- UTF-8 ByteString for headers
 18 
 19 
 20 -- | Top-level response from the Brave Search API, containing the
 21 -- original query information and web search results.
 22 data SearchResponse = SearchResponse
 23   { query :: QueryInfo
 24   , web :: WebResults
 25   } deriving (Show)
 26 
 27 -- | Metadata about the original query as echoed back by the API.
 28 data QueryInfo = QueryInfo
 29   { original :: T.Text
 30   } deriving (Show)
 31 
 32 -- | Container wrapping the list of individual web results returned by the API.
 33 data WebResults = WebResults
 34   { results :: [WebResult]
 35   } deriving (Show)
 36 
 37 -- | A single web search result. Several fields are optional ('Maybe')
 38 -- because the API may omit them depending on the result type.
 39 data WebResult = WebResult
 40   { type_ :: T.Text
 41   , index :: Maybe Int
 42   , all :: Maybe Bool
 43   , title :: Maybe T.Text
 44   , url :: Maybe T.Text
 45   , description :: Maybe T.Text
 46   } deriving (Show)
 47 
 48 -- JSON decoders mapping API fields to our Haskell types
 49 instance FromJSON SearchResponse where
 50   parseJSON = withObject "SearchResponse" $ \v -> SearchResponse
 51     <$> v .: "query"
 52     <*> v .: "web"
 53 
 54 instance FromJSON QueryInfo where
 55   parseJSON = withObject "QueryInfo" $ \v -> QueryInfo
 56     <$> v .: "original"
 57 
 58 instance FromJSON WebResults where
 59   parseJSON = withObject "WebResults" $ \v -> WebResults
 60     <$> v .: "results"
 61 
 62 -- Use (.:) for required fields and (.:?) for optional ones
 63 instance FromJSON WebResult where
 64   parseJSON = withObject "WebResult" $ \v -> WebResult
 65     <$> v .: "type"
 66     <*> v .:? "index"
 67     <*> v .:? "all"
 68     <*> v .:? "title"
 69     <*> v .:? "url"
 70     <*> v .:? "description"
 71 
 72 -- | Perform a Brave Search with the given API key (as raw bytes) and text query.
 73 getSearchSuggestions :: BS.ByteString -> T.Text -> IO (Either T.Text [T.Text])
 74 getSearchSuggestions apiKey query = do
 75   -- Build base request
 76   let baseUrl = "https://api.search.brave.com/res/v1/web/search"
 77   request0 <- parseRequest baseUrl
 78   -- Add query parameters (URL-encoded) and headers
 79   let request1 = setRequestQueryString
 80                    [ ("q", Just $ encodeUtf8 query)
 81                    , ("country", Just "US")
 82                    , ("count", Just "5")
 83                    ]
 84                    request0
 85       request  = setRequestHeader "Accept" ["application/json"]
 86                $ setRequestHeader "X-Subscription-Token" [apiKey]
 87                $ request1
 88 
 89   -- Run the request and catch exceptions as Either
 90   result <- try $ httpLBS request
 91 
 92   -- Unwrap the result and handle errors (network, non-200 status, JSON)
 93   case result of
 94     Left e -> return . Left $ T.pack $ "Network error: " ++ show (e :: HttpException)
 95     Right response ->
 96       let status = getResponseStatusCode response
 97       in if status /= 200
 98            then return . Left $ T.pack $ "HTTP error: " ++ show status
 99            else case eitherDecode (getResponseBody response) of
100                   Left err -> return . Left $ T.pack $ "JSON parsing error: " ++ err
101                   Right SearchResponse{..} ->
102                     let originalQuery = original query
103                         webResults    = results web
104                         suggestions   = ("Original Query: " <> originalQuery)
105                                       : map formatResult webResults
106                     in return $ Right suggestions
107 
108 -- Format a single WebResult into a readable line
109 formatResult :: WebResult -> T.Text
110 formatResult WebResult{..} =
111   let titleText = maybe "N/A" ("Title: " <>) title
112       urlText = maybe "N/A" ("URL: " <>) url
113       descText = maybe "N/A" ("Description: " <>) (fmap (T.take 100) description) -- truncate description
114   in T.intercalate " | " [titleText, urlText, descText]

Haskell Code Description for Brave Search Suggestions

This Haskell code implements a function, getSearchSuggestions, that fetches search suggestions from the Brave Search API.

Functionality:

  • getSearchSuggestions:
    • Takes an API key and a search query as input.
    • Constructs a URL to send a request to the Brave Search API, specifying the query, country, and result count.
    • Sets up the HTTP request with necessary headers, including the API key.
    • Makes the request and handles potential network errors.
    • Checks the response status code. If it’s 200 (OK), proceeds to parse the JSON response.
    • Extracts search results and formats them, including the original query.
    • Returns either an error message (if something went wrong) or a list of formatted search suggestions.

Key Features:

  • Data Types:
    • Defines data types to model the JSON structure of the Brave Search API response, including SearchResponse, QueryInfo, WebResults, and WebResult.
  • JSON Parsing:
    • Uses the aeson library to parse the JSON response into the defined data types.
  • Error Handling:
    • Employs try from the Control.Exception module to gracefully handle potential network errors during the HTTP request.
  • HTTP Request:
    • Utilizes the Network.HTTP.Simple library to make the HTTP request to the Brave Search API.
  • Formatting:
    • The formatResult function formats each search result into a user-friendly string, including the title, URL, and a shortened description.

Libraries Used:

  • Network.HTTP.Simple - For making HTTP requests.
  • Data.Aeson - For JSON parsing and encoding.
  • Data.Text - For efficient text handling.
  • Control.Exception - For error handling.
  • Network.HTTP.Client - For additional HTTP functionalities.
  • Data.ByteString.Char8 - For working with byte strings.

Language Extensions:

  • OverloadedStrings - Allows the use of string literals as Text values.
  • RecordWildCards - Enables convenient access to record fields using wildcards.

Overall:

This code provides a basic but functional way to interact with the Brave Search API to retrieve and format search suggestions. It demonstrates good practices in Haskell programming, including data modeling, error handling, and the use of relevant libraries.

Here is an example Main.hs file to use this library:

 1 {-# LANGUAGE OverloadedStrings #-}
 2 -- Allows string literals like "foo" to be used as `Text`
 3 module Main where
 4 
 5 import BraveSearch (getSearchSuggestions)
 6 import qualified Data.ByteString.Char8 as BS
 7 import System.Environment (lookupEnv)
 8 import qualified Data.Text as T
 9 import qualified Data.Text.IO as TIO
10 
11 -- Entry point: runs an interactive search
12 main :: IO ()
13 main = do
14   -- Read API key from environment; fail with a descriptive message if unset
15   maybeKey <- lookupEnv "BRAVE_SEARCH_API_KEY"
16   case maybeKey of
17     Nothing -> TIO.putStrLn "Error: BRAVE_SEARCH_API_KEY environment variable is not set. Please set it to your Brave Search API key."
18     Just apiKeyRaw -> do
19       let apiKey = BS.pack apiKeyRaw
20 
21       -- Prompt the user for a search query
22       TIO.putStrLn "Enter a search query:"
23       query <- TIO.getLine
24 
25       -- Call the function to get search suggestions
26       result <- getSearchSuggestions apiKey query
27 
28       -- Handle `Either`: Left is an error, Right is a list of suggestion lines
29       case result of
30         Left err -> TIO.putStrLn $ "Error: " <> err
31         Right suggestions -> do
32           TIO.putStrLn "Search suggestions:"
33           mapM_ (TIO.putStrLn . ("- " <>)) suggestions -- print each suggestion

Test Code Explanation

The code interacts with the BraveSearch module to demonstrate how to fetch and display search suggestions from the Brave Search API.

Code Breakdown

  1. Imports

    • It imports:
      • BraveSearch to use the getSearchSuggestions function.
      • System.Environment to get the API key from an environment variable.
      • Data.Text and Data.Text.IO for working with text input and output.
  2. main Function

    1. Get API Key

      • It uses lookupEnv "BRAVE_SEARCH_API_KEY" to safely look up the Brave Search API key from an environment variable named BRAVE_SEARCH_API_KEY. If the variable is not set, it prints a descriptive error message and exits. Otherwise, it proceeds with the key.
    2. Prompt for Query

      • It prints the message “Enter a search query:” to the console, prompting the user to input a search term.
      • It reads the user’s input using TIO.getLine and stores it in the query variable.
    3. Fetch Search Suggestions

      • It calls the getSearchSuggestions function from the BraveSearch module, passing the API key and the user’s query.
      • It stores the result of this call in the result variable.
    4. Handle Result

      • It uses a case expression to handle the two possible outcomes of the getSearchSuggestions call:

        • Left err:

          • If there was an error (e.g., network issue, HTTP error, JSON parsing error), it prints the error message prefixed with “Error:”.
        • Right suggestions:

          • If the call was successful and returned a list of search suggestions:
            • Prints “Search suggestions:” to the console.
            • Uses mapM_ to iterate over the suggestions list and print each suggestion in the following format: - suggestion text

This test code provides a basic example of how to use the getSearchSuggestions function from the BraveSearch module.

Here is the output:

$ cabal run     
Enter a search query:
find a consultant for AI and common lisp, and the semantic web
Search suggestions:
- Original Query: find a consultant for AI and common lisp, and the semantic web
- Title: Mark Watson: AI Practitioner and Lisp Hacker | URL: https://markwatson.com/ | Description: I am the author of 20+ books on Artificial Intelligence, <strong>Common</strong> <strong>Lisp</stron
- Title: Lisp (programming language) - Wikipedia | URL: https://en.wikipedia.org/wiki/Lisp_(programming_language) | Description: Scheme is a statically scoped and properly tail-recursive dialect of <strong>the</strong> <strong>Li
- Title: The Lisp approach to AI (Part 1). If you are a programmer that reads… | by Sebastian Valencia | AI Society | Medium | URL: https://medium.com/ai-society/the-lisp-approach-to-ai-part-1-a48c7385a913 | Description: If you are a programmer that reads about the history and random facts of this lovely craft