Det er spilleaften, dine venner sidder omkring spillebordet og venter på at se, hvilken Dungeons & Dragons (D&D)-karakter de bliver, og den mission, de vil begive sig ud på. I aften er du Dungeon Master (fortæller og guide), skaberen af spændende møder for at udfordre og tryllebinde dine spillere. Din troværdige D&D Monster Manual indeholder tusindvis af væsner. At finde det perfekte monster til hver situation blandt de utallige muligheder kan være overvældende. Den ideelle fjende skal matche omgivelserne, sværhedsgraden og fortællingen i øjeblikket.
Hvad hvis vi kunne skabe et værktøj, der øjeblikkeligt finder det monster, der passer bedst til hvert scenarie? Et værktøj, der overvejer flere faktorer samtidigt , og sikrer, at hvert møde er så fordybende og spændende som muligt?
Lad os gå i gang med vores egen søgen: Byg det ultimative monster-finding-system ved hjælp af kraften i vektorsøgning med flere attributter!
Vektorsøgning repræsenterer en revolution inden for informationssøgning. Vektorindlejring - ved at tage hensyn til kontekst og semantisk betydning - giver vektorsøgning mulighed for at returnere mere relevante og nøjagtige resultater, håndtere ikke kun strukturerede, men også ustrukturerede data og flere sprog, og skalere. Men for at generere højkvalitetssvar i applikationer fra den virkelige verden er vi ofte nødt til at tildele forskellige vægte til specifikke attributter for vores dataobjekter.
Der er to almindelige tilgange til vektorsøgning med flere attributter. Begge starter med at indlejre hver attribut for et dataobjekt separat. Den største forskel mellem disse to tilgange er, hvordan vores indlejringer opbevares og søges .
spaces
lader os også vægte hver egenskab på forespørgselstidspunktet for at vise mere relevante resultater uden efterbehandling. Nedenfor vil vi bruge disse to tilgange til at implementere et vektorsøgeværktøj med flere attributter - en Dungeons and Dragons monsterfinder! Vores enkle implementeringer, især den anden, vil illustrere, hvordan man kan skabe mere kraftfulde og fleksible søgesystemer, dem, der kan håndtere komplekse, mangesidede forespørgsler med lethed, uanset hvad du bruger.
Hvis du er ny til vektorlighedssøgning, så fortvivl ikke! Vi har dig dækket - tjek vores byggeklodsartikler .
Okay, lad os gå på monsterjagt!
Først genererer vi et lille syntetisk datasæt af monstre ved at spørge en Large Language Model (LLM):
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": "..."}, ... ] }
Lad os tage et kig på et eksempel på det datasæt, som vores LLM genererede. Bemærk: LLM-generering er ikke-deterministisk, så dine resultater kan variere.
Her er vores første fem monstre:
# | navn | se | levested | opførsel |
---|---|---|---|---|
0 | Luminoth | Møllignende væsen med glødende vinger og antenne | Tætte skove og jungler med selvlysende flora | Udsender beroligende lysmønstre for at kommunikere og tiltrække bytte |
1 | Aqua Wraith | Gennemsigtig humanoid figur lavet af strømmende vand | Floder, søer og kystområder | Shapeshifts for at blande sig med vandområder og styrer strømme |
2 | Stenhjerte Golem | Massiv humanoid sammensat af sammenlåsende klippeformationer | Klippebjerge og gamle ruiner | Går i dvale i århundreder, vågner for at beskytte sit territorium |
3 | Whispering Shade | Skyggefuldt, amorft væsen med glødende øjne | Mørke skove og forladte bygninger | Nærer sig af frygt og hvisker foruroligende sandheder |
4 | Zephyr danser | Yndefuldt fuglevæsen med iriserende fjer | Høje bjergtoppe og vindomsuste sletter | Skaber fascinerende luftskærme for at tiltrække venner |
...og vores genererede forespørgsler:
| Se | Habitat | Opførsel |
---|---|---|---|
0 | Glødende | Mørke steder | Let manipulation |
1 | Elementært | Ekstreme miljøer | Miljøkontrol |
2 | Formskifte | Varierede landskaber | Illusion skabelse |
3 | Krystallinsk | Mineralrige områder | Energioptagelse |
4 | Æterisk | Atmosfærisk | Sind indflydelse |
Se originalt datasæt og eksempler på forespørgsler her .
Lad os opsætte parametre, vi vil bruge i begge vores tilgange - naive og Superlinked - nedenfor.
Vi genererer vores vektorindlejringer med:
sentence-transformers/all-mpnet-base-v2.
For nemheds skyld begrænser vi vores output til top 3 kampe. (For komplet kode, inklusive nødvendige importer og hjælpefunktioner, se notesbogen .)
LIMIT = 3 MODEL_NAME = "sentence-transformers/all-mpnet-base-v2"
Lad os nu sætte gang i vores monstersøgning med flere attributter! Først vil vi prøve den naive tilgang.
I vores naive tilgang indlejrer vi attributter uafhængigt og gemmer dem i forskellige indekser. På forespørgselstidspunktet kører vi flere kNN-søgninger på alle indekserne og kombinerer derefter alle vores delresultater til ét.
Vi starter med at definere en klasse
NaiveRetriever
at udføre lighedsbaseret søgning på vores datasæt ved hjælp af vores all-mpnet-base-v2
-genererede indlejringer.
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"))
Lad os bruge den første forespørgsel fra vores genererede liste ovenfor og søge efter monstre ved hjælp af vores naive_retriever
:
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } naive_retriever.search(query)
Vores
naive_retriever
returnerer følgende søgeresultater for hver attribut:
id | score_look | se |
---|---|---|
Whispering Shade | 0,503578 | Skyggefuldt, amorft væsen med glødende øjne |
Sandstorm Djinn | 0,407344 | Hvirvlende hvirvel af sand med lysende symboler |
Luminoth | 0,378619 | Møllignende væsen med glødende vinger og antenne |
Fantastisk! Vores returnerede monsterresultater er relevante - de har alle en eller anden "glødende" egenskab.
Lad os se, hvad den naive tilgang returnerer, når vi søger efter de to andre attributter.
id | score_habitat | levested |
---|---|---|
Whispering Shade | 0,609567 | Mørke skove og forladte bygninger |
Svampe netværk | 0,438856 | Underjordiske huler og fugtige skove |
Thornvine Elemental | 0,423421 | Tilgroede ruiner og tætte jungler |
id | score_adfærd | opførsel |
---|---|---|
Levende graffiti | 0,385741 | Formskifter for at blande sig med omgivelserne og absorberer pigmenter |
Crystalwing Drake | 0,385211 | Opsamler ædelsten og kan bryde lys til kraftige stråler |
Luminoth | 0,345566 | Udsender beroligende lysmønstre for at kommunikere og tiltrække bytte |
Alle de hentede monstre har de ønskede egenskaber. Ved første øjekast kan de naive søgeresultater virke lovende. Men vi skal finde monstre, der besidder alle tre egenskaber samtidigt . Lad os slå vores resultater sammen for at se, hvor godt vores monstre klarer sig til at nå dette mål:
id | score_look | score_habitat | score_adfærd |
---|---|---|---|
Whispering Shade | 0,503578 | 0,609567 | |
Sandstorm Djinn | 0,407344 | | |
Luminoth | 0,378619 | | 0,345566 |
Svampe netværk | | 0,438856 | |
Thornvine Elemental | | 0,423421 | |
Levende graffiti | | | 0,385741 |
Crystalwing Drake | | | 0,385211 |
Og her bliver grænserne for den naive tilgang tydelige. Lad os evaluere:
look
: Tre monstre blev hentet (Whispering Shade, Sandstorm Djinn og Luminoth).habitat
: Kun ét monster fra look
var relevant (Whispering Shade).behavior
: Kun ét monster fra look
var relevant (Luminoth), men det er forskelligt fra det, der er relevant for habitat
.Kort sagt formår den naive søgetilgang ikke at finde monstre, der opfylder alle kriterier på én gang. Måske kan vi løse dette problem ved proaktivt at hente flere monstre for hver egenskab? Lad os prøve det med 6 monstre pr. egenskab i stedet for 3. Lad os se på, hvad denne tilgang genererer:
id | score_look | score_habitat | score_adfærd |
---|---|---|---|
Whispering Shade | 0,503578 | 0,609567 | |
Sandstorm Djinn | 0,407344 | 0,365061 | |
Luminoth | 0,378619 | | 0,345566 |
Nebula vandmænd | 0,36627 | | 0,259969 |
Dreamweaver blæksprutte | 0,315679 | | |
Kvanteildflue | 0,288578 | | |
Svampe netværk | | 0,438856 | |
Thornvine Elemental | | 0,423421 | |
Mist Phantom | | 0,366816 | 0,236649 |
Stenhjerte Golem | | 0,342287 | |
Levende graffiti | | | 0,385741 |
Crystalwing Drake | | | 0,385211 |
Aqua Wraith | | | 0,283581 |
Vi har nu hentet 13 monstre (mere end halvdelen af vores lille datasæt!), og har stadig det samme problem: ikke ét af disse monstre blev hentet for alle tre attributter.
At øge antallet af hentede monstre (ud over 6) kan løse vores problem, men det skaber yderligere problemer:
Sammenfattende er den naive tilgang for usikker og ineffektiv til brugbar multi-attribut søgning, især i produktion.
Lad os implementere vores anden tilgang for at se, om den gør noget bedre end den naive.
Først definerer vi skema, mellemrum, indeks og forespørgsel:
@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 starter vi eksekveren og uploader dataene:
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])
Lad os køre den samme forespørgsel, som vi kørte i vores naive tilgangsimplementering ovenfor:
query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } app.query( monster_query, limit=LIMIT, **query, **default_weights )
id | score | se | levested | opførsel |
---|---|---|---|---|
Whispering Shade | 0,376738 | Skyggefuldt, amorft væsen med glødende øjne | Mørke skove og forladte bygninger | Nærer sig af frygt og hvisker foruroligende sandheder |
Luminoth | 0,340084 | Møllignende væsen med glødende vinger og antenne | Tætte skove og jungler med selvlysende flora | Udsender beroligende lysmønstre for at kommunikere og tiltrække bytte |
Levende graffiti | 0,330587 | Todimensionelt, farverigt væsen, der bebor flade overflader | Byområder, især vægge og billboards | Formskifter for at blande sig med omgivelserne og absorberer pigmenter |
Og voila! Denne gang rangerer hvert af vores top returnerede monstre højt i en score, der repræsenterer en slags "middelværdi" af alle tre egenskaber, vi ønsker, at vores monster skal have. Lad os opdele hvert monsters score i detaljer:
id | se | levested | opførsel | total |
---|---|---|---|---|
Whispering Shade | 0,167859 | 0,203189 | 0,005689 | 0,376738 |
Luminoth | 0,126206 | 0,098689 | 0,115189 | 0,340084 |
Levende graffiti | 0,091063 | 0,110944 | 0,12858 | 0,330587 |
Vores andet og tredje resultat, Luminoth og Living Graffiti, besidder begge de tre ønskede egenskaber. Topresultatet, Whispering Shade, selvom det er mindre relevant med hensyn til lysmanipulation - som afspejlet i dets behavior
(0,006), har "glødende" funktioner og et mørkt miljø, der gør, at dets look
(0,168) og habitat
(0,203) scorer meget høj, hvilket giver den den højeste samlede score (0,377), hvilket gør den til det mest relevante monster generelt. Hvilken forbedring!
Kan vi kopiere vores resultater? Lad os prøve en anden forespørgsel og finde ud af det.
query = { 'look': 'shapeshifting', 'habitat': 'varied landscapes', 'behavior': 'illusion creation' }
id | score | se | levested | opførsel |
---|---|---|---|---|
Mist Phantom | 0,489574 | Æterisk, tågelignende humanoid med skiftende funktioner | Sumpe, moser og tågede kyster | Lokker rejsende på afveje med illusioner og hvisken |
Zephyr danser | 0,342075 | Yndefuldt fuglevæsen med iriserende fjer | Høje bjergtoppe og vindomsuste sletter | Skaber fascinerende luftskærme for at tiltrække venner |
Whispering Shade | 0,337434 | Skyggefuldt, amorft væsen med glødende øjne | Mørke skove og forladte bygninger | Nærer sig af frygt og hvisker foruroligende sandheder |
Stor! Vores resultater er igen fremragende.
Hvad hvis vi vil finde monstre, der ligner et specifikt monster fra vores datasæt? Lad os prøve det med et monster, vi endnu ikke har set - Harmonic Coral. Vi kunne udtrække attributter for dette monster og oprette forespørgselsparametre manuelt. Men Superlinked har en with_vector
-metode, vi kan bruge på forespørgselsobjektet. Fordi hvert monsters id er dets navn, kan vi udtrykke vores anmodning så enkelt som:
app.query( monster_query.with_vector(monster, "Harmonic Coral"), **default_weights, limit=LIMIT )
id | score | se | levested | opførsel |
---|---|---|---|---|
Harmonisk koral | 1 | Forgrenende, musikinstrumentlignende struktur med vibrerende ranker | Lavvandede hav og tidevandsbassiner | Skaber komplekse melodier til at kommunikere og påvirke følelser |
Dreamweaver blæksprutte | 0,402288 | Blæksprutter med tentakler, der skinner som nordlys | Dybe havgrave og undersøiske huler | Påvirker nærliggende skabningers drømme |
Aqua Wraith | 0,330869 | Gennemsigtig humanoid figur lavet af strømmende vand | Floder, søer og kystområder | Shapeshifts for at blande sig med vandområder og styrer strømme |
Det øverste resultat er det mest relevante, selve Harmonic Coral, som forventet. De to andre monstre, vores søgning henter, er Dreamweaver Octopus og Aqua Wraith. Begge deler vigtige tematiske ( attribut ) elementer med Harmonic Coral:
habitat
)behavior
)look
) Antag nu, at vi ønsker at lægge større vægt på look
. Superlinked-rammen lader os nemt justere vægte på forespørgselstidspunktet. For nem sammenligning søger vi efter monstre, der ligner Harmonic Coral, men med vores vægte justeret til at favorisere look
.
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 | se | levested | opførsel |
---|---|---|---|---|
Harmonisk koral | 0,57735 | Forgrenende, musikinstrumentlignende struktur med vibrerende ranker | Lavvandede hav og tidevandsbassiner | Skaber komplekse melodier til at kommunikere og påvirke følelser |
Thornvine Elemental | 0,252593 | Plantelignende væsen med en krop af snoede vinstokke og torne | Tilgroede ruiner og tætte jungler | Vokser hurtigt og styrer det omgivende planteliv |
Plasma slange | 0,243241 | Slangelignende væsen lavet af knitrende energi | Elektriske storme og kraftværker | Nærer sig med elektriske strømme og kan kortslutte teknologi |
Vores resultater har alle (behørigt) lignende udseende - "Forgrenede med vibrerende ranker", "Plantelignende væsen med en krop af snoede vinstokke og torne", "Slangelignende".
Lad os nu lave en ny søgning, ignorere udseendet og i stedet lede efter monstre, der ligner hinanden med hensyn til habitat
og behavior
samtidigt:
weights = { "look_weight": 0, "habitat_weight": 1.0, "behavior_weight": 1.0 }
id | score | se | levested | opførsel |
---|---|---|---|---|
Harmonisk koral | 0,816497 | Forgrenende, musikinstrumentlignende struktur med vibrerende ranker | Lavt hav og tidevandsbassiner | Skaber komplekse melodier til at kommunikere og påvirke følelser |
Dreamweaver blæksprutte | 0,357656 | Blæksprutter med tentakler, der skinner som nordlys | Dybe havgrave og undersøiske huler | Påvirker nærliggende skabningers drømme |
Mist Phantom | 0,288106 | Æterisk, tågelignende humanoid med skiftende funktioner | Sumpe, moser og tågede kyster | Lokker rejsende på afveje med illusioner og hvisken |
Igen giver Superlinked-tilgangen fantastiske resultater. Alle tre monstre lever i vandfyldte miljøer og besidder sindkontrollerende evner.
Lad os endelig prøve en anden søgning, der vægter alle tre egenskaber forskelligt - for at finde monstre, der i sammenligning med Harmonic Coral ligner noget, lever i meget forskellige habitater og har meget ens adfærd:
weights = { "look_weight": 0.5, "habitat_weight": -1.0, "behavior_weight": 1.0 }
id | score | se | levested | opførsel |
---|---|---|---|---|
Harmonisk koral | 0,19245 | Forgrenende, musikinstrumentlignende struktur med vibrerende ranker | Lavt hav og tidevandsbassiner | Skaber komplekse melodier til at kommunikere og påvirke følelser |
Luminoth | 0,149196 | Møllignende væsen med glødende vinger og antenne | Tætte skove og jungler med selvlysende flora | Udsender beroligende lysmønstre for at kommunikere og tiltrække bytte |
Zephyr danser | 0,136456 | Yndefuldt fuglevæsen med iriserende fjer | Høje bjergtoppe og vindomsuste sletter | Skaber fascinerende luftskærme for at tiltrække venner |
Flotte resultater igen! Vores to andre hentede monstre - Luminoth og Zephyr Dancer - har adfærd, der ligner Harmonic Coral og lever i habitater, der er forskellige fra Harmonic Coral's. De ser også meget anderledes ud end Harmonic Coral. (Mens Harmonic Corals ranker og Luminoths antenne er noget lignende funktioner, har vi kun nedvægtet look_weight
med 0,5, og ligheden mellem de to monstre ender der.)
Lad os se, hvordan disse monstres samlede score bryder ud med hensyn til individuelle egenskaber:
id | se | levested | opførsel | total |
---|---|---|---|---|
Harmonisk koral | 0,19245 | -0,3849 | 0,3849 | 0,19245 |
Luminoth | 0,052457 | -0,068144 | 0,164884 | 0,149196 |
Zephyr danser | 0,050741 | -0,079734 | 0,165449 | 0,136456 |
Ved negativ vægtning af habitat_weight
(-1,0) "skubber vi bevidst væk" monstre med lignende habitater og overflader i stedet monstre, hvis miljø er anderledes end Harmonic Coral's - som det ses i Luminoths og Zephyr Dancers negative habitat
. Luminoths og Zephyr Dancers behavior
er relativt høje, hvilket indikerer deres adfærdsmæssige lighed med Harmonic Coral. Deres look
er positive, men lavere, hvilket afspejler en vis , men ikke ekstrem visuel lighed med Harmonic Coral.
Kort sagt, vores strategi med at nedvægte habitat_weight
til -1,0 og look_weight
til 0,5, men at holde behavior_weight
på 1,0, viser sig at være effektiv til at afsløre monstre, der deler vigtige adfærdsegenskaber med Harmonic Coral, men som har meget forskellige miljøer og ser i det mindste noget anderledes ud.
Vektorsøgning med flere attributter er et betydeligt fremskridt inden for informationssøgning, der tilbyder mere nøjagtighed, kontekstuel forståelse og fleksibilitet end grundlæggende semantisk lighedssøgning. Alligevel er vores naive tilgang (ovenfor) - lagring og søgning af attributvektorer separat, og derefter kombination af resultater - begrænset i evner, subtilitet og effektivitet, når vi skal hente objekter med flere samtidige attributter. (Desuden tager flere kNN-søgninger mere tid end en enkelt søgning med sammenkædede vektorer.)
For at håndtere scenarier som dette er det bedre at gemme alle dine attributvektorer i det samme vektorlager og udføre en enkelt søgning og vægte dine attributter på forespørgselstidspunktet. Superlinked-tilgangen er mere nøjagtig, effektiv og skalerbar end den naive tilgang til enhver applikation, der kræver hurtig, pålidelig, nuanceret, multi-attribute vektorhentning - uanset om din use case tackler virkelige dataudfordringer i dit e-handels- eller anbefalingssystem ... eller noget helt andet, som at kæmpe mod monstre.
Oprindeligt udgivet her .