mapbox-search-integration
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseMapbox Search Integration Skill
Mapbox搜索集成技能
Expert guidance for implementing Mapbox search functionality in applications. Covers the complete workflow from asking the right discovery questions, selecting the appropriate search product, to implementing production-ready integrations following best practices from the Mapbox search team.
为在应用中实现Mapbox搜索功能提供专业指导。涵盖从提出正确的需求调研问题、选择合适的搜索产品,到遵循Mapbox搜索团队最佳实践完成生产级集成的完整工作流。
Use This Skill When
适用场景
User says things like:
- "I need to add search to my map"
- "I need a search bar for my mapping app"
- "How do I implement location search?"
- "I want users to search for places/addresses"
- "I need geocoding in my application"
This skill complements :
mapbox-search-patterns- = Tool and parameter selection
mapbox-search-patterns - = Complete implementation workflow
mapbox-search-integration
当用户提出以下需求时使用本技能:
- "我需要为地图添加搜索功能"
- "我的地图应用需要一个搜索栏"
- "如何实现地点搜索?"
- "我希望用户能搜索地点/地址"
- "我的应用需要地理编码功能"
本技能与互补:
mapbox-search-patterns- = 工具与参数选择
mapbox-search-patterns - = 完整实现工作流
mapbox-search-integration
Discovery Phase: Ask the Right Questions
需求调研阶段:提出正确的问题
Before jumping into code, ask these questions to understand requirements:
在开始编码前,先通过以下问题明确需求:
Question 1: What are users searching for?
问题1:用户要搜索什么内容?
Ask: "What do you want users to search for?"
Common answers and implications:
- "Addresses" → Focus on address geocoding, consider Search Box API or Geocoding API
- "Points of interest / businesses" → POI search, use Search Box API with category search
- "Both addresses and POIs" → Search Box API (unified search)
- "Specific types of places" (restaurants, hotels, etc.) → Category search or filtered POI search
- "Custom locations" (user-created places) → May need custom data + search integration
提问: "你希望用户搜索什么类型的内容?"
常见回答及对应方案:
- "地址" → 重点关注地址地理编码,考虑使用Search Box API或Geocoding API
- "兴趣点/商家" → POI搜索,使用带分类搜索的Search Box API
- "地址和兴趣点都需要" → Search Box API(统一搜索)
- "特定类型的地点"(餐厅、酒店等)→ 分类搜索或过滤式POI搜索
- "自定义地点"(用户创建的地点)→ 可能需要自定义数据+搜索集成
Question 2: What's the geographic scope?
问题2:地理范围是什么?
Ask: "Where will users be searching?"
Common answers and implications:
- "Single country" (e.g., "only USA") → Use parameter, better results, lower cost
country - "Specific region" → Use parameter for bounding box constraint
bbox - "Global" → No country restriction, but may need language parameter
- "Multiple specific countries" → Use array parameter
country
Follow-up: "Do you need to limit results to a specific area?" (delivery zone, service area, etc.)
提问: "用户将在哪些区域进行搜索?"
常见回答及对应方案:
- "单个国家"(例如:"仅美国")→ 使用参数,可获得更优结果且成本更低
country - "特定区域" → 使用参数设置边界框约束
bbox - "全球范围" → 不设置国家限制,但可能需要配置语言参数
- "多个特定国家" → 使用数组参数
country
跟进提问: "是否需要将结果限制在特定区域内?"(配送区域、服务区域等)
Question 3: What's the search interaction pattern?
问题3:搜索交互模式是怎样的?
Ask: "How will users interact with search?"
Common answers and implications:
- "Search-as-you-type / autocomplete" → Use Search Box API with , implement debouncing
autocomplete: true - "Search button / final query" → Can use either API, no autocomplete needed
- "Both" (autocomplete + refine) → Two-stage search, autocomplete then detailed results
- "Voice input" → Consider speech-to-text integration, handle longer queries
提问: "用户将如何与搜索功能交互?"
常见回答及对应方案:
- "输入即搜/自动补全" → 使用开启的Search Box API,实现防抖处理
autocomplete: true - "点击搜索按钮/最终查询" → 可使用任意API,无需自动补全
- "两者兼具"(自动补全+细化查询)→ 两阶段搜索,先自动补全再展示详细结果
- "语音输入" → 考虑集成语音转文本功能,处理长查询
Question 4: What platform?
问题4:目标平台是什么?
Ask: "What platform is this for?"
Common answers and implications:
- "Web application" → Mapbox Search JS (easiest), or direct API calls for advanced cases
- "iOS app" → Search SDK for iOS (recommended), or direct API integration for advanced cases
- "Android app" → Search SDK for Android (recommended), or direct API integration for advanced cases
- "Multiple platforms" → Platform-specific SDKs (recommended), or direct API approach for consistency
- "React app" → Mapbox Search JS React (easiest with UI), or Search JS Core for custom UI
- "Vue / Angular / Other framework" → Mapbox Search JS Core or Web, or direct API calls
提问: "该功能是为哪个平台开发的?"
常见回答及对应方案:
- "Web应用" → Mapbox Search JS(最简单),高级场景可直接调用API
- "iOS应用" → Search SDK for iOS(推荐),高级场景可直接集成API
- "Android应用" → Search SDK for Android(推荐),高级场景可直接集成API
- "多平台" → 推荐使用平台专属SDK,或直接调用API以保持一致性
- "React应用" → Mapbox Search JS React(带UI的最简方案),或使用Search JS Core自定义UI
- "Vue / Angular / 其他框架" → Mapbox Search JS Core或Web版本,或直接调用API
Question 5: How will results be used?
问题5:搜索结果将如何使用?
Ask: "What happens when a user selects a result?"
Common answers and implications:
- "Fly to location on map" → Need coordinates, map integration
- "Show details / info" → Need to retrieve and display result properties
- "Fill form fields" → Need to parse address components
- "Start navigation" → Need coordinates, integrate with directions
- "Multiple selection" → Need to handle selection state, possibly show markers
提问: "用户选择结果后会执行什么操作?"
常见回答及对应方案:
- "在地图上飞至该地点" → 需要坐标,集成地图功能
- "展示详情/信息" → 需要获取并展示结果属性
- "填充表单字段" → 需要解析地址组件
- "启动导航" → 需要坐标,集成导航功能
- "多选" → 需要处理选择状态,可能需要展示标记
Question 6: Expected usage volume?
问题6:预期使用量是多少?
Ask: "How many searches do you expect per month?"
Implications:
- Low volume (< 10k) → Free tier sufficient, simple implementation
- Medium volume (10k-100k) → Consider caching, optimize API calls
- High volume (> 100k) → Implement debouncing, caching, batch operations, monitor costs
提问: "预计每月有多少次搜索请求?"
对应方案:
- 低用量(< 10k)→ 免费套餐足够,实现方式简单
- 中用量(10k-100k)→ 考虑缓存,优化API调用
- 高用量(> 100k)→ 实现防抖、缓存、批量操作,监控成本
Product Selection Decision Tree
产品选择决策树
Based on discovery answers, recommend the right product:
根据调研结果,推荐合适的产品:
Recommended: Search Box API (Modern, Unified)
推荐:Search Box API(现代、统一)
Use when:
- User needs both addresses AND POIs
- Building a modern web/mobile app
- Want autocomplete functionality
- Need session-based pricing
- Want the simplest integration
Advantages:
- ✅ Unified search (addresses + POIs)
- ✅ Session-based pricing (cheaper for autocomplete)
- ✅ Modern API design
- ✅ Built-in autocomplete support
- ✅ Better POI coverage
Products:
- Search Box API (REST) - Direct API integration
- Mapbox Search JS (SDK) - Web integration with three components:
- Search JS React - Easy search integration via React library with UI
- Search JS Web - Easy search integration via Web Components with UI
- Search JS Core - JavaScript (node or web) wrapper for API, build your own UI
- Search SDK for iOS - Native iOS integration
- Search SDK for Android - Native Android integration
适用场景:
- 用户同时需要地址和POI搜索
- 开发现代Web/移动应用
- 需要自动补全功能
- 希望按会话计费
- 追求最简单的集成方式
优势:
- ✅ 统一搜索(地址+POI)
- ✅ 按会话计费(自动补全场景更经济)
- ✅ 现代API设计
- ✅ 内置自动补全支持
- ✅ 更全面的POI覆盖
相关产品:
- Search Box API(REST)- 直接API集成
- Mapbox Search JS(SDK)- Web集成,包含三个组件:
- Search JS React - 通过React库快速集成带UI的搜索功能
- Search JS Web - 通过Web Components快速集成带UI的搜索功能
- Search JS Core - JavaScript(Node.js或Web)API封装,用于自定义UI
- Search SDK for iOS - 原生iOS集成
- Search SDK for Android - 原生Android集成
Geocoding API
Geocoding API
Use when:
- Only need address geocoding (no POIs)
- Existing integration to maintain
- Need permanent geocoding (not search)
- Batch geocoding jobs
Note: Prefer Search Box API unless the user specifically says they only want address geocoding.
适用场景:
- 仅需要地址地理编码(无需POI)
- 需维护现有集成
- 需要永久地理编码(而非搜索)
- 批量地理编码任务
注意: 除非用户明确表示仅需要地址地理编码,否则优先选择Search Box API。
Integration Patterns by Platform
按平台划分的集成模式
Important: Always prefer using SDKs (Mapbox Search JS, Search SDK for iOS/Android) over calling APIs directly. SDKs handle debouncing, session tokens, error handling, and provide UI components. Only use direct API calls for advanced use cases.
重要提示: 优先使用SDK(Mapbox Search JS、Search SDK for iOS/Android)而非直接调用API。SDK会处理防抖、会话令牌、错误处理,并提供UI组件。仅在高级场景下使用直接API调用。
Web: Mapbox Search JS (Recommended)
Web端:Mapbox Search JS(推荐)
Option 1: Search JS React (Easiest - React apps with UI)
选项1:Search JS React(最简方案 - 带UI的React应用)
When to use: React application, want autocomplete UI component, fastest implementation
Installation:
bash
npm install @mapbox/search-js-reactComplete implementation:
jsx
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
function App() {
const [map, setMap] = React.useState(null);
React.useEffect(() => {
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox accessToken="YOUR_MAPBOX_TOKEN" onRetrieve={handleRetrieve} placeholder="Search for places" />
<div id="map" style={{ height: '600px' }} />
</div>
);
}适用场景: React应用,需要自动补全UI组件,追求最快实现速度
安装:
bash
npm install @mapbox/search-js-react完整实现:
jsx
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
function App() {
const [map, setMap] = React.useState(null);
React.useEffect(() => {
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox accessToken="YOUR_MAPBOX_TOKEN" onRetrieve={handleRetrieve} placeholder="Search for places" />
<div id="map" style={{ height: '600px' }} />
</div>
);
}Option 2: Search JS Web (Web Components with UI)
选项2:Search JS Web(带UI的Web Components)
When to use: Vanilla JavaScript, Web Components, or any framework, want autocomplete UI
Complete implementation:
html
<!DOCTYPE html>
<html>
<head>
<script src="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.js"></script>
<link href="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.css" rel="stylesheet" />
</head>
<body>
<div id="search"></div>
<div id="map" style="height: 600px;"></div>
<script>
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Initialize Search Box
const search = new MapboxSearchBox();
search.accessToken = 'YOUR_MAPBOX_TOKEN';
// CRITICAL: Set options based on discovery
search.options = {
language: 'en',
country: 'US', // If single-country (from Question 2)
proximity: 'ip', // Or specific coordinates
types: 'address,poi' // Based on Question 1
};
search.mapboxgl = mapboxgl;
search.marker = true; // Auto-add marker on result selection
// Handle result selection
search.addEventListener('retrieve', (event) => {
const result = event.detail;
// Fly to result
map.flyTo({
center: result.geometry.coordinates,
zoom: 15,
essential: true
});
// Optional: Show popup with details
new mapboxgl.Popup()
.setLngLat(result.geometry.coordinates)
.setHTML(
`<h3>${result.properties.name}</h3>
<p>${result.properties.full_address || ''}</p>`
)
.addTo(map);
});
// Attach to DOM
document.getElementById('search').appendChild(search);
</script>
</body>
</html>Key implementation notes:
- ✅ Set if single-country search (better results, lower cost)
country - ✅ Set based on what users search for
types - ✅ Use to bias results to user location
proximity - ✅ Handle event for result selection
retrieve - ✅ Integrate with map (flyTo, markers, popups)
适用场景: Vanilla JavaScript、Web Components或任意框架,需要自动补全UI
完整实现:
html
<!DOCTYPE html>
<html>
<head>
<script src="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.js"></script>
<link href="https://api.mapbox.com/search-js/v1.0.0-beta.18/web.css" rel="stylesheet" />
<script src="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.js"></script>
<link href="https://api.mapbox.com/mapbox-gl-js/v3.0.0/mapbox-gl.css" rel="stylesheet" />
</head>
<body>
<div id="search"></div>
<div id="map" style="height: 600px;"></div>
<script>
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Initialize Search Box
const search = new MapboxSearchBox();
search.accessToken = 'YOUR_MAPBOX_TOKEN';
// CRITICAL: Set options based on discovery
search.options = {
language: 'en',
country: 'US', // If single-country (from Question 2)
proximity: 'ip', // Or specific coordinates
types: 'address,poi' // Based on Question 1
};
search.mapboxgl = mapboxgl;
search.marker = true; // Auto-add marker on result selection
// Handle result selection
search.addEventListener('retrieve', (event) => {
const result = event.detail;
// Fly to result
map.flyTo({
center: result.geometry.coordinates,
zoom: 15,
essential: true
});
// Optional: Show popup with details
new mapboxgl.Popup()
.setLngLat(result.geometry.coordinates)
.setHTML(
`<h3>${result.properties.name}</h3>
<p>${result.properties.full_address || ''}</p>`
)
.addTo(map);
});
// Attach to DOM
document.getElementById('search').appendChild(search);
</script>
</body>
</html>关键实现注意事项:
- ✅ 若为单国家搜索,设置参数(结果更优,成本更低)
country - ✅ 根据用户搜索内容设置参数
types - ✅ 使用参数将结果偏向用户位置
proximity - ✅ 处理事件以响应结果选择
retrieve - ✅ 与地图集成(飞至地点、标记、弹窗)
Option 3: Search JS Core (Custom UI)
选项3:Search JS Core(自定义UI)
When to use: Need custom UI design, full control over UX, works in any framework or Node.js
Installation:
bash
npm install @mapbox/search-js-coreComplete implementation:
javascript
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
// Initialize search session
const search = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Your custom search input
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('results');
// Handle user input
searchInput.addEventListener('input', async (e) => {
const query = e.target.value;
if (query.length < 2) {
resultsContainer.innerHTML = '';
return;
}
// Get suggestions (Search JS Core handles debouncing and session tokens)
const response = await search.suggest(query, {
proximity: map.getCenter().toArray(),
country: 'US', // Optional
types: ['address', 'poi']
});
// Render custom results UI
resultsContainer.innerHTML = response.suggestions
.map(
(suggestion) => `
<div class="result-item" data-id="${suggestion.mapbox_id}">
<strong>${suggestion.name}</strong>
<div>${suggestion.place_formatted}</div>
</div>
`
)
.join('');
});
// Handle result selection
resultsContainer.addEventListener('click', async (e) => {
const resultItem = e.target.closest('.result-item');
if (!resultItem) return;
const mapboxId = resultItem.dataset.id;
// Retrieve full details
const result = await search.retrieve(mapboxId);
const feature = result.features[0];
const [lng, lat] = feature.geometry.coordinates;
// Update map
map.flyTo({ center: [lng, lat], zoom: 15 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Clear search
searchInput.value = feature.properties.name;
resultsContainer.innerHTML = '';
});Key benefits:
- ✅ Full control over UI/UX
- ✅ Search JS Core handles session tokens automatically
- ✅ Works in any framework (React, Vue, Angular, etc.)
- ✅ Can use in Node.js for server-side search
适用场景: 需要自定义UI设计,完全控制用户体验,适用于任意框架或Node.js
安装:
bash
npm install @mapbox/search-js-core完整实现:
javascript
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
// Initialize search session
const search = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
// Initialize map
mapboxgl.accessToken = 'YOUR_MAPBOX_TOKEN';
const map = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
// Your custom search input
const searchInput = document.getElementById('search-input');
const resultsContainer = document.getElementById('results');
// Handle user input
searchInput.addEventListener('input', async (e) => {
const query = e.target.value;
if (query.length < 2) {
resultsContainer.innerHTML = '';
return;
}
// Get suggestions (Search JS Core handles debouncing and session tokens)
const response = await search.suggest(query, {
proximity: map.getCenter().toArray(),
country: 'US', // Optional
types: ['address', 'poi']
});
// Render custom results UI
resultsContainer.innerHTML = response.suggestions
.map(
(suggestion) => `
<div class="result-item" data-id="${suggestion.mapbox_id}">
<strong>${suggestion.name}</strong>
<div>${suggestion.place_formatted}</div>
</div>
`
)
.join('');
});
// Handle result selection
resultsContainer.addEventListener('click', async (e) => {
const resultItem = e.target.closest('.result-item');
if (!resultItem) return;
const mapboxId = resultItem.dataset.id;
// Retrieve full details
const result = await search.retrieve(mapboxId);
const feature = result.features[0];
const [lng, lat] = feature.geometry.coordinates;
// Update map
map.flyTo({ center: [lng, lat], zoom: 15 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Clear search
searchInput.value = feature.properties.name;
resultsContainer.innerHTML = '';
});核心优势:
- ✅ 完全控制UI/UX
- ✅ Search JS Core自动处理会话令牌
- ✅ 适用于任意框架(React、Vue、Angular等)
- ✅ 可在Node.js中用于服务端搜索
Option 4: Direct API Integration (Advanced - Last Resort)
选项4:直接API集成(高级 - 最后选择)
When to use: Very specific requirements that SDKs don't support, or server-side integration where Search JS Core doesn't fit
Important: Only use direct API calls when SDKs don't meet your needs. You'll need to handle debouncing and session tokens manually.
When to use: Custom UI, framework integration, need full control
Complete implementation with debouncing:
javascript
import mapboxgl from 'mapbox-gl';
class MapboxSearch {
constructor(accessToken, options = {}) {
this.accessToken = accessToken;
this.options = {
country: options.country || null, // e.g., 'US'
language: options.language || 'en',
proximity: options.proximity || 'ip',
types: options.types || 'address,poi',
limit: options.limit || 5,
...options
};
this.debounceTimeout = null;
this.sessionToken = this.generateSessionToken();
}
generateSessionToken() {
return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
// CRITICAL: Debounce to avoid API spam
async search(query, callback, debounceMs = 300) {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(async () => {
const results = await this.performSearch(query);
callback(results);
}, debounceMs);
}
async performSearch(query) {
if (!query || query.length < 2) return [];
const params = new URLSearchParams({
q: query,
access_token: this.accessToken,
session_token: this.sessionToken,
language: this.options.language,
limit: this.options.limit
});
// Add optional parameters
if (this.options.country) {
params.append('country', this.options.country);
}
if (this.options.types) {
params.append('types', this.options.types);
}
if (this.options.proximity && this.options.proximity !== 'ip') {
params.append('proximity', this.options.proximity);
}
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
if (!response.ok) {
throw new Error(`Search API error: ${response.status}`);
}
const data = await response.json();
return data.suggestions || [];
} catch (error) {
console.error('Search error:', error);
return [];
}
}
async retrieve(suggestionId) {
const params = new URLSearchParams({
access_token: this.accessToken,
session_token: this.sessionToken
});
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/retrieve/${suggestionId}?${params}`);
if (!response.ok) {
throw new Error(`Retrieve API error: ${response.status}`);
}
const data = await response.json();
// Session ends on retrieve - generate new token for next search
this.sessionToken = this.generateSessionToken();
return data.features[0];
} catch (error) {
console.error('Retrieve error:', error);
return null;
}
}
}
// Usage example
const search = new MapboxSearch('YOUR_MAPBOX_TOKEN', {
country: 'US', // Based on discovery Question 2
types: 'poi', // Based on discovery Question 1
proximity: [-122.4194, 37.7749] // Or 'ip' for user location
});
// Attach to input field
const input = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
input.addEventListener('input', (e) => {
const query = e.target.value;
search.search(query, (results) => {
displayResults(results);
});
});
function displayResults(results) {
resultsContainer.innerHTML = results
.map(
(result) => `
<div class="result" data-id="${result.mapbox_id}">
<strong>${result.name}</strong>
<p>${result.place_formatted || ''}</p>
</div>
`
)
.join('');
// Handle result selection
resultsContainer.querySelectorAll('.result').forEach((el) => {
el.addEventListener('click', async () => {
const feature = await search.retrieve(el.dataset.id);
handleResultSelection(feature);
});
});
}
function handleResultSelection(feature) {
const [lng, lat] = feature.geometry.coordinates;
// Fly map to result
map.flyTo({
center: [lng, lat],
zoom: 15
});
// Add marker
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Close results
resultsContainer.innerHTML = '';
input.value = feature.properties.name;
}Critical implementation details:
- ✅ Debouncing: Wait 300ms after user stops typing before API call
- ✅ Session tokens: Use same token for suggest + retrieve, generate new after
- ✅ Error handling: Handle API errors gracefully
- ✅ Parameter optimization: Only send parameters you need
- ✅ Result display: Show name + formatted address
- ✅ Selection handling: Retrieve full feature on selection
适用场景: SDK无法满足的特定需求,或Search JS Core不适用的服务端集成
重要提示: 仅当SDK无法满足需求时使用直接API调用。需手动处理防抖和会话令牌。
适用场景: 自定义UI、框架集成、需要完全控制
带防抖的完整实现:
javascript
import mapboxgl from 'mapbox-gl';
class MapboxSearch {
constructor(accessToken, options = {}) {
this.accessToken = accessToken;
this.options = {
country: options.country || null, // e.g., 'US'
language: options.language || 'en',
proximity: options.proximity || 'ip',
types: options.types || 'address,poi',
limit: options.limit || 5,
...options
};
this.debounceTimeout = null;
this.sessionToken = this.generateSessionToken();
}
generateSessionToken() {
return `${Date.now()}-${Math.random().toString(36).substring(7)}`;
}
// CRITICAL: Debounce to avoid API spam
async search(query, callback, debounceMs = 300) {
clearTimeout(this.debounceTimeout);
this.debounceTimeout = setTimeout(async () => {
const results = await this.performSearch(query);
callback(results);
}, debounceMs);
}
async performSearch(query) {
if (!query || query.length < 2) return [];
const params = new URLSearchParams({
q: query,
access_token: this.accessToken,
session_token: this.sessionToken,
language: this.options.language,
limit: this.options.limit
});
// Add optional parameters
if (this.options.country) {
params.append('country', this.options.country);
}
if (this.options.types) {
params.append('types', this.options.types);
}
if (this.options.proximity && this.options.proximity !== 'ip') {
params.append('proximity', this.options.proximity);
}
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
if (!response.ok) {
throw new Error(`Search API error: ${response.status}`);
}
const data = await response.json();
return data.suggestions || [];
} catch (error) {
console.error('Search error:', error);
return [];
}
}
async retrieve(suggestionId) {
const params = new URLSearchParams({
access_token: this.accessToken,
session_token: this.sessionToken
});
try {
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/retrieve/${suggestionId}?${params}`);
if (!response.ok) {
throw new Error(`Retrieve API error: ${response.status}`);
}
const data = await response.json();
// Session ends on retrieve - generate new token for next search
this.sessionToken = this.generateSessionToken();
return data.features[0];
} catch (error) {
console.error('Retrieve error:', error);
return null;
}
}
}
// Usage example
const search = new MapboxSearch('YOUR_MAPBOX_TOKEN', {
country: 'US', // Based on discovery Question 2
types: 'poi', // Based on discovery Question 1
proximity: [-122.4194, 37.7749] // Or 'ip' for user location
});
// Attach to input field
const input = document.getElementById('search-input');
const resultsContainer = document.getElementById('search-results');
input.addEventListener('input', (e) => {
const query = e.target.value;
search.search(query, (results) => {
displayResults(results);
});
});
function displayResults(results) {
resultsContainer.innerHTML = results
.map(
(result) => `
<div class="result" data-id="${result.mapbox_id}">
<strong>${result.name}</strong>
<p>${result.place_formatted || ''}</p>
</div>
`
)
.join('');
// Handle result selection
resultsContainer.querySelectorAll('.result').forEach((el) => {
el.addEventListener('click', async () => {
const feature = await search.retrieve(el.dataset.id);
handleResultSelection(feature);
});
});
}
function handleResultSelection(feature) {
const [lng, lat] = feature.geometry.coordinates;
// Fly map to result
map.flyTo({
center: [lng, lat],
zoom: 15
});
// Add marker
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
// Close results
resultsContainer.innerHTML = '';
input.value = feature.properties.name;
}关键实现细节:
- ✅ 防抖处理:用户停止输入300ms后再调用API
- ✅ 会话令牌:在suggest和retrieve请求中使用同一令牌,请求完成后生成新令牌
- ✅ 错误处理:优雅处理API错误
- ✅ 参数优化:仅发送必要的参数
- ✅ 结果展示:显示名称+格式化地址
- ✅ 选择处理:选择结果时获取完整要素信息
React Integration Pattern
React集成模式
Best Practice: Use Search JS React for easiest implementation, or Search JS Core for custom UI.
最佳实践: 使用Search JS React实现最简集成,或使用Search JS Core自定义UI。
Option 1: Search JS React (Recommended - Easiest)
选项1:Search JS React(推荐 - 最简方案)
javascript
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
import { useState } from 'react';
function MapboxSearchComponent() {
const [map, setMap] = useState(null);
useEffect(() => {
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox
accessToken="YOUR_MAPBOX_TOKEN"
onRetrieve={handleRetrieve}
placeholder="Search for places"
options={{
country: 'US', // Optional
types: 'address,poi'
}}
/>
<div id="map" style={{ height: '600px' }} />
</div>
);
}Benefits:
- ✅ Complete UI component provided
- ✅ No manual debouncing needed
- ✅ No manual session token management
- ✅ Production-ready out of the box
javascript
import { SearchBox } from '@mapbox/search-js-react';
import mapboxgl from 'mapbox-gl';
import { useState } from 'react';
function MapboxSearchComponent() {
const [map, setMap] = useState(null);
useEffect(() => {
const mapInstance = new mapboxgl.Map({
container: 'map',
style: 'mapbox://styles/mapbox/streets-v12',
center: [-122.4194, 37.7749],
zoom: 12
});
setMap(mapInstance);
}, []);
const handleRetrieve = (result) => {
const [lng, lat] = result.features[0].geometry.coordinates;
map.flyTo({ center: [lng, lat], zoom: 14 });
new mapboxgl.Marker().setLngLat([lng, lat]).addTo(map);
};
return (
<div>
<SearchBox
accessToken="YOUR_MAPBOX_TOKEN"
onRetrieve={handleRetrieve}
placeholder="Search for places"
options={{
country: 'US', // Optional
types: 'address,poi'
}}
/>
<div id="map" style={{ height: '600px' }} />
</div>
);
}优势:
- ✅ 提供完整的UI组件
- ✅ 无需手动处理防抖
- ✅ 无需手动管理会话令牌
- ✅ 开箱即可用于生产环境
Option 2: Search JS Core (Custom UI)
选项2:Search JS Core(自定义UI)
javascript
import { useState, useEffect } from 'react';
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
function MapboxSearchComponent({ country, types = 'address,poi' }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// Search JS Core handles debouncing and session tokens automatically
const searchSession = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
useEffect(() => {
const performSearch = async () => {
if (!query || query.length < 2) {
setResults([]);
return;
}
setIsLoading(true);
try {
const response = await searchSession.suggest(query, {
country,
types,
limit: 5
});
setResults(response.suggestions || []);
} catch (error) {
console.error('Search error:', error);
setResults([]);
} finally {
setIsLoading(false);
}
};
performSearch();
}, [query]);
const handleResultClick = async (suggestion) => {
try {
const result = await searchSession.retrieve(suggestion);
const feature = result.features[0];
// Handle result (fly to location, add marker, etc.)
onResultSelect(feature);
setQuery(feature.properties.name);
setResults([]);
} catch (error) {
console.error('Retrieve error:', error);
}
};
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for a place..."
className="search-input"
/>
{isLoading && <div className="loading">Searching...</div>}
{results.length > 0 && (
<div className="search-results">
{results.map((result) => (
<div key={result.mapbox_id} className="search-result" onClick={() => handleResultClick(result)}>
<strong>{result.name}</strong>
{result.place_formatted && <p>{result.place_formatted}</p>}
</div>
))}
</div>
)}
</div>
);
}Benefits:
- ✅ Full control over UI design
- ✅ Search JS Core handles debouncing automatically
- ✅ Search JS Core handles session tokens automatically
- ✅ Cleaner code than direct API calls
Note: For React apps, prefer Search JS React (Option 1) unless you need a completely custom UI design.
javascript
import { useState, useEffect } from 'react';
import { SearchSession } from '@mapbox/search-js-core';
import mapboxgl from 'mapbox-gl';
function MapboxSearchComponent({ country, types = 'address,poi' }) {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
// Search JS Core handles debouncing and session tokens automatically
const searchSession = new SearchSession({
accessToken: 'YOUR_MAPBOX_TOKEN'
});
useEffect(() => {
const performSearch = async () => {
if (!query || query.length < 2) {
setResults([]);
return;
}
setIsLoading(true);
try {
const response = await searchSession.suggest(query, {
country,
types,
limit: 5
});
setResults(response.suggestions || []);
} catch (error) {
console.error('Search error:', error);
setResults([]);
} finally {
setIsLoading(false);
}
};
performSearch();
}, [query]);
const handleResultClick = async (suggestion) => {
try {
const result = await searchSession.retrieve(suggestion);
const feature = result.features[0];
// Handle result (fly to location, add marker, etc.)
onResultSelect(feature);
setQuery(feature.properties.name);
setResults([]);
} catch (error) {
console.error('Retrieve error:', error);
}
};
return (
<div className="search-container">
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search for a place..."
className="search-input"
/>
{isLoading && <div className="loading">Searching...</div>}
{results.length > 0 && (
<div className="search-results">
{results.map((result) => (
<div key={result.mapbox_id} className="search-result" onClick={() => handleResultClick(result)}>
<strong>{result.name}</strong>
{result.place_formatted && <p>{result.place_formatted}</p>}
</div>
))}
</div>
)}
</div>
);
}优势:
- ✅ 完全控制UI设计
- ✅ Search JS Core自动处理防抖
- ✅ Search JS Core自动处理会话令牌
- ✅ 代码比直接API调用更简洁
注意: 对于React应用,除非需要完全自定义UI设计,否则优先选择Search JS React(选项1)。
iOS: Search SDK for iOS (Recommended)
iOS端:Search SDK for iOS(推荐)
Option 1: Search SDK with UI (Easiest)
选项1:带UI的Search SDK(最简方案)
When to use: iOS app, want pre-built search UI, fastest implementation
Installation:
swift
// Add to Package.swift or SPM
dependencies: [
.package(url: "https://github.com/mapbox/mapbox-search-ios.git", from: "2.0.0")
]Complete implementation with built-in UI:
swift
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchController: MapboxSearchController!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
setupMap()
setupSearchWithUI()
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
func setupSearchWithUI() {
// MapboxSearchController provides complete UI automatically
searchController = MapboxSearchController()
searchController.delegate = self
// Present the search UI
present(searchController, animated: true)
}
}
extension SearchViewController: SearchControllerDelegate {
func searchResultSelected(_ searchResult: SearchResult) {
// SDK handled all the search interaction
// Just respond to selection
mapView.camera.fly(to: CameraOptions(
center: searchResult.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: searchResult.coordinate)
mapView.annotations.pointAnnotations = [annotation]
dismiss(animated: true)
}
}适用场景: iOS应用,需要预构建的搜索UI,追求最快实现速度
安装:
swift
// Add to Package.swift or SPM
dependencies: [
.package(url: "https://github.com/mapbox/mapbox-search-ios.git", from: "2.0.0")
]带内置UI的完整实现:
swift
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchController: MapboxSearchController!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
setupMap()
setupSearchWithUI()
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
func setupSearchWithUI() {
// MapboxSearchController provides complete UI automatically
searchController = MapboxSearchController()
searchController.delegate = self
// Present the search UI
present(searchController, animated: true)
}
}
extension SearchViewController: SearchControllerDelegate {
func searchResultSelected(_ searchResult: SearchResult) {
// SDK handled all the search interaction
// Just respond to selection
mapView.camera.fly(to: CameraOptions(
center: searchResult.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: searchResult.coordinate)
mapView.annotations.pointAnnotations = [annotation]
dismiss(animated: true)
}
}Option 2: Search SDK Core (Custom UI)
选项2:Search SDK Core(自定义UI)
When to use: Need custom UI, integrate with UISearchController, full control over UX
Complete implementation:
swift
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchEngine: SearchEngine!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine(accessToken: "YOUR_MAPBOX_TOKEN")
setupSearchBar()
setupMap()
}
func setupSearchBar() {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let query = searchController.searchBar.text, !query.isEmpty else {
return
}
// Search SDK handles debouncing automatically
searchEngine.search(query: query) { [weak self] result in
switch result {
case .success(let results):
self?.displayResults(results)
case .failure(let error):
print("Search error: \(error)")
}
}
}
func displayResults(_ results: [SearchResult]) {
// Display results in custom table view
// When user selects a result:
handleResultSelection(results[0])
}
func handleResultSelection(_ result: SearchResult) {
mapView.camera.fly(to: CameraOptions(
center: result.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: result.coordinate)
mapView.annotations.pointAnnotations = [annotation]
}
}适用场景: 需要自定义UI,与UISearchController集成,完全控制用户体验
完整实现:
swift
import MapboxSearch
import MapboxMaps
class SearchViewController: UIViewController {
private var searchEngine: SearchEngine!
private var mapView: MapView!
override func viewDidLoad() {
super.viewDidLoad()
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine(accessToken: "YOUR_MAPBOX_TOKEN")
setupSearchBar()
setupMap()
}
func setupSearchBar() {
let searchController = UISearchController(searchResultsController: nil)
searchController.searchResultsUpdater = self
searchController.obscuresBackgroundDuringPresentation = false
navigationItem.searchController = searchController
}
func setupMap() {
mapView = MapView(frame: view.bounds)
view.addSubview(mapView)
}
}
extension SearchViewController: UISearchResultsUpdating {
func updateSearchResults(for searchController: UISearchController) {
guard let query = searchController.searchBar.text, !query.isEmpty else {
return
}
// Search SDK handles debouncing automatically
searchEngine.search(query: query) { [weak self] result in
switch result {
case .success(let results):
self?.displayResults(results)
case .failure(let error):
print("Search error: \(error)")
}
}
}
func displayResults(_ results: [SearchResult]) {
// Display results in custom table view
// When user selects a result:
handleResultSelection(results[0])
}
func handleResultSelection(_ result: SearchResult) {
mapView.camera.fly(to: CameraOptions(
center: result.coordinate,
zoom: 15
))
let annotation = PointAnnotation(coordinate: result.coordinate)
mapView.annotations.pointAnnotations = [annotation]
}
}Option 3: Direct API Integration (Advanced)
选项3:直接API集成(高级)
When to use: Very specific requirements, server-side iOS backend
Important: Only use if SDK doesn't meet your needs. You must handle debouncing and session tokens manually.
swift
// Direct API calls - see Web direct API example
// Not recommended for iOS - use Search SDK instead适用场景: 特定需求,iOS后端服务端集成
重要提示: 仅当SDK无法满足需求时使用。需手动处理防抖和会话令牌。
swift
// Direct API calls - see Web direct API example
// Not recommended for iOS - use Search SDK insteadAndroid: Search SDK for Android (Recommended)
Android端:Search SDK for Android(推荐)
Option 1: Search SDK with UI (Easiest)
选项1:带UI的Search SDK(最简方案)
When to use: Android app, want pre-built search UI, fastest implementation
Installation:
gradle
// Add to build.gradle
dependencies {
implementation 'com.mapbox.search:mapbox-search-android-ui:2.0.0'
implementation 'com.mapbox.maps:android:11.0.0'
}Complete implementation with built-in UI:
kotlin
import com.mapbox.search.ui.view.SearchBottomSheetView
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchView: SearchBottomSheetView
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
mapView = findViewById(R.id.map_view)
// SearchBottomSheetView provides complete UI automatically
searchView = findViewById(R.id.search_view)
searchView.initializeSearch(
savedInstanceState,
SearchBottomSheetView.Configuration()
)
// Handle result selection
searchView.addOnSearchResultClickListener { searchResult ->
// SDK handled all the search interaction
val coordinate = searchResult.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
searchView.hide()
}
}
}适用场景: Android应用,需要预构建的搜索UI,追求最快实现速度
安装:
gradle
// Add to build.gradle
dependencies {
implementation 'com.mapbox.search:mapbox-search-android-ui:2.0.0'
implementation 'com.mapbox.maps:android:11.0.0'
}带内置UI的完整实现:
kotlin
import com.mapbox.search.ui.view.SearchBottomSheetView
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchView: SearchBottomSheetView
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_search)
mapView = findViewById(R.id.map_view)
// SearchBottomSheetView provides complete UI automatically
searchView = findViewById(R.id.search_view)
searchView.initializeSearch(
savedInstanceState,
SearchBottomSheetView.Configuration()
)
// Handle result selection
searchView.addOnSearchResultClickListener { searchResult ->
// SDK handled all the search interaction
val coordinate = searchResult.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
searchView.hide()
}
}
}Option 2: Search SDK Core (Custom UI)
选项2:Search SDK Core(自定义UI)
When to use: Need custom UI, integrate with SearchView, full control over UX
Complete implementation:
kotlin
import com.mapbox.search.SearchEngine
import com.mapbox.search.SearchEngineSettings
import com.mapbox.search.SearchOptions
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchEngine: SearchEngine
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine.createSearchEngine(
SearchEngineSettings("YOUR_MAPBOX_TOKEN")
)
setupSearchView()
setupMap()
}
private fun setupSearchView() {
val searchView = findViewById<SearchView>(R.id.search_view)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
performSearch(query)
return true
}
override fun onQueryTextChange(newText: String): Boolean {
if (newText.length >= 2) {
// Search SDK handles debouncing automatically
performSearch(newText)
}
return true
}
})
}
private fun performSearch(query: String) {
val options = SearchOptions(
countries = listOf("US"),
limit = 5
)
searchEngine.search(query, options) { results ->
results.onSuccess { searchResults ->
displayResults(searchResults)
}.onFailure { error ->
Log.e("Search", "Error: $error")
}
}
}
private fun displayResults(results: List<SearchResult>) {
// Display in custom RecyclerView
handleResultSelection(results[0])
}
private fun handleResultSelection(result: SearchResult) {
val coordinate = result.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
}
}适用场景: 需要自定义UI,与SearchView集成,完全控制用户体验
完整实现:
kotlin
import com.mapbox.search.SearchEngine
import com.mapbox.search.SearchEngineSettings
import com.mapbox.search.SearchOptions
import com.mapbox.maps.MapView
class SearchActivity : AppCompatActivity() {
private lateinit var searchEngine: SearchEngine
private lateinit var mapView: MapView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Initialize Search Engine (SDK handles debouncing and session tokens)
searchEngine = SearchEngine.createSearchEngine(
SearchEngineSettings("YOUR_MAPBOX_TOKEN")
)
setupSearchView()
setupMap()
}
private fun setupSearchView() {
val searchView = findViewById<SearchView>(R.id.search_view)
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
override fun onQueryTextSubmit(query: String): Boolean {
performSearch(query)
return true
}
override fun onQueryTextChange(newText: String): Boolean {
if (newText.length >= 2) {
// Search SDK handles debouncing automatically
performSearch(newText)
}
return true
}
})
}
private fun performSearch(query: String) {
val options = SearchOptions(
countries = listOf("US"),
limit = 5
)
searchEngine.search(query, options) { results ->
results.onSuccess { searchResults ->
displayResults(searchResults)
}.onFailure { error ->
Log.e("Search", "Error: $error")
}
}
}
private fun displayResults(results: List<SearchResult>) {
// Display in custom RecyclerView
handleResultSelection(results[0])
}
private fun handleResultSelection(result: SearchResult) {
val coordinate = result.coordinate
mapView.getMapboxMap().flyTo(
CameraOptions.Builder()
.center(Point.fromLngLat(coordinate.longitude, coordinate.latitude))
.zoom(15.0)
.build()
)
}
}Option 3: Direct API Integration (Advanced)
选项3:直接API集成(高级)
When to use: Very specific requirements, server-side Android backend
Important: Only use if SDK doesn't meet your needs. You must handle debouncing and session tokens manually.
kotlin
// Direct API calls - see Web direct API example
// Not recommended for Android - use Search SDK instead适用场景: 特定需求,Android后端服务端集成
重要提示: 仅当SDK无法满足需求时使用。需手动处理防抖和会话令牌。
kotlin
// Direct API calls - see Web direct API example
// Not recommended for Android - use Search SDK insteadNode.js: Mapbox Search JS Core (Recommended)
Node.js端:Mapbox Search JS Core(推荐)
Option 1: Search JS Core (Recommended)
选项1:Search JS Core(推荐)
When to use: Server-side search, backend API, serverless functions
Installation:
bash
npm install @mapbox/search-js-coreComplete implementation:
javascript
import { SearchSession } from '@mapbox/search-js-core';
// Initialize search session (handles session tokens automatically)
const search = new SearchSession({
accessToken: process.env.MAPBOX_TOKEN
});
// Express.js API endpoint example
app.get('/api/search', async (req, res) => {
const { query, proximity, country } = req.query;
try {
// Get suggestions (Search JS Core handles session management)
const response = await search.suggest(query, {
proximity: proximity ? proximity.split(',').map(Number) : undefined,
country: country,
limit: 10
});
res.json(response.suggestions);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Retrieve full details for a selected result
app.get('/api/search/:id', async (req, res) => {
try {
const result = await search.retrieve(req.params.id);
res.json(result.features[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});Key benefits:
- ✅ Search JS Core handles session tokens automatically
- ✅ Perfect for serverless (Vercel, Netlify, AWS Lambda)
- ✅ Same API as browser Search JS Core
- ✅ No manual debouncing needed (handle at API gateway level)
适用场景: 服务端搜索、后端API、无服务器函数
安装:
bash
npm install @mapbox/search-js-core完整实现:
javascript
import { SearchSession } from '@mapbox/search-js-core';
// Initialize search session (handles session tokens automatically)
const search = new SearchSession({
accessToken: process.env.MAPBOX_TOKEN
});
// Express.js API endpoint example
app.get('/api/search', async (req, res) => {
const { query, proximity, country } = req.query;
try {
// Get suggestions (Search JS Core handles session management)
const response = await search.suggest(query, {
proximity: proximity ? proximity.split(',').map(Number) : undefined,
country: country,
limit: 10
});
res.json(response.suggestions);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Retrieve full details for a selected result
app.get('/api/search/:id', async (req, res) => {
try {
const result = await search.retrieve(req.params.id);
res.json(result.features[0]);
} catch (error) {
res.status(500).json({ error: error.message });
}
});核心优势:
- ✅ Search JS Core自动处理会话令牌
- ✅ 非常适合无服务器环境(Vercel、Netlify、AWS Lambda)
- ✅ 与浏览器端Search JS Core使用同一API
- ✅ 无需手动处理防抖(在API网关层处理)
Option 2: Direct API Integration (Advanced)
选项2:直接API集成(高级)
When to use: Very specific requirements, need features not in Search JS Core
Implementation:
javascript
import fetch from 'node-fetch';
async function searchPlaces(query, options = {}) {
const params = new URLSearchParams({
q: query,
access_token: process.env.MAPBOX_TOKEN,
session_token: generateSessionToken(), // You must manage this
...options
});
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
return response.json();
}Important: Only use direct API calls if Search JS Core doesn't meet your needs. You'll need to handle session tokens manually.
适用场景: Search JS Core无法满足的特定需求
实现:
javascript
import fetch from 'node-fetch';
async function searchPlaces(query, options = {}) {
const params = new URLSearchParams({
q: query,
access_token: process.env.MAPBOX_TOKEN,
session_token: generateSessionToken(), // You must manage this
...options
});
const response = await fetch(`https://api.mapbox.com/search/searchbox/v1/suggest?${params}`);
return response.json();
}重要提示: 仅当Search JS Core无法满足需求时使用直接API调用。需手动管理会话令牌。
Best Practices: "The Good Parts"
最佳实践:"核心要点"
1. Debouncing (CRITICAL for Autocomplete)
1. 防抖处理(自动补全场景必备)
Note: Debouncing is only a concern if you are calling the API directly. Mapbox Search JS and the Search SDKs handle debouncing automatically.
Problem: Every keystroke = API call = expensive + slow
Solution: Wait until user stops typing (for direct API integration)
javascript
let debounceTimeout;
function debouncedSearch(query) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
performSearch(query);
}, 300); // 300ms is optimal for most use cases
}Why 300ms?
- Fast enough to feel responsive
- Slow enough to avoid spam
- Industry standard (Google uses ~300ms)
注意: 仅在直接调用API时需要考虑防抖。Mapbox Search JS和各平台Search SDK会自动处理防抖。
问题: 每次按键都触发API调用 → 成本高+速度慢
解决方案: 用户停止输入后再调用API(仅适用于直接API集成)
javascript
let debounceTimeout;
function debouncedSearch(query) {
clearTimeout(debounceTimeout);
debounceTimeout = setTimeout(() => {
performSearch(query);
}, 300); // 300ms是大多数场景的最优值
}为什么是300ms?
- 足够快,保证响应性
- 足够慢,避免不必要的请求
- 行业标准(Google使用约300ms)
2. Session Token Management
2. 会话令牌管理
Note: Session tokens are only a concern if you are calling the API directly. Mapbox Search JS and the Search SDKs for iOS/Android handle session tokens automatically.
Problem: Search Box API charges per session, not per request
What's a session?
- Starts with first suggest request
- Ends with retrieve request
- Use same token for all requests in session
Implementation (direct API calls only):
javascript
class SearchSession {
constructor() {
this.token = this.generateToken();
}
generateToken() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
async suggest(query) {
// Use this.token for all suggest requests
return fetch(`...?session_token=${this.token}`);
}
async retrieve(id) {
const result = await fetch(`...?session_token=${this.token}`);
// Session ends - generate new token
this.token = this.generateToken();
return result;
}
}Cost impact:
- ✅ Correct: 1 session = unlimited suggests + 1 retrieve = 1 charge
- ❌ Wrong: No session token = each request charged separately
注意: 仅在直接调用API时需要考虑会话令牌。Mapbox Search JS和iOS/Android Search SDK会自动处理会话令牌。
问题: Search Box API按会话计费,而非按请求计费
什么是会话?
- 从第一个suggest请求开始
- 到retrieve请求结束
- 会话内所有请求使用同一令牌
实现(仅适用于直接API调用):
javascript
class SearchSession {
constructor() {
this.token = this.generateToken();
}
generateToken() {
return `${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
}
async suggest(query) {
// Use this.token for all suggest requests
return fetch(`...?session_token=${this.token}`);
}
async retrieve(id) {
const result = await fetch(`...?session_token=${this.token}`);
// Session ends - generate new token
this.token = this.generateToken();
return result;
}
}成本影响:
- ✅ 正确做法:1个会话 = 无限次suggest + 1次retrieve = 1次计费
- ❌ 错误做法:不使用会话令牌 → 每个请求单独计费
3. Geographic Filtering
3. 地理过滤
Always set location context when possible:
javascript
// GOOD: Specific country
{
country: 'US';
}
// GOOD: Proximity to user
{
proximity: [-122.4194, 37.7749];
}
// GOOD: Bounding box for service area
{
bbox: [-122.5, 37.7, -122.3, 37.9];
}
// BAD: No geographic context
{
} // Returns global results, slower, less relevantTip: Use the Location Helper tool to easily calculate bounding boxes for your service area.
Why it matters:
- ✅ Better result relevance
- ✅ Faster response times
- ✅ Lower ambiguity
- ✅ Better user experience
尽可能设置位置上下文:
javascript
// 推荐:特定国家
{
country: 'US';
}
// 推荐:偏向用户位置
{
proximity: [-122.4194, 37.7749];
}
// 推荐:服务区域边界框
{
bbox: [-122.5, 37.7, -122.3, 37.9];
}
// 不推荐:无地理上下文
{
} // 返回全球结果,速度慢,相关性低提示: 使用Location Helper工具轻松计算服务区域的边界框。
重要性:
- ✅ 结果相关性更高
- ✅ 响应速度更快
- ✅ 减少歧义
- ✅ 用户体验更好
4. Error Handling
4. 错误处理
Handle all failure cases:
javascript
async function performSearch(query) {
try {
const response = await fetch(searchUrl);
// Check HTTP status
if (!response.ok) {
if (response.status === 429) {
// Rate limited
showError('Too many requests. Please wait a moment.');
return [];
} else if (response.status === 401) {
// Invalid token
showError('Search is unavailable. Please check configuration.');
return [];
} else {
// Other error
showError('Search failed. Please try again.');
return [];
}
}
const data = await response.json();
// Check for results
if (!data.suggestions || data.suggestions.length === 0) {
showMessage('No results found. Try a different search.');
return [];
}
return data.suggestions;
} catch (error) {
// Network error
console.error('Search error:', error);
showError('Network error. Please check your connection.');
return [];
}
}处理所有失败场景:
javascript
async function performSearch(query) {
try {
const response = await fetch(searchUrl);
// 检查HTTP状态
if (!response.ok) {
if (response.status === 429) {
// 请求超限
showError('请求过多,请稍后再试。');
return [];
} else if (response.status === 401) {
// 令牌无效
showError('搜索功能不可用,请检查配置。');
return [];
} else {
// 其他错误
showError('搜索失败,请重试。');
return [];
}
}
const data = await response.json();
// 检查结果
if (!data.suggestions || data.suggestions.length === 0) {
showMessage('未找到结果,请尝试其他搜索词。');
return [];
}
return data.suggestions;
} catch (error) {
// 网络错误
console.error('Search error:', error);
showError('网络错误,请检查连接。');
return [];
}
}5. Result Display UX
5. 结果展示用户体验
Show enough context for disambiguation:
html
<div class="search-result">
<div class="result-name">Starbucks</div>
<div class="result-address">123 Main St, San Francisco, CA</div>
<div class="result-type">Coffee Shop</div>
</div>Not just:
html
<div>Starbucks</div>
<!-- Which Starbucks? -->展示足够的上下文以消除歧义:
html
<div class="search-result">
<div class="result-name">Starbucks</div>
<div class="result-address">123 Main St, San Francisco, CA</div>
<div class="result-type">Coffee Shop</div>
</div>而不是:
html
<div>Starbucks</div>
<!-- 哪家星巴克? -->6. Loading States
6. 加载状态
Always show loading feedback:
javascript
function performSearch(query) {
showLoadingSpinner();
fetch(searchUrl)
.then((response) => response.json())
.then((data) => {
hideLoadingSpinner();
displayResults(data.suggestions);
})
.catch((error) => {
hideLoadingSpinner();
showError('Search failed');
});
}始终显示加载反馈:
javascript
function performSearch(query) {
showLoadingSpinner();
fetch(searchUrl)
.then((response) => response.json())
.then((data) => {
hideLoadingSpinner();
displayResults(data.suggestions);
})
.catch((error) => {
hideLoadingSpinner();
showError('Search failed');
});
}7. Accessibility
7. 可访问性
Make search keyboard-navigable:
html
<input type="search" role="combobox" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false" />
<ul id="search-results" role="listbox">
<li role="option" tabindex="0">Result 1</li>
<li role="option" tabindex="0">Result 2</li>
</ul>Keyboard support:
- ⬆️⬇️ Arrow keys: Navigate results
- Enter: Select result
- Escape: Close results
使搜索支持键盘导航:
html
<input type="search" role="combobox" aria-autocomplete="list" aria-controls="search-results" aria-expanded="false" />
<ul id="search-results" role="listbox">
<li role="option" tabindex="0">Result 1</li>
<li role="option" tabindex="0">Result 2</li>
</ul>键盘支持:
- ⬆️⬇️ 方向键:导航结果
- Enter:选择结果
- Escape:关闭结果列表
8. Mobile Optimizations
8. 移动端优化
iOS/Android specific considerations:
swift
// iOS: Adjust for keyboard
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { notification in
// Adjust view for keyboard
}
// Handle tap outside to dismiss
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)Make touch targets large enough:
- Minimum: 44x44pt (iOS) / 48x48dp (Android)
- Ensure adequate spacing between results
iOS/Android特定注意事项:
swift
// iOS: 适配键盘
NotificationCenter.default.addObserver(
forName: UIResponder.keyboardWillShowNotification,
object: nil,
queue: .main
) { notification in
// 调整视图以适配键盘
}
// 点击外部关闭键盘
let tapGesture = UITapGestureRecognizer(target: self, action: #selector(dismissKeyboard))
view.addGestureRecognizer(tapGesture)确保触摸目标足够大:
- 最小值:44x44pt(iOS)/ 48x48dp(Android)
- 确保结果之间有足够的间距
9. Caching (For High-Volume Apps)
9. 缓存(高流量应用)
Cache recent/popular searches:
javascript
class SearchCache {
constructor(maxSize = 50) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(query) {
const key = query.toLowerCase();
return this.cache.get(key);
}
set(query, results) {
const key = query.toLowerCase();
// LRU eviction
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
results,
timestamp: Date.now()
});
}
isValid(entry, maxAgeMs = 5 * 60 * 1000) {
return entry && Date.now() - entry.timestamp < maxAgeMs;
}
}
// Usage
const cache = new SearchCache();
async function search(query) {
const cached = cache.get(query);
if (cache.isValid(cached)) {
return cached.results;
}
const results = await performAPISearch(query);
cache.set(query, results);
return results;
}缓存近期/热门搜索:
javascript
class SearchCache {
constructor(maxSize = 50) {
this.cache = new Map();
this.maxSize = maxSize;
}
get(query) {
const key = query.toLowerCase();
return this.cache.get(key);
}
set(query, results) {
const key = query.toLowerCase();
// LRU淘汰策略
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value;
this.cache.delete(firstKey);
}
this.cache.set(key, {
results,
timestamp: Date.now()
});
}
isValid(entry, maxAgeMs = 5 * 60 * 1000) {
return entry && Date.now() - entry.timestamp < maxAgeMs;
}
}
// 使用
const cache = new SearchCache();
async function search(query) {
const cached = cache.get(query);
if (cache.isValid(cached)) {
return cached.results;
}
const results = await performAPISearch(query);
cache.set(query, results);
return results;
}10. Token Security
10. 令牌安全
CRITICAL: Scope tokens properly:
javascript
// Create token with only search scopes
// In Mapbox dashboard or via API:
{
"scopes": [
"search:read",
"styles:read", // Only if showing map
"fonts:read" // Only if showing map
],
"allowedUrls": [
"https://yourdomain.com/*"
]
}Never:
- ❌ Use secret tokens (sk.*) in client-side code
- ❌ Give tokens more scopes than needed
- ❌ Skip URL restrictions on public tokens
See skill for details.
mapbox-token-security关键: 正确设置令牌权限:
javascript
// 创建仅包含搜索权限的令牌
// 在Mapbox控制台或通过API创建:
{
"scopes": [
"search:read",
"styles:read", // 仅在显示地图时需要
"fonts:read" // 仅在显示地图时需要
],
"allowedUrls": [
"https://yourdomain.com/*"
]
}严禁:
- ❌ 在客户端代码中使用秘密令牌(sk.*)
- ❌ 授予令牌超出需求的权限
- ❌ 跳过公开令牌的URL限制
详情请参考技能。
mapbox-token-securityCommon Pitfalls and How to Avoid Them
常见陷阱及避免方法
❌ Pitfall 1: No Debouncing
❌ 陷阱1:未实现防抖
Problem:
javascript
input.addEventListener('input', (e) => {
performSearch(e.target.value); // API call on EVERY keystroke!
});Impact:
- 🔥 Expensive (hundreds of unnecessary API calls)
- 🐌 Slow (race conditions, outdated results)
- 💥 Rate limiting (429 errors)
Solution: Always debounce (see Best Practice #1)
问题:
javascript
input.addEventListener('input', (e) => {
performSearch(e.target.value); // 每次按键都触发API调用!
});影响:
- 🔥 成本高(数百次不必要的API调用)
- 🐌 速度慢(竞态条件、过时结果)
- 💥 请求超限(429错误)
解决方案: 始终实现防抖(参见最佳实践#1)
❌ Pitfall 2: Ignoring Session Tokens
❌ 陷阱2:忽略会话令牌
Problem:
javascript
// No session token = each request charged separately
fetch('...suggest?q=query&access_token=xxx');Impact:
- 💰 Costs 10-100x more than necessary
- Budget blown on redundant charges
Solution: Use session tokens (see Best Practice #2)
问题:
javascript
// 无会话令牌 → 每个请求单独计费
fetch('...suggest?q=query&access_token=xxx');影响:
- 💰 成本是正确做法的10-100倍
- 预算因冗余计费而超支
解决方案: 使用会话令牌(参见最佳实践#2)
❌ Pitfall 3: No Geographic Context
❌ 陷阱3:无地理上下文
Problem:
javascript
// Searching globally for "Paris"
{
q: 'Paris';
} // Paris, France? Paris, Texas? Paris, Kentucky?Impact:
- 😕 Confusing results (wrong country)
- 🐌 Slower responses
- 😞 Poor user experience
Solution:
javascript
// Much better
{ q: 'Paris', country: 'US', proximity: user_location }问题:
javascript
// 全球范围搜索"Paris"
{
q: 'Paris';
} // 法国巴黎?美国得克萨斯州巴黎?美国肯塔基州巴黎?影响:
- 😕 结果混淆(错误的国家)
- 🐌 响应速度慢
- 😞 用户体验差
解决方案:
javascript
// 更好的做法
{ q: 'Paris', country: 'US', proximity: user_location }❌ Pitfall 4: Poor Mobile UX
❌ 陷阱4:移动端用户体验差
Problem:
html
<!-- Tiny touch targets -->
<div style="height: 20px; padding: 2px;">Search result</div>Impact:
- 😤 Frustrating to tap
- 🎯 Accidental selections
- ⭐ Bad reviews
Solution:
css
.search-result {
min-height: 48px; /* Android minimum */
padding: 12px;
margin: 4px 0;
}问题:
html
<!-- 过小的触摸目标 -->
<div style="height: 20px; padding: 2px;">Search result</div>影响:
- 😤 点击困难
- 🎯 误选
- ⭐ 差评
解决方案:
css
.search-result {
min-height: 48px; /* Android最小值 */
padding: 12px;
margin: 4px 0;
}❌ Pitfall 5: Not Handling Empty Results
❌ 陷阱5:未处理空结果
Problem:
javascript
// Just shows empty container
displayResults([]); // User sees blank space - is it loading? broken?Impact:
- ❓ User confusion
- 🤔 Is it working?
Solution:
javascript
if (results.length === 0) {
showMessage('No results found. Try a different search term.');
}问题:
javascript
// 仅显示空容器
displayResults([]); // 用户看到空白区域 - 是加载中?还是故障?影响:
- ❓ 用户困惑
- 🤔 功能是否正常?
解决方案:
javascript
if (results.length === 0) {
showMessage('未找到结果,请尝试其他搜索词。');
}❌ Pitfall 6: Blocking on Slow Networks
❌ 陷阱6:慢网络下阻塞应用
Problem:
javascript
// No timeout = waits forever on slow network
await fetch(searchUrl);Impact:
- ⏰ Appears frozen
- 😫 User frustration
Solution:
javascript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch(searchUrl, { signal: controller.signal }).finally(() => clearTimeout(timeout));问题:
javascript
// 无超时设置 → 在慢网络下无限等待
await fetch(searchUrl);影响:
- ⏰ 应用看似冻结
- 😫 用户沮丧
解决方案:
javascript
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 5000);
fetch(searchUrl, { signal: controller.signal }).finally(() => clearTimeout(timeout));❌ Pitfall 7: Ignoring Result Types
❌ 陷阱7:忽略结果类型
Problem:
javascript
// Treating all results the same
displayResult(result.name); // But is it an address? POI? Region?Impact:
- 🤷 Unclear what was selected
- 🗺️ Wrong zoom level
- 📍 Inappropriate markers
Solution:
javascript
function handleResult(result) {
const type = result.feature_type;
if (type === 'poi') {
map.flyTo({ center: coords, zoom: 17 }); // Close zoom
addPOIMarker(result);
} else if (type === 'address') {
map.flyTo({ center: coords, zoom: 16 });
addAddressMarker(result);
} else if (type === 'place') {
map.flyTo({ center: coords, zoom: 12 }); // Wider view for city
}
}问题:
javascript
// 所有结果一视同仁
displayResult(result.name); // 但它是地址?POI?区域?影响:
- 🤷 选择的内容不明确
- 🗺️ 错误的缩放级别
- 📍 不合适的标记
解决方案:
javascript
function handleResult(result) {
const type = result.feature_type;
if (type === 'poi') {
map.flyTo({ center: coords, zoom: 17 }); // 近距离缩放
addPOIMarker(result);
} else if (type === 'address') {
map.flyTo({ center: coords, zoom: 16 });
addAddressMarker(result);
} else if (type === 'place') {
map.flyTo({ center: coords, zoom: 12 }); // 城市级宽视图
}
}❌ Pitfall 8: Race Conditions
❌ 陷阱8:竞态条件
Problem:
javascript
// Fast typing: "san francisco"
// API responses arrive out of order:
// "san f" results arrive AFTER "san francisco" resultsImpact:
- 🔀 Wrong results displayed
- 😵 Confusing UX
Solution:
javascript
let searchCounter = 0;
async function performSearch(query) {
const currentSearch = ++searchCounter;
const results = await fetchResults(query);
// Only display if this is still the latest search
if (currentSearch === searchCounter) {
displayResults(results);
}
}问题:
javascript
// 快速输入:"san francisco"
// API响应顺序混乱:
// "san f"的结果在"san francisco"的结果之后到达影响:
- 🔀 显示错误的结果
- 😵 混淆的用户体验
解决方案:
javascript
let searchCounter = 0;
async function performSearch(query) {
const currentSearch = ++searchCounter;
const results = await fetchResults(query);
// 仅当这是最新的搜索请求时才显示结果
if (currentSearch === searchCounter) {
displayResults(results);
}
}Framework-Specific Guidance
框架特定指导
React Best Practices
React最佳实践
Best Practice: Use Search JS React or Search JS Core instead of building custom hooks with direct API calls.
最佳实践: 使用Search JS React或Search JS Core,而非通过直接API调用自定义钩子。
Option 1: Use Search JS React (Recommended)
选项1:使用Search JS React(推荐)
javascript
import { SearchBox } from '@mapbox/search-js-react';
// Easiest - just use the SearchBox component
function MyComponent() {
return (
<SearchBox
accessToken="YOUR_TOKEN"
onRetrieve={(result) => {
// Handle result
}}
options={{
country: 'US',
types: 'address,poi'
}}
/>
);
}javascript
import { SearchBox } from '@mapbox/search-js-react';
// 最简方案 - 直接使用SearchBox组件
function MyComponent() {
return (
<SearchBox
accessToken="YOUR_TOKEN"
onRetrieve={(result) => {
// 处理结果
}}
options={{
country: 'US',
types: 'address,poi'
}}
/>
);
}Option 2: Custom Hook with Search JS Core
选项2:基于Search JS Core的自定义钩子
javascript
import { useState, useCallback, useRef, useEffect } from 'react';
import { SearchSession } from '@mapbox/search-js-core';
// Custom hook using Search JS Core (handles debouncing and session tokens)
function useMapboxSearch(accessToken, options = {}) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Search JS Core handles session tokens automatically
const searchSessionRef = useRef(null);
useEffect(() => {
searchSessionRef.current = new SearchSession({ accessToken });
}, [accessToken]);
const search = useCallback(
async (query) => {
if (!query || query.length < 2) {
setResults([]);
return;
}
setIsLoading(true);
setError(null);
try {
// Search JS Core handles debouncing and session tokens
const response = await searchSessionRef.current.suggest(query, options);
setResults(response.suggestions || []);
} catch (err) {
setError(err.message);
setResults([]);
} finally {
setIsLoading(false);
}
},
[options]
);
const retrieve = useCallback(async (suggestion) => {
try {
// Search JS Core handles session tokens automatically
const result = await searchSessionRef.current.retrieve(suggestion);
return result.features[0];
} catch (err) {
setError(err.message);
throw err;
}
}, []);
return { results, isLoading, error, search, retrieve };
}Benefits of using Search JS Core:
- ✅ No manual session token management
- ✅ No manual debouncing needed
- ✅ No race condition handling needed (SDK handles it)
- ✅ Cleaner, simpler code
- ✅ Production-ready error handling built-in
javascript
import { useState, useCallback, useRef, useEffect } from 'react';
import { SearchSession } from '@mapbox/search-js-core';
// 使用Search JS Core的自定义钩子(自动处理防抖和会话令牌)
function useMapboxSearch(accessToken, options = {}) {
const [results, setResults] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [error, setError] = useState(null);
// Search JS Core自动处理会话令牌
const searchSessionRef = useRef(null);
useEffect(() => {
searchSessionRef.current = new SearchSession({ accessToken });
}, [accessToken]);
const search = useCallback(
async (query) => {
if (!query || query.length < 2) {
setResults([]);
return;
}
setIsLoading(true);
setError(null);
try {
// Search JS Core自动处理防抖和会话令牌
const response = await searchSessionRef.current.suggest(query, options);
setResults(response.suggestions || []);
} catch (err) {
setError(err.message);
setResults([]);
} finally {
setIsLoading(false);
}
},
[options]
);
const retrieve = useCallback(async (suggestion) => {
try {
// Search JS Core自动处理会话令牌
const result = await searchSessionRef.current.retrieve(suggestion);
return result.features[0];
} catch (err) {
setError(err.message);
throw err;
}
}, []);
return { results, isLoading, error, search, retrieve };
}使用Search JS Core的优势:
- ✅ 无需手动管理会话令牌
- ✅ 无需手动处理防抖
- ✅ 无需处理竞态条件(SDK已处理)
- ✅ 代码更简洁
- ✅ 内置生产级错误处理
Vue Composition API (Using Search JS Core - Recommended)
Vue组合式API(使用Search JS Core - 推荐)
javascript
import { ref, watch } from 'vue';
import { SearchSession } from '@mapbox/search-js-core';
export function useMapboxSearch(accessToken, options = {}) {
const query = ref('');
const results = ref([]);
const isLoading = ref(false);
// Use Search JS Core - handles debouncing and session tokens automatically
const searchSession = new SearchSession({ accessToken });
const performSearch = async (searchQuery) => {
if (!searchQuery || searchQuery.length < 2) {
results.value = [];
return;
}
isLoading.value = true;
try {
// Search JS Core handles debouncing and session tokens
const response = await searchSession.suggest(searchQuery, options);
results.value = response.suggestions || [];
} catch (error) {
console.error('Search error:', error);
results.value = [];
} finally {
isLoading.value = false;
}
};
// Watch query changes (Search JS Core handles debouncing)
watch(query, (newQuery) => {
performSearch(newQuery);
});
const retrieve = async (suggestion) => {
// Search JS Core handles session tokens automatically
const feature = await searchSession.retrieve(suggestion);
return feature;
};
return {
query,
results,
isLoading,
retrieve
};
}Key benefits:
- ✅ Search JS Core handles debouncing automatically (no lodash needed)
- ✅ Session tokens managed automatically (no manual token generation)
- ✅ Simpler code, fewer dependencies
- ✅ Same API works in browser and Node.js
javascript
import { ref, watch } from 'vue';
import { SearchSession } from '@mapbox/search-js-core';
export function useMapboxSearch(accessToken, options = {}) {
const query = ref('');
const results = ref([]);
const isLoading = ref(false);
// 使用Search JS Core - 自动处理防抖和会话令牌
const searchSession = new SearchSession({ accessToken });
const performSearch = async (searchQuery) => {
if (!searchQuery || searchQuery.length < 2) {
results.value = [];
return;
}
isLoading.value = true;
try {
// Search JS Core自动处理防抖和会话令牌
const response = await searchSession.suggest(searchQuery, options);
results.value = response.suggestions || [];
} catch (error) {
console.error('Search error:', error);
results.value = [];
} finally {
isLoading.value = false;
}
};
// 监听查询变化(Search JS Core自动处理防抖)
watch(query, (newQuery) => {
performSearch(newQuery);
});
const retrieve = async (suggestion) => {
// Search JS Core自动处理会话令牌
const feature = await searchSession.retrieve(suggestion);
return feature;
};
return {
query,
results,
isLoading,
retrieve
};
}核心优势:
- ✅ Search JS Core自动处理防抖(无需lodash)
- ✅ 会话令牌自动管理(无需手动生成)
- ✅ 代码更简洁,依赖更少
- ✅ 同一API适用于浏览器和Node.js
Testing Strategy
测试策略
Unit Tests
单元测试
javascript
// Mock fetch for testing
global.fetch = jest.fn();
describe('MapboxSearch', () => {
beforeEach(() => {
fetch.mockClear();
});
test('debounces search requests', async () => {
const search = new MapboxSearch('fake-token');
// Rapid-fire searches
search.search('san');
search.search('san f');
search.search('san fr');
search.search('san francisco');
// Wait for debounce
await new Promise((resolve) => setTimeout(resolve, 400));
// Should only make one API call
expect(fetch).toHaveBeenCalledTimes(1);
});
test('handles empty results', async () => {
fetch.mockResolvedValue({
ok: true,
json: async () => ({ suggestions: [] })
});
const search = new MapboxSearch('fake-token');
const results = await search.performSearch('xyz');
expect(results).toEqual([]);
});
test('handles API errors', async () => {
fetch.mockResolvedValue({
ok: false,
status: 429
});
const search = new MapboxSearch('fake-token');
const results = await search.performSearch('test');
expect(results).toEqual([]);
});
});javascript
// Mock fetch用于测试
global.fetch = jest.fn();
describe('MapboxSearch', () => {
beforeEach(() => {
fetch.mockClear();
});
test('防抖搜索请求', async () => {
const search = new MapboxSearch('fake-token');
// 快速连续搜索
search.search('san');
search.search('san f');
search.search('san fr');
search.search('san francisco');
// 等待防抖超时
await new Promise((resolve) => setTimeout(resolve, 400));
// 应仅发起一次API调用
expect(fetch).toHaveBeenCalledTimes(1);
});
test('处理空结果', async () => {
fetch.mockResolvedValue({
ok: true,
json: async () => ({ suggestions: [] })
});
const search = new MapboxSearch('fake-token');
const results = await search.performSearch('xyz');
expect(results).toEqual([]);
});
test('处理API错误', async () => {
fetch.mockResolvedValue({
ok: false,
status: 429
});
const search = new MapboxSearch('fake-token');
const results = await search.performSearch('test');
expect(results).toEqual([]);
});
});Integration Tests
集成测试
javascript
describe('Search Integration', () => {
test('complete search flow', async () => {
const search = new MapboxSearch(process.env.MAPBOX_TOKEN);
// Perform search
const suggestions = await search.performSearch('San Francisco');
expect(suggestions.length).toBeGreaterThan(0);
// Retrieve first result
const feature = await search.retrieve(suggestions[0].mapbox_id);
expect(feature.geometry.coordinates).toBeDefined();
expect(feature.properties.name).toBe('San Francisco');
});
});javascript
describe('Search Integration', () => {
test('完整搜索流程', async () => {
const search = new MapboxSearch(process.env.MAPBOX_TOKEN);
// 执行搜索
const suggestions = await search.performSearch('San Francisco');
expect(suggestions.length).toBeGreaterThan(0);
// 获取第一个结果的详情
const feature = await search.retrieve(suggestions[0].mapbox_id);
expect(feature.geometry.coordinates).toBeDefined();
expect(feature.properties.name).toBe('San Francisco');
});
});Monitoring and Analytics
监控与分析
Track Key Metrics
跟踪关键指标
javascript
// Track search usage
function trackSearch(query, resultsCount) {
analytics.track('search_performed', {
query_length: query.length,
results_count: resultsCount,
had_results: resultsCount > 0
});
}
// Track selections
function trackSelection(result, position) {
analytics.track('search_result_selected', {
result_type: result.feature_type,
result_position: position,
had_address: !!result.properties.full_address
});
}
// Track errors
function trackError(errorType, query) {
analytics.track('search_error', {
error_type: errorType,
query_length: query.length
});
}javascript
// 跟踪搜索使用情况
function trackSearch(query, resultsCount) {
analytics.track('search_performed', {
query_length: query.length,
results_count: resultsCount,
had_results: resultsCount > 0
});
}
// 跟踪结果选择
function trackSelection(result, position) {
analytics.track('search_result_selected', {
result_type: result.feature_type,
result_position: position,
had_address: !!result.properties.full_address
});
}
// 跟踪错误
function trackError(errorType, query) {
analytics.track('search_error', {
error_type: errorType,
query_length: query.length
});
}Monitor for Issues
监控问题
- 📊 Zero-result rate (should be < 20%)
- ⚡ Average response time
- 💥 Error rate
- 🎯 Selection rate (users selecting vs abandoning)
- 💰 API usage vs budget
- 📊 零结果率(应<20%)
- ⚡ 平均响应时间
- 💥 错误率
- 🎯 选择率(用户选择结果 vs 放弃搜索)
- 💰 API使用量 vs 预算
Checklist: Production-Ready Search
生产就绪搜索检查清单
Before launching, verify:
Configuration:
- Token properly scoped (search:read only)
- URL restrictions configured
- Geographic filtering set (country, proximity, or bbox)
- Types parameter set based on use case
- Language parameter set if needed
Implementation:
- Debouncing implemented (300ms recommended)
- Session tokens used correctly
- Error handling for all failure cases
- Loading states shown
- Empty results handled gracefully
- Race conditions prevented
UX:
- Touch targets at least 44pt/48dp
- Results show enough context (name + address)
- Keyboard navigation works
- Accessibility attributes set
- Mobile keyboard handled properly
Performance:
- Caching implemented (if high volume)
- Request timeout set
- Minimal data fetched
- Bundle size optimized
Testing:
- Unit tests for core logic
- Integration tests with real API
- Tested on slow networks
- Tested with various query types
- Mobile device testing
Monitoring:
- Analytics tracking set up
- Error logging configured
- Usage monitoring in place
- Budget alerts configured
上线前,请验证以下内容:
配置:
- 令牌权限正确(仅search:read)
- 配置了URL限制
- 设置了地理过滤(country、proximity或bbox)
- 根据使用场景设置了types参数
- 必要时设置了language参数
实现:
- 实现了防抖(推荐300ms)
- 正确使用了会话令牌
- 处理了所有失败场景的错误
- 显示了加载状态
- 优雅处理了空结果
- 防止了竞态条件
用户体验:
- 触摸目标至少为44pt/48dp
- 结果显示了足够的上下文(名称+地址)
- 键盘导航正常工作
- 设置了可访问性属性
- 正确处理了移动端键盘
性能:
- 实现了缓存(高流量场景)
- 设置了请求超时
- 获取的数据最少
- 优化了包大小
测试:
- 核心逻辑的单元测试
- 与真实API的集成测试
- 慢网络下的测试
- 各种查询类型的测试
- 移动设备测试
监控:
- 设置了分析跟踪
- 配置了错误日志
- 监控了使用情况
- 配置了预算警报
Integration with Other Skills
与其他技能的集成
Works with:
- mapbox-search-patterns: Parameter selection and optimization
- mapbox-web-integration-patterns: Framework-specific patterns
- mapbox-token-security: Token management and security
- mapbox-web-performance-patterns: Optimizing search performance
可与以下技能配合使用:
- mapbox-search-patterns:参数选择与优化
- mapbox-web-integration-patterns:框架特定模式
- mapbox-token-security:令牌管理与安全
- mapbox-web-performance-patterns:优化搜索性能
Resources
资源
Quick Decision Guide
快速决策指南
User says: "I need location search"
- Ask discovery questions (Questions 1-6 above)
- Recommend product:
- Search Box API (most cases)
- Direct API (custom needs)
- Platform SDK (mobile)
- Implement with:
- ✅ Debouncing
- ✅ Session tokens
- ✅ Geographic filtering
- ✅ Error handling
- ✅ Good UX
- Test thoroughly
- Monitor in production
Remember: The best search implementation asks the right questions first, then builds exactly what the user needs - no more, no less.
用户说:"我需要地点搜索功能"
- 提出调研问题(上述问题1-6)
- 推荐产品:
- Search Box API(大多数场景)
- 直接API(自定义需求)
- 平台SDK(移动应用)
- 实现时包含:
- ✅ 防抖
- ✅ 会话令牌
- ✅ 地理过滤
- ✅ 错误处理
- ✅ 良好的用户体验
- 全面测试
- 生产环境监控
记住: 最佳的搜索实现始于提出正确的问题,然后构建用户真正需要的功能——不多不少。