Guía de migración del almacén de vectores de Python del kernel semántico

Información general

En esta guía se describen las actualizaciones principales del almacén de vectores introducidas en la versión 1.34 del kernel semántico, que representa una revisión significativa de la implementación del almacén de vectores para alinearse con el SDK de .NET y proporcionar una API más unificada e intuitiva. Los cambios consolidan todo en semantic_kernel.data.vector y mejoran la arquitectura del conector.

Resumen de mejoras clave

  • Modelo de campo unificado: VectorStoreField una sola clase reemplaza varios tipos de campo.
  • Incrustaciones integradas: generación de inserción directa en especificaciones de campo vectorial
  • Búsqueda simplificada: creación sencilla de funciones de búsqueda directamente en colecciones
  • Estructura consolidada: todo en semantic_kernel.data.vector y semantic_kernel.connectors
  • Búsqueda de texto mejorada: funcionalidades mejoradas de búsqueda de texto con conectores optimizados
  • Desuso: los antiguos memory_stores están en desuso en favor de la nueva arquitectura de almacén de vectores

1. Representaciones vectoriales integradas y actualizaciones de modelos o campos del almacén de vectores

Hay una serie de cambios en la forma en que define el modelo de almacén de vectores, lo más importante es que ahora se admiten incrustaciones integradas directamente en las definiciones de campo del almacén de vectores. Esto significa que, al especificar un campo para que sea un vector, el contenido de ese campo se incrusta automáticamente mediante el generador de inserción especificado, como el modelo de inserción de texto de OpenAI. Esto simplifica el proceso de creación y administración de campos vectoriales.

Al definir ese campo, debe asegurarse de tres cosas, especialmente cuando se usa un modelo Pydantic:

  1. escritura: es probable que el campo tenga tres tipos, list[float], str o algo más para la entrada en el generador de inserción, y None para cuando el campo no esté configurado.
  2. valor predeterminado: el campo debe tener un valor predeterminado de None u otra cosa, por lo que no hay ningún error al obtener registros de get o search con include_vectors=False los que es el valor predeterminado ahora.

Hay dos preocupaciones aquí, la primera es que al decorar una clase con vectorstoremodel, la primera anotación de tipo del campo se usa para rellenar el type parámetro de la VectorStoreField clase, por lo que debe asegurarse de que la primera anotación de tipo es el tipo correcto para que se cree la colección de almacén de vectores con, a menudo list[float]. De forma predeterminada, los métodos get y search no incluyen vectores en los resultados, por lo que el campo necesita un valor predeterminado y la escritura debe corresponder a eso, por lo que a menudo se permite None y el valor predeterminado se establece en None. Cuando se crea el campo, los valores que deben insertarse se encuentran en este campo, a menudo cadenas, por lo que str también debe incluirse. El motivo de este cambio es permitir más flexibilidad en lo que está incrustado y lo que realmente se almacena en campos de datos, sería una configuración común:

from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
from typing import Annotated
from dataclasses import dataclass

@vectorstoremodel
@dataclass
class MyRecord:
    content: Annotated[str, VectorStoreField('data', is_indexed=True, is_full_text_indexed=True)]
    title: Annotated[str, VectorStoreField('data', is_indexed=True, is_full_text_indexed=True)]
    id: Annotated[str, VectorStoreField('key')]
    vector: Annotated[list[float] | str | None, VectorStoreField(
        'vector', 
        dimensions=1536, 
        distance_function="cosine",
        embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"),
    )] = None

    def __post_init__(self):
        if self.vector is None:
            self.vector = f"Title: {self.title}, Content: {self.content}"

Tenga en cuenta el método post_init, que crea un valor insertado, que es más que un único campo. Los tres tipos también están presentes.

Antes: clases de campo independientes

from semantic_kernel.data import (
    VectorStoreRecordKeyField,
    VectorStoreRecordDataField, 
    VectorStoreRecordVectorField
)

# Old approach with separate field classes
fields = [
    VectorStoreRecordKeyField(name="id"),
    VectorStoreRecordDataField(name="text", is_filterable=True, is_full_text_searchable=True),
    VectorStoreRecordVectorField(name="vector", dimensions=1536, distance_function="cosine")
]

Después: Unified VectorStoreField con incrustaciones integradas

from semantic_kernel.data.vector import VectorStoreField
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbedding

