Without defining similarity.
No rules to write, no weights to tune, no model to train. Send Hilliance any data: it learns how your records should be compared — scales, meaning, weights, even differently named fields — and returns exact top matches from millions of objects in microseconds, with the evidence behind each one.
docker run -p 8080:8080 hilliance/hilliance
Three pairs that any exact match, and most fuzzy ones, would call different. No mapping written, no rules configured, no model trained: each number is what Hilliance returned after learning the population.
{ "type": "mug" }
vs
{ "type": "cup" }
Different words. Meaning is compared, not spelling — and "diesel engine" scores 0.37 against the same mug.
{ "color": "yellow" }
vs
{ "colour": "gold" }
Different field names. colour resolves onto color because their values agree, not because someone mapped them.
{ "diameter_cm": 27 }
vs
{ "diameter_mm": 270 }
Different units. Same size, so identity. The same two keys holding 27 and 27 score 0.00.
Measured by the engine on its demo population. See a whole comparison, strand by strand →
Both are the same engine and the same three requests — what changes is what you do with the numbers that come back.
Different names, different units, half the fields missing, no shared ids. Batch the feed against the catalogue and let relatedness sort accept from review from reject.
Read the walkthrough →Two records of one product rarely share their fields. Ask every object for its neighbours, and read coverage beside similarity to tell a duplicate from a thin coincidence.
Read the walkthrough →2,000 synthetic kitchen products, deliberately messy: half say colour and half say color, capacities arrive in millilitres and litres, diameters in centimetres and millimetres, and plenty of fields are simply missing. Nothing maps any of that — the engine worked it out when it learned them, and you can ask it anything below.
docker run -p 8080:8080 -v hilliance-data:/data hilliance/hilliancecurl -O https://hilliance.com/demo-products.jsonl
curl -X POST 'localhost:8080/spaces?name=demo&id_field=sku' \
-H 'Content-Type: application/x-ndjson' \
--data-binary @demo-products.jsonlThis query names the colour field the other way round and gives the capacity in litres. Neither appears in half the catalogue, and it matches anyway.
curl -X POST localhost:8080/spaces/demo/search \
-H 'content-type: application/json' -d '{
"object": {"type": "coffee mug", "color": "mustard",
"capacity_l": 0.35},
"limit": 3, "explain": true }'The viewer is already running inside the server you started.
open http://localhost:8080/spaces/demo/exploreThe sample file is demo-products.jsonl — 2,000 rows, 619 KB, generated for this page. Your own data works the same way: there is nothing to configure between step 1 and step 3.
Comparing products, vehicles, suppliers or anything else people describe with fields looks simple until you meet the data. Four things break most approaches.
One record has color and radius_mm. Another has colour and radius_cm. Same things, different names and units.
No record fully describes its thing. A field that isn't there is no evidence, and shouldn't count against a match.
Is radius 10 versus 11 a big gap? Among teacups, yes. Among bicycle wheels, no. The population decides.
"mug" and "cup" are close. "mug" and "diesel engine" aren't. Matching on spelling gets this wrong.
An edge between two objects is made of one strand per property. Shared strands are measured, missing ones cost coverage rather than similarity, and differently named ones are lined up from the data. Every edge reports four numbers.
Measured in a Space learned from 125,000 products with the bundled semantic model.
Offer more values and every value finds a closer partner, so a set score climbs with the length of the lists compared, related or not. Measured on 400 unrelated tags:
Nothing in common in any of them.
So a property holding lists learns a chance curve instead of one prior: what a value expects to find as its best match among 1, 2, 4 … 1024 others, sampled from its own values at learn time. A comparison is then charged the chance its own sizes deserve.
On 5,000 machines with option lists of 2 to 200 entries. The long lists had been costing the properties around them half their discrimination.
How alike the two objects are on the properties both have, from 0 to 1. null if they share nothing, and distance is 1 − similarity.
How much of the pair's available evidence could actually be compared.
Expected similarity over every property in the Space, with unshared ones filled by the population prior. Search ranks by it.
The evidence for or against being related, centred on chance, from −1 to +1. The same ranking as score, on a scale you can read.
Similarity of 0 isn't "unrelated": two random products score about 0.5 on a property. Relatedness puts chance at 0 and scales by the evidence the whole Space could offer, so it is the number to automate on.
Measured on 125,000 synthetic products. It's an exact threshold on the evidence search ranks by, so it prunes rather than costs.
Text is compared by meaning, including values that never appear in your data. Each property is calibrated against its own population, so 0.9 means the same thing whether it's a type, a colour or a number.
Neighbours by meaning, not spelling. A diesel engine is a product too, and that's about all it shares with a mug.
lemon never appears in the data. It's embedded on the fly and calibrated like every other colour.
Numbers are placed in their own distribution. The distance between two values is how much of the population lies between them.
Every conversation about similarity arrives at "but similarity is subjective". It's worth being exact about which part is, and what this engine does about it.
Two objects described by the same values are the same on the evidence available, and the engine returns exactly 1.0 — not 0.98, and with no setting that could make it anything else.
Below identity some bias is unavoidable. Count every predicate definable over a finite set of objects and any two of them share exactly the same number: treat all evidence as equal and a swan is as similar to a duck as to another swan. A usable scale between 0 and 1 cannot be had without privileging some evidence over the rest. The question is never whether to interpret, only where the interpretation comes from.
{ "diameter_cm": 27 } vs { "diameter_mm": 270 }
1.0different numbers, same size
{ "diameter_cm": 27 } vs { "diameter_mm": 27 }
0.0same number, different size
Identical digits, no similarity; different digits, identity. What separates them is a model of what the numbers refer to — here the unit table, elsewhere a property's vocabulary or its distribution.
Where each of those stances comes from — the distributions, the vocabularies, the weighting — is set out in the documentation.
The schema is the largest single lever on every number here, and it's yours, decided before any comparison happens.
The same two objects score differently in a catalogue of mugs and a catalogue of everything. Numbers from different Spaces are not comparable.
Semantic similarity inherits the text the model was trained on. That fast and firm sit far apart in one corpus is a fact about that corpus, not about beds.
Substitutable on a shelf, priced alike, shipped alike — different questions, and no population picks between them for you.
A Space is a learned, versioned comparison environment for one population, like products or suppliers. Learning it works out everything you'd otherwise specify by hand.
Objects flatten into path and value pairs. Nested fields get dotted paths, arrays become sets, and objects inside an array keep their fields together: a red S and a blue L is not a red L and a blue S.
Each property's kind is inferred from the population. SKUs and UUIDs are recognised as identifiers and given no weight.
Names only propose a match. color and colour merge because their values agree, radius_mm and radius_cm merge and convert, and width and height never do.
Every number gets its position in the property's distribution, so skewed data needs no special care. A query beyond the range keeps going rather than pinning to the maximum.
Informative properties weigh more. What several properties say in common counts once, and what each says alone keeps its full weight. A tenant or owner column can be marked filter_only: filterable, but never evidence.
Properties observed in both objects are compared. With explain, each one reports its similarity, weight and status.
This ships inside the server. Nothing to install, nothing to deploy, no separate service and no account: the container you already started serves it, on your own data, offline. One request opens it — every object a point, laid out so alike things sit together.
open http://localhost:8080/spaces/products/explore
distinctiveness, most telling first
Draw around a cluster — or hit Select region — and Hilliance says what those objects have in common: the properties that set them apart, most distinctive first, against the whole population. The same call explains a search result.
The layout is approximate, and the viewer says so. Ten properties genuinely need about ten dimensions. Everything except the positions is exact: neighbours, numbers, search results and explanations all come from the ordinary API.
Properties are ordered by how often neighbours agree on them, so the most informative colouring comes first. In 2D or a 3D turntable: drag to spin, and the cloud never turns over.
See the object's JSON and its exact nearest neighbours, with relatedness, similarity and coverage, in microseconds. Lines join it to the nearest plotted objects.
Paste an object and the matches light up where they are. The page takes its credential from the address fragment, which never reaches a server's logs — hand it an end user's token and the viewer shows that user's objects and nothing else.
Top-10 over two million synthetic 10-property objects, ranked by score, median of 31 runs on an Apple M3 Pro. How fast depends on the query, so here are the ends of the range rather than the flattering middle.
Everything above is exact: pruning reads fewer blocks but never changes a result — add "exhaustive": true to any query and compare. Don't take our word for the timings: hilliance-bench generates the population from a seed and measures the published image over its ordinary API, so you can run it on your own hardware in two commands. The full tables, the sustained-load soak and the approximate mode are in the performance documentation.
Anyone may self-host Hilliance, in production, at any size, behind their own product, free and without telling us. The one thing you may not do is offer Hilliance itself to third parties as a hosted service. That single carve-out is the whole of the licence.
Run it on your own machines, for your own product.
no licence to buy, no size limit, no reporting
Offer Hilliance itself to third parties, as a service.
priced per engagement, terms to suit
Every dependency is permissive, and CI runs cargo deny so a copyleft crate can't arrive unnoticed in an upgrade. Other terms are available: ask.
Why the carve-out exists, plainly: without it, the first person to read the source could run the cloud service before we do. Everything else — your servers, your product, your customers — is free.
Stop writing rules for what's similar. Bring a population of records, and Hilliance learns the space and shows its working.