Skip to content

Commit

Permalink
Merge pull request #547 from Pinelab-studio/feat/fpgrowth-impl
Browse files Browse the repository at this point in the history
New Plugin: Frequently bought together products
  • Loading branch information
martijnvdbrug authored Dec 12, 2024
2 parents e6548d9 + 9dd000d commit 956a23d
Show file tree
Hide file tree
Showing 22 changed files with 2,793 additions and 2,067 deletions.
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.0.0 (2024-12-12)

- Implemented FPGrowth algorithm for defining frequently bought together relations
- Added experiment mode to test the support level needed
- Return sorted list of related products based on support level

# 0.0.2 (2024-12-03)

- Patched compatibility range to >= 2.2.0
Expand Down
111 changes: 111 additions & 0 deletions packages/vendure-plugin-frequently-bought-together/LICENSE
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
Checkout https://www.tldrlegal.com/license/business-source-license-bsl-1-1
for a short summary of the BSL 1.1 license.
Disclaimer: the link above is only a short summary of the Full Text. The license below is leading.

--------------------------------------------------------------------------------

Licensor: Pinelab

Licensed Work: Pinelab Frequently Bought Together plugin for Vendure
The Licensed Work is Copyright © 2024 Pinelab

Additional Use Grant: 1. License Purchase Requirement: The right to use the
Licensed Work in a production environment is granted only
when a valid license has been purchased from the
Vendure Plugin Marketplace. Proof of purchase must be
maintained and provided upon request by the Licensor.

2. Single Project Restriction: You are permitted to use
the Licensed Work in the production environment of only one
project. For the purposes of this License, a "project" is
defined as one or more server instances of the Vendure
application connected to a single production database.
Use of the Licensed Work in multiple projects or multiple
production databases is prohibited without obtaining an
additional license from the Licensor.

The terms of the License will be governed by and construed
in accordance with Dutch law of the Netherlands.

Change Date: Three years from release date

Change License: GNU General Public License (GPL) v3

--------------------------------------------------------------------------------

Business Source License 1.1

Terms

The Licensor hereby grants you the right to copy, modify, create derivative
works, redistribute, and make non-production use of the Licensed Work. The
Licensor may make an Additional Use Grant, above, permitting limited production
use.

Effective on the Change Date, or the fourth anniversary of the first publicly
available distribution of a specific version of the Licensed Work under this
License, whichever comes first, the Licensor hereby grants you rights under the
terms of the Change License, and the rights granted in the paragraph above
terminate.

If your use of the Licensed Work does not comply with the requirements currently
in effect as described in this License, you must purchase a commercial license
from the Licensor, its affiliated entities, or authorized resellers, or you must
refrain from using the Licensed Work.

All copies of the original and modified Licensed Work, and derivative works of
the Licensed Work, are subject to this License. This License applies separately
for each version of the Licensed Work and the Change Date may vary for each
version of the Licensed Work released by Licensor.

You must conspicuously display this License on each original or modified copy of
the Licensed Work. If you receive the Licensed Work in original or modified form
from a third party, the terms and conditions set forth in this License apply to
your use of that work.

Any use of the Licensed Work in violation of this License will automatically
terminate your rights under this License for the current and all other versions
of the Licensed Work.

This License does not grant you any right in any trademark or logo of Licensor
or its affiliates (provided that you may use a trademark or logo of Licensor as
expressly required by this License).

TO THE EXTENT PERMITTED BY APPLICABLE LAW, THE LICENSED WORK IS PROVIDED ON AN
“AS IS” BASIS. LICENSOR HEREBY DISCLAIMS ALL WARRANTIES AND CONDITIONS, EXPRESS
OR IMPLIED, INCLUDING (WITHOUT LIMITATION) WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE, NON-INFRINGEMENT, AND TITLE.

MariaDB hereby grants you permission to use this License’s text to license your
works, and to refer to it using the trademark “Business Source License”, as long
as you comply with the Covenants of Licensor below.

Covenants of Licensor

