搜索服务 #
搜索服务概述 #
为应用添加搜索功能可以极大提升用户体验。Heroku 提供了多种搜索服务 Add-ons。
服务对比 #
| 服务 | 特点 | 适用场景 |
|---|---|---|
| Elasticsearch | 功能强大,自定义程度高 | 复杂搜索需求 |
| Algolia | 快速部署,开箱即用 | 即时搜索 |
| Bonsai | Elasticsearch 托管 | 经济实惠 |
Elasticsearch #
安装配置 #
bash
# 安装 Bonsai Elasticsearch
heroku addons:create bonsai:sandbox
# 查看配置
heroku config:get BONSAI_URL
Node.js 集成 #
bash
npm install @elastic/elasticsearch
javascript
const { Client } = require('@elastic/elasticsearch');
const client = new Client({
node: process.env.BONSAI_URL
});
// 创建索引
async function createIndex() {
await client.indices.create({
index: 'products',
body: {
mappings: {
properties: {
name: { type: 'text' },
description: { type: 'text' },
price: { type: 'float' },
category: { type: 'keyword' },
created_at: { type: 'date' }
}
}
}
});
}
// 索引文档
async function indexProduct(product) {
await client.index({
index: 'products',
id: product.id,
body: {
name: product.name,
description: product.description,
price: product.price,
category: product.category,
created_at: new Date()
}
});
}
// 批量索引
async function bulkIndex(products) {
const body = products.flatMap(product => [
{ index: { _index: 'products', _id: product.id } },
product
]);
await client.bulk({ body });
}
// 搜索
async function searchProducts(query) {
const { body } = await client.search({
index: 'products',
body: {
query: {
multi_match: {
query: query,
fields: ['name', 'description'],
fuzziness: 'AUTO'
}
},
highlight: {
fields: {
name: {},
description: {}
}
}
}
});
return body.hits.hits.map(hit => ({
...hit._source,
score: hit._score,
highlight: hit.highlight
}));
}
// 高级搜索
async function advancedSearch(params) {
const { query, category, minPrice, maxPrice, sort } = params;
const { body } = await client.search({
index: 'products',
body: {
query: {
bool: {
must: [
{
multi_match: {
query: query,
fields: ['name^2', 'description']
}
}
],
filter: [
...(category ? [{ term: { category } }] : []),
...(minPrice || maxPrice ? [{
range: {
price: {
...(minPrice && { gte: minPrice }),
...(maxPrice && { lte: maxPrice })
}
}
}] : [])
]
}
},
sort: sort ? [{ [sort.field]: sort.order }] : undefined
}
});
return body.hits.hits.map(hit => hit._source);
}
Python 集成 #
bash
pip install elasticsearch
python
from elasticsearch import Elasticsearch
es = Elasticsearch([os.environ.get('BONSAI_URL')])
# 创建索引
es.indices.create(index='products', body={
'mappings': {
'properties': {
'name': {'type': 'text'},
'description': {'type': 'text'},
'price': {'type': 'float'}
}
}
})
# 索引文档
es.index(index='products', id=1, body={
'name': 'Product Name',
'description': 'Product Description',
'price': 99.99
})
# 搜索
results = es.search(index='products', body={
'query': {
'multi_match': {
'query': 'search term',
'fields': ['name', 'description']
}
}
})
同步数据库 #
javascript
// 同步 PostgreSQL 到 Elasticsearch
const { Pool } = require('pg');
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
async function syncProducts() {
const { rows: products } = await pool.query('SELECT * FROM products');
const body = products.flatMap(product => [
{ index: { _index: 'products', _id: product.id } },
{
name: product.name,
description: product.description,
price: parseFloat(product.price),
category: product.category
}
]);
if (body.length > 0) {
await client.bulk({ body });
}
console.log(`Synced ${products.length} products`);
}
// 增量同步
async function syncProduct(productId) {
const { rows: [product] } = await pool.query(
'SELECT * FROM products WHERE id = $1',
[productId]
);
if (product) {
await client.index({
index: 'products',
id: product.id,
body: product
});
}
}
Algolia #
安装配置 #
bash
# 安装 Algolia
heroku addons:create algolia:community
# 查看配置
heroku config:get ALGOLIA_APP_ID
heroku config:get ALGOLIA_API_KEY
Node.js 集成 #
bash
npm install algoliasearch
javascript
const algoliasearch = require('algoliasearch');
const client = algoliasearch(
process.env.ALGOLIA_APP_ID,
process.env.ALGOLIA_API_KEY
);
const index = client.initIndex('products');
// 配置索引
async function configureIndex() {
await index.setSettings({
searchableAttributes: ['name', 'description', 'category'],
attributesForFaceting: ['category', 'price'],
customRanking: ['desc(popularity)', 'desc(created_at)'],
hitsPerPage: 20
});
}
// 添加文档
async function addProduct(product) {
await index.saveObject({
objectID: product.id,
name: product.name,
description: product.description,
price: product.price,
category: product.category,
popularity: product.popularity || 0,
created_at: new Date().toISOString()
});
}
// 批量添加
async function addProducts(products) {
const objects = products.map(product => ({
objectID: product.id,
...product
}));
await index.saveObjects(objects);
}
// 搜索
async function searchProducts(query, options = {}) {
const { hits, nbHits, page, nbPages } = await index.search(query, {
page: options.page || 0,
hitsPerPage: options.hitsPerPage || 20,
facets: options.facets || ['category'],
facetFilters: options.facetFilters
});
return {
results: hits,
total: nbHits,
page,
totalPages: nbPages
};
}
// 分面搜索
async function searchWithFacets(query, filters) {
const { hits, facets } = await index.search(query, {
facets: ['category', 'price'],
facetFilters: filters
});
return {
results: hits,
facets
};
}
// 删除文档
async function deleteProduct(productId) {
await index.deleteObject(productId);
}
// 部分更新
async function updateProduct(productId, updates) {
await index.partialUpdateObject({
objectID: productId,
...updates
});
}
Python 集成 #
bash
pip install algoliasearch
python
from algoliasearch.search_client import SearchClient
client = SearchClient.create(
os.environ.get('ALGOLIA_APP_ID'),
os.environ.get('ALGOLIA_API_KEY')
)
index = client.init_index('products')
# 添加文档
index.save_object({
'objectID': 1,
'name': 'Product Name',
'description': 'Product Description',
'price': 99.99
})
# 搜索
results = index.search('query', {
'page': 0,
'hitsPerPage': 20
})
前端集成 #
html
<!-- 即时搜索 UI -->
<script src="https://cdn.jsdelivr.net/npm/algoliasearch@4/dist/algoliasearch-lite.umd.js"></script>
<script src="https://cdn.jsdelivr.net/npm/instantsearch.js@4"></script>
<div id="searchbox"></div>
<div id="hits"></div>
<div id="pagination"></div>
<script>
const search = instantsearch({
indexName: 'products',
searchClient: algoliasearch('APP_ID', 'SEARCH_ONLY_API_KEY')
});
search.addWidgets([
instantsearch.widgets.searchBox({
container: '#searchbox'
}),
instantsearch.widgets.hits({
container: '#hits',
templates: {
item: `
<div>
<h3>{{name}}</h3>
<p>{{description}}</p>
<span>${{price}}</span>
</div>
`
}
}),
instantsearch.widgets.pagination({
container: '#pagination'
})
]);
search.start();
</script>
同步策略 #
javascript
// 实时同步
async function onProductChange(change) {
const product = change.after.data();
if (!change.after.exists) {
await index.deleteObject(change.before.id);
} else {
await index.saveObject({
objectID: change.after.id,
...product
});
}
}
// 定时全量同步
const cron = require('node-cron');
cron.schedule('0 3 * * *', async () => {
console.log('Starting full sync...');
await syncAllProducts();
});
搜索功能 #
自动补全 #
javascript
// Elasticsearch Suggest
async function getSuggestions(query) {
const { body } = await client.search({
index: 'products',
body: {
suggest: {
product_suggest: {
prefix: query,
completion: {
field: 'suggest',
size: 10
}
}
}
}
});
return body.suggest.product_suggest[0].options;
}
// Algolia
const index = client.initIndex('products');
index.setSettings({
searchableAttributes: ['name', 'category']
});
// 前端自动补全
search.addWidgets([
instantsearch.widgets.autocomplete({
container: '#autocomplete',
placeholder: 'Search products'
})
]);
拼写纠错 #
javascript
// Elasticsearch
async function searchWithSpellCheck(query) {
const { body } = await client.search({
index: 'products',
body: {
query: {
match: {
name: {
query: query,
fuzziness: 'AUTO'
}
}
},
suggest: {
text: query,
simple_phrase: {
phrase: {
field: 'name.trigram',
size: 1
}
}
}
}
});
return {
results: body.hits.hits,
suggestion: body.suggest.simple_phrase[0].options[0]?.text
};
}
高亮显示 #
javascript
// Elasticsearch
async function searchWithHighlight(query) {
const { body } = await client.search({
index: 'products',
body: {
query: {
multi_match: {
query: query,
fields: ['name', 'description']
}
},
highlight: {
pre_tags: ['<mark>'],
post_tags: ['</mark>'],
fields: {
name: {},
description: {}
}
}
}
});
return body.hits.hits.map(hit => ({
...hit._source,
highlight: hit.highlight
}));
}
性能优化 #
索引优化 #
javascript
// Elasticsearch 索引设置
await client.indices.create({
index: 'products',
body: {
settings: {
number_of_shards: 3,
number_of_replicas: 1,
analysis: {
analyzer: {
product_analyzer: {
type: 'custom',
tokenizer: 'standard',
filter: ['lowercase', 'stemmer']
}
}
}
}
}
});
// Algolia 设置
await index.setSettings({
searchableAttributes: ['name', 'description'],
customRanking: ['desc(popularity)'],
typoTolerance: true,
ignorePlurals: true
});
缓存策略 #
javascript
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 });
async function cachedSearch(query) {
const cacheKey = `search:${query}`;
const cached = cache.get(cacheKey);
if (cached) {
return cached;
}
const results = await searchProducts(query);
cache.set(cacheKey, results);
return results;
}
最佳实践 #
1. 索引设计 #
javascript
// 合理设计索引结构
const productMapping = {
name: {
type: 'text',
analyzer: 'standard',
fields: {
keyword: { type: 'keyword' },
suggest: { type: 'completion' }
}
},
description: { type: 'text' },
price: { type: 'float' },
category: { type: 'keyword' },
tags: { type: 'keyword' },
created_at: { type: 'date' }
};
2. 搜索体验 #
javascript
// 提供多种搜索选项
async function enhancedSearch(params) {
const { query, filters, sort, page } = params;
return await index.search(query, {
page,
hitsPerPage: 20,
facetFilters: filters,
sort,
attributesToHighlight: ['name', 'description'],
attributesToRetrieve: ['name', 'description', 'price', 'image']
});
}
3. 监控搜索 #
javascript
// 记录搜索日志
async function logSearch(query, results, userId) {
await db.query(
'INSERT INTO search_logs (query, results_count, user_id, created_at) VALUES ($1, $2, $3, $4)',
[query, results.length, userId, new Date()]
);
}
// 分析搜索数据
async function getSearchAnalytics() {
const { rows } = await db.query(`
SELECT query, COUNT(*) as count
FROM search_logs
WHERE created_at > NOW() - INTERVAL '7 days'
GROUP BY query
ORDER BY count DESC
LIMIT 20
`);
return rows;
}
故障排查 #
搜索无结果 #
bash
# 检查索引状态
heroku run "curl $BONSAI_URL/_cat/indices?v"
# 检查文档数量
heroku run "curl $BONSAI_URL/products/_count"
# 检查映射
heroku run "curl $BONSAI_URL/products/_mapping"
性能问题 #
bash
# 检查索引大小
heroku run "curl $BONSAI_URL/_cat/indices?v&h=index,store.size,docs.count"
# 检查搜索性能
heroku run "curl -X GET '$BONSAI_URL/products/_search?pretty' -H 'Content-Type: application/json' -d '{\"query\":{\"match_all\":{}},\"profile\":true}'"
下一步 #
搜索服务掌握后,接下来学习 自定义域名 了解域名配置!
最后更新:2026-03-28