diff --git a/examples/multi-currency/README.md b/examples/multi-currency/README.md new file mode 100644 index 000000000..445fb86eb --- /dev/null +++ b/examples/multi-currency/README.md @@ -0,0 +1,106 @@ +# Multi-Currency Vespa Application + +This Vespa application demonstrates multi-currency price handling using global documents holding currency conversion rates to USD. +Item prices are stored in their local currencies, but the app can hydrate and rank items based on their USD equivalent prices. +Price range Filtering can be done by using native currency filtering. + +## Architecture + +The application consists of two document types: +- `currency`: Global document storing currency conversion factors to USD +- `item`: Items with prices in different currencies, referencing currency documents + +The `currency.xml` file contains conversion rates between 30 different currencies. + +## Setup + +1. **Start Vespa container**: + ```bash + vespa config set target local + docker pull vespaengine/vespa + docker run --detach --name vespa --hostname vespa-container \ + --publish 127.0.0.1:8080:8080 --publish 127.0.0.1:19071:19071 \ + vespaengine/vespa + ``` + +2. **Wait for Vespa to be ready**: + ```bash + vespa status deploy --wait 300 + ``` + +3. **Deploy the application**: + ```bash + vespa deploy --wait 300 + ``` + +## Loading Data + +1. **Feed currency conversion factors**: + +Convert the `currency.xml` file to Vespa documents with conversion factors to USD.: +```bash +python3 currency_xml_to_vespa_docs.py | vespa feed - +``` + +The documents look like: +```jsonl +{"put": "id:mynamespace:currency::usd", "fields": {"factor": 1.0}} +{"put": "id:mynamespace:currency::aud", "fields": {"factor": 0.67884054}} +{"put": "id:mynamespace:currency::cad", "fields": {"factor": 0.76028283}} +{"put": "id:mynamespace:currency::cny", "fields": {"factor": 0.1442157}} +``` + +2. **Feed items with currency references**: + +Feed the sample documents: + +```bash +vespa feed items.jsonl +``` + +The item documents look like: + +```bash +{"put": "id:shopping:item::item-1", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 3836, "item_name": "emerald gemstone bracelet"}} +{"put": "id:shopping:item::item-2", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 14, "item_name": "Handmade ceramic ring dish"}} +{"put": "id:shopping:item::item-3", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 45, "item_name": "Handmade wooden cutting board"}} +``` + +## Querying + +1. **View all documents**: + ```bash + vespa visit + ``` + +2. **Query items with currency-based price filtering**: +```bash +vespa query 'select * from item where (currency_ref matches "id:shopping:currency::usd" and price >= 4000.0)' +``` + +3. **Filter by all currencies within a range**: + ```bash + vespa query yql="select * from item where $(python3 generate_price_filter_query.py --min_price 20 --max_price 100 --currency USD)" + ``` +4. **Combine currency filtering with ranking using USD price**: + ```bash + vespa query yql="select * from item where userQuery() AND ($(python3 generate_price_filter_query.py --min_price 20 --max_price 100 --currency USD))" query="vintage" + ``` + + +## Key Features + +- **Global currency documents**: Currency data is replicated across all content nodes +- **Cross-document field import**: Items can access currency factors via `currency_ref.factor` +- **USD price calculation**: Rank profile computes `price_usd: price * currency_factor` + +## Schema Details + +### Currency Schema +- `factor`: Double field representing conversion rate to USD + +### Item Schema +- `item_name`: String field for item description +- `price`: Double field for price in local currency +- `currency_ref`: Reference to currency document +- Imported field: `currency_factor` from referenced currency document diff --git a/examples/multi-currency/currency.xml b/examples/multi-currency/currency.xml new file mode 100644 index 000000000..5c089ed83 --- /dev/null +++ b/examples/multi-currency/currency.xml @@ -0,0 +1,908 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/examples/multi-currency/currency_rates.jsonl b/examples/multi-currency/currency_rates.jsonl new file mode 100644 index 000000000..14b37763f --- /dev/null +++ b/examples/multi-currency/currency_rates.jsonl @@ -0,0 +1,30 @@ +{"put": "id:mynamespace:currency::usd", "fields": {"factor": 1.0}} +{"put": "id:mynamespace:currency::aud", "fields": {"factor": 0.67884054}} +{"put": "id:mynamespace:currency::cad", "fields": {"factor": 0.76028283}} +{"put": "id:mynamespace:currency::cny", "fields": {"factor": 0.1442157}} +{"put": "id:mynamespace:currency::czk", "fields": {"factor": 0.04914198}} +{"put": "id:mynamespace:currency::dkk", "fields": {"factor": 0.16289298}} +{"put": "id:mynamespace:currency::hkd", "fields": {"factor": 0.13271224}} +{"put": "id:mynamespace:currency::huf", "fields": {"factor": 0.00303453}} +{"put": "id:mynamespace:currency::inr", "fields": {"factor": 0.0120344}} +{"put": "id:mynamespace:currency::idr", "fields": {"factor": 6.354e-05}} +{"put": "id:mynamespace:currency::ils", "fields": {"factor": 0.30717248}} +{"put": "id:mynamespace:currency::jpy", "fields": {"factor": 0.00720471}} +{"put": "id:mynamespace:currency::myr", "fields": {"factor": 0.24654832}} +{"put": "id:mynamespace:currency::mxn", "fields": {"factor": 0.055135}} +{"put": "id:mynamespace:currency::mad", "fields": {"factor": 0.11377527}} +{"put": "id:mynamespace:currency::nzd", "fields": {"factor": 0.62968327}} +{"put": "id:mynamespace:currency::nok", "fields": {"factor": 0.10324712}} +{"put": "id:mynamespace:currency::php", "fields": {"factor": 0.01839195}} +{"put": "id:mynamespace:currency::sgd", "fields": {"factor": 0.81559416}} +{"put": "id:mynamespace:currency::vnd", "fields": {"factor": 3.957e-05}} +{"put": "id:mynamespace:currency::zar", "fields": {"factor": 0.05826327}} +{"put": "id:mynamespace:currency::sek", "fields": {"factor": 0.11001705}} +{"put": "id:mynamespace:currency::chf", "fields": {"factor": 1.29600829}} +{"put": "id:mynamespace:currency::thb", "fields": {"factor": 0.03207987}} +{"put": "id:mynamespace:currency::gbp", "fields": {"factor": 1.42450142}} +{"put": "id:mynamespace:currency::twd", "fields": {"factor": 0.03562586}} +{"put": "id:mynamespace:currency::try", "fields": {"factor": 0.02602804}} +{"put": "id:mynamespace:currency::eur", "fields": {"factor": 1.21521449}} +{"put": "id:mynamespace:currency::pln", "fields": {"factor": 0.28613941}} +{"put": "id:mynamespace:currency::brl", "fields": {"factor": 0.18918612}} diff --git a/examples/multi-currency/currency_xml_to_vespa_docs.py b/examples/multi-currency/currency_xml_to_vespa_docs.py new file mode 100644 index 000000000..9a30898af --- /dev/null +++ b/examples/multi-currency/currency_xml_to_vespa_docs.py @@ -0,0 +1,50 @@ +import sys +import xml.etree.ElementTree as ET +import json + + +def parse_currencies(root) -> set[str]: + currencies = [] + for currency in root.findall('.//currency'): + currencies.append(currency.get('code')) + + return sorted(currencies) + + +def convert_currency_xml_to_vespa_jsonl(xml_file) -> list[str]: + # Parse the XML file + tree = ET.parse(xml_file) + root = tree.getroot() + + currencies = parse_currencies(root) + + # Add USD to USD conversion (factor = 1.0) + usd_doc = { + "put": "id:shopping:currency::usd", + "fields": {"factor": 1.0} + } + currency_rates = [json.dumps(usd_doc) + '\n'] + + # Find all rate elements where 'to' attribute is 'USD' + for rate in root.findall('.//rate[@to="USD"]'): + currency = rate.get('from').lower() + factor = float(rate.get('rate')) + + # Create Vespa document + doc = { + "put": f"id:shopping:currency::{currency}", + "fields": { + "code": currency, + "idx": currencies.index(currency.upper()), + "factor": factor, + } + } + + currency_rates.append(json.dumps(doc)) + + return currency_rates + +# Usage +if __name__ == "__main__": + currency_docs = convert_currency_xml_to_vespa_jsonl('currency.xml') + sys.stdout.write("\n".join(currency_docs) + "\n") \ No newline at end of file diff --git a/examples/multi-currency/filtering_perf_benchmark.py b/examples/multi-currency/filtering_perf_benchmark.py new file mode 100644 index 000000000..f424e27ab --- /dev/null +++ b/examples/multi-currency/filtering_perf_benchmark.py @@ -0,0 +1,219 @@ +# Benchmark filtering native with a single price_usd filter vs. the dynamic OR across multiple currencies. +# If the dynamic OR is fast enough, using the global currency document and runtime conversions is preferable, +# as it removes the need to update price_usd in all non-USD items for every currency conversion rate change. + +# - delete all Vespa documents. +# - feed the currency conversion documents. +# - generate and save 1mm documents with both native and usd prices. +# - feed those docs into vespa +# - generate and save 5,000 random queries with price filters in various currencies and random keywords. +# - run the queries with both native and usd price filters. +# - compare the results and timings. + + +# filtering_perf_benchmark.py +# python3 filtering_perf_benchmark.py --help to see options +import numpy as np +import json +import random +import subprocess +import time +from pathlib import Path + +from vespa.application import Vespa +from vespa.io import VespaQueryResponse + +from generate_price_filter_query import load_conversion_rates + + +RATE_TABLE, _ = load_conversion_rates("currency.xml") + +def pct(values: list[float], *percents: int) -> list[float]: + """ + Returns the requested percentiles from the input list. + + pct([1, 2, 3, 4], 25, 50, 75) -> [1.75, 2.5, 3.25] + """ + arr = np.asarray(values, dtype=np.float64) + # numpy >= 1.22: use method instead of interpolation + return np.percentile(arr, percents, method="linear").tolist() + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +def _run_cli(cmd: list[str], stdin: bytes | None = None) -> str: + """ + Runs a Vespa CLI command and returns stdout. + Raises `subprocess.CalledProcessError` on failure. + """ + res = subprocess.run(cmd, input=stdin, check=True, text=True, stdout=subprocess.PIPE) + return res.stdout + + +def _feed_jsonl(lines: list[str]) -> None: + """ + Feeds JSONL documents using `vespa feed -`. + """ + data = ("\n".join(lines) + "\n").encode() + _run_cli(["vespa", "feed", "-"], stdin=data) + +def feed_currency_documents(factors: dict[str, float], ns: str) -> None: + """ + Feeds `currency` documents with conversion factors. + """ + jsonl = [ + json.dumps({"put": f"id:{ns}:currency::{code.lower()}", + "fields": {"factor": factor}}) + for code, factor in factors.items() + ] + _feed_jsonl(jsonl) + +# Price buckets for random price generation. +BUCKETS: list[tuple[int,int]] = [(0,5), (5,10), (10,20), (20,40), (40,80), (80,150), (150,300), (300,600), (600,1000), (1000,10000)] + +def random_price_cents() -> int: + bucket_idx = random.randint(0, len(BUCKETS) - 1) + return random.randint(BUCKETS[bucket_idx][0] * 100, BUCKETS[bucket_idx][1] * 100) + + +CURRENCY_PROBS = { + "USD": 0.70304, + "EUR": 0.11294, + "GBP": 0.10312, + "CAD": 0.03630, + "AUD": 0.02066, + "TRY": 0.00474, + "INR": 0.00286, + "PHP": 0.00197, + "VND": 0.00174, + "HKD": 0.00167, + "SEK": 0.00125, + "IDR": 0.00115, + "NZD": 0.00113, + "CHF": 0.00110, + "ILS": 0.00109, + "MAD": 0.00103, + "SGD": 0.00103, + "MYR": 0.00077, + "ZAR": 0.00074, + "MXN": 0.00062, + "DKK": 0.00060, + "NOK": 0.00044, + "TWD": 0.00001, + "PLN": 0.00001, + "THB": 0.00001, + "JPY": 0.00001, + "CZK": 0.00001, + "CNY": 0.00001, +} + +# ---------------------------------------------------------------------- +def random_currency() -> str: + currencies = list(CURRENCY_PROBS.keys()) + weights = list(CURRENCY_PROBS.values()) + return random.choices(currencies, weights=weights, k=1)[0] + +tokens = [f"token{i}" for i in range(1, 1001)] # Example tokens for item titles +def generate_items(): + """ + Generates a list of item documents with random prices and currency references. + This is a placeholder function; replace with actual item generation logic. + """ + items = [] + for i in range(1, 1_000_001): + price_usd = random_price_cents() + currency = random_currency() + price_native = RATE_TABLE[("USD", currency.upper())] * price_usd # Convert to the target currency + item = { + "put": f"id:shopping:item::item-{i}", + "fields": { + "currency_ref": f"id:shopping:currency::{currency.lower()}", + "price_usd": price_usd, + "price": price_native, + } + } + if currency.lower() != "usd": + item["fields"][f"price_{currency.lower()}"] = price_native + + items.append(json.dumps(item)) + return items + + +def vespa_feed(jsonl_file: Path) -> None: + _run_cli(["vespa", "feed", str(jsonl_file)]) + + +if __name__ == "__main__": + reindex = True + if reindex: + docker_feed = Vespa(url="localhost", port=8080) + _ = docker_feed.delete_all_docs(content_cluster_name="shopping", schema='currency') + _ = docker_feed.delete_all_docs(content_cluster_name="shopping", schema='item') + + from currency_xml_to_vespa_docs import convert_currency_xml_to_vespa_jsonl + currency_docs = convert_currency_xml_to_vespa_jsonl('currency.xml') + currency_docs_file = Path("currency_docs.jsonl") + currency_docs_file.write_text("\n".join(currency_docs) + "\n") + vespa_feed(currency_docs_file) + + # generate 1mm sample items with random prices, item_name, and currency. + items = generate_items() + items_file = Path("1mm_items.jsonl") + items_file.write_text("\n".join(items) + "\n") + vespa_feed(items_file) + + app = Vespa(url="http://localhost", port=8080) + + from generate_price_filter_query import generate_price_filter_query + + # record the latency for both queries + lat_multi: list[float] = [] + lat_single: list[float] = [] + + for i in range(1, 1000): + prices = [random_price_cents() for _ in range(2)] + price_usd_min = min(prices) + price_usd_max = max(prices) + currency = random_currency() + rate = RATE_TABLE[("USD", currency.upper())] + min_price = rate * price_usd_min + max_price = rate * price_usd_max + + multi_currency_where=f"select * from item where {generate_price_filter_query(min_price, max_price, currency.lower())}" + single_currency_where=f"select * from item where price_usd >= {price_usd_min} and price_usd <= {price_usd_max}" + + exec_plan = [ + ("multi", multi_currency_where, lat_multi), + ("single", single_currency_where, lat_single), + ] + random.shuffle(exec_plan) + + # run queries in randomized order + for tag, query_str, lat_list in exec_plan: + start = time.perf_counter() + r: VespaQueryResponse = app.query({ + "yql": query_str, + "ranking": "unranked", + "hits": 0, + }) + latency = time.perf_counter() - start + lat_list.append(latency) + results: int = r.number_documents_retrieved + + if tag == "multi": + multi_currency_results = results + else: + single_currency_results = results + + #if multi_currency_results != single_currency_results: + # print(f"Latency: multi={lat_multi[-1]} single={lat_single[-1]}. Total hits mismatch: {multi_currency_results} vs {single_currency_results} currency:{currency}, min_price: {min_price}, max_price: {max_price} price_usd_min: {price_usd_min}, price_usd_max: {price_usd_max} rate: {rate}") + #else: + # print(f"Latency: multi={lat_multi[-1]} single={lat_single[-1]}. Total hits: {multi_currency_results} currency:{currency}, min_price: {min_price}, max_price: {max_price} price_usd_min: {price_usd_min}, price_usd_max: {price_usd_max} rate: {rate}") + + print(f"latency for multi-currency query: {pct(lat_multi, [25, 50, 75, 90, 95, 99])}") + print(f"latency for price_usd query: {pct(lat_single, [25, 50, 75, 90, 95, 99])}") + + + + diff --git a/examples/multi-currency/generate_price_filter_query.py b/examples/multi-currency/generate_price_filter_query.py new file mode 100644 index 000000000..e04093e43 --- /dev/null +++ b/examples/multi-currency/generate_price_filter_query.py @@ -0,0 +1,96 @@ +import sys +import argparse +import xml.etree.ElementTree as ET + + +def load_conversion_rates(xml_file: str) -> tuple[dict[tuple[str, str], float], set[str]]: + """ + Parses the currency XML file and builds a conversion rate table. + Returns a dictionary of rates and a sorted list of all currencies. + """ + try: + tree = ET.parse(xml_file) + root = tree.getroot() + except (ET.ParseError, FileNotFoundError) as e: + print(f"Error: Could not read or parse the currency XML file '{xml_file}'.\n{e}", file=sys.stderr) + sys.exit(1) + + rates: dict[tuple[str, str], float] = {} + all_currencies: set[str] = set() + + for rate_element in root.findall('.//rate'): + from_curr: str = rate_element.get('from').upper() + to_curr: str = rate_element.get('to').upper() + rate_value: float = float(rate_element.get('rate')) + rates[(from_curr, to_curr)] = rate_value + all_currencies.add(from_curr) + all_currencies.add(to_curr) + + all_currencies = sorted(all_currencies) + + for currency in all_currencies: + rates[(currency, currency)] = 1.0 + + return rates, all_currencies + + +rates, all_currencies = load_conversion_rates("currency.xml") + + +def currency_idx(currency: str) -> int: + return all_currencies.index(currency.upper()) + + +def price_filter(currency: str, min_price: float, max_price: float) -> str: + # This is fast, too. Only ~2x slower than one price_usd query + return f"(currency_idx = {currency_idx(currency)} and price >= {min_price} and price <= {max_price})" + + # 'matches' is roughly 80ms slower. How to speed this up? + # return f"(currency_ref matches \"id:shopping:currency::{currency.lower()}\" and price >= {min_price} and price <= {max_price})" + + # TODO: how can we get exact matches for currency_code to work? + # return f"(currency_code = \"{currency.lower()}\" and price >= {min_price} and price <= {max_price})" + + # using a field for each currency is fast. 1.75x the price_usd query + # return f"(price_{currency.lower()} >= {min_price} and price_{currency.lower()} <= {max_price})" + + +def generate_price_filter_query(min_price: float, max_price: float, currency: str) -> str: + if min_price > max_price: + raise ValueError("min_price cannot be greater than max_price.") + + source_currency: str = currency.upper() + + or_conditions: list[str] = [] + for target_currency in all_currencies: + if (source_currency, target_currency) in rates: + rate: float = rates[(source_currency, target_currency)] + converted_min: float = min_price * rate + converted_max: float = max_price * rate + + or_conditions.append(price_filter(target_currency, converted_min, converted_max)) + else: + print(f"Warning: No conversion rate from {source_currency} to {target_currency}. Skipping.", + file=sys.stderr) + + return " or ".join(or_conditions) + + +def main() -> None: + """ + Main function to generate the Vespa query. + """ + parser = argparse.ArgumentParser( + description="Generate a Vespa query to filter by price across multiple currencies." + ) + parser.add_argument('--min_price', type=float, required=True, help='Minimum price.') + parser.add_argument('--max_price', type=float, required=True, help='Maximum price.') + parser.add_argument('--currency', type=str, required=True, + help='The currency for the given min/max price (e.g., USD).') + + args = parser.parse_args() + print(generate_price_filter_query(args.min_price, args.max_price, args.currency)) + + +if __name__ == "__main__": + main() diff --git a/examples/multi-currency/items.jsonl b/examples/multi-currency/items.jsonl new file mode 100644 index 000000000..5a2f96ed4 --- /dev/null +++ b/examples/multi-currency/items.jsonl @@ -0,0 +1,100 @@ +{"put": "id:shopping:item::item-1", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 3836, "item_name": "emerald gemstone bracelet"}} +{"put": "id:shopping:item::item-2", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 14, "item_name": "Handmade ceramic ring dish"}} +{"put": "id:shopping:item::item-3", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 45, "item_name": "Handmade wooden cutting board"}} +{"put": "id:shopping:item::item-4", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 34, "item_name": "Personalized wooden cutting board"}} +{"put": "id:shopping:item::item-5", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 7, "item_name": "Vintage letterpress greeting card"}} +{"put": "id:shopping:item::item-6", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 4172, "item_name": "14k gold hammered ring"}} +{"put": "id:shopping:item::item-7", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 1664, "item_name": "diamond engagement ring"}} +{"put": "id:shopping:item::item-8", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 4027, "item_name": "sapphire drop earrings"}} +{"put": "id:shopping:item::item-9", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 21, "item_name": "Handmade knitted scarf"}} +{"put": "id:shopping:item::item-10", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 55, "item_name": "Personalized letterpress greeting card"}} +{"put": "id:shopping:item::item-11", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 1, "item_name": "Minimalist beaded necklace"}} +{"put": "id:shopping:item::item-12", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 24, "item_name": "Handcrafted hand stamped necklace"}} +{"put": "id:shopping:item::item-13", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 49, "item_name": "Artisan crochet baby blanket"}} +{"put": "id:shopping:item::item-14", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 17, "item_name": "Handcrafted macrame wall hanging"}} +{"put": "id:shopping:item::item-15", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 20, "item_name": "Vintage crochet baby blanket"}} +{"put": "id:shopping:item::item-16", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 29, "item_name": "Minimalist personalized leather wallet"}} +{"put": "id:shopping:item::item-17", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 4241, "item_name": "custom engagement ring"}} +{"put": "id:shopping:item::item-18", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 52, "item_name": "Personalized handmade pottery bowl"}} +{"put": "id:shopping:item::item-19", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 20, "item_name": "Handmade resin keychain"}} +{"put": "id:shopping:item::item-20", "fields": {"currency_ref": "id:shopping:currency::myr", "price": 23, "item_name": "Handcrafted macrame wall hanging"}} +{"put": "id:shopping:item::item-21", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 23, "item_name": "Personalized personalized leather wallet"}} +{"put": "id:shopping:item::item-22", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 63, "item_name": "Handcrafted geode coaster set"}} +{"put": "id:shopping:item::item-23", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 63, "item_name": "Handmade leather journal"}} +{"put": "id:shopping:item::item-24", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 3383, "item_name": "18k gold hoop earrings"}} +{"put": "id:shopping:item::item-25", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 45, "item_name": "Rustic beaded necklace"}} +{"put": "id:shopping:item::item-26", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 3408, "item_name": "18k gold hoop earrings"}} +{"put": "id:shopping:item::item-27", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 36, "item_name": "Handmade personalized leather wallet"}} +{"put": "id:shopping:item::item-28", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 41, "item_name": "Rustic beaded necklace"}} +{"put": "id:shopping:item::item-29", "fields": {"currency_ref": "id:shopping:currency::php", "price": 52, "item_name": "Eco-friendly macrame wall hanging"}} +{"put": "id:shopping:item::item-30", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 16, "item_name": "Minimalist beaded necklace"}} +{"put": "id:shopping:item::item-31", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 48, "item_name": "Rustic ceramic mug"}} +{"put": "id:shopping:item::item-32", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 11, "item_name": "Minimalist embroidered tea towel"}} +{"put": "id:shopping:item::item-33", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 20, "item_name": "Handmade polymer clay earrings"}} +{"put": "id:shopping:item::item-34", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 44, "item_name": "Boho ceramic ring dish"}} +{"put": "id:shopping:item::item-35", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 9, "item_name": "Minimalist polymer clay earrings"}} +{"put": "id:shopping:item::item-36", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 28, "item_name": "Artisan ceramic ring dish"}} +{"put": "id:shopping:item::item-37", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 17, "item_name": "Eco-friendly beaded necklace"}} +{"put": "id:shopping:item::item-38", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 26, "item_name": "Eco-friendly knitted scarf"}} +{"put": "id:shopping:item::item-39", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 1, "item_name": "Custom polymer clay earrings"}} +{"put": "id:shopping:item::item-40", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 3, "item_name": "Handcrafted leather journal"}} +{"put": "id:shopping:item::item-41", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 20, "item_name": "Minimalist embroidered tea towel"}} +{"put": "id:shopping:item::item-42", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 44, "item_name": "Boho polymer clay earrings"}} +{"put": "id:shopping:item::item-43", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 27, "item_name": "Boho ceramic ring dish"}} +{"put": "id:shopping:item::item-44", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 30, "item_name": "Personalized polymer clay earrings"}} +{"put": "id:shopping:item::item-45", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 40, "item_name": "Handmade hand-poured soap"}} +{"put": "id:shopping:item::item-46", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 17, "item_name": "Artisan felted wool slippers"}} +{"put": "id:shopping:item::item-47", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 34, "item_name": "Custom personalized leather wallet"}} +{"put": "id:shopping:item::item-48", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 18, "item_name": "Eco-friendly ceramic ring dish"}} +{"put": "id:shopping:item::item-49", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 4, "item_name": "Artisan crochet baby blanket"}} +{"put": "id:shopping:item::item-50", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 24, "item_name": "Handcrafted leather journal"}} +{"put": "id:shopping:item::item-51", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 4550, "item_name": "emerald gemstone bracelet"}} +{"put": "id:shopping:item::item-52", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 39, "item_name": "Handmade resin keychain"}} +{"put": "id:shopping:item::item-53", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 35, "item_name": "Rustic felted wool slippers"}} +{"put": "id:shopping:item::item-54", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 30, "item_name": "Minimalist hand-poured soap"}} +{"put": "id:shopping:item::item-55", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 66, "item_name": "Handmade hand stamped necklace"}} +{"put": "id:shopping:item::item-56", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 36, "item_name": "Custom personalized leather wallet"}} +{"put": "id:shopping:item::item-57", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 2924, "item_name": "14k gold hammered ring"}} +{"put": "id:shopping:item::item-58", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 45, "item_name": "Artisan felted wool slippers"}} +{"put": "id:shopping:item::item-59", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 64, "item_name": "Artisan ceramic ring dish"}} +{"put": "id:shopping:item::item-60", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 11, "item_name": "Minimalist embroidered tea towel"}} +{"put": "id:shopping:item::item-61", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 14, "item_name": "Eco-friendly handmade pottery bowl"}} +{"put": "id:shopping:item::item-62", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 56, "item_name": "Eco-friendly polymer clay earrings"}} +{"put": "id:shopping:item::item-63", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 18, "item_name": "Minimalist macrame wall hanging"}} +{"put": "id:shopping:item::item-64", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 4566, "item_name": "18k gold hoop earrings"}} +{"put": "id:shopping:item::item-65", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 30, "item_name": "Eco-friendly personalized leather wallet"}} +{"put": "id:shopping:item::item-66", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 46, "item_name": "Personalized letterpress greeting card"}} +{"put": "id:shopping:item::item-67", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 43, "item_name": "Artisan knitted scarf"}} +{"put": "id:shopping:item::item-68", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 56, "item_name": "Handmade felted wool slippers"}} +{"put": "id:shopping:item::item-69", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 52, "item_name": "Vintage ceramic succulent planter"}} +{"put": "id:shopping:item::item-70", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 46, "item_name": "Minimalist hand stamped necklace"}} +{"put": "id:shopping:item::item-71", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 1849, "item_name": "diamond engagement ring"}} +{"put": "id:shopping:item::item-72", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 18, "item_name": "Handcrafted hand-poured soap"}} +{"put": "id:shopping:item::item-73", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 42, "item_name": "Eco-friendly embroidered tea towel"}} +{"put": "id:shopping:item::item-74", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 4126, "item_name": "sapphire drop earrings"}} +{"put": "id:shopping:item::item-75", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 35, "item_name": "Personalized polymer clay earrings"}} +{"put": "id:shopping:item::item-76", "fields": {"currency_ref": "id:shopping:currency::cad", "price": 3714, "item_name": "custom engagement ring"}} +{"put": "id:shopping:item::item-77", "fields": {"currency_ref": "id:shopping:currency::eur", "price": 10, "item_name": "Handmade handmade pottery bowl"}} +{"put": "id:shopping:item::item-78", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 51, "item_name": "Handcrafted resin keychain"}} +{"put": "id:shopping:item::item-79", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 42, "item_name": "Personalized embroidered tea towel"}} +{"put": "id:shopping:item::item-80", "fields": {"currency_ref": "id:shopping:currency::aud", "price": 31, "item_name": "Personalized ceramic mug"}} +{"put": "id:shopping:item::item-81", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 3876, "item_name": "18k gold hoop earrings"}} +{"put": "id:shopping:item::item-82", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 2519, "item_name": "18k gold hoop earrings"}} +{"put": "id:shopping:item::item-83", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 27, "item_name": "Boho wooden cutting board"}} +{"put": "id:shopping:item::item-84", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 12, "item_name": "Minimalist hand-poured soap"}} +{"put": "id:shopping:item::item-85", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 33, "item_name": "Custom geode coaster set"}} +{"put": "id:shopping:item::item-86", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 37, "item_name": "Rustic ceramic ring dish"}} +{"put": "id:shopping:item::item-87", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 18, "item_name": "Eco-friendly macrame wall hanging"}} +{"put": "id:shopping:item::item-88", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 47, "item_name": "Vintage leather journal"}} +{"put": "id:shopping:item::item-89", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 14, "item_name": "Minimalist personalized leather wallet"}} +{"put": "id:shopping:item::item-90", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 1, "item_name": "Minimalist knitted scarf"}} +{"put": "id:shopping:item::item-91", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 46, "item_name": "Handmade soy wax candle"}} +{"put": "id:shopping:item::item-92", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 24, "item_name": "Vintage handmade pottery bowl"}} +{"put": "id:shopping:item::item-93", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 28, "item_name": "Personalized ceramic ring dish"}} +{"put": "id:shopping:item::item-94", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 17, "item_name": "Eco-friendly leather journal"}} +{"put": "id:shopping:item::item-95", "fields": {"currency_ref": "id:shopping:currency::gbp", "price": 1207, "item_name": "emerald gemstone bracelet"}} +{"put": "id:shopping:item::item-96", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 6, "item_name": "Eco-friendly hand stamped necklace"}} +{"put": "id:shopping:item::item-97", "fields": {"currency_ref": "id:shopping:currency::cad", "price": 49, "item_name": "Handmade macrame wall hanging"}} +{"put": "id:shopping:item::item-98", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 20, "item_name": "Vintage leather journal"}} +{"put": "id:shopping:item::item-99", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 55, "item_name": "Personalized embroidered tea towel"}} +{"put": "id:shopping:item::item-100", "fields": {"currency_ref": "id:shopping:currency::usd", "price": 42, "item_name": "Custom ceramic mug"}} diff --git a/examples/multi-currency/requirements.txt b/examples/multi-currency/requirements.txt new file mode 100644 index 000000000..c88f22c81 --- /dev/null +++ b/examples/multi-currency/requirements.txt @@ -0,0 +1,2 @@ +numpy +pyvespa \ No newline at end of file diff --git a/examples/multi-currency/schemas/currency.sd b/examples/multi-currency/schemas/currency.sd new file mode 100644 index 000000000..cc841c12f --- /dev/null +++ b/examples/multi-currency/schemas/currency.sd @@ -0,0 +1,21 @@ +schema currency { + document currency { + field idx type byte { + indexing: summary | attribute + attribute: fast-search + rank: filter + } + + field code type string { + indexing: attribute | summary + attribute: fast-search + } + + # multiplier for converting to USD + field factor type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + } +} diff --git a/examples/multi-currency/schemas/item.sd b/examples/multi-currency/schemas/item.sd new file mode 100644 index 000000000..03ba08965 --- /dev/null +++ b/examples/multi-currency/schemas/item.sd @@ -0,0 +1,224 @@ +schema item { + document item { + field item_name type string { + indexing: index | summary + match: text + index: enable-bm25 + } + field price type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + field price_usd type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + field price_aud type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_brl type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_cad type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_chf type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_cny type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_czk type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_dkk type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_eur type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_gbp type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_hkd type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_huf type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_idr type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_ils type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_inr type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_jpy type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_mad type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_mxn type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_myr type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_nok type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_nzd type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_php type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_pln type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_sgd type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_sek type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_thb type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_try type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_twd type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_vnd type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field price_zar type double { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + + field currency_ref type reference { + indexing: attribute | summary + attribute: fast-search + rank: filter + } + } + import field currency_ref.code as currency_code {} + import field currency_ref.idx as currency_idx {} + import field currency_ref.factor as currency_factor {} + + fieldset default { + fields: item_name + } + + rank-profile default { + function price_usd() { + expression: attribute(price) * attribute(currency_factor) + } + first-phase { + expression: bm25(item_name) * log(price_usd) + } + summary-features { + attribute(price) + price_usd + bm25(item_name) + } + } + + document-summary default { + summary item_name {} + summary currency_factor {} + } +} \ No newline at end of file diff --git a/examples/multi-currency/services.xml b/examples/multi-currency/services.xml new file mode 100644 index 000000000..ca391d5c6 --- /dev/null +++ b/examples/multi-currency/services.xml @@ -0,0 +1,20 @@ + + + + + + + + + + + + 1 + + + + + + + +