In consideration of the right to use this License’s text and the “Business
Source License” name and trademark, Licensor covenants to MariaDB, and to all
other recipients of the licensed work to be provided by Licensor:

1. To specify as the Change License the GPL Version 2.0 or any later version,
or a license that is compatible with GPL Version 2.0 or a later version,
where “compatible” means that software provided under the Change License can
be included in a program with software provided under GPL Version 2.0 or a
later version. Licensor may specify additional Change Licenses without
limitation.

2. To either: (a) specify an additional grant of rights to use that does not
impose any additional restriction on the right granted in this License, as
the Additional Use Grant; or (b) insert the text “None”.

3. To specify a Change Date.

4. Not to modify this License in any other way.

Notice

The Business Source License (this document, or the "License") is not an Open
Source license. However, the Licensed Work will eventually be made available
under an Open Source License, as stated in this License.

License text copyright © 2023 MariaDB plc, All Rights Reserved.
“Business Source License” is a trademark of MariaDB plc.
130 changes: 130 additions & 0 deletions packages/vendure-plugin-frequently-bought-together/README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,133 @@
# Frequently Bought Together Plugin

### [Official documentation here](https://pinelab-plugins.com/plugin/vendure-plugin-frequently-bought-together)

This plugin finds products that are often bought together by looking at past orders. You can integrate these frequently bought together products on your storefront, and so increase your revenue.

## Installation

1. To install the plugin, add it to your Vendure config and include its Admin UI extension in the Admin UI plugin:

```ts
import { FrequentlyBoughtTogetherPlugin } from '@vendure-hub/pinelab-frequently-bought-together-plugin';

const vendureConfig = {
plugins: [
FrequentlyBoughtTogetherPlugin.init({
// Disable this in production!
experimentMode: true,
// Test this support level first. See below for more details.
supportLevel: 0.001,
}),
AdminUiPlugin.init({
port: 3002,
route: 'admin',
app: compileUiExtensions({
outputPath: path.join(__dirname, '__admin-ui'),
extensions: [FrequentlyBoughtTogetherPlugin.ui],
}),
}),
],
};
```

2. Run a database migration to add the custom fields to your database.

## Storefront usage

You can get the related product via the shop API with the following query:

```graphql
{
product(id: 2) {
id
name
slug
frequentlyBoughtWith {
id
name
slug
}
}
}
```

Product relations in the Shop API are sorted by support, meaning that the most bought together products will appear first in the list.
The admin UI shows relations in random order due to the unordered nature of SQL relations.

## Experiment mode

Each shop's optimal support level varies based on data density. For example, a shop with many variants and few orders requires a lower support level. To experiment with support levels:

1. Start the server locally, and make sure you have set `experimentMode: true` in the plugin's init function.
2. Go to `http://localhost:3000/admin-api` or use a GraphQL client like Yaak to use the admin API
3. Execute the following query against the admin api:

```graphql
{
previewFrequentlyBoughtTogether(support: 0.1) {
# The peak amount of memory that was used during calculation. This should be a max of 80% of your worker instance
maxMemoryUsedInMB
# The different products that are included in the relations
uniqueProducts
# Total number of item sets
totalItemSets
# Most confident item sets
bestItemSets {
# E.g. ['product-1', 'product-5']
items
# Support is the number of orders this combination was in
support
}
# Least confident item sets
worstItemSets {
items
support
}
}
}
```

When you have found your desired support level, you set it in the plugin:

```ts
FrequentlyBoughtTogetherPlugin.init({
// Disable experiment mode in production!
experimentMode: false,
supportLevel: 0.00005
}),
```

1. Run the server again
2. Go to `/admin/catalog/products`
3. Click the three buttons at the top right and click `Calculate frequently bought together relations`

The frequently bought together relations are now set on your products.

Tips for Tweaking Support Levels:

- Start high (e.g., `0.1`) and gradually reduce (`0.01`, `0.001`, etc.).
- Review the worst item sets:
- Increase the support level if they seem irrelevant.
- If support equals 1, it indicates a single order—a poor indicator of frequent purchases.
- Monitor memory usage to avoid exceeding worker RAM.

