Appearance
Product Search for Dify
This page describes the standalone product-search contract merged by backend PR #564 at final head 74c9cf996c8f2bbf944f9f0b54ddf62136cc62cd. The API implementation is source-verified at that revision. The Dify migration checklist is documented by the PR author; the live workflow, deployment and end-to-end cutover were not verified for this page.
Ownership and request flow
Dify owns conversation history and resolves each customer turn into one complete query. For example, a follow-up such as “the same in beige under 600 euros” must become a standalone product request that retains the intended product type and other criteria. The backend stores no conversation/search state and returns no confirmation token.
The backend resolves the configured store and collection, loads the collection schema, asks OpenAI for one typed intent, validates fields/operators/units and business constraints, then sends a normal Typesense search. A clarification exits before the product search; schema lookup and intent planning may already have run. The backend verifies returned hits against the compiled constraints and projects allowed response fields.
HTTP contract
Both methods use /api/v2/products/search inside Sanctum authentication, require the auth-basic token ability, and have the route throttle 30,1 (30 requests per minute). Use a chatbot bearer token in the authorization header, never in query parameters. Keep credentials in the integration's approved secret storage.
| Input | Contract |
|---|---|
GET q or query | Required standalone string, at most 2,000 characters. q is a GET-only alias; when both names are supplied, their values must match. Use query parameters and no request body. |
POST query | Required standalone string, at most 2,000 characters, in a JSON body. q is not a POST query alias. |
store_id | Required positive integer resolving to a server-configured store. No client-selected collection or fallback to another store. |
per_page | Optional positive integer up to 250; omission or null uses the configured default (7 in source). The effective limit is capped by global and catalog settings (global default 20). Read meta.per_page for the effective limit. |
response_format | Optional typesense (default) or multi_search. This selects the result wrapper, not a different search pipeline. |
Illustrative requests; store 3 must be configured in the target environment:
http
GET /api/v2/products/search?q=Canap%C3%A9%20beige%20moins%20de%20600%20euros&store_id=3&per_page=7
Accept: application/json
Authorization: Bearer <chatbot-service-token>json
{
"query": "Un canapé beige à moins de 600 euros, prix croissant",
"store_id": 3,
"per_page": 7,
"response_format": "typesense"
}For POST, send Content-Type: application/json and the same bearer authentication. The examples contain placeholders, not working credentials or a production destination.
The removed fields message, history, conversation_id, search_state and confirmed_search are rejected with HTTP 422 even when present as null. Correct the adapter instead of silently retrying a different contract.
Legacy Typesense options such as nl_query, nl_model_id, query_by, page, search_cutoff_ms, exhaustive_search and exclude_fields are ignored. The server owns search fields, filters, limits and deadlines. The adapter returns the first page; it does not provide client-controlled pagination or a generic Typesense proxy.
Sources: request validation, route and authorization, controller.
Responses and branching
Successful search results include data.products, data.intent and meta. Each product has id, sku, name, price, currency, url, image and allowed attributes. Missing catalog values can produce null fields; url and image accept only HTTP(S) URLs. image comes from the catalog's image field, so do not assume the backend synthesizes it from firstImage.
The default wrapper also has top-level hits[].document and found. The alternative wrapper has results[0].hits and results[0].found. Projected hit documents can retain fields such as firstImage, image, final_price and url when present and allowed by server configuration. Hit highlight and highlights payloads are removed.
found / meta.candidate_found remain the provider's candidate count. They are not the number of verified products returned; use meta.returned or data.products.length for that. meta.excluded_unverified counts discarded candidates, and meta.partial signals exclusions or provider cutoff. A response can be matches with partial: true; show only its verified products without claiming the search is exhaustive.
| HTTP | Body outcome | Consumer action |
|---|---|---|
| 200 | success: true, status: matches | Render verified products, preserving API order when requested sorting matters. |
| 200 | success: true, status: no_matches | State that no matches were found for these criteria. Do not silently relax them. |
| GET 422 / POST 200 | success: false, status: clarification | Ask the returned message, resolve the answer in Dify, then send a new complete query. No product search or data result is returned. |
| GET 422 / POST 200 | success: false, status: incomplete_results | Explain that the returned candidates could not verify the criteria. Do not treat this as a genuine zero-match result. |
| 422 | status: invalid_request, validation errors | Correct the input. Service-level errors may instead expose error.code / error.stage. |
| 401 / 403 / 429 | Authentication, ability or throttle rejection | Handle as an access/rate-limit failure; the middleware response need not use the search envelope. |
| 502 / 503 / 504 | success: false, status: error, unavailable or timeout | Report a search failure. Unknown/unconfigured catalogs fail closed; never replace failure with empty cards. |
A clarification example (timings and ID vary):
json
{
"success": false,
"status": "clarification",
"message": "À quel type de produit faites-vous référence ?",
"error": {"code": "clarification_required", "stage": "validation"},
"meta": {"request_id": "<uuid>", "timings": {}}
}Branch on both HTTP status and the body. In particular, POST HTTP 200 alone does not mean a search succeeded. Controller-generated responses include Cache-Control: private, no-store.
Sources: response projection, counts and error mapping, GET status mapping, compiler clarification and constraints.
Dify migration checklist
The backend PR's integration notes describe an existing Typesense GET node, a card formatter, a separate direct /multi_search fallback, and accumulated results. Those workflow details are author-reported and not independently verified here. Confirm them against the workflow version being changed.
- Resolve history into one complete query, retaining constraints that the customer has not changed. Send the query and the configured store; do not send conversation fields or the old Typesense prompt wrapper.
- Point the primary GET node at the approved chatbot API origin and path, using the chatbot bearer credential. Keep
response_formatunset/typesenseif the consumer expects top-levelhits; validate the actual parser against the projected catalog fields. - Give non-2xx GET responses a separate clarification/error branch. For POST, inspect
successandstatuseven on HTTP 200. Do not automatically retry clarification/validation failures as a broader search. - Disable any direct Typesense fallback that would bypass the validated pipeline on failure or zero matches. A genuine
no_matchesis final for the submitted criteria until the customer agrees to change them. - Clear prior result accumulators for a new resolved search while retaining conversation history. Remove unconditional formatter sorting if it overrides the customer's requested order.
- Test success, real zero matches, clarification, incomplete results, malformed input, access failures, unknown store and provider timeout/outage. Also test a follow-up with retained criteria, changed criteria, and descending sort to detect stale cards or formatter overrides.
A successful response-parser replay alone does not establish that the live Dify error paths, fallback, state reset or ordering are correct.
Server configuration and diagnostics
The feature is disabled by default (PRODUCT_SEARCH_ENABLED=false). A deployable configuration requires PRODUCT_SEARCH_ENABLED=true, OPENAI_API_KEY, TYPESENSE_URL, TYPESENSE_API_KEY, TYPESENSE_STORES_JSON, and PRODUCT_SEARCH_MISSING_ATTRIBUTE_POLICY=exclude. Each configured store needs collection, currency, nonempty query_by, and nonempty allowed_fields; an explicitly allowed collection template can supply the collection instead. Legacy store mappings remain fallback inputs. Do not infer that a target environment is configured or enabled from this page.
Typesense provider connection settings and catalog mappings are server-owned. typesense.stores takes precedence over legacy product_search.stores; otherwise a collection template must explicitly allow the requested store ID. Catalog configuration supplies currency, query/allowed/response fields, unit mappings and applicable mandatory/availability constraints. A client cannot override those settings. Curated hits are sent through provider-side filtering with filter_curated_hits=true.
Source defaults are a 15-second pipeline deadline, 10-second OpenAI timeout, 3-second Typesense timeout and 2-second Typesense connection timeout; the remaining pipeline budget can shorten a stage. These are configuration defaults, not a latency promise. Provider credentials and collection names do not belong in Dify request payloads or customer-facing messages.
Use the returned request ID and stage timings to separate configuration, schema, planner, validation and provider failures. The dedicated product_search logger records a query hash and timings; query/intent payload logging is off by default. Its optional redaction covers email-like strings and phone-like numbers, not every kind of sensitive text, so do not enable payload logging casually or copy customer queries into issues.
Sources: feature configuration, Typesense configuration, catalog resolution, provider client, planner, service diagnostics.
Evidence and acceptance limits
Agent-led source review on 2026-10-19 checked the final merged API implementation and feature tests. reviewed_at records this evidence review, not human acceptance or deployment approval. The final API reference reports local success for 60 product-search tests with 444 assertions, PHPStan, Pint, and Rector. Those checks were not rerun against a live provider for this documentation task, and GitHub exposes no final-head remote CI result.
The source change is merged. Deployment, target catalog configuration, live provider outage handling, owner acceptance, and Dify cutover remain unverified. Publishing this page does not enable the endpoint, migrate Dify, or deploy either system. Production inspection or changes require approval for each exact command and target under the repository guidance.