Vector product search¶
vector-product-search
Add vector retrieval beside lexical product search, with one embedding per purchasable unit and exact SKU lookup kept first. Use when adding embedding search, hybrid ranking, or re-embedding after catalog edits.
Version 0.3.2 · MIT · Source · Rule
Install this skill
Copy the skill and its rule into your project:
The skill goes to .agents/skills/ and the rule to .agents/rules/. See Command line for other directories.
Use this skill when lexical search is not enough for paraphrased queries. Facets, stock, and lexical ranking stay in the marketplace search skill. This skill adds a vector index. It does not replace SKU and GTIN lookup.
Do not claim a recall or conversion number. Choose a model, record its id and dimension, and measure queries you have labeled.
Procedure¶
- Embed one text per purchasable unit. A variant with its own title or attributes gets its own vector. Copy the parent name into that text.
- Use the same model, dimension, and similarity metric for indexing and querying. Record
model_idand a hash of the embedded text on the document. - Keep price, stock, and facet ids as filter fields. Do not rely on the embedding to enforce "blue" or "in stock".
- Resolve an exact normalized SKU or GTIN before any vector query. If it hits one unit, return that unit first.
- Combine lexical and vector scores with a formula you write down. Re-embed when the text hash changes, not when only the price changes.
Normalization and hybrid scoring are in the index notes. Worked examples and checks are in evals/tasks.json.
What is embedded¶
Include the fields a shopper might paraphrase: title, brand, category names, and attribute labels. Run the same text normalization as lexical search, including Persian Yeh, Kaf, digits, and ZWNJ removal, before you embed.
Exclude price, stock counts, and SKUs from the embedded text. Price and stock change without the words changing, and a SKU is handled by exact match. If you later include them, the content hash must change when they change.
Query¶
- Normalize the query.
- Exact identifier match wins.
- Otherwise run the lexical query and the vector query with the same filters (facet ids, stock policy, category).
- Fuse the two ranked lists. A default you may start from is a weighted sum after each list is normalized to the same scale. Change the weight only when labeled queries say so.
- Page with a keyset on the fused score plus
unit_id.
Apply filters in both retrievals. A vector hit that ignores the selected color is a bug.
Re-embedding¶
- Store
model_id, dimension, metric (cosineunless the index documents another), andtext_hash. - A catalog write recomputes the hash. If the hash or the model id differs, enqueue that unit.
- Switching models rebuilds the index. Do not query an index with a different model than the one that wrote it.
- Delete the vector when the unit is deleted. A parent rebuild replaces that parent's vectors as a set.
Gotchas¶
- Embedding only the parent returns one vector for every size and color.
- Cosine and dot product are not interchangeable unless the vectors are normalized and you meant that equivalence.
- A vector score must not outrank an exact SKU match.
- Indexing the raw Persian string and querying the normalized string misses. Normalize both.
- Do not log shopper queries next to identifiers you are not allowed to retain. The query text can contain an address or a name the shopper pasted.
Verification¶
- A SKU query returns that unit even when a nearer vector belongs to another product.
- A color filter removes other colors from the vector results, not only from the lexical results.
- Changing the price does not change
text_hash. Changing the title does, and that unit is re-embedded. - Deleting a variant removes its vector.
Evaluation¶
Worked examples and the checks they must satisfy are in evals/tasks.json.
References¶
Vector index beside lexical search¶
Document¶
| Field | Role |
|---|---|
unit_id |
Purchasable unit. Primary key. |
parent_id |
Set for variants. |
sku, gtin |
Exact match. Not part of the embedded text. |
text_hash |
Hash of the normalized text that was embedded. |
model_id |
Embedding model identifier and its dimension. |
embedding |
Vector from that model only. |
facet_ids, in_stock, price, currency |
Filters and display. Not a reason to re-embed. |
Embedded text¶
Normalize with the marketplace search normalizer before hashing and embedding. The hash is of the normalized text. If you change the normalizer, rebuild the index. Old hashes would not match the new text.
Fusion¶
Normalize each retrieval's scores onto [0, 1] within that result set, then:
Start at lexical_weight = 0.6 and vector_weight = 0.4 if you have no labeled queries yet. This is a starting point, not a measured optimum. Units that appear in only one list use 0 for the missing score.
Exact SKU or GTIN matches skip fusion and are placed first.
Metric¶
Write down cosine similarity or the index's metric next to model_id. Query with that metric. Do not mix cosine in one environment and inner product in another unless both sides use normalized vectors and you have recorded that they match.
Evaluation¶
Fixtures from evals/tasks.json. See evaluation fixtures for how the checks work.
sku-before-vector¶
Return the product whose SKU matches the query before any nearest-vector result.
Must include: sku, vector
Must not include: embedding of the price
Worked example
Normalize the query and look up sku and gtin first.
Only if that lookup misses, run the vector query. The embedding text does not include the price.
reembed-on-title¶
Re-embed a variant when its title changes and skip re-embedding when only the price changes.
Must include: text_hash, model_id
Must not include: cosine and dot product
Worked example
Store model_id and text_hash next to the vector.
A title edit changes the hash and enqueues that unit. A price edit leaves the hash in place because price is a filter field.