Back to the blog ·

A product advisor for your own shop

A shop has a problem that grows with the number of items: customers know what they want, but not what you call it. The built-in search compares strings. Type "something warm for autumn in dark green" and you get nothing – even though you stock exactly that.

Vector search exists for this. What follows is the setup I built for my own shop: two n8n workflows, a vector database, a language model, and Telegram as the interface.

Two halves

Deliberately two separate workflows, because they run on completely different rhythms.

The sync fetches the catalogue and writes it into the database as vectors. It runs rarely – on catalogue changes or on a schedule.

The chat answers individual questions. It runs on every message.

That split is not a matter of taste. Producing an embedding costs time and money; doing it for the entire catalogue on every query would be absurd. The catalogue is translated once and then sits ready.

What belongs in a product document

The step that decides the quality of the whole thing is an unremarkable one: what exactly are you vectorising?

The naive answer is "the product description". On Shopify that is HTML, full of markup, sizing tables and shipping notes that are identical on every item. Vectorise that and all products start to resemble each other – the shared ballast outweighs the differences.

Better is a deliberately assembled text per product: title, cleaned description, product type, tags, variants. Short, dense, no repetition. Price and stock do not belong in the vectorised text but alongside it as metadata – they change, and they are irrelevant to what a product means. Nobody searches semantically for "19.90".

Why search is a tool, not a fixed step

The obvious design would be: question in, search, hits to the language model, answer out. Fixed sequence, always the same.

I hung vector search on an agent as a tool instead. The model decides for itself whether to search. On "hello" it does not. On "do you have that in blue?" it searches using context from the previous message. On "how long does shipping take?" it does not search at all, because that is not a product question.

The price is one extra model call per request. The gain is a conversation that does not feel like a form.

The bug I found while looking

Laying the two workflows side by side, I noticed that the sync produces its vectors with a model from OpenAI while the chat searches with a local model through Ollama. Two different models, one shared vector space – that cannot work, and in the worst case nobody notices.

Because that failure is general rather than specific to my setup, it has a post of its own.

Two details that are easy to miss

Memory needs a key. The store holding the conversation has to be separated per user, and the key for that is the Telegram chat ID. Without it, every user shares one history – user B gets answers built on user A's conversation. In a shop context that is not a cosmetic flaw, it is a data leak.

The number of hits is a trade-off. Too few and the right product is missing. Too many and the model drowns in options and turns vague. Ten is a workable starting point for a modest catalogue – but it is a dial you tune with real questions, not with guesses.

What I do not like about the result

Two things worth knowing before you copy this.

The model answers fluently even when the hits are useless. There is no built-in doubt. When the search turns up nothing suitable, a language model will invent a plausible recommendation rather than say "we don't have that". You have to instruct it explicitly – and check even then.

And the catalogue ages. Between two sync runs the bot recommends items that may be sold out. For prices and stock there is no way around querying them fresh at answer time instead of trusting the last import.

An advisor like this is not magic, it is a chain of individually unremarkable building blocks. It gets interesting where the chain breaks – and it almost always breaks at the inconspicuous links: the document, the memory key, the embedding model.