# New unified approach with integrated embeddings
embedding_service = OpenAITextEmbedding(
    ai_model_id="text-embedding-3-small"
)

fields = [
    VectorStoreField(
        "key",
        name="id",
    ),
    VectorStoreField(
        "data",
        name="text",
        is_indexed=True,  # Previously is_filterable
        is_full_text_indexed=True  # Previously is_full_text_searchable
    ),
    VectorStoreField(
        "vector",
        name="vector",
        dimensions=1536,
        distance_function="cosine",
        embedding_generator=embedding_service  # Integrated embedding generation
    )
]

Cambios clave en la definición de campo

  1. Clase de campo único: VectorStoreField reemplaza todos los tipos de campo anteriores.
  2. Especificación de tipo de campo: use field_type: Literal["key", "data", "vector"] el parámetro , puede ser un parámetro posicional, por lo que VectorStoreField("key") es válido.
  3. Propiedades mejoradas:
    • storage_name se ha agregado y, cuando se establece, se usa como nombre de campo en el almacén de vectores, de lo contrario, se usa el parámetro name.
    • dimensions ahora es un parámetro obligatorio para los campos vectoriales.
    • distance_function y index_kind son opcionales y se establecerán en DistanceFunction.DEFAULT y IndexKind.DEFAULT, respectivamente, si no se especifican. Solo para campos vectoriales, cada implementación de almacén de vectores tiene una lógica que elige un valor predeterminado para ese almacén.
  4. Cambia el nombre de la propiedad:
    • property_type type_→ como atributo y type en constructores
    • is_filterableis_indexed
    • is_full_text_searchableis_full_text_indexed
  5. Incrustaciones integradas: agregue embedding_generator directamente a los campos vectoriales, como alternativa, puede establecer el embedding_generator en la propia colección del almacén de vectores, que se usará para todos los campos vectoriales de ese almacén, este valor tiene prioridad sobre el generador de inserción de nivel de colección.

2. Nuevos métodos en tiendas y colecciones

Interfaz de almacén mejorada

from semantic_kernel.connectors.in_memory import InMemoryStore

# Before: Limited collection methods
collection = InMemoryStore.get_collection("my_collection", record_type=MyRecord)

# After: Slimmer collection interface with new methods
collection = InMemoryStore.get_collection(MyRecord)
# if the record type has the `vectorstoremodel` decorator it can contain both the collection_name and the definition for the collection.

# New methods for collection management
await store.collection_exists("my_collection")
await store.ensure_collection_deleted("my_collection")
# both of these methods, create a simple model to streamline doing collection management tasks.
# they both call the underlying `VectorStoreCollection` methods, see below.

Interfaz de colección mejorada

from semantic_kernel.connectors.in_memory import InMemoryCollection

collection = InMemoryCollection(
    record_type=MyRecord,
    embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small")  # Optional, if there is no embedding generator set on the record type
)
# If both the collection and the record type have an embedding generator set, the record type's embedding generator will be used for the collection. If neither is set, it is assumed the vector store itself can create embeddings, or that vectors are included in the records already, if that is not the case, it will likely raise.

# Enhanced collection operations
await collection.collection_exists()
await collection.ensure_collection_exists()
await collection.ensure_collection_deleted()

# CRUD methods
# Removed batch operations, all CRUD operations can now take both a single record or a list of records
records = [
    MyRecord(id="1", text="First record"),
    MyRecord(id="2", text="Second record")
]
ids = ["1", "2"]
# this method adds vectors automatically
await collection.upsert(records)

# You can do get with one or more ids, and it will return a list of records
await collection.get(ids)  # Returns a list of records
# you can also do a get without ids, with top, skip and order_by parameters
await collection.get(top=10, skip=0, order_by='id')
# the order_by parameter can be a string or a dict, with the key being the field name and the value being True for ascending or False for descending order.
# At this time, not all vector stores support this method.

# Delete also allows for single or multiple ids
await collection.delete(ids)

query = "search term"
# New search methods, these use the built-in embedding generator to take the value and create a vector
results = await collection.search(query, top=10)
results = await collection.hybrid_search(query, top=10)

# You can also supply a vector directly
query_vector = [0.1, 0.2, 0.3]  # Example vector
results = await collection.search(vector=query_vector, top=10)
results = await collection.hybrid_search(query, vector=query_vector, top=10)

