search_products_lucene
Search Open Food Facts products using Lucene query syntax for advanced filtering, boolean logic, and negation capabilities.
Instructions
Search Open Food Facts using the Search-a-licious Elasticsearch backend. Powered by Lucene query syntax with full boolean logic and negation support.
Use this instead of search_products_standard when you need:
Negation queries: find gluten-free cereals with allergens_tags_without="en:gluten"
Filter-only browsing: categories_tags without any text query (standard API times out on this)
Combined text + filter with relevance scoring: text matches are ranked by relevance within filter results
Boolean logic in raw Lucene: brands:"kellogg*" OR brands:"nestle"
Trade-offs vs search_products_standard:
Counts are approximate (capped at 10,000 for large result sets)
Brand tag matching may be narrower (less normalization than standard)
Data has a short sync delay (hours) from the primary database
popularity sort uses scan counts rather than the standard popularity algorithm
Response format matches search_products_standard: { count, page, page_size, page_count, products: [...] }
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| query | No | Free-text search terms. Combined with any filter params using AND logic. Omit to browse by filters alone (unlike search_products_standard, filter-only queries work here without timeouts). | |
| categories_tags | No | Filter by category tag (e.g. "en:breakfast-cereals"). Added as categories_tags:"value" in the Lucene query. | |
| brands_tags | No | Filter by brand tag (e.g. "nutella"). Added as brands_tags:"value" in the Lucene query. | |
| nutrition_grades_tags | No | Filter by Nutri-Score grade (a, b, c, d, e). Added as nutriscore_grade:"value". | |
| labels_tags | No | Filter by label tag (e.g. "en:organic", "en:fair-trade"). Added as labels_tags:"value". | |
| countries_tags | No | Filter by country tag (e.g. "en:united-kingdom", "en:france"). Added as countries_tags:"value". | |
| allergens_tags_without | No | EXCLUDE products containing this allergen (e.g. "en:gluten", "en:milk"). This is negation — a capability unique to this tool. Added as -allergens_tags:"value". Use for allergen-free searches. | |
| lucene_query | No | Raw Lucene query string for full control. If provided, all other filter params are ignored. Supports field:value, negation (-field:value), quoted phrases, wildcards. Examples: 'categories_tags:"en:beverages" nutriscore_grade:a -allergens_tags:"en:gluten"', 'brands:"kellogg*"' | |
| sort_by | No | Sort order. Note: uses different underlying fields than search_products_standard. | |
| sort_descending | No | Sort in descending order (default: true). Set false for ascending (e.g. lowest nutriscore_score first). | |
| page | No | Page number (default: 1) | |
| page_size | No | Results per page (default: 24, max: 100) | |
| fields | No | Fields to return per product. Defaults to: code, product_name, brands, categories, nutriscore_grade, nova_group, image_url, quantity |
Implementation Reference
- The actual handler function for the `search_products_lucene` tool which constructs the Lucene query, fetches from the Search-a-licious API, normalizes the response, and returns the result.
async (args) => { const luceneQuery = args.lucene_query ?? buildLuceneQuery(args); const url = new URL(`${SEARCH_A_LICIOUS_BASE}/search`); url.searchParams.set('q', luceneQuery); url.searchParams.set('page', String(args.page)); url.searchParams.set('page_size', String(args.page_size)); if (args.sort_by) { const mappedField = SORT_FIELD_MAP[args.sort_by] ?? args.sort_by; const prefix = args.sort_descending ? '-' : ''; url.searchParams.set('sort_by', `${prefix}${mappedField}`); } const fields = args.fields ?? DEFAULT_FIELDS; url.searchParams.set('fields', fields.join(',')); const response = await fetch(url.toString(), { headers: { 'User-Agent': config.userAgent, Accept: 'application/json', }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Search-a-licious API error: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json() as Record<string, unknown>; const normalized = normalizeResponse(data); return jsonResult(normalized); }, ); } - Zod input schema for the `search_products_lucene` tool.
const inputSchema = strictSchemaWithAliases( { query: z.string().optional().describe('Free-text search terms. Combined with any filter params using AND logic. Omit to browse by filters alone (unlike search_products_standard, filter-only queries work here without timeouts).'), categories_tags: z.string().optional().describe('Filter by category tag (e.g. "en:breakfast-cereals"). Added as categories_tags:"value" in the Lucene query.'), brands_tags: z.string().optional().describe('Filter by brand tag (e.g. "nutella"). Added as brands_tags:"value" in the Lucene query.'), nutrition_grades_tags: z.string().optional().describe('Filter by Nutri-Score grade (a, b, c, d, e). Added as nutriscore_grade:"value".'), labels_tags: z.string().optional().describe('Filter by label tag (e.g. "en:organic", "en:fair-trade"). Added as labels_tags:"value".'), countries_tags: z.string().optional().describe('Filter by country tag (e.g. "en:united-kingdom", "en:france"). Added as countries_tags:"value".'), allergens_tags_without: z.string().optional().describe('EXCLUDE products containing this allergen (e.g. "en:gluten", "en:milk"). This is negation — a capability unique to this tool. Added as -allergens_tags:"value". Use for allergen-free searches.'), lucene_query: z.string().optional().describe('Raw Lucene query string for full control. If provided, all other filter params are ignored. Supports field:value, negation (-field:value), quoted phrases, wildcards. Examples: \'categories_tags:"en:beverages" nutriscore_grade:a -allergens_tags:"en:gluten"\', \'brands:"kellogg*"\''), sort_by: z.enum([ 'popularity', 'product_name', 'created_t', 'last_modified_t', 'nutriscore_score', 'ecoscore_score', ]).optional().describe('Sort order. Note: uses different underlying fields than search_products_standard.'), sort_descending: z.boolean().optional().default(true).describe('Sort in descending order (default: true). Set false for ascending (e.g. lowest nutriscore_score first).'), page: z.number().int().min(1).default(1).describe('Page number (default: 1)'), page_size: z.number().int().min(1).max(100).default(24).describe('Results per page (default: 24, max: 100)'), fields: z.array(z.string()).optional().describe(`Fields to return per product. Defaults to: ${DEFAULT_FIELDS.join(', ')}`), }, { q: 'query', search: 'query', }, ); - src/tools/search-products-lucene.ts:127-187 (registration)Registration function for `search_products_lucene` within the MCP server.
export function registerSearchProductsLucene(server: McpServer, config: Config): void { server.registerTool( 'search_products_lucene', { title: 'Search products (Lucene)', description: `Search Open Food Facts using the Search-a-licious Elasticsearch backend. Powered by Lucene query syntax with full boolean logic and negation support. Use this instead of search_products_standard when you need: - Negation queries: find gluten-free cereals with allergens_tags_without="en:gluten" - Filter-only browsing: categories_tags without any text query (standard API times out on this) - Combined text + filter with relevance scoring: text matches are ranked by relevance within filter results - Boolean logic in raw Lucene: brands:"kellogg*" OR brands:"nestle" Trade-offs vs search_products_standard: - Counts are approximate (capped at 10,000 for large result sets) - Brand tag matching may be narrower (less normalization than standard) - Data has a short sync delay (hours) from the primary database - popularity sort uses scan counts rather than the standard popularity algorithm Response format matches search_products_standard: { count, page, page_size, page_count, products: [...] }`, inputSchema, annotations: { readOnlyHint: true, }, }, async (args) => { const luceneQuery = args.lucene_query ?? buildLuceneQuery(args); const url = new URL(`${SEARCH_A_LICIOUS_BASE}/search`); url.searchParams.set('q', luceneQuery); url.searchParams.set('page', String(args.page)); url.searchParams.set('page_size', String(args.page_size)); if (args.sort_by) { const mappedField = SORT_FIELD_MAP[args.sort_by] ?? args.sort_by; const prefix = args.sort_descending ? '-' : ''; url.searchParams.set('sort_by', `${prefix}${mappedField}`); } const fields = args.fields ?? DEFAULT_FIELDS; url.searchParams.set('fields', fields.join(',')); const response = await fetch(url.toString(), { headers: { 'User-Agent': config.userAgent, Accept: 'application/json', }, }); if (!response.ok) { const errorText = await response.text(); throw new Error(`Search-a-licious API error: ${response.status} ${response.statusText} - ${errorText}`); } const data = await response.json() as Record<string, unknown>; const normalized = normalizeResponse(data); return jsonResult(normalized); }, ); }