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/browserUsing 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 | |
|---|---|
publicId | The index's public ID, from its Public access page. |
baseUrl | The API to call. Leave it out to use Findlane's hosted API. |
fetch | A fetch implementation to use instead of the global one, for tests or instrumentation. |
events | Whether client.events sends anything: true (the default), false, or "afterConsent". See Consent. |
eventsUrl | Where 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. |
visitorIds | true to send random IDs for this browser and this visit with events, when the index takes them (0.12.0). See Visitor IDs. |
signedKey | For 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. |
imageHosts | The 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
| Field | Type | |
|---|---|---|
q | string | The query, up to 200 characters. |
filters | object | Field values to keep: a value, in, all, not, notIn, a gte/lte range, and $or groups. See Filters. |
facets | string[] | Facet fields to count values for, up to 32. Other fields are left out. |
disjunctiveFacets | string[] | Facets counted without their own filter. See Facets. |
sort | string | A sort ID from the index's settings, or "relevance". |
limit | number | Hits to return, 1 to 50, or 0 with clusters. Default 20. |
offset | number | Hits 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. |
highlight | boolean or { preTag, postTag } | Mark matched words in highlights. |
attributesToRetrieve | string[] | Return only these fields. |
boost, bury | filters, or a list of up to 10 | Records matching them first, or last, in searches by relevance; a list counts each once (0.20.0). See Boost and bury. |
context | string 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). |
bounds | boolean | Adds 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
| Field | Type | |
|---|---|---|
hits | { id, score, record, highlights? }[] | The matching records. score is null without a query or when sorting by a field. |
total | number | Matches: a lower bound when totalRelation is "gte". |
totalRelation | "eq" or "gte" | Whether total is exact. |
hasMore | boolean | Whether there are hits after this page. |
facets | Record<string, Record<string, number>> | Counts per value of each requested facet. See Large indexes. |
facetsEstimated | boolean | true when the facet counts are estimates, on large indexes. |
sort | string | The sort used. |
didYouMean | { q, applied, changes } or null | With 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. |
rewrittenQuery | string | When 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. |
redirect | string | A 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 null | With 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 null | With bounds: the box around every match with a place on the map. |
tookMs | number | Time 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 }, …]boxis a plain{ north, south, east, west }in degrees, or a map library's own bounds object withgetNorth()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 aTypeError.zoomis 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
}| Status | Meaning |
|---|---|
400 | The request isn't valid, such as an unknown facet or sort. The message says why. |
401 | The signed key isn't valid for this index, or has expired (signedKey asks a function for a new one first). |
402 | The 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. |
403 | This origin isn't allowed, the public ID doesn't exist, or the index's Limits and bots settings block this kind of traffic. |
429 | Too many searches from this visitor or to this index: 240 and 6,000 a minute by default. |
503 | Too 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.