La nueva implementación del almacén de vectores cambia de objetos FilterClause basados en cadenas a expresiones lambda más eficaces y con tipado seguro o filtros invocables.

Antes: Objetos de FilterClause

from semantic_kernel.data.text_search import SearchFilter, EqualTo, AnyTagsEqualTo
from semantic_kernel.data.vector_search import VectorSearchFilter

# Creating filters using FilterClause objects
text_filter = SearchFilter()
text_filter.equal_to("category", "AI")
text_filter.equal_to("status", "active")

# Vector search filters
vector_filter = VectorSearchFilter()
vector_filter.equal_to("category", "AI")
vector_filter.any_tag_equal_to("tags", "important")

# Using in search
results = await collection.search(
    "query text",
    options=VectorSearchOptions(filter=vector_filter)
)

Después: Filtros de expresiones lambda

# When defining the collection with the generic type hints, most IDE's will be able to infer the type of the record, so you can use the record type directly in the lambda expressions.
collection = InMemoryCollection[str, MyRecord](MyRecord)

# Using lambda expressions for more powerful and type-safe filtering
# The code snippets below work on a data model with more fields then defined earlier.

# Direct lambda expressions
results = await collection.search(
    "query text", 
    filter=lambda record: record.category == "AI" and record.status == "active"
)

# Complex filtering with multiple conditions
results = await collection.search(
    "query text",
    filter=lambda record: (
        record.category == "AI" and 
        record.score > 0.8 and
        "important" in record.tags
    )
)

# Combining conditions with boolean operators
results = await collection.search(
    "query text",
    filter=lambda record: (
        record.category == "AI" or record.category == "ML"
    ) and record.published_date >= datetime(2024, 1, 1)
)

# Range filtering (now possible with lambda expressions)
results = await collection.search(
    "query text",
    filter=lambda record: 0.5 <= record.confidence_score <= 0.9
)

Sugerencias de migración para filtros

  1. Igualdad simple: filter.equal_to("field", "value") se convierte en lambda r: r.field == "value"
  2. Varias condiciones: usa operadores and/or encadenados en lugar de múltiples llamadas a filtros
  3. Contención de etiquetas y matrices: filter.any_tag_equal_to("tags", "value") se convierte en lambda r: "value" in r.tags
  4. Funcionalidades mejoradas: compatibilidad con consultas de rango, lógica booleana compleja y predicados personalizados

4. Mejora de la facilidad de creación de funciones de búsqueda

Antes: Creación de funciones de búsqueda con VectorStoreTextSearch

from semantic_kernel.connectors.in_memory import InMemoryCollection
from semantic_kernel.data import VectorStoreTextSearch