## Channel specific support

To set different support levels for channels:

```ts
FrequentlyBoughtTogetherPlugin.init({
// Disable experiment mode in production!
experimentMode: false,
supportLevel: (ctx) => {
if (ctx.channel.token === 'channel-with-lots-of-variants') {
return 0.000001
} else {
return 0.0001
}
}
}),
```

Use the `vendure-token` header to preview queries for specific channels.
10 changes: 10 additions & 0 deletions packages/vendure-plugin-frequently-bought-together/codegen.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
schema: 'src/**/*.ts'
generates:
./src/generated-graphql-types.ts:
plugins:
- typescript
- typescript-operations
config:
avoidOptionals: false
scalars:
DateTime: Date
14 changes: 9 additions & 5 deletions packages/vendure-plugin-frequently-bought-together/package.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"name": "@pinelab/vendure-plugin-frequently-bought-together",
"name": "@vendure-hub/pinelab-frequently-bought-together-plugin",
"version": "0.0.2",
"description": "Increase average order value by suggesting frequently bought together products based on past orders. Also known as related products or product recommendations.",
"keywords": [
Expand All @@ -11,7 +11,7 @@
"author": "Martijn van de Brug <[email protected]>",
"homepage": "https://pinelab-plugins.com/",
"repository": "https://github.com/Pinelab-studio/pinelab-vendure-plugins",
"license": "MIT",
"license": "SEE LICENSE IN FILE 'LICENSE'",
"private": false,
"publishConfig": {
"access": "public"
Expand All @@ -21,15 +21,19 @@
"files": [
"dist",
"README.md",
"CHANGELOG.md"
"CHANGELOG.md",
"LICENSE"
],
"scripts": {
"build": "rimraf dist && tsc",
"build": "rimraf dist && yarn generate && tsc && copyfiles -u 1 'src/ui/**/*' dist/",
"start": "yarn ts-node test/dev-server.ts",
"generate": "graphql-codegen --config codegen.yml",
"test": "vitest run --bail 1",
"lint": "eslint ."
},
"dependencies": {
"catch-unknown": "^2.0.0"
"@vendure-hub/vendure-hub-plugin": "^0.0.2",
"catch-unknown": "^2.0.0",
"node-fpgrowth": "^1.2.1"
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,61 @@
import gql from 'graphql-tag';

const frequentlyBoughtTogetherAdminApiExtensions = gql``;
export const adminApiExtensions = gql`
${frequentlyBoughtTogetherAdminApiExtensions}
type FrequentlyBoughtTogetherPreview {
"""
The max memory used during the calculation.
Make sure this doesn't exceed your worker's memory limit.
process.memoryUsage().rss is used to calculate this.
"""
maxMemoryUsedInMB: Int!
"""
The total number of sets found.
"""
totalItemSets: Int!
"""
The number of unique products for which a related product was found
"""
uniqueProducts: Int!
"""
The item sets with the most support
"""
bestItemSets: [FrequentlyBoughtTogetherItemSet!]!
"""
The item sets with the worst support. If these make sense, the others probably do too.
"""
worstItemSets: [FrequentlyBoughtTogetherItemSet!]!
}
"""
An item set with a support value.
An item set is a combination of products which are frequently bought together, e.g. ['product-1', 'product-2', 'product-3']
Support is the number of orders this combination was in
"""
type FrequentlyBoughtTogetherItemSet {
items: [String!]!
support: Int!
}
extend type Query {
"""
Preview the frequently bought together item sets,
to check what level of support is reasonable for your data set
"""
previewFrequentlyBoughtTogether(
support: Float!
): FrequentlyBoughtTogetherPreview!
}
extend type Mutation {
"""
Trigger the job to calculate and set frequently bought together products.
"""
triggerFrequentlyBoughtTogetherCalculation: Boolean!
}
`;

export const shopApiExtensions = gql`
extend type Product {
frequentlyBoughtWith: [Product!]!
}
`;
Loading

0 comments on commit 956a23d

Please sign in to comment.