Skip to content
Start free

SDKs

Browser SDK

@findlane/browser searches an index from the browser with its public ID. It has no dependencies and is about 1 KB.

Install

npm install @findlane/browser

Using React? @findlane/browser/react has a search box, a results page with filters, and hooks, in the same package.

Or, without a build step, load it with a script tag. It defines a global Findlane, so use new Findlane.SearchBrowserClient(...) instead of the import below. See Script tag.

<script src="https://cdn.findlane.dev/browser/0/findlane.min.js"></script>

Create a client

import { SearchBrowserClient } from "@findlane/browser"

const client = new SearchBrowserClient({ publicId: "pub_your_public_id" })
Option
publicIdThe index's public ID, from its Public access page.
baseUrlThe API to call. Leave it out to use Findlane's hosted API.
fetchA fetch implementation to use instead of the global one, for tests or instrumentation.
eventsWhether client.events sends anything: true (the default), false, or "afterConsent". See Consent.
eventsUrlWhere events go: a route on your own site that passes them on, such as "/findlane/events", which ad blockers let through (0.10.0). See Through your own domain.
visitorIdstrue to send random IDs for this browser and this visit with events, when the index takes them (0.12.0). See Visitor IDs.
signedKeyFor a logged-in person: a signed key from your server, or a function that fetches one, which searches and suggestions send. Its restrictions replace the index's public settings (0.25.0). See Logged-in visitors.
imageHostsThe hosts your records' images are on, when the index has optimized images on for them, such as ["cdn.shop.example"]: imageUrl resizes them from the first render, before any answer (0.29.0).

Browser search works from the index's allowed origins, or with a signed key; see Security.

search(request, options)

type Product = {
  objectID: string
  title: string
  brand: { name: string }
  price: number
  image: string
}

const results = await client.search<Product>(
  { q: "trail shoe", facets: ["brand.name"], limit: 24, highlight: true },
  { signal: controller.signal }
)

The type parameter types each hit's record. options.signal cancels the request.

Request

FieldType
qstringThe query, up to 200 characters.
filtersobjectField values to keep: a value, in, all, not, notIn, a gte/lte range, and $or groups. See Filters.
facetsstring[]Facet fields to count values for, up to 32. Other fields are left out.
disjunctiveFacetsstring[]Facets counted without their own filter. See Facets.
sortstringA sort ID from the index's settings, or "relevance".
limitnumberHits to return, 1 to 50, or 0 with clusters. Default 20.
offsetnumberHits to skip, up to 5,000.
countMode"page" or "exact""exact" counts every match in total.
facetMode"page" or "exact""exact" counts facet values across every match.
highlightboolean or { preTag, postTag }Mark matched words in highlights.
attributesToRetrievestring[]Return only these fields.
boost, buryfilters, or a list of up to 10Records matching them first, or last, in searches by relevance; a list counts each once (0.20.0). See Boost and bury.
contextstring or string[]Names the page sends, which the index's rules can fire on, such as "category-page" (0.19.0).
near{ lat, lng, radius? } or { visitor: true, radius? }Nearest first from a point or the visitor; radius in meters keeps only what's within it. Adds distance to each hit.
within{ north, south, east, west }Only records inside a box, in degrees.
tile{ z, x, y }Only records on a map tile, instead of near or within (0.33.0). tilesFor says which a view needs.
clusters{ cells, stats? }With tile: the tile's matches grouped by cell, with cells of 1, 2, 4, 8 or 16 a side and stats on up to three number fields (0.33.0). A stat can be { field, gt }, to leave out values at or below gt (0.34.0).
spread{ cells, perCell? }With tile: the page is each cell's best records, up to 10 a cell, rather than the top of one list (0.33.0).
boundsbooleanAdds bounds: the box around every match, not only the page (0.33.0).
subindexes{ [name]: request | true }The index's subindexes to search too, up to 5, counted as one search with this one (0.22.0).

Response

FieldType
hits{ id, score, record, highlights? }[]The matching records. score is null without a query or when sorting by a field.
totalnumberMatches: a lower bound when totalRelation is "gte".
totalRelation"eq" or "gte"Whether total is exact.
hasMorebooleanWhether there are hits after this page.
facetsRecord<string, Record<string, number>>Counts per value of each requested facet. See Large indexes.
facetsEstimatedbooleantrue when the facet counts are estimates, on large indexes.
sortstringThe sort used.
didYouMean{ q, applied, changes } or nullWith the index's didYouMean setting: the corrected query. See Did you mean.
appliedRules{ id, userData? }[]When the query fired the index's rules: each rule, with its userData for a banner or a redirect. See Rules.
rewrittenQuerystringWhen a rule replaced words: the query that was searched.
related{ hits, total }With the index's relatedResults setting, when nothing matched: the products closest in meaning (0.17.0). See Related results.
banners{ image, link?, title?, position?, rule }[]Rules' banners, to show at their places among the hits (0.20.0). See Rules.
redirectstringA page a rule sends the search to, such as /pages/returns (0.20.0).
subindexes{ [name]: response | { error, status } }With subindexes: each subindex's results, or its error (0.22.0).
abTest{ id, variant }While the index runs an A/B test: the shopper's group, which the client's events then carry (0.26.0). Nothing to do with it.
near{ lat, lng } or nullWith near: the point distances are from, or null when the visitor's location isn't known.
clusters{ count, lat, lng, bounds, stats? }[]With clusters: one for each cell with matches.
bounds{ north, south, east, west } or nullWith bounds: the box around every match with a place on the map.
tookMsnumberTime the search took on the server.