collection = InMemoryCollection(collection_name='collection', record_type=MyRecord)
search = VectorStoreTextSearch.from_vectorized_search(vectorized_search=collection, embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"))

search_function = search.create_search(
    function_name='search',
    ...
)

Después: Creación de la función Direct Search

collection = InMemoryCollection(MyRecord)
# Create search function directly on collection
search_function = collection.create_search_function(
    function_name="search",
    search_type="vector",  # or "keyword_hybrid"
    top=10,
    vector_property_name="vector",  # Name of the vector field
)

# Add to kernel directly
kernel.add_function(plugin_name="memory", function=search_function)

5. Cambio de nombre del conector e importación de cambios

Consolidación de rutas de importación

# Before: Scattered imports
from semantic_kernel.connectors.memory.azure_cognitive_search import AzureCognitiveSearchMemoryStore
from semantic_kernel.connectors.memory.chroma import ChromaMemoryStore
from semantic_kernel.connectors.memory.pinecone import PineconeMemoryStore
from semantic_kernel.connectors.memory.qdrant import QdrantMemoryStore

# After: Consolidated under connectors
from semantic_kernel.connectors.azure_ai_search import AzureAISearchStore
from semantic_kernel.connectors.chroma import ChromaVectorStore
from semantic_kernel.connectors.pinecone import PineconeVectorStore
from semantic_kernel.connectors.qdrant import QdrantVectorStore

# Alternative after: Consolidated with lazy loading:
from semantic_kernel.connectors.memory import (
    AzureAISearchStore,
    ChromaVectorStore,
    PineconeVectorStore,
    QdrantVectorStore,
    WeaviateVectorStore,
    RedisVectorStore
)

Cambio de nombre de clase de conector

Nombre anterior Nombre nuevo
AzureCosmosDBforMongoDB* CosmosMongo*
AzureCosmosDBForNoSQL* CosmosNoSql*

6. Mejoras de búsqueda de texto y eliminación del conector de Bing

Se ha eliminado el Bing Connector y se ha mejorado la interfaz de búsqueda de texto.

Se ha quitado el conector de búsqueda de texto de Bing. Migración a proveedores de búsqueda alternativos:

# Before: Bing Connector (REMOVED)
from semantic_kernel.connectors.search.bing import BingConnector

bing_search = BingConnector(api_key="your-bing-key")

# After: Use Brave Search or other providers
from semantic_kernel.connectors.brave import BraveSearch
# or
from semantic_kernel.connectors.search import BraveSearch

brave_search = BraveSearch()

# Create text search function
text_search_function = brave_search.create_search_function(
    function_name="web_search",
    query_parameter_name="query",
    description="Search the web for information"
)

kernel.add_function(plugin_name="search", function=text_search_function)

Métodos de búsqueda mejorados

Antes: Tres métodos de búsqueda independientes con diferentes tipos de valor devuelto

from semantic_kernel.connectors.brave import BraveSearch
brave_search = BraveSearch()
# Before: Separate search methods
search_results: KernelSearchResult[str] = await brave_search.search(
    query="semantic kernel python",
    top=5,
)

search_results: KernelSearchResult[TextSearchResult] = await brave_search.get_text_search_results(
    query="semantic kernel python",
    top=5,
)

search_results: KernelSearchResult[BraveWebPage] = await brave_search.get_search_results(
    query="semantic kernel python",
    top=5,
)

Después: Método de búsqueda unificado con el parámetro de tipo de salida

from semantic_kernel.data.text_search import SearchOptions
# Enhanced search results with metadata
search_results: KernelSearchResult[str] = await brave_search.search(
    query="semantic kernel python",
    output_type=str, # can also be TextSearchResult or anything else for search engine specific results, default is `str`
    top=5,
    filter=lambda result: result.country == "NL",  # Example filter
)

async for result in search_results.results:
    assert isinstance(result, str)  # or TextSearchResult if using that type
    print(f"Result: {result}")
    print(f"Metadata: {search_results.metadata}")

7. Desuso de almacenes de memoria antiguos

Todos los almacenes de memoria antiguos, basados en MemoryStoreBase se han movido a semantic_kernel.connectors.memory_stores y ahora están marcados como en desuso. La mayoría de ellas tienen una nueva implementación equivalente basada en VectorStore y VectorStoreCollection, que se puede encontrar en semantic_kernel.connectors.memory.

Estos conectores se quitarán completamente:

  • AstraDB
  • Milvus
  • Usearch

Si aún necesita cualquiera de estos, asegúrese de tomar el código del módulo en desuso y de la carpeta semantic_kernel.memory, o implemente su propia colección de almacenes vectoriales basándose en la nueva clase VectorStoreCollection.

Si hay una gran demanda basada en los comentarios de GitHub, consideraremos devolverlos, pero por ahora, no se mantienen y se quitarán en el futuro.

Migración desde SemanticTextMemory

# Before: SemanticTextMemory (DEPRECATED)
from semantic_kernel.memory import SemanticTextMemory
from semantic_kernel.connectors.ai.open_ai import OpenAITextEmbeddingGenerationService

embedding_service = OpenAITextEmbeddingGenerationService(ai_model_id="text-embedding-3-small")
memory = SemanticTextMemory(storage=vector_store, embeddings_generator=embedding_service)

# Store memory
await memory.save_information(collection="docs", text="Important information", id="doc1")

# Search memory  
results = await memory.search(collection="docs", query="important", limit=5)
# After: Direct Vector Store Usage
from semantic_kernel.data.vector import VectorStoreField, vectorstoremodel
from semantic_kernel.connectors.in_memory import InMemoryCollection

# Define data model
@vectorstoremodel
@dataclass
class MemoryRecord:
    id: Annotated[str, VectorStoreField('key')]
    text: Annotated[str, VectorStoreField('data', is_full_text_indexed=True)]
    embedding: Annotated[list[float] | str | None, VectorStoreField('vector', dimensions=1536, distance_function="cosine", embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small"))] = None

# Create vector store with integrated embeddings
collection = InMemoryCollection(
    record_type=MemoryRecord,
    embedding_generator=OpenAITextEmbedding(ai_model_id="text-embedding-3-small")  # Optional, if not set on the record type
)

# Store with automatic embedding generation
record = MemoryRecord(id="doc1", text="Important information", embedding='Important information')
await collection.upsert(record)

# Search with built-in function
search_function = collection.create_search_function(
    function_name="search_docs",
    search_type="vector"
)

Migración del complemento de memoria

Cuando desee tener un complemento que también pueda guardar información, puede crearlo fácilmente de esta manera:

# Before: TextMemoryPlugin (DEPRECATED)
from semantic_kernel.core_plugins import TextMemoryPlugin

memory_plugin = TextMemoryPlugin(memory)
kernel.add_plugin(memory_plugin, "memory")
# After: Custom plugin using vector store search functions
from semantic_kernel.functions import kernel_function

class VectorMemoryPlugin:
    def __init__(self, collection: VectorStoreCollection):
        self.collection = collection
    
    @kernel_function(name="save")
    async def save_memory(self, text: str, key: str) -> str:
        record = MemoryRecord(id=key, text=text, embedding=text)
        await self.collection.upsert(record)
        return f"Saved to {self.collection.collection_name}"
    
    @kernel_function(name="search") 
    async def search_memory(self, query: str, limit: int = 5) -> str:
        results = await self.collection.search(
            query, top=limit, vector_property_name="embedding"
        )        
        return "\n".join([r.record.text async for r in results.results])

# Register the new plugin
memory_plugin = VectorMemoryPlugin(collection)
kernel.add_plugin(memory_plugin, "memory")

Paso 1: Actualizar importaciones

  • [ ] Reemplazar las importaciones del almacén de memoria por equivalentes de almacenamiento de vectores
  • [ ] Actualizar las importaciones de campo para usar VectorStoreField
  • [ ] Eliminar importaciones del conector de Bing

Paso 2: Actualizar definiciones de campo

  • [ ] Convertir en clase unificada VectorStoreField
  • [ ] Actualizar nombres de propiedad (is_filterableis_indexed)
  • [ ] Agregar generadores de inserción integrados a campos vectoriales

Paso 3: Actualizar el uso de la colección

  • [ ] Reemplazar las operaciones de memoria por métodos de almacén de vectores
  • [ ] Usar nuevas operaciones por lotes cuando corresponda
  • [ ] Implementación de la creación de nuevas funciones de búsqueda

Paso 4: Actualización de la implementación de búsqueda

  • [ ] Reemplazar las funciones de búsqueda manual por create_search_function
  • [ ] Actualizar la búsqueda de texto para usar nuevos proveedores
  • [ ] Implementación de la búsqueda híbrida donde es beneficioso
  • [ ] Migrar de FilterClause a lambda expresiones para filtrar

Paso 5: Quitar código en desuso

  • [ ] Eliminar el uso de SemanticTextMemory
  • [ ] Quitar TextMemoryPlugin dependencias

Ventajas de rendimiento y características

Mejoras de rendimiento

  • Operaciones por lotes: los nuevos métodos batch upsert/delete mejoran el rendimiento.
  • Incrustaciones integradas: elimina los pasos de generación de inserción independientes.
  • Búsqueda optimizada: las funciones de búsqueda integradas están optimizadas para cada tipo de almacén

Mejoras de características

  • Búsqueda híbrida: combina vectores y búsqueda de texto para obtener mejores resultados
  • Filtrado avanzado: expresiones de filtro mejoradas e indexación

Experiencia para el desarrollador

  • API simplificada: menos clases y métodos para aprender
  • Interfaz coherente: enfoque unificado en todos los almacenes de vectores
  • Mejor documentación: Borrar ejemplos y rutas de migración
  • Preparado para el futuro: Compatible con el SDK de .NET para un desarrollo multiplataforma coherente

Conclusión

Las actualizaciones del almacén de vectores descritas anteriormente representan una mejora significativa en el SDK de Python para kernel semántico. La nueva arquitectura unificada proporciona un mejor rendimiento, características mejoradas y una experiencia de desarrollador más intuitiva. Aunque la migración requiere actualizar las importaciones y refactorizar el código existente, las ventajas en el mantenimiento y la funcionalidad hacen que esta actualización sea muy recomendable.

Para obtener ayuda adicional con la migración, consulte los ejemplos actualizados en el samples/concepts/memory/ directorio y la documentación completa de la API.