openvibers/openvibe.deals Commits

Search documents say each thing once (#200)

OP OpenVibers committed 11 days ago · 3caca1b6a2 · 3 files, +28 −6

* Search documents say each thing once: an offer's body leaves out its title; no description, a price-and-store summary

A result's snippet reads the summary and body together: an offer's read "FuelCell 796V4 Tennis
Shoes FuelCell 796V4 Tennis Shoes zappos.com 89.95 USD as of 2026-11-01T…" (14 of 38 Deals snippets
repeated a phrase), and an offer with no description had no summary. An offer's summary is its
description, else "$89.95 at zappos.com, as of Nov 1, 2026."; its body holds only a long
description, that price line beside a description, and a product name that differs from the
title. A product's body leaves out its name.

* An offer without a price says "Price not stated" in its summary line

missing-price.test read the body for "price not stated"; the line now lives in the summary when
the offer has no description. It reads "Price not stated, at <store>." and the test reads the
summary and body together, and refuses $0.00.

* A product's Search body: one line a store, its price and day, without the product's name again

Every product snippet repeated: its body listed each store's offer by a title that is the
product's name, with an ISO time. Each line is now "$89.95 at zappos.com, as of Nov 1, 2026.",
prefixed with the offer's own title only where it differs.

Changes

ddiff --git a/server/domain/indexing.js b/server/domain/indexing.js
iindex b9e134d..bd95cbf 100644
---- a/server/domain/indexing.js
++++ b/server/domain/indexing.js
@@@ -58,20 +58,33 @@ function createIndexing({ config, store, publication, outbox, catalog, indexnow
}, { traceparent });
}
++ // A result's snippet reads the summary and the body together, so the body holds only what the title and summary do
++ // not: an offer's body repeated its title twice ("FuelCell 796V4 Tennis Shoes\nFuelCell 796V4 Tennis Shoes\n…",
++ // 2026-11-02) and an offer with no description had no summary at all.
++ const same = (a, b) => String(a || '').trim().toLowerCase() === String(b || '').trim().toLowerCase();
++ const MONTHS = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
++ const day = (t) => { const d = new Date(t); return Number.isNaN(d.getTime()) ? '' : `${MONTHS[d.getUTCMonth()]} ${d.getUTCDate()}, ${d.getUTCFullYear()}`; };
++ const money = (price, currency) => (currency === 'USD' || !currency ? `$${Number(price).toFixed(2)}` : `${Number(price).toFixed(2)} ${currency}`);
++ function offerLine(v) {
++ const store = v.store && (v.store.name || v.store.domain);
++ if (!v.latest || v.latest.price == null) return `Price not stated${store ? `, at ${store}` : ''}.`;
++ return `${money(v.latest.price, v.latest.currency)}${store ? ` at ${store}` : ''}, as of ${day(v.latest.observed_at)}.`;
++ }
++
function offerDocument(v) {
const r = v.root;
const provenance = v.sources.filter((s) => s.ref_service === 'sources' && !s.removed_at).slice(0, 40).map((s) => ({
service: 'sources', type: 'item', id: s.ref_id, ...(Number.isInteger(s.ref_revision) ? { revision: s.ref_revision } : {}),
url: s.url || undefined, retrievedAt: s.retrieved_at || undefined, label: s.source_key || undefined,
}));
-- const priceText = v.latest && v.latest.price != null ? `${v.latest.price}${v.latest.currency ? ` ${v.latest.currency}` : ''} as of ${iso(v.latest.observed_at)}` : 'price not stated';
return hooks.buildIndexDocument({
owner: 'deals', type: 'offer', id: r.id, revision: 0,
state: r.status === 'disabled' ? 'unpublished' : 'published', visibility: 'public',
canonicalUrl: publication.abs(publication.offerPath(r)),
title: r.title,
-- summary: r.description ? r.description.slice(0, 1000) : null,
-- body: [r.title, r.description, v.product && v.product.name, v.store && (v.store.name || v.store.domain), priceText].filter(Boolean).join('\n'),
++ summary: r.description ? r.description.slice(0, 1000) : offerLine(v) || null,
++ body: [r.description && r.description.length > 1000 ? r.description : '', r.description ? offerLine(v) : '',
++ v.product && !same(v.product.name, r.title) ? v.product.name : ''].filter(Boolean).join('\n'),
facets: {
status: r.status, freshness: v.freshness.state, store: v.store ? v.store.domain : null, category: r.category,
currency: v.latest && v.latest.currency ? v.latest.currency : null, product: v.product ? v.product.slug : null,
@@@ -97,12 +110,14 @@ function createIndexing({ config, store, publication, outbox, catalog, indexnow
async function indexProduct(product) {
if (!product) return null;
const pv = await publication.productView(product);
-- const lines = pv.offers.filter((v) => v.root.status === 'active').slice(0, 20).map((v) => `${v.root.title}${v.latest && v.latest.price != null ? ` — ${v.latest.price} ${v.latest.currency || ''} as of ${iso(v.latest.observed_at)}` : ''}`);
++ // One line a store: its price and day, with the offer's own title only where it differs from the product's
++ // (every line began with the same name and an ISO time: 19 of 19 product snippets repeated, 2026-11-02).
++ const lines = pv.offers.filter((v) => v.root.status === 'active').slice(0, 20).map((v) => [same(v.root.title, product.name) ? '' : v.root.title, offerLine(v)].filter(Boolean).join(': '));
const doc = hooks.buildIndexDocument({
owner: 'deals', type: 'product', id: product.id, revision: 0, state: 'published', visibility: 'public',
canonicalUrl: publication.abs(publication.productPath(product)), title: product.name,
summary: product.description || null,
-- body: [product.name, product.brand, ...pv.aliases.map((a) => a.value), ...lines].filter(Boolean).join('\n'),
++ body: [product.brand, ...pv.aliases.map((a) => a.value).filter((a) => !same(a, product.name)), ...lines].filter(Boolean).join('\n'),
facets: { brand: product.brand || null, category: product.category || null, offers: pv.offers.filter((v) => v.root.status === 'active').length },
authorship: { mode: 'human' },
decision: pv.decision,
ddiff --git a/test/freshness.test.js b/test/freshness.test.js
iindex 34145b3..b347100 100644
---- a/test/freshness.test.js
++++ b/test/freshness.test.js
@@@ -68,6 +68,11 @@ const { sourceItem } = require('./helpers/mocks');
assert.strictEqual(doc.facets.freshness, 'stale');
assert.strictEqual(doc.indexability.decision, 'noindex');
assert.ok(doc.revision > before[before.length - 1].payload.revision);
++ // The document says what the page says once: the title stays out of the body, and an offer without a description
++ // is summed up by its price and store (2026-11-02: bodies repeated the title twice, summaries were empty).
++ assert.ok(!String(doc.body || '').toLowerCase().includes(String(doc.title).toLowerCase()), `the body does not repeat the title: ${doc.body}`);
++ assert.ok(doc.summary, 'a summary');
++ assert.ok(!/T\d\d:\d\d/.test(`${doc.summary} ${doc.body || ''}`), 'no ISO timestamps in the text');
await t.ctx.worker.tick();
assert.strictEqual((await t.events('deals.index_document.upserted')).filter((e) => e.payload.id === offer.id).length, after.length);
});
ddiff --git a/test/missing-price.test.js b/test/missing-price.test.js
iindex 52b3ffd..4d9cb1e 100644
---- a/test/missing-price.test.js
++++ b/test/missing-price.test.js
@@@ -86,7 +86,9 @@ const { sourceItem } = require('./helpers/mocks');
const item = feed.json().items.find((i) => i.url.endsWith(noPrice.slug));
assert.match(item.summary, /Price not stated/);
const doc = (await t.events('deals.index_document.upserted')).filter((e) => e.payload.id === noPrice.id).pop().payload;
-- assert.match(doc.body, /price not stated/);
++ // The summary carries the price line when the offer has no description; either way it is "not stated", never 0.
++ assert.match(`${doc.summary || ''} ${doc.body || ''}`, /price not stated/i);
++ assert.doesNotMatch(`${doc.summary || ''} ${doc.body || ''}`, /\$0\.00|\b0 USD/);
assert.strictEqual(doc.facets.currency, undefined);
});