Het is spelletjesavond, je vrienden zitten rond de speltafel en wachten om te zien welk Dungeons & Dragons (D&D) personage ze worden en welke quest ze gaan doen. Vanavond ben jij Dungeon Master (verhalenverteller en gids), bedenker van spannende ontmoetingen om je spelers uit te dagen en te boeien. Je vertrouwde D&D Monster Manual bevat duizenden wezens. Het vinden van het perfecte monster voor elke situatie uit de talloze opties kan overweldigend zijn. De ideale vijand moet passen bij de setting, moeilijkheidsgraad en het verhaal van het moment.
Wat als we een tool konden creëren die direct het monster vindt dat het meest geschikt is voor elk scenario? Een tool die meerdere factoren tegelijk overweegt , en ervoor zorgt dat elke ontmoeting zo meeslepend en spannend mogelijk is?
Laten we zelf op zoek gaan: bouw het ultieme monsterzoeksysteem met behulp van de kracht van multi-attribuut vectorzoekopdrachten!
Vector zoeken vertegenwoordigt een revolutie in het ophalen van informatie. Vector embedding - door rekening te houden met context en semantische betekenis - stelt vector zoeken in staat om relevantere en nauwkeurigere resultaten te retourneren, niet alleen gestructureerde maar ook ongestructureerde data en meerdere talen te verwerken en te schalen. Maar om hoogwaardige antwoorden te genereren in real-world toepassingen, moeten we vaak verschillende gewichten toekennen aan specifieke kenmerken van onze data-objecten.
Er zijn twee gangbare benaderingen voor multi-attribute vector search. Beide beginnen met het afzonderlijk insluiten van elk attribuut van een data-object. Het belangrijkste verschil tussen deze twee benaderingen is hoe onze insluitingen worden opgeslagen en doorzocht .
spaces
van Superlinked laten ons ook elk attribuut wegen op het moment van de query om relevantere resultaten naar voren te brengen, zonder nabewerking. Hieronder gebruiken we deze twee benaderingen om een multi-attribute vector search tool te implementeren - een Dungeons and Dragons monster finder! Onze eenvoudige implementaties, met name de tweede, illustreren hoe u krachtigere en flexibelere zoeksystemen kunt maken, systemen die complexe, veelzijdige zoekopdrachten met gemak aankunnen, ongeacht uw use case.
Als u nieuw bent in vector similarity search, maak u dan geen zorgen! Wij hebben u gedekt - bekijk onze bouwstenen artikelen .
Oké, laten we op monsterjacht gaan!
Eerst genereren we een kleine synthetische dataset van monsters door een Large Language Model (LLM) op te starten:
Generate two JSON lists: 'monsters' and 'queries'. 1. 'monsters' list: Create 20 unique monsters with the following properties: - name: A distinctive name - look: Brief description of appearance (2-3 sentences) - habitat: Where the monster lives (2-3 sentences) - behavior: How the monster acts (2-3 sentences) Ensure some monsters share similar features while remaining distinct. 2. 'queries' list: Create 5 queries to search for monsters: - Each query should be in the format: {look: "...", habitat: "...", behavior: "..."} - Use simple, brief descriptions (1-3 words per field) - Make queries somewhat general to match multiple monsters Output format: { "monsters": [ {"name": "...", "look": "...", "habitat": "...", "behavior": "..."}, ... ], "queries": [ {"look": "...", "habitat": "...", "behavior": "..."}, ... ] }
Laten we eens kijken naar een voorbeeld van de dataset die onze LLM heeft gegenereerd. Let op: LLM-generatie is niet-deterministisch, dus uw resultaten kunnen afwijken.
Hier zijn onze eerste vijf monsters:
# | naam | Look | leefgebied | gedrag |
---|---|---|---|---|
0 | Lichtgevend | Motachtig wezen met gloeiende vleugels en antenne | Dichte bossen en jungles met bioluminescente flora | Zendt rustgevende lichtpatronen uit om te communiceren en prooien aan te trekken |
1 | Aqua-Wraith | Doorschijnend humanoïde figuur gemaakt van stromend water | Rivieren, meren en kustgebieden | Verandert van vorm om zich aan te passen aan waterlichamen en controleert stromingen |
2 | Steenhart Golem | Massieve humanoïde bestaande uit in elkaar grijpende rotsformaties | Rotsachtige bergen en oude ruïnes | Houdt eeuwenlang een winterslaap en ontwaakt om zijn territorium te beschermen |
3 | Fluisterende schaduw | Schaduwrijk, amorf wezen met gloeiende ogen | Donkere bossen en verlaten gebouwen | Voedt zich met angst en fluistert verontrustende waarheden |
4 | Zephyr-danser | Sierlijk vogelwezen met iriserende veren | Hoge bergtoppen en door de wind geteisterde vlakten | Creëert betoverende luchtshows om partners aan te trekken |
...en onze gegenereerde query's:
| Kijk | Leefgebied | Gedrag |
---|---|---|---|
0 | Gloeiend | Donkere plekken | Lichtmanipulatie |
1 | Elementair | Extreme omgevingen | Milieubeheersing |
2 | Vormverandering | Gevarieerde landschappen | Illusie creatie |
3 | Kristallijn | Mineraalrijke gebieden | Energieabsorptie |
4 | Etherisch | Atmosferisch | Invloed van de geest |
Bekijk hier de originele dataset en queryvoorbeelden.
Hieronder stellen we de parameters in die we in beide benaderingen gebruiken: Naïef en Superlinked.
Wij genereren onze vector-embeddings met:
sentence-transformers/all-mpnet-base-v2.
Om het eenvoudig te houden, beperken we onze uitvoer tot de top 3 matches. (Voor de volledige code, inclusief de benodigde imports en helperfuncties, zie het notebook .)
LIMIT = 3 MODEL_NAME = "sentence-transformers/all-mpnet-base-v2"
Laten we nu beginnen met onze multi-attribute monster search! Eerst proberen we de naïeve aanpak.
In onze naïeve aanpak embedden we attributen onafhankelijk en slaan ze op in verschillende indices. Tijdens de query voeren we meerdere kNN-zoekopdrachten uit op alle indices en combineren we vervolgens al onze gedeeltelijke resultaten tot één.
We beginnen met het definiëren van een klasse
NaiveRetriever
om een op overeenkomsten gebaseerde zoekopdracht uit te voeren op onze dataset, met behulp van onze door all-mpnet-base-v2
gegenereerde insluitingen.
class NaiveRetriever: def __init__(self, data: pd.DataFrame): self.model = SentenceTransformer(MODEL_NAME) self.data = data.copy() self.ids = self.data.index.to_list() self.knns = {} for key in self.data: embeddings = self.model.encode(self.data[key].values) knn = NearestNeighbors(metric="cosine").fit(embeddings) self.knns[key] = knn def search_key(self, key: str, value: str, limit: int = LIMIT) -> pd.DataFrame: embedding = self.model.encode(value) knn = self.knns[key] distances, indices = knn.kneighbors( [embedding], n_neighbors=limit, return_distance=True ) ids = [self.ids[i] for i in indices[0]] similarities = (1 - distances).flatten() # by definition: # cosine distance = 1 - cosine similarity result = pd.DataFrame( {"id": ids, f"score_{key}": similarities, key: self.data[key][ids]} ) result.set_index("id", inplace=True) return result def search(self, query: dict, limit: int = LIMIT) -> pd.DataFrame: results = [] for key, value in query.items(): if key not in self.knns: continue result_key = self.search_key(key, value, limit=limit) result_key.drop(columns=[key], inplace=True) results.append(result_key) merged_results = pd.concat(results, axis=1) merged_results["score"] = merged_results.mean(axis=1, skipna=False) merged_results.sort_values("score", ascending=False, inplace=True) return merged_results naive_retriever = NaiveRetriever(df.set_index("name"))
Laten we de eerste query uit de hierboven gegenereerde lijst gebruiken en met behulp van onze naive_retriever
naar monsters zoeken:
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } naive_retriever.search(query)
Ons
naive_retriever
retourneert de volgende zoekresultaten voor elk kenmerk:
id | score_kijken | Look |
---|---|---|
Fluisterende schaduw | 0,503578 | Schaduwrijk, amorf wezen met gloeiende ogen |
Zandstorm Djinn | 0,407344 | Wervelende zandwerveling met gloeiende symbolen |
Lichtgevend | 0,378619 | Motachtig wezen met gloeiende vleugels en antenne |
Geweldig! Onze geretourneerde monsterresultaten zijn relevant - ze hebben allemaal een "gloeiende" eigenschap.
Laten we eens kijken wat de naïeve aanpak oplevert als we naar de andere twee kenmerken zoeken.
id | score_habitat | leefgebied |
---|---|---|
Fluisterende schaduw | 0,609567 | Donkere bossen en verlaten gebouwen |
Schimmelnetwerk | 0,438856 | Ondergrondse grotten en vochtige bossen |
Doornwijn Elementair | 0,423421 | Overwoekerde ruïnes en dichte jungles |
id | score_gedrag | gedrag |
---|---|---|
Levende graffiti | 0,385741 | Verandert van vorm om op te gaan in de omgeving en absorbeert pigmenten |
Kristalvleugel Drake | 0,385211 | Verzamelt kostbare edelstenen en kan licht breken in krachtige stralen |
Lichtgevend | 0,345566 | Zendt rustgevende lichtpatronen uit om te communiceren en prooien aan te trekken |
Alle opgehaalde monsters bezitten de gewenste attributen. Op het eerste gezicht lijken de naïeve zoekresultaten veelbelovend. Maar we moeten monsters vinden die alle drie de attributen tegelijkertijd bezitten. Laten we onze resultaten samenvoegen om te zien hoe goed onze monsters dit doel bereiken:
id | score_kijken | score_habitat | score_gedrag |
---|---|---|---|
Fluisterende schaduw | 0,503578 | 0,609567 | |
Zandstorm Djinn | 0,407344 | | |
Lichtgevend | 0,378619 | | 0,345566 |
Schimmelnetwerk | | 0,438856 | |
Doornwijn Elementair | | 0,423421 | |
Levende graffiti | | | 0,385741 |
Kristalvleugel Drake | | | 0,385211 |
En hier worden de grenzen van de naïeve benadering duidelijk. Laten we evalueren:
look
: Er zijn drie monsters teruggevonden (Whispering Shade, Sandstorm Djinn en Luminoth).habitat
: Slechts één monster uit de look
was relevant (Whispering Shade).behavior
: Slechts één monster uit de look
was relevant (Luminoth), maar het is anders dan het monster dat relevant is voor habitat
.Kortom, de naïeve zoekbenadering slaagt er niet in om monsters te vinden die aan alle criteria tegelijk voldoen. Misschien kunnen we dit probleem oplossen door proactief meer monsters voor elk kenmerk op te halen? Laten we het proberen met 6 monsters per kenmerk, in plaats van 3. Laten we eens kijken wat deze benadering genereert:
id | score_kijken | score_habitat | score_gedrag |
---|---|---|---|
Fluisterende schaduw | 0,503578 | 0,609567 | |
Zandstorm Djinn | 0,407344 | 0,365061 | |
Lichtgevend | 0,378619 | | 0,345566 |
Nevelkwal | 0,36627 | | 0,259969 |
Dreamweaver-octopus | 0,315679 | | |
Kwantum vuurvliegje | 0,288578 | | |
Schimmelnetwerk | | 0,438856 | |
Doornwijn Elementair | | 0,423421 | |
Mistfantoom | | 0,366816 | 0,236649 |
Steenhart Golem | | 0,342287 | |
Levende graffiti | | | 0,385741 |
Kristalvleugel Drake | | | 0,385211 |
Aqua-Wraith | | | 0,283581 |
We hebben inmiddels 13 monsters opgehaald (meer dan de helft van onze kleine dataset!), en nog steeds hebben we hetzelfde probleem: geen enkel monster is voor alle drie de kenmerken opgehaald.
Het verhogen van het aantal teruggevonden monsters (meer dan 6) zou ons probleem kunnen oplossen, maar het creëert ook extra problemen:
Kortom, de naïeve aanpak is te onzeker en inefficiënt voor een levensvatbare zoekopdracht met meerdere kenmerken, vooral in productie.
Laten we onze tweede aanpak eens toepassen om te zien of die beter werkt dan de naïeve aanpak.
Eerst definiëren we het schema, de ruimtes, de index en de query:
@schema class Monster: id: IdField look: String habitat: String behavior: String monster = Monster() look_space = TextSimilaritySpace(text=monster.look, model=MODEL_NAME) habitat_space = TextSimilaritySpace(text=monster.habitat, model=MODEL_NAME) behavior_space = TextSimilaritySpace(text=monster.behavior, model=MODEL_NAME) monster_index = Index([look_space, habitat_space, behavior_space]) monster_query = ( Query( monster_index, weights={ look_space: Param("look_weight"), habitat_space: Param("habitat_weight"), behavior_space: Param("behavior_weight"), }, ) .find(monster) .similar(look_space.text, Param("look")) .similar(habitat_space.text, Param("habitat")) .similar(behavior_space.text, Param("behavior")) .limit(LIMIT) ) default_weights = { "look_weight": 1.0, "habitat_weight": 1.0, "behavior_weight": 1.0 }
Nu starten we de executor en uploaden de gegevens:
monster_parser = DataFrameParser(monster, mapping={monster.id: "name"}) source: InMemorySource = InMemorySource(monster, parser=monster_parser) executor = InMemoryExecutor(sources=[source], indices=[monster_index]) app = executor.run() source.put([df])
Laten we dezelfde query uitvoeren als in onze naïeve aanpak-implementatie hierboven:
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } app.query( monster_query, limit=LIMIT, **query, **default_weights )
id | score | Look | leefgebied | gedrag |
---|---|---|---|---|
Fluisterende schaduw | 0,376738 | Schaduwrijk, amorf wezen met gloeiende ogen | Donkere bossen en verlaten gebouwen | Voedt zich met angst en fluistert verontrustende waarheden |
Lichtgevend | 0,340084 | Motachtig wezen met gloeiende vleugels en antenne | Dichte bossen en jungles met bioluminescente flora | Zendt rustgevende lichtpatronen uit om te communiceren en prooien aan te trekken |
Levende graffiti | 0,330587 | Tweedimensionaal, kleurrijk wezen dat op vlakke oppervlakken leeft | Stedelijke gebieden, met name muren en billboards | Verandert van vorm om op te gaan in de omgeving en absorbeert pigmenten |
Et voila! Deze keer scoort elk van onze best teruggekeerde monsters hoog in een score die een soort "gemiddelde" vertegenwoordigt van alle drie de kenmerken die we willen dat ons monster heeft. Laten we de score van elk monster in detail uitsplitsen:
id | Look | leefgebied | gedrag | totaal |
---|---|---|---|---|
Fluisterende schaduw | 0,167859 | 0,203189 | 0,005689 | 0,376738 |
Lichtgevend | 0,126206 | 0,098689 | 0,115189 | 0,340084 |
Levende graffiti | 0,091063 | 0,110944 | 0,12858 | 0,330587 |
Onze tweede en derde resultaten, Luminoth en Living Graffiti, bezitten allebei alle drie de gewenste kenmerken. Het beste resultaat, Whispering Shade, is weliswaar minder relevant in termen van lichtmanipulatie - zoals blijkt uit de behavior
(0,006), maar heeft "gloeiende" kenmerken en een donkere omgeving waardoor zijn look
(0,168) en habitat
(0,203) erg hoog scoren, wat hem de hoogste totaalscore (0,377) oplevert, wat hem het meest relevante monster in het algemeen maakt. Wat een verbetering!
Kunnen we onze resultaten repliceren? Laten we een andere query proberen en erachter komen.
query = { 'look': 'shapeshifting', 'habitat': 'varied landscapes', 'behavior': 'illusion creation' }
id | score | Look | leefgebied | gedrag |
---|---|---|---|---|
Mistfantoom | 0,489574 | Etherische, mistachtige humanoïde met veranderende kenmerken | Moerassen, heidevelden en mistige kustlijnen | Lokt reizigers op een dwaalspoor met illusies en gefluister |
Zephyr-danser | 0,342075 | Sierlijk vogelwezen met iriserende veren | Hoge bergtoppen en door de wind geteisterde vlakten | Creëert betoverende luchtshows om partners aan te trekken |
Fluisterende schaduw | 0,337434 | Schaduwrijk, amorf wezen met gloeiende ogen | Donkere bossen en verlaten gebouwen | Voedt zich met angst en fluistert verontrustende waarheden |
Geweldig! Onze resultaten zijn opnieuw uitstekend.
Wat als we monsters willen vinden die lijken op een specifiek monster uit onze dataset? Laten we het proberen met een monster dat we nog niet hebben gezien - Harmonic Coral. We kunnen attributen voor dit monster extraheren en handmatig queryparameters maken. Maar Superlinked heeft een with_vector
-methode die we kunnen gebruiken op het query-object. Omdat de id van elk monster zijn naam is, kunnen we onze aanvraag zo eenvoudig uitdrukken als:
app.query( monster_query.with_vector(monster, "Harmonic Coral"), **default_weights, limit=LIMIT )
id | score | Look | leefgebied | gedrag |
---|---|---|---|---|
Harmonisch koraal | 1 | Vertakte, muziekinstrumentachtige structuur met trillende ranken | Ondiepe zeeën en getijdenpoelen | Creëert complexe melodieën om emoties te communiceren en te beïnvloeden |
Dreamweaver-octopus | 0,402288 | Koppotigen met tentakels die glinsteren als poollicht | Diepe oceaantroggen en onderwatergrotten | Beïnvloedt de dromen van nabijgelegen wezens |
Aqua-Wraith | 0,330869 | Doorschijnend humanoïde figuur gemaakt van stromend water | Rivieren, meren en kustgebieden | Verandert van vorm om zich aan te passen aan waterlichamen en controleert stromingen |
Het bovenste resultaat is het meest relevante, Harmonic Coral zelf, zoals verwacht. De andere twee monsters die onze zoekopdracht ophaalt zijn Dreamweaver Octopus en Aqua Wraith. Beide delen belangrijke thematische ( attribuut ) elementen met Harmonic Coral:
habitat
)behavior
)look
) Stel nu dat we meer belang willen hechten aan het kenmerk look
. Met het Superlinked-framework kunnen we gewichten eenvoudig aanpassen tijdens de query. Voor een eenvoudige vergelijking zoeken we naar monsters die lijken op Harmonic Coral, maar waarbij onze gewichten zijn aangepast om look
te bevoordelen.
weights = { "look_weight": 1.0, "habitat_weight": 0, "behavior_weight": 0 } app.query( monster_query.with_vector(monster, "Harmonic Coral"), limit=LIMIT, **weights )
id | score | Look | leefgebied | gedrag |
---|---|---|---|---|
Harmonisch koraal | 0,57735 | Vertakte, muziekinstrumentachtige structuur met trillende ranken | Ondiepe zeeën en getijdenpoelen | Creëert complexe melodieën om emoties te communiceren en te beïnvloeden |
Doornwijn Elementair | 0,252593 | Plantachtig wezen met een lichaam van gedraaide wijnranken en doornen | Overwoekerde ruïnes en dichte jungles | Groeit snel en beheerst het omringende plantenleven |
Plasma slang | 0,243241 | Slangachtig wezen gemaakt van knetterende energie | Elektrische stormen en elektriciteitscentrales | Voedt zich met elektrische stromen en kan technologie kortsluiten |
Onze resultaten lijken allemaal (passend genoeg) op elkaar: "Vertakt met trillende ranken", "Plant-achtig wezen met een lichaam van kronkelige wijnranken en doornen", "Slang-achtig".
Laten we nu nog een zoekopdracht uitvoeren, waarbij we het uiterlijk negeren en in plaats daarvan op zoek gaan naar monsters die qua habitat
en behavior
op elkaar lijken:
weights = { "look_weight": 0, "habitat_weight": 1.0, "behavior_weight": 1.0 }
id | score | Look | leefgebied | gedrag |
---|---|---|---|---|
Harmonisch koraal | 0,816497 | Vertakte, muziekinstrumentachtige structuur met trillende ranken | Ondiepe zeeën en getijdenpoelen | Creëert complexe melodieën om emoties te communiceren en te beïnvloeden |
Dreamweaver-octopus | 0,357656 | Koppotigen met tentakels die glinsteren als poollicht | Diepe oceaantroggen en onderwatergrotten | Beïnvloedt de dromen van nabijgelegen wezens |
Mistfantoom | 0,288106 | Etherische, mistachtige humanoïde met veranderende kenmerken | Moerassen, heidevelden en mistige kustlijnen | Lokt reizigers op een dwaalspoor met illusies en gefluister |
Opnieuw levert de Superlinked-aanpak geweldige resultaten op. Alle drie de monsters leven in waterige omgevingen en bezitten gedachtenbeheersende vermogens.
Laten we tot slot nog een zoekopdracht uitvoeren, waarbij we alle drie de kenmerken anders wegen. Zo vinden we monsters die er, vergeleken met Harmonic Coral, enigszins hetzelfde uitzien, in heel andere habitats leven en zich heel vergelijkbaar gedragen:
weights = { "look_weight": 0.5, "habitat_weight": -1.0, "behavior_weight": 1.0 }
id | score | Look | leefgebied | gedrag |
---|---|---|---|---|
Harmonisch koraal | 0,19245 | Vertakte, muziekinstrumentachtige structuur met trillende ranken | Ondiepe zeeën en getijdenpoelen | Creëert complexe melodieën om emoties te communiceren en te beïnvloeden |
Lichtgevend | 0,149196 | Motachtig wezen met gloeiende vleugels en antenne | Dichte bossen en jungles met bioluminescente flora | Zendt rustgevende lichtpatronen uit om te communiceren en prooien aan te trekken |
Zephyr-danser | 0,136456 | Sierlijk vogelwezen met iriserende veren | Hoge bergtoppen en door de wind geteisterde vlakten | Creëert betoverende luchtshows om partners aan te trekken |
Weer geweldige resultaten! Onze twee andere teruggevonden monsters — Luminoth en Zephyr Dancer — vertonen gedrag dat lijkt op Harmonic Coral en leven in andere habitats dan die van Harmonic Coral. Ze zien er ook heel anders uit dan Harmonic Coral. (Hoewel de ranken van Harmonic Coral en de antenne van Luminoth enigszins vergelijkbare kenmerken hebben, hebben we de look_weight
slechts met 0,5 verlaagd, en daar houdt de gelijkenis tussen de twee monsters op.)
Laten we eens kijken hoe de totale scores van deze monsters verdeeld zijn in termen van individuele eigenschappen:
id | Look | leefgebied | gedrag | totaal |
---|---|---|---|---|
Harmonisch koraal | 0,19245 | -0,3849 | 0,3849 | 0,19245 |
Lichtgevend | 0,052457 | -0,068144 | 0,164884 | 0,149196 |
Zephyr-danser | 0,050741 | -0,079734 | 0,165449 | 0,136456 |
Door habitat_weight
negatief te wegen (-1.0), "duwen" we opzettelijk monsters met vergelijkbare habitats weg en in plaats daarvan komen er monsters aan de oppervlakte waarvan de omgevingen verschillen van die van Harmonic Coral - zoals te zien is in de negatieve habitat
van Luminoth en Zephyr Dancer. De behavior
van Luminoth en Zephyr Dancer zijn relatief hoog, wat hun gedragsgelijkenis met Harmonic Coral aangeeft. Hun look
zijn positief maar lager, wat enige maar geen extreme visuele gelijkenis met Harmonic Coral weerspiegelt.
Kortom, onze strategie om habitat_weight
terug te brengen naar -1,0 en look_weight
naar 0,5, maar behavior_weight
op 1,0 te houden, blijkt effectief bij het aan de oppervlakte brengen van monsters die belangrijke gedragskenmerken delen met Harmonic Coral, maar die een heel andere omgeving hebben en er in ieder geval iets anders uitzien.
Multi-attribute vector search is een significante vooruitgang in information retrieval, en biedt meer nauwkeurigheid, contextueel begrip en flexibiliteit dan basic semantic similarity search. Toch is onze naïeve aanpak (hierboven) - het apart opslaan en doorzoeken van attribuutvectoren en het vervolgens combineren van resultaten - beperkt in vermogen, subtiliteit en efficiëntie wanneer we objecten met meerdere gelijktijdige attributen moeten ophalen. (Bovendien nemen meerdere kNN-zoekopdrachten meer tijd in beslag dan een enkele zoekopdracht met geconcateneerde vectoren.)
Om dit soort scenario's te verwerken, is het beter om al uw attribuutvectoren in dezelfde vectoropslag op te slaan en één zoekopdracht uit te voeren, waarbij u uw attributen op het moment van de query weegt. De Superlinked-benadering is nauwkeuriger, efficiënter en schaalbaarder dan de naïeve benadering voor elke toepassing die snelle, betrouwbare, genuanceerde, multi-attribuut vectoropvraging vereist - of uw use case nu gaat over het aanpakken van echte data-uitdagingen in uw e-commerce- of aanbevelingssysteem... of iets heel anders, zoals het bestrijden van monsters.
Oorspronkelijk hier gepubliceerd.