multiSearch(queries, options)

Searches several indexes in one request (0.22.0): up to 10 searches, each with its index's public ID as index (this client's when left out), counted as one search for each workspace they search. Answers each search's results with its index, or its error and status, in order. See Several indexes in one request.

suggest(request, options)

For a search that found nothing, a query that finds results, or null. It takes the same request as search and needs the index's didYouMean setting. See Did you mean.

const suggestion = await client.suggest({ q: "prtien" })
// { q: "protein", applied: false, changes: [{ from: "prtien", to: "protein" }] }

querySuggestions(request, options)

Whole searches to offer as the visitor types, the best first (0.14.0). request is { q, limit? }, with limit from 1 to 10 (5 by default). Each suggestion is { q, typed }, where typed is the part of q that was typed, as UTF-16 offsets. See Query suggestions.

import { suggestionParts } from "@findlane/browser"

const suggestions = await client.querySuggestions({ q: "hrek" })
// [{ q: "hrekkjavöku búningar", typed: [0, 4] }]
suggestionParts(suggestions[0])
// [{ text: "hrek", typed: true }, { text: "kjavöku búningar", typed: false }]

They don't count as searches, so ask on each pause in typing, with the same signal as the search.

popularSearches(options)

The index's popular searches, for an empty search box, with the owner's pinned searches first (0.14.0). Empty until the index has enough searches. The list changes daily, so the client asks once per limit (6 by default) and keeps the answer.

const popular = await client.popularSearches({ limit: 6 })
// [{ q: "gríma" }, { q: "latex" }]

warm()

Opens the connection to the API before the first search needs it, as a preconnect hint in the page would (0.16.0). Call it when the shopper focuses the search box; the React components do. It does something once per page, and nothing outside a browser.

input.addEventListener("focus", () => client.warm(), { once: true })

tilesFor(box, zoom)

The fixed map tiles covering a map's view, for tile in a search (0.33.0). It's a function of the package rather than the client's. In React, useMapSearch calls it for you.

import { tilesFor } from "@findlane/browser"

const tiles = tilesFor({ north: 64.17, south: 64.12, east: -21.8, west: -22 }, 13.6)
// [{ z: 12, x: 1797, y: 1087 }, …]
  • box is a plain { north, south, east, west } in degrees, or a map library's own bounds object with getNorth() and the like, such as MapLibre's, Mapbox's or Leaflet's (0.34.0). West greater than east crosses the antimeridian. Any other box throws a TypeError.
  • zoom is the map's. Tiles are one level above it, at most six on a desktop screen, and at most 16 for a box much wider than its zoom, which gets larger tiles.
  • In a script tag, it's Findlane.tilesFor (0.34.0).

clusterFor(place, clusters)

The cluster a record is in, from the clusters of its tile, for one marker an area (0.34.0). Give clusters and spread the same cells. It's undefined when none holds the place.

import { clusterFor } from "@findlane/browser"

const { clusters, hits } = await client.search({ tile, limit: 50, clusters: { cells: 8 }, spread: { cells: 8 } })
for (const hit of hits) {
  const cluster = clusterFor(hit.record._geoloc, clusters)
  // Draw the hit's photo, with cluster.count - 1 others there.
}

A cluster's bounds are rounded, so comparing a place with them directly misses records on an edge. In a script tag, it's Findlane.clusterFor.

imageUrl(src, width)

An image's address resized by optimized images (0.27.0): src resized to width CSS pixels (rounded up to one of the widths Findlane makes) and sent as WebP from images.findlane.dev (0.28.0), when the index has optimized images on and src is on one of its image hosts. Otherwise it returns src as it is. The client learns the hosts from search answers, so build image addresses from results. It remembers where an index's images come from in the browser, and opens that connection with warm() and the first search on later pages. Load images with crossorigin="anonymous", so they use it.

img.src = client.imageUrl(hit.record.image, 240)
img.srcset = `${client.imageUrl(hit.record.image, 240)} 1x, ${client.imageUrl(hit.record.image, 480)} 2x`
img.crossOrigin = "anonymous"

