Un ejército de las tinieblas impulsado por IA Es la noche de juegos, tus amigos están sentados alrededor de la mesa de juego, esperando ver en qué personaje de Dungeons & Dragons (D&D) se convertirán y en qué misión se embarcarán. Esta noche, eres el Dungeon Master (narrador y guía), creador de emocionantes encuentros para desafiar y cautivar a tus jugadores. Tu confiable Manual de Monstruos de D&D contiene de criaturas. Encontrar el monstruo perfecto para cada situación entre la gran cantidad de opciones puede ser abrumador. El enemigo ideal debe coincidir con el entorno, la dificultad y la narrativa del momento. miles ¿Qué pasaría si pudiéramos crear una herramienta que encuentre al instante el monstruo más adecuado para cada escenario? ¿Una , asegurando que cada encuentro sea lo más envolvente y emocionante posible? herramienta que considere múltiples factores simultáneamente Embarquémonos en nuestra propia búsqueda: ¡construir el sistema definitivo para encontrar monstruos, utilizando el poder de la búsqueda vectorial de múltiples atributos! Creando criaturas con búsqueda vectorial, ¿por qué hacerlo? representa una revolución en la recuperación de información. La incorporación de vectores (al tener en cuenta el contexto y el significado semántico) permite que la búsqueda vectorial arroje resultados más relevantes y precisos, gestione no solo datos estructurados sino también no estructurados y en varios idiomas, y sea escalable. Pero para generar respuestas de alta calidad en aplicaciones del mundo real, a menudo necesitamos asignar diferentes pesos a atributos específicos de nuestros objetos de datos. La búsqueda vectorial Existen dos enfoques comunes para la búsqueda de vectores de atributos múltiples. Ambos comienzan incorporando por separado cada atributo de un objeto de datos. La principal diferencia entre estos dos enfoques radica en cómo se y nuestras incorporaciones. almacenan se buscan el enfoque : almacenar cada vector de atributo en almacenes de vectores separados (uno por atributo), realizar una búsqueda separada para cada atributo, combinar los resultados de la búsqueda y realizar un posprocesamiento (por ejemplo, ponderación) según sea necesario. ingenuo El enfoque : concatenar y almacenar todos los vectores de atributos en el mismo almacén de vectores (utilizando la funcionalidad incorporada de Superlinked), lo que nos permite , con las consiguientes ganancias de eficiencia. de Superlinked nos permiten ponderar cada atributo en el momento de la consulta para obtener resultados más relevantes, sin posprocesamiento. Superlinked buscar solo una vez spaces también A continuación, utilizaremos estos dos enfoques para implementar una herramienta de búsqueda de vectores de atributos múltiples: ¡un buscador de monstruos de Dungeons and Dragons! Nuestras sencillas implementaciones, especialmente la segunda, ilustrarán cómo crear sistemas de búsqueda más potentes y flexibles, que puedan manejar consultas complejas y multifacéticas con facilidad, cualquiera sea su caso de uso. Si no tienes experiencia en búsquedas de similitud de vectores, ¡no te preocupes! Te ayudamos: consulta nuestros . artículos sobre componentes básicos ¡Muy bien, vamos a cazar monstruos! Conjunto de datos Primero, generaremos un pequeño conjunto de datos sintéticos de monstruos, solicitando un Modelo de Lenguaje Grande (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": "..."}, ... ] } Echemos un vistazo a una muestra del conjunto de datos generado por nuestro LLM. Nota: la generación de LLM no es determinista, por lo que los resultados pueden diferir. Aquí están nuestros primeros cinco monstruos: # nombre mirar hábitat comportamiento 0 Luminoth Criatura parecida a una polilla con alas y antenas brillantes. Bosques densos y selvas con flora bioluminiscente Emite patrones de luz relajantes para comunicarse y atraer presas. 1 Espectro acuático Figura humanoide translúcida hecha de agua fluyendo. Ríos, lagos y zonas costeras Cambia de forma para mezclarse con los cuerpos de agua y controla las corrientes. 2 Gólem de corazón de piedra Humanoide masivo compuesto de formaciones rocosas entrelazadas. Montañas rocosas y ruinas antiguas Hiberna durante siglos y despierta para proteger su territorio. 3 Sombra susurrante Ser sombrío y amorfo con ojos brillantes. Bosques oscuros y edificios abandonados. Se alimenta del miedo y susurra verdades inquietantes. 4 Bailarina del céfiro Graciosa criatura aviar con plumas iridiscentes. Altas cumbres montañosas y llanuras azotadas por el viento Crea exhibiciones aéreas fascinantes para atraer parejas. ...y nuestras consultas generadas: Mirar Hábitat Comportamiento 0 Brillante Lugares oscuros Manipulación de la luz 1 Elemental Entornos extremos Control ambiental 2 Cambio de forma Paisajes variados Creación de ilusiones 3 Cristalino Zonas ricas en minerales Absorción de energía 4 Etéreo Atmosférico Influencia de la mente Vea el conjunto de datos original y ejemplos de consultas . aquí Recuperación Configuremos a continuación los parámetros que usaremos en ambos enfoques (ingenuo y supervinculado). Generamos nuestras incrustaciones vectoriales con: sentence-transformers/all-mpnet-base-v2. Para simplificar, limitaremos nuestra salida a las 3 coincidencias principales. (Para ver el código completo, incluidas las importaciones y funciones auxiliares necesarias, consulte el ). cuaderno LIMIT = 3 MODEL_NAME = "sentence-transformers/all-mpnet-base-v2" Ahora, ¡comencemos nuestra búsqueda de monstruos con múltiples atributos! Primero, probaremos el enfoque . ingenuo Enfoque ingenuo En nuestro enfoque ingenuo, incorporamos atributos de forma independiente y los almacenamos en diferentes índices. En el momento de la consulta, ejecutamos múltiples búsquedas kNN en todos los índices y luego combinamos todos nuestros resultados parciales en uno. Comenzamos definiendo una clase NaiveRetriever para realizar una búsqueda basada en similitud en nuestro conjunto de datos, utilizando nuestras incrustaciones generadas por . 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")) Utilicemos la primera consulta de nuestra lista generada anteriormente y busquemos monstruos usando nuestro : naive_retriever query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } naive_retriever.search(query) Nuestro naive_retriever devuelve los siguientes resultados de búsqueda para cada atributo: identificación puntuación_mira mirar Sombra susurrante 0,503578 Ser sombrío y amorfo con ojos brillantes. Djinn de tormenta de arena 0,407344 Vórtice de arena con símbolos brillantes Luminoth 0,378619 Criatura parecida a una polilla con alas y antenas brillantes. ¡Genial! Los resultados de los monstruos que obtuvimos son relevantes: todos tienen alguna característica "brillante". Veamos qué devuelve el enfoque ingenuo cuando buscamos los otros dos atributos. identificación puntuación_hábitat hábitat Sombra susurrante 0,609567 Bosques oscuros y edificios abandonados. Red de hongos 0,438856 Cavernas subterráneas y bosques húmedos Elemental de la vid espinosa 0,423421 Ruinas cubiertas de vegetación y selvas densas identificación puntuación_comportamiento comportamiento Grafiti viviente 0,385741 Cambia de forma para mimetizarse con el entorno y absorbe pigmentos. Draco de alas de cristal 0,385211 Almacena gemas preciosas y puede refractar la luz en rayos poderosos. Luminoth 0,345566 Emite patrones de luz relajantes para comunicarse y atraer presas. Todos los monstruos recuperados poseen los atributos deseados. A primera vista, los resultados de la búsqueda ingenua pueden parecer prometedores, pero necesitamos encontrar monstruos que posean . Combinemos nuestros resultados para ver qué tan bien logran nuestros monstruos este objetivo: los tres atributos simultáneamente identificación puntuación_mira puntuación_hábitat puntuación_comportamiento Sombra susurrante 0,503578 0,609567 Djinn de tormenta de arena 0,407344 Luminoth 0,378619 0,345566 Red de hongos 0,438856 Elemental de la vid espinosa 0,423421 Grafiti viviente 0,385741 Draco de alas de cristal 0,385211 Y aquí es donde se hacen evidentes los límites del enfoque ingenuo. Evaluemos: Relevancia por atributo: : Se recuperaron tres monstruos (Whispering Shade, Sandstorm Djinn y Luminoth). look : Solo un monstruo de los resultados fue relevante (Whispering Shade). habitat look : Solo un monstruo de los resultados fue relevante (Luminoth), pero es diferente del que es relevante para . behavior look habitat Relevancia general: No se recuperó ningún monstruo para los tres atributos simultáneamente. Los resultados están fragmentados: diferentes monstruos son relevantes para diferentes atributos. En resumen, el enfoque de búsqueda ingenua no logra encontrar monstruos que satisfagan todos los criterios a la vez. Probemos con 6 monstruos por atributo, en lugar de 3. Veamos lo que genera este enfoque: ¿Quizás podamos solucionar este problema recuperando de manera proactiva más monstruos para cada atributo? identificación puntuación_mira puntuación_hábitat puntuación_comportamiento Sombra susurrante 0,503578 0,609567 Djinn de tormenta de arena 0,407344 0,365061 Luminoth 0,378619 0,345566 Medusa nebulosa 0,36627 0,259969 Pulpo de Dreamweaver 0,315679 Luciérnaga cuántica 0,288578 Red de hongos 0,438856 Elemental de la vid espinosa 0,423421 Fantasma de niebla 0,366816 0,236649 Gólem de corazón de piedra 0,342287 Grafiti viviente 0,385741 Draco de alas de cristal 0,385211 Espectro acuático 0,283581 Ya hemos recuperado 13 monstruos (¡más de la mitad de nuestro pequeño conjunto de datos!) y tenemos el mismo problema: ninguno de estos monstruos fue recuperado para los tres atributos. todavía Aumentar el número de monstruos recuperados (más allá de 6) resolver nuestro problema, pero crea problemas adicionales: podría En producción, recuperar más resultados (múltiples búsquedas kNN) alarga considerablemente el tiempo de búsqueda. Por cada nuevo atributo que introducimos, nuestras posibilidades de encontrar un monstruo "completo" (con todos los atributos de nuestra consulta) disminuyen exponencialmente. Para evitarlo, tenemos que recuperar muchos más monstruos vecinos más cercanos, lo que hace que el número total de monstruos recuperados crezca exponencialmente. Todavía no tenemos garantía de que recuperaremos monstruos que posean todos los atributos deseados. Si logramos recuperar monstruos que satisfacen todos los criterios a la vez, tendremos que gastar recursos adicionales para conciliar los resultados. En resumen, el enfoque ingenuo es demasiado incierto e ineficiente para una búsqueda multiatributo viable, especialmente en producción. Enfoque supervinculado Implementemos nuestro segundo enfoque para ver si funciona mejor que el ingenuo. Primero, definimos el esquema, los espacios, el índice y la consulta: @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 } Ahora, iniciamos el ejecutor y cargamos los datos: 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]) Ejecutemos la misma consulta que ejecutamos en nuestra implementación del enfoque ingenuo anterior: query = { 'look': 'glowing', 'habitat': 'dark places', 'behavior': 'light manipulation' } app.query( monster_query, limit=LIMIT, **query, **default_weights ) identificación puntaje mirar hábitat comportamiento Sombra susurrante 0,376738 Ser sombrío y amorfo con ojos brillantes. Bosques oscuros y edificios abandonados. Se alimenta del miedo y susurra verdades inquietantes. Luminoth 0,340084 Criatura parecida a una polilla con alas y antenas brillantes. Bosques densos y selvas con flora bioluminiscente Emite patrones de luz relajantes para comunicarse y atraer presas. Grafiti viviente 0,330587 Criatura colorida y bidimensional que habita en superficies planas. Áreas urbanas, en particular muros y vallas publicitarias. Cambia de forma para mimetizarse con el entorno y absorbe pigmentos. ¡Y listo! Esta vez, cada uno de nuestros mejores monstruos tiene una puntuación alta que representa una especie de "media" de las tres características que queremos que tenga nuestro monstruo. Desglosemos la puntuación de cada monstruo en detalle: identificación mirar hábitat comportamiento total Sombra susurrante 0,167859 0,203189 0,005689 0,376738 Luminoth 0,126206 0,098689 0,115189 0,340084 Grafiti viviente 0,091063 0,110944 0,12858 0,330587 Tanto el segundo como el tercer resultado, Luminoth y Living Graffiti, poseen las tres características deseadas. El resultado superior, Whispering Shade, aunque es menos relevante en términos de manipulación de la luz (como se refleja en su puntuación (0,006), tiene características "brillantes" y un entorno oscuro que hacen que su (0,168) y (0,203) tengan puntuaciones muy altas, lo que le otorga la puntuación total más alta (0,377), lo que lo convierte en el monstruo más relevante en general. ¡Qué mejora! behavior look habitat ¿Podemos replicar nuestros resultados? Probemos con otra consulta y averigüémoslo. query = { 'look': 'shapeshifting', 'habitat': 'varied landscapes', 'behavior': 'illusion creation' } identificación puntaje mirar hábitat comportamiento Fantasma de niebla 0,489574 Humanoide etéreo, parecido a la niebla, con rasgos cambiantes. Pantanos, páramos y costas brumosas Atrae a los viajeros por mal camino con ilusiones y susurros. Bailarina del céfiro 0,342075 Graciosa criatura aviar con plumas iridiscentes. Altas cumbres montañosas y llanuras azotadas por el viento Crea exhibiciones aéreas fascinantes para atraer parejas. Sombra susurrante 0,337434 Ser sombrío y amorfo con ojos brillantes. Bosques oscuros y edificios abandonados. Se alimenta del miedo y susurra verdades inquietantes. ¡Genial! Nuestros resultados vuelven a ser excelentes. ¿Qué sucede si queremos encontrar monstruos que sean similares a un monstruo específico de nuestro conjunto de datos? Probémoslo con un monstruo que aún no hemos visto: Harmonic Coral. extraer atributos para este monstruo y crear parámetros de consulta manualmente. Pero Superlinked tiene un método que podemos usar en el objeto de consulta. Debido a que el id de cada monstruo es su nombre, podemos expresar nuestra solicitud de manera tan simple como: Podríamos with_vector app.query( monster_query.with_vector(monster, "Harmonic Coral"), **default_weights, limit=LIMIT ) identificación puntaje mirar hábitat comportamiento Coral armónico 1 Estructura ramificada, similar a un instrumento musical, con zarcillos vibrantes. Mares poco profundos y pozas de marea Crea melodías complejas para comunicar e influir en las emociones. Pulpo de Dreamweaver 0,402288 Cefalópodo con tentáculos que brillan como auroras Fosas oceánicas profundas y cuevas submarinas Influye en los sueños de las criaturas cercanas. Espectro acuático 0,330869 Figura humanoide translúcida hecha de agua fluyendo. Ríos, lagos y zonas costeras Cambia de forma para mezclarse con los cuerpos de agua y controla las corrientes. El resultado principal es el más relevante, el propio Harmonic Coral, como se esperaba. Los otros dos monstruos que recupera nuestra búsqueda son Dreamweaver Octopus y Aqua Wraith. Ambos comparten elementos temáticos ( ) importantes con Harmonic Coral: atributos Hábitats acuáticos ( ) habitat Capacidad de influir o manipular su entorno ( ) behavior Características visuales dinámicas o fluidas ( ) look Ponderación de atributos Supongamos ahora que queremos dar más importancia al atributo . El marco Superlinked nos permite ajustar fácilmente los pesos en el momento de la consulta. Para facilitar la comparación, buscaremos monstruos similares a Harmonic Coral, pero con nuestros pesos ajustados para favorecer . 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 ) identificación puntaje mirar hábitat comportamiento Coral armónico 0,57735 Estructura ramificada, similar a un instrumento musical, con zarcillos vibrantes. Mares poco profundos y pozas de marea Crea melodías complejas para comunicar e influir en las emociones. Elemental de la vid espinosa 0,252593 Criatura parecida a una planta con un cuerpo de enredaderas y espinas retorcidas. Ruinas cubiertas de vegetación y selvas densas Crece rápidamente y controla la vida vegetal circundante. Serpiente de plasma 0,243241 Criatura con forma de serpiente hecha de energía crepitante. Tormentas eléctricas y centrales eléctricas Se alimenta de corrientes eléctricas y puede cortocircuitar la tecnología. Todos nuestros resultados tienen (apropiadamente) apariencias similares: "Rama con zarcillos vibrantes", "Criatura parecida a una planta con un cuerpo de enredaderas y espinas retorcidas", "Parecido a una serpiente". Ahora, hagamos otra búsqueda, ignorando la apariencia y buscando en cambio monstruos que sean similares en términos de y simultáneamente: habitat behavior weights = { "look_weight": 0, "habitat_weight": 1.0, "behavior_weight": 1.0 } identificación puntaje mirar hábitat comportamiento Coral armónico 0,816497 Estructura ramificada, similar a un instrumento musical, con zarcillos vibrantes. Mares poco profundos y pozas de marea Crea melodías complejas para comunicar e influir en las emociones. Pulpo de Dreamweaver 0,357656 Cefalópodo con tentáculos que brillan como auroras Fosas oceánicas profundas y cuevas submarinas Influye en los sueños de las criaturas cercanas. Fantasma de niebla 0,288106 Humanoide etéreo, parecido a la niebla, con rasgos cambiantes. Pantanos, páramos y costas brumosas Atrae a los viajeros por mal camino con ilusiones y susurros. Una vez más, el método Superlinked produce excelentes resultados. Los tres monstruos viven en entornos acuáticos y poseen habilidades de control mental. Por último, intentemos otra búsqueda, ponderando los tres atributos de manera diferente, para encontrar monstruos que, en comparación con el Coral Armónico, se vean algo similares, vivan en hábitats muy diferentes y posean un comportamiento muy similar: weights = { "look_weight": 0.5, "habitat_weight": -1.0, "behavior_weight": 1.0 } identificación puntaje mirar hábitat comportamiento Coral armónico 0,19245 Estructura ramificada, similar a un instrumento musical, con zarcillos vibrantes. Mares poco profundos y pozas de marea Crea melodías complejas para comunicar e influir en las emociones. Luminoth 0,149196 Criatura parecida a una polilla con alas y antenas brillantes. Bosques densos y selvas con flora bioluminiscente Emite patrones de luz relajantes para comunicarse y atraer presas. Bailarina del céfiro 0,136456 Graciosa criatura aviar con plumas iridiscentes. Altas cumbres montañosas y llanuras azotadas por el viento Crea exhibiciones aéreas fascinantes para atraer parejas. ¡Otra vez excelentes resultados! Nuestros otros dos monstruos recuperados, Luminoth y Zephyr Dancer, tienen un comportamiento similar al de Harmonic Coral y viven en hábitats diferentes a los de Harmonic Coral. También se ven muy diferentes a Harmonic Coral. (Si bien los zarcillos de Harmonic Coral y la antena de Luminoth son características algo similares, solo redujimos el en 0,5, y el parecido entre los dos monstruos termina allí). look_weight Veamos cómo se distribuyen las puntuaciones generales de estos monstruos en términos de atributos individuales: identificación mirar hábitat comportamiento total Coral armónico 0,19245 -0,3849 0,3849 0,19245 Luminoth 0,052457 -0,068144 0,164884 0,149196 Bailarina del céfiro 0,050741 -0,079734 0,165449 0,136456 Al ponderar negativamente (-1,0), "rechazamos" deliberadamente a los monstruos con hábitats similares y, en su lugar, sacamos a la superficie monstruos cuyos entornos son diferentes al de Harmonic Coral, como se ve en las puntuaciones negativas de Luminoth y Zephyr Dancer. Las puntuaciones de Luminoth y Zephyr Dancer son relativamente altas, lo que indica su similitud conductual con Harmonic Coral. Sus puntuaciones son positivas, pero más bajas, lo que refleja similitud visual, aunque no extrema, con Harmonic Coral. habitat_weight habitat behavior look cierta En resumen, nuestra estrategia de reducir a -1,0 y a 0,5, pero mantener en 1,0 resulta eficaz para sacar a la superficie monstruos que comparten características de comportamiento clave con Coral Armónico, pero que tienen entornos muy diferentes y tienen un aspecto al menos algo diferente. habitat_weight look_weight behavior_weight Conclusión La búsqueda de vectores de atributos múltiples es un avance significativo en la recuperación de información, ya que ofrece más precisión, comprensión contextual y flexibilidad que la búsqueda básica de similitud semántica. Sin embargo, nuestro enfoque ingenuo (arriba) -almacenar y buscar vectores de atributos por separado, combinar los resultados- es limitado en capacidad, sutileza y eficiencia cuando necesitamos recuperar objetos con múltiples atributos simultáneos. (Además, múltiples búsquedas kNN toman más tiempo que una sola búsqueda con vectores concatenados). y luego Para manejar situaciones como esta, es mejor almacenar todos los vectores de atributos en el mismo almacén de vectores y realizar , ponderando los atributos en el momento de la consulta. El enfoque Superlinked es más preciso, eficiente y escalable que el enfoque ingenuo para cualquier aplicación que requiera una recuperación de vectores de atributos múltiples, rápida, confiable y matizada, ya sea que su caso de uso sea abordar desafíos de datos del mundo real en su sistema de comercio electrónico o de recomendación... o algo completamente diferente, como luchar contra monstruos. una única búsqueda Colaboradores Andrey Pikunov, autor Mór Kapronczay, editor Publicado originalmente . aquí