En AI-drevet Army of Darkness 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 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. tusindvis Hvad hvis vi kunne skabe et værktøj, der øjeblikkeligt finder det monster, der passer bedst til hvert scenarie? Et , og sikrer, at hvert møde er så fordybende og spændende som muligt? værktøj, der overvejer flere faktorer samtidigt 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! Oprettelse af væsner med vektorsøgning, hvorfor gøre det? 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. Vektorsøgning 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 og . opbevares søges den tilgang - gem hver attributvektor i separate vektorlagre (én pr. attribut), udfør en separat søgning for hver attribut, kombiner søgeresultater og efterbearbejd (f.eks. vægt) efter behov. naive -tilgangen - sammenkæde og gem alle attributvektorer i det samme vektorlager (ved hjælp af Superlinkeds indbyggede funktionalitet), som giver os mulighed for at , med tilhørende effektivitetsgevinster. Superlinkeds lader os vægte hver egenskab på forespørgselstidspunktet for at vise mere relevante resultater uden efterbehandling. Superlinked søge kun én gang spaces også 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! Datasæt 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 Hentning 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 tilgang. naive Naiv 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 -genererede indlejringer. all-mpnet-base-v2 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 . Lad os slå vores resultater sammen for at se, hvor godt vores monstre klarer sig til at nå dette mål: alle tre egenskaber samtidigt 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: Relevans efter egenskab: : Tre monstre blev hentet (Whispering Shade, Sandstorm Djinn og Luminoth). look : Kun ét monster fra var relevant (Whispering Shade). habitat look : Kun ét monster fra var relevant (Luminoth), men det er forskelligt fra det, der er relevant for . behavior look habitat Samlet relevans: Intet enkelt monster blev hentet for alle tre attributter samtidigt. Resultaterne er fragmenterede: forskellige monstre er relevante for forskellige egenskaber. Kort sagt formår den naive søgetilgang ikke at finde monstre, der opfylder alle kriterier på én gang. Lad os prøve det med 6 monstre pr. egenskab i stedet for 3. Lad os se på, hvad denne tilgang genererer: Måske kan vi løse dette problem ved proaktivt at hente flere monstre for hver egenskab? 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 det samme problem: ikke ét af disse monstre blev hentet for alle tre attributter. stadig At øge antallet af hentede monstre (ud over 6) løse vores problem, men det skaber yderligere problemer: kan I produktion forlænger søgning af flere resultater (flere kNN-søgninger) søgetiden mærkbart. For hver ny attribut, vi introducerer, falder vores chancer for at finde et "komplet" monster - med alle attributterne i vores forespørgsel - eksponentielt. For at forhindre dette er vi nødt til at hente mange flere nærmeste naboer (monstre), hvilket får det samlede antal af hentede monstre til at vokse eksponentielt. Vi har stadig ingen garanti for, at vi vil hente monstre, der besidder alle vores ønskede egenskaber. Hvis det lykkes os at hente monstre, der opfylder alle kriterier på én gang, bliver vi nødt til at bruge yderligere overhead-afstemningsresultater. Sammenfattende er den naive tilgang for usikker og ineffektiv til brugbar multi-attribut søgning, især i produktion. Superlinket tilgang 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 (0,006), har "glødende" funktioner og et mørkt miljø, der gør, at dets (0,168) og (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! behavior look habitat 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 udtrække attributter for dette monster og oprette forespørgselsparametre manuelt. Men Superlinked har en -metode, vi kan bruge på forespørgselsobjektet. Fordi hvert monsters id er dets navn, kan vi udtrykke vores anmodning så enkelt som: kunne with_vector 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 ( ) elementer med Harmonic Coral: attribut Akvatiske levesteder ( ) habitat Evne til at påvirke eller manipulere deres omgivelser ( ) behavior Dynamiske eller flydende visuelle egenskaber ( ) look Attributvægtning Antag nu, at vi ønsker at lægge større vægt på . 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 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 og samtidigt: habitat behavior 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 med 0,5, og ligheden mellem de to monstre ender der.) look_weight 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 (-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 . Luminoths og Zephyr Dancers er relativt høje, hvilket indikerer deres adfærdsmæssige lighed med Harmonic Coral. Deres er positive, men lavere, hvilket afspejler , men ikke ekstrem visuel lighed med Harmonic Coral. habitat_weight habitat behavior look en vis Kort sagt, vores strategi med at nedvægte til -1,0 og til 0,5, men at holde 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. habitat_weight look_weight behavior_weight Konklusion 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, 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.) og derefter For at håndtere scenarier som dette er det bedre at gemme alle dine attributvektorer i det samme vektorlager og udføre 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. en enkelt søgning Bidragydere Andrey Pikunov, forfatter Mór Kapronczay, redaktør Oprindeligt udgivet . her