Each hit's placeholder is a preview of its image: placeholderUrl(hit.placeholder) draws it as a PNG data address, and placeholderCss and revealCss (0.27.1) are the styles while the image loads and as it sharpens in. Optimized images shows them together.

recentSearches

The visitor's recent searches, kept in their browser's localStorage per public ID, and never sent to Findlane (0.14.0): list(limit?) (5 by default, the newest first), add(q), remove(q), and clear(). It keeps up to 10, without repeats.

recommendations(objectID, options)

A product's Goes with, Similar and Bought together lists, for indexes with the recommendations add-on. Options are the request's other fields.

const { ready, goesWith, similar, together } = await client.recommendations("sku-123", { limit: 6 })

list(id, options)

One of the index's lists, such as popular: a row of products (hits) or a field's values with their counts (values), with its title and browse (its filters and sort, for a "See all" link). Built ahead and answered from a cache near the visitor, so it isn't counted as a search (0.15.0). options.limit gives fewer than the list holds. An index without the list throws a FindlaneError with status 404.

lists(ids, options)

Several lists in one request, up to 10, in the order asked; IDs the index has no list for are left out. Without IDs, all of the index's lists (0.15.0).

const [halloween, themes] = await client.lists(["halloween", "themes"])

events

Views, clicks, add-to-carts and orders, for the Events tab in Analytics and Bought together (0.8.0). They're sent in the background and never throw. See Events.

Method
view(objectID)A product page was viewed. A repeat of the last view within 5 seconds isn't sent (0.11.0).
click(objectID, details?)A product was clicked: position (from 1), and query and filters for a search result, or list and from for a recommendation.
landed(q)The page shows search q from its address. When an ad, an email or another site opened the page, clicks on that search are marked so (0.24.0).
suggestion(query, details?)A query suggestion was picked: position (from 1) and kind, typed, popular or recent (0.14.0).
bannerClick(rule, { query, position }?)A rule's banner was clicked: the rule each banner in a response names (0.21.0).
facetMatch(match, { query, position }?)A facet match was picked: the match as the response gave it (0.31.0).
addToCart(items, { currency }?)Products went into the cart: [{ objectID, quantity, price }].
purchase({ orderID, items, currency })An order. It counts for Bought together only if the index allows orders from browsers.
consent(granted)With events: "afterConsent": true sends what's held and what follows; false drops it, and deletes the visitor IDs from the browser.
forget()With visitorIds: deletes the IDs from the browser, and asks Findlane to delete the events sent with them (0.12.0).
flush()Sends what's waiting now.
client.events.click(hit.id, { query, position: index + 1, filters })

Errors

A failed request throws a FindlaneError with the HTTP status and the API's message:

import { FindlaneError } from "@findlane/browser"

try {
  await client.search({ q })
} catch (error) {
  if (error instanceof FindlaneError && error.status === 429)
    showMessage("Too many searches, try again shortly")
  else throw error
}
StatusMeaning
400The request isn't valid, such as an unknown facet or sort. The message says why.
401The signed key isn't valid for this index, or has expired (signedKey asks a function for a new one first).
402The workspace has used the searches it allows this month: Free's, or a paid plan's up to its spending limit. Cached searches still work.
403This origin isn't allowed, the public ID doesn't exist, or the index's Limits and bots settings block this kind of traffic.
429Too many searches from this visitor or to this index: 240 and 6,000 a minute by default.
503Too many of the workspace's searches were running at once, or the search took longer than 10 seconds: a search the cache holds is answered from it instead. Also when Findlane couldn't reach its database for a moment.

A cancelled request rejects with the AbortError from fetch.

Script tag

No build step needed: load the SDK from cdn.findlane.dev, and it defines a global Findlane.

<script src="https://cdn.findlane.dev/browser/0/findlane.min.js"></script>
<script>
  const client = new Findlane.SearchBrowserClient({ publicId: "pub_your_public_id" })

  client.search({ q: "jacket", limit: 10 }).then(({ hits }) => {
    for (const hit of hits) console.log(hit.record.title)
  })
</script>

In a module script, import it instead:

<script type="module">
  import { SearchBrowserClient } from "https://cdn.findlane.dev/browser/0/findlane.mjs"

  const client = new SearchBrowserClient({ publicId: "pub_your_public_id" })
  const { hits } = await client.search({ q: "jacket" })
</script>

browser/0/ always serves the latest 0.x release. To stay on one release, pin its version, and add its integrity hash so the browser checks the file:

<script
  src="https://cdn.findlane.dev/browser/0.34.0/findlane.min.js"
  integrity="sha384-VHEpSimqxx1A5HEA+qokMhGHWX2cjNASxVW+Y46AvJNU8Ef6ixtAZBji8qptS8zW"
  crossorigin="anonymous"
></script>

browser/versions.json lists every release with